refactor: solidify startup UX and engine-aware preprocessing

- Slim core deps: move ML stack to optional extras (paddle/tables/figures/scientific/full)
- Lazy settings proxy with config search paths (env var, cwd, user dir)
- New commands: init, demo, setup [basic|full], first-run guard on run/watch
- Engine-aware preprocessing: Paddle gets original image (fixes dark mode 0.83->0.95)
- Results table shows Skipped count; lazy run-dir creation
- kg_ocr marked experimental with extra, Docker defaults with OCR_PIPELINE_CONFIG
- 25/25 tests, ruff clean
This commit is contained in:
2026-07-19 22:14:54 +02:00
parent deab02610b
commit a0211155fa
59 changed files with 2426 additions and 629 deletions

View File

@@ -27,7 +27,9 @@ class ImagePreprocessor:
if max(height, width) <= self.max_dim:
return image
scale = self.max_dim / max(height, width)
return cv2.resize(image, (int(width * scale), int(height * scale)), interpolation=cv2.INTER_AREA)
return cv2.resize(
image, (int(width * scale), int(height * scale)), interpolation=cv2.INTER_AREA
)
def deskew(self, image: np.ndarray) -> np.ndarray:
if not self.config.deskew:
@@ -43,10 +45,16 @@ class ImagePreprocessor:
return image
height, width = image.shape[:2]
matrix = cv2.getRotationMatrix2D((width // 2, height // 2), angle, 1.0)
return cv2.warpAffine(image, matrix, (width, height), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
return cv2.warpAffine(
image, matrix, (width, height), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE
)
def denoise(self, image: np.ndarray) -> np.ndarray:
return cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21) if self.config.denoise else image
return (
cv2.fastNlMeansDenoisingColored(image, None, 10, 10, 7, 21)
if self.config.denoise
else image
)
def apply_clahe(self, image: np.ndarray) -> np.ndarray:
if not self.config.clahe:
@@ -61,8 +69,12 @@ class ImagePreprocessor:
return image
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
horizontal = cv2.morphologyEx(binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1)))
vertical = cv2.morphologyEx(binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40)))
horizontal = cv2.morphologyEx(
binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (40, 1))
)
vertical = cv2.morphologyEx(
binary, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, (1, 40))
)
mask = cv2.bitwise_or(horizontal, vertical)
result = image.copy()
result[mask > 0] = (255, 255, 255)
@@ -72,7 +84,9 @@ class ImagePreprocessor:
if not self.config.adaptive_threshold:
return image
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
)
return cv2.cvtColor(binary, cv2.COLOR_GRAY2BGR)
def preprocess_image(self, image: np.ndarray) -> np.ndarray: