Refactor of all annotating things.
parent
d725b9edbc
commit
5c25ed42a3
274
annotating.py
274
annotating.py
|
|
@ -2,6 +2,8 @@ import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import glob
|
import glob
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
MARGIN_LEFT = 300
|
MARGIN_LEFT = 300
|
||||||
|
|
@ -81,6 +83,22 @@ def make_dictionary(root_dir):
|
||||||
|
|
||||||
return result_data
|
return result_data
|
||||||
|
|
||||||
|
def make_base_image(pdf_path):
|
||||||
|
pages = convert_from_path(pdf_path)
|
||||||
|
|
||||||
|
# Calculate total dimensions
|
||||||
|
total_h = sum(page.height for page in pages)
|
||||||
|
max_w = max(page.width for page in pages)
|
||||||
|
|
||||||
|
# Create concatenated base image
|
||||||
|
base_img = Image.new("RGBA", (max_w, total_h), "white")
|
||||||
|
|
||||||
|
current_y = 0
|
||||||
|
for page in pages:
|
||||||
|
base_img.paste(page.convert("RGBA"), (0, current_y))
|
||||||
|
current_y += page.height
|
||||||
|
return (base_img, total_h, max_w)
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import shutil
|
import shutil
|
||||||
from pdf2image import convert_from_path
|
from pdf2image import convert_from_path
|
||||||
|
|
@ -111,7 +129,6 @@ def normalize_mathtext(text):
|
||||||
text = re.sub('\u0010', "", text)
|
text = re.sub('\u0010', "", text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
import re
|
|
||||||
def wrap_latex_text(text, width_chars):
|
def wrap_latex_text(text, width_chars):
|
||||||
"""
|
"""
|
||||||
Wraps text but keeps LaTeX math blocks ($...$) intact.
|
Wraps text but keeps LaTeX math blocks ($...$) intact.
|
||||||
|
|
@ -178,8 +195,6 @@ def render_latex_text(text, width_px, bg_color=(255, 255, 255, 255), max_lines=N
|
||||||
|
|
||||||
fig = plt.figure(figsize=(fig_width, fig_height), dpi=dpi)
|
fig = plt.figure(figsize=(fig_width, fig_height), dpi=dpi)
|
||||||
|
|
||||||
# print(wrapped_text)
|
|
||||||
# print("\n\n")
|
|
||||||
# NOTE: wrap=False because we did it ourselves
|
# NOTE: wrap=False because we did it ourselves
|
||||||
plt.text(0.01, 0.95, wrapped_text, fontsize=fontsize,
|
plt.text(0.01, 0.95, wrapped_text, fontsize=fontsize,
|
||||||
verticalalignment='top', horizontalalignment='left',
|
verticalalignment='top', horizontalalignment='left',
|
||||||
|
|
@ -199,74 +214,129 @@ def render_latex_text(text, width_px, bg_color=(255, 255, 255, 255), max_lines=N
|
||||||
final_img.alpha_composite(img)
|
final_img.alpha_composite(img)
|
||||||
return final_img
|
return final_img
|
||||||
|
|
||||||
def add_local_fb(fb, draw, final_img, hmin, image_offset_y, last_text_bottom):
|
def compose_label_image(base_img, label, result, hmin,
|
||||||
# raw_pos = fb.get('pos')
|
render_fn=render_latex_text,
|
||||||
box = fb.get('box_2d')
|
draw_callback=None):
|
||||||
if not box or len(box) < 4:
|
"""
|
||||||
return
|
Composes the final image with annotations.
|
||||||
|
|
||||||
ymin, xmin, ymax, xmax = box[0], box[1], box[2], box[3]
|
Args:
|
||||||
|
base_img: The source PDF converted to image.
|
||||||
|
label: Label name (e.g. "Ex1").
|
||||||
|
result: The JSON result object (score, feedbacks).
|
||||||
|
hmin: Vertical offset coordinate.
|
||||||
|
render_fn: Function to render text to image (allows threading injection).
|
||||||
|
draw_callback: Optional function(type, draw_obj, position_dict, data_dict)
|
||||||
|
called when elements are placed. Used for checkboxes.
|
||||||
|
"""
|
||||||
|
score = result.get('score', 0)
|
||||||
|
error = result.get('error', "")
|
||||||
|
feedbacks = result.get('feedback', [])
|
||||||
|
|
||||||
|
# Filter deleted items (used by reading_annotations.py)
|
||||||
|
feedbacks = [f for f in feedbacks if "to_delete" not in f]
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
# 1. Prepare Headers
|
||||||
|
header_elements = []
|
||||||
|
|
||||||
|
# Score
|
||||||
|
score_text = f"{label} ; Note : {score}"
|
||||||
|
if error and error != "null": score_text += f" | Error: {error}"
|
||||||
|
|
||||||
|
img_score = render_fn(score_text, base_img.width // 2, fontsize=18)
|
||||||
|
header_elements.append({"type": "score", "img": img_score, "data": result})
|
||||||
|
|
||||||
|
# Global Feedbacks
|
||||||
|
for idx, fb in enumerate(global_fb):
|
||||||
|
img_fb = render_fn(fb['text'], base_img.width)
|
||||||
|
header_elements.append({"type": "global_fb", "img": img_fb, "data": fb, "index": idx})
|
||||||
|
|
||||||
|
# Calculate Header Height
|
||||||
|
header_height = sum(el["img"].height for el in header_elements)
|
||||||
|
total_height = base_img.height + header_height
|
||||||
|
|
||||||
|
# Create Canvas
|
||||||
|
final_img = Image.new("RGB", (base_img.width + MARGIN_LEFT, total_height), "white")
|
||||||
|
|
||||||
|
# Draw Headers
|
||||||
|
current_y = 0
|
||||||
|
draw = ImageDraw.Draw(final_img, "RGBA")
|
||||||
|
|
||||||
|
for el in header_elements:
|
||||||
|
final_img.paste(el["img"], (0, current_y))
|
||||||
|
|
||||||
|
if draw_callback:
|
||||||
|
# Hook for checkboxes
|
||||||
|
draw_callback("header_item", draw,
|
||||||
|
{"x": 0, "y": current_y, "w": el["img"].width, "h": el["img"].height},
|
||||||
|
el)
|
||||||
|
current_y += el["img"].height
|
||||||
|
|
||||||
|
# Paste Base Image
|
||||||
|
image_offset_y = current_y
|
||||||
|
final_img.paste(base_img, (MARGIN_LEFT, image_offset_y))
|
||||||
|
|
||||||
|
# 2. Draw Local Annotations
|
||||||
|
draw = ImageDraw.Draw(final_img, "RGBA") # Refresh draw object
|
||||||
|
last_text_bottom = 0
|
||||||
|
|
||||||
|
for idx, fb in enumerate(local_fb):
|
||||||
|
box = fb.get('box_2d')
|
||||||
|
ymin, xmin, ymax, xmax = box
|
||||||
|
|
||||||
target_ymin = (ymin - hmin) + image_offset_y
|
target_ymin = (ymin - hmin) + image_offset_y
|
||||||
target_ymax = (ymax - hmin) + image_offset_y
|
target_ymax = (ymax - hmin) + image_offset_y
|
||||||
target_xmin = xmin + MARGIN_LEFT
|
target_xmin = xmin + MARGIN_LEFT
|
||||||
target_xmax = xmax + MARGIN_LEFT
|
target_xmax = xmax + MARGIN_LEFT
|
||||||
|
|
||||||
# Draw Rectangle
|
# Draw Rectangle (if not suppressed)
|
||||||
|
if "norectangle" not in fb:
|
||||||
draw.rectangle([target_xmin, target_ymin, target_xmax, target_ymax], outline="red", width=3)
|
draw.rectangle([target_xmin, target_ymin, target_xmax, target_ymax], outline="red", width=3)
|
||||||
|
|
||||||
# Render Text with transparent red background
|
if draw_callback:
|
||||||
# (255, 0, 0, 50) is transparent red
|
draw_callback("local_rect", draw,
|
||||||
txt_img = render_latex_text(
|
{"box": [target_xmin, target_ymin, target_xmax, target_ymax]},
|
||||||
fb['text'],
|
{"data": fb, "index": idx})
|
||||||
width_px=ANNOT_WIDTH,
|
|
||||||
bg_color=(255, 200, 200, 180), # Light Red semi-transparent
|
|
||||||
max_lines=3
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate placement
|
# Render Text
|
||||||
txt_h = txt_img.height
|
txt_img = render_fn(fb['text'], width_px=ANNOT_WIDTH,
|
||||||
|
bg_color=(255, 200, 200, 180), max_lines=3)
|
||||||
|
|
||||||
|
# Calculate Position
|
||||||
center_y = (target_ymin + target_ymax) / 2
|
center_y = (target_ymin + target_ymax) / 2
|
||||||
paste_y = center_y - (txt_h / 2)
|
paste_y = center_y - (txt_img.height / 2)
|
||||||
|
|
||||||
paste_y = max(paste_y, image_offset_y)
|
paste_y = max(paste_y, image_offset_y)
|
||||||
|
|
||||||
# Prevent overlap with previous text
|
|
||||||
if paste_y < last_text_bottom:
|
if paste_y < last_text_bottom:
|
||||||
paste_y = last_text_bottom + 5 # Move down + padding
|
paste_y = last_text_bottom + 5
|
||||||
|
|
||||||
# Check for overflow and resize if necessary
|
# Resize canvas if needed
|
||||||
required_height = int(paste_y + txt_h + 20) # +20 for bottom padding
|
required_height = int(paste_y + txt_img.height + 20)
|
||||||
if required_height > final_img.height:
|
if required_height > final_img.height:
|
||||||
# Create a new taller image
|
|
||||||
new_final = Image.new("RGB", (final_img.width, required_height), "white")
|
new_final = Image.new("RGB", (final_img.width, required_height), "white")
|
||||||
# Paste the current image content onto the new one
|
|
||||||
new_final.paste(final_img, (0, 0))
|
new_final.paste(final_img, (0, 0))
|
||||||
final_img = new_final
|
final_img = new_final
|
||||||
# Re-initialize the draw object for the new image so subsequent rectangles are drawn correctly
|
|
||||||
draw = ImageDraw.Draw(final_img, "RGBA")
|
draw = ImageDraw.Draw(final_img, "RGBA")
|
||||||
|
|
||||||
|
# Paste Text
|
||||||
# Paste in the left margin
|
|
||||||
final_img.paste(txt_img, (10, int(paste_y)), mask=txt_img)
|
final_img.paste(txt_img, (10, int(paste_y)), mask=txt_img)
|
||||||
last_text_bottom = paste_y + txt_h
|
|
||||||
return (draw, final_img, last_text_bottom)
|
|
||||||
|
|
||||||
def make_base_image(pdf_path):
|
if draw_callback:
|
||||||
pages = convert_from_path(pdf_path)
|
draw_callback("local_text", draw,
|
||||||
|
{"x": 10, "y": int(paste_y), "w": txt_img.width, "h": txt_img.height},
|
||||||
|
{"data": fb, "index": idx})
|
||||||
|
|
||||||
# Calculate total dimensions
|
last_text_bottom = paste_y + txt_img.height
|
||||||
total_h = sum(page.height for page in pages)
|
|
||||||
max_w = max(page.width for page in pages)
|
|
||||||
|
|
||||||
# Create concatenated base image
|
return final_img
|
||||||
base_img = Image.new("RGBA", (max_w, total_h), "white")
|
|
||||||
|
def natural_key(text):
|
||||||
|
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))]
|
||||||
|
|
||||||
current_y = 0
|
|
||||||
for page in pages:
|
|
||||||
base_img.paste(page.convert("RGBA"), (0, current_y))
|
|
||||||
current_y += page.height
|
|
||||||
return (base_img, total_h, max_w)
|
|
||||||
|
|
||||||
def process_correction(root_dir, data, all_labels, overwrite=False):
|
def process_correction(root_dir, data, all_labels, overwrite=False):
|
||||||
for student_id, labels in data.items():
|
for student_id, labels in data.items():
|
||||||
|
|
@ -274,8 +344,6 @@ def process_correction(root_dir, data, all_labels, overwrite=False):
|
||||||
# Prepare output directory: Dir/Anot_CopieID
|
# Prepare output directory: Dir/Anot_CopieID
|
||||||
output_dir = os.path.join(root_dir, "Anot", f"Copie{student_id}")
|
output_dir = os.path.join(root_dir, "Anot", f"Copie{student_id}")
|
||||||
|
|
||||||
# output_dir = os.path.join(root_dir, f"Anot_Copie{student_id}")
|
|
||||||
|
|
||||||
# Check if already processed (Concat.jpg exists)
|
# Check if already processed (Concat.jpg exists)
|
||||||
concat_path = os.path.join(output_dir, "Concat.jpg")
|
concat_path = os.path.join(output_dir, "Concat.jpg")
|
||||||
if os.path.exists(concat_path) and not overwrite:
|
if os.path.exists(concat_path) and not overwrite:
|
||||||
|
|
@ -290,8 +358,11 @@ def process_correction(root_dir, data, all_labels, overwrite=False):
|
||||||
os.makedirs(output_dir)
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
d_notes = dict.fromkeys(all_labels,"")
|
d_notes = dict.fromkeys(all_labels,"")
|
||||||
|
label_images = []
|
||||||
|
|
||||||
for label, content in labels.items():
|
labels = sorted(list(labels.items()), key=natural_key)
|
||||||
|
|
||||||
|
for label, content in labels:
|
||||||
# 1. Find PDF path
|
# 1. Find PDF path
|
||||||
copie_folder = f"Copie{student_id}"
|
copie_folder = f"Copie{student_id}"
|
||||||
pdf_rel_path = os.path.join(copie_folder, f"{label}.pdf")
|
pdf_rel_path = os.path.join(copie_folder, f"{label}.pdf")
|
||||||
|
|
@ -303,114 +374,37 @@ def process_correction(root_dir, data, all_labels, overwrite=False):
|
||||||
|
|
||||||
# 2. Convert PDF to Image
|
# 2. Convert PDF to Image
|
||||||
try:
|
try:
|
||||||
(base_img, total_h, max_w) = annotating.make_base_image(pdf_full_path)
|
(base_img, _, _) = make_base_image(pdf_full_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error converting {pdf_full_path}: {e}")
|
print(f"Error converting {pdf_full_path}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
coordinates = content.get('coordinates', (0, 0)) # (hmin, hmax)
|
|
||||||
hmin = coordinates[0]
|
|
||||||
result = content.get('result', {})
|
result = content.get('result', {})
|
||||||
|
coordinates = content.get('coordinates', (0, 0)) # (hmin, hmax)
|
||||||
score = result.get('score', 0)
|
score = result.get('score', 0)
|
||||||
error = result.get('error', "")
|
|
||||||
feedbacks = result.get('feedback', [])
|
|
||||||
|
|
||||||
# Organize feedbacks
|
|
||||||
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')]
|
|
||||||
# Sort local feedback by Y position
|
|
||||||
local_fb.sort(key=lambda x: x['box_2d'][0])
|
|
||||||
|
|
||||||
# --- PREPARE HEADERS ---
|
|
||||||
header_elements = []
|
|
||||||
score_text = f"{label} ; Note : {score}"
|
|
||||||
d_notes[label] = str(score)
|
d_notes[label] = str(score)
|
||||||
if error and error != "null":
|
|
||||||
score_text += f" | Error: {error}"
|
|
||||||
|
|
||||||
# Render Row 1
|
final_img = compose_label_image(base_img, label, result, coordinates[0])
|
||||||
row1_img = render_latex_text(score_text, base_img.width,fontsize=18)
|
|
||||||
header_elements.append(row1_img)
|
|
||||||
|
|
||||||
# --- OTHER HEADERS
|
|
||||||
# Render Global Feedbacks (Rows 2+)
|
|
||||||
for fb in global_fb:
|
|
||||||
fb_img = render_latex_text(fb['text'], base_img.width)
|
|
||||||
header_elements.append(fb_img)
|
|
||||||
|
|
||||||
# Calculate total new height
|
|
||||||
header_height = sum(img.height for img in header_elements)
|
|
||||||
total_height = base_img.height + header_height
|
|
||||||
|
|
||||||
# Create Canvas
|
|
||||||
final_img = Image.new("RGB", (base_img.width + MARGIN_LEFT, total_height), "white")
|
|
||||||
|
|
||||||
# Paste Headers
|
|
||||||
current_y = 0
|
|
||||||
for elem in header_elements:
|
|
||||||
final_img.paste(elem, (0, current_y))
|
|
||||||
current_y += elem.height
|
|
||||||
|
|
||||||
# Paste Original Image
|
|
||||||
# Note: current_y is now the offset for the actual image content
|
|
||||||
image_offset_y = current_y
|
|
||||||
final_img.paste(base_img, (MARGIN_LEFT, image_offset_y))
|
|
||||||
|
|
||||||
# --- DRAW LOCAL ANNOTATIONS ---
|
|
||||||
draw = ImageDraw.Draw(final_img, "RGBA")
|
|
||||||
|
|
||||||
last_text_bottom = 0
|
|
||||||
|
|
||||||
for fb in local_fb:
|
|
||||||
(draw, final_img, last_text_bottom) = add_local_fb(fb, draw, final_img, hmin, image_offset_y, last_text_bottom)
|
|
||||||
|
|
||||||
# 7. Save Image
|
# 7. Save Image
|
||||||
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)
|
||||||
|
label_images.append(final_img)
|
||||||
|
|
||||||
json_path = os.path.join(output_dir, "score.json")
|
# Save scores
|
||||||
with open(json_path, "w") as f:
|
with open(os.path.join(output_dir, "score.json"), "w") as f:
|
||||||
json.dump(d_notes, f, indent=4)
|
json.dump(d_notes, f, indent=4)
|
||||||
concat_display_image(output_dir)
|
|
||||||
|
|
||||||
|
# Concatenate
|
||||||
|
if label_images:
|
||||||
from pathlib import Path
|
max_w = max(i.width for i in label_images)
|
||||||
import subprocess
|
total_h = sum(i.height for i in label_images)
|
||||||
|
|
||||||
def concat_display_image(subdir):
|
|
||||||
subdir = Path(subdir)
|
|
||||||
# Find valid images, excluding previous concatenations
|
|
||||||
images = sorted([
|
|
||||||
f for f in subdir.glob("*.jpg")
|
|
||||||
if f.name != "Concat.jpg"
|
|
||||||
])
|
|
||||||
|
|
||||||
if not images:
|
|
||||||
return
|
|
||||||
images.sort(key=lambda f: [int(n) for n in re.findall(r'\d+', str(f))])
|
|
||||||
|
|
||||||
|
|
||||||
# Load images
|
|
||||||
opened_imgs = [Image.open(img) for img in images]
|
|
||||||
|
|
||||||
# Calculate dimensions (max width, sum of heights)
|
|
||||||
max_w = max(i.width for i in opened_imgs)
|
|
||||||
total_h = sum(i.height for i in opened_imgs)
|
|
||||||
|
|
||||||
# Create canvas and paste vertically
|
|
||||||
canvas = Image.new('RGB', (max_w, total_h))
|
canvas = Image.new('RGB', (max_w, total_h))
|
||||||
current_y = 0
|
cy = 0
|
||||||
for img in opened_imgs:
|
for img in label_images:
|
||||||
canvas.paste(img, (0, current_y))
|
canvas.paste(img, (0, cy))
|
||||||
current_y += img.height
|
cy += img.height
|
||||||
|
canvas.save(concat_path)
|
||||||
# Save
|
|
||||||
save_path = subdir / "Concat.jpg"
|
|
||||||
canvas.save(save_path)
|
|
||||||
print(f"Saved: {save_path}")
|
|
||||||
# subprocess.call(('xdg-open', save_path))
|
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -37,145 +37,62 @@ def draw_checkbox(draw, x, y, size=BOX_SIZE, label=None, fill="white"):
|
||||||
|
|
||||||
return [x, y, x + size, y + size]
|
return [x, y, x + size, y + size]
|
||||||
|
|
||||||
def safe_render_latex(text, **kwargs):
|
def safe_render_latex(*args, **kwargs):
|
||||||
"""Thread-safe wrapper for latex rendering."""
|
"""Thread-safe wrapper for latex rendering."""
|
||||||
with LATEX_LOCK:
|
with LATEX_LOCK:
|
||||||
return annotating.render_latex_text(text, **kwargs)
|
return annotating.render_latex_text(*args, **kwargs)
|
||||||
|
|
||||||
def render_header(label, score, feedbacks, base_width):
|
class CheckboxRenderer:
|
||||||
"""Generates the score line and global feedback elements."""
|
def __init__(self, label_name):
|
||||||
elements = []
|
self.label = label_name
|
||||||
|
self.checkboxes = [] # List of {type, box, etc.}
|
||||||
# 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 = []
|
|
||||||
|
|
||||||
|
def callback(self, kind, draw, pos, meta):
|
||||||
|
"""
|
||||||
|
Called by compose_label_image during rendering.
|
||||||
|
pos contains {x, y, w, h} or {box}.
|
||||||
|
meta contains {data, index, etc.}
|
||||||
|
"""
|
||||||
|
if kind == "header_item":
|
||||||
|
# meta['data'] is either result object (for score) or feedback object
|
||||||
|
if meta.get("type") == "score":
|
||||||
|
# Draw score boxes
|
||||||
|
start_x = pos['w'] + 20
|
||||||
for val in SCORES:
|
for val in SCORES:
|
||||||
box = draw_checkbox(draw_score, start_x, 5, SCORE_BOX_SIZE, str(val))
|
box = draw_checkbox(draw, start_x, pos['y'] + 5, BOX_SIZE, str(val))
|
||||||
local_boxes.append({
|
self.checkboxes.append({
|
||||||
"type": "score", "label": label, "value": val,
|
"type": "score", "label": self.label, "value": val,
|
||||||
"rel_box": box, "elem_y": 0
|
"rel_box": box # Will be adjusted for global Y later
|
||||||
})
|
})
|
||||||
start_x += SCORE_BOX_SIZE + 60
|
start_x += BOX_SIZE + 60
|
||||||
elements.append((score_line_img, local_boxes))
|
elif meta.get("type") == "global_fb":
|
||||||
|
# Draw delete box for global feedback
|
||||||
# Global Feedback
|
bx = pos['w'] - BOX_SIZE - 5
|
||||||
for i, fb in enumerate(feedbacks):
|
by = pos['y'] + 5
|
||||||
fb_img = safe_render_latex(fb['text'], width_px=base_width)
|
box = draw_checkbox(draw, bx, by, BOX_SIZE)
|
||||||
draw_fb = ImageDraw.Draw(fb_img)
|
self.checkboxes.append({
|
||||||
bx = fb_img.width - BOX_SIZE - 5
|
"type": "del_global", "label": self.label, "index": meta["index"],
|
||||||
by = 5
|
"rel_box": box, "text_preview": meta["data"]["text"][:20]
|
||||||
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, total_h, max_w = annotating.make_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_rect", "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,
|
elif kind == "local_rect":
|
||||||
bg_color=(255, 200, 200, 180), max_lines=3)
|
# Delete rect checkbox
|
||||||
container_h = max(txt_img_raw.height, BOX_SIZE)
|
b = pos['box'] # [xmin, ymin, xmax, ymax]
|
||||||
txt_img = Image.new("RGBA", (ANNOT_WIDTH, container_h), (255, 255, 255, 0))
|
box = draw_checkbox(draw, b[2] - BOX_SIZE, b[1], BOX_SIZE)
|
||||||
txt_img.paste(txt_img_raw, (0, 0))
|
self.checkboxes.append({
|
||||||
|
"type": "del_local_rect", "label": self.label, "index": meta["index"],
|
||||||
d_txt = ImageDraw.Draw(txt_img)
|
"final_box": box, "text_preview": meta["data"]["text"][:20]
|
||||||
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
|
elif kind == "local_text":
|
||||||
|
# Delete whole local feedback checkbox
|
||||||
|
bx = pos['x'] + pos['w'] - BOX_SIZE
|
||||||
|
by = pos['y']
|
||||||
|
box = draw_checkbox(draw, bx, by, BOX_SIZE)
|
||||||
|
self.checkboxes.append({
|
||||||
|
"type": "del_local", "label": self.label, "index": meta["index"],
|
||||||
|
"final_box": box, "text_preview": meta["data"]["text"][:20]
|
||||||
|
})
|
||||||
|
|
||||||
import re
|
import re
|
||||||
def natural_key(text):
|
def natural_key(text):
|
||||||
|
|
@ -192,19 +109,31 @@ def process_student(args):
|
||||||
os.makedirs(output_dir)
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
label_images = []
|
label_images = []
|
||||||
student_checkboxes = []
|
all_checkboxes = []
|
||||||
processed_labels_order = []
|
sorted_labels = sorted(labels.items(), key=lambda x: natural_key(x[0]))
|
||||||
|
|
||||||
for label, content in sorted(labels.items(), key=lambda x: natural_key(x[0])):
|
for label, content in sorted_labels:
|
||||||
img, boxes = process_label(root_dir, student_id, label, content)
|
pdf_path = content['pdf_path']
|
||||||
if img:
|
if not os.path.exists(pdf_path): continue
|
||||||
label_images.append(img)
|
|
||||||
student_checkboxes.append(boxes)
|
|
||||||
processed_labels_order.append(label)
|
|
||||||
|
|
||||||
if not label_images:
|
base_img, _, _ = annotating.make_base_image(pdf_path)
|
||||||
return
|
|
||||||
|
|
||||||
|
# Initialize the hook
|
||||||
|
cb_renderer = CheckboxRenderer(label)
|
||||||
|
|
||||||
|
# Render using the shared engine
|
||||||
|
final_img = annotating.compose_label_image(
|
||||||
|
base_img, label, content['result'], content['coordinates'][0],
|
||||||
|
render_fn=safe_render_latex,
|
||||||
|
draw_callback=cb_renderer.callback
|
||||||
|
)
|
||||||
|
|
||||||
|
label_images.append(final_img)
|
||||||
|
all_checkboxes.append(cb_renderer.checkboxes)
|
||||||
|
|
||||||
|
if not label_images: return
|
||||||
|
|
||||||
|
# Concatenate
|
||||||
max_w = max(i.width for i in label_images)
|
max_w = max(i.width for i in label_images)
|
||||||
total_h = sum(i.height 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")
|
concat_img = Image.new("RGB", (max_w, total_h), "white")
|
||||||
|
|
@ -212,11 +141,14 @@ def process_student(args):
|
||||||
final_json_map = []
|
final_json_map = []
|
||||||
current_y = 0
|
current_y = 0
|
||||||
|
|
||||||
for label_name, img, boxes in zip(processed_labels_order, label_images, student_checkboxes):
|
for img, boxes in zip(label_images, all_checkboxes):
|
||||||
concat_img.paste(img, (0, current_y))
|
concat_img.paste(img, (0, current_y))
|
||||||
|
|
||||||
|
# Adjust coordinates for concatenated image
|
||||||
for item in boxes:
|
for item in boxes:
|
||||||
b = item['final_box']
|
# item might have 'rel_box' (header) or 'final_box' (local)
|
||||||
|
# Both were relative to the label image. We just add current_y.
|
||||||
|
b = item.get('final_box') or item.get('rel_box')
|
||||||
item['global_box'] = [b[0], b[1] + current_y, b[2], b[3] + current_y]
|
item['global_box'] = [b[0], b[1] + current_y, b[2], b[3] + current_y]
|
||||||
final_json_map.append(item)
|
final_json_map.append(item)
|
||||||
|
|
||||||
|
|
@ -254,7 +186,6 @@ def process_student(args):
|
||||||
# concat_img.save(os.path.join(output_dir, "Concat.pdf"), "PDF", resolution=72.0)
|
# concat_img.save(os.path.join(output_dir, "Concat.pdf"), "PDF", resolution=72.0)
|
||||||
# concat_img.save(os.path.join(output_dir, "Reference.jpg"))
|
# concat_img.save(os.path.join(output_dir, "Reference.jpg"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: python annotating_with_checks.py <Dir>")
|
print("Usage: python annotating_with_checks.py <Dir>")
|
||||||
|
|
|
||||||
|
|
@ -136,11 +136,7 @@ def detect_checks_and_notes(output_dir):
|
||||||
|
|
||||||
notes.putalpha(Image.fromarray(final_alpha))
|
notes.putalpha(Image.fromarray(final_alpha))
|
||||||
|
|
||||||
# Debuging : Issues
|
# notes.show()
|
||||||
# - The artifacts increase as we go lower in the image
|
|
||||||
# - Tous les rectangles sont présents
|
|
||||||
# -
|
|
||||||
notes.show()
|
|
||||||
|
|
||||||
return actions, notes
|
return actions, notes
|
||||||
|
|
||||||
|
|
@ -172,7 +168,6 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
|
||||||
actions_by_label[l].append(a)
|
actions_by_label[l].append(a)
|
||||||
|
|
||||||
for label, acts in sorted(actions_by_label.items(), key=lambda x: natural_key(x[0])):
|
for label, acts in sorted(actions_by_label.items(), key=lambda x: natural_key(x[0])):
|
||||||
# print(label)
|
|
||||||
if label not in labels: continue
|
if label not in labels: continue
|
||||||
|
|
||||||
content = labels[label]
|
content = labels[label]
|
||||||
|
|
@ -222,91 +217,22 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
|
||||||
# We use a temporary modified dictionary
|
# We use a temporary modified dictionary
|
||||||
temp_data = {student_id: labels}
|
temp_data = {student_id: labels}
|
||||||
|
|
||||||
# Run the original process (but we need to intercept it to not save, or just let it save)
|
|
||||||
|
|
||||||
# Hijack the output dir in logic or copy code?
|
|
||||||
# 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}")
|
output_dir = os.path.join(root_dir, "Bnot", f"Copie{student_id}")
|
||||||
|
|
||||||
# ... (Reuse rendering logic from annotating.py exactly) ...
|
|
||||||
# See below for condensed integration
|
|
||||||
|
|
||||||
final_concats = []
|
final_concats = []
|
||||||
|
|
||||||
for label, content in sorted(labels.items(), key=lambda x: natural_key(x[0])):
|
sorted_labels = sorted(labels.items(), key=lambda x: natural_key(x[0]))
|
||||||
|
for label, content in sorted_labels:
|
||||||
# ... [PDF to Image Conversion] ...
|
# ... [PDF to Image Conversion] ...
|
||||||
copie_folder = f"Copie{student_id}"
|
copie_folder = f"Copie{student_id}"
|
||||||
pdf_path = os.path.join(root_dir, copie_folder, f"{label}.pdf")
|
pdf_path = os.path.join(root_dir, copie_folder, f"{label}.pdf")
|
||||||
if not os.path.exists(pdf_path): continue
|
if not os.path.exists(pdf_path): continue
|
||||||
|
|
||||||
(base_img, total_h, max_w) = annotating.make_base_image(pdf_path)
|
(base_img, _total_h, _max_w) = annotating.make_base_image(pdf_path)
|
||||||
|
img = annotating.compose_label_image(
|
||||||
|
base_img, label, content['result'], content['coordinates'][0]
|
||||||
|
)
|
||||||
|
|
||||||
# ... [Draw Header/Margin (Clean)] ...
|
final_concats.append(img)
|
||||||
margin_left = MARGIN_LEFT
|
|
||||||
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), True)]
|
|
||||||
|
|
||||||
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:
|
|
||||||
render = annotating.render_latex_text(fb['text'], base_img.width)
|
|
||||||
header_imgs.append((render, "to_delete" not in fb))
|
|
||||||
|
|
||||||
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, keep) in header_imgs:
|
|
||||||
if keep:
|
|
||||||
label_img.paste(i, (0, cy))
|
|
||||||
else:
|
|
||||||
blank = Image.new("RGB", (i.width, i.height), "white")
|
|
||||||
label_img.paste(blank, (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
|
|
||||||
if "norectangle" not in fb:
|
|
||||||
draw.rectangle([xmin+margin_left, t_ymin, xmax+margin_left, t_ymax],
|
|
||||||
outline="red", width=3)
|
|
||||||
|
|
||||||
txt = annotating.render_latex_text(fb['text'], ANNOT_WIDTH,
|
|
||||||
(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")
|
|
||||||
|
|
||||||
if not "to_delete" in fb:
|
|
||||||
label_img.paste(txt, (10, int(py)), mask=txt)
|
|
||||||
last_bot = py + txt.height
|
|
||||||
|
|
||||||
final_concats.append(label_img)
|
|
||||||
|
|
||||||
# Concatenate Labels
|
# Concatenate Labels
|
||||||
if not final_concats: return
|
if not final_concats: return
|
||||||
|
|
@ -321,19 +247,11 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
|
||||||
|
|
||||||
# 3. Overlay Manual Notes
|
# 3. Overlay Manual Notes
|
||||||
if notes_layer:
|
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)
|
full_clean_img.paste(notes_layer, (0,0), mask=notes_layer)
|
||||||
|
|
||||||
# Save final Concat.jpg
|
# Save final Concat.jpg
|
||||||
final_path = os.path.join(output_dir, "Concat.jpg")
|
full_clean_img.save(os.path.join(output_dir, "Concat.jpg"))
|
||||||
full_clean_img.save(final_path)
|
print(f"Saved regenerated: {os.path.join(output_dir, 'Concat.jpg')}")
|
||||||
print(f"Saved regenerated: {final_path}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue