Python/컴퓨터 비전

Python(54)- 모폴로지 처리, 레이블링, 테서렉트

두설날 2024. 7. 23. 16:45

*이 글을 읽기전에 작성자 개인의견이 있으니, 다른 블로그와 교차로 읽는것을 권장합니다.*

모폴로지 처리:

영상의 밝은 영역이나 어두운 영역을 축소 또는 확대하는 기법
    cv2.getStructuringElement(구조 요소의 모양, 사이즈)
    - 구조 요소의 모양
        1. 직사각형(cv2.MORPH_RECT)
        - 가장 단순한 형태로, 모든 요소가 같은 값을 가지는 정사각형 또는 직사각형
        - 팽창과 침식 연산에서 동일하게 작동
        - 객체 가장자리를 따라 명확한 변화를 줄 때 유용
        2. 타원형(cv2.MORPH_ELLIPSE)
        - 가장자리 부분을 더 부드럽게 처리
        - 객체의 둥근 모양을 유지하면서 노이즈를 제거할 때 유용
        3. 십자형(cv2.MORPH_CROSS)
        - 중심을 기준으로 수직 및 수평 방향으로 영향
        - 얇은 라인 구조를 강화하거나 제거하는데 유용

    침식(erosion) 연산

    cv2.erode(영상, 구조요소, 출력영상, 고정점 위치)
    - 이미지를 깎아내는 연산
    - 객체 크기가 감소되고 배경은 확대
    - 작은 크기의 객체(잡음)제거 효과가 있음
    - 어두운 부분의 노이즈를 제거하는 효과가 있음

   팽창(dilation) 연산

    cv2.dilate(영상, 구조요소, 출력영상, 고정점 위치)
    - 물체의 주변을 확장하는 연산
    - 팽창 연산은 객체 외곽을 확대시키는 연산
    - 객체 크기가 증가되고 배경은 감소
    - 객체 내부의 홑이 있다면 홑을 채울 수도 있음
    - 밝은 부분의 노이즈를 제거하는 효과

import cv2
import numpy as np

img = cv2.imread('./circuit.bmp', cv2.IMREAD_GRAYSCALE)
gse = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
gse = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dst1 = cv2.dilate(img, gse)
dst2 = cv2.erode(img, gse)

cv2.imshow('img', img)
cv2.imshow('dst1', dst1)
cv2.imshow('dst2', dst2)
cv2.waitKey()

원본
팽창 연산
침식 연산

    열림(Opening)
    - 팽창 연산과 침식 연산의 조합
    - 침식 연산을 적용한 다음, 팽창 연산을 적용
    - 침식 연산으로 인해 밝은 영역이 줄어들고, 어두운 영역이 늘어남
    - 객체 크기 감소를 원래대로 복구할 수 있음
    - 작은 객체를 제거하고 구멍을 메우는데 사용

    닫힘(closing)
    - 팽창 연산과 침식 연산의 조합
    - 팽창 연산을 적용한 다음, 침식 연산을 적용
    - 어두운 영역이 줄어들고 밝은 영역이 늘어남
    - 작은 구멍을 메우고 끊어진 객체를 연결하는 데 사용

    그래디언트
    - 영상에 팽창 연산을 적용한 결과에서 영상에 침식 연산을 적용한 결과를 뺌
    - 경계가 강조된 영상을 생성
    - 엣지 검출, 객체의 윤곽 추출, 영상 분할 등에 사용


    레이블링

    - 이진화, 모폴로지를 수행하면 객체와 배경 영역을 구분할 수 있게됨
    - 객체 단위 분석을 통해 각 객체를 분할하여 특징을 분석하고 객체의 위치, 크기 정보, 모양 분석, ROI 추출 등이 가능함
    - 서로 연결되어 있는 객체 픽셀에 고유번호를 할당하여 영역 기반 모양분석, 레이블맵, 바운딩 박스, 픽셀 갯수, 무게 중심, 좌표 등을 반환할 수 있게 함
    cv2.connectedComponents(영상, 레이블맵)
    레이블맵 : 픽셀 연결 관계(4방향 연결, 8방향 연결)
    반환 : 객체 갯수, 레이블 맵 행렬

    cv2.connectedComponentsWithStats(영상, 레이블맵)
    반환 : 객체 갯수, 레이블 맵 행렬, (객체 위치, 가로세로길이, 면적 등 행렬), 무게 중심 정보

import cv2

img = cv2.imread('./keyboard.bmp', cv2.IMREAD_GRAYSCALE)
_, img_bin = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cnt, labels, stats, centroids = cv2.connectedComponentsWithStats(img_bin)
print('cnt: ', cnt)
print('labels: ', labels)
print('stats: ', stats)
print('centroids: ', centroids)

for i in range(1, cnt):
    (x, y, w, h, area) = stats[i]
    if area < 30:
        continue
    cv2.rectangle(dst, (x, y, w, h), (0, 255, 255)) # garyscale에선 색상 못넣음 ->cvtColor사용 변환

cv2.imshow('img', img)
cv2.imshow('img_bin', img_bin)
cv2.imshow('dst', dst)
cv2.waitKey()

원본
thresh_binary ❘ thresh_otsu
rectangle labeling

객체의 외곽선 검출
cv2.findContours(영상, 검출모드, 외곽선 좌표 근사화 방법)
검출모드
    cv2.RETR_EXTERNAL: 외부 외곽선만 검출
    cv2.RETR_LIST: 모든 외곽선을 검출하며, 계층 관계는 무시
    cv2.RETR_CCOMP: 모든 외곽선을 검출하며, 계층 관계를 2단계로 구성
    cv2.RETR_TREE: 모든 외곽선을 검출하며, 전체 계층 관계를 구성
        첫번째 계층: 바깥쪽 윤곽선
        두번째 계층: 내부 윤곽선
    cv2.RETR_TREE: 모든 외곽선을 검출하며, 전체 계층 관계를 구성

import cv2
import random

img = cv2.imread('./contours.bmp', cv2.IMREAD_GRAYSCALE)

# hierarchy: 계층정보
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
print(contours)
print(hierarchy)
dst = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))

cv2.drawContours(dst, contours, -1, color, 3)
cv2.imshow('img', img)
cv2.imshow('dst', dst)
cv2.waitKey()

외곽선 좌표 근사화 방법
외곽선 점들의 저장 방식과 정확도를 정의
    cv2.CHAIN_APPROX_NONE: 모든 외곽선의 점을 지정
    cv2.CAHIN_APPROX_SIMPLE: 수평, 수직, 대각선 방향 점들은 그 점의 끝점만 저장하여 압축

외곽선 그리기
    cv2.drawContours(영상, 외곽선 좌표 정보, 외곽선 인덱스, 색상, 두께)
    외곽선 인덱스: -1을 지정하면 모든 외곽선을 그림

# milkdrop.bmp 영상을 이용하여 이진화를 시키고, 외곽선을 검출하여 외곽선을 랜덤한 색상으로 표기
import cv2
import numpy as np
import random

img = cv2.imread('./milkdrop.bmp', cv2.IMREAD_GRAYSCALE)
_, img_bin = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
contours, _ = cv2.findContours(img_bin, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

h, w = img.shape[:2]
dst = np.zeros((h, w, 3), np.uint8)

for i in range(len(contours)):
    color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
    cv2.drawContours(dst, contours, i, color, 2)

cv2.imshow('img', img)
cv2.imshow('img_bin', img_bin)
cv2.imshow('dst', dst)
cv2.waitKey()

cv2.THRESH_BINARY_INV: 이진 반전, cv2.THRESH_OTSU: 이 옵션은 이미지의 히스토그램을 분석하여 최적의 임계값을 자동으로 계산, 첫 번째 값 _는 Otsu의 임계값(임계값 T). 코드에서 이 값을 무시하기 위해 _로 처리

cv2.RETR_EXTERNAL: 이미지에서 외부 윤곽선만을 검출, cv2.CHAIN_APPROX_NONE: 윤곽선의 모든 점을 저장, contours: 검출된 윤곽선의 리스트, _: 계층 정보가 반환. 여기서도 계층 정보는 필요하지 않으므로 _로 처리

원본
threshold부분
Contours 그리기

외곽선의 길이 구하기
cv2.arcLength(외곽선 좌표, 폐곡선 여부)

면적 구하기
cv2.contourArea(외곽선 좌표, False)

바운딩 박스 구하기
cv2.boundingRect(외곽선 좌표)


외곽선 근사화
검출한 외곽선 정보를 분석하여 정점수가 적은 외곽선 또는 다각형으로 표현할 수 있게 만드는 것
cv2.approxPolyDP(외곽선 좌표, 근사화 정밀도 조절, 폐곡선 여부)
근사화 정밀도 조절: 입력 외곽선과 근사화된 외곽선 사이의 최대 길이. 값이 작을수록 다각형이 정확해지고, 꼭지점 수가 늘어남

블록 부분이 있는지 여부
cv2.isContourConvex()
블록 부분이 있으면 True, 없으면 False

블록 외피를 계산
cv2.convexHull()
주어진 점 집합을 둘러싸는 가장 작은 블록 다각형을 반환

import cv2
import math


def setLabel(img, pts, label):
    (x, y, w, h) = cv2.boundingRect(pts)
    pt1 = (x, y)
    pt2 = (x + w, y + h)
    cv2.rectangle(img, pt1, pt2, (0, 0, 255), 2)
    cv2.putText(img, label, pt1, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255))


img = cv2.imread('./polygon.bmp')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, img_bin = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
contours, _ = cv2.findContours(img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# print(contours)

for pts in contours:
    if cv2.contourArea(pts) < 200:
        continue
    approx = cv2.approxPolyDP(pts, cv2.arcLength(pts, True) * 0.02, True)
    # print(approx)
    vtc = len(approx)
    print(vtc)

    if vtc == 3:
        setLabel(img, pts, "TRI")
    elif vtc == 4:
        setLabel(img, pts, 'RECT')
    else:
        length = cv2.arcLength(pts, True)
        area = cv2.contourArea(pts)
        ratio = 4. * math.pi * area / (length * length)  # 원인지 여부
        if ratio > 0.70:
            setLabel(img, pts, 'CIR')
        else:
            setLabel(img, pts, 'NONAME')

cv2.imshow('img', img)
cv2.imshow('gray', gray)
cv2.imshow('img_bin', img_bin)
cv2.waitKey()

vtc ==3은 삼각형 tri표시, vtc==4은 사각형 RECT표시, ratio가 0.7이상은 원형 CIR표시, 그외는 NONAME처리
BGR2GRAY 로 gray처리
이진화 및 외부 윤곽선 처리


OCR(Optical Character Recognition)
- 광학 문자 인식
- 영상이나 문서에서 텍스트를 자동으로 인식하고 컴퓨터가 이행할 수 있는 텍스트 데이터로 변환하는 프로세스
- Tesseract, EasyOCR, PaddleOCR, CLOVA OCR(네이버 API), Cloud Vision(구글 API) ..

테서렉트
- 오픈 소스 OCR 라이브러리로 구글에서 개발하고 현재는 여러 커뮤니티에 의해 유지보수
https://github.com/UB-Mannheim/tesseract/wiki 를 다운로드
- tesseract-ocr-w64-setup-5.4.0.20240606.exe (64비트) 를 다운로드 -> Choose Components에서 Additional Script Data(download) 트리를 내림 -> Hangul Script와 hangul Vertical Script 체크, Additional Language Data(download) 트리를 내림 -> korean 체크

윈도우 환경설정
탐색기 -> '내 PC' 마우스 오른쪽 버튼 클릭 '속성' -> 창 최대화 후 우측 메뉴 '고급 시스템 설정' -> '환경 변수' 버튼 클릭 -> '시스템 변수'에서 'path'를 선택하고 '편집' 버튼 클릭 -> '새로 만들기' 버튼 클릭 -> 테서렉트 설치 경로 추가(C:\Program Files\Tesseract-OCR) -> 이후 파이참 재실행

팀과제
Tesseract, EasyOCR, PaddleOCR, CLOVA OCR(네이버 API), Cloud Vision(구글 API) ..
위 라이브러리 또는 API를 사용하여 OCR 프로젝트를 개발
(포스터, 간판, 주민등록증, 운전면허증, 차량번호인식 ..)


import cv2
import pytesseract

img = cv2.imread('./hello.png')
dst = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# lang = 'kor', lang='eng', lang='kor+eng'
text = pytesseract.image_to_string(dst, lang='kor+eng')
print(text)

import numpy as np

ndarr1 = np.array([1, 5, 1, 4, 4])
ndarr2 = np.array([9, 4, 0, 4, 0])
result = np.lexsort((ndarr2, ndarr1))
# ndarr2 배열을 기준으로 오름차순으로 정렬
# 0, 0, 4, 4, 9
# 동일한 값의 경우 ndarr2 배열의 기준으로 ndarr1 오름차순 한 인덱스를 반환
#
print(result)

surnames = ('Hertz',    'Galilei', 'Hertz')
first_names = ('Heinrich', 'Galileo', 'Gustav') # 1 2 0
ind = np.lexsort((first_names, surnames))
print(ind)


a = [1, 5, 1, 4, 3, 4, 4]  # First sequence
b = [9, 4, 0, 4, 0, 2, 1]  # Second sequence
ind = np.lexsort((b, a))  # Sort by `a`, then by `b`
print(ind)


x = [[1, 2, 3, 4],
     [4, 3, 2, 1],
     [2, 1, 4, 3]]
y = [[2, 2, 1, 1],
     [1, 2, 1, 2],
     [1, 1, 2, 1]]
print(np.lexsort((x, y), axis=1))

import cv2
import pytesseract
import numpy as np

def reorderPts(pts):
    '''
    [[903. 199.]
     [179. 200.]
     [159. 593.]
     [938. 581.]]

    [[179 200]]
    [[159 593]]
    [[938 581]]
    [[903 199]]
    '''
    # print(pts)
    # print(pts[:, 1])
    # print(pts[:, 0])
    idx = np.lexsort((pts[:, 1], pts[:, 0]))
    # print(idx)
    pts = pts[idx]
    # print(pts)

    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]]
    # print(pts)
    return pts


img = cv2.imread('./namecard.jpg')

dw, dh = 700, 400
srcQuad = np.array([[0, 0], [0, 0], [0, 0], [0, 0]], np.float32)
dstQuad = np.array([[0, 0], [0, dh], [dw, dh], [dw, 0]], np.float32)
dst = np.zeros((dh, dw), np.uint8)

src_gray = cv2.cvtColor(img, 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)

cpy = img.copy()
for pts in contours:
    if cv2.contourArea(pts) < 1000:
        continue

    approx = cv2.approxPolyDP(pts, cv2.arcLength(pts, True) * 0.02, True)
    cv2.polylines(cpy, [approx], True, (0, 255, 0), 2)
    # print(approx)

    srcQuad = reorderPts(approx.reshape(4, 2).astype(np.float32))
    pers = cv2.getPerspectiveTransform(srcQuad, dstQuad)
    dst = cv2.warpPerspective(img, pers, (dw, dh))
    dst_gray = cv2.cvtColor(dst, cv2.COLOR_BGR2GRAY)
    print(pytesseract.image_to_string(dst_gray, lang='kor+eng'))

cv2.imshow('img', img)
cv2.imshow('cpy', cpy)
cv2.imshow('dst', dst)
cv2.waitKey()

원본
외곽선 처리
getPerpectiveTransform, WrapPerspective 이용해서 외곽선 안쪽 이미지만 출력