상세 컨텐츠

본문 제목

챗지피티를 이용한 파이썬 바이브 코딩 - 미점등 LED 픽셀 위치 파악하기

Data Science/Python

by FDG 2026. 2. 21. 23:43

본문

챗지피티, 클로드, 퍼플렉시티, 제미나이, 그록으로 간단하게 요청해서 코딩을 시작했는데,

모두 제대로 검출을 하지 못했음. 문제 있는 부분 수정해 달라고 했지만, 내가 생각하는 바가 정확하게 전달이 되지 않는 느낌 받음.

 

챗지피티로 다시 시작했고 내가 "문제"를 인식하는 과정을 순서대로 알려주고 단계별로 결과를 출력하게 해서 점검하고 문제없으면 확장하는 방식으로 바이브 코딩 진행.

 

느낀 점은 구체적으로 조건을 주고, 단계별로 확인을 하면서 다음 단계로 넘어가야 한다는 점.

복잡할수록 단계 정해서 시켜야 함. 어떤 문제가 발생했을 때도 구체적으로 알려줘야 코드 개선이 됨.

 

"문제"는 이미지를 보고 이빨이 빠진 곳의 위치를 찾으라는 것이었는데, x, y 개수가 몇 개인지를 고정한 후부터 정확도가 올라갔음.

사람이라면 전체가 몇 개인지, x, y 개수가 몇 개인지 몰라도 점으로 선을 만들어 따라가면서 좌우 비교해 보면 빠진 느낌을 받을 수가 있고 그곳이 이빨 빠진 곳이라는 감을 잡을 수 있는데...

바이브 코딩에서는 x, y 개수가 몇 개인지를 알려주기 전까지는 정확도가 낮았음.  

 

AGI가 나오기 전까지는 구체적으로 지시를 할 수 있는 사람이 능력 있는 사람일 거 같음.

대학에서도 바이브 코딩으로 문제를 해결하는 과목이 생길 것 같음.

문제 이미지. 디지털 카메라로 찍어서 회전돼 있고 렌즈 왜곡이 있음.

 

distorsion_image.jpg
0.35MB
픽셀 찾아서 GRID 인식하는 과정

 

이빨 빠진 곳 확인한 이미지
등간격 GRID로 미점등 위치를 재구성!

import cv2
import numpy as np

# ============================================================
# Distorted LED Grid Analyzer (with debug exports)
# (modified for separate grid_row_n, grid_col_n)
# ============================================================

# 아래 코드는 카메라로 찍혀 왜곡(회전/기울어짐/렌즈왜곡)이 있는 LED 점등 사진에서
# 흰색으로 켜진 LED 점(블롭)을 검출하고
# 그 점들이 **50×50 격자(행/열)**라는 가정 하에
# **행 곡선(행 라인), 열 곡선(열 라인)**을 각각 모델링(다항식+RANSAC)해서
# 각 격자 교차점에 LED가 실제로 존재하는지(최근접 거리로) 판단하고
# **꺼진 위치(row,col + 이미지좌표)**를 저장하는 파이프라인입니다.
    
# ---------- colors (BGR) ----------
PINK  = (255, 0, 255)
GREEN = (0, 255, 0)
RED   = (0, 0, 255)
CYAN  = (255, 255, 0)   # rows
WHITE = (255, 255, 255)
YELLOW= (0, 255, 255)   # cols

def detect_led_components(img_bgr,
                          thr_mode="otsu", fixed_thr=220,
                          morph_open=1,
                          area_band=0.60,
                          max_led=5000):
    gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
    gray = cv2.GaussianBlur(gray, (3, 3), 0)

    if thr_mode == "otsu":
        _, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    elif thr_mode == "fixed":
        _, bw = cv2.threshold(gray, fixed_thr, 255, cv2.THRESH_BINARY)
    else:
        raise ValueError("thr_mode must be 'otsu' or 'fixed'")

    if morph_open and morph_open > 0:
        k = 2 * morph_open + 1
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k))
        bw = cv2.morphologyEx(bw, cv2.MORPH_OPEN, kernel, iterations=1)

    nlab, labels, stats, centroids = cv2.connectedComponentsWithStats(bw, connectivity=8)
    if nlab <= 1:
        return bw, labels, stats, centroids, []

    areas = stats[1:, cv2.CC_STAT_AREA].astype(np.float32)
    med = float(np.median(areas))
    low = med * (1 - area_band)
    high = med * (1 + area_band)

    keep = (areas >= low) & (areas <= high)
    kept_labels = (np.where(keep)[0] + 1).astype(int)

    if len(kept_labels) > max_led:
        kept_areas = areas[keep]
        diffs = np.abs(kept_areas - med)
        order = np.argsort(diffs)[:max_led]
        kept_labels = kept_labels[order]

    return bw, labels, stats, centroids, kept_labels.tolist()

def minrect_top_edge(points_xy: np.ndarray):
    rect = cv2.minAreaRect(points_xy.astype(np.float32))
    box = cv2.boxPoints(rect).astype(np.float32)

    idx = np.argsort(box[:, 1])
    top2 = box[idx[:2]]

    if top2[0, 0] <= top2[1, 0]:
        A, B = top2[0], top2[1]
    else:
        A, B = top2[1], top2[0]

    v = B - A
    L = float(np.linalg.norm(v) + 1e-9)
    e = (v / L).astype(np.float32)
    n = np.array([-e[1], e[0]], dtype=np.float32)
    n /= (np.linalg.norm(n) + 1e-9)
    return box, A.astype(np.float32), B.astype(np.float32), e, n

def to_edge_coords(pts_xy, origin, e, n):
    d = pts_xy - origin
    x = d @ e
    y = d @ n
    return x.astype(np.float32), y.astype(np.float32)

def from_edge_coords(x, y, origin, e, n):
    return (origin[None, :] + np.outer(x, e) + np.outer(y, n)).astype(np.float32)

def kmeans_1d(values, k, attempts=10, seed=0):
    v = values.astype(np.float32).reshape(-1, 1)
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, 1e-4)
    cv2.setRNGSeed(int(seed))
    _, labels, centers = cv2.kmeans(v, k, None, criteria, attempts, cv2.KMEANS_PP_CENTERS)
    labels = labels.reshape(-1)
    centers = centers.reshape(-1)
    order = np.argsort(centers)
    centers_sorted = centers[order]
    inv = np.empty_like(order)
    inv[order] = np.arange(k)
    labels_sorted = inv[labels]
    return centers_sorted, labels_sorted

def fit_poly_and_score(x, y, degree):
    coef = np.polyfit(x, y, degree)
    yhat = np.polyval(coef, x)
    ss_res = float(np.sum((y - yhat) ** 2))
    ss_tot = float(np.sum((y - np.mean(y)) ** 2) + 1e-9)
    r2 = 1.0 - ss_res / ss_tot
    rmse = float(np.sqrt(ss_res / max(1, len(y))))
    return coef, r2, rmse

def polyfit_ransac(x, y, degree=2, iters=800, inlier_sigma=3.0, min_inliers=20, seed=0):
    rng = np.random.default_rng(seed)
    N = len(x)
    if N < degree + 2:
        return None, np.zeros(N, dtype=bool), -np.inf, np.inf

    med = np.median(y)
    mad = np.median(np.abs(y - med)) + 1e-6
    thr = float(inlier_sigma * mad)

    best = None
    sample_n = degree + 2

    for _ in range(iters):
        try:
            idx = rng.choice(N, size=sample_n, replace=False)
        except ValueError:
            break
        try:
            coef0 = np.polyfit(x[idx], y[idx], degree)
        except np.linalg.LinAlgError:
            continue

        resid = np.abs(y - np.polyval(coef0, x))
        mask = resid <= thr
        if mask.sum() < min_inliers:
            continue

        try:
            coef, r2, rmse = fit_poly_and_score(x[mask], y[mask], degree)
        except np.linalg.LinAlgError:
            continue

        score = float(mask.sum()) + 5.0 * r2 - 0.02 * rmse
        if best is None or score > best[0]:
            best = (score, coef, mask, r2, rmse)

    if best is None:
        coef, r2, rmse = fit_poly_and_score(x, y, degree)
        return coef, np.ones(N, dtype=bool), r2, rmse

    _, coef, mask, r2, rmse = best
    return coef, mask, r2, rmse

def fit_rows_cols(points_xy_img, origin_A, e, n,
                  grid_row_n=50,
                  grid_col_n=50,
                  row_degree=2,
                  col_degree=2,
                  row_band_factor=0.45,
                  col_band_factor=0.45,
                  inlier_sigma=3.0,
                  seed=0):
    x_all, y_all = to_edge_coords(points_xy_img, origin=origin_A, e=e, n=n)

    # 행/열 개수 분리
    y_centers, _ = kmeans_1d(y_all, grid_row_n, seed=seed + 10)
    x_centers, _ = kmeans_1d(x_all, grid_col_n, seed=seed + 20)

    dy_step = float(np.median(np.diff(np.sort(y_centers))))
    dx_step = float(np.median(np.diff(np.sort(x_centers))))

    row_band = max(2.0, row_band_factor * dy_step)
    col_band = max(2.0, col_band_factor * dx_step)

    rows = []
    cols = []

    # rows
    for r in range(grid_row_n):
        target_y = float(y_centers[r])
        idx = np.where(np.abs(y_all - target_y) <= row_band)[0]
        if len(idx) < 12:
            rows.append(None)
            continue

        xr = x_all[idx]
        yr = y_all[idx]
        coef, inliers, r2, rmse = polyfit_ransac(
            xr, yr, degree=row_degree,
            iters=1200, inlier_sigma=inlier_sigma,
            min_inliers=max(12, int(0.35*len(idx))),
            seed=seed + 100 + r
        )
        if coef is None or inliers.sum() < 8:
            rows.append(None)
            continue

        rows.append({"r": r, "target_y": target_y, "coef": coef, "r2": float(r2), "rmse": float(rmse)})

    # cols
    for c in range(grid_col_n):
        target_x = float(x_centers[c])
        idx = np.where(np.abs(x_all - target_x) <= col_band)[0]
        if len(idx) < 12:
            cols.append(None)
            continue

        yc = y_all[idx]
        xc = x_all[idx]
        coef, inliers, r2, rmse = polyfit_ransac(
            yc, xc, degree=col_degree,
            iters=1200, inlier_sigma=inlier_sigma,
            min_inliers=max(12, int(0.35*len(idx))),
            seed=seed + 200 + c
        )
        if coef is None or inliers.sum() < 8:
            cols.append(None)
            continue

        cols.append({"c": c, "target_x": target_x, "coef": coef, "r2": float(r2), "rmse": float(rmse)})

    frame = {
        "A": origin_A, "e": e, "n": n,
        "x_all": x_all, "y_all": y_all,
        "x_centers": x_centers, "y_centers": y_centers,
        "dx_step": dx_step, "dy_step": dy_step,
        "row_band": row_band, "col_band": col_band,
        "grid_row_n": grid_row_n,
        "grid_col_n": grid_col_n,
    }
    return frame, rows, cols

def draw_rows_cols_on_original(img_bgr, frame, rows, cols,
                               grid_row_n=50,
                               grid_col_n=50,
                               samples=300,
                               thickness=2):
    out = img_bgr.copy()
    A, e, n = frame["A"], frame["e"], frame["n"]

    x_min = float(np.min(frame["x_all"]))
    x_max = float(np.max(frame["x_all"]))
    y_min = float(np.min(frame["y_all"]))
    y_max = float(np.max(frame["y_all"]))

    xs = np.linspace(x_min, x_max, samples).astype(np.float32)
    for r in range(grid_row_n):
        row = rows[r]
        if row is None:
            continue
        ys = np.polyval(row["coef"], xs).astype(np.float32)
        pts = from_edge_coords(xs, ys, origin=A, e=e, n=n).astype(np.int32).reshape(-1, 1, 2)
        cv2.polylines(out, [pts], False, CYAN, thickness, cv2.LINE_AA)

    ys = np.linspace(y_min, y_max, samples).astype(np.float32)
    for c in range(grid_col_n):
        col = cols[c]
        if col is None:
            continue
        xs2 = np.polyval(col["coef"], ys).astype(np.float32)
        pts = from_edge_coords(xs2, ys, origin=A, e=e, n=n).astype(np.int32).reshape(-1, 1, 2)
        cv2.polylines(out, [pts], False, YELLOW, thickness, cv2.LINE_AA)

    return out

def save_debug_lines(img_bgr, frame, rows, cols,
                     grid_row_n=50,
                     grid_col_n=50,
                     samples=500,
                     thickness=2,
                     prefix="line",
                     start_index=0):
    A, e, n = frame["A"], frame["e"], frame["n"]
    x_min = float(np.min(frame["x_all"]))
    x_max = float(np.max(frame["x_all"]))
    y_min = float(np.min(frame["y_all"]))
    y_max = float(np.max(frame["y_all"]))

    idx = int(start_index)

    xs = np.linspace(x_min, x_max, samples).astype(np.float32)
    for r in range(grid_row_n):
        canvas = img_bgr.copy()
        row = rows[r]
        if row is not None:
            ys = np.polyval(row["coef"], xs).astype(np.float32)
            pts = from_edge_coords(xs, ys, origin=A, e=e, n=n).astype(np.int32).reshape(-1, 1, 2)
            cv2.polylines(canvas, [pts], False, CYAN, thickness, cv2.LINE_AA)
            cv2.putText(canvas, f"ROW {r:02d}  R2={row['r2']:.3f} RMSE={row['rmse']:.2f}px",
                        (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, WHITE, 2, cv2.LINE_AA)
        else:
            cv2.putText(canvas, f"ROW {r:02d}  (NOT FOUND)",
                        (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, RED, 2, cv2.LINE_AA)

        cv2.imwrite(f"{prefix}{idx:04d}.jpg", canvas)
        idx += 1

    ys = np.linspace(y_min, y_max, samples).astype(np.float32)
    for c in range(grid_col_n):
        canvas = img_bgr.copy()
        col = cols[c]
        if col is not None:
            xs2 = np.polyval(col["coef"], ys).astype(np.float32)
            pts = from_edge_coords(xs2, ys, origin=A, e=e, n=n).astype(np.int32).reshape(-1, 1, 2)
            cv2.polylines(canvas, [pts], False, YELLOW, thickness, cv2.LINE_AA)
            cv2.putText(canvas, f"COL {c:02d}  R2={col['r2']:.3f} RMSE={col['rmse']:.2f}px",
                        (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, WHITE, 2, cv2.LINE_AA)
        else:
            cv2.putText(canvas, f"COL {c:02d}  (NOT FOUND)",
                        (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, RED, 2, cv2.LINE_AA)

        cv2.imwrite(f"{prefix}{idx:04d}.jpg", canvas)
        idx += 1

    return idx

def compute_intersections_and_occupancy(frame, rows, cols,
                                        grid_row_n=50,
                                        grid_col_n=50,
                                        iters=8,
                                        nn_thr_factor=0.45):
    A, e, n = frame["A"], frame["e"], frame["n"]
    pts_base = np.stack([frame["x_all"], frame["y_all"]], axis=1).astype(np.float32)
    flann = cv2.flann_Index(pts_base, dict(algorithm=1, trees=5))
    nn_thr = float(nn_thr_factor * min(frame["dx_step"], frame["dy_step"]))

    occ = np.zeros((grid_row_n, grid_col_n), dtype=bool)
    node_xy_img  = np.zeros((grid_row_n, grid_col_n, 2), dtype=np.float32)
    nn_dist = np.full((grid_row_n, grid_col_n), np.inf, dtype=np.float32)

    x_centers = frame["x_centers"]
    y_centers = frame["y_centers"]

    for r in range(grid_row_n):
        row = rows[r]
        if row is None:
            continue
        for c in range(grid_col_n):
            col = cols[c]
            if col is None:
                continue

            x = float(x_centers[c])
            y = float(y_centers[r])

            for _ in range(iters):
                y = float(np.polyval(row["coef"], x))
                x = float(np.polyval(col["coef"], y))

            q = np.array([[x, y]], dtype=np.float32)
            _, d2 = flann.knnSearch(q, 1, params={})
            dist = float(np.sqrt(d2[0, 0]))
            nn_dist[r, c] = dist
            occ[r, c] = (dist <= nn_thr)

            p_img = from_edge_coords(np.array([x], np.float32),
                                     np.array([y], np.float32),
                                     origin=A, e=e, n=n)[0]
            node_xy_img[r, c] = p_img

    return occ, node_xy_img, nn_dist, nn_thr

def render_regular_grid_image(occ, cell=20, margin=20, radius=6):
    H = margin * 2 + (occ.shape[0] - 1) * cell
    W = margin * 2 + (occ.shape[1] - 1) * cell
    img = np.zeros((H, W, 3), dtype=np.uint8)
    for r in range(occ.shape[0]):
        y = margin + r * cell
        for c in range(occ.shape[1]):
            x = margin + c * cell
            color = GREEN if occ[r, c] else RED
            cv2.circle(img, (x, y), radius, color, 1, cv2.LINE_AA)
    return img

def run_pipeline(img_path="distorsion_image.jpg",
                 grid_row_n=50,
                 grid_col_n=50,
                 thr_mode="otsu",
                 fixed_thr=220,
                 morph_open=1,
                 area_band=0.60,
                 max_led=5000,
                 row_degree=2,
                 col_degree=2,
                 inlier_sigma=3.0,
                 seed=0,
                 debug=False,
                 debug_prefix="line"):

    img = cv2.imread(img_path, cv2.IMREAD_COLOR)
    if img is None:
        raise FileNotFoundError(img_path)

    bw, labels_map, stats, centroids, kept_labels = detect_led_components(
        img,
        thr_mode=thr_mode,
        fixed_thr=fixed_thr,
        morph_open=morph_open,
        area_band=area_band,
        max_led=max_led
    )
    cv2.imwrite("binary.png", bw)

    if len(kept_labels) < 200:
        raise RuntimeError(f"검출된 점이 너무 적습니다: {len(kept_labels)}")

    pts_img = centroids[np.array(kept_labels, dtype=int)].astype(np.float32)

    box, A, B, e, n = minrect_top_edge(pts_img)

    frame, rows, cols = fit_rows_cols(
        pts_img,
        origin_A=A, e=e, n=n,
        grid_row_n=grid_row_n,
        grid_col_n=grid_col_n,
        row_degree=row_degree,
        col_degree=col_degree,
        row_band_factor=0.45,
        col_band_factor=0.45,
        inlier_sigma=inlier_sigma,
        seed=seed
    )

    overlay_curves = draw_rows_cols_on_original(
        img, frame, rows, cols,
        grid_row_n=grid_row_n,
        grid_col_n=grid_col_n,
        samples=300,
        thickness=1,
    )
    cv2.imwrite("overlay_curves.png", overlay_curves)

    if debug:
        save_debug_lines(
            img_bgr=img,
            frame=frame,
            rows=rows,
            cols=cols,
            grid_row_n=grid_row_n,
            grid_col_n=grid_col_n,
            samples=500,
            thickness=1,
            prefix=debug_prefix,
            start_index=0
        )

    occ, node_xy_img, nn_dist, nn_thr = compute_intersections_and_occupancy(
        frame, rows, cols,
        grid_row_n=grid_row_n,
        grid_col_n=grid_col_n,
        iters=8,
        nn_thr_factor=0.45
    )
    np.save(f"occupancy_{grid_row_n}x{grid_col_n}.npy", occ)

    missing = np.argwhere(~occ)
    
    # 1) 기존: row col만 저장
    # np.savetxt("missing_rc.txt", missing, fmt="%d", header="row col", comments="")

    # 2) 추가: row col x y 같이 저장
    #    node_xy_img[r, c] 가 float32 (x,y) 이미지 좌표
    if len(missing) > 0:
        miss_xy = node_xy_img[missing[:, 0], missing[:, 1]]  # (N,2)
        miss_rcxy = np.hstack([missing.astype(np.int32), miss_xy.astype(np.float32)])  # (N,4)

        np.savetxt(
            "missing_rcxy.txt",
            miss_rcxy,
            fmt=["%d", "%d", "%.2f", "%.2f"],
            header="row col x_img y_img",
            comments=""
        )
    else:
        # 빈 파일도 헤더는 남기고 싶으면:
        with open("missing_rcxy.txt", "w", encoding="utf-8") as f:
            f.write("row col x_img y_img\n")

    print(f"NN threshold (base-frame): {nn_thr:.2f}")
    print(f"Missing count: {len(missing)} / {grid_row_n*grid_col_n}")
    overlay_nodes = img.copy()
    for r in range(grid_row_n):
        for c in range(grid_col_n):
            x, y = node_xy_img[r, c]
            if not np.isfinite(x) or not np.isfinite(y):
                continue
            color = GREEN if occ[r, c] else RED
            cv2.circle(overlay_nodes, (int(round(x)), int(round(y))), 7, color, 1, cv2.LINE_AA)
    cv2.imwrite("overlay_nodes_on_original.png", overlay_nodes)

    grid_img = render_regular_grid_image(occ, cell=20, margin=20, radius=6)
    cv2.imwrite("grid_regular.png", grid_img)

    print("Saved:")
    print("- binary.png")
    print("- overlay_curves.png (rows=CYAN, cols=YELLOW)  [ORIGINAL base]")
    print("- overlay_nodes_on_original.png")
    print("- grid_regular.png")
    # print("- missing_rc.txt")
    print("- missing_rcxy.txt  (row,col + x_img,y_img)")
    if debug:
        print(f"- {debug_prefix}0000.jpg ... (per-line debug)")

    return {"occ": occ, "missing_rc": missing, "frame": frame}

if __name__ == "__main__":
    run_pipeline(
        img_path="distorsion_image.jpg",
        grid_row_n=50,
        grid_col_n=50,   # 예: 50 x 50 배열
        thr_mode="otsu",
        fixed_thr=220,
        morph_open=1,
        area_band=0.60,
        max_led=5000,
        row_degree=2,
        col_degree=2,
        inlier_sigma=3.0,
        seed=0,
        debug=False,  # debug=True이면 과정들을 line0000.jpg ...로 출력 
        debug_prefix="line"
    )
    print('Defective LED inspection has been finished.')

 

LED 미점등 수량 파악하기_파이썬_바이브코딩_챗지피티.ipynb
0.02MB


함수별 목적 + 언제 어떻게 튜닝하는지

1) detect_led_components(...)

목적

  • 입력 이미지에서 밝은 LED 픽셀을 이진화 → connectedComponentsWithStats로 점(컴포넌트) 검출
  • 면적(Area) 통계로 “LED로 보이는 점”만 골라 kept_labels로 반환

핵심 파라미터 튜닝

  • thr_mode="otsu" | "fixed"
    • 조명/노출이 매번 바뀌면 otsu가 편함.
    • LED 밝기가 늘 비슷하고, 배경이 들쭉날쭉하면 fixed가 더 안정적일 때가 많음.
  • fixed_thr=220
    • fixed 모드에서 LED가 덜 잡히면 낮추고(예: 200, 180), 배경 잡음이 많이 잡히면 올림(예: 235).
  • morph_open=1
    • 점 주변에 노이즈가 붙어서 컴포넌트가 울퉁불퉁/붙는 경우 값을 올림(2~3).
    • 반대로 LED가 작은데 열림(open) 때문에 점이 사라지면 0 또는 1로 낮춤.
  • area_band=0.60
    • LED 점 크기 분포가 안정적이면 0.4~0.6 권장.
    • 렌즈왜곡/가장자리 흐림으로 점 크기 편차가 커지면 0.8~1.2로 늘려 “너무 많이 버리는 문제”를 줄임.
    • 반대로 먼지/반사 등 이상하게 큰/작은 점이 섞이면 0.3~0.5로 좁혀 걸러냄.
  • max_led=5000
    • “과검출” 폭주 방지용. 50×50이면 정상 점등이 2500 근처일 텐데, 환경 따라 더 잡힐 수 있어 5000 정도는 무난.

2) minrect_top_edge(points_xy)

목적

  • 검출된 점들의 외곽을 기반으로 minAreaRect를 구하고
  • 그 사각형의 **상단 모서리 방향을 기준축(e)**으로 잡아
    • e: “가로축(열 방향)”에 해당하는 단위 벡터
    • n: “세로축(행 방향)”에 해당하는 단위 벡터(법선)
  • 즉, 사진이 회전돼 있어도 **격자 좌표계(엣지 좌표)**를 하나 잡는 단계

튜닝 포인트

  • 직접 파라미터는 없고, 여기서 방향이 이상하게 잡히면 대부분 검출 점들이 엉망(배경 과검출, LED 점이 너무 적음)인 경우라 detect_led_components 쪽을 먼저 개선하는 게 우선입니다.

3) to_edge_coords(pts_xy, origin, e, n) / from_edge_coords(x, y, origin, e, n)

목적

  • 이미지 좌표(x_img, y_img)를 “엣지 좌표계(x_edge, y_edge)”로 변환 / 역변환
  • 이후 모든 라인 피팅과 교차점 계산을 이 좌표계에서 수행 (회전/기울어짐에 강해짐)

튜닝

  • 없음. (좌표계 품질은 2번의 축 추정 품질에 좌우)

4) kmeans_1d(values, k, ...)

목적

  • 1차원 값들을 k개 군집으로 분할해 행 중심들(y_centers), **열 중심들(x_centers)**을 추정
  • “LED가 50행/50열에 분포한다”는 가정을 직접 사용

언제 문제가 생기나 / 튜닝

  • 일부가 많이 꺼져 있거나(결측이 많음) 한쪽이 거의 안 잡히면 kmeans가 중심을 이상하게 배치할 수 있음.
  • 튜닝은 주로 seed, 그리고 근본적으로는 검출 품질(1번) + **밴드폭(6번 row_band/col_band)**로 해결합니다.

5) fit_poly_and_score(x, y, degree) / polyfit_ransac(...)

목적

  • fit_poly_and_score: 단순 다항식 피팅 후 R², RMSE 평가
  • polyfit_ransac:
    • 랜덤 샘플로 초기 다항식을 만들고
    • 잔차 기반 inlier를 모아
    • inlier로 재피팅 → 가장 좋은 모델 선택
    • 결과: 왜곡/오검출 점들(아웃라이어)을 버리고 “그럴듯한 라인”을 얻음

핵심 파라미터 튜닝

  • degree=2 (row_degree/col_degree)
    • 렌즈왜곡이 약하면 1(직선)이 더 안정적일 수 있음.
    • 휘어짐이明显하면 2 유지, 심하면 3도 가능하지만 과적합(라인이 출렁) 위험 증가.
  • iters=1200
    • 점이 많고 아웃라이어가 많으면 증가(2000~4000).
    • 너무 느리면 감소.
  • inlier_sigma=3.0
    • 라인 주변 점들이 퍼져 있으면(검출 jitter/블러) ↑ (예: 4~6)
    • 아웃라이어를 더 강하게 배제하고 싶으면 ↓ (예: 2~2.5)
  • min_inliers
    • 현재는 max(12, int(0.35*len(idx)))라 꽤 보수적.
    • LED 결측이 많아 라인 점이 적으면 0.2~0.25 수준으로 완화하는 방식도 가능.

6) fit_rows_cols(...)

목적

  • 전체 점을 엣지좌표로 변환
  • kmeans로 행 중심(y_centers), 열 중심(x_centers) 추정
  • 각 행/열마다 “중심선 주변 점들”만 모아서 RANSAC 다항식 피팅
    • rows: y = f(x)
    • cols: x = g(y)
  • 결과로 frame(좌표계+센터+스텝/밴드 등) + rows/cols 모델 리스트 생성

가장 중요한 튜닝 포인트

  • row_band_factor, col_band_factor (현재 0.45)
    • 라인 주변 점을 얼마나 넓게 모을지 결정
    • 라인이 자주 None(“NOT FOUND”)로 뜨면 → 밴드 폭을 키우세요
      • 예: 0.45 → 0.6~0.9
    • 반대로 서로 다른 행/열 점이 섞여 피팅이 흔들리면 → 줄이세요
      • 예: 0.45 → 0.3~0.4
  • inlier_sigma
    • 밴드 내에 점을 충분히 모아도 RANSAC이 자꾸 실패하면 inlier_sigma를 키우는 쪽이 빠른 해결책인 경우가 많습니다.
  • grid_row_n, grid_col_n
    • 50×50이 확실할 때만 고정. (다르면 kmeans부터 틀어집니다)

7) draw_rows_cols_on_original(...)

목적

  • rows(청록), cols(노랑) 곡선을 원본 이미지 위에 오버레이해서 overlay_curves.png 저장

튜닝

  • samples: 라인 부드러움(많을수록 매끈, 느려짐)
  • thickness: 보기 좋게 조절

8) save_debug_lines(...)

목적

  • 디버깅용으로 한 장에 한 줄(row/col)만 그린 이미지를 줄줄이 저장
  • row/col별 R2, RMSE를 찍어서 “어느 라인이 망가졌는지” 빠르게 확인

언제 켜나

  • row/col이 많이 None이거나, overlay_curves가 이상할 때 debug=True로 켜고
    • “몇 번째 행/열부터 무너지는지” 확인하면 튜닝 방향이 바로 나옵니다.
  • 보통 이걸 보고 row_band_factor/col_band_factor, inlier_sigma, degree를 조절합니다.

9) compute_intersections_and_occupancy(...)

목적

  • 각 (r,c)에 대해
    • 시작점: (x_centers[c], y_centers[r])
    • 반복:
      • row식으로 y 업데이트
      • col식으로 x 업데이트
        → row와 col의 교차점을 수치적으로 수렴시키는 방식
  • 그 교차점이 실제 LED 점과 가까운지 **최근접거리(NN)**로 판정
  • 출력:
    • occ[r,c]: 점등(검출됨) 여부
    • node_xy_img[r,c]: 해당 격자 노드의 이미지 좌표
    • nn_dist[r,c]: 최근접거리
    • nn_thr: 점등 판정 임계치

가장 중요한 튜닝 포인트

  • nn_thr_factor=0.45
    • 검출 점이 교차점과 조금 멀어도 “켜짐”으로 인정하려면 ↑ (0.55~0.8)
    • 오검출을 줄이고 엄격하게 하려면 ↓ (0.3~0.45)
  • iters=8
    • 라인 곡률이 크면 10~15로 늘리면 수렴이 좋아질 때가 있음.
    • 너무 늘리면 큰 차이는 없고 느려짐.

10) render_regular_grid_image(occ, ...)

목적

  • 결과를 “정규격자”로 보기 좋게 렌더링 (grid_regular.png)
    • 초록: 켜짐
    • 빨강: 꺼짐(또는 미검출)

튜닝

  • cell, radius 등은 시각화만

11) run_pipeline(...)

목적

  • 전체 과정을 순서대로 실행하고 결과 파일 저장
  • missing_rcxy.txt로 결함 위치를 (row,col,x_img,y_img)로 저장

실전에서 파라미터 조정 순서(추천)

  1. detect_led_components가 제대로 점을 잡는지부터
    • binary.png에서 LED만 깔끔히 흰색인지 확인
    • 문제 있으면 thr_mode/fixed_thr/morph_open/area_band 조정
  2. overlay_curves.png에서 행/열 라인이 그럴듯한지 확인
    • 라인이 중간중간 끊기면 row_band_factor/col_band_factor ↑ 또는 inlier_sigma ↑
    • 라인이 출렁이면 degree ↓ 또는 밴드 ↓
  3. overlay_nodes_on_original.png에서 노드가 LED 위치에 잘 얹히는지 확인
    • 노드는 맞는데 miss가 과하게 뜨면 nn_thr_factor ↑
    • 반대로 대충 다 켜짐 처리되면 nn_thr_factor ↓

“이럴 때는 이렇게” 빠른 처방전

  • LED 점이 거의 안 잡힘:
    thr_mode="fixed" + fixed_thr 낮추기(220→200→180), morph_open 0~1
  • 배경 반사/노이즈가 너무 많이 잡힘:
    fixed_thr 올리기, morph_open 2~3, area_band 줄이기(0.6→0.4)
  • 행/열 라인이 많이 None:
    row_band_factor/col_band_factor 올리기(0.45→0.7), inlier_sigma 올리기(3→4~6)
  • 라인이 구불구불 과적합 느낌:
    degree를 2→1로 낮추거나, 밴드를 줄이기(0.45→0.35)
  • 결함(missing)이 과하게 뜸(실제로는 켜졌는데):
    nn_thr_factor 올리기(0.45→0.6~0.75)
  • 결함이 너무 적게 뜸(꺼진 것도 켜짐으로 판정):
    nn_thr_factor 내리기(0.45→0.35~0.4)

관련글 더보기

댓글 영역