VOCR / preprocess.py
sirimiri's picture
Delete fix_register_beam.py, patch_beam_search.py, usage_examples.py; remove module docstrings, section banners, and inline explanatory comments from all source files
b8571ca
Raw
History Blame Contribute Delete
7.78 kB
import cv2
import numpy as np
import subprocess
import argparse
import os
import tempfile
from pathlib import Path
def enhance_imagemagick(image_path: str, output_path: str) -> bool:
cmd = [
'convert', image_path,
'-resize', '50%',
'-colorspace', 'gray',
'-blur', '0x0.5',
'-normalize',
'-lat', '15x15-8%',
'-threshold', '45%',
'-morphology', 'Open', 'Disk:0.4',
'-morphology', 'Dilate', 'Disk:0.4',
output_path
]
try:
result = subprocess.run(cmd, capture_output=True, timeout=120)
return result.returncode == 0
except Exception as e:
print(f" ImageMagick error: {e}")
return False
def filter_connected_components(image: np.ndarray,
min_area: int = 30,
max_area_ratio: float = 0.01,
dilate: bool = True) -> np.ndarray:
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image.copy()
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
h, w = gray.shape
max_area = int(h * w * max_area_ratio)
num, labels, stats, _ = cv2.connectedComponentsWithStats(binary)
clean = np.ones_like(gray) * 255
kept = 0
for i in range(1, num):
area = stats[i, cv2.CC_STAT_AREA]
if min_area < area < max_area:
clean[labels == i] = 0
kept += 1
if dilate:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
clean = cv2.erode(clean, kernel, iterations=1)
print(f" CC filter: {kept}/{num-1} components kept "
f"(min={min_area}, max={max_area})")
return cv2.cvtColor(clean, cv2.COLOR_GRAY2BGR)
def analyze_image(image_path: str) -> dict:
img = cv2.imread(image_path)
if img is None:
return {'needs_enhance': False}
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
mean = float(gray.mean())
std = float(gray.std())
noise = float(cv2.Laplacian(gray, cv2.CV_64F).var())
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
saturation = float(hsv[:,:,1].mean())
is_colored_bg = saturation > 30 and mean < 220
needs_enhance = (
mean < 190 or
std < 30 or
is_colored_bg
)
return {
'needs_enhance': needs_enhance,
'mean': round(mean, 1),
'std': round(std, 1),
'noise': round(noise, 1),
'is_colored_bg': is_colored_bg,
'saturation': round(saturation, 1),
}
def preprocess(image_path: str, output_path: str,
min_area: int = 30,
max_area_ratio: float = 0.01,
dilate: bool = True,
auto: bool = True,
keep_temp: bool = False) -> bool:
print(f"\nProcessing: {image_path}")
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
if auto:
info = analyze_image(image_path)
print(f" [ANALYZE] mean={info['mean']}, std={info['std']}, "
f"noise={info['noise']:.0f}, colored_bg={info['is_colored_bg']}")
if not info.get('needs_enhance', True):
print(f" [SKIP] Image is clean, no enhancement needed")
import shutil
shutil.copy2(image_path, output_path)
return True
else:
print(f" [ENHANCE] Image needs enhancement")
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
tmp = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False)
tmp_path = tmp.name
tmp.close()
print(f" [1] ImageMagick LAT...")
ok = enhance_imagemagick(image_path, tmp_path)
if not ok:
print(" ImageMagick failed! Check if installed: brew install imagemagick")
return False
print(f" [2] Connected Components filtering...")
img = cv2.imread(tmp_path)
if img is None:
print(" Cannot read enhanced image!")
return False
result = filter_connected_components(img, min_area, max_area_ratio, dilate)
cv2.imwrite(output_path, result)
print(f" Saved: {output_path}")
if not keep_temp:
os.remove(tmp_path)
return True
def preprocess_dir(input_dir: str, output_dir: str,
extensions: set = None, **kwargs) -> int:
if extensions is None:
extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'}
paths = sorted(
p for p in Path(input_dir).iterdir()
if p.suffix.lower() in extensions
)
print(f"Found {len(paths)} images in {input_dir}")
success = 0
for i, p in enumerate(paths):
out_path = os.path.join(output_dir, p.stem + '_processed.jpg')
print(f"\n[{i+1}/{len(paths)}]")
if preprocess(str(p), out_path, **kwargs):
success += 1
print(f"\nDone! {success}/{len(paths)} images processed.")
return success
def compare(original_path: str, processed_path: str,
output_path: str = None) -> np.ndarray:
orig = cv2.imread(original_path)
proc = cv2.imread(processed_path)
if orig is None or proc is None:
return None
target_h = 800
orig_h, orig_w = orig.shape[:2]
proc_h, proc_w = proc.shape[:2]
orig_r = cv2.resize(orig, (int(orig_w * target_h / orig_h), target_h))
proc_r = cv2.resize(proc, (int(proc_w * target_h / proc_h), target_h))
label_h = 40
canvas_w = orig_r.shape[1] + proc_r.shape[1] + 10
canvas = np.ones((target_h + label_h, canvas_w, 3), dtype=np.uint8) * 240
canvas[label_h:label_h+target_h, :orig_r.shape[1]] = orig_r
cv2.putText(canvas, 'ORIGINAL', (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (50,50,50), 2)
x_off = orig_r.shape[1] + 10
canvas[label_h:label_h+target_h, x_off:x_off+proc_r.shape[1]] = proc_r
cv2.putText(canvas, 'PROCESSED', (x_off+10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,100,0), 2)
if output_path:
cv2.imwrite(output_path, canvas)
print(f" Compare saved: {output_path}")
return canvas
def main():
parser = argparse.ArgumentParser(
description="Image Preprocessor - Tiền xử lý ảnh scan sách cũ"
)
parser.add_argument("--image", required=True,
help="Đường dẫn ảnh hoặc thư mục")
parser.add_argument("--output", default="./output/enhanced",
help="Thư mục lưu ảnh đã xử lý")
parser.add_argument("--min_area", type=int, default=30,
help="Diện tích tối thiểu của CC (mặc định: 30)")
parser.add_argument("--max_area_ratio", type=float, default=0.01,
help="Tỷ lệ diện tích tối đa (mặc định: 0.01)")
parser.add_argument("--no_dilate", action="store_true")
parser.add_argument("--no_auto", action="store_true",
help="Tắt auto-detect, luôn enhance")
parser.add_argument("--compare", action="store_true",
help="Lưu ảnh so sánh before/after")
args = parser.parse_args()
kwargs = {
'min_area': args.min_area,
'max_area_ratio': args.max_area_ratio,
'dilate': not args.no_dilate,
'auto': not args.no_auto,
}
if os.path.isdir(args.image):
preprocess_dir(args.image, args.output, **kwargs)
else:
stem = Path(args.image).stem
out_path = os.path.join(args.output, f"{stem}_processed.jpg")
ok = preprocess(args.image, out_path, **kwargs)
if ok and args.compare:
compare_path = os.path.join(args.output, f"{stem}_compare.jpg")
compare(args.image, out_path, compare_path)
if __name__ == "__main__":
main()