2일차에 해결을 위한 아이디어를 떠올렸고, 그 아이디어를 직접 코드로 구현한 날이다.

HSV → Lab 색 공간 변환

기존 HSV 색 공간의 문제점

기존 HSV 색 공간의 문제점

제어의 문제가 아닌 비전 인식이 문제라는 생각이 들자마자 노이즈를 줄이거나, 조도 변화에 영향을 적게 받는 색상 인식 방법을 찾아보았다.

기존 시스템은 직관적인 HSV(Hue, Saturation, Value) 색 공간을 활용하였다.

유채색인 빨간색의 경우 HSV 색 공간의 H 채널을 통해 조명 환경에 크게 영향을 받지 않고 검출이 가능했다.

그러나 무채색 영역인 차선 역할을 하는 회색과 도로 역할을 하는 검정색을 구분하는 과정에서 인식률 저하 문제가 있었다.

검정색과 회색은 모두 채도가 낮아 H 정보로는 구분이 불가능하며, V 값의 차이로만 분류해야 했다.

  • 그러다보니 검정색 바닥이 조명을 강하게 반사할 경우 V 값이 상승해서 회색으로 인식하거나
  • 회색 물체에 그림자가 드리워서 V 값이 하락해서 검은색으로 인식하는 경우가 있었다.

즉, HSV 색 공간의 V 채널은 물리적인 빛의 세기에 비례하므로, 조명 노이즈에 취약하다는 한계가 있었다.

Lab 색 공간 도입

Lab 색 공간 도입

이런 문제를 해결하기 위해, 인간의 시각 인지 특성을 기반으로 설계된 Lab 색 공간으로 변환했다.

  • Lab는 밝기 정보(L)이 색상 정보(a, b)와 완전히 독립적이다.
    • L (Lightness): 0(검정) ~ 100(흰색) 사이의 밝기 값을 나타낸다.
    • a: +값은 빨강, -값은 녹색 방향 (적-녹)
    • b: +값은 노랑, -값은 파랑 방향 (황-청)
  • 따라서 조명 변화로 인해 밝기가 변하더라도 a, b 채널 분석을 통해 빨간색과 같은 유채색을 여전히 강건하게 검출할 수 있다.
  • 회색과 검정색은 아래와 같은 이유로 강건성을 확보할 수 있었다.
    • 반사광에 의한 오인식 (검정 → 회색으로 보이는 경우)
      • HSV의 한계: 검정색 바닥에 강한 조명이 반사될 경우, HSV는 물리적인 빛의 양에만 반응하여 밝기 수치가 상승한다. 이로 인해 검정색이 마치 밝은 회색처럼 인식되어 두 색상을 구분하기 어려워진다.
      • Lab의 개선: 인간의 시각 인지 특성 기반이기 때문에 반사광으로 인해 빛이 조금 강해지더라도, L 채널 값은 검정색 고유의 어두운 수준을 안정적으로 유지한다. 결과적으로 반사된 검정과 실제 회색 사이에 뚜렷한 차이가 확보되어 명확한 구분이 가능하다.
    • 그림자에 의한 오인식 (회색 → 검정으로 보이는 경우)
      • HSV의 한계: 어두운 그림자 영역에서는 HSV의 색상과 채도 정보가 뭉개지기 쉽다. 이 때문에 그림자가 드리운 회색을 단순한 검정색으로 잘못 검출한다.
      • Lab의 개선: 그림자 때문에 L 채널 값이 낮아지더라도, a와 b 채널은 색이 없는 무채색이라는 회색의 성질을 그대로 유지한다. 밝기만 보는 것이 아니라 a, b 채널을 통해 색상 유무를 먼저 판단하므로, 어두운 곳에서도 회색을 검정색의 구분이 가능하다.
  • 코드에는 개발 편의성과 비교를 위해 HSV, Lab 모드를 실행시 선택할 수 있게 하였다.

이진화 데이터 필터링

ROI 영역에서 도로를 볼 때, 벽 건너의 도로 또는 그림자 진 영역을 “진행할 도로 방향”로 인식하는 것이 문제였다.

카메라 이진화 결과에는 도로로 보이는 검정 픽셀이 여러 덩어리로 생길 수 있어서 발생하는 문제였다.

나의 개선 아이디어는 다음과 같았다.

차가 실제로 밝고 있는 도로 덩어리(=P3가 속한 덩어리)를 기준으로, 그 덩어리만 남긴 마스크에서 P1/P2/P3를 다시 계산하자.

왜 하필 P3인가?

  • P3는 차 바로 앞을 보고 있는 점이라,
    • 가장 가까운 도로 정보라서 픽셀이 크고 안정적이고
    • “지금 차가 실제로 올라가 있는 도로” 일 확률이 높다.

수정된 인지 부분 요약

원본 프레임  
  ↓  
`calculate_roi_points()` → ROI 좌표 계산  
  ↓  
`apply_roi_overlay()` → ROI 박스만 시각화(디버그용)  
  ↓  
`warp_perspective()` → ROI 기준 버드아이뷰(IPM) 변환  
  ↓  
모드 분기(perception.mode)  
  ├─ Lab: `lane_detection_lab.detect_road_lines()` → 차선 이진화(Lab 임계, 빨강/회색)  
  └─ HSV: 그레이스케일 변환 → `lane_detection_hsv.detect_road_lines()` → 차선 이진화(HSV 빨강 + 그레이 임계)  
  ↓  
`compute_lane_error()` → (binary 반전) 도로 무게중심 x 좌표(센트로이드, 시각화용)  
  ↓  
`estimate_heading()`  
  ├─ (binary 반전) road_mask 생성 + morphology close로 연결  
  ├─ connected components 분리  
  ├─ P3 포함 컴포넌트만 선택(+P2 인접 시 병합) → `target_mask 화면 출력`  
  ├─ P1 튐 방지 여유 확장(dilate, p1_margin_px)  
  └─ `target_mask` 기준 밴드 중심(P1~P3) 재계산 → slope_norm/heading_offset 계산  
  ↓   
`visualize_binary_debug()`  
  ├─ debug_input = bitwise_not(target_mask) (선택 도로 영역 강조)  
  └─ DIR/FPS/steer/heading/턴 임계, P1~P3, 센트로이드(노란선) 표시

추가된 파라미터 설명

  1. connect_close_px : 도로 마스크 연결 범위 (0~20 스케일) - 증가 시 더 큰 간격까지 도로 영역 연결 (노이즈 증가 가능)
  2. merge_gap_px : p2/p3 병합 시 허용 간격 (0~20 스케일) - p2와 p3가 분리되어 있어도, merge_gap_px 범위 내에 있으면 하나의 도로 영역으로 합침 (노이즈 대비책)
  3. p1_margin_px : 하단(p3)에서 선택된 도로 영역을 확장 (0~20 스케일) - 상단에서는 도로가 좁아지거나 차선이 보이지 않을 수 있어, 하단 기준 영역을 확장해 상단 중심 계산의 안정성 증가

인지 로직(중요)

  • Lab 처리해서 나온 binary_frame에는 차선이 흰색, 도로가 검은색이 된다.
  • binary_frame 색상을 한 번 뒤집어 도로를 흰색으로 만든다.
    • 도로를 흰색 덩어리로 만들어야 컴포넌트를 더 깔끔하게 분석 할 수 있기 때문이다.
    • 내부적으로 도로는 흰색으로 처리가 되지만 일관성을 위해 설명에 첨부된 사진에는 도로를 검은색으로 나타냈다.
  • 노이즈로 인한 작은 구멍/끊김을 메우기 위해 Morph_close(connect_close_px)를 적용해서 도로 덩어리가 더 잘 연결된 하나의 덩어리가 되게 만든다.

    Gemini_Generated_Image_29zba429zba429zb.png

    • 노이즈를 제거하기 위해 모폴로지 연산을 사용하였다.

      image.png

    • 커널의 모양을 정의해서 각 픽셀마다 커널을 기준으로 연산을 수행한다.

    3x3 사각형 커널을 이용한 모폴로지 침식 연산의 결과

    image.png

    • 커널 내의 모든 픽셀이 1을 만족하면 중앙을 1로 유지, 만족하지 않는다면 0으로 전환 시킨다.

    3x3 사각형 커널을 이용한 모폴로지 팽창 연산의 결과

    image.png

    • 커널 내에 픽셀이 하나라도 1을 만족한다면 중앙을 1로 변경한다.

    모폴로지 close 연산 (팽창 → 침식)

    image.png

    • 팽창 연산을 먼저 수행하고, 침식 연산을 하게되면 대상 내부의 끊어져 보이는 곳을 연결하거나 구멍을 메우고, 대상 요소의 원래 크기를 유지한다.
    • 모폴로지 close 연산을 통해 도로 내부에 빛 반사로 인한 노이즈를 억제시키는 역할을 한다.

        import cv2
               
        # 구조화 요소 커널, 사각형 (3x3) 생성
        k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
              
        # 닫힘 연산 적용
        closing = cv2.morphologyEx(img2, cv2.MORPH_CLOSE, k)
      
  • connectedComponentsWithStats()로 도로(우리가 보기에는 검은색이지만 코드 내부적으로는 흰색) 덩어리들을 라벨링 한 뒤, P3 좌표에 해당하는 위치에 겹치는 라벨을 정답 도로 덩어리로 고른다.

      # 1. p3 밴드의 중심 좌표 가져오기
      p3_meta = find_band_meta(2)  # p3는 밴드 인덱스 2
      _, p3_pt = p3_meta  # p3_pt = (cx, cy) 좌표
        
      # 2. p3 좌표 위치의 라벨 번호 확인
      def get_label(pt):
          x, y = pt
          return int(labels[y, x])  # 해당 좌표의 라벨 번호 반환
        
      p3_label = get_label(p3_pt)
        
      # 4. 해당 라벨만 선택
      selected_mask = (labels == p3_label).astype(np.uint8) * 255
    
  • 내가 고른 라벨만 selected_mask로 남기고, 색을 다시 뒤집어서 최종 출력 마스크는 정답 도로 덩어리만 검정으로 유지되고, 나머지 검정 덩어리 부분들은 흰색으로 날아간 상태를 만든다.

    제목 없음-1 - 14-12-2025 10-27-21.png

    • P3가 위치하는 검정색 영역만 남기고 이어지지 않은 검정색 영역은 흰색으로 바꿔버린 모습이다.
    • 결과적으로 P1은 절대 도로 반대편 영역에 찍힐 수 없게 되고 바로 앞 도로를 따라 우회전 할 수 있게 된다.

      image.png

    • 상단 이미지는 카메라 화면의 ROI 영역이다. 길 건너 도로의 그림자를 오인식하던 문제를 해결할 수 있었다. P1이 올바른 도로 위에 찍히는 것을 볼 수 있다.

진동 저감 장치 부착

image.png

  • 카메라 모듈이 흔들려서 안정적인 데이터 수신이 힘들었다.
  • 엘라스토머 재질을 통해서 바닥 가진을 저감시켜서 안정적인 영상을 얻을 수 있었다.

    image.gif
    개선 전 image.gif
    개선 후

Updated: