File size: 4,262 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
111
112
113
114
115
116
117
118
"""Vérifie si une image a déjà été utilisée pour l'entraînement/validation
du CRNN (annotator/annotations/annotations.json), afin de choisir des photos
réellement inédites pour les tests de démonstration.

Compare par hash perceptuel (pHash) pour détecter aussi les quasi-doublons
(même écran re-photographié, recadré, recompressé) — pas seulement les noms
de fichiers identiques.

Usage :
    python tools/check_seen_image.py chemin/vers/photo.jpg [autre.jpg ...]
"""
import argparse
import json
import os
import sys

import numpy as np
from PIL import Image

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ANNOTATIONS_FILE = os.path.join(REPO_ROOT, "annotator", "annotations", "annotations.json")

HASH_SIZE = 16
SIMILARITY_THRESHOLD = 10  # distance de Hamming ; en dessous = quasi-identique


def phash(image_path: str, hash_size: int = HASH_SIZE) -> np.ndarray:
    img = Image.open(image_path).convert("L").resize(
        (hash_size + 1, hash_size), Image.LANCZOS
    )
    pixels = np.asarray(img, dtype=np.float32)
    diff = pixels[:, 1:] > pixels[:, :-1]
    return diff.flatten()


def hamming_distance(a: np.ndarray, b: np.ndarray) -> int:
    return int(np.count_nonzero(a != b))


def _index_repo_filenames() -> dict:
    """Indexe une fois tous les fichiers image du dépôt par nom de fichier,
    pour retrouver les sources annotées même quand `image_path` ne pointe
    plus vers un chemin valide (dossiers déplacés/renommés)."""
    exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tiff"}
    index: dict = {}
    skip_dirs = {".git", ".env", "__pycache__", "node_modules", "dataset_lines",
                "dataset_doctr", "crops", "outputs", "outputs_test", "photos"}
    for root, dirs, files in os.walk(REPO_ROOT):
        dirs[:] = [d for d in dirs if d not in skip_dirs]
        for f in files:
            if os.path.splitext(f)[1].lower() in exts:
                index.setdefault(f, os.path.join(root, f))
    return index


def build_reference_hashes() -> dict:
    """Calcule les hash de toutes les images sources annotées (introuvables
    localement sont ignorées silencieusement)."""
    if not os.path.exists(ANNOTATIONS_FILE):
        raise FileNotFoundError(f"Introuvable : {ANNOTATIONS_FILE}")

    ann = json.load(open(ANNOTATIONS_FILE, encoding="utf-8"))
    filename_index = _index_repo_filenames()
    ann_dir = os.path.dirname(ANNOTATIONS_FILE)

    refs = {}
    for name, entry in ann.items():
        rel_path = entry.get("image_path", "")
        candidates = [
            os.path.normpath(os.path.join(ann_dir, rel_path)) if rel_path else None,
            os.path.normpath(os.path.join(REPO_ROOT, "images", name)),
            filename_index.get(name),
        ]
        for c in candidates:
            if c and os.path.exists(c):
                try:
                    refs[name] = phash(c)
                except Exception:
                    pass
                break
    return refs


def check_image(path: str, refs: dict) -> None:
    try:
        h = phash(path)
    except Exception as e:
        print(f"{path} : impossible de lire l'image ({e})")
        return

    best_name, best_dist = None, None
    for name, ref_hash in refs.items():
        d = hamming_distance(h, ref_hash)
        if best_dist is None or d < best_dist:
            best_dist, best_name = d, name

    if best_dist is not None and best_dist <= SIMILARITY_THRESHOLD:
        print(f"{os.path.basename(path)} : DEJA VU (proche de '{best_name}', "
              f"distance={best_dist}/{len(h)}) -> ne pas utiliser pour tester la generalisation.")
    else:
        dist_info = f"(plus proche : '{best_name}', distance={best_dist})" if best_name else ""
        print(f"{os.path.basename(path)} : INEDITE {dist_info} -> bon candidat pour le test.")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("images", nargs="+", help="Chemins des images à vérifier")
    args = parser.parse_args()

    refs = build_reference_hashes()
    print(f"{len(refs)} images de référence indexées (annotations.json).\n")
    for path in args.images:
        check_image(path, refs)


if __name__ == "__main__":
    main()