Spaces:
Sleeping
Sleeping
| """Prétraitement image : détection de l'écran LCD puis découpage en lignes | |
| de valeurs (prix / volume / prix du litre), avant reconnaissance CRNN. | |
| Cette étape reste indépendante du modèle de reconnaissance : elle correspond | |
| au travail de prétraitement/détection d'écran attendu par le cahier des | |
| charges (§6.1, §8.2/8.3), et reste identique quel que soit le moteur de | |
| lecture des chiffres utilisé en aval. | |
| """ | |
| import cv2 | |
| import numpy as np | |
| from typing import List, Tuple | |
| FIELD_NAMES = ["prix", "volume", "prix_litre"] | |
| def detect_screen_region(img: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int, int, int]]: | |
| """Tente de localiser la zone de l'écran LCD dans l'image. | |
| Retourne (crop, (x, y, w, h)) ou (img, (0,0,W,H)) si rien de convaincant | |
| n'a été trouvé (l'appelant doit alors traiter l'image entière). | |
| """ | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img.copy() | |
| blurred = cv2.GaussianBlur(gray, (5, 5), 0) | |
| edges = cv2.Canny(blurred, 30, 100) | |
| kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5)) | |
| closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) | |
| contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, | |
| cv2.CHAIN_APPROX_SIMPLE) | |
| h_img, w_img = img.shape[:2] | |
| best_rect, best_area = None, 0 | |
| for cnt in contours: | |
| x, y, w, h = cv2.boundingRect(cnt) | |
| area = w * h | |
| if area > 0.02 * h_img * w_img and 1.0 < (w / max(h, 1)) < 8.0: | |
| if area > best_area: | |
| best_area = area | |
| best_rect = (x, y, w, h) | |
| if best_rect: | |
| x, y, w, h = best_rect | |
| pad = 10 | |
| x1 = max(0, x - pad); y1 = max(0, y - pad) | |
| x2 = min(w_img, x + w + pad); y2 = min(h_img, y + h + pad) | |
| return img[y1:y2, x1:x2], (x1, y1, x2 - x1, y2 - y1) | |
| return img, (0, 0, w_img, h_img) | |
| def split_lcd_lines(lcd_crop: np.ndarray, | |
| n_lines: int = 3) -> Tuple[List[np.ndarray], List[Tuple[int, int]]]: | |
| """Découpe le crop d'écran en `n_lines` bandes horizontales (une par | |
| valeur affichée : prix, volume, prix du litre), par projection | |
| horizontale des pixels sombres, avec repli sur un découpage proportionnel | |
| si la projection ne trouve pas exactement le bon nombre de bandes. | |
| NB : `n_lines=3` par défaut (et non 2) car la majorité des écrans du jeu | |
| d'annotation affichent les trois valeurs (prix, volume, prix/litre) — | |
| un défaut à 2 aurait systématiquement ignoré la troisième ligne. | |
| """ | |
| gray = cv2.cvtColor(lcd_crop, cv2.COLOR_BGR2GRAY) | |
| h, w = gray.shape | |
| clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) | |
| enh = clahe.apply(gray) | |
| _, bin_img = cv2.threshold(enh, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) | |
| if np.mean(bin_img) > 128: | |
| bin_img = cv2.bitwise_not(bin_img) | |
| row_sums = np.sum(bin_img == 255, axis=1).astype(np.float32) | |
| smoothed = cv2.GaussianBlur( | |
| row_sums.reshape(-1, 1), (1, max(3, h // 30) * 2 + 1), 0 | |
| ).flatten() | |
| threshold = max(smoothed.max() * 0.08, 2) | |
| active = smoothed > threshold | |
| bands: List[Tuple[int, int]] = [] | |
| in_band = False | |
| start = 0 | |
| for y in range(h): | |
| if active[y] and not in_band: | |
| start, in_band = y, True | |
| elif not active[y] and in_band: | |
| bands.append((start, y)) | |
| in_band = False | |
| if in_band: | |
| bands.append((start, h)) | |
| min_h = h * 0.06 | |
| bands = [(s, e) for s, e in bands if e - s >= min_h] | |
| merged: List[Tuple[int, int]] = [] | |
| for band in bands: | |
| if merged and band[0] - merged[-1][1] < h * 0.05: | |
| merged[-1] = (merged[-1][0], band[1]) | |
| else: | |
| merged.append(band) | |
| bands = [tuple(band) for band in merged] | |
| if len(bands) != n_lines: | |
| band_h = h / n_lines | |
| bands = [(int(i * band_h), int((i + 1) * band_h)) for i in range(n_lines)] | |
| crops: List[np.ndarray] = [] | |
| normalized_bands: List[Tuple[int, int]] = [] | |
| for start_y, end_y in bands: | |
| pad = max(2, int(h * 0.02)) | |
| y0 = max(0, start_y - pad) | |
| y1 = min(h, end_y + pad) | |
| crops.append(lcd_crop[y0:y1, :]) | |
| normalized_bands.append((y0, y1 - y0)) | |
| return crops, normalized_bands | |