AI 탐구노트

OpenCV를 이용한 물체 크기 측정 본문

DIY 테스트

OpenCV를 이용한 물체 크기 측정

42morrow 2024. 11. 6. 16:29

1.서론

컴퓨터 비전 기술이 발전하면서 OpenCV를 활용하여 사진 속 물체의 크기를 측정하는 기술이 다양하게 응용되고 있습니다. 최근 나온 iPad 등에는 LiDar가 장착되어 있어 물체의 크기를 측정할 수 있도록 앱이 출시되어 있습니다. 원래 2D 이미지 만으로는 정확한 크기를 측정하기 힘든데 LiDar가 깊이를 측정할 수 있도록 해 줌으로써 그 한계를 넘어설 수 있도록 하고 있죠. 라이다 대신 ToF Camera를 이용해도 같은 효과를 거둘 수 있습니다. 

 

하지만, 저는 그런 장비가 없어서... 이번 테스트에서는 OpenCV 라이브러리를 활용해 특정 기준 객체와 비교하여 사진 속 물체의 길이와 넓이를 측정하는 방법을 단계별로 설명합니다. 

 


2.필요한 준비물

  • Python 및 OpenCV 설치: OpenCV를 이용해 이미지 처리를 하기 위해 Python과 OpenCV 라이브러리를 설치해야 합니다.
  • 기준 물체: 물체 크기를 정확히 측정하기 위해 사진 속에 기준이 될 물체(참조 물체)가 있어야 합니다. 보통은 크기가 일정한 동전이나 카드 혹은 자를 이용합니다. 이번에는 A4 용지를 바탕에 깔고 그 위에 물체를 올려놓고 크기를 측정하도록 합니다. 

 

3.테스트 

3.1. 환경 구성

 

OpenCV와 numpy 정도만 있으면 테스트가 가능합니다. 

!pip install opencv-python numpy

 

 

3.2.테스트 이미지

A4 용지를 아래에 깔고 몇 가지 물체를 올려봤습니다. 직사각형 형태가 아닌 자유 형상의 경우에는 고려해야 하는 상황이 많을텐데, 그것까지 해 보려는 것은 아니고 혹시나 기본 코드에서 이런 것도 측정이 잘 되나 하는 것을 확인하기 위한 용도입니다. 

 

사진 : 테스트 용 사진

 

 

3.3.테스트 코드

테스트에 사용한 코드는 아래 영상에서 소개하고 있는 방식을 그대로 따라 했습니다.

 

 

 

처리하는 전반적인 과정을 요약해 보면 다음과 같습니다.

 

  • OpenCV로 이미지를 읽기
  • gray 변환 (cvtColor) -> blur 변환 (GaussianBlur) -> edge 추출 (Canny) -> 경계부분 처리 (dilate, erode) 
  • 물체의 윤곽 추출 -> 가장 큰 것 추출 (A4 용지)
  • A4 용지 영역을 평면으로 warp 처리 
  • warp 처리된 이미지 상에서의 물체들 윤곽 추출
  • 좌표 기준으로 길이 측정
  • 화면 상에 길이 표시

 

app.py

import cv2
import numpy as np
import utils

webcam = False
path = 'measure_01.jpg'
cap = cv2.VideoCapture(0)
cap.set(10,160)
cap.set(3,1920)
cap.set(4,1440)

scale = 3 
wP = 297*scale
hP = 210*scale


while True:
    if webcam: success, img = cap.read()
    else: img = cv2.imread(path)

    img, conts = utils.getContours(img,minArea=50000, filter=4)

    if len(conts) != 0:
        biggest = conts[0][2]
        # print(biggest)

        imgWarp = utils.warpImg(img, biggest, wP, hP)
        imgContours2, conts2 = utils.getContours(imgWarp,
                                                minArea=2000, filter=4, 
                                                cThr=[50,50], draw=False)
        
        if len(conts)!=0:
            for obj in conts2:
                cv2.polylines(imgContours2,[obj[2]], True, (0,255,0),2)
                nPoints = utils.reorder(obj[2])
                nW = round((utils.findDis(nPoints[0][0]//scale, nPoints[1][0]//scale)/10),1)
                nH = round((utils.findDis(nPoints[0][0]//scale, nPoints[2][0]//scale)/10),1)
                
                cv2.arrowedLine(imgContours2, (nPoints[0][0][0], nPoints[0][0][1]), (nPoints[1][0][0], nPoints[1][0][1]),
                                (255, 0, 255), 3, 8, 0, 0.05)
                cv2.arrowedLine(imgContours2, (nPoints[0][0][0], nPoints[0][0][1]), (nPoints[2][0][0], nPoints[2][0][1]),
                                (255, 0, 255), 3, 8, 0, 0.05)
                x, y, w, h = obj[3]
                cv2.putText(imgContours2, '{}cm'.format(nW), (x + 30, y - 10), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1.5,
                            (255, 0, 255), 2)
                cv2.putText(imgContours2, '{}cm'.format(nH), (x - 70, y + h // 2), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1.5,
                            (255, 0, 255), 2)
        cv2.imshow('A4', imgContours2)


    img = cv2.resize(img,(0,0),None,0.5,0.5)
    cv2.imshow('Original', img)
    cv2.waitKey(1)

 

 

utils.py

import cv2
import numpy as np

def getContours(img, cThr=[100,100], showCanny=False, minArea=1000, filter=0, draw=False):
    imgGray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    imgBlur = cv2.GaussianBlur(imgGray, (5,5),1)
    imgCanny = cv2.Canny(imgBlur,cThr[0], cThr[1])

    kernel = np.ones((5,5))
    imgDial = cv2.dilate(imgCanny, kernel, iterations=3 )
    imgThre = cv2.erode(imgDial, kernel, iterations=2)

    if showCanny: cv2.imshow('Canny', imgThre)

    contours, hieracy = cv2.findContours(imgThre, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    finalContours = []
    for i in contours:
        area = cv2.contourArea(i)
        if area > minArea:
            peri = cv2.arcLength(i, True)
            approx = cv2.approxPolyDP(i, 0.02*peri, True)
            bbox = cv2.boundingRect(approx)
            if filter >0:
                if len(approx) == filter:
                    finalContours.append([len(approx), area, approx,bbox,i])
            else:
                finalContours.append([len(approx), area, approx,bbox,i])
    finalContours = sorted(finalContours, key=lambda x:x[1], reverse=True)
    if draw: 
        for con in finalContours:
            cv2.drawContours(img, con[4], -1,(0,0,255),3)

    return img, finalContours


def reorder(myPoints):
    print(myPoints.shape)
    myPointsNew = np.zeros_like(myPoints)
    myPoints = myPoints.reshape((4,2))
    add = myPoints.sum(1)
    myPointsNew[0] = myPoints[np.argmin(add)]
    myPointsNew[3] = myPoints[np.argmax(add)]
    diff = np.diff(myPoints, axis=1)
    myPointsNew[1] = myPoints[np.argmin(diff)]
    myPointsNew[2] = myPoints[np.argmax(diff)]
    return myPointsNew


def warpImg(img, points, w,h, pad=20):
    # print(points)
    points = reorder(points)
    pts1 = np.float32(points)
    pts2 = np.float32([[0,0],[w,0],[0,h],[w,h]])
    matrix = cv2.getPerspectiveTransform(pts1, pts2)
    imgWarp = cv2.warpPerspective(img,matrix,(w,h))
    imgWarp = imgWarp[pad:imgWarp.shape[0]- pad, pad:imgWarp.shape[1]-pad]
    return imgWarp

def findDis(pts1, pts2):
    return ((pts2[0]-pts1[0])**2+(pts2[1]-pts1[1])**2)**0.5

 

3.4.테스트 결과 

 

위 코드를 이용한 테스트 결과는 다음과 같습니다. 정확도를 떠나서 일단 이렇게 된다는게 재미있네요.

 


원래 물체의 각각 크기는 다음과 같습니다.

  • 초록색 커터케이스 : 8.9cm x 1.4cm
  • 스테이플러통 : 5.4cm x 2.8cm
  • 포스트잇은 5.0cm x 5.0cm 

그러니 위의 측정 결과는 대충 입체가 아닌 평면으로 촬영된 세로 쪽은 대부분 비슷하게 맞고 짧은 쪽은 입체 형태로 촬영되어 제법 오차가 나고 있습니다. 

 

2개의 물체가 측정되지 않은 것은 아무래도 엣지 검출과 관련된 것 같습니다. Canny 를 이용해 윤곽선을 추출한 것을 보면 아래와 같이 나오는데 Canny 엣지 검출이 직선적이고 뚜렷한 경계에 더 잘 작동하는 특징이 있어서이지 않을까 싶고, OpenCV의 다른 필터를 잘 써보면 포함시킬 수 있을 것 같긴 한데 그건 제 범위를 넘어섭니다. 

 

 

4.후기

이번 테스트는 OpenCV의 컴퓨터 비전 기술을 이용해 물체의 크기를 측정하는 것을 해 봤습니다. 다음 번에 기회가 되면 AI 모델을 이용하는 방식으로 진행해 볼까 합니다. 요구사항이 뭐냐에 따라서 거기에 맞는 적정 기술을 도입하는 것이 바람직 할테니 둘 다 방법을 알아 두는 것이 좋을 것 같아서인데, 앞서 언급한 것처럼 요새는 워낙 장비(예: 스마트폰)가 좋아져서 자동 측정이 되는 상황이라 경쟁력을 가지려면 높은 정확도를 확보할 수 있는 방안이 있긴 해야할 것 같다는 생각입니다.