""" Bounding Box & Layout Region Visualizer. Renders color-coded semi-transparent bounding boxes, labels, and confidence tags on PIL images. """ import os import io import base64 from typing import List, Optional, Tuple, Dict, Any from PIL import Image, ImageDraw, ImageFont from core.models import Region, REGION_COLORS, RegionType def get_default_font(size: int = 12) -> ImageFont.ImageFont: """Load default font or TrueType font if available.""" font_candidates = [ "arial.ttf", "segoeui.ttf", "tahoma.ttf", "DejaVuSans.ttf", "C:\\Windows\\Fonts\\arial.ttf", "C:\\Windows\\Fonts\\segoeui.ttf" ] for font_name in font_candidates: try: return ImageFont.truetype(font_name, size=size) except Exception: continue return ImageFont.load_default() def render_annotated_image( image: Image.Image, regions: List[Region], draw_labels: bool = True, draw_confidence: bool = True, draw_text_snippet: bool = False, selected_region_types: Optional[List[str]] = None, alpha_fill: int = 40 ) -> Image.Image: """ Renders high-quality color-coded bounding boxes onto the document image. Uses PIL ImageDraw with an alpha overlay layer for translucent bounding box fills. """ # Ensure working on RGBA copy for clean translucency base_img = image.convert("RGBA") overlay = Image.new("RGBA", base_img.size, (255, 255, 255, 0)) draw_base = ImageDraw.Draw(base_img) draw_overlay = ImageDraw.Draw(overlay) font = get_default_font(size=11) font_small = get_default_font(size=9) img_w, img_h = base_img.size for region in regions: # Filter if user selected specific region types if selected_region_types and region.region_type not in selected_region_types: continue box = region.box if not box or len(box) < 4: continue x1, y1, x2, y2 = [int(v) for v in box[:4]] # Clamp to image dimensions x1 = max(0, min(img_w - 1, x1)) y1 = max(0, min(img_h - 1, y1)) x2 = max(x1 + 1, min(img_w, x2)) y2 = max(y1 + 1, min(img_h, y2)) color_spec = REGION_COLORS.get(region.region_type, REGION_COLORS[RegionType.OTHER.value]) rgb = color_spec["rgb"] fill_rgba = (rgb[0], rgb[1], rgb[2], alpha_fill) border_rgba = (rgb[0], rgb[1], rgb[2], 230) # 1. Draw Translucent Fill on Overlay draw_overlay.rectangle([x1, y1, x2, y2], fill=fill_rgba) # 2. Draw Solid Crisp Border on Overlay line_width = 2 if region.region_type in [RegionType.TABLE.value, RegionType.TITLE_HEADER.value] else 1 draw_overlay.rectangle([x1, y1, x2, y2], outline=border_rgba, width=line_width) # 3. Draw Category Badge & Label Tag if draw_labels: conf_str = f" ({int(region.confidence * 100)}%)" if (draw_confidence and region.confidence is not None) else "" label_text = f"{region.region_type}{conf_str}" # Measure label size try: bbox_text = font.getbbox(label_text) tw = bbox_text[2] - bbox_text[0] th = bbox_text[3] - bbox_text[1] except Exception: tw, th = len(label_text) * 6, 12 badge_padding_x = 4 badge_padding_y = 2 badge_w = tw + badge_padding_x * 2 badge_h = th + badge_padding_y * 2 # Position badge above box or just inside if at top edge badge_y1 = y1 - badge_h if y1 >= badge_h else y1 badge_y2 = badge_y1 + badge_h badge_x1 = x1 badge_x2 = min(img_w, x1 + badge_w) # Draw Badge Background (Solid RGB) draw_overlay.rectangle([badge_x1, badge_y1, badge_x2, badge_y2], fill=(rgb[0], rgb[1], rgb[2], 240)) # Draw Badge Text (White) draw_overlay.text((badge_x1 + badge_padding_x, badge_y1 + badge_padding_y - 1), label_text, fill=(255, 255, 255, 255), font=font) # Alpha composite the overlay onto the base image annotated_rgba = Image.alpha_composite(base_img, overlay) return annotated_rgba.convert("RGB") def save_visualization( image: Image.Image, regions: List[Region], output_path: str, **kwargs ) -> str: """Renders and saves the annotated image to disk.""" os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) annotated = render_annotated_image(image, regions, **kwargs) annotated.save(output_path, "PNG", optimize=True) return output_path def image_to_base64_jpeg(image: Image.Image, quality: int = 90) -> str: """Converts a PIL image to a base64 encoded data URI.""" buf = io.BytesIO() image.convert("RGB").save(buf, format="JPEG", quality=quality) encoded = base64.b64encode(buf.getvalue()).decode("utf-8") return f"data:image/jpeg;base64,{encoded}"