File size: 4,290 Bytes
b510add
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""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