명함 검출과 인식

InSung-Na·2023년 5월 3일
0

Part 11. OpenCV

목록 보기
5/6
post-thumbnail

해당 글은 제로베이스데이터스쿨 학습자료를 참고하여 작성되었습니다

1. 명함 검출과 인식 개요

명함 사진의 조건

  • 명함은 흰색이고, 배경은 충분히 어둡다
  • 명함은 각진 사각형 모양이고, 가로, 세로 비율을 9:5 이다.
  • 명함은 충분히 크게 촬영되었다

명함 검출과 인식 수행 과정

  • Grayscale 영상 사용
    • 컬러 정보가 중요하지 않음
    • 메모리 사용량, 연산 시간 유리

2. 이진화

영상의 이진화(Binaryization)

  • 영상의 픽셀 값을 0 또는 1(255)로 만드는 연산
    • 객체와 배경으로 분리
    • ROI vs Non-ROI
    • 특정 임계값(Threshold)보다 크면 255, 작으면 0으로 설정

자동임계값 결정방법: Otsu Method

  • 입력 영상이 객체와 배경 두 개로 구성되어 있다고 가정
  • 두 픽셀 분포의 분산의 합이 최소가 되는 임계값 결정
  • 효과적인 수식전개와 재귀식을 이용하여 빠르게 임계값을 결정

OpenCV 이진화(임계값) 함수

cv2.threshold(src, thresh, maxval, type, dst=None) -> retval, dst

  • src: 입력 영상 (다채널, 8비트 또는 32비트 실수형)
  • thresh: 임계값
  • maxval: 결과 이진 영상에서 최대 픽셀 값 (보통 255를 지정)
  • type: 임계값에 의한 변환 함수 지정 또는 자동 임계값 설정 방법
    • cv2.THRESH_BINARY : 이진화
    • cv2.THRESH_BINARY_INV : 이진화 후 반전
    • cv2.THRESH_BINARY | cv2.THRESH_OTSU : 자동이진화
  • retval : 사용된 임계값
  • dst : 임계값 영상(출력)

데이터의 픽셀 분포(Python)

  • 120 ~ 200 사이에서 threshold가 결정될 것으로 예상됨
import sys
import cv2
import numpy as np
import seaborn as sns

# 픽셀데이터 분포 확인
src = cv2.imread("./data/namecard1.jpg")
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

sns.histplot(src_gray.reshape(-1,));

이미지 이진화(Python)

import sys
import cv2

src = cv2.imread("./data/namecard1.jpg")

if src is None:
    print("Image load failed!")
    sys.exit()

# 흑백으로 변환
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

# 자동 이진화
th, src_bin = cv2.threshold(src_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
print("Auto threshold:", th)

cv2.imshow('src', src)
cv2.imshow('src_bin', src_bin)
cv2.waitKey()
cv2.destroyAllWindows()
---------------------------------------------------
Auto threshold: 133.0

3. 객체의 외곽선 검출

외곽선 검출

  • 객체의 외곽선 좌표를 모두 추출하는 작업
  • 바깥쪽 & 안쪽 외곽선 -> 외곽선의 계층 구조도 표현 가능

외곽선 객체 표현

  • numpy.ndarray
  • shape=(K, 1, 2)(K는 외곽선 좌표 개수)
    • 원래 형태는 (K, 2)이지만 C++ 기반인 OpenCV를 Python으로 변환과정에서 모두 처리하지 못함. 1은 의미없는 숫자.
  • dtype=numpy.int32
  • 여러 객체의 전체 외곽선은 객체 하나의 외곽선을 원소로 갖는 리스트로 표현

외곽선 검출 함수

cv2.findContours(image, mode, method, contours=None, hierarchy=None, offset=None) -> contours, hierarchy

  • image: 입력 영상. non-zero 픽셀을 객체로 간주함.
  • mode: 외곽선 검출 모드. cv2.RETR_ 로 시작하는 상수
    • cv2.RETR_EXTERNAL : 바깥쪽 외곽선만 검출
    • cv2.RETR_LIST : 모든 외곽선 검출
    • cv2.RETR_CCOMP : 2레벨 계층 구조로 검출
    • cv2.RETR_TREE : 전체 계층 구조로 검출
  • method: 외곽선 근사화 방법.
    • 근사화가 필요하지 않으면 cv2.CHAIN_APPROX_NONE 지정
  • contours: 검출된 외곽선 좌표. numpy.ndarray로 구성된 리스트.
    • len(contours)=전체 외곽선 개수(N).
    • contours[i].shape=(K, 1, 2).
    • contours[i].dtype=numpy.int32.
  • hierarchy: 외곽선 계층 정보. 자세한 내용은 OpenCV 도움말 참고.
  • offset: 좌표 값 이동 옵셋. 기본값은 (0, 0).

외곽선 그리기

cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None) -> image

  • image: 입출력 영상
  • contours: (cv2.findContours() 함수로 구한) 외곽선 좌표 정보
  • contourIdx: 외곽선 인덱스. 음수(-1)를 지정하면 모든 외곽선을 그림.
  • color: 외곽선 색상
  • thickness: 외곽선 두께. thinkness < 0이면 내부를 채운다.
  • lineType: LINE_4, LINE_8, LINE_AA 중 하나 지정
  • hierarchy: 외곽선 계층 정보.
  • maxLevel: 그리기를 수행할 최대 외곽선 레벨

외곽선 그리기(Python)

import sys
import random
import cv2

# 영상 불러오기
src = cv2.imread("./data/namecard1.jpg")

if src is None:
    print("Image load failed!")
    sys.exit()
    
# 입력 영상을 그레이스케일 영상으로 변환
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

# 자동 이진화
_, src_bin = cv2.threshold(src_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

# 외곽선 검출 및 명함 검출
contours, _ = cv2.findContours(src_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# 이진 영상을 컬러 영상 형식으로 변환
dst = cv2.cvtColor(src_bin, cv2.COLOR_GRAY2BGR)

# 검출된 외곽선 모두 그리기
for i in range(len(contours)):
    c = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
    cv2.drawContours(dst, contours, i, c, 2)

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

4. OpenCV 외곽선 함수

주요 외곽선 관련 함수

외곽선 길이 구하기

cv2.arcLength(curve, closed) -> retval

  • curve: 외곽선 좌표. numpy.ndarray. shape=(K, 1, 2).
  • closed: True이면 폐곡선으로 간주
  • retval: 외곽선 길이

면적 구하기

cv2.contourArea(contour, oriented=None) -> retval

  • contour: 외곽선 좌표. numpy.ndarray. shape=(K, 1, 2).
  • oriented: True이면 외곽선 진행 방향에 따라 부호 있는 면적을 반환. 기본값은 False.
  • retval: 외곽선으로 구성된 면적

외곽선 근사화

cv2.approxPolyDP(curve, epsilon, closed, approxCurve=None) -> pproxCurve

  • curve: 입력 곡선 좌표. numpy.ndarray. shape=(K, 1, 2).
  • epsilon: 근사화 정밀도 조절. 입력 곡선과 근사화 곡선 간의 최대 거리.
  • closed: True를 전달하면 폐곡선으로 간주
  • approxCurve: 근사화된 곡선 좌표. numpy.ndarray. shape=(k, 1, 2)

reference : 위키피디아

명함 검출(Python)

import sys
import cv2


# 영상 불러오기
src = cv2.imread('./data/namecard1.jpg')

if src is None:
    print('Image load failed!')
    sys.exit()

# 입력 영상을 그레이스케일 영상으로 변환
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

# 자동 이진화
_, src_bin = cv2.threshold(src_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

# 외곽선 검출 및 명함 검출
contours, _ = cv2.findContours(src_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

for pts in contours:
    # 너무 작은 객체는 무시
    if cv2.contourArea(pts) < 1000:
        continue

    # 외곽선 근사화
    approx = cv2.approxPolyDP(pts, cv2.arcLength(pts, True)*0.02, True)

    # 사각형으로 근사화되면 외곽선 표시
    if len(approx) == 4:
        cv2.polylines(src, [approx], True, (0, 255, 0), 2, cv2.LINE_AA)

cv2.imshow('src', src)
cv2.waitKey()
cv2.destroyAllWindows()

5. 명함 똑바로 펴기

명함 똑바로 펴기(수동)

영상의 기하학적 변환

투시 변환 행렬

cv2.getPerspectiveTransform(src, dst, solveMethod=None) -> retval

  • src: 4개의 원본 좌표점. numpy.ndarray. shape=(4, 2).
    • e.g) np.array([[x1, y1], [x2, y2], [x3, y3], [x4, y4]], np.float32)
  • dst: 4개의 결과 좌표점. numpy.ndarray. shape=(4, 2).
  • 반환값: 3x3 크기의 투시 변환 행렬

영상의 투시 변환

cv2.warpPerspective(src, M, dsize, dst=None, flags=None, borderMode=None, borderValue=None) -> dst

  • src: 입력 영상
  • M: 3x3 변환 행렬. numpy.ndarray.
  • dsize: 결과 영상의 크기. (0, 0)을 지정하면 src와 같은 크기.
  • dst: 출력 영상
  • flags: 보간법. 기본값은 cv2.INTER_LINEAR
  • borderMode: 가장자리 픽셀 확장 방식.
  • borderValue: cv2.BORDER_CONSTANT일 때 사용할 상수 값. 기본값은 0.

명함 똑바로 펴기(수동)(Python)

import sys
import numpy as np
import cv2


# 영상 불러오기
src = cv2.imread('namecard1.jpg')

if src is None:
    print('Image load failed!')
    sys.exit()

# 출력 영상 설정
w, h = 720, 400
srcQuad = np.array([[324, 308], [760, 369], [718, 611], [231, 517]], np.float32)
dstQuad = np.array([[0, 0], [w-1, 0], [w-1, h-1], [0, h-1]], np.float32)
dst = np.zeros((h, w), np.uint8)

pers = cv2.getPerspectiveTransform(srcQuad, dstQuad)
dst = cv2.warpPerspective(src, pers, (w, h))

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

명함 똑바로 펴기(자동)

문제점

  • cv2.approxPolyDP() 함수가 반환한 approx 좌표의 문제점
    • 해당 좌표는 임의의 순서 -> 좌측 꼭짓점을 기준으로 반시계방향으로 지정
    • approx.shape=(4,1,2) -> (4,2)로 변경해야 함

명함 똑바로 펴기(자동)(Python)

import sys
import numpy as np
import cv2

# approx 좌표 순서 지정
def reorderPts(pts):
    idx = np.lexsort((pts[:, 1], pts[:, 0]))  # 칼럼0 -> 칼럼1 순으로 정렬한 인덱스를 반환
    pts = pts[idx]  # x좌표로 정렬

    if pts[0, 1] > pts[1, 1]:
        pts[[0, 1]] = pts[[1, 0]]

    if pts[2, 1] < pts[3, 1]:
        pts[[2, 3]] = pts[[3, 2]]

    return pts


# 영상 불러오기
src = cv2.imread('./data/namecard2.jpg')

if src is None:
    print('Image load failed!')
    sys.exit()

# 출력 영상 설정
w, h = 720, 400
srcQuad = np.array([[0, 0], [0, h], [w, h], [w, 0]], np.float32)
dstQuad = np.array([[0, 0], [0, h], [w, h], [w, 0]], np.float32)
dst = np.zeros((h, w), np.uint8)

# 입력 영상을 그레이스케일 영상으로 변환
src_gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)

# 자동 이진화
_, src_bin = cv2.threshold(src_gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

# 외곽선 검출 및 명함 검출
contours, _ = cv2.findContours(src_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

for pts in contours:
    # 너무 작은 객체는 무시
    if cv2.contourArea(pts) < 1000:
        continue

    # 외곽선 근사화
    approx = cv2.approxPolyDP(pts, cv2.arcLength(pts, True)*0.02, True)

    # 사각형으로 근사화되면 외곽선 표시
    if len(approx) == 4:
        #cv2.polylines(src, [approx], True, (0, 255, 0), 2, cv2.LINE_AA)
        corners = approx.reshape(4, 2).astype(np.float32)
        srcQuad = reorderPts(corners)


pers = cv2.getPerspectiveTransform(srcQuad, dstQuad)
dst = cv2.warpPerspective(src, pers, (w, h))

cv2.imshow('src', src)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()

0개의 댓글