Make reading grouped annotations faster.

master
Sébastien Miquel 2026-03-10 15:04:50 +01:00
parent a658cb72e0
commit d288daecd1
2 changed files with 27 additions and 23 deletions

View File

@ -42,7 +42,7 @@ def process_images(base_dir):
print(f"Error: Directory '{search_path}' not found.") print(f"Error: Directory '{search_path}' not found.")
sys.exit(1) 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 student_name = img_path.stem # Filename without extension
# 4. Find Score # 4. Find Score

View File

@ -2,6 +2,7 @@ import sys
import os import os
import json import json
import collections import collections
import concurrent.futures
from pathlib import Path from pathlib import Path
from PIL import Image 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, Modifies data based on actions, pastes label-specific note crops,
regenerates label images for consistency, saves dirty ones, regenerates label images for consistency, saves dirty ones,
and generates Concat.jpg in the BGnot/Copie{id} directory. 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}") output_dir = os.path.join(root_dir, "BGnot", f"Copie{student_id}")
os.makedirs(output_dir, exist_ok=True) 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': if act['type'] == 'score':
result['score'] = act['value'] result['score'] = act['value']
dirty_labels.add(label) 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': elif act['type'] == 'del_global':
if act['index'] < len(global_fb): if act['index'] < len(global_fb):
global_fb[act['index']]["to_delete"] = True global_fb[act['index']]["to_delete"] = True
dirty_labels.add(label) 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'): elif act['type'] in ('del_local', 'del_local_rect'):
if act['index'] < len(local_fb): if act['index'] < len(local_fb):
target = local_fb[act['index']] target = local_fb[act['index']]
if act['type'] == 'del_local': if act['type'] == 'del_local':
target["to_delete"] = True target["to_delete"] = True
print(f" > Deleted local feedback in {label}") logs.append(f" > Deleted local feedback in {label}")
else: else:
target["norectangle"] = True target["norectangle"] = True
print(f" > Deleted rect in {label}") logs.append(f" > Deleted rect in {label}")
dirty_labels.add(label) dirty_labels.add(label)
# --- 2. Process Images (Regenerate & Concatenate) --- # --- 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: if (label in dirty_labels) or has_notes:
save_path = os.path.join(output_dir, f"{label}.jpg") save_path = os.path.join(output_dir, f"{label}.jpg")
final_img.save(save_path) 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) 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: if not perfect_no_comment:
concat_list_F.append(final_img) concat_list_F.append(final_img)
# --- 3. Save Final Outputs --- # --- 3. Save Final Outputs ---
with open(score_path, "w") as f: with open(score_path, "w") as f:
json.dump(d_notes, f, indent=4) json.dump(d_notes, f, indent=4)
print(f" Saved {score_path}") logs.append(f" Saved {score_path}")
if concat_list: if concat_list:
max_w = max(i.width for i in 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 y += img.height
full_img.save(os.path.join(output_dir, "Concat.jpg")) 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: if concat_list_F:
max_w = max(i.width for i in 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) 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 y += img.height
full_img.save(os.path.join(output_dir, "Concat_F.jpg")) 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__": if __name__ == "__main__":
@ -223,20 +228,10 @@ if __name__ == "__main__":
'old_header_h': img_info.get("header_height", 0) 'old_header_h': img_info.get("header_height", 0)
} }
# --- 2. Dispatch data back to students and regenerate --- def process_student(sid):
# 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):
if sid not in original_data: if sid not in original_data:
continue return ""
return apply_actions_and_regenerate_grouped(
print(f"\nProcessing compilation for: Copie{sid}")
apply_actions_and_regenerate_grouped(
root_dir, root_dir,
original_data, original_data,
sid, sid,
@ -244,3 +239,12 @@ if __name__ == "__main__":
notes_by_student[sid], notes_by_student[sid],
all_labels 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)