WIP : reading checks
parent
7ea5cbc1e5
commit
c94c773c10
|
|
@ -0,0 +1,258 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import concurrent.futures
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# Fix for Matplotlib in threads: Set backend to non-interactive 'Agg'
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import annotating
|
||||||
|
from annotating import MARGIN_LEFT, ANNOT_WIDTH
|
||||||
|
|
||||||
|
# Global lock for Matplotlib/Latex rendering to prevent race conditions
|
||||||
|
LATEX_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
BOX_SIZE = 30
|
||||||
|
SCORE_BOX_SIZE = 30
|
||||||
|
SCORES = [x * 0.5 for x in range(9)] # 0.0 to 4.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
CHECKBOX_FONT = ImageFont.truetype("DejaVuSans.ttf", 20)
|
||||||
|
except IOError:
|
||||||
|
try:
|
||||||
|
CHECKBOX_FONT = ImageFont.truetype("arial.ttf", 20)
|
||||||
|
except IOError:
|
||||||
|
CHECKBOX_FONT = ImageFont.load_default()
|
||||||
|
|
||||||
|
def draw_checkbox(draw, x, y, size=BOX_SIZE, label=None, fill="white"):
|
||||||
|
if label:
|
||||||
|
draw.text((x - BOX_SIZE-5, y + 2), str(label), fill="black", font=CHECKBOX_FONT)
|
||||||
|
draw.rectangle([x, y, x + size, y + size], fill=fill, outline="black", width=2)
|
||||||
|
|
||||||
|
return [x, y, x + size, y + size]
|
||||||
|
|
||||||
|
def safe_render_latex(text, **kwargs):
|
||||||
|
"""Thread-safe wrapper for latex rendering."""
|
||||||
|
with LATEX_LOCK:
|
||||||
|
return annotating.render_latex_text(text, **kwargs)
|
||||||
|
|
||||||
|
def create_base_image(pdf_path):
|
||||||
|
"""Converts PDF pages to a single vertically stacked image."""
|
||||||
|
try:
|
||||||
|
pages = annotating.convert_from_path(pdf_path)
|
||||||
|
total_h = sum(page.height for page in pages)
|
||||||
|
max_w = max(page.width for page in pages)
|
||||||
|
base_img = Image.new("RGBA", (max_w, total_h), "white")
|
||||||
|
cy = 0
|
||||||
|
for page in pages:
|
||||||
|
base_img.paste(page.convert("RGBA"), (0, cy))
|
||||||
|
cy += page.height
|
||||||
|
return base_img
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def render_header(label, score, feedbacks, base_width):
|
||||||
|
"""Generates the score line and global feedback elements."""
|
||||||
|
elements = []
|
||||||
|
|
||||||
|
# Score Line
|
||||||
|
score_text_img = safe_render_latex(f"{label} ; Note : {score}", width_px=base_width // 2, fontsize=18)
|
||||||
|
score_line_h = max(score_text_img.height, SCORE_BOX_SIZE + 10)
|
||||||
|
score_line_img = Image.new("RGBA", (base_width, score_line_h), (255, 255, 255, 0))
|
||||||
|
score_line_img.paste(score_text_img, (0, 0))
|
||||||
|
|
||||||
|
draw_score = ImageDraw.Draw(score_line_img)
|
||||||
|
start_x = score_text_img.width + 20
|
||||||
|
local_boxes = []
|
||||||
|
|
||||||
|
for val in SCORES:
|
||||||
|
box = draw_checkbox(draw_score, start_x, 5, SCORE_BOX_SIZE, str(val))
|
||||||
|
local_boxes.append({
|
||||||
|
"type": "score", "label": label, "value": val,
|
||||||
|
"rel_box": box, "elem_y": 0
|
||||||
|
})
|
||||||
|
start_x += SCORE_BOX_SIZE + 60
|
||||||
|
elements.append((score_line_img, local_boxes))
|
||||||
|
|
||||||
|
# Global Feedback
|
||||||
|
for i, fb in enumerate(feedbacks):
|
||||||
|
fb_img = safe_render_latex(fb['text'], width_px=base_width)
|
||||||
|
draw_fb = ImageDraw.Draw(fb_img)
|
||||||
|
bx = fb_img.width - BOX_SIZE - 5
|
||||||
|
by = 5
|
||||||
|
box = draw_checkbox(draw_fb, bx, by, BOX_SIZE)
|
||||||
|
|
||||||
|
elements.append((fb_img, [{
|
||||||
|
"type": "del_global", "label": label, "index": i,
|
||||||
|
"text_preview": fb['text'][:20], "rel_box": box, "elem_y": 0
|
||||||
|
}]))
|
||||||
|
|
||||||
|
return elements
|
||||||
|
|
||||||
|
def process_label(root_dir, student_id, label, content):
|
||||||
|
"""Processes a single label (PDF) -> Annotated Image + Checkboxes."""
|
||||||
|
copie_folder = f"Copie{student_id}"
|
||||||
|
pdf_path = os.path.join(root_dir, copie_folder, f"{label}.pdf")
|
||||||
|
|
||||||
|
if not os.path.exists(pdf_path):
|
||||||
|
return None, []
|
||||||
|
|
||||||
|
base_img = create_base_image(pdf_path)
|
||||||
|
if not base_img:
|
||||||
|
return None, []
|
||||||
|
|
||||||
|
# Extract Data
|
||||||
|
coordinates = content.get('coordinates', (0, 0))
|
||||||
|
hmin = coordinates[0]
|
||||||
|
result = content.get('result', {})
|
||||||
|
score = result.get('score', 0)
|
||||||
|
feedbacks = result.get('feedback', [])
|
||||||
|
|
||||||
|
global_fb = [f for f in feedbacks if not f.get('box_2d')]
|
||||||
|
local_fb = [f for f in feedbacks if f.get('box_2d')]
|
||||||
|
local_fb.sort(key=lambda x: x['box_2d'][0])
|
||||||
|
|
||||||
|
checkbox_map = []
|
||||||
|
|
||||||
|
# 1. Render Header
|
||||||
|
header_elements = render_header(label, score, global_fb, base_img.width)
|
||||||
|
header_height = sum(x[0].height for x in header_elements)
|
||||||
|
|
||||||
|
# 2. Assemble Base + Header
|
||||||
|
total_height = base_img.height + header_height
|
||||||
|
final_img = Image.new("RGB", (base_img.width + MARGIN_LEFT, total_height), "white")
|
||||||
|
|
||||||
|
current_y = 0
|
||||||
|
for img, boxes in header_elements:
|
||||||
|
final_img.paste(img, (0, current_y))
|
||||||
|
for b in boxes:
|
||||||
|
b['final_box'] = [b['rel_box'][0], b['rel_box'][1] + current_y,
|
||||||
|
b['rel_box'][2], b['rel_box'][3] + current_y]
|
||||||
|
checkbox_map.append(b)
|
||||||
|
current_y += img.height
|
||||||
|
|
||||||
|
image_offset_y = current_y
|
||||||
|
final_img.paste(base_img, (MARGIN_LEFT, image_offset_y))
|
||||||
|
|
||||||
|
# 3. Draw Local Annotations
|
||||||
|
draw = ImageDraw.Draw(final_img, "RGBA")
|
||||||
|
last_text_bottom = 0
|
||||||
|
|
||||||
|
for i, fb in enumerate(local_fb):
|
||||||
|
box = fb.get('box_2d')
|
||||||
|
if not box: continue
|
||||||
|
|
||||||
|
ymin, xmin, ymax, xmax = box
|
||||||
|
target_ymin = (ymin - hmin) + image_offset_y
|
||||||
|
target_ymax = (ymax - hmin) + image_offset_y
|
||||||
|
target_xmin = xmin + MARGIN_LEFT
|
||||||
|
target_xmax = xmax + MARGIN_LEFT
|
||||||
|
|
||||||
|
draw.rectangle([target_xmin, target_ymin, target_xmax, target_ymax], outline="red", width=3)
|
||||||
|
|
||||||
|
rect_cb_box = draw_checkbox(draw, target_xmax - BOX_SIZE, target_ymin, BOX_SIZE)
|
||||||
|
checkbox_map.append({
|
||||||
|
"type": "del_local", "label": label, "index": i,
|
||||||
|
"text_preview": fb['text'][:20], "final_box": rect_cb_box
|
||||||
|
})
|
||||||
|
|
||||||
|
txt_img_raw = safe_render_latex(fb['text'], width_px=ANNOT_WIDTH, bg_color=(255, 200, 200, 180), max_lines=3)
|
||||||
|
container_h = max(txt_img_raw.height, BOX_SIZE)
|
||||||
|
txt_img = Image.new("RGBA", (ANNOT_WIDTH, container_h), (255, 255, 255, 0))
|
||||||
|
txt_img.paste(txt_img_raw, (0, 0))
|
||||||
|
|
||||||
|
d_txt = ImageDraw.Draw(txt_img)
|
||||||
|
draw_checkbox(d_txt, ANNOT_WIDTH - BOX_SIZE, 0, BOX_SIZE)
|
||||||
|
|
||||||
|
center_y = (target_ymin + target_ymax) / 2
|
||||||
|
paste_y = max(center_y - (txt_img.height / 2), image_offset_y)
|
||||||
|
if paste_y < last_text_bottom: paste_y = last_text_bottom + 5
|
||||||
|
|
||||||
|
req_h = int(paste_y + txt_img.height + 20)
|
||||||
|
if req_h > final_img.height:
|
||||||
|
new_final = Image.new("RGB", (final_img.width, req_h), "white")
|
||||||
|
new_final.paste(final_img, (0,0))
|
||||||
|
final_img = new_final
|
||||||
|
draw = ImageDraw.Draw(final_img, "RGBA")
|
||||||
|
|
||||||
|
final_img.paste(txt_img, (10, int(paste_y)), mask=txt_img)
|
||||||
|
checkbox_map.append({
|
||||||
|
"type": "del_local", "label": label, "index": i,
|
||||||
|
"text_preview": fb['text'][:20],
|
||||||
|
"final_box": [10 + ANNOT_WIDTH - BOX_SIZE, int(paste_y), 10 + ANNOT_WIDTH, int(paste_y) + BOX_SIZE]
|
||||||
|
})
|
||||||
|
last_text_bottom = paste_y + txt_img.height
|
||||||
|
|
||||||
|
return final_img, checkbox_map
|
||||||
|
|
||||||
|
import re
|
||||||
|
def natural_key(text):
|
||||||
|
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))]
|
||||||
|
|
||||||
|
def process_student(args):
|
||||||
|
"""Thread worker: Processes one student."""
|
||||||
|
root_dir, student_id, labels = args
|
||||||
|
print(f"Generating Checkable PDF for: {student_id}")
|
||||||
|
|
||||||
|
output_dir = os.path.join(root_dir, "Bnot", f"Copie{student_id}")
|
||||||
|
if os.path.exists(output_dir):
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
|
label_images = []
|
||||||
|
student_checkboxes = []
|
||||||
|
processed_labels_order = []
|
||||||
|
|
||||||
|
for label, content in sorted(labels.items(), key=lambda x: natural_key(x[0])):
|
||||||
|
img, boxes = process_label(root_dir, student_id, label, content)
|
||||||
|
if img:
|
||||||
|
label_images.append(img)
|
||||||
|
student_checkboxes.append(boxes)
|
||||||
|
processed_labels_order.append(label)
|
||||||
|
|
||||||
|
if not label_images:
|
||||||
|
return
|
||||||
|
|
||||||
|
max_w = max(i.width for i in label_images)
|
||||||
|
total_h = sum(i.height for i in label_images)
|
||||||
|
concat_img = Image.new("RGB", (max_w, total_h), "white")
|
||||||
|
|
||||||
|
final_json_map = []
|
||||||
|
current_y = 0
|
||||||
|
|
||||||
|
for label_name, img, boxes in zip(processed_labels_order, label_images, student_checkboxes):
|
||||||
|
concat_img.paste(img, (0, current_y))
|
||||||
|
|
||||||
|
for item in boxes:
|
||||||
|
b = item['final_box']
|
||||||
|
item['global_box'] = [b[0], b[1] + current_y, b[2], b[3] + current_y]
|
||||||
|
final_json_map.append(item)
|
||||||
|
|
||||||
|
current_y += img.height
|
||||||
|
|
||||||
|
with open(os.path.join(output_dir, "checkboxes.json"), "w") as f:
|
||||||
|
json.dump(final_json_map, f, indent=2)
|
||||||
|
|
||||||
|
concat_img.save(os.path.join(output_dir, "Reference.png"))
|
||||||
|
concat_img.save(os.path.join(output_dir, "Concat.pdf"), "PDF", resolution=100.0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python annotating_with_checks.py <Dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
root_dir = sys.argv[1]
|
||||||
|
results = annotating.make_dictionary(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
tasks = [(root_dir, sid, lbls) for sid, lbls in results.items()]
|
||||||
|
|
||||||
|
# print(tasks)
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
|
||||||
|
executor.map(process_student, tasks)
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import shutil
|
||||||
|
from PIL import Image, ImageChops
|
||||||
|
Image.MAX_IMAGE_PIXELS = None
|
||||||
|
from pdf2image import convert_from_path
|
||||||
|
import annotating # Reuse rendering logic
|
||||||
|
|
||||||
|
def detect_checks_and_notes(output_dir):
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
actions: List of dicts {type, label, ...} for checked boxes
|
||||||
|
notes_img: RGBA image of manual notes (checks masked out)
|
||||||
|
"""
|
||||||
|
pdf_path = os.path.join(output_dir, "Concat_annotated.pdf")
|
||||||
|
ref_path = os.path.join(output_dir, "Reference.png")
|
||||||
|
json_path = os.path.join(output_dir, "checkboxes.json")
|
||||||
|
|
||||||
|
if not (os.path.exists(pdf_path) and os.path.exists(ref_path)):
|
||||||
|
print(f"Missing files in {output_dir}")
|
||||||
|
return [], None
|
||||||
|
|
||||||
|
# Load Coordinates
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
boxes = json.load(f)
|
||||||
|
|
||||||
|
# Load Reference
|
||||||
|
ref_img = Image.open(ref_path).convert("RGB")
|
||||||
|
|
||||||
|
# Load User PDF (First page only, assuming it's one long strip)
|
||||||
|
# Warning: If the PDF is huge, pdf2image might split pages or OOM.
|
||||||
|
# Assuming user didn't change page dimensions/order.
|
||||||
|
try:
|
||||||
|
user_pages = convert_from_path(pdf_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading PDF: {e}")
|
||||||
|
return [], None
|
||||||
|
print("Debug : user_pages", len(user_pages))
|
||||||
|
# Concatenate PDF pages back to one image if user saved as multiple pages
|
||||||
|
# (Xournal++ might preserve the long format or split it)
|
||||||
|
total_h = sum(p.height for p in user_pages)
|
||||||
|
user_img = Image.new("RGB", (user_pages[0].width, total_h))
|
||||||
|
y = 0
|
||||||
|
for p in user_pages:
|
||||||
|
user_img.paste(p, (0, y))
|
||||||
|
y += p.height
|
||||||
|
|
||||||
|
# Resize user_img to match ref_img if slight mismatch (DPI export diffs)
|
||||||
|
if user_img.size != ref_img.size:
|
||||||
|
print("Debug : size mismatch : ", user_img.size, ref_img.size)
|
||||||
|
user_img = user_img.resize(ref_img.size, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# --- Detection Phase ---
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
# Convert to numpy for analysis
|
||||||
|
ref_arr = np.array(ref_img)
|
||||||
|
user_arr = np.array(user_img)
|
||||||
|
|
||||||
|
# Diff for analysis
|
||||||
|
# Simple absolute difference
|
||||||
|
diff = np.abs(ref_arr.astype(int) - user_arr.astype(int)).astype(np.uint8)
|
||||||
|
# Convert to grayscale for thresholding
|
||||||
|
diff_gray = np.mean(diff, axis=2)
|
||||||
|
|
||||||
|
# Threshold for "Checked"
|
||||||
|
CHECK_THRESHOLD = 30 # intensity diff
|
||||||
|
DENSITY_THRESHOLD = 0.05 # 5% of pixels darkened
|
||||||
|
|
||||||
|
# Mask to hide checkmarks from the "Notes" extraction
|
||||||
|
mask_img = Image.new("L", ref_img.size, 255) # White = keep, Black = hide
|
||||||
|
mask_draw = ImageDraw.Draw(mask_img)
|
||||||
|
|
||||||
|
for box in boxes:
|
||||||
|
# global_box: [x1, y1, x2, y2]
|
||||||
|
b = box['global_box']
|
||||||
|
x1, y1, x2, y2 = map(int, b)
|
||||||
|
|
||||||
|
# Ensure bounds
|
||||||
|
x1, y1 = max(0, x1), max(0, y1)
|
||||||
|
x2, y2 = min(ref_img.width, x2), min(ref_img.height, y2)
|
||||||
|
|
||||||
|
# Analyze ROI
|
||||||
|
roi = diff_gray[y1+5:y2-5, x1+5:x2-5]
|
||||||
|
if roi.size == 0: continue
|
||||||
|
|
||||||
|
changed_pixels = np.sum(roi > CHECK_THRESHOLD)
|
||||||
|
density = changed_pixels / roi.size
|
||||||
|
|
||||||
|
if density > DENSITY_THRESHOLD:
|
||||||
|
print("A checked box !", density, b)
|
||||||
|
actions.append(box)
|
||||||
|
# It's checked, so we mask this area out for manual notes
|
||||||
|
# Expand mask slightly to catch sloppy ticks
|
||||||
|
mask_draw.rectangle([x1-5, y1-5, x2+5, y2+5], fill=0)
|
||||||
|
else:
|
||||||
|
# print("A box, not checked !", density)
|
||||||
|
# Even if not "checked", mask the box area slightly to avoid
|
||||||
|
# artifacts if user hovered over it, though arguably we keep it.
|
||||||
|
# Let's strictly mask only if checked to verify detection?
|
||||||
|
# No, prompt says "not extract the part that are just checking".
|
||||||
|
# If user checked it, we mask it.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- Extraction Phase ---
|
||||||
|
# Create the "Manual Notes" layer
|
||||||
|
# Logic: User - Ref. If Diff is dark -> Note.
|
||||||
|
# We want a transparent image with just the pen strokes.
|
||||||
|
|
||||||
|
# 1. Get difference image
|
||||||
|
diff_img = ImageChops.difference(ref_img, user_img).convert("L")
|
||||||
|
|
||||||
|
# 2. Threshold to remove JPEG noise (white background isn't perfect)
|
||||||
|
# Pixels that are different enough:
|
||||||
|
diff_data = np.array(diff_img)
|
||||||
|
# Create alpha channel: 0 where no diff, 255 where diff
|
||||||
|
alpha = np.where(diff_data > 20, 255, 0).astype(np.uint8)
|
||||||
|
|
||||||
|
# 3. Create output image (Black strokes, variable alpha)
|
||||||
|
# Or Copy user colors? Better to copy user pixels.
|
||||||
|
notes = user_img.convert("RGBA")
|
||||||
|
r, g, b, a = notes.split()
|
||||||
|
|
||||||
|
# Combine the diff-based alpha with the box-mask
|
||||||
|
mask_arr = np.array(mask_img)
|
||||||
|
final_alpha = np.minimum(alpha, mask_arr)
|
||||||
|
|
||||||
|
notes.putalpha(Image.fromarray(final_alpha))
|
||||||
|
|
||||||
|
return actions, notes
|
||||||
|
|
||||||
|
from PIL import ImageDraw
|
||||||
|
|
||||||
|
def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_layer):
|
||||||
|
"""
|
||||||
|
Modifies data based on actions, calls annotating.process_correction logic,
|
||||||
|
overlays notes, saves Concat.jpg.
|
||||||
|
"""
|
||||||
|
labels = data[student_id]
|
||||||
|
|
||||||
|
# 1. Apply Actions to Data
|
||||||
|
# Sort actions to handle indices correctly (delete from end?)
|
||||||
|
# But we regenerate from dictionary, so modifying the dictionary is fine.
|
||||||
|
|
||||||
|
# Separate actions by label
|
||||||
|
actions_by_label = {}
|
||||||
|
for a in actions:
|
||||||
|
l = a['label']
|
||||||
|
if l not in actions_by_label: actions_by_label[l] = []
|
||||||
|
actions_by_label[l].append(a)
|
||||||
|
|
||||||
|
for label, acts in actions_by_label.items():
|
||||||
|
if label not in labels: continue
|
||||||
|
|
||||||
|
content = labels[label]
|
||||||
|
result = content['result']
|
||||||
|
feedbacks = result.get('feedback', [])
|
||||||
|
|
||||||
|
# Split feedbacks again to match indices
|
||||||
|
global_fb_indices = [i for i, f in enumerate(feedbacks) if not f.get('box_2d')]
|
||||||
|
local_fb_indices = [i for i, f in enumerate(feedbacks) if f.get('box_2d')]
|
||||||
|
# Sort local by Y to match generation order in annotating.py
|
||||||
|
local_fb_sorted_map = sorted(local_fb_indices, key=lambda i: feedbacks[i]['box_2d'][0])
|
||||||
|
|
||||||
|
items_to_remove = set()
|
||||||
|
|
||||||
|
for act in acts:
|
||||||
|
if act['type'] == 'set_score':
|
||||||
|
result['score'] = act['value']
|
||||||
|
print(f" > Updated score for {label} to {act['value']}")
|
||||||
|
|
||||||
|
elif act['type'] == 'del_global':
|
||||||
|
# act['index'] is the index within the global_fb list
|
||||||
|
# We need to find the actual index in the main list
|
||||||
|
if act['index'] < len(global_fb_indices):
|
||||||
|
real_idx = global_fb_indices[act['index']]
|
||||||
|
items_to_remove.add(real_idx)
|
||||||
|
print(f" > Deleted global feedback in {label}")
|
||||||
|
|
||||||
|
elif act['type'] == 'del_local':
|
||||||
|
# act['index'] is index in sorted local list
|
||||||
|
if act['index'] < len(local_fb_sorted_map):
|
||||||
|
real_idx = local_fb_sorted_map[act['index']]
|
||||||
|
items_to_remove.add(real_idx)
|
||||||
|
print(f" > Deleted local feedback in {label}")
|
||||||
|
|
||||||
|
# Remove feedbacks (in reverse to preserve indices)
|
||||||
|
for idx in sorted(list(items_to_remove), reverse=True):
|
||||||
|
del feedbacks[idx]
|
||||||
|
|
||||||
|
# 2. Regenerate Clean Image
|
||||||
|
# We use a temporary modified dictionary
|
||||||
|
temp_data = {student_id: labels}
|
||||||
|
|
||||||
|
# Run the original process (but we need to intercept it to not save, or just let it save)
|
||||||
|
# annotating.process_correction saves to "Anot_CopieID".
|
||||||
|
# We want "Bnot_CopieID" (updated).
|
||||||
|
|
||||||
|
# Hijack the output dir in logic or copy code?
|
||||||
|
# Easiest: Let's create a temporary helper or modify annotating logic slightly?
|
||||||
|
# The prompt implies we use `annotating.py` logic.
|
||||||
|
# Let's call `annotating.process_correction` but point it to a temp root or modify path?
|
||||||
|
# No, `process_correction` takes `root_dir` and writes to `Anot_...`.
|
||||||
|
# Let's just implement the rendering loop here to be safe and clean,
|
||||||
|
# overlaying the notes at the end.
|
||||||
|
|
||||||
|
output_dir = os.path.join(root_dir, "Bnot", f"Copie{student_id}")
|
||||||
|
# Don't delete output_dir, we need it.
|
||||||
|
|
||||||
|
# ... (Reuse rendering logic from annotating.py exactly) ...
|
||||||
|
# See below for condensed integration
|
||||||
|
|
||||||
|
final_concats = []
|
||||||
|
|
||||||
|
for label, content in labels.items():
|
||||||
|
# ... [PDF to Image Conversion] ...
|
||||||
|
copie_folder = f"Copie{student_id}"
|
||||||
|
pdf_path = os.path.join(root_dir, copie_folder, f"{label}.pdf")
|
||||||
|
if not os.path.exists(pdf_path): continue
|
||||||
|
|
||||||
|
pages = annotating.convert_from_path(pdf_path)
|
||||||
|
base_img = Image.new("RGBA", (max(p.width for p in pages), sum(p.height for p in pages)), "white")
|
||||||
|
y=0
|
||||||
|
for p in pages: base_img.paste(p.convert("RGBA"), (0,y)); y+=p.height
|
||||||
|
|
||||||
|
# ... [Draw Header/Margin (Clean)] ...
|
||||||
|
margin_left = 200
|
||||||
|
result = content['result']
|
||||||
|
coordinates = content.get('coordinates', (0,0))
|
||||||
|
hmin = coordinates[0]
|
||||||
|
|
||||||
|
score_text = f"{label} ; Note : {result.get('score', 0)}"
|
||||||
|
if result.get('error') and result.get('error') != "null": score_text += f" | Error: {result.get('error')}"
|
||||||
|
|
||||||
|
header_imgs = [annotating.render_latex_text(score_text, base_img.width, fontsize=18)]
|
||||||
|
|
||||||
|
feedbacks = result.get('feedback', [])
|
||||||
|
# Separate again (now cleaned)
|
||||||
|
global_fb = [f for f in feedbacks if not f.get('box_2d')]
|
||||||
|
local_fb = [f for f in feedbacks if f.get('box_2d')]
|
||||||
|
local_fb.sort(key=lambda x: x['box_2d'][0])
|
||||||
|
|
||||||
|
for fb in global_fb: header_imgs.append(annotating.render_latex_text(fb['text'], base_img.width))
|
||||||
|
|
||||||
|
total_h = base_img.height + sum(i.height for i in header_imgs)
|
||||||
|
label_img = Image.new("RGB", (base_img.width + margin_left, total_h), "white")
|
||||||
|
|
||||||
|
cy = 0
|
||||||
|
for i in header_imgs: label_img.paste(i, (0, cy)); cy+=i.height
|
||||||
|
img_offset_y = cy
|
||||||
|
label_img.paste(base_img, (margin_left, img_offset_y))
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(label_img, "RGBA")
|
||||||
|
last_bot = 0
|
||||||
|
for fb in local_fb:
|
||||||
|
box = fb['box_2d']
|
||||||
|
ymin, xmin, ymax, xmax = box
|
||||||
|
t_ymin = (ymin - hmin) + img_offset_y
|
||||||
|
t_ymax = (ymax - hmin) + img_offset_y
|
||||||
|
draw.rectangle([xmin+margin_left, t_ymin, xmax+margin_left, t_ymax], outline="red", width=3)
|
||||||
|
|
||||||
|
txt = annotating.render_latex_text(fb['text'], 500, (255,200,200,180), max_lines=3)
|
||||||
|
py = max((t_ymin+t_ymax)/2 - txt.height/2, img_offset_y)
|
||||||
|
if py < last_bot: py = last_bot + 5
|
||||||
|
|
||||||
|
if py + txt.height + 20 > label_img.height:
|
||||||
|
new_l = Image.new("RGB", (label_img.width, int(py+txt.height+20)), "white")
|
||||||
|
new_l.paste(label_img, (0,0))
|
||||||
|
label_img = new_l
|
||||||
|
draw = ImageDraw.Draw(label_img, "RGBA")
|
||||||
|
|
||||||
|
label_img.paste(txt, (10, int(py)), mask=txt)
|
||||||
|
last_bot = py + txt.height
|
||||||
|
|
||||||
|
final_concats.append(label_img)
|
||||||
|
|
||||||
|
# Concatenate Labels
|
||||||
|
if not final_concats: return
|
||||||
|
|
||||||
|
mw = max(i.width for i in final_concats)
|
||||||
|
th = sum(i.height for i in final_concats)
|
||||||
|
full_clean_img = Image.new("RGB", (mw, th), "white")
|
||||||
|
y=0
|
||||||
|
for i in final_concats:
|
||||||
|
full_clean_img.paste(i, (0,y))
|
||||||
|
y+=i.height
|
||||||
|
|
||||||
|
# 3. Overlay Manual Notes
|
||||||
|
if notes_layer:
|
||||||
|
# Notes layer might be different size if regenerated image size changed (e.g. deleted comments reduced height)
|
||||||
|
# However, usually reducing content reduces height, so we align top-left.
|
||||||
|
# But `notes_layer` is based on the "Reference.png" which had boxes.
|
||||||
|
# The new `full_clean_img` does NOT have boxes. The dimensions should be identical
|
||||||
|
# unless removing a feedback at the very bottom shrinks the image.
|
||||||
|
|
||||||
|
# We paste notes_layer on top.
|
||||||
|
full_clean_img.paste(notes_layer, (0,0), mask=notes_layer)
|
||||||
|
|
||||||
|
# Save final Concat.jpg
|
||||||
|
final_path = os.path.join(output_dir, "Concat.jpg")
|
||||||
|
full_clean_img.save(final_path)
|
||||||
|
print(f"Saved regenerated: {final_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python reading_annotations.py <Dir>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
root_dir = sys.argv[1]
|
||||||
|
|
||||||
|
# Load original data
|
||||||
|
original_data = annotating.make_dictionary(root_dir)
|
||||||
|
|
||||||
|
# Process each Bnot folder
|
||||||
|
for student_id in original_data.keys():
|
||||||
|
bnot_dir = os.path.join(root_dir, "Bnot", f"Copie{student_id}")
|
||||||
|
if os.path.exists(bnot_dir):
|
||||||
|
print(f"Processing annotations for: {student_id}")
|
||||||
|
actions, notes = detect_checks_and_notes(bnot_dir)
|
||||||
|
if actions or notes:
|
||||||
|
apply_actions_and_regenerate(root_dir, original_data, student_id, actions, notes)
|
||||||
|
else:
|
||||||
|
print(" No changes detected or missing files.")
|
||||||
Loading…
Reference in New Issue