lsatone / backend /ai_bubble_placement.py
3v324v23's picture
Update Comic123 with local comic folder files
83e35a7
"""
AI-Powered Speech Bubble Placement System
Simplified and robust bubble positioning.
"""
import cv2
from typing import Tuple, Optional
class AIBubblePlacer:
"""
AIBubblePlacer finds the best position for a speech bubble.
This version uses a simpler, more reliable heuristic-based approach.
"""
def __init__(self):
# These values are based on a panel size of 300x200
self.bubble_width = 160
self.bubble_height = 80
self.panel_width = 300
self.panel_height = 200
self.padding = 10 # Minimum distance from the panel edge
def place_bubble_ai(self, image_path: str, lip_coords: Optional[Tuple[int, int]] = None) -> Tuple[int, int]:
"""
Determines the optimal placement for a speech bubble.
The strategy is:
1. If a face is detected, try to place the bubble above the face.
2. If that's not possible, try other corners (top-left, top-right).
3. If no face is found, analyze the image for the quietest corner.
4. Always ensure the bubble stays within the panel boundaries.
"""
try:
image = cv2.imread(image_path)
if image is None:
return (50, 20) # Fallback
# If a primary speaker is identified (lip_coords are valid)
if lip_coords and lip_coords != (-1, -1):
lip_x, lip_y = lip_coords
# --- Primary Strategy: Place bubble ABOVE the speaker's head ---
# Center the bubble horizontally over the lips
ideal_x = lip_x - (self.bubble_width // 2)
# Place it well above the lips to clear the head
ideal_y = lip_y - self.bubble_height - 40 # 40px buffer
# Check if this position is valid (within panel bounds)
if ideal_y > self.padding:
final_x = self._clamp(ideal_x, self.padding, self.panel_width - self.bubble_width - self.padding)
final_y = self._clamp(ideal_y, self.padding, self.panel_height - self.bubble_height - self.padding)
return (int(final_x), int(final_y))
# --- Fallback Strategy: Find the best corner if no face or space above is poor ---
return self._find_best_corner(image)
except Exception as e:
print(f"ERROR in AI bubble placer: {e}")
return (50, 20) # Final fallback
def _clamp(self, value, min_value, max_value):
"""Helper function to keep a value within a specific range."""
return max(min_value, min(value, max_value))
def _get_region_clarity(self, image, rect):
"""Calculates the 'clarity' of a region (low edge count is clearer)."""
x, y, w, h = rect
roi = image[y:y+h, x:x+w]
if roi.size == 0:
return float('inf') # Invalid region
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray_roi, 100, 200)
return np.sum(edges == 0) # Return count of non-edge pixels
def _find_best_corner(self, image):
"""Analyzes the four corners of the image to find the least busy one."""
h, w, _ = image.shape
# Define the four corner regions where a bubble could go
corner_regions = {
"top_left": (self.padding, self.padding, self.bubble_width, self.bubble_height),
"top_right": (w - self.bubble_width - self.padding, self.padding, self.bubble_width, self.bubble_height),
"bottom_left": (self.padding, h - self.bubble_height - self.padding, self.bubble_width, self.bubble_height),
"bottom_right": (w - self.bubble_width - self.padding, h - self.bubble_height - self.padding, self.bubble_width, self.bubble_height)
}
best_corner_name = None
max_clarity = -1
for name, rect in corner_regions.items():
clarity = self._get_region_clarity(image, rect)
if clarity > max_clarity:
max_clarity = clarity
best_corner_name = name
# Return the top-left coordinate of the best corner
best_rect = corner_regions.get(best_corner_name, ("top_left", (self.padding, self.padding)))
return (best_rect[0], best_rect[1])
# Global instance
ai_bubble_placer = AIBubblePlacer()