본문 바로가기

Embedded LAB Semina

공까지의 거리 추적하기(1)

출처 : https://dkssud8150.github.io/posts/perception/

 

[데브코스] 12주차 - DeepLearning Perception Applications

Do focus on developing ‘your ability’ rather than waste time on making you famous.

dkssud8150.github.io

이 글은 위의 출처를 기반으로 이해한 내용에 모르는 부분을 공부한 내용을 추가하여 작성했다.

.

.

.

Vision Geometry의 기본 지식_카메라의 투영 과정

카메라 좌표계에서 Z 축은 카메라의 광축(optical axis)과 동일하게 정의된다. 이 광축은 카메라 렌즈가 바라보는 방향을 나타내며, 이미지를 통해 물체의 깊이를 나타내는 축이다.

 

  • 월드 좌표계 (World Coordinate System): 일반적으로 Xw, Yw, Zw로 정의되며, 우리가 일상적으로 사용하는 좌표계와 동일하다.
  • 카메라 좌표계 (Camera Coordinate System): 카메라를 기준으로 정의된 좌표계로, 카메라 렌즈가 바라보는 방향을 Zc 축으로 한다.

이렇게 정의하면 카메라의 움직임을 단순화하고, 이미지 평면에 물체가 어떻게 투영되는지를 쉽게 이해할 수 있다고 한다.

 

3차원 위치를 가진 객체가 (Xw, Yw, Zw) 좌표를 가진 P로 표현되었을 때, 이 점이 카메라에 투영되면 (u,v) 좌표 지점에 상이 존재하는 이미지 평면을 얻게 된다.

 

동일한 크기의 물체여도 가까이서 촬영하면 크게 보이고, 멀리서 촬영하면 작게 보이는 이유는

멀리 있으면 물체가 이루는 각도가 작아서 상이 작게 맺히고, 가까우면 물체가 이루는 각도가 커서 상이 크게 맺히기 때문이다.

 

가장 처음 그림에서의 projection matrix를 세우면 아래와 같다.

 

이 부분을 통해 이미지 평면에서의 좌표와 실제 3차원 세계에서의 좌표를 변환하는 과정을 설명할 수 있다.

 

# 내부 파라미터 행렬 (Intrinsic Matrix) : 카메라 자체의 특성을 나타낸다.

여기에서 카메라의 초점 거리 (focal length), 이미지 센서의 주점 (principal point)이다.

 

# 외부 파라미터 행렬 (Extrinsic Matrix) : 카메라와 실제 3차원 세계의 관계를 나타낸다.

여기에서  회전 행렬 (rotation matrix) 요소로, 카메라의 회전을 나타내고,

 평행 이동 벡터 (translation vector) 요소로, 카메라의 위치를 나타낸다.

 

((이후의 내용은 연산이 아직 명확히 이해되지 않아서 찾아보는 중이다..)


어쨌든,

Vision-based ACC with a single camera: bounds on range and range rate accuracy 논문을 봤을 때(위 데브코스 ~ 사이트는 논문 기반의 내용이었다.) 단안 카메라를 통한 범위(깊이 정보, 거리 등) 추정할 수 있다는 것을 확인할 수 있다.  LIDAR 및 스테레오 이미지를 사용하는 것이 아니라, 원근 법칙을 이용하여 간접 범위(범위 정보를 추정하는 방법)만을 이용한 단안 카메라를 사용하여 직렬 생산 ACC 제품을 만들어 거리 제어를 수행하는 것이 가능하도록 했다.

 

단안 시각 시스템을 사용했을 때의 두 가지의 문제점이 존재한다.

  1. 대상에 대한 깊이 정보가 부족하다는 것
  2. 깊이 정보가 패턴 인식 기술(이미지의 특징을 인식)에 크게 의존적이라는 것

이를 해결하기 위한 원근 법칙 망막 발산(retinal divergence)만을 사용하여 ACC의 요구 정확도를 만족시킬 지 관건이다.

 

원근법을 사용한다는 것은 차량의 크기 차량 바닥의 위치를 활용한다는 것이다. 도로면의 기하학적 정보를 활용하면 더 정확한 결과를 얻을 수 있다. 도로의 경우 도로 양쪽 가장자리가 평행하게 수렴하여 소실점을 형성하게 되는데,

 

같은 방식을 적용할 내 경기장의 경우는 바닥면의 테두리 경계가 그 역할을 할 수 있을거라 생각한다.

그래서 우선 경기장의 양쪽 경계선을 구분하고자 한다.

 

1차 시도

 

1차 시도는 메스에서 구분했던건데 카메라의 문제로,

밝은거 인식하는 부분에 문제가 생긴건지 간헐적으로 이상한 화면을 잡음

그래서 집에와서 다시 시도했을 땐, 무지 밝은데 어둡다고 인식해서 일단 hsv를 조정함

2차 시도

hsv 경계 조절하고, 이때는 테두리의 진한 부분을 따로 잡지 않고, 필드 영역만으로 시도했던 기억이 난다.

3차 시도

선의 임계 각도를 설정하고자 했는데, 그건 왜인지 잘 적용이 되지 않았고,

가우시안 블러를 적용해보고 && 에지 검출을 한 뒤에 허프 연산을 수행하도록 했다.

4차 시도 

canny 에지의 두 번째 수를 500까지 높였다. 보통 1:2 혹은 1:3을 사용한다는데 예상과 다른 결과다..

그리고 허프 변환의 threshold, minLineLength, maxLineGap 파라미터를 조정했더니

내가 원하는 위치의 양쪽 라인이 간헐적으로 검출된다.

5차 시도

4차 시도에서 오른쪽 선은 잘 찾아내는데 왼쪽 선은 계속 잡았다가 못잡았다가 하길래

edge 이미지를 봤을 때 드는 생각은 왼쪽 경계 중에서 멀리 있는 부분은 선이라고 보기 애매한데,

허프p라인의 threshold를 좀 높여서 그런가 싶어서 이미지의 아래에서부터 2/3만 ROI로 잘랐다.


아 그리고 생각을 못했는데, canny 에지는 보통 그레이스케일 이미지를 사용하므로,,

mask(가우시안 블러까지 적용된)를 그레이 스케일로 변환하고, 이진화까지 한 다음에 적용하면

잔디무늬에 의해 생기는 것으로 추정되는 저 자글거림,,을 해결할 수 있을지도,,, (다음주에 테스트할 것)

전체코드

import cv2
import numpy as np

class FieldBoundaryDetector:
    def __init__(self, camera):
        self.camera = camera
        self.field_lower = np.array([40, 22, 150])
        self.field_upper = np.array([90, 255, 255])
        # boundry_lower & upper은 제외함

    # 카메라에서 이미지를 캡처하는 함수
    def get_image(self):
        ret, frame = self.camera.read()
        if not ret:
            print("Failed to capture image")
            return None
        return frame

    # 전처리(run에서 호출)
    def preprocess_image(self, image, lower, upper):
        hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
        mask = cv2.inRange(hsv, lower, upper)
        mask = cv2.GaussianBlur(mask, (5, 5), 0)
        # 가우시안에서 0은 표준편차임(표준 편차가 클수록 블러 효과가 강해짐)
        # 0으로 설정하면 OpenCV가 자동으로 커널 크기에 맞는 표준 편차를 계산하여 사용
        return mask

    # Canny 에지 검출기
    def detect_edges(self, mask):
        edges = cv2.Canny(mask, 480, 500)
        return edges

    # 허프로 에지 이미지에서 직선 찾기
    def find_lines(self, edges):
        lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=110, minLineLength=180, maxLineGap=50)
        # 1(rho): 거리 해상도를 픽셀 단위로 설정, 1이면 모든 픽셀을 고려하는 것을 의미
        # np.pi / 180 : 각도 해상도를 라디안 단위로 설정(1도로 설정되어 있음)
        # 각도해상도가 작을수록 허프 공간에서 더 세밀하게 직선을 검출할 수 있지만, 연산량이 증가
        # threshold : 선을 검출하기 위해 필요한 최소 교차점 수
        # maxLineGap : 동일한 선으로 연결될 수 있는 점들 사이의 최대 허용 간격
        return lines
        # lines는 검출된 선들의 리스트로, 각 선은 [x1, y1, x2, y2] 형태의 4-요소 벡터로 표현됨(시점과 종점)

    # 상위 max_lines 개의 직선만 그림
    def draw_lines(self, image, lines, roi_offset, max_lines=2):
        if lines is not None:
            # 직선을 길이 순으로 정렬 (내림차순)
            lines = sorted(lines, key=lambda line: np.sqrt((line[0][0] - line[0][2]) ** 2 + (line[0][1] - line[0][3]) ** 2), reverse=True)
            for i, line in enumerate(lines):
                if i >= max_lines:
                    break
                for x1, y1, x2, y2 in line:
                    # 좌표를 원본 이미지의 크기에 맞게 조정
                    cv2.line(image, (x1, y1 + roi_offset), (x2, y2 + roi_offset), (255, 0, 255), 2)  # 핑크색 선

    def run(self):
        while True:
            image = self.get_image()
            if image is None:
                break

            # 이미지의 크기 가져오기
            height, width = image.shape[:2] # image.shape는 (height, width, channels)의 형태, [:2]는 배열의 처음 두 요소를 선택
            roi_offset = height // 3  # 높이의 3분의 1을 오프셋으로 사용
            roi = image[roi_offset:height, 0:width] # 슬라이싱[시:종]

            # 관심영역(ROI)에서 필드 마스크 생성
            field_mask = self.preprocess_image(roi, self.field_lower, self.field_upper)
            edges = self.detect_edges(field_mask)
            lines = self.find_lines(edges)
            self.draw_lines(image, lines, roi_offset, max_lines=2)

            # 결과 이미지를 윈도우에 표시
            cv2.imshow('Field Detection', image)
            cv2.imshow('Field Mask', field_mask)
            cv2.imshow('Canny Edges', edges)

            if cv2.waitKey(1) & 0xFF == ord('q'):
                break

        cv2.destroyAllWindows()

# 카메라 객체 생성 및 클래스 인스턴스화, 실행
camera = cv2.VideoCapture(1)
detector = FieldBoundaryDetector(camera)
detector.run()
camera.release()

 

수정할 것 : gaussian blur > mask 로 수정할 것 !

근데 얘는 실제 상황에서 써먹지 못할 것 같다. 생각을 못한 부분이 있는데 실제 대회 영상을 보니 로봇 크기 대비 경기장이 넒기 때문에 한 번에 시야에 소실점 추출에 쓸 만한 라인 자체가 안나올 것 같음 ,,, 새로운 방식을 찾아보겠다요..ㅜㅅㅜ

 

전처리 순서 :: raw > blur > mask > 마스크 간소화 > 추출

 

--> 전처리 이후에 해야할 것

- 노란색 중에서 화살표/깃발(+홀) 구별

- 노란색 홀에서 공이 들어갔는 지 확인하는 알고리즘

- 빨간색 공 및 노란색 홀까지의 거리

등등,,,