|
|
|
|
|
""" |
|
|
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) |