presets.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. """
  2. 银行流水等场景的水印去除预设(页级 / 单元格级)。
  3. 对外 YAML 只需 method、enabled、contrast_enhancement 等少量键;
  4. mask / hough / adaptive 细参由此模块提供,避免配置漂移。
  5. """
  6. from __future__ import annotations
  7. import copy
  8. from typing import Any, Dict, Literal, Optional
  9. Scope = Literal["page", "cell"]
  10. Method = Literal["threshold", "masked", "masked_adaptive"]
  11. _DETECT_DEFAULT: Dict[str, Any] = {
  12. "ratio_threshold": 0.025,
  13. "midtone_low": 100,
  14. "midtone_high": 220,
  15. "check_diagonal": True,
  16. "diagonal_angle_range": (30, 60),
  17. }
  18. _MASK_PAGE: Dict[str, Any] = {
  19. "mask_mode": "light_on_white",
  20. "text_protect_gray_max": 130,
  21. "light_gray_low": 236,
  22. "light_gray_high": 253,
  23. "whiten_gray_low": 200,
  24. "direction_filter": "hough",
  25. "morph_close_kernel": 0,
  26. "morph_dilate_kernel": 0,
  27. "min_component_area": 200,
  28. "debug_block_maps": False,
  29. "debug_block_size": 48,
  30. "hough_midtone_low": 200,
  31. "hough_midtone_high": 254,
  32. "hough_canny_low": 30,
  33. "hough_canny_high": 100,
  34. "hough_threshold": 25,
  35. "hough_min_line_length": 35,
  36. "hough_max_line_gap": 18,
  37. "hough_line_thickness": 12,
  38. "hough_band_dilate_radius": 16,
  39. "hough_use_angle_statistics": True,
  40. "hough_angle_tolerance": 5.0,
  41. "hough_secondary_peak_ratio": 0.35,
  42. "hough_min_length_percentile": 25.0,
  43. "midtone_low": 95,
  44. "midtone_high": 235,
  45. "remove_horizontal_vertical": True,
  46. "diagonal_enhance": True,
  47. "diagonal_kernel_length": 25,
  48. "horizontal_kernel_length": 35,
  49. "vertical_kernel_length": 35,
  50. "morph_open_kernel": 2,
  51. "dmorph_close_kernel": 3,
  52. "text_protect_percentile": 10.0,
  53. "background_threshold": 248,
  54. "seal_protect": True,
  55. }
  56. _MASK_CELL: Dict[str, Any] = {
  57. **_MASK_PAGE,
  58. "min_component_area": 60,
  59. "hough_min_line_length": 18,
  60. "hough_max_line_gap": 12,
  61. "hough_line_thickness": 8,
  62. "hough_band_dilate_radius": 10,
  63. "hough_threshold": 20,
  64. "text_protect_gray_max": 125,
  65. }
  66. _ADAPTIVE_PAGE: Dict[str, Any] = {
  67. "whiten_mode": "mask_fill",
  68. "text_percentile": 10.0,
  69. "watermark_percentile": 70.0,
  70. "background_percentile": 95.0,
  71. "background_threshold": 248,
  72. "wm_margin": 12,
  73. "text_protect_max": 120,
  74. }
  75. _ADAPTIVE_CELL: Dict[str, Any] = {
  76. **_ADAPTIVE_PAGE,
  77. "text_protect_max": 110,
  78. "wm_margin": 10,
  79. }
  80. _CONTRAST_PAGE_DEFAULT: Dict[str, Any] = {
  81. "enabled": True,
  82. "method": "text_restore",
  83. "text_black_target": 85,
  84. "background_threshold": 248,
  85. "text_lo_percentile": 1.0,
  86. "text_hi_percentile": 99.0,
  87. }
  88. _CONTRAST_CELL_DEFAULT: Dict[str, Any] = {
  89. "enabled": False,
  90. "method": "text_restore",
  91. "text_black_target": 88,
  92. "background_threshold": 248,
  93. "text_lo_percentile": 1.0,
  94. "text_hi_percentile": 99.0,
  95. }
  96. def _base_preset(scope: Scope, method: Method) -> Dict[str, Any]:
  97. mask = _MASK_CELL if scope == "cell" else _MASK_PAGE
  98. adaptive = _ADAPTIVE_CELL if scope == "cell" else _ADAPTIVE_PAGE
  99. contrast = (
  100. copy.deepcopy(_CONTRAST_CELL_DEFAULT)
  101. if scope == "cell"
  102. else copy.deepcopy(_CONTRAST_PAGE_DEFAULT)
  103. )
  104. threshold = 175 if scope == "page" else 170
  105. cfg: Dict[str, Any] = {
  106. "enabled": True,
  107. "detect_before_remove": scope == "page",
  108. "detect": copy.deepcopy(_DETECT_DEFAULT),
  109. "method": method,
  110. "threshold": threshold,
  111. "morph_close_kernel": 0,
  112. "contrast_enhancement": contrast,
  113. "debug_options": {
  114. "enabled": False,
  115. "save_compare": True,
  116. "image_format": "png",
  117. "subdir": "watermark_removal",
  118. },
  119. }
  120. if method in ("masked", "masked_adaptive"):
  121. cfg["mask"] = copy.deepcopy(mask)
  122. if method == "masked_adaptive":
  123. cfg["adaptive"] = copy.deepcopy(adaptive)
  124. return cfg
  125. PAGE_WATERMARK_PRESETS: Dict[str, Dict[str, Any]] = {
  126. "threshold": _base_preset("page", "threshold"),
  127. "masked": _base_preset("page", "masked"),
  128. "masked_adaptive": _base_preset("page", "masked_adaptive"),
  129. }
  130. CELL_WATERMARK_PRESETS: Dict[str, Dict[str, Any]] = {
  131. "threshold": _base_preset("cell", "threshold"),
  132. "masked": _base_preset("cell", "masked"),
  133. "masked_adaptive": _base_preset("cell", "masked_adaptive"),
  134. }
  135. def get_preset(scope: Scope, method: str) -> Dict[str, Any]:
  136. method = method or "masked_adaptive"
  137. presets = CELL_WATERMARK_PRESETS if scope == "cell" else PAGE_WATERMARK_PRESETS
  138. if method not in presets:
  139. method = "masked_adaptive"
  140. return copy.deepcopy(presets[method])
  141. def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
  142. out = copy.deepcopy(base)
  143. for k, v in override.items():
  144. if k in out and isinstance(out[k], dict) and isinstance(v, dict):
  145. out[k] = _deep_merge(out[k], v)
  146. else:
  147. out[k] = copy.deepcopy(v)
  148. return out
  149. def merge_watermark_config(
  150. scope: Scope,
  151. user_cfg: Optional[Dict[str, Any]] = None,
  152. *,
  153. method: Optional[str] = None,
  154. ) -> Dict[str, Any]:
  155. """将用户 YAML 片段与 scope 预设合并;保留旧版 mask/adaptive 全量覆盖能力。"""
  156. user_cfg = user_cfg or {}
  157. m = method or user_cfg.get("method") or "masked_adaptive"
  158. merged = get_preset(scope, str(m))
  159. for key in (
  160. "enabled",
  161. "detect_before_remove",
  162. "method",
  163. "threshold",
  164. "morph_close_kernel",
  165. ):
  166. if key in user_cfg:
  167. merged[key] = user_cfg[key]
  168. for nested in ("detect", "mask", "adaptive", "contrast_enhancement", "debug_options"):
  169. if nested in user_cfg and isinstance(user_cfg[nested], dict):
  170. merged[nested] = _deep_merge(merged.get(nested) or {}, user_cfg[nested])
  171. if method:
  172. merged["method"] = method
  173. return merged