File size: 5,432 Bytes
0d3fe75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""Подготовка датасета: нарезка панелей кузова на патчи и разбиение train/val.

Из исходных фото 4000x1846 (плоские панели — образцы окраски) автоматически
вырезается область панели (по яркости/контрасту), затем нарезаются перекрытые
патчи 512x512. Дефектные образцы → класс 1, образцы без дефектов → класс 0.

Запуск:  python -m src.prepare_data
"""
from __future__ import annotations
import shutil
import random
from pathlib import Path

import cv2
import numpy as np

from . import config as C


def imread_unicode(path: Path) -> np.ndarray | None:
    """cv2.imread не понимает Cyrillic-пути на Windows — обходим через np.fromfile."""
    try:
        data = np.fromfile(str(path), dtype=np.uint8)
        if data.size == 0:
            return None
        return cv2.imdecode(data, cv2.IMREAD_COLOR)
    except Exception:
        return None


def imwrite_unicode(path: Path, img: np.ndarray, params=None) -> bool:
    ext = path.suffix or ".jpg"
    ok, buf = cv2.imencode(ext, img, params or [])
    if not ok:
        return False
    buf.tofile(str(path))
    return True


def crop_panel(bgr: np.ndarray) -> np.ndarray:
    """Вырезает прямоугольник панели из светлого фона.

    На исходных фото панель окраски лежит на белом столе. Бинаризуем изображение
    по Оцу, берём наибольший контур и вырезаем его bounding box.
    """
    gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
    blur = cv2.GaussianBlur(gray, (9, 9), 0)
    # Панель темнее белого фона -> инвертированный Оцу
    _, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    th = cv2.morphologyEx(th, cv2.MORPH_OPEN, np.ones((15, 15), np.uint8))
    contours, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not contours:
        return bgr
    c = max(contours, key=cv2.contourArea)
    x, y, w, h = cv2.boundingRect(c)
    # отступаем внутрь, чтобы не зацепить край/тень/наклейку
    pad = int(0.04 * min(w, h))
    x1, y1 = max(0, x + pad), max(0, y + pad)
    x2, y2 = min(bgr.shape[1], x + w - pad), min(bgr.shape[0], y + h - pad)
    if x2 - x1 < 200 or y2 - y1 < 200:
        return bgr
    return bgr[y1:y2, x1:x2]


def slice_patches(panel: np.ndarray, size: int, stride: int) -> list[np.ndarray]:
    """Нарезает панель на квадратные патчи с заданным шагом."""
    h, w = panel.shape[:2]
    if h < size or w < size:
        # маленькая панель: один центральный ресайз
        return [cv2.resize(panel, (size, size))]
    patches = []
    ys = list(range(0, h - size + 1, stride))
    xs = list(range(0, w - size + 1, stride))
    if ys[-1] != h - size:
        ys.append(h - size)
    if xs[-1] != w - size:
        xs.append(w - size)
    for y in ys:
        for x in xs:
            patches.append(panel[y:y + size, x:x + size])
    return patches


def main(val_ratio: float = 0.2, seed: int = C.SEED) -> None:
    random.seed(seed)
    src_pairs = [
        (C.SRC_DEFECT, "defect"),
        (C.SRC_CLEAN, "clean"),
    ]

    # пересобираем выходные каталоги
    if C.DATA_PATCHES.exists():
        shutil.rmtree(C.DATA_PATCHES)
    for split in ("train", "val"):
        for cls in ("defect", "clean"):
            (C.DATA_PATCHES / split / cls).mkdir(parents=True, exist_ok=True)

    # также продублируем оригиналы в data/raw для удобства
    C.DATA_RAW.mkdir(parents=True, exist_ok=True)
    for src_dir, cls in src_pairs:
        out = C.DATA_RAW / cls
        out.mkdir(exist_ok=True)
        for f in src_dir.iterdir():
            if f.suffix.lower() in {".jpg", ".jpeg", ".png"}:
                shutil.copy2(f, out / f.name)

    total = {"train": 0, "val": 0}
    for src_dir, cls in src_pairs:
        files = [f for f in src_dir.iterdir()
                 if f.suffix.lower() in {".jpg", ".jpeg", ".png"}]
        random.shuffle(files)
        n_val = max(1, int(len(files) * val_ratio))
        val_files = set(files[:n_val])

        for f in files:
            split = "val" if f in val_files else "train"
            img = imread_unicode(f)
            if img is None:
                print(f"[skip] не удалось прочитать {f}")
                continue
            panel = crop_panel(img) if C.PANEL_CROP else img
            patches = slice_patches(panel, C.PATCH_SIZE, C.PATCH_STRIDE)
            stem = f.stem
            for i, p in enumerate(patches):
                out_path = C.DATA_PATCHES / split / cls / f"{stem}_{i:03d}.jpg"
                imwrite_unicode(out_path, p, [cv2.IMWRITE_JPEG_QUALITY, 92])
                total[split] += 1
            print(f"[{split}/{cls}] {f.name}: {len(patches)} патчей")

    print(f"\nИтого патчей: train={total['train']}  val={total['val']}")
    print(f"Готовый датасет: {C.DATA_PATCHES}")


if __name__ == "__main__":
    main()