RoBoLoG

[OpenCV] 체커보드(Checkerboard) 패턴을 인식하는 다양한 방법! 본문

Study/OpenCV

[OpenCV] 체커보드(Checkerboard) 패턴을 인식하는 다양한 방법!

SKJun 2024. 5. 21. 18:16

[OpenCV] 체커보드(Checkerboard) 패턴을 인식하는 다양한 방법!

 


체커보드를 인식해보자!

 

이런 체커보드(Checkerboard)를 인식할 수 있는 다양한 방법에 대해 알아보도록 하겠습니다. 

실시간으로 웹캠을 통해 코너를 인식하여 가시화할 수 있는 코드를 제공합니다.

모든 코드는 Python으로 작성하였습니다!


1. OpenCV의 findChessboardCorners 이용

import cv2
import numpy as np

# 웹캠 캡처 객체 생성
cap = cv2.VideoCapture(0)

# 해상도 설정
cap.set(3, 320)  # 너비
cap.set(4, 240)  # 높이

# 웹캠이 열리지 않으면 오류 메시지 출력
if not cap.isOpened():
    print("웹캠을 열 수 없습니다.")
    exit()

# 체커보드 크기 설정 (4x4 보드, 내부 코너의 수)
board_size = (4, 4)

while True:
    # 프레임 읽기
    ret, frame = cap.read()
    
    # 프레임을 읽지 못하면 루프 종료
    if not ret:
        print("프레임을 읽을 수 없습니다.")
        break

    # 그레이스케일로 변환
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 어댑티브 쓰레숄딩 적용 (이미지 이진화)
    gray = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)

    # 체크보드 코너 찾기
    ret, corners = cv2.findChessboardCorners(gray, board_size, cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE)

    # 코너를 찾았으면
    if ret:
        # 서브픽셀 정확도 향상
        corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), (cv2.TermCriteria_EPS + cv2.TermCriteria_MAX_ITER, 30, 0.001))
        
        # 코너 그리기
        cv2.drawChessboardCorners(frame, board_size, corners, ret)

    # 결과 보여주기
    cv2.imshow('Checkerboard Detection', frame)

    # 'q' 키를 누르면 루프 종료
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

# 캡처 객체와 창 닫기
cap.release()
cv2.destroyAllWindows()

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

 

전체를 인식하려면 6X6 보드를 인식해야하는데, 이상하게 바운더리에 있는 보드들은 인식이 잘 안되더라구요. 그래서 이보다 작은 4X4 사이즈로 인식을 해보았습니다. 끝쪽 바운더리를 인식하는 것이 이 방법으로는 어려운 것 같네요. (참고: https://forum.opencv.org/t/opencv-unable-to-recognize-any-checkerboard-pattern/2709)

 

하지만 만약, 아래 코드를 통해 4X4에서 인식한 것에서 거리를 기준으로 6X6으로 확장할 수는 있습니다.

import numpy as np
import cv2

def detect_and_expand_corners(image, pattern_size_inner):
    """4x4 체커보드 패턴의 내부 코너를 감지하고 이를 6x6으로 확장"""
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 기본 4x4 체커보드 코너 찾기
    ret, corners = cv2.findChessboardCorners(gray, pattern_size_inner, None)
    
    if not ret:
        print("체커보드 코너를 찾을 수 없습니다.")
        return image

    # 서브픽셀 정확도로 코너 위치를 조정
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
    corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)

    # 기존 4x4 코너를 기반으로 6x6 코너 확장
    corners = corners.reshape(4, 4, 2)
    expanded_corners = np.zeros((6, 6, 2), dtype=np.float32)

    # 기존 4x4 코너 복사
    expanded_corners[1:5, 1:5] = corners

    # 상단과 하단 경계 코너 추가
    for j in range(1, 5):
        expanded_corners[0, j] = 2 * expanded_corners[1, j] - expanded_corners[2, j]
        expanded_corners[5, j] = 2 * expanded_corners[4, j] - expanded_corners[3, j]

    # 좌측과 우측 경계 코너 추가
    for i in range(1, 5):
        expanded_corners[i, 0] = 2 * expanded_corners[i, 1] - expanded_corners[i, 2]
        expanded_corners[i, 5] = 2 * expanded_corners[i, 4] - expanded_corners[i, 3]

    # 네 모서리 코너 추가
    expanded_corners[0, 0] = 2 * expanded_corners[1, 1] - expanded_corners[2, 2]
    expanded_corners[0, 5] = 2 * expanded_corners[1, 4] - expanded_corners[2, 3]
    expanded_corners[5, 0] = 2 * expanded_corners[4, 1] - expanded_corners[3, 2]
    expanded_corners[5, 5] = 2 * expanded_corners[4, 4] - expanded_corners[3, 3]

    # 코너 시각화
    for i in range(6):
        for j in range(6):
            cv2.circle(image, tuple(expanded_corners[i, j].astype(int)), 5, (0, 255, 0), -1)
    
    return image

def main():
    # 실시간 웹캠 캡처 설정
    cap = cv2.VideoCapture(0)

    # 체커보드 패턴 크기
    pattern_size_inner = (4, 4)  # 4x4 내부 코너

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        # 커스텀 코너 감지 및 확장
        frame_with_corners = detect_and_expand_corners(frame, pattern_size_inner)

        # 프레임 표시
        cv2.imshow('Expanded Chessboard Corners', frame_with_corners)

        # 'q' 키를 누르면 루프 종료
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

이 방법은 작은 박스에서 큰 박스로 강제 확장하는 것이기 때문에 정확도가 좀 떨어질 수 있습니다.

그리고 findChessboardCornersSB 함수도 존재하는데, 최대한 체크보드를 잡으려고 노력하는 느낌입니다.


2. OpenCV의 findChessboardCorners 이용

import cv2
import numpy as np

def detect_checkerboard_corners():
    # 카메라 열기
    cap = cv2.VideoCapture(0)

    while True:
        # 프레임 캡처
        ret, frame = cap.read()

        if not ret:
            print("이미지를 캡처할 수 없습니다.")
            break

        # 그레이스케일로 변환
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # Harris 코너 검출
        gray = np.float32(gray)
        dst = cv2.cornerHarris(gray, 2, 3, 0.04)

        # 코너 표시를 위해 결과를 팽창
        dst = cv2.dilate(dst, None)

        # 최적의 값을 위한 임계값 설정, 이미징에 따라 다를 수 있음
        frame[dst > 0.01 * dst.max()] = [0, 0, 255]

        # 결과 프레임 표시
        cv2.imshow('Harris Corners', frame)

        # 'q' 키가 눌리면 루프 종료
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # 카메라 해제 및 창 닫기
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    detect_checkerboard_corners()

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

 

이 방법의 문제점은 체커보드의 꼭짓점은 잘 찾는데, 문제는 체커보드가 아닌 것에서도 뭔가 feature를 찾는다는 것입니다. 추가적인 조건으로 체커보드의 꼭짓점만 가져올 수 있도록 후처리를 해야겠군요.


3. OpenCV의 FastFeatureDetector_create 이용

import cv2

def detect_fast_corners_video():
    # 카메라 열기
    cap = cv2.VideoCapture(0)  # 기본 카메라(일반적으로 첫 번째 연결된 카메라)에서 비디오 캡처 객체 초기화

    # FAST 코너 감지 객체 생성
    fast = cv2.FastFeatureDetector_create()  # FAST(Features from Accelerated Segment Test) 감지기 객체 생성

    # FAST 감지기 설정
    fast.setThreshold(50)  # 코너 감지 임계값 설정 (높을수록 감지되는 코너의 수가 줄어듦)
    fast.setNonmaxSuppression(True)  # 비최대 억제 기능 사용 설정
    fast.setType(cv2.FAST_FEATURE_DETECTOR_TYPE_9_16)  # FAST 감지기 유형 설정

    while True:
        # 프레임별 캡처
        ret, frame = cap.read()  # 카메라에서 프레임 읽기

        if not ret:  # 프레임 캡처에 실패한 경우
            print("Failed to capture image")  # 오류 메시지 출력
            break  # 루프 종료

        # 그레이스케일로 변환
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 캡처한 프레임을 그레이스케일로 변환

        # 키포인트 감지
        keypoints = fast.detect(gray, None)  # 그레이스케일 이미지에서 키포인트 감지

        # 키포인트 그리기
        frame_with_keypoints = cv2.drawKeypoints(frame, keypoints, None, color=(255, 0, 0))  # 키포인트를 원본 프레임에 그리기

        # 결과 프레임 표시
        cv2.imshow('FAST Corners', frame_with_keypoints)  # 키포인트가 그려진 프레임을 윈도우에 표시

        # 'q' 키가 눌리면 루프 종료
        if cv2.waitKey(1) & 0xFF == ord('q'):  # 1밀리초 대기 후 키 입력 확인
            break  # 'q' 키가 눌리면 루프 종료

    # 카메라 자원 해제 및 모든 창 닫기
    cap.release()  # 카메라 자원 해제
    cv2.destroyAllWindows()  # 모든 OpenCV 창 닫기

if __name__ == "__main__":
    detect_fast_corners_video()  # 메인 함수 호출

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

이 방법의 문제점도 findChessboardCorners와 마찬가지로 체커보드의 꼭짓점은 잘 찾는데, 문제는 체커보드가 아닌 것에서도 뭔가 feature를 찾는다는 것입니다. 추가적인 조건으로 체커보드의 꼭짓점만 가져올 수 있도록 후처리를 해야겠군요.


4. OpenCV의 goodFeaturesToTrack 이용

import cv2
import numpy as np

def detect_shi_tomasi_corners_video():
    # 카메라 열기
    cap = cv2.VideoCapture(0)  # 기본 카메라(일반적으로 첫 번째 연결된 카메라)에서 비디오 캡처 객체 초기화

    while True:
        # 프레임별 캡처
        ret, frame = cap.read()  # 카메라에서 프레임 읽기

        if not ret:  # 프레임 캡처에 실패한 경우
            print("Failed to capture image")  # 오류 메시지 출력
            break  # 루프 종료

        # 그레이스케일로 변환
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 캡처한 프레임을 그레이스케일로 변환

        # Shi-Tomasi 코너 감지
        corners = cv2.goodFeaturesToTrack(gray, maxCorners=36, qualityLevel=0.01, minDistance=10)  # Shi-Tomasi 알고리즘을 사용하여 코너 감지
        if corners is not None:  # 감지된 코너가 있는 경우
            corners = np.int0(corners)  # 코너 좌표를 정수형으로 변환

            # 코너 그리기
            for corner in corners:  # 감지된 각 코너에 대해
                x, y = corner.ravel()  # 코너의 좌표를 구함
                cv2.circle(frame, (x, y), 3, (0, 255, 0), -1)  # 원본 프레임에 초록색 원으로 코너를 그림

        # 결과 프레임 표시
        cv2.imshow('Shi-Tomasi Corners', frame)  # 코너가 그려진 프레임을 윈도우에 표시

        # 'q' 키가 눌리면 루프 종료
        if cv2.waitKey(1) & 0xFF == ord('q'):  # 1밀리초 대기 후 키 입력 확인
            break  # 'q' 키가 눌리면 루프 종료

    # 카메라 자원 해제 및 모든 창 닫기
    cap.release()  # 카메라 자원 해제
    cv2.destroyAllWindows()  # 모든 OpenCV 창 닫기

if __name__ == "__main__":
    detect_shi_tomasi_corners_video()  # 메인 함수 호출

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

이 방법의 문제점도 findChessboardCorners와 마찬가지로 체커보드의 꼭짓점은 잘 찾는데, 문제는 체커보드가 아닌 것에서도 뭔가 feature를 찾는다는 것입니다. 하지만 maxCorners 를 잘 이용해서 feature 개수를 설정하는 조건을 붙인다면 특정 환경에서는 잘 먹힐 수도 있겠군요.


5. Scikit-image의 corner_harris, corner_peaks 이용

import cv2
import numpy as np
from skimage.feature import corner_harris, corner_peaks
from skimage.color import rgb2gray
import matplotlib.pyplot as plt

# 웹캠 캡처 객체 생성
cap = cv2.VideoCapture(0)  # 기본 카메라(일반적으로 첫 번째 연결된 카메라)에서 비디오 캡처 객체 초기화

if not cap.isOpened():  # 웹캠을 열 수 없는 경우
    print("웹캠을 열 수 없습니다.")  # 오류 메시지 출력
    exit()  # 프로그램 종료

while True:
    # 프레임 읽기
    ret, frame = cap.read()  # 카메라에서 프레임 읽기
    
    if not ret:  # 프레임 캡처에 실패한 경우
        print("프레임을 읽을 수 없습니다.")  # 오류 메시지 출력
        break  # 루프 종료

    # 그레이스케일로 변환
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 캡처한 프레임을 그레이스케일로 변환
    
    # Harris 코너 검출기 사용
    coords = corner_peaks(corner_harris(gray), num_peaks=50, min_distance=5)  # Harris 알고리즘을 사용하여 코너 검출
    
    # 코너 그리기
    for coord in coords:  # 검출된 각 코너에 대해
        y, x = coord  # 코너의 좌표를 구함
        cv2.circle(frame, (x, y), 3, (0, 255, 0), -1)  # 원본 프레임에 초록색 원으로 코너를 그림

    # 결과 보여주기
    cv2.imshow('Checkerboard Detection with Scikit-Image', frame)  # 코너가 그려진 프레임을 윈도우에 표시

    # 'q' 키를 누르면 루프 종료
    if cv2.waitKey(1) & 0xFF == ord('q'):  # 1밀리초 대기 후 키 입력 확인
        break  # 'q' 키가 눌리면 루프 종료

# 캡처 객체와 창 닫기
cap.release()  # 카메라 자원 해제
cv2.destroyAllWindows()  # 모든 OpenCV 창 닫기

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

이 방법의 문제점도 findChessboardCorners와 마찬가지로 체커보드의 꼭짓점은 잘 찾는데, 문제는 체커보드가 아닌 것에서도 뭔가 feature를 찾는다는 것입니다. 하지만 num_peaks 를 잘 이용해서 feature 개수를 설정하는 조건을 붙인다면 특정 환경에서는 잘 먹힐 수도 있겠군요.


6. OpenCV의 findContours 이용

import cv2
import numpy as np

# 웹캠 캡처 객체 생성
cap = cv2.VideoCapture(0)  # 기본 카메라(일반적으로 첫 번째 연결된 카메라)에서 비디오 캡처 객체 초기화

if not cap.isOpened():  # 웹캠을 열 수 없는 경우
    print("웹캠을 열 수 없습니다.")  # 오류 메시지 출력
    exit()  # 프로그램 종료

def order_points(pts):
    rect = np.zeros((4, 2), dtype="float32")  # 4개의 점을 담을 배열 초기화

    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]  # 좌상단 점
    rect[2] = pts[np.argmax(s)]  # 우하단 점

    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]  # 우상단 점
    rect[3] = pts[np.argmax(diff)]  # 좌하단 점

    return rect

def find_rectangles(frame):
    # 그레이스케일로 변환
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 프레임을 그레이스케일로 변환
    # 블러링 적용
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)  # 노이즈 감소를 위한 가우시안 블러 적용
    # 에지 검출
    edged = cv2.Canny(blurred, 50, 150)  # Canny 에지 검출
    # 컨투어 찾기
    contours, _ = cv2.findContours(edged, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)  # 컨투어 검출

    rectangles = []

    for contour in contours:
        # 컨투어 근사화
        epsilon = 0.02 * cv2.arcLength(contour, True)  # 근사화 정확도 설정
        approx = cv2.approxPolyDP(contour, epsilon, True)  # 컨투어 근사화

        # 사각형 검출 (4개의 점으로 구성된 컨투어)
        if len(approx) == 4:
            # 사각형이 올바르게 검출되었는지 확인 (원근 왜곡 보정)
            rect = order_points(approx.reshape(4, 2))
            (tl, tr, br, bl) = rect

            # 각 변의 길이를 계산
            widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
            widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
            maxWidth = max(int(widthA), int(widthB))

            heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
            heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
            maxHeight = max(int(heightA), int(heightB))

            # 너비와 높이가 30을 넘는 경우에만 사각형으로 추가
            if maxWidth > 30 and maxHeight > 30:
                dst = np.array([
                    [0, 0],
                    [maxWidth - 1, 0],
                    [maxWidth - 1, maxHeight - 1],
                    [0, maxHeight - 1]], dtype="float32")  # 목적 좌표 설정

                M = cv2.getPerspectiveTransform(rect, dst)  # 원근 변환 행렬 계산
                warp = cv2.warpPerspective(frame, M, (maxWidth, maxHeight))  # 원근 변환 적용

                rectangles.append(approx)  # 사각형 목록에 추가

    return rectangles

while True:
    # 프레임 읽기
    ret, frame = cap.read()  # 카메라에서 프레임 읽기
    
    if not ret:  # 프레임 캡처에 실패한 경우
        print("프레임을 읽을 수 없습니다.")  # 오류 메시지 출력
        break  # 루프 종료

    # 사각형 찾기
    rectangles = find_rectangles(frame)  # 프레임에서 사각형 찾기

    # 사각형 그리기
    for rect in rectangles:  # 검출된 각 사각형에 대해
        cv2.drawContours(frame, [rect], -1, (0, 255, 0), 2)  # 원본 프레임에 초록색으로 사각형 그리기

    # 결과 보여주기
    cv2.imshow('Rectangles Detection', frame)  # 사각형이 그려진 프레임을 윈도우에 표시

    # 'q' 키를 누르면 루프 종료
    if cv2.waitKey(1) & 0xFF == ord('q'):  # 1밀리초 대기 후 키 입력 확인
        break  # 'q' 키가 눌리면 루프 종료

# 캡처 객체와 창 닫기
cap.release()  # 카메라 자원 해제
cv2.destroyAllWindows()  # 모든 OpenCV 창 닫기

 

이 코드를 실행하면 다음과 같은 결과가 나옵니다.

 

어쩌면 가장 무식한 방법인 것 같습니다. 모든 Contour를 찾아버리는 것이지요! 체크보드를 인식하는데 과연 활용할 수 있을지는 미지수이지만, 경우에 따라 좋은 솔루션이 될 것 같습니다!


마치며...

 

 

이 글에서는 OpenCV를 사용하여 체커보드 패턴을 인식하는 다양한 방법에 대해 알아보았습니다. 각 방법은 상황에 따라 적합한 용도가 있으며, 프로젝트의 요구 사항에 맞게 선택할 수 있습니다. 체커보드 패턴 인식은 로봇 비전, 카메라 캘리브레이션, 증강 현실 등 여러 분야에서 중요한 역할을 합니다. 여러분도 이 글을 참고하여 OpenCV를 활용한 다양한 응용 프로그램을 개발해 보시길 바랍니다. 앞으로도 더 많은 컴퓨터 비전 기술과 응용 방법에 대해 탐구하시길 바랍니다. 감사합니다!

728x90
반응형