feat: add contrast/sharpen strength controls, disable binarization

Major improvements to preprocessing controls:

Backend:
- Add contrast_strength (0.5-3.0) and sharpen_strength (0.5-2.0) to PreprocessingConfig
- Auto-detection now calculates optimal strength based on image quality metrics:
  - Lower contrast → Higher contrast_strength
  - Lower edge strength → Higher sharpen_strength
- Disable binarization in auto mode (rarely beneficial)
- CLAHE clipLimit now scales with contrast_strength
- Sharpening uses unsharp mask with variable strength

Frontend:
- Add strength sliders for contrast and sharpen in manual mode
- Sliders show current value and strength level (輕微/正常/強/最強)
- Move binarize option to collapsible "進階選項" section
- Updated i18n translations for strength labels

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-11-27 17:18:44 +08:00
parent f6d2957592
commit 5982fff71c
7 changed files with 212 additions and 53 deletions

View File

@@ -110,34 +110,61 @@ class LayoutPreprocessingService:
"""
Determine optimal preprocessing config based on image quality.
Auto-detection calculates appropriate strength values:
- Lower image contrast → Higher contrast_strength
- Lower edge strength → Higher sharpen_strength
- Binarization is disabled by default (rarely beneficial)
Args:
metrics: Image quality metrics from analyze_image_quality()
Returns:
PreprocessingConfig with recommended settings
"""
# Determine contrast enhancement
# Determine contrast enhancement and strength
if metrics.contrast < self.contrast_threshold:
contrast = PreprocessingContrastEnum.CLAHE
# Calculate strength based on how far below threshold
# contrast=40 threshold, contrast=20 → strength=2.0, contrast=30 → strength=1.5
contrast_ratio = (self.contrast_threshold - metrics.contrast) / self.contrast_threshold
contrast_strength = min(1.0 + contrast_ratio * 2.0, 3.0) # Range: 1.0 to 3.0
else:
contrast = PreprocessingContrastEnum.NONE
contrast_strength = 1.0
# Determine sharpening
sharpen = metrics.edge_strength < self.edge_threshold
# Determine sharpening and strength
if metrics.edge_strength < self.edge_threshold:
sharpen = True
# Calculate strength based on how far below threshold
# edge=15 threshold, edge=5 → strength=1.67, edge=10 → strength=1.33
edge_ratio = (self.edge_threshold - metrics.edge_strength) / self.edge_threshold
sharpen_strength = min(1.0 + edge_ratio * 1.0, 2.0) # Range: 1.0 to 2.0
else:
sharpen = False
sharpen_strength = 1.0
# Determine binarization (only for very low contrast)
binarize = metrics.contrast < self.binarize_threshold
# Binarization is disabled by default - it rarely helps and often hurts
# Only enable for extremely low contrast (< 15) which indicates a scan quality issue
binarize = False # Disabled by default
logger.debug(
f"Auto config: contrast={contrast} strength={contrast_strength:.2f}, "
f"sharpen={sharpen} strength={sharpen_strength:.2f}, binarize={binarize}"
)
return PreprocessingConfig(
contrast=contrast,
contrast_strength=round(contrast_strength, 2),
sharpen=sharpen,
sharpen_strength=round(sharpen_strength, 2),
binarize=binarize
)
def apply_contrast_enhancement(
self,
image: np.ndarray,
method: PreprocessingContrastEnum
method: PreprocessingContrastEnum,
strength: float = 1.0
) -> np.ndarray:
"""
Apply contrast enhancement to image.
@@ -145,6 +172,11 @@ class LayoutPreprocessingService:
Args:
image: Input image (BGR)
method: Enhancement method (none, histogram, clahe)
strength: Enhancement strength (0.5-3.0, default 1.0)
- 0.5: Subtle enhancement
- 1.0: Normal enhancement
- 2.0: Strong enhancement
- 3.0: Maximum enhancement
Returns:
Enhanced image (BGR)
@@ -157,12 +189,17 @@ class LayoutPreprocessingService:
l_channel, a_channel, b_channel = cv2.split(lab)
if method == PreprocessingContrastEnum.HISTOGRAM:
# Standard histogram equalization
l_enhanced = cv2.equalizeHist(l_channel)
# Standard histogram equalization (strength affects blending)
l_equalized = cv2.equalizeHist(l_channel)
# Blend original with equalized based on strength
alpha = min(strength, 1.0) # Cap at 1.0 for histogram
l_enhanced = cv2.addWeighted(l_equalized, alpha, l_channel, 1 - alpha, 0)
elif method == PreprocessingContrastEnum.CLAHE:
# Contrast Limited Adaptive Histogram Equalization
# clipLimit controls contrast amplification: 2.0 is default, up to 6.0 for strong
clip_limit = self.clahe_clip_limit * strength # 2.0 * 1.0 = 2.0, 2.0 * 2.0 = 4.0
clahe = cv2.createCLAHE(
clipLimit=self.clahe_clip_limit,
clipLimit=clip_limit,
tileGridSize=self.clahe_tile_grid_size
)
l_enhanced = clahe.apply(l_channel)
@@ -175,18 +212,33 @@ class LayoutPreprocessingService:
return enhanced_bgr
def apply_sharpening(self, image: np.ndarray) -> np.ndarray:
def apply_sharpening(self, image: np.ndarray, strength: float = 1.0) -> np.ndarray:
"""
Apply sharpening to enhance edges and faint lines.
Apply sharpening to enhance edges and faint lines using unsharp mask.
Args:
image: Input image (BGR)
strength: Sharpening strength (0.5-2.0, default 1.0)
- 0.5: Subtle sharpening
- 1.0: Normal sharpening
- 1.5: Strong sharpening
- 2.0: Maximum sharpening
Returns:
Sharpened image (BGR)
"""
# Apply unsharp mask style sharpening
sharpened = cv2.filter2D(image, -1, self.sharpen_kernel)
# Use unsharp mask technique for better control
# 1. Create blurred version
# 2. Subtract from original (scaled by strength)
# 3. Add back to original
# Gaussian blur with sigma based on strength
sigma = 1.0
blurred = cv2.GaussianBlur(image, (0, 0), sigma)
# Unsharp mask: original + (original - blurred) * strength
# This is equivalent to: original * (1 + strength) - blurred * strength
sharpened = cv2.addWeighted(image, 1.0 + strength, blurred, -strength, 0)
# Clip values to valid range
sharpened = np.clip(sharpened, 0, 255).astype(np.uint8)
@@ -277,15 +329,19 @@ class LayoutPreprocessingService:
# Step 1: Contrast enhancement
if config.contrast != PreprocessingContrastEnum.NONE:
processed = self.apply_contrast_enhancement(processed, config.contrast)
processed = self.apply_contrast_enhancement(
processed,
config.contrast,
strength=config.contrast_strength
)
was_processed = True
logger.debug(f"Applied contrast enhancement: {config.contrast}")
logger.debug(f"Applied contrast enhancement: {config.contrast} (strength={config.contrast_strength})")
# Step 2: Sharpening
if config.sharpen:
processed = self.apply_sharpening(processed)
processed = self.apply_sharpening(processed, strength=config.sharpen_strength)
was_processed = True
logger.debug("Applied sharpening")
logger.debug(f"Applied sharpening (strength={config.sharpen_strength})")
# Step 3: Binarization (last step, overwrites color)
if config.binarize: