Spaces:
Sleeping
Sleeping
| """ | |
| Debug visualization utilities. | |
| This module handles: | |
| - Credit card overlay | |
| - Finger contour and axis visualization | |
| - Ring zone highlighting | |
| - Cross-section measurement display | |
| - Result annotation | |
| """ | |
| import cv2 | |
| import numpy as np | |
| from typing import Dict, Any, Optional, List, Tuple | |
| # Import shared visualization constants | |
| from .viz_constants import ( | |
| FONT_FACE, | |
| Color, | |
| FontScale, | |
| FontThickness, | |
| Size, | |
| Layout, | |
| get_scaled_font_size, | |
| ) | |
| # Font scaling parameters (specific to final visualization) | |
| FONT_BASE_SCALE = FontScale.BODY # Base font scale at reference height | |
| FONT_REFERENCE_HEIGHT = 1200 # Reference image height for font scaling | |
| FONT_MIN_SCALE = FontScale.BODY # Minimum font scale regardless of image size | |
| def get_scaled_font_params(image_height: int) -> Dict[str, float]: | |
| """ | |
| Calculate font parameters scaled to image dimensions. | |
| Args: | |
| image_height: Height of the image in pixels | |
| Returns: | |
| Dictionary containing scaled font parameters | |
| """ | |
| font_scale = max(FONT_MIN_SCALE, image_height / FONT_REFERENCE_HEIGHT) | |
| scale_factor = font_scale / FONT_BASE_SCALE | |
| return { | |
| "font_scale": font_scale, | |
| "text_thickness": int(FontThickness.BODY * scale_factor), | |
| "line_thickness": int(Size.LINE_THICK * scale_factor), | |
| "contour_thickness": int(Size.CONTOUR_THICK * scale_factor), | |
| "corner_radius": int(Size.CORNER_RADIUS * scale_factor), | |
| "endpoint_radius": int(Size.ENDPOINT_RADIUS * scale_factor), | |
| "intersection_radius": int(Size.INTERSECTION_RADIUS * scale_factor), | |
| "text_offset": int(Layout.TEXT_OFFSET_Y * scale_factor), | |
| "label_offset": int(Layout.LABEL_OFFSET * scale_factor), | |
| "line_height": int(Layout.RESULT_TEXT_LINE_HEIGHT * scale_factor), | |
| "y_start": int(Layout.RESULT_TEXT_Y_START * scale_factor), | |
| "x_offset": int(Layout.RESULT_TEXT_X_OFFSET * scale_factor), | |
| } | |
| def create_debug_visualization( | |
| image: np.ndarray, | |
| card_result: Optional[Dict[str, Any]] = None, | |
| contour: Optional[np.ndarray] = None, | |
| axis_data: Optional[Dict[str, Any]] = None, | |
| zone_data: Optional[Dict[str, Any]] = None, | |
| width_data: Optional[Dict[str, Any]] = None, | |
| measurement_cm: Optional[float] = None, | |
| confidence: Optional[float] = None, | |
| scale_px_per_cm: Optional[float] = None, | |
| ) -> np.ndarray: | |
| """ | |
| Create debug visualization overlay on original image. | |
| Args: | |
| image: Original BGR image | |
| card_result: Credit card detection result | |
| contour: Finger contour points | |
| axis_data: Finger axis data | |
| zone_data: Ring zone data | |
| width_data: Width measurement data | |
| measurement_cm: Final measurement in cm | |
| confidence: Overall confidence score | |
| scale_px_per_cm: Scale factor | |
| Returns: | |
| Annotated BGR image | |
| """ | |
| # Create a copy for drawing | |
| vis = image.copy() | |
| # Draw credit card overlay | |
| if card_result is not None: | |
| vis = draw_card_overlay(vis, card_result, scale_px_per_cm) | |
| # Draw finger contour and axis | |
| if contour is not None: | |
| vis = draw_finger_contour(vis, contour) | |
| if axis_data is not None: | |
| vis = draw_finger_axis(vis, axis_data) | |
| # Draw ring zone | |
| if zone_data is not None and axis_data is not None: | |
| vis = draw_ring_zone(vis, zone_data, axis_data) | |
| # Draw cross-section measurements | |
| if width_data is not None and zone_data is not None: | |
| vis = draw_cross_sections(vis, width_data) | |
| # Add measurement annotation with JSON information | |
| if measurement_cm is not None and confidence is not None: | |
| vis = add_measurement_text( | |
| vis, | |
| measurement_cm, | |
| confidence, | |
| scale_px_per_cm=scale_px_per_cm, | |
| card_detected=card_result is not None, | |
| finger_detected=contour is not None, | |
| view_angle_ok=True, # This is passed from caller | |
| ) | |
| return vis | |
| def draw_card_overlay( | |
| image: np.ndarray, | |
| card_result: Dict[str, Any], | |
| scale_px_per_cm: Optional[float] = None, | |
| ) -> np.ndarray: | |
| """Draw credit card detection overlay.""" | |
| corners = card_result["corners"].astype(np.int32) | |
| params = get_scaled_font_params(image.shape[0]) | |
| # Draw quadrilateral | |
| cv2.polylines(image, [corners], isClosed=True, color=Color.CARD, | |
| thickness=params["contour_thickness"]) | |
| # Draw corner points with labels | |
| corner_labels = ["TL", "TR", "BR", "BL"] | |
| for corner, label in zip(corners, corner_labels): | |
| cv2.circle(image, tuple(corner), params["corner_radius"], Color.CARD, -1) | |
| cv2.putText( | |
| image, | |
| label, | |
| tuple(corner + np.array([params["label_offset"], -params["label_offset"]])), | |
| FONT_FACE, | |
| params["font_scale"], | |
| Color.CARD, | |
| params["text_thickness"], | |
| ) | |
| # Add scale annotation | |
| if scale_px_per_cm is not None: | |
| center = np.mean(corners, axis=0).astype(np.int32) | |
| text = f"Card: {scale_px_per_cm:.1f} px/cm" | |
| cv2.putText( | |
| image, | |
| text, | |
| tuple(center), | |
| FONT_FACE, | |
| params["font_scale"] * 1.2, | |
| Color.CARD, | |
| params["text_thickness"], | |
| ) | |
| return image | |
| def draw_finger_contour( | |
| image: np.ndarray, | |
| contour: np.ndarray, | |
| ) -> np.ndarray: | |
| """Draw finger contour.""" | |
| params = get_scaled_font_params(image.shape[0]) | |
| contour_int = contour.astype(np.int32).reshape((-1, 1, 2)) | |
| cv2.polylines(image, [contour_int], isClosed=True, color=Color.FINGER, | |
| thickness=params["contour_thickness"]) | |
| return image | |
| def draw_finger_axis( | |
| image: np.ndarray, | |
| axis_data: Dict[str, Any], | |
| ) -> np.ndarray: | |
| """Draw finger axis line.""" | |
| palm_end = axis_data["palm_end"].astype(np.int32) | |
| tip_end = axis_data["tip_end"].astype(np.int32) | |
| params = get_scaled_font_params(image.shape[0]) | |
| # Draw axis line | |
| cv2.line(image, tuple(palm_end), tuple(tip_end), Color.AXIS_LINE, | |
| params["line_thickness"]) | |
| # Mark endpoints | |
| cv2.circle(image, tuple(palm_end), params["endpoint_radius"], Color.AXIS_PALM, -1) | |
| cv2.circle(image, tuple(tip_end), params["endpoint_radius"], Color.AXIS_TIP, -1) | |
| # Add labels | |
| cv2.putText( | |
| image, | |
| "Palm", | |
| tuple(palm_end + np.array([params["text_offset"], params["text_offset"]])), | |
| FONT_FACE, | |
| params["font_scale"], | |
| Color.AXIS_PALM, | |
| params["text_thickness"], | |
| ) | |
| cv2.putText( | |
| image, | |
| "Tip", | |
| tuple(tip_end + np.array([params["text_offset"], params["text_offset"]])), | |
| FONT_FACE, | |
| params["font_scale"], | |
| Color.AXIS_TIP, | |
| params["text_thickness"], | |
| ) | |
| return image | |
| def draw_ring_zone( | |
| image: np.ndarray, | |
| zone_data: Dict[str, Any], | |
| axis_data: Dict[str, Any], | |
| ) -> np.ndarray: | |
| """Draw ring-wearing zone band.""" | |
| direction = axis_data["direction"] | |
| perp = np.array([-direction[1], direction[0]], dtype=np.float32) | |
| start_point = zone_data["start_point"] | |
| end_point = zone_data["end_point"] | |
| # Create zone band (perpendicular lines at start and end) | |
| # Make the band wide enough to be visible | |
| band_width = 200 # pixels | |
| start_left = start_point + perp * band_width | |
| start_right = start_point - perp * band_width | |
| end_left = end_point + perp * band_width | |
| end_right = end_point - perp * band_width | |
| # Draw zone band as a semi-transparent overlay | |
| overlay = image.copy() | |
| zone_poly = np.array([start_left, start_right, end_right, end_left], dtype=np.int32) | |
| cv2.fillPoly(overlay, [zone_poly], Color.RING_ZONE) | |
| cv2.addWeighted(overlay, 0.2, image, 0.8, 0, image) | |
| # Draw zone boundaries | |
| params = get_scaled_font_params(image.shape[0]) | |
| cv2.line( | |
| image, | |
| tuple(start_left.astype(np.int32)), | |
| tuple(start_right.astype(np.int32)), | |
| Color.RING_ZONE, | |
| params["line_thickness"], | |
| ) | |
| cv2.line( | |
| image, | |
| tuple(end_left.astype(np.int32)), | |
| tuple(end_right.astype(np.int32)), | |
| Color.RING_ZONE, | |
| params["line_thickness"], | |
| ) | |
| # Add zone label | |
| label_offset = int(40 * params["font_scale"] / FONT_BASE_SCALE) | |
| label_pos = zone_data["center_point"].astype(np.int32) + np.array([band_width + label_offset, 0], dtype=np.int32) | |
| cv2.putText( | |
| image, | |
| "Ring Zone", | |
| tuple(label_pos), | |
| FONT_FACE, | |
| params["font_scale"] * 1.2, | |
| Color.RING_ZONE, | |
| params["text_thickness"], | |
| ) | |
| return image | |
| def draw_cross_sections( | |
| image: np.ndarray, | |
| width_data: Dict[str, Any], | |
| ) -> np.ndarray: | |
| """Draw cross-section sample lines and intersection points.""" | |
| params = get_scaled_font_params(image.shape[0]) | |
| sample_points = width_data.get("sample_points", []) | |
| for left, right in sample_points: | |
| left_int = tuple(np.array(left, dtype=np.int32)) | |
| right_int = tuple(np.array(right, dtype=np.int32)) | |
| # Draw cross-section line | |
| cv2.line(image, left_int, right_int, Color.CROSS_SECTION, | |
| max(2, params["line_thickness"] // 2)) | |
| # Draw intersection points | |
| cv2.circle(image, left_int, params["intersection_radius"], Color.POINT, -1) | |
| cv2.circle(image, right_int, params["intersection_radius"], Color.POINT, -1) | |
| return image | |
| def add_measurement_text( | |
| image: np.ndarray, | |
| measurement_cm: float, | |
| confidence: float, | |
| scale_px_per_cm: Optional[float] = None, | |
| card_detected: bool = True, | |
| finger_detected: bool = True, | |
| view_angle_ok: bool = True, | |
| ) -> np.ndarray: | |
| """Add measurement result text overlay with JSON information.""" | |
| h, w = image.shape[:2] | |
| # Create larger semi-transparent background for more text | |
| overlay = image.copy() | |
| cv2.rectangle(overlay, (10, 10), (1100, 550), (0, 0, 0), -1) | |
| cv2.addWeighted(overlay, 0.7, image, 0.3, 0, image) | |
| # Confidence level indicator | |
| if confidence > 0.85: | |
| level = "HIGH" | |
| level_color = Color.TEXT_SUCCESS | |
| elif confidence >= 0.6: | |
| level = "MEDIUM" | |
| level_color = (0, 255, 255) # Yellow | |
| else: | |
| level = "LOW" | |
| level_color = Color.TEXT_ERROR | |
| # Build text lines with JSON information | |
| text_lines = [ | |
| ("=== MEASUREMENT RESULT ===", Color.TEXT_PRIMARY, False), | |
| (f"Finger Diameter: {measurement_cm:.2f} cm", Color.TEXT_PRIMARY, False), | |
| (f"Confidence: {confidence:.3f} ({level})", level_color, True), | |
| ("", Color.TEXT_PRIMARY, False), # Empty line | |
| ("=== QUALITY FLAGS ===", Color.TEXT_PRIMARY, False), | |
| (f"Card Detected: {'YES' if card_detected else 'NO'}", Color.TEXT_SUCCESS if card_detected else Color.TEXT_ERROR, False), | |
| (f"Finger Detected: {'YES' if finger_detected else 'NO'}", Color.TEXT_SUCCESS if finger_detected else Color.TEXT_ERROR, False), | |
| (f"View Angle OK: {'YES' if view_angle_ok else 'NO'}", Color.TEXT_SUCCESS if view_angle_ok else Color.TEXT_ERROR, False), | |
| ] | |
| # Add scale information if available | |
| if scale_px_per_cm is not None: | |
| text_lines.insert(3, (f"Scale: {scale_px_per_cm:.2f} px/cm", Color.TEXT_PRIMARY, False)) | |
| # Get scaled font parameters | |
| params = get_scaled_font_params(image.shape[0]) | |
| for i, (text, color, is_bold) in enumerate(text_lines): | |
| if text: # Skip empty lines for drawing | |
| thickness = params["text_thickness"] + 1 if is_bold else params["text_thickness"] | |
| cv2.putText( | |
| image, | |
| text, | |
| (params["x_offset"], params["y_start"] + i * params["line_height"]), | |
| FONT_FACE, | |
| params["font_scale"], | |
| color, | |
| thickness, | |
| ) | |
| return image | |