| |
| """ |
| Smart Bubble Placement System |
| Uses image analysis to find optimal bubble positions without CAM data |
| """ |
|
|
| import cv2 |
| import numpy as np |
| import math |
| from typing import Tuple, List, Optional |
| from backend.utils import get_panel_type, types |
|
|
| BUBBLE_WIDTH = 200 |
| BUBBLE_HEIGHT = 94 |
|
|
| class SmartBubblePlacer: |
| def __init__(self): |
| """Initialize smart bubble placement system""" |
| self.face_detector = None |
| try: |
| from backend.speech_bubble.modern_face_detection import ModernFaceDetector |
| self.face_detector = ModernFaceDetector() |
| except ImportError: |
| print("Modern face detector not available, using basic placement") |
| |
| def analyze_image_content(self, image_path: str) -> dict: |
| """ |
| Analyze image content to find optimal bubble placement areas |
| Returns: { |
| 'face_regions': [(x, y, w, h), ...], |
| 'empty_areas': [(x, y, w, h), ...], |
| 'busy_areas': [(x, y, w, h), ...], |
| 'edges': [(x, y), ...] |
| } |
| """ |
| image = cv2.imread(image_path) |
| if image is None: |
| return {'face_regions': [], 'empty_areas': [], 'busy_areas': [], 'edges': []} |
| |
| height, width = image.shape[:2] |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
| |
| |
| face_regions = self._detect_faces(image) |
| |
| |
| empty_areas = self._find_empty_areas(gray) |
| |
| |
| busy_areas = self._find_busy_areas(gray) |
| |
| |
| edges = self._find_edge_positions(width, height) |
| |
| return { |
| 'face_regions': face_regions, |
| 'empty_areas': empty_areas, |
| 'busy_areas': busy_areas, |
| 'edges': edges |
| } |
| |
| def _detect_faces(self, image) -> List[Tuple[int, int, int, int]]: |
| """Detect face regions in image""" |
| if self.face_detector: |
| |
| try: |
| |
| if isinstance(image, str): |
| |
| img = cv2.imread(image) |
| else: |
| |
| img = image |
| |
| faces = self.face_detector.detect_faces_opencv(img) |
| face_regions = [] |
| for face in faces: |
| if face != (-1, -1): |
| |
| x, y = face |
| face_regions.append((x-50, y-50, 100, 100)) |
| return face_regions |
| except Exception as e: |
| print(f"Face detection error: {e}") |
| return [] |
| else: |
| |
| try: |
| if isinstance(image, str): |
| img = cv2.imread(image) |
| else: |
| img = image |
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') |
| faces = face_cascade.detectMultiScale(gray, 1.1, 4) |
| return [(x, y, w, h) for (x, y, w, h) in faces] |
| except Exception as e: |
| print(f"Fallback face detection error: {e}") |
| return [] |
| |
| def _find_empty_areas(self, gray_image) -> List[Tuple[int, int, int, int]]: |
| """Find areas with low variance (good for bubbles)""" |
| |
| kernel = np.ones((20, 20), np.float32) / 400 |
| mean = cv2.filter2D(gray_image.astype(np.float32), -1, kernel) |
| mean_sq = cv2.filter2D((gray_image.astype(np.float32))**2, -1, kernel) |
| variance = mean_sq - mean**2 |
| |
| |
| threshold = np.percentile(variance, 20) |
| low_var_mask = variance < threshold |
| |
| |
| num_labels, labels = cv2.connectedComponents(low_var_mask.astype(np.uint8)) |
| |
| empty_areas = [] |
| for label in range(1, num_labels): |
| mask = labels == label |
| if np.sum(mask) > 1000: |
| y_coords, x_coords = np.where(mask) |
| x_min, x_max = x_coords.min(), x_coords.max() |
| y_min, y_max = y_coords.min(), y_coords.max() |
| empty_areas.append((x_min, y_min, x_max-x_min, y_max-y_min)) |
| |
| return empty_areas |
| |
| def _find_busy_areas(self, gray_image) -> List[Tuple[int, int, int, int]]: |
| """Find areas with high variance (avoid for bubbles)""" |
| |
| kernel = np.ones((20, 20), np.float32) / 400 |
| mean = cv2.filter2D(gray_image.astype(np.float32), -1, kernel) |
| mean_sq = cv2.filter2D((gray_image.astype(np.float32))**2, -1, kernel) |
| variance = mean_sq - mean**2 |
| |
| |
| threshold = np.percentile(variance, 80) |
| high_var_mask = variance > threshold |
| |
| |
| num_labels, labels = cv2.connectedComponents(high_var_mask.astype(np.uint8)) |
| |
| busy_areas = [] |
| for label in range(1, num_labels): |
| mask = labels == label |
| if np.sum(mask) > 500: |
| y_coords, x_coords = np.where(mask) |
| x_min, x_max = x_coords.min(), x_coords.max() |
| y_min, y_max = y_coords.min(), y_coords.max() |
| busy_areas.append((x_min, y_min, x_max-x_min, y_max-y_min)) |
| |
| return busy_areas |
| |
| def _find_edge_positions(self, width: int, height: int) -> List[Tuple[int, int]]: |
| """Find good edge positions for bubbles""" |
| margin = 50 |
| edge_positions = [] |
| |
| |
| for x in range(margin, width - margin, 100): |
| edge_positions.append((x, margin)) |
| |
| |
| for y in range(margin, height - margin, 100): |
| edge_positions.append((width - margin, y)) |
| |
| |
| corner_margin = 80 |
| for x in range(width - corner_margin - 100, width - corner_margin, 50): |
| for y in range(margin, margin + 100, 50): |
| edge_positions.append((x, y)) |
| |
| return edge_positions |
| |
| def get_optimal_bubble_position(self, image_path: str, panel_coords: Tuple[int, int, int, int], |
| lip_coords: Optional[Tuple[int, int]] = None) -> Tuple[int, int]: |
| """ |
| Find optimal bubble position based on image analysis |
| """ |
| |
| analysis = self.analyze_image_content(image_path) |
| |
| |
| left, right, top, bottom = panel_coords |
| panel_width = right - left |
| panel_height = bottom - top |
| |
| |
| candidates = [] |
| |
| |
| for edge_x, edge_y in analysis['edges']: |
| if (left <= edge_x <= right and top <= edge_y <= bottom): |
| candidates.append((edge_x, edge_y, 100)) |
| |
| |
| for x, y, w, h in analysis['empty_areas']: |
| center_x = x + w // 2 |
| center_y = y + h // 2 |
| if (left <= center_x <= right and top <= center_y <= bottom): |
| candidates.append((center_x, center_y, 80)) |
| |
| |
| upper_y = top + panel_height * 0.2 |
| for x in range(left + 50, right - 50, 100): |
| candidates.append((x, upper_y, 70)) |
| |
| |
| corner_margin = 40 |
| corners = [ |
| (left + corner_margin, top + corner_margin), |
| (right - corner_margin, top + corner_margin), |
| (left + corner_margin, top + panel_height * 0.3), |
| (right - corner_margin, top + panel_height * 0.3) |
| ] |
| for x, y in corners: |
| candidates.append((x, y, 60)) |
| |
| |
| filtered_candidates = [] |
| for x, y, score in candidates: |
| |
| overlaps_face = False |
| for fx, fy, fw, fh in analysis['face_regions']: |
| if (fx <= x <= fx + fw and fy <= y <= fy + fh): |
| overlaps_face = True |
| break |
| |
| |
| overlaps_busy = False |
| for bx, by, bw, bh in analysis['busy_areas']: |
| if (bx <= x <= bx + bw and by <= y <= by + bh): |
| overlaps_busy = True |
| break |
| |
| |
| too_close_to_lip = False |
| if lip_coords and lip_coords != (-1, -1): |
| lip_x, lip_y = lip_coords |
| distance = math.sqrt((x - lip_x)**2 + (y - lip_y)**2) |
| if distance < 80: |
| too_close_to_lip = True |
| |
| if not overlaps_face and not overlaps_busy and not too_close_to_lip: |
| filtered_candidates.append((x, y, score)) |
| |
| |
| if filtered_candidates: |
| |
| filtered_candidates.sort(key=lambda x: x[2], reverse=True) |
| best_x, best_y, _ = filtered_candidates[0] |
| return (best_x, best_y) |
| else: |
| |
| return (left + panel_width // 2, top + panel_height * 0.2) |
|
|
| def get_smart_bubble_position(image_path: str, panel_coords: Tuple[int, int, int, int], |
| lip_coords: Optional[Tuple[int, int]] = None) -> Tuple[int, int]: |
| """Main function to get smart bubble position""" |
| placer = SmartBubblePlacer() |
| return placer.get_optimal_bubble_position(image_path, panel_coords, lip_coords) |