Refactor of all annotating things.
This commit is contained in:
+77
-146
@@ -37,145 +37,62 @@ def draw_checkbox(draw, x, y, size=BOX_SIZE, label=None, fill="white"):
|
||||
|
||||
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."""
|
||||
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):
|
||||
"""Generates the score line and global feedback elements."""
|
||||
elements = []
|
||||
class CheckboxRenderer:
|
||||
def __init__(self, label_name):
|
||||
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))
|
||||
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:
|
||||
box = draw_checkbox(draw, start_x, pos['y'] + 5, BOX_SIZE, str(val))
|
||||
self.checkboxes.append({
|
||||
"type": "score", "label": self.label, "value": val,
|
||||
"rel_box": box # Will be adjusted for global Y later
|
||||
})
|
||||
start_x += BOX_SIZE + 60
|
||||
elif meta.get("type") == "global_fb":
|
||||
# Draw delete box for global feedback
|
||||
bx = pos['w'] - BOX_SIZE - 5
|
||||
by = pos['y'] + 5
|
||||
box = draw_checkbox(draw, bx, by, BOX_SIZE)
|
||||
self.checkboxes.append({
|
||||
"type": "del_global", "label": self.label, "index": meta["index"],
|
||||
"rel_box": box, "text_preview": meta["data"]["text"][:20]
|
||||
})
|
||||
|
||||
draw_score = ImageDraw.Draw(score_line_img)
|
||||
start_x = score_text_img.width + 20
|
||||
local_boxes = []
|
||||
elif kind == "local_rect":
|
||||
# Delete rect checkbox
|
||||
b = pos['box'] # [xmin, ymin, xmax, ymax]
|
||||
box = draw_checkbox(draw, b[2] - BOX_SIZE, b[1], BOX_SIZE)
|
||||
self.checkboxes.append({
|
||||
"type": "del_local_rect", "label": self.label, "index": meta["index"],
|
||||
"final_box": box, "text_preview": meta["data"]["text"][:20]
|
||||
})
|
||||
|
||||
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, 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,
|
||||
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
|
||||
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
|
||||
def natural_key(text):
|
||||
@@ -192,19 +109,31 @@ def process_student(args):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
label_images = []
|
||||
student_checkboxes = []
|
||||
processed_labels_order = []
|
||||
all_checkboxes = []
|
||||
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])):
|
||||
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)
|
||||
for label, content in sorted_labels:
|
||||
pdf_path = content['pdf_path']
|
||||
if not os.path.exists(pdf_path): continue
|
||||
|
||||
if not label_images:
|
||||
return
|
||||
base_img, _, _ = annotating.make_base_image(pdf_path)
|
||||
|
||||
# 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)
|
||||
total_h = sum(i.height for i in label_images)
|
||||
concat_img = Image.new("RGB", (max_w, total_h), "white")
|
||||
@@ -212,11 +141,14 @@ def process_student(args):
|
||||
final_json_map = []
|
||||
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))
|
||||
|
||||
# Adjust coordinates for concatenated image
|
||||
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]
|
||||
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, "Reference.jpg"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python annotating_with_checks.py <Dir>")
|
||||
|
||||
Reference in New Issue
Block a user