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

@@ -41,6 +41,20 @@ export default function PreprocessingSettings({
onConfigChange({ ...config, [field]: value })
}
const getStrengthLabel = (value: number, type: 'contrast' | 'sharpen') => {
if (type === 'contrast') {
if (value <= 0.75) return t('processing.preprocessing.strength.subtle')
if (value <= 1.25) return t('processing.preprocessing.strength.normal')
if (value <= 2.0) return t('processing.preprocessing.strength.strong')
return t('processing.preprocessing.strength.maximum')
} else {
if (value <= 0.75) return t('processing.preprocessing.strength.subtle')
if (value <= 1.25) return t('processing.preprocessing.strength.normal')
if (value <= 1.5) return t('processing.preprocessing.strength.strong')
return t('processing.preprocessing.strength.maximum')
}
}
return (
<div className={cn('border rounded-lg p-4 bg-white', className)}>
{/* Header */}
@@ -131,14 +145,14 @@ export default function PreprocessingSettings({
{/* Manual Configuration (shown only when mode is 'manual') */}
{mode === 'manual' && (
<div className="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-200 space-y-3">
<div className="mt-4 p-3 bg-gray-50 rounded-lg border border-gray-200 space-y-4">
<h4 className="text-sm font-medium text-gray-700">
{t('processing.preprocessing.manualConfig')}
</h4>
{/* Contrast Enhancement */}
<div>
<label className="block text-xs font-medium text-gray-600 mb-1.5">
<div className="space-y-2">
<label className="block text-xs font-medium text-gray-600">
{t('processing.preprocessing.contrast.label')}
</label>
<select
@@ -157,38 +171,99 @@ export default function PreprocessingSettings({
</option>
))}
</select>
{/* Contrast Strength Slider */}
{config.contrast !== 'none' && (
<div className="pt-1">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>{t('processing.preprocessing.strength.label')}</span>
<span className="font-medium text-gray-700">
{config.contrast_strength.toFixed(1)} ({getStrengthLabel(config.contrast_strength, 'contrast')})
</span>
</div>
<input
type="range"
min="0.5"
max="3.0"
step="0.1"
value={config.contrast_strength}
onChange={(e) => handleConfigChange('contrast_strength', parseFloat(e.target.value))}
disabled={disabled}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
<div className="flex justify-between text-xs text-gray-400 mt-0.5">
<span>0.5</span>
<span>3.0</span>
</div>
</div>
)}
</div>
{/* Sharpen Toggle */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.sharpen}
onChange={(e) => handleConfigChange('sharpen', e.target.checked)}
disabled={disabled}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
{t('processing.preprocessing.sharpen')}
</span>
</label>
{/* Sharpen Section */}
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.sharpen}
onChange={(e) => handleConfigChange('sharpen', e.target.checked)}
disabled={disabled}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
{t('processing.preprocessing.sharpen')}
</span>
</label>
{/* Binarize Toggle */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.binarize}
onChange={(e) => handleConfigChange('binarize', e.target.checked)}
disabled={disabled}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
{t('processing.preprocessing.binarize')}
</span>
<span className="text-xs text-orange-600">
({t('processing.preprocessing.binarizeWarning')})
</span>
</label>
{/* Sharpen Strength Slider */}
{config.sharpen && (
<div className="pl-6 pt-1">
<div className="flex justify-between text-xs text-gray-500 mb-1">
<span>{t('processing.preprocessing.strength.label')}</span>
<span className="font-medium text-gray-700">
{config.sharpen_strength.toFixed(1)} ({getStrengthLabel(config.sharpen_strength, 'sharpen')})
</span>
</div>
<input
type="range"
min="0.5"
max="2.0"
step="0.1"
value={config.sharpen_strength}
onChange={(e) => handleConfigChange('sharpen_strength', parseFloat(e.target.value))}
disabled={disabled}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
<div className="flex justify-between text-xs text-gray-400 mt-0.5">
<span>0.5</span>
<span>2.0</span>
</div>
</div>
)}
</div>
{/* Binarize Toggle - Hidden by default, shown only in advanced mode */}
<details className="pt-2">
<summary className="text-xs text-gray-500 cursor-pointer hover:text-gray-700">
{t('processing.preprocessing.advanced')}
</summary>
<div className="mt-2 pl-2 border-l-2 border-gray-200">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.binarize}
onChange={(e) => handleConfigChange('binarize', e.target.checked)}
disabled={disabled}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">
{t('processing.preprocessing.binarize')}
</span>
<span className="text-xs text-orange-600">
({t('processing.preprocessing.binarizeWarning')})
</span>
</label>
</div>
</details>
</div>
)}

View File

@@ -68,9 +68,9 @@
"title": "影像前處理",
"mode": {
"auto": "自動模式",
"autoDesc": "系統自動分析影像品質,決定最佳的前處理方式",
"autoDesc": "系統自動分析影像品質,決定最佳的前處理方式和強度",
"manual": "手動模式",
"manualDesc": "手動選擇前處理選項,完全控制處理流程",
"manualDesc": "手動選擇前處理選項和強度,完全控制處理流程",
"disabled": "停用前處理",
"disabledDesc": "不進行任何前處理,直接使用原始影像"
},
@@ -84,8 +84,16 @@
"clahe": "CLAHE 自適應均衡化"
},
"sharpen": "邊緣銳化",
"strength": {
"label": "強度",
"subtle": "輕微",
"normal": "正常",
"strong": "強",
"maximum": "最強"
},
"advanced": "進階選項",
"binarize": "二值化處理",
"binarizeWarning": "可能影響顏色資訊",
"binarizeWarning": "不建議使用",
"note": "前處理僅影響版面偵測階段,用於改善表格和文字區塊的識別。原始影像仍用於最終的 OCR 文字提取,確保最佳識別品質。"
}
},

View File

@@ -39,7 +39,9 @@ export default function ProcessingPage() {
const [preprocessingMode, setPreprocessingMode] = useState<PreprocessingMode>('auto')
const [preprocessingConfig, setPreprocessingConfig] = useState<PreprocessingConfig>({
contrast: 'clahe',
contrast_strength: 1.0,
sharpen: true,
sharpen_strength: 1.0,
binarize: false,
})

View File

@@ -100,7 +100,9 @@ export type PreprocessingContrast = 'none' | 'histogram' | 'clahe'
*/
export interface PreprocessingConfig {
contrast: PreprocessingContrast
contrast_strength: number // 0.5-3.0, default 1.0
sharpen: boolean
sharpen_strength: number // 0.5-2.0, default 1.0
binarize: boolean
}