From d288daecd1a434af5a77bb8d51cb7ad5e2e8337d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Tue, 10 Mar 2026 15:04:50 +0100 Subject: [PATCH] Make reading grouped annotations faster. --- add_final_score.py | 2 +- reading_grouped_annotations.py | 48 ++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/add_final_score.py b/add_final_score.py index 9319775..7d1bc57 100644 --- a/add_final_score.py +++ b/add_final_score.py @@ -42,7 +42,7 @@ def process_images(base_dir): print(f"Error: Directory '{search_path}' not found.") sys.exit(1) - for img_path in search_path.glob("*/*.jpg"): + for img_path in sorted(search_path.glob("*/*.jpg")): student_name = img_path.stem # Filename without extension # 4. Find Score diff --git a/reading_grouped_annotations.py b/reading_grouped_annotations.py index b74260c..dab07d2 100644 --- a/reading_grouped_annotations.py +++ b/reading_grouped_annotations.py @@ -2,6 +2,7 @@ import sys import os import json import collections +import concurrent.futures from pathlib import Path from PIL import Image @@ -14,7 +15,9 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la Modifies data based on actions, pastes label-specific note crops, regenerates label images for consistency, saves dirty ones, and generates Concat.jpg in the BGnot/Copie{id} directory. + Returns a string of accumulated log messages. """ + logs = [f"\nProcessing compilation for: Copie{student_id}"] output_dir = os.path.join(root_dir, "BGnot", f"Copie{student_id}") os.makedirs(output_dir, exist_ok=True) @@ -44,23 +47,23 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la if act['type'] == 'score': result['score'] = act['value'] dirty_labels.add(label) - print(f" > Updated score for {label} to {act['value']}") + logs.append(f" > Updated score for {label} to {act['value']}") elif act['type'] == 'del_global': if act['index'] < len(global_fb): global_fb[act['index']]["to_delete"] = True dirty_labels.add(label) - print(f" > Deleted global feedback in {label}") + logs.append(f" > Deleted global feedback in {label}") elif act['type'] in ('del_local', 'del_local_rect'): if act['index'] < len(local_fb): target = local_fb[act['index']] if act['type'] == 'del_local': target["to_delete"] = True - print(f" > Deleted local feedback in {label}") + logs.append(f" > Deleted local feedback in {label}") else: target["norectangle"] = True - print(f" > Deleted rect in {label}") + logs.append(f" > Deleted rect in {label}") dirty_labels.add(label) # --- 2. Process Images (Regenerate & Concatenate) --- @@ -113,7 +116,7 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la if (label in dirty_labels) or has_notes: save_path = os.path.join(output_dir, f"{label}.jpg") final_img.save(save_path) - print(f" Saved dirty image: {label}.jpg") + logs.append(f" Saved dirty image: {label}.jpg") concat_list.append(final_img) @@ -126,11 +129,10 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la if not perfect_no_comment: concat_list_F.append(final_img) - # --- 3. Save Final Outputs --- with open(score_path, "w") as f: json.dump(d_notes, f, indent=4) - print(f" Saved {score_path}") + logs.append(f" Saved {score_path}") if concat_list: max_w = max(i.width for i in concat_list) @@ -143,7 +145,8 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la y += img.height full_img.save(os.path.join(output_dir, "Concat.jpg")) - print(f" Saved regenerated Concat.jpg") + logs.append(f" Saved regenerated Concat.jpg") + if concat_list_F: max_w = max(i.width for i in concat_list_F) total_h = sum(i.height for i in concat_list_F) @@ -155,7 +158,9 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la y += img.height full_img.save(os.path.join(output_dir, "Concat_F.jpg")) - print(f" Saved regenerated Concat_F.jpg") + logs.append(f" Saved regenerated Concat_F.jpg") + + return "\n".join(logs) if __name__ == "__main__": @@ -223,20 +228,10 @@ if __name__ == "__main__": 'old_header_h': img_info.get("header_height", 0) } - # --- 2. Dispatch data back to students and regenerate --- - # affected_students = set(actions_by_student.keys()).union(set(notes_by_student.keys())) - - # if not affected_students: - # print("\nNo changes detected in any grouped annotations.") - # sys.exit(0) - - # for sid in sorted(affected_students, key=natural_key): - for sid in sorted(original_data.keys(), key=natural_key): + def process_student(sid): if sid not in original_data: - continue - - print(f"\nProcessing compilation for: Copie{sid}") - apply_actions_and_regenerate_grouped( + return "" + return apply_actions_and_regenerate_grouped( root_dir, original_data, sid, @@ -244,3 +239,12 @@ if __name__ == "__main__": notes_by_student[sid], all_labels ) + + # --- 2. Process each student concurrently using 4 threads --- + sids = sorted(original_data.keys(), key=natural_key) + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + futures = {executor.submit(process_student, sid): sid for sid in sids} + for future in concurrent.futures.as_completed(futures): + output = future.result() + if output: + print(output)