Spaces:
Sleeping
Sleeping
| """ | |
| core/preprocess.py | |
| ์ด๋ฏธ์ง ์ ์ฒ๋ฆฌ ๊ณ์ธต. | |
| ์ญํ : | |
| - ํ์ผ ์ ํจ์ฑ ๊ฒ์ฌ (๊ฒฝ๋ก, ํ์ฅ์, ์ฝ๊ธฐ ๊ฐ๋ฅ ์ฌ๋ถ) | |
| - OpenCV ๊ธฐ๋ฐ ์ด๋ฏธ์ง ํ์ง ๊ฐ์ (Audiveris OMR ์ ํ๋ ํฅ์ ๋ชฉ์ ) | |
| ์ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ (apply_preprocessing): | |
| 1. ์ด๋ฏธ์ง ๋ก๋ ๋ฐ ๊ฒ์ฆ (cv2.imread) | |
| 2. Grayscale ๋ณํ | |
| 3. ๊ฐ๋ฒผ์ด ๋ ธ์ด์ฆ ์ ๊ฑฐ (GaussianBlur 3x3) | |
| 4. ์ด์งํ (Otsu ๋๋ Adaptive Threshold) | |
| 5. [์ ํ์ ] Deskew โ ๊ธฐ์ธ๊ธฐ ๋ณด์ (์คํ์ , deskew_enabled=True ์) | |
| ์ฃผ์: | |
| - apply_preprocessing์ opencv-python ํ์ (pip install opencv-python) | |
| - opencv ๋ฏธ์ค์น ์ PreprocessError ๋ฐ์ โ pipeline์์ fallback ์ฒ๋ฆฌ | |
| - mock ๋ชจ๋์์๋ pipeline์ด ์ด ํจ์๋ฅผ ํธ์ถํ์ง ์์ | |
| ํ๊ณ: | |
| - deskew๋ ยฑ10๋ ๋ฒ์์์๋ง ๋์. ๊ณผ๋ํ๊ฒ ๊ธฐ์ธ์ด์ง ์ด๋ฏธ์ง๋ ๋ณด์ ๋ถ๊ฐ | |
| - ์๊ธ์จ, ๊ทธ๋ฆผ์, ์ ํด์๋ ์ด๋ฏธ์ง๋ ์ด์งํ ํ ์คํ๋ ค ํ์ง ์ ํ ๊ฐ๋ฅ | |
| - ์ฐํ ํ๊ธฐ ์ ๋ณด๋ Adaptive Threshold๊ฐ ๋ ์ ํฉํ ์ ์์ | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import TYPE_CHECKING | |
| if TYPE_CHECKING: | |
| from .models import ConvertOptions | |
| SUPPORTED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".tiff", ".tif", ".bmp", ".pdf"} | |
| class PreprocessError(Exception): | |
| """์ ์ฒ๋ฆฌ ๋จ๊ณ ์ค๋ฅ.""" | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # ํ์ผ ๊ฒ์ฆ (opencv ๋ถํ์, ํญ์ ๋์) | |
| # --------------------------------------------------------------------------- | |
| def validate_image_path(input_path: str) -> Path: | |
| """ | |
| ์ ๋ ฅ ํ์ผ ๊ฒฝ๋ก๋ฅผ ๊ฒ์ฆํ๊ณ Path ๊ฐ์ฒด๋ฅผ ๋ฐํ. | |
| Raises: | |
| PreprocessError: ํ์ผ์ด ์๊ฑฐ๋ ์ง์๋์ง ์๋ ํ์์ธ ๊ฒฝ์ฐ | |
| """ | |
| path = Path(input_path) | |
| if not path.exists(): | |
| raise PreprocessError(f"ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค: {input_path}") | |
| if not path.is_file(): | |
| raise PreprocessError(f"ํ์ผ์ด ์๋๋๋ค: {input_path}") | |
| ext = path.suffix.lower() | |
| if ext not in SUPPORTED_EXTENSIONS: | |
| raise PreprocessError( | |
| f"์ง์ํ์ง ์๋ ํ์ผ ํ์: {ext}. " | |
| f"์ง์ ํ์: {', '.join(sorted(SUPPORTED_EXTENSIONS))}" | |
| ) | |
| return path | |
| def get_file_info(path: Path) -> dict: | |
| """ํ์ผ ๊ธฐ๋ณธ ์ ๋ณด๋ฅผ ๋ฐํ.""" | |
| stat = path.stat() | |
| return { | |
| "filename": path.name, | |
| "extension": path.suffix.lower(), | |
| "size_bytes": stat.st_size, | |
| "absolute_path": str(path.resolve()), | |
| } | |
| def preprocess_image(input_path: str) -> dict: | |
| """ | |
| ํ์ผ ์ ํจ์ฑ ๊ฒ์ฌ๋ง ์ํ. ์ ์ฒ๋ฆฌ ์์ด ์๋ณธ ๊ฒฝ๋ก๋ฅผ ๋ฐํ. | |
| pipeline์์ ๋จผ์ ํธ์ถํ์ฌ ํ์ผ์ ๊ฒ์ฆํ ๋ค, | |
| Audiveris ๋ชจ๋๋ผ๋ฉด apply_preprocessing()์ ์ถ๊ฐ ํธ์ถํ๋ค. | |
| """ | |
| path = validate_image_path(input_path) | |
| info = get_file_info(path) | |
| info["preprocessed_path"] = str(path.resolve()) | |
| info["preprocessing_applied"] = [] | |
| return info | |
| # --------------------------------------------------------------------------- | |
| # OpenCV ์ ์ฒ๋ฆฌ (opencv-python ํ์) | |
| # --------------------------------------------------------------------------- | |
| def apply_preprocessing( | |
| input_path: str, | |
| output_path: str, | |
| options: ConvertOptions, | |
| debug_dir: str = "", | |
| ) -> dict: | |
| """ | |
| OpenCV ๊ธฐ๋ฐ ์ด๋ฏธ์ง ์ ์ฒ๋ฆฌ๋ฅผ ์ ์ฉํ๊ณ ๊ฒฐ๊ณผ๋ฅผ output_path์ ์ ์ฅ. | |
| Args: | |
| input_path: ์๋ณธ ์ด๋ฏธ์ง ๊ฒฝ๋ก | |
| output_path: ์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ ์ ์ฅ ๊ฒฝ๋ก (.png ๊ถ์ฅ) | |
| options: ConvertOptions (binarize_method, deskew_enabled ์ฌ์ฉ) | |
| Returns: | |
| dict: ์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ ์ ๋ณด | |
| - applied: True | |
| - steps: ์ ์ฉ๋ ๋จ๊ณ ๋ชฉ๋ก | |
| - input_size: (w, h) | |
| - output_path: ๊ฒฐ๊ณผ ํ์ผ ๊ฒฝ๋ก | |
| Raises: | |
| PreprocessError: opencv ๋ฏธ์ค์น, ์ด๋ฏธ์ง ์ฝ๊ธฐ ์คํจ, ์ ์ฅ ์คํจ ์ | |
| """ | |
| try: | |
| import cv2 | |
| import numpy as np | |
| except ImportError: | |
| raise PreprocessError( | |
| "opencv-python์ด ์ค์น๋์ง ์์์ต๋๋ค.\n" | |
| "์ค์น ๋ช ๋ น: pip install opencv-python" | |
| ) | |
| img = cv2.imread(input_path, cv2.IMREAD_COLOR) | |
| if img is None: | |
| raise PreprocessError( | |
| f"์ด๋ฏธ์ง๋ฅผ ์ฝ์ ์ ์์ต๋๋ค: {input_path}\n" | |
| f"ํ์ผ์ด ์์๋์๊ฑฐ๋ ์ง์ํ์ง ์๋ ํ์์ผ ์ ์์ต๋๋ค." | |
| ) | |
| h, w = img.shape[:2] | |
| applied: list[str] = [] | |
| def _save_step(name: str, img_data) -> None: | |
| if debug_dir: | |
| cv2.imwrite(str(Path(debug_dir) / name), img_data) | |
| # 1. Grayscale | |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) | |
| applied.append("grayscale") | |
| _save_step("step_01_grayscale.png", gray) | |
| # 2. ๋ ธ์ด์ฆ ์ ๊ฑฐ (๊ฐ๋ฒผ์ด Gaussian blur, ์ ํ์ ) | |
| if options.blur_enabled: | |
| denoised = cv2.GaussianBlur(gray, (3, 3), 0) | |
| applied.append("gaussian_blur_3x3") | |
| _save_step("step_02_gaussian_blur.png", denoised) | |
| else: | |
| denoised = gray | |
| # 3. ์ด์งํ (์ ํ์ โ ๊ธฐ๋ณธ off, Audiveris ์์ฒด ์ด์งํ ์ ๋ขฐ) | |
| processed = denoised | |
| if options.binarize_enabled: | |
| processed = _binarize(denoised, options.binarize_method) | |
| applied.append(f"binarize:{options.binarize_method}") | |
| _save_step(f"step_03_binarized_{options.binarize_method}.png", processed) | |
| # 4. Deskew (์ ํ์ ) | |
| if options.deskew_enabled: | |
| processed, angle = _deskew(processed) | |
| if abs(angle) > 0.01: | |
| applied.append(f"deskew:{angle:.2f}deg") | |
| _save_step(f"step_04_deskew_{angle:.2f}deg.png", processed) | |
| else: | |
| applied.append("deskew:skipped(angle<0.5)") | |
| # ์ ์ฅ | |
| ok = cv2.imwrite(output_path, processed) | |
| if not ok: | |
| raise PreprocessError( | |
| f"์ ์ฒ๋ฆฌ ์ด๋ฏธ์ง ์ ์ฅ ์คํจ: {output_path}\n" | |
| f"์ถ๋ ฅ ๋๋ ํ ๋ฆฌ๊ฐ ์กด์ฌํ๋์ง ํ์ธํ์ธ์." | |
| ) | |
| return { | |
| "applied": True, | |
| "steps": applied, | |
| "input_path": input_path, | |
| "output_path": output_path, | |
| "input_size": (w, h), | |
| } | |
| def _binarize(gray_img, method: str): | |
| """ | |
| Grayscale ์ด๋ฏธ์ง๋ฅผ ์ด์งํ. | |
| method: | |
| "otsu" : ์ ์ญ Otsu ์๊ณ๊ฐ. ๋ช ์ ๋๋น๊ฐ ๋ถ๋ช ํ ์ค์บ ์ ๋ณด์ ์ ํฉ. | |
| "adaptive" : ์ง์ญ Adaptive Threshold. ์กฐ๋ช ๋ถ๊ท ์ผ / ์ฐํ ํ๊ธฐ ์ ๋ณด์ ์ ํฉ. | |
| """ | |
| try: | |
| import cv2 | |
| except ImportError: | |
| raise PreprocessError("opencv-python์ด ํ์ํฉ๋๋ค.") | |
| if method == "adaptive": | |
| return cv2.adaptiveThreshold( | |
| gray_img, 255, | |
| cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY, | |
| blockSize=15, C=8, | |
| ) | |
| # default: otsu | |
| _, binary = cv2.threshold( | |
| gray_img, 0, 255, | |
| cv2.THRESH_BINARY + cv2.THRESH_OTSU, | |
| ) | |
| return binary | |
| def _deskew(binary_img): | |
| """ | |
| ์ด์งํ๋ ์ด๋ฏธ์ง์ ๊ธฐ์ธ๊ธฐ๋ฅผ ๋ณด์ . | |
| Returns: | |
| (corrected_img, angle_degrees) | |
| ํ๊ณ: | |
| - ยฑ10๋ ์ด๋ด์ ๊ธฐ์ธ๊ธฐ๋ง ๋ณด์ . ๋ฒ์ ์ด๊ณผ ์ ์๋ณธ ๋ฐํ. | |
| - ์ ๋ณด ์ ์ฒด๊ฐ ๊ธฐ์ธ์ด์ง ๊ฒฝ์ฐ์๋ง ์ ํจ. ๊ฐ๋ณ ๋ณดํ ๊ธฐ์ธ๊ธฐ๋ ๋ฏธ์ฒ๋ฆฌ. | |
| - ์ด๋์ด ํฝ์ ์ด ๋๋ฌด ์ ์ผ๋ฉด ๊ฐ๋ ์ถ์ ๋ถ๊ฐ โ ์๋ณธ ๋ฐํ. | |
| """ | |
| try: | |
| import cv2 | |
| import numpy as np | |
| except ImportError: | |
| return binary_img, 0.0 | |
| # ์ด๋์ด ํฝ์ ์ขํ ์ถ์ถ (์ด์งํ๋ ์ด๋ฏธ์ง: ์ ๋ณด ์ /์ํ = 0) | |
| dark_coords = np.column_stack(np.where(binary_img < 128)) | |
| if len(dark_coords) < 200: | |
| return binary_img, 0.0 | |
| # (row, col) โ (x=col, y=row) ๋ณํ ํ minAreaRect | |
| points = dark_coords[:, ::-1].astype(np.float32) | |
| rect = cv2.minAreaRect(points) | |
| angle = rect[2] # range: (-90, 0] | |
| # (-90, -45] โ ์ธ๋ก ๋ฐฉํฅ ๋ฐ์ค โ +90 ๋ณด์ | |
| if angle < -45: | |
| angle = 90.0 + angle | |
| # ์ด์ angle โ (-45, 45) | |
| # ๋๋ฌด ํฌ๋ฉด ๋ณด์ ๋ถ๊ฐ (์์ผ๋ก ์ฐํ ์ด๋ฏธ์ง ๋ฑ) | |
| if abs(angle) > 10.0: | |
| return binary_img, 0.0 | |
| # ๋๋ฌด ์์ผ๋ฉด ์๋ฏธ ์์ | |
| if abs(angle) < 0.5: | |
| return binary_img, 0.0 | |
| h, w = binary_img.shape | |
| center = (w // 2, h // 2) | |
| M = cv2.getRotationMatrix2D(center, angle, 1.0) | |
| corrected = cv2.warpAffine( | |
| binary_img, M, (w, h), | |
| flags=cv2.INTER_LINEAR, | |
| borderMode=cv2.BORDER_REPLICATE, | |
| ) | |
| return corrected, angle | |