RealComic / backend /speech_bubble /smart_bubble_placement.py
3v324v23's picture
Update Comic123 with local comic folder files
83e35a7
#!/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)