File size: 11,466 Bytes
b27cd24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
import os
import json

def read_result_file(path):
    """
    Reads your result file where each record is:

    <filename>
    ```json
    { ... }
    ```

    Returns a dict mapping filename -> parsed JSON dict.
    """
    results = {}
    with open(path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    i = 0
    while i < len(lines):
        line = lines[i].strip()
        if not line:
            i += 1
            continue

        # line should be filename like IMG_5954.JPG
        if line.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")):
            filename = line
            i += 1
            # skip ```json
            if i < len(lines) and lines[i].strip().startswith("```"):
                i += 1
            json_lines = []
            while i < len(lines) and not lines[i].strip().startswith("```"):
                json_lines.append(lines[i])
                i += 1
            # skip closing ```
            if i < len(lines) and lines[i].strip().startswith("```"):
                i += 1
            try:
                obj = json.loads("".join(json_lines))
                results[filename[:-4]] = obj
            except Exception as e:
                print(f"[warn] failed to parse {filename}: {e}")
        else:
            i += 1
    return results

def load_all_json_recursive_with_paths(root_folder):
    """
    Recursively reads all .json files and returns:
      - gt_dict:  name(no ext) -> parsed JSON dict
      - gt_paths: name(no ext) -> absolute file path
    """
    gt_dict, gt_paths = {}, {}
    for dirpath, _, filenames in os.walk(root_folder):
        for fname in filenames:
            if fname.lower().endswith(".json"):
                key = os.path.splitext(fname)[0]  # strip extension
                fpath = os.path.join(dirpath, fname)
                try:
                    with open(fpath, "r", encoding="utf-8") as f:
                        gt_dict[key] = json.load(f)
                        gt_paths[key] = os.path.abspath(fpath)
                except Exception as e:
                    print(f"[warn] Failed to load {fpath}: {e}")
    return gt_dict, gt_paths

def bool_to_yesno(x):
    if isinstance(x, bool):
        return "yes" if x else "no"
    # also accept strings like "true"/"false"
    s = str(x).strip().lower()
    if s in {"true", "yes", "1"}:
        return "yes"
    if s in {"false", "no", "0"}:
        return "no"
    return s  # fallback

FLAT_KEYS = [
    "number_of_turns",
    "line_shape",
    "line_facing_direction",
    "number_of_people_in_line",
    "line_purpose",
    "start_person_description",
    "end_person_description",
    "counter_person_description",
    "boundary_present",
    "boundary_types",
    "end_of_line_visible",
    "end_of_line_location_if_visible",
    "direction_to_turn_to_see_end_if_not_visible",
    "start_of_line_visible",
    "start_of_line_location_if_visible",
    "direction_to_turn_to_see_start_if_not_visible",
    "line_completeness",
]

def normalize_gt_to_flat(gt: dict) -> dict:
    """
    Convert your GT (nested) structure to the flat schema used by GPT results.
    Any missing fields are filled with sensible 'N/A' / 'unknown' defaults.
    """
    # safe gets
    ls = gt.get("line_shape", {})
    ppl = gt.get("people", {})
    bnd = gt.get("boundary", {})
    sol = gt.get("start_of_line", {})
    eol = gt.get("end_of_line", {})

    flat = {
        "number_of_turns":            int(ls.get("turns")) if isinstance(ls.get("turns"), (int, float, str)) and str(ls.get("turns")).isdigit() else int(ls.get("turns") or 0),
        "line_shape":                 ls.get("description", "unknown"),

        "line_facing_direction":      ppl.get("direction_they_are_facing", "unknown"),
        "number_of_people_in_line":   int(ppl.get("number_of_people") or 0),

        "line_purpose":               gt.get("line_purpose", "unknown"),
        "start_person_description":   ppl.get("start_person_description", "unknown"),
        "end_person_description":     ppl.get("end_person_description", "unknown"),
        "counter_person_description": ppl.get("counter_person_description", "unknown"),

        "boundary_present":           bool_to_yesno(bnd.get("boundary_present", "no")),
        "boundary_types":             bnd.get("boundary_types", "none"),

        "end_of_line_visible":                    eol.get("visible", "no"),
        "end_of_line_location_if_visible":        eol.get("location_if_visible", "N/A"),
        "direction_to_turn_to_see_end_if_not_visible": eol.get("direction_to_turn_if_not_visible", "N/A"),

        "start_of_line_visible":                  sol.get("visible", "no"),
        "start_of_line_location_if_visible":      sol.get("location_if_visible", "N/A"),
        "direction_to_turn_to_see_start_if_not_visible": sol.get("direction_to_turn_if_not_visible", "N/A"),

        "line_completeness":          gt.get("line_completeness", "partial"),
    }

    # Ensure every expected key is present
    for k in FLAT_KEYS:
        flat.setdefault(k, "N/A")

    return flat

# ---------- New helper: write image paths for GT JSON files ----------
def _find_image_path_in_gt_obj(gt_obj):
    """
    Try to find an image path stored inside a GT JSON object.
    Searches common key names, recursively.
    """
    candidate_keys = {
        "image_path", "img_path", "path", "file_path", "filename", "file_name",
        "image", "img", "source_image", "source_path"
    }
    valid_exts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")

    def _search(obj):
        if isinstance(obj, dict):
            for k, v in obj.items():
                if k in candidate_keys and isinstance(v, str) and v.lower().endswith(valid_exts):
                    return os.path.abspath(v)
            for v in obj.values():
                out = _search(v)
                if out:
                    return out
        elif isinstance(obj, list):
            for v in obj:
                out = _search(v)
                if out:
                    return out
        return None

    return _search(gt_obj)

def write_image_paths_with_gt_json(gt_dict, output_txt, image_root=None):
    """
    Write one image path per line for every GT JSON entry.

    Priority:
      1) use image path stored inside the GT JSON, if present
      2) if image_root is provided, find a file with the same basename under image_root
      3) otherwise write the basename only and warn
    """
    valid_exts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")
    unresolved = []

    with open(output_txt, "w", encoding="utf-8") as f:
        for key in sorted(gt_dict.keys()):
            gt_obj = gt_dict[key]
            image_path = _find_image_path_in_gt_obj(gt_obj)

            if image_path is None and image_root is not None:
                found = None
                for dirpath, _, filenames in os.walk(image_root):
                    for fname in filenames:
                        stem, ext = os.path.splitext(fname)
                        if stem == key and ext.lower() in valid_exts:
                            found = os.path.abspath(os.path.join(dirpath, fname))
                            break
                    if found is not None:
                        break
                image_path = found

            if image_path is None:
                unresolved.append(key)
                f.write(f"{key}\n")
            else:
                f.write(f"{image_path}\n")

    print(f"Wrote image paths for {len(gt_dict)} GT JSON files to: {os.path.abspath(output_txt)}")
    if unresolved:
        print(f"[warn] Could not resolve full image path for {len(unresolved)} entries; wrote basename only.")
        for k in unresolved[:20]:
            print(f"  unresolved: {k}")
        if len(unresolved) > 20:
            print("  ...")

# ---------- Evaluation ----------
def evaluate_accuracy_with_mismatches(results: dict, gt_dict: dict, gt_paths: dict):
    """
    results: { image_name(no ext or with ext) : GPT flat dict }
    gt_dict: { image_name(no ext)            : GT nested dict }
    gt_paths:{ image_name(no ext)            : path to GT json }

    Returns:
      - per-key accuracy dict
      - overall accuracy
      - mismatches: dict[field] = list of (image_name, gpt_value, gt_value, gt_path)
    """
    # Normalize result keys to match GT keys (strip extension if needed)
    def norm_key(k):
        return os.path.splitext(k)[0]

    correct_counts = {k: 0 for k in FLAT_KEYS}
    total_counts   = {k: 0 for k in FLAT_KEYS}
    mismatches     = {k: [] for k in FLAT_KEYS}

    common = set(map(norm_key, results.keys())) & set(gt_dict.keys())

    for img_norm in sorted(common):
        # get original result key (might include extension)
        # pick the first key in results whose basename (no ext) matches
        rkey = next(k for k in results.keys() if norm_key(k) == img_norm)
        gpt_res = results[rkey]
        gt_res  = normalize_gt_to_flat(gt_dict[img_norm])

        for k in FLAT_KEYS:
            total_counts[k] += 1
            gv = str(gpt_res.get(k)).strip().lower()
            tv = str(gt_res.get(k)).strip().lower()
            if gv == tv:
                correct_counts[k] += 1
            else:
                mismatches[k].append((rkey, gpt_res.get(k), gt_res.get(k), gt_paths.get(img_norm, "N/A")))

    accuracies = {k: (correct_counts[k] / total_counts[k] if total_counts[k] else None) for k in FLAT_KEYS}
    overall = sum(correct_counts.values()) / sum(total_counts.values()) if sum(total_counts.values()) else None
    return accuracies, overall, mismatches

# ---------- Pretty print the three requested mismatch lists ----------
def print_specific_mismatches(mismatches):
    fields = [
        "line_facing_direction",
        "end_of_line_visible",
        "start_of_line_visible",
    ]
    for f in fields:
        print(f"\nIncorrect {f}:")
        if not mismatches.get(f):
            print("  (none)")
            continue
        for img_name, gpt_v, gt_v, gt_path in mismatches[f]:
            print(f"  {img_name} | GPT={gpt_v!r} | GT={gt_v!r} | GT JSON: {gt_path}")

if __name__ == "__main__":
    # Example usage
    results = read_result_file("gpt_line_test.jsonl")
    print(f"Loaded {len(results)} records")

    folder = "/scratch/ds5725/linefinder/LineFinder/GT_json"
    gt_dict, gt_paths = load_all_json_recursive_with_paths(folder)
    print(f"Loaded {len(gt_dict)} JSON files")

    # NEW: write all image paths with GT JSON to a txt file.
    # If your GT JSON already stores image paths, image_root is not needed.
    # Otherwise set image_root to your image folder and the script will try to
    # match by basename, e.g. IMG_0001.json -> IMG_0001.jpg/png/...
    write_image_paths_with_gt_json(
        gt_dict,
        output_txt="images_with_gt_json.txt",
        image_root=None,  # e.g. "/scratch/ds5725/linefinder/LineFinder/Images"
    )
    exit()
    # Optional: quick diff to see mismatches
    def diff_records(a: dict, b: dict):
        diffs = {}
        for k in FLAT_KEYS:
            if a.get(k) != b.get(k):
                diffs[k] = (a.get(k), b.get(k))
        return diffs

    acc, overall, mismatches = evaluate_accuracy_with_mismatches(results, gt_dict, gt_paths)

    print("Per-key accuracy:")
    for k, v in acc.items():
        print(f"{k:40s}: {v:.2%}")
    print(f"Overall accuracy: {overall:.2%}")
    print_specific_mismatches(mismatches)