Barcode Detection

Stella Kim·2021년 7월 17일
1

Image Processing

목록 보기
8/8
post-thumbnail

바코드 검출

지금까지 공부했던 다양한 영상처리 기술을 적용하여 바코드를 검출할 수 있다. 정확도 계산 프로그램을 활용하여 높은 정확도를 얻을 수 있도록 한다.

문제 분석

바코드 검출은 지금까지 공부한 다양한 영상처리 기술들을 활용하여 구현해야 한다. 기본적으로, 바코드 자체는 수평 또는 수직선으로 구성되어 있으므로 경계 강도에 대한 정보를 사용해서 검출할 수 있도록 해야 한다. 이 외에도 영상 평활화, 영상 분할, 모폴로지 변환, 연결 요소 생성 등의 영상처리 기술을 사용하여 최종적으로 원본 영상에 검출한 바코드 영역을 표시하고 결과 영상으로 별도로 저장할 수 있어야 한다.

설계 및 구현

  1. 다양한 영상처리 기술을 적용하기 위해 입력 데이터셋의 RGB 영상들을 모두 Grayscale 영상으로 변환해준다.

  2. 수평 및 수직 방향으로 경계 강도를 계산한다. 이때, 음수를 포함하는 값들의 범위를 조절해준다.

  3. 수평 및 수직 방향을 기준으로 바코드 후보 영역을 검출하기 위하여 수평(수직) 방향의 경계 강도 영상에서 수직(수평) 방향의 경계 강도 영상을 뺀다.

  4. 영상에 포함된 노이즈를 제거하고자 영상 평활화를 수행한다.

  5. 임계화를 수행한다.

  6. Close 모폴로지 변환을 수행한 후, 침식과 팽창 연산을 반복하여 적용한다.

  7. 연결 요소를 생성한 후 가장 크기가 큰 한 개의 연결 요소를 선택한다.

  8. 수평 방향 및 수직 방향의 처리 과정에서 선택한 두 개의 연결 요소의 크기를 비교하여 크기가 더 큰 연결 요소를 최종 바코드 영역으로 검출한다.

  9. 원본 영상에 검출한 바코드 영역을 표시한 후 결과 영상을 저장한다.

  10. 지정한 폴더에 포함된 모든 영상에 대해 위의 단계들을 반복한다.

구현한 내용은 아래와 같다.

def detectBarcode(img):
    sharpx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize = -1)
    sharpx = cv2.convertScaleAbs(sharpx)
    sharpy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize = -1)
    sharpy = cv2.convertScaleAbs(sharpy)

먼저, 본격적으로 바코드를 검출하고자 여러 영상처리 기술들이 적용되는 detectBarcode 함수 내의 구현 내용이다. Sobel 연산자를 사용하여 수평 및 수직 방향으로 경계 강도를 계산했다. 두 번째 인자에 해당하는 ddepth의 경우 이어지는 코드에서 convertScaleAbs 함수를 사용하여 sobel 결과에 절대값을 적용하고 값 범위를 8비트 unsigned int로 변경해주는 과정을 거치기 때문에 cv2.CV_64F로 값을 주었다. x축과 y축으로의 미분 차수는 각각 수평 방향은 1과 0으로, 수직 방향은 0과 1로 주었고, ksize를 –1로 설정하면 3x3의 Scharr filter가 적용되어 Sobel보다 더 나은 결과를 보여준다고 하여 위와 같이 설정하였다.

dstx = cv2.subtract(sharpx, sharpy)
dstx = cv2.GaussianBlur(dstx, (9, 7), 0)
th, dstx = cv2.threshold(dstx, 100, 200, cv2.THRESH_BINARY)

먼저 수평 방향을 기준으로 바코드 후보 영역을 검출하였는데, 이를 위해 cv2.subtract 함수를 사용하여 수평 방향의 경계 강도 영상에서 수직 방향의 경계강도 영상을 빼주었다. 이후 cv2.GaussianBlur를 통해 영상 평활화를 수행하였다. 지금까지의 모든 OpenCV를 사용한 Gaussian Blurring에서는 kernel의 width와 height를 모두 동일한 크기로 설정했었는데, F1 Score를 높이기 위해 여러 인자 값들을 변경하던 중 width와 height의 값을 다르게 설정하고 파일을 실행시켰을 때 더 나은 점수를 얻을 수 있어 위와 같이 최종적으로는 (9, 7)로 kernel size를 고정하였다. 이후 cv2.threshold를 통해 임계화를 수행하였다. threshold값은 100으로 설정하였다. 그리하여 Grayscale 영상을 바코드 검출에 용이하도록 Binary 영상으로 바꿔주었다.

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (61, 9))
dstx = cv2.morphologyEx(dstx, cv2.MORPH_CLOSE, kernel)
dstx = cv2.erode(dstx, kernel, iterations=3)
dstx = cv2.dilate(dstx, kernel, iterations=3)

다음으로, 모폴로지 변환을 적용하기 위해 우선적으로 cv2.getStructuringElement를 통해 수평 방향으로 긴 사각형 모양을 갖는 구조적 요소를 생성해주었다. 다양한 값을 넣고 실험해보았지만 최종적으로 (61, 9)가 가장 최적의 결과를 도출해낸다고 생각하여 위와 같이 값을 고정하였다. 이후 Close 모폴로지 연산을 수행했고, 이어서 cv2.erode, cv2.dilate 함수를 사용하여 총 3번을 반복하며 침식과 팽창 연산을 적용하였다.

(contours, hierarchy) = cv2.findContours(dstx, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
    maxx = np.array([[0, 0], [0, 0], [0, 0], [0, 0]])
else:
    maxx = sorted(contours, key=cv2.contourArea, reverse=True)[0]

위에서 제시한 영상처리 기술들을 모두 적용한 영상에 cv2.findContours를 사용하여 연결 요소를 찾아내고자 하였고, 여기서 가장 크기가 큰 한 개의 연결 요소만을 선택하기 위해 sorted 함수를 사용하였다. 위 코드만 가지고 실행시켰을 경우, 아예 검출하지 못한 경우에 대해 ERROR가 발생하며 프로그램이 종료되어, 아예 연결 요소를 찾지 못한 경우를 대비해 if-else문으로 if문에 걸렸을 경우 임의의 0으로 이루어진 배열을 max 값으로 지정해주었다.

dsty = cv2.subtract(sharpy, sharpx)
dsty = cv2.GaussianBlur(dsty, (7, 9), 0)
th, dsty = cv2.threshold(dsty, 100, 200, cv2.THRESH_BINARY)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 61))
dsty = cv2.morphologyEx(dsty, cv2.MORPH_CLOSE, kernel)
dsty = cv2.erode(dsty, kernel, iterations=3)
dsty = cv2.dilate(dsty, kernel, iterations=3)

(contours, hierarchy) = cv2.findContours(dsty, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
    maxy = np.array([[0, 0], [0, 0], [0, 0], [0, 0]])
else:
    maxy = sorted(contours, key=cv2.contourArea, reverse=True)[0]

앞서 서술했던 과정들을 수직 방향 기준으로 바코드 후보 영역 검출을 위해 동일하게 진행한다.

if (len(maxx) > len(maxy)):
    rect = cv2.minAreaRect(maxx)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    if (box[2][0] > box[0][0]):
        temp = box[2][0]
        box[2][0] = box[0][0]
        box[0][0] = temp
else:
    rect = cv2.minAreaRect(maxy)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    if (box[2][0] > box[0][0]):
        temp = box[2][0]
        box[2][0] = box[0][0]
        box[0][0] = temp
return box

수평 방향과 수직 방향을 비교하여 연결 요소의 크기가 더 큰 방향의 값을 최종 바코드 영역으로 판단하여 이에 대한 좌표값을 반환하기 위한 if-else문이다. 이중 if문으로 구현하였는데, detect.dat 파일을 열어 바코드에 해당하는 사각형의 좌표를 확인 및 분석해본 결과 배열의 [0]번째 값과 [2]번째 값 순서의 교환으로 인해 정확도가 감소하여 이를 방지하고자 지정하였다.

ap = argparse.ArgumentParser()
ap.add_argument("-d", "--dataset", required=True, help="path to the dataset folder")
ap.add_argument("-r", "--detectset", required=True, help="path to the detectset folder")
ap.add_argument("-f", "--detect", required=True, help="path to the detect file")
args = vars(ap.parse_args())

dataset = args["dataset"]
detectset = args["detectset"]
detectfile = args["detect"]

--dataset으로 검출할 바코드 영상을 포함하는 폴더의 경로를 입력받는다. --detectset에는 검출 결과 영상을 저장할 폴더명을 입력받는다. --detect는 검출한 바코드 위치를 저장할 파일명을 입력받는다. 받아온 값들을 각각 dataset, detectset, detectfile 변수에 저장한다.

for imagePath in glob.glob(dataset + "/*.jpg"):
    print(imagePath, '처리중...')

    image = cv2.imread(imagePath)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    points = detectBarcode(gray)

for문을 이용하여 dataset에 있는 모든 jpg 형식의 이미지 파일에 대해 바코드 검출을 수행한다. 이에 대한 함수인 detectBarcode를 호출하기 전, 여러 영상처리 기술을 적용하기 위하여 imread를 통해 읽어온 이미지를 cv2.cvtColor 함수를 사용하여 Grayscale 영상으로 변환시켜준다. 변환시킨 영상을 detectBarcode의 입력 인자로 넣는다.

detectimg = cv2.rectangle(image, (points[2][0], points[2][1]), (points[0][0], points[0][1]), (0, 255, 0), 2)

detectBarcode에서 반환받은 값인 points를 사용하여 cv2.rectangle을 통해 바코드라고 검출한 영역을 원본 영상에 사각형으로 나타낸다.

실험 결과

  • python detect.py --dataset dataset --detectset result --detect detect.dat 으로 명령을 주었을 경우

[정확도 분석]

정확도 분석 결과는 사진 제시 순서대로 x 방향으로의 경계 강도 검출 결과, y 방향으로의 경계 강도 검출 결과, Gaussian Blurring 결과, Thresholding 결과, Morphology 변환 결과, 최종 바코드 검출 결과이다.

1) 정확도 85% 이상


평균 정확도: 92%
원인 분석: 실제 바코드 영역에 비해 숫자에 해당하는 부분까지 하나의 연결된 영역으로 인식했지만, 최대한 바코드 영역만 검출하였기에 높은 정확도가 나왔다고 분석했다.



평균 정확도: 94%
원인 분석: 지금까지 여러 데이터셋의 중간 과정 영상에서 보았던 Morphology 변환 결과 중 가장 깔끔한 결과라 그런지 그만큼 높은 정확도를 보이며 올바른 바코드 영역 검출에 성공할 수 있었다고 분석했다.

2) 정확도 60~85%


평균 정확도: 68%
원인 분석: Threshold 영역서 이미지 속 물체의 밑바닥 부분이 함께 검출되었기도 했고, 무엇보다 Morphology 변환에서 결정적으로 이 밑바닥 부분이 바코드 영역과 하나의 연결 요소라고 분할되었기 때문에 이같이 부정확한 결과가 나왔다고 분석했다.



평균 정확도: 82%
원인 분석: 기울어진 이미지에 대해 기하학적 변환과 관련된 영상처리를 수행하지 않고 직사각형 모양인 바코드를 검출하려다보니 어느정도의 삭감되는 정확도는 감안해야 됐다고 분석했다. 그럼에도 양호한 결과를 도출해낸 것 같다고 생각했다.

3) 정확도 60% 미만


평균 정확도: 33%
원인 분석: 이미지상의 물체 자체도 깨끗하지 못한 점, 그래서인지 경계 강도 검출에서부터 영상처리가 말끔하게 진행되지 못해 최종적으로 가장 낮은 정확도를 보였다고 판단했다. Threshold에서도 바코드 부분만을 검출해내지 못했고, 그렇기 때문에 책으로 판단되는 물체 전체를 바코드 영역이라고 검출했다고 분석했다.

[소요 시간]

모든 영상을 처리하는 데에는 대략 23초의 시간이 소요되었다.

  • python accuracy.py --reference ref.dat --detect detect.dat 으로 명령을 주었을 경우

    정확도 계산 프로그램을 이용하여 F1 Score를 구해본 결과 0.9의 결과를 얻을 수 있었다.

코드

자세한 코드는 Github에서 확인할 수 있다.

profile
취업 준비 용으로 사용했던 기술 블로그입니다. 이제는 업로드 거의 안 할지도..

0개의 댓글