|
|
|
|
|
"""
|
|
|
Bubble Detection Module - Optimized
|
|
|
===================================
|
|
|
|
|
|
Uses YOLO (You Only Look Once) model to detect text bubbles in manga/comic images
|
|
|
with advanced filtering to prevent duplicates and overlapping detections.
|
|
|
|
|
|
Features:
|
|
|
- Confidence threshold filtering
|
|
|
- Non-Maximum Suppression (NMS)
|
|
|
- Overlap filtering
|
|
|
- Size-based filtering
|
|
|
|
|
|
Author: MangaTranslator Team
|
|
|
License: MIT
|
|
|
"""
|
|
|
|
|
|
import torch.serialization
|
|
|
from ultralytics import YOLO
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
def calculate_iou(box1, box2):
|
|
|
"""
|
|
|
Calculate Intersection over Union (IoU) between two bounding boxes
|
|
|
|
|
|
Args:
|
|
|
box1, box2: [x1, y1, x2, y2, confidence, class_id]
|
|
|
|
|
|
Returns:
|
|
|
float: IoU value between 0 and 1
|
|
|
"""
|
|
|
|
|
|
x1_1, y1_1, x2_1, y2_1 = box1[:4]
|
|
|
x1_2, y1_2, x2_2, y2_2 = box2[:4]
|
|
|
|
|
|
|
|
|
x1_inter = max(x1_1, x1_2)
|
|
|
y1_inter = max(y1_1, y1_2)
|
|
|
x2_inter = min(x2_1, x2_2)
|
|
|
y2_inter = min(y2_1, y2_2)
|
|
|
|
|
|
if x2_inter <= x1_inter or y2_inter <= y1_inter:
|
|
|
return 0.0
|
|
|
|
|
|
intersection_area = (x2_inter - x1_inter) * (y2_inter - y1_inter)
|
|
|
|
|
|
|
|
|
area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
|
|
|
area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
|
|
|
union_area = area1 + area2 - intersection_area
|
|
|
|
|
|
return intersection_area / union_area if union_area > 0 else 0.0
|
|
|
|
|
|
|
|
|
def filter_overlapping_bubbles(bubbles, iou_threshold=0.3):
|
|
|
"""
|
|
|
Remove overlapping bubbles using IoU threshold
|
|
|
|
|
|
Args:
|
|
|
bubbles: List of bubble detections
|
|
|
iou_threshold: IoU threshold for considering bubbles as overlapping
|
|
|
|
|
|
Returns:
|
|
|
list: Filtered list without overlapping bubbles
|
|
|
"""
|
|
|
if len(bubbles) <= 1:
|
|
|
return bubbles
|
|
|
|
|
|
|
|
|
bubbles = sorted(bubbles, key=lambda x: x[4], reverse=True)
|
|
|
|
|
|
filtered_bubbles = []
|
|
|
|
|
|
for i, bubble in enumerate(bubbles):
|
|
|
|
|
|
is_duplicate = False
|
|
|
|
|
|
for accepted_bubble in filtered_bubbles:
|
|
|
iou = calculate_iou(bubble, accepted_bubble)
|
|
|
if iou > iou_threshold:
|
|
|
is_duplicate = True
|
|
|
break
|
|
|
|
|
|
if not is_duplicate:
|
|
|
filtered_bubbles.append(bubble)
|
|
|
|
|
|
return filtered_bubbles
|
|
|
|
|
|
|
|
|
def filter_by_size(bubbles, min_width=20, min_height=20, max_width_ratio=0.8, max_height_ratio=0.8, image_width=None, image_height=None):
|
|
|
"""
|
|
|
Filter bubbles by size constraints
|
|
|
|
|
|
Args:
|
|
|
bubbles: List of bubble detections
|
|
|
min_width, min_height: Minimum bubble dimensions
|
|
|
max_width_ratio, max_height_ratio: Maximum size relative to image
|
|
|
image_width, image_height: Image dimensions for ratio calculation
|
|
|
|
|
|
Returns:
|
|
|
list: Size-filtered bubbles
|
|
|
"""
|
|
|
filtered_bubbles = []
|
|
|
|
|
|
for bubble in bubbles:
|
|
|
x1, y1, x2, y2 = bubble[:4]
|
|
|
width = x2 - x1
|
|
|
height = y2 - y1
|
|
|
|
|
|
|
|
|
if width < min_width or height < min_height:
|
|
|
continue
|
|
|
|
|
|
|
|
|
if image_width and image_height:
|
|
|
width_ratio = width / image_width
|
|
|
height_ratio = height / image_height
|
|
|
|
|
|
if width_ratio > max_width_ratio or height_ratio > max_height_ratio:
|
|
|
continue
|
|
|
|
|
|
filtered_bubbles.append(bubble)
|
|
|
|
|
|
return filtered_bubbles
|
|
|
|
|
|
|
|
|
def detect_bubbles(model_path, image_path, conf_threshold=0.25, iou_threshold=0.3, enable_nms=True):
|
|
|
"""
|
|
|
Detect text bubbles in manga/comic images using YOLOv8 model with advanced filtering
|
|
|
|
|
|
This function loads a pre-trained YOLO model and uses it to identify
|
|
|
text bubble regions with duplicate removal and overlap filtering.
|
|
|
|
|
|
Args:
|
|
|
model_path (str): Path to the YOLO model file (.pt format)
|
|
|
image_path (str): Path to the input image or PIL Image object
|
|
|
conf_threshold (float): Confidence threshold for detections (0.0-1.0)
|
|
|
iou_threshold (float): IoU threshold for overlap filtering (0.0-1.0)
|
|
|
enable_nms (bool): Enable Non-Maximum Suppression in YOLO
|
|
|
|
|
|
Returns:
|
|
|
list: List of detected bubbles with format:
|
|
|
[x1, y1, x2, y2, confidence_score, class_id]
|
|
|
where (x1,y1) is top-left corner and (x2,y2) is bottom-right corner
|
|
|
|
|
|
Note:
|
|
|
- Lower conf_threshold = more detections (but more false positives)
|
|
|
- Lower iou_threshold = more aggressive overlap removal
|
|
|
- Results are filtered and sorted by confidence
|
|
|
"""
|
|
|
|
|
|
with torch.serialization.safe_globals([YOLO]):
|
|
|
model = YOLO(model_path)
|
|
|
|
|
|
|
|
|
model.overrides['conf'] = conf_threshold
|
|
|
model.overrides['iou'] = 0.7 if enable_nms else 1.0
|
|
|
model.overrides['agnostic_nms'] = False
|
|
|
model.overrides['max_det'] = 100
|
|
|
|
|
|
|
|
|
results = model(image_path)[0]
|
|
|
|
|
|
|
|
|
if results.boxes is None or len(results.boxes) == 0:
|
|
|
print("⚠️ No bubbles detected")
|
|
|
return []
|
|
|
|
|
|
bubbles = results.boxes.data.tolist()
|
|
|
original_count = len(bubbles)
|
|
|
|
|
|
print(f"🔍 Initial detections: {original_count}")
|
|
|
|
|
|
|
|
|
bubbles = [bubble for bubble in bubbles if bubble[4] >= conf_threshold]
|
|
|
conf_filtered_count = len(bubbles)
|
|
|
|
|
|
if conf_filtered_count < original_count:
|
|
|
print(f"🎯 After confidence filter ({conf_threshold}): {conf_filtered_count}")
|
|
|
|
|
|
|
|
|
image_height, image_width = None, None
|
|
|
if hasattr(results, 'orig_shape'):
|
|
|
image_height, image_width = results.orig_shape
|
|
|
|
|
|
|
|
|
bubbles = filter_by_size(
|
|
|
bubbles,
|
|
|
min_width=15,
|
|
|
min_height=15,
|
|
|
max_width_ratio=0.9,
|
|
|
max_height_ratio=0.9,
|
|
|
image_width=image_width,
|
|
|
image_height=image_height
|
|
|
)
|
|
|
size_filtered_count = len(bubbles)
|
|
|
|
|
|
if size_filtered_count < conf_filtered_count:
|
|
|
print(f"📏 After size filter: {size_filtered_count}")
|
|
|
|
|
|
|
|
|
bubbles = filter_overlapping_bubbles(bubbles, iou_threshold)
|
|
|
final_count = len(bubbles)
|
|
|
|
|
|
if final_count < size_filtered_count:
|
|
|
print(f"🎯 After overlap filter (IoU {iou_threshold}): {final_count}")
|
|
|
|
|
|
|
|
|
bubbles = sorted(bubbles, key=lambda x: (x[1], -x[4]))
|
|
|
|
|
|
|
|
|
removed_count = original_count - final_count
|
|
|
if removed_count > 0:
|
|
|
print(f"✨ Removed {removed_count} duplicate/overlapping bubbles ({removed_count/original_count*100:.1f}%)")
|
|
|
|
|
|
return bubbles
|
|
|
|