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)