File size: 10,547 Bytes
83e35a7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
#!/usr/bin/env python3
"""
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)
# 1. Detect faces
face_regions = self._detect_faces(image)
# 2. Find empty areas (low variance regions)
empty_areas = self._find_empty_areas(gray)
# 3. Find busy areas (high variance regions)
busy_areas = self._find_busy_areas(gray)
# 4. Find edge positions
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:
# Use modern face detector with image object
try:
# Convert image to format expected by face detector
if isinstance(image, str):
# If it's a file path, read the image
img = cv2.imread(image)
else:
# If it's already an image object
img = image
faces = self.face_detector.detect_faces_opencv(img)
face_regions = []
for face in faces:
if face != (-1, -1):
# Create face region around detected point
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:
# Fallback to basic face detection
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)"""
# Calculate local variance
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
# Find low variance regions
threshold = np.percentile(variance, 20) # Bottom 20% variance
low_var_mask = variance < threshold
# Find connected components
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: # Minimum area
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)"""
# Calculate local variance
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
# Find high variance regions
threshold = np.percentile(variance, 80) # Top 20% variance
high_var_mask = variance > threshold
# Find connected components
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: # Minimum area
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 = []
# Top edge
for x in range(margin, width - margin, 100):
edge_positions.append((x, margin))
# Right edge
for y in range(margin, height - margin, 100):
edge_positions.append((width - margin, y))
# Top-right corner area
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
"""
# Analyze image content
analysis = self.analyze_image_content(image_path)
# Get panel dimensions
left, right, top, bottom = panel_coords
panel_width = right - left
panel_height = bottom - top
# Generate candidate positions
candidates = []
# 1. Edge positions (highest priority)
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)) # High score
# 2. Empty areas (good for bubbles)
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)) # Good score
# 3. Upper area positions (preferred)
upper_y = top + panel_height * 0.2 # 20% from top
for x in range(left + 50, right - 50, 100):
candidates.append((x, upper_y, 70)) # Medium score
# 4. Corner positions
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)) # Lower score
# Filter out positions that overlap with faces or busy areas
filtered_candidates = []
for x, y, score in candidates:
# Check if position overlaps with face regions
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
# Check if position overlaps with busy areas
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
# Check distance from lip if provided
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: # 80px minimum distance
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))
# Select best position
if filtered_candidates:
# Sort by score (highest first)
filtered_candidates.sort(key=lambda x: x[2], reverse=True)
best_x, best_y, _ = filtered_candidates[0]
return (best_x, best_y)
else:
# Fallback to upper center
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) |