Files
SnapOtter/packages/ai/python/ocr_preprocess.py
T

87 lines
2.7 KiB
Python

"""Image preprocessing pipeline for OCR accuracy improvement.
Uses OpenCV for deskew, adaptive binarization, CLAHE contrast enhancement,
and denoising. All operations work on the image file in-place (overwrite).
"""
import cv2
import numpy as np
import sys
import json
def emit_progress(percent, stage):
print(json.dumps({"progress": percent, "stage": stage}), file=sys.stderr, flush=True)
def deskew(image):
"""Detect and correct rotation/skew using Hough line transform."""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=100, minLineLength=100, maxLineGap=10)
if lines is None:
return image
angles = []
for line in lines:
x1, y1, x2, y2 = line[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if abs(angle) < 45:
angles.append(angle)
if not angles:
return image
median_angle = np.median(angles)
if abs(median_angle) < 0.5 or abs(median_angle) > 15:
return image
h, w = image.shape[:2]
center = (w // 2, h // 2)
matrix = cv2.getRotationMatrix2D(center, median_angle, 1.0)
rotated = cv2.warpAffine(image, matrix, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
return rotated
def binarize(image):
"""Adaptive thresholding for high-contrast black/white."""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) if len(image.shape) == 3 else image
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
)
return cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
def enhance_contrast(image):
"""CLAHE contrast enhancement for uneven lighting."""
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
l = clahe.apply(l)
enhanced = cv2.merge([l, a, b])
return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)
def denoise(image):
"""Bilateral filter to remove noise while preserving text edges."""
return cv2.bilateralFilter(image, 9, 75, 75)
def preprocess(input_path, output_path):
"""Run the full preprocessing pipeline and save result.
Steps: deskew -> enhance contrast -> denoise -> binarize
Order matters: binarize last because it strips color info needed by CLAHE.
"""
image = cv2.imread(input_path)
if image is None:
raise ValueError(f"Cannot read image: {input_path}")
image = deskew(image)
image = enhance_contrast(image)
image = denoise(image)
image = binarize(image)
cv2.imwrite(output_path, image)
return output_path