Inici del render

This commit is contained in:
Guillem Hernandez Sola
2026-04-10 18:14:54 +02:00
parent f92ea8410b
commit 458915278e
3 changed files with 484 additions and 57 deletions

View File

@@ -1,5 +1,6 @@
import re
import os
import json
import cv2
import numpy as np
import easyocr
@@ -126,10 +127,6 @@ def merge_nearby_clusters(raw_clusters, proximity_px=80):
# ─────────────────────────────────────────────
# CROP-BASED OCR RE-READ
# For each cluster bounding box, crop the
# original image with padding and re-run OCR
# at higher quality. This fixes garbled text
# in small or low-contrast bubbles.
# ─────────────────────────────────────────────
def reread_cluster_crop(
image,
@@ -142,23 +139,10 @@ def reread_cluster_crop(
"""
Crops a cluster region from the full image, upscales it,
and re-runs OCR for higher accuracy on small text.
Args:
image : Full-page image as numpy array (BGR)
bbox : (x1, y1, x2, y2) cluster bounding box
reader : Initialized EasyOCR Reader
source_lang : Language code string
padding_px : Pixels of padding around the crop (default: 20)
upscale_factor: How much to enlarge the crop before OCR (default: 2.5)
Returns:
Single cleaned string with all OCR lines merged top-to-bottom,
or None if OCR found nothing.
"""
img_h, img_w = image.shape[:2]
x1, y1, x2, y2 = bbox
# Add padding, clamp to image bounds
x1 = max(0, int(x1) - padding_px)
y1 = max(0, int(y1) - padding_px)
x2 = min(img_w, int(x2) + padding_px)
@@ -168,16 +152,12 @@ def reread_cluster_crop(
if crop.size == 0:
return None
# Upscale for better OCR on small text
new_w = int(crop.shape[1] * upscale_factor)
new_h = int(crop.shape[0] * upscale_factor)
upscaled = cv2.resize(crop, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
# Light sharpening to improve OCR on manga fonts
new_w = int(crop.shape[1] * upscale_factor)
new_h = int(crop.shape[0] * upscale_factor)
upscaled = cv2.resize(crop, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
sharpened = cv2.filter2D(upscaled, -1, kernel)
# Save temp crop and OCR it
temp_path = "_temp_crop_ocr.png"
cv2.imwrite(temp_path, sharpened)
@@ -190,8 +170,7 @@ def reread_cluster_crop(
if not crop_results:
return None
# Sort detections top-to-bottom and join lines
crop_results.sort(key=lambda r: r[0][0][1]) # sort by top-left Y
crop_results.sort(key=lambda r: r[0][0][1])
lines = [text.strip() for _, text, conf in crop_results if text.strip()]
return fix_hyphens(lines) if lines else None
@@ -240,7 +219,6 @@ def cluster_into_bubbles(ocr_results, eps=80, min_samples=1, proximity_px=80):
merged_clusters = merge_nearby_clusters(raw_clusters, proximity_px=proximity_px)
print(f" After merge: {len(merged_clusters)} cluster(s)")
# Sort in reading order
row_band_px = 150
def cluster_sort_key(items):
@@ -288,19 +266,11 @@ def compute_auto_eps(image_path, base_eps=80, reference_width=750):
# ─────────────────────────────────────────────
# OCR QUALITY SCORE
# Heuristic to detect garbled OCR output.
# Low score = likely garbage, trigger re-read.
# ─────────────────────────────────────────────
def ocr_quality_score(text):
"""
Returns a quality score 0.01.0 for an OCR result.
Penalises:
- High ratio of non-alphabetic characters
- Very short text (< 4 chars)
- Suspicious character combos (,,- etc.)
A score below 0.5 triggers a crop re-read.
Low score triggers a crop re-read.
"""
if not text or len(text) < 2:
return 0.0
@@ -309,12 +279,46 @@ def ocr_quality_score(text):
total_chars = len(text)
alpha_ratio = alpha_chars / total_chars
# Penalise suspicious patterns
garbage_patterns = [r",,", r"\.\.-", r"[^\w\s\'\!\?\.,-]{2,}"]
penalty = sum(0.2 for p in garbage_patterns if re.search(p, text))
score = alpha_ratio - penalty
return max(0.0, min(1.0, score))
return max(0.0, min(1.0, alpha_ratio - penalty))
# ─────────────────────────────────────────────
# BUBBLE JSON EXPORT
# Saves bbox_dict to bubbles.json so the
# renderer can load exact cluster positions.
# ─────────────────────────────────────────────
def export_bubble_boxes(bbox_dict, filepath="bubbles.json"):
"""
Serialises bbox_dict to a JSON file.
Format written:
{
"1": {"x": 120, "y": 45, "w": 180, "h": 210},
...
}
Args:
bbox_dict : Dict {bubble_id (int): (x1, y1, x2, y2)}
filepath : Output path (default: 'bubbles.json')
"""
export = {}
for bubble_id, (x1, y1, x2, y2) in bbox_dict.items():
export[str(bubble_id)] = {
"x": int(x1),
"y": int(y1),
"w": int(x2 - x1),
"h": int(y2 - y1),
}
with open(filepath, "w", encoding="utf-8") as f:
json.dump(export, f, indent=2, ensure_ascii=False)
print(f"📦 Bubble boxes saved → {filepath}")
for bubble_id, vals in export.items():
print(f" #{bubble_id}: ({vals['x']},{vals['y']}) {vals['w']}×{vals['h']}px")
# ─────────────────────────────────────────────
@@ -360,31 +364,33 @@ def translate_manga_text(
target_lang="ca",
confidence_threshold=0.15,
export_to_file=None,
export_bubbles_to="bubbles.json", # ← NEW: path for bubble boxes JSON
min_text_length=2,
cluster_eps="auto",
proximity_px=80,
filter_sound_effects=True,
quality_threshold=0.5, # below this → trigger crop re-read
upscale_factor=2.5, # crop upscale multiplier for re-read
quality_threshold=0.5,
upscale_factor=2.5,
debug=False,
):
"""
Full pipeline:
OCR → filter → DBSCAN cluster → proximity merge
→ quality check → crop re-read if needed
→ fix hyphens → translate
→ fix hyphens → translate → export txt + json
Args:
image_path : Path to your image file
source_lang : Source language code (default: 'it')
target_lang : Target language code (default: 'ca')
confidence_threshold : Min OCR confidence (default: 0.15)
export_to_file : Save output to .txt (default: None)
export_to_file : Save translations to .txt (default: None)
export_bubbles_to : Save bubble boxes to .json (default: 'bubbles.json')
min_text_length : Min characters per detection(default: 2)
cluster_eps : DBSCAN eps or 'auto' (default: 'auto')
proximity_px : Post-merge proximity px (default: 80)
filter_sound_effects : Skip onomatopoeia/SFX (default: True)
quality_threshold : Min quality score 01 before re-read (default: 0.5)
quality_threshold : Min quality score 01 (default: 0.5)
upscale_factor : Crop upscale for re-read (default: 2.5)
debug : Save debug_clusters.png (default: False)
"""
@@ -396,7 +402,7 @@ def translate_manga_text(
else:
eps = float(cluster_eps)
# ── 2. Load full image (needed for crop re-reads) ─────────────────────────
# ── 2. Load full image ────────────────────────────────────────────────────
full_image = cv2.imread(image_path)
if full_image is None:
print(f"❌ Could not load image: {image_path}")
@@ -410,7 +416,7 @@ def translate_manga_text(
# ── 4. Initialize translator ──────────────────────────────────────────────
translator = GoogleTranslator(source=source_lang, target=target_lang)
# ── 5. Run OCR on full image ──────────────────────────────────────────────
# ── 5. Run OCR ────────────────────────────────────────────────────────────
print(f"\nRunning OCR on: {image_path}")
results = reader.readtext(image_path, paragraph=False)
print(f" Raw detections: {len(results)}")
@@ -453,27 +459,24 @@ def translate_manga_text(
if debug:
save_debug_clusters(image_path, filtered, bubble_dict)
# ── 9. Fix hyphens → first-pass text ─────────────────────────────────────
# ── 9. Fix hyphens ────────────────────────────────────────────────────────
clean_bubbles = {
i: fix_hyphens(lines)
for i, lines in bubble_dict.items()
if lines
}
# ── 10. Quality check crop re-read for low-quality bubbles ─────────────
# ── 10. Quality check + crop re-read ──────────────────────────────────────
print("Checking OCR quality per bubble...")
for i, text in clean_bubbles.items():
score = ocr_quality_score(text)
score = ocr_quality_score(text)
status = "" if score >= quality_threshold else "🔁"
print(f" Bubble #{i}: score={score:.2f} {status} '{text[:60]}'")
if score < quality_threshold:
print(f" → Re-reading bubble #{i} from crop...")
reread = reread_cluster_crop(
full_image,
bbox_dict[i],
reader,
source_lang,
full_image, bbox_dict[i], reader, source_lang,
upscale_factor=upscale_factor,
)
if reread:
@@ -520,11 +523,15 @@ def translate_manga_text(
print(divider)
print(summary)
# ── 12. Export ────────────────────────────────────────────────────────────
# ── 12. Export translations .txt ──────────────────────────────────────────
if export_to_file:
with open(export_to_file, "w", encoding="utf-8") as f:
f.write("\n".join(output_lines))
print(f"📄 Output saved to: {export_to_file}")
print(f"📄 Translations saved {export_to_file}")
# ── 13. Export bubble boxes .json ─────────────────────────────────────────
if export_bubbles_to:
export_bubble_boxes(bbox_dict, filepath=export_bubbles_to)
# ─────────────────────────────────────────────
@@ -550,10 +557,11 @@ if __name__ == "__main__":
confidence_threshold = 0.15,
min_text_length = 2,
export_to_file = "output.txt",
export_bubbles_to = "bubbles.json", # ← NEW
cluster_eps = "auto",
proximity_px = 80,
filter_sound_effects = True,
quality_threshold = 0.5, # bubbles scoring below this get re-read
upscale_factor = 2.5, # how much to enlarge the crop for re-read
quality_threshold = 0.5,
upscale_factor = 2.5,
debug = True,
)