kocr/core/preprocessor.py

195 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""KOCR v4 — 图像预处理 + 自动图像分析调参"""
import cv2
import numpy as np
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ImageQuality:
"""图像质量分析结果"""
brightness: float = 0.0 # 平均亮度 [0-255]
contrast: float = 0.0 # 标准偏差
blur_score: float = 0.0 # 拉普拉斯方差(越小越模糊)
shadow_ratio: float = 0.0 # 阴影面积占比
photo_type: str = 'normal' # flash / dark / blurry / normal
needs_shadow_removal: bool = True
needs_sharpen: bool = False
needs_deskew: bool = True
def analyze_image(img: np.ndarray) -> ImageQuality:
"""
自动分析图像质量,返回各项指标和推荐的照片类型
Args:
img: 输入图像BGR格式
Returns:
ImageQuality: 包含所有分析结果
"""
h, w = img.shape[:2]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. 亮度分析(取直方图均值)
brightness = float(np.mean(gray))
# 2. 对比度(灰度标准偏差)
contrast = float(np.std(gray))
# 3. 模糊检测(拉普拉斯方差)
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
blur_score = float(laplacian.var())
# 4. 阴影检测
# 用大核膨胀分离背景和前景
dilated = cv2.dilate(gray, np.ones((15, 15), np.uint8))
bg = cv2.medianBlur(dilated, 31)
diff = cv2.absdiff(gray.astype(np.float32), bg.astype(np.float32))
shadow_mask = diff < 20 # 与背景差异小的区域视为阴影
shadow_ratio = float(np.sum(shadow_mask) / (h * w))
# 5. 判定照片类型
if brightness >= 200:
photo_type = 'flash'
elif brightness <= 60:
photo_type = 'dark'
elif blur_score < 80:
photo_type = 'blurry'
else:
photo_type = 'normal'
# 6. 预处理策略建议
needs_shadow_removal = shadow_ratio > 0.3 and photo_type != 'flash'
needs_sharpen = blur_score < 150
needs_deskew = photo_type not in ('flash',) # 闪光灯照片也可能歪
return ImageQuality(
brightness=round(brightness, 1),
contrast=round(contrast, 1),
blur_score=round(blur_score, 1),
shadow_ratio=round(shadow_ratio, 3),
photo_type=photo_type,
needs_shadow_removal=needs_shadow_removal,
needs_sharpen=needs_sharpen,
needs_deskew=needs_deskew,
)
class VoucherPreprocessor:
"""记账凭证图像预处理器,支持自动调参"""
def __init__(self, max_side=1600, denoise=True, deskew=True,
remove_shadow=True, sharpen=True):
self.max_side = max_side
self.denoise = denoise
self.deskew = deskew
self.remove_shadow = remove_shadow
self.sharpen = sharpen
@classmethod
def from_config(cls, config: dict, photo_type: str = 'normal'):
"""根据配置和照片类型创建预处理器实例
Args:
config: config.yaml 中 preprocessing 章节
photo_type: 照片类型(用于覆盖默认参数)
"""
params = {
'max_side': config.get('max_side', 1600),
'denoise': config.get('denoise', True),
'deskew': config.get('deskew', True),
'remove_shadow': config.get('remove_shadow', True),
'sharpen': config.get('sharpen', True),
}
# 应用照片类型覆盖
overrides = config.get('photo_type_overrides', {}).get(photo_type, {})
params.update(overrides)
return cls(**params)
def _resize(self, img):
"""统一缩放长边不超过max_side保持比例"""
h, w = img.shape[:2]
if max(h, w) > self.max_side:
scale = self.max_side / max(h, w)
new_w, new_h = int(w * scale), int(h * scale)
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA)
return img
def _remove_shadow(self, img):
"""去除拍照阴影、纸张背景不均"""
rgb_planes = cv2.split(img)
result_planes = []
for plane in rgb_planes:
dilated_img = cv2.dilate(plane, np.ones((7, 7), np.uint8))
bg_img = cv2.medianBlur(dilated_img, 21)
diff_img = 255 - cv2.absdiff(plane, bg_img)
norm_img = cv2.normalize(diff_img, None, alpha=0, beta=255,
norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8UC1)
result_planes.append(norm_img)
return cv2.merge(result_planes)
def _deskew(self, img):
"""基于霍夫直线的倾斜矫正"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
lines = cv2.HoughLines(edges, 1, np.pi / 180, 200)
if lines is None:
return img
angles = []
for line in lines:
_, theta = line[0]
angle = (theta * 180 / np.pi) - 90
if abs(angle) < 15:
angles.append(angle)
if not angles:
return img
median_angle = np.median(angles)
h, w = img.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, median_angle, 1.0)
rotated = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
return rotated
def _denoise(self, img):
"""高斯去噪"""
return cv2.GaussianBlur(img, (3, 3), 0)
def _sharpen_op(self, img):
"""USM 锐化"""
blur = cv2.GaussianBlur(img, (0, 0), 3)
sharp = cv2.addWeighted(img, 1.5, blur, -0.5, 0)
return sharp
def process(self, img_or_path):
"""完整预处理入口
Args:
img_or_path: 图像路径或 numpy 数组
Returns:
预处理后的图像
"""
if isinstance(img_or_path, str):
img = cv2.imread(img_or_path)
if img is None:
raise ValueError(f"无法读取图片:{img_or_path}")
else:
img = img_or_path.copy()
img = self._resize(img)
if self.remove_shadow:
img = self._remove_shadow(img)
if self.deskew:
img = self._deskew(img)
if self.denoise:
img = self._denoise(img)
if self.sharpen:
img = self._sharpen_op(img)
return img