""" Image Annotation Layout Generates a form interface for annotating images with: - Bounding boxes (rectangular regions) - Polygons (arbitrary shapes) - Freeform drawing (brush strokes) - Landmarks (point annotations) Uses Fabric.js for canvas-based annotation with zoom/pan support. """ import logging import json from .identifier_utils import ( safe_generate_layout, escape_html_content ) logger = logging.getLogger(__name__) # Default colors for labels if not specified DEFAULT_COLORS = [ "#FF6B6B", # Red "#4ECDC4", # Teal "#45B7D1", # Blue "#96CEB4", # Green "#FFEAA7", # Yellow "#DDA0DD", # Plum "#98D8C8", # Mint "#F7DC6F", # Gold "#BB8FCE", # Purple "#85C1E9", # Light Blue ] # Valid annotation tools VALID_TOOLS = ["bbox", "polygon", "freeform", "landmark", "fill", "eraser", "brush"] def generate_image_annotation_layout(annotation_scheme): """ Generate HTML for an image annotation interface. Args: annotation_scheme (dict): Configuration including: - name: Schema identifier - description: Display description - tools: List of tools to enable (bbox, polygon, freeform, landmark) - labels: List of label definitions with name and optional color - zoom_enabled: Whether to enable zoom (default: True) - pan_enabled: Whether to enable pan (default: True) - min_annotations: Minimum required annotations (default: 0) - max_annotations: Maximum allowed annotations (default: null/unlimited) - freeform_brush_size: Brush size for freeform tool (default: 5) - freeform_simplify: Whether to simplify freeform paths (default: True) Returns: tuple: (html_string, key_bindings) html_string: Complete HTML for the image annotation interface key_bindings: List of keyboard shortcuts Raises: ValueError: If required fields are missing or invalid """ return safe_generate_layout(annotation_scheme, _generate_image_annotation_layout_internal) def _generate_image_annotation_layout_internal(annotation_scheme): """ Internal function to generate image annotation layout after validation. """ schema_name = annotation_scheme.get('name', 'image_annotation') logger.debug(f"Generating image annotation layout for schema: {schema_name}") # Validate required fields if "labels" not in annotation_scheme: error_msg = f"Missing labels in schema: {schema_name}" logger.error(error_msg) raise ValueError(error_msg) if "tools" not in annotation_scheme: error_msg = f"Missing tools in schema: {schema_name}" logger.error(error_msg) raise ValueError(error_msg) # Validate tools tools = annotation_scheme["tools"] if not isinstance(tools, list) or not tools: error_msg = f"tools must be a non-empty list in schema: {schema_name}" logger.error(error_msg) raise ValueError(error_msg) invalid_tools = [t for t in tools if t not in VALID_TOOLS] if invalid_tools: error_msg = f"Invalid tools: {invalid_tools}. Valid tools are: {VALID_TOOLS}" logger.error(error_msg) raise ValueError(error_msg) # Process labels with colors labels = _process_labels(annotation_scheme["labels"]) # Get configuration options zoom_enabled = annotation_scheme.get("zoom_enabled", True) pan_enabled = annotation_scheme.get("pan_enabled", True) min_annotations = annotation_scheme.get("min_annotations", 0) max_annotations = annotation_scheme.get("max_annotations", None) freeform_brush_size = annotation_scheme.get("freeform_brush_size", 5) freeform_simplify = annotation_scheme.get("freeform_simplify", True) # Segmentation mask configuration brush_size = annotation_scheme.get("brush_size", 20) eraser_size = annotation_scheme.get("eraser_size", 20) mask_opacity = annotation_scheme.get("mask_opacity", 0.5) # AI support configuration ai_support = annotation_scheme.get("ai_support", {}) ai_enabled = ai_support.get("enabled", False) # source_field: Links this annotation schema to a display field from instance_display source_field = annotation_scheme.get("source_field", "") # Build config object for JavaScript js_config = { "schemaName": schema_name, "tools": tools, "labels": labels, "zoomEnabled": zoom_enabled, "panEnabled": pan_enabled, "minAnnotations": min_annotations, "maxAnnotations": max_annotations, "freeformBrushSize": freeform_brush_size, "freeformSimplify": freeform_simplify, "brushSize": brush_size, "eraserSize": eraser_size, "maskOpacity": mask_opacity, "aiSupport": ai_enabled, "aiFeatures": ai_support.get("features", {}) if ai_enabled else {}, "sourceField": source_field, } # Generate HTML html = _generate_html(annotation_scheme, js_config, schema_name, labels, tools, ai_enabled, ai_support) # Generate keybindings keybindings = _generate_keybindings(labels, tools) logger.info(f"Successfully generated image annotation layout for {schema_name}") return html, keybindings def _process_labels(labels_config): """ Process label configuration and assign colors. Args: labels_config: List of label configs (strings or dicts) Returns: List of processed label dicts with name, color, and optional key_value """ processed = [] for i, label in enumerate(labels_config): if isinstance(label, str): processed.append({ "name": label, "color": DEFAULT_COLORS[i % len(DEFAULT_COLORS)], }) elif isinstance(label, dict): processed.append({ "name": label.get("name", f"label_{i}"), "color": label.get("color", DEFAULT_COLORS[i % len(DEFAULT_COLORS)]), "key_value": label.get("key_value"), }) else: processed.append({ "name": str(label), "color": DEFAULT_COLORS[i % len(DEFAULT_COLORS)], }) return processed def _generate_html(annotation_scheme, js_config, schema_name, labels, tools, ai_enabled=False, ai_support=None): """ Generate the HTML for the image annotation interface. """ escaped_name = escape_html_content(schema_name) description = escape_html_content(annotation_scheme.get('description', '')) config_json = json.dumps(js_config) # source_field attribute for linking to display fields source_field = annotation_scheme.get("source_field", "") source_field_attr = f' data-source-field="{escape_html_content(source_field)}"' if source_field else "" # Generate tool buttons tool_buttons = _generate_tool_buttons(tools) # Generate label selector label_selector = _generate_label_selector(labels) # Generate AI toolbar if enabled ai_toolbar_html = "" ai_init_script = "" if ai_enabled: ai_features = ai_support.get("features", {}) if ai_support else {} ai_toolbar_html = _generate_ai_toolbar(ai_features) ai_init_script = _generate_ai_init_script(escaped_name) html = f'''
{description}
Tools: {tool_buttons}
Label: {label_selector}
Annotations: 0
{ai_toolbar_html}
''' return html def _generate_ai_toolbar(ai_features): """ Generate HTML for the AI assistance toolbar. """ # Determine which buttons to show based on features detection_enabled = ai_features.get("detection", True) pre_annotate_enabled = ai_features.get("pre_annotate", True) classification_enabled = ai_features.get("classification", False) hint_enabled = ai_features.get("hint", True) buttons = [] if detection_enabled: buttons.append( '' ) if pre_annotate_enabled: buttons.append( '' ) if classification_enabled: buttons.append( '' ) if hint_enabled: buttons.append( '' ) if not buttons: return "" return f'''
AI Assist: {" ".join(buttons)}
''' def _generate_ai_init_script(escaped_name): """ Generate JavaScript initialization code for AI assistant. """ return f''' // Initialize AI assistant if enabled and VisualAIAssistantManager is available if (config.aiSupport && typeof VisualAIAssistantManager !== 'undefined') {{ var annotationId = Array.from(document.querySelectorAll('.annotation-form')).indexOf( document.getElementById('{escaped_name}') ); container.aiAssistant = new VisualAIAssistantManager({{ annotationType: 'image_annotation', annotationId: annotationId >= 0 ? annotationId : 0, annotationManager: manager }}); }} ''' def _generate_tool_buttons(tools): """ Generate HTML for tool selection buttons. """ tool_info = { "bbox": {"label": "Box", "title": "Bounding Box (B)", "icon": "□"}, "polygon": {"label": "Polygon", "title": "Polygon (P)", "icon": "⬡"}, "freeform": {"label": "Draw", "title": "Freeform Draw (F)", "icon": "✎"}, "landmark": {"label": "Point", "title": "Landmark Point (L)", "icon": "◉"}, "brush": {"label": "Brush", "title": "Segmentation Brush (M)", "icon": "🖌️"}, "fill": {"label": "Fill", "title": "Flood Fill (G)", "icon": "🪣"}, "eraser": {"label": "Eraser", "title": "Eraser (E)", "icon": "⌫"}, } buttons = [] for tool in tools: info = tool_info.get(tool, {"label": tool, "title": tool, "icon": "?"}) buttons.append( f'' ) return "\n".join(buttons) def _generate_label_selector(labels): """ Generate HTML for label selection buttons. """ buttons = [] for label in labels: name = escape_html_content(label["name"]) color = label["color"] key_hint = f' ({label["key_value"]})' if label.get("key_value") else "" buttons.append( f'' ) return "\n".join(buttons) def _generate_keybindings(labels, tools): """ Generate keybinding list for the schema. """ keybindings = [] # Tool shortcuts tool_keys = { "bbox": ("b", "Bounding Box tool"), "polygon": ("p", "Polygon tool"), "freeform": ("f", "Freeform draw tool"), "landmark": ("l", "Landmark point tool"), "brush": ("m", "Segmentation brush tool"), "fill": ("g", "Flood fill tool"), "eraser": ("e", "Eraser tool"), } for tool in tools: if tool in tool_keys: keybindings.append(tool_keys[tool]) # Label shortcuts for label in labels: if label.get("key_value"): keybindings.append((label["key_value"], f"Select label: {label['name']}")) # Common shortcuts keybindings.extend([ ("Del", "Delete selected"), ("+/-", "Zoom in/out"), ("0", "Fit to view"), ]) return keybindings