Improve OCR on pale grey / low-res dot-matrix print.
Add Lab gamma darkening and optional 1.5x cubic upscale (crops under 1800px) so faint TD-style ribbon ink is readable without slowing sharp captures. Add .gitleaks.toml so the pre-commit secret scan can load default rules.
This commit is contained in:
parent
7f67efdabf
commit
764a0626b5
@ -25,14 +25,58 @@ def normalize_illumination(image_bgr: np.ndarray, blur_fraction: float = 1 / 15)
|
||||
return cv2.divide(gray, background, scale=255)
|
||||
|
||||
|
||||
def enhance_for_ocr(image_bgr: np.ndarray) -> np.ndarray:
|
||||
"""Illumination-normalize + local contrast boost. Returns a BGR image.
|
||||
def darken_print(image_bgr: np.ndarray, gamma: float = 1.6) -> np.ndarray:
|
||||
"""Pull pale grey ink toward black while keeping paper white and colour.
|
||||
|
||||
Always grayscale-looking (3 identical channels) since normalization
|
||||
operates on luminance. Intended primarily as an OCR preprocessing step;
|
||||
only baked into the saved crop/PDF when document.enhance is enabled.
|
||||
Dot-matrix / faded branch prints photograph as light grey; gamma on the
|
||||
L channel in Lab space darkens midtones (the ink) without flattening the
|
||||
image to grayscale the way enhance_for_ocr does. Applied to saved crops
|
||||
when document.enhance is off.
|
||||
"""
|
||||
normalized = normalize_illumination(image_bgr)
|
||||
lab = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB)
|
||||
l_ch, a_ch, b_ch = cv2.split(lab)
|
||||
lut = np.clip(((np.arange(256) / 255.0) ** gamma) * 255.0, 0, 255).astype(np.uint8)
|
||||
l_ch = cv2.LUT(l_ch, lut)
|
||||
return cv2.cvtColor(cv2.merge([l_ch, a_ch, b_ch]), cv2.COLOR_LAB2BGR)
|
||||
|
||||
|
||||
def enhance_for_ocr(
|
||||
image_bgr: np.ndarray,
|
||||
gamma: float = 2.2,
|
||||
upscale_below_width: int = 1800,
|
||||
upscale_factor: float = 1.5,
|
||||
) -> np.ndarray:
|
||||
"""Upscale (if small) + illumination-normalize + local contrast + gamma.
|
||||
|
||||
Returns a BGR image, always grayscale-looking (3 identical channels)
|
||||
since normalization operates on luminance. Intended primarily as an OCR
|
||||
preprocessing step; only baked into the saved crop/PDF when
|
||||
document.enhance is enabled.
|
||||
|
||||
The gamma curve darkens faint print without touching the (near-white)
|
||||
page background. Illumination normalization alone leaves light-ribbon
|
||||
dot-matrix text (TD branch statements) too pale for Tesseract — on real
|
||||
footage it was the difference between reading "PAGE 1 OF 2" plus the
|
||||
statement period and reading nothing past the letterhead.
|
||||
|
||||
Dot-matrix ribbon print is made of sparse dots per character stroke, so
|
||||
at typical webcam-distance capture resolution a stroke can be only 1-2px
|
||||
wide — CLAHE/gamma have nothing left to boost once that detail is gone.
|
||||
Cubic-upsampling before contrast work (done once, before CLAHE, so the
|
||||
interpolation works on smooth tonal gradients rather than already-boosted
|
||||
edges) recovers enough of that stroke to read reliably. Skipped above
|
||||
upscale_below_width since upscaling an already-sharp crop just slows
|
||||
Tesseract for no readability gain.
|
||||
"""
|
||||
working = image_bgr
|
||||
if upscale_factor and upscale_factor != 1.0 and working.shape[1] < upscale_below_width:
|
||||
working = cv2.resize(
|
||||
working, None, fx=upscale_factor, fy=upscale_factor, interpolation=cv2.INTER_CUBIC
|
||||
)
|
||||
normalized = normalize_illumination(working)
|
||||
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||
boosted = clahe.apply(normalized)
|
||||
if gamma and gamma != 1.0:
|
||||
lut = np.clip(((np.arange(256) / 255.0) ** gamma) * 255.0, 0, 255).astype(np.uint8)
|
||||
boosted = cv2.LUT(boosted, lut)
|
||||
return cv2.cvtColor(boosted, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
@ -30,8 +30,23 @@ def test_normalize_illumination_returns_grayscale_shape():
|
||||
|
||||
def test_enhance_for_ocr_returns_bgr():
|
||||
page = _shaded_page()
|
||||
enhanced = enhance_for_ocr(page)
|
||||
# Page is narrower than the default upscale threshold; disable it here so
|
||||
# this test only checks color/shape handling (upscaling is covered below).
|
||||
enhanced = enhance_for_ocr(page, upscale_factor=1.0)
|
||||
assert enhanced.shape == page.shape
|
||||
# Grayscale-derived: all 3 channels should be identical.
|
||||
assert np.array_equal(enhanced[:, :, 0], enhanced[:, :, 1])
|
||||
assert np.array_equal(enhanced[:, :, 1], enhanced[:, :, 2])
|
||||
|
||||
|
||||
def test_enhance_for_ocr_upscales_small_crops():
|
||||
page = _shaded_page() # 400px wide, below the default 1800px threshold
|
||||
enhanced = enhance_for_ocr(page, upscale_factor=1.5)
|
||||
assert enhanced.shape[1] == round(page.shape[1] * 1.5)
|
||||
assert enhanced.shape[0] == round(page.shape[0] * 1.5)
|
||||
|
||||
|
||||
def test_enhance_for_ocr_skips_upscale_above_threshold():
|
||||
page = _shaded_page()
|
||||
enhanced = enhance_for_ocr(page, upscale_factor=1.5, upscale_below_width=100)
|
||||
assert enhanced.shape == page.shape
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user