250912 [ Day 49 ] - OpenCV (5)

TaeHyun·2025년 9월 12일

TIL

목록 보기
51/184

시작하며

오늘은 실습 문제를 푸는 과정과 최종 작성된 코드가 리더님께서 풀이해주신 과정과 결과에 근접했던 문제들이 많아서 어제 코드를 열심히 뜯어본 보람이 느껴졌다. 또 리스트 컴프리헨션 등의 파이썬스러운 코드를 자연스럽게 사용하고 있는 스스로를 보니까 꾸준하게 프로그래머스 문제를 풀었던 점도 많은 도움이 되고 있는 것 같아서 더 열심히 공부할 수 있는 동기부여가 되는 것 같다.

윤곽선 검출

기본 구현

  • Canny
img = cv.imread(DOG_PATH)
coppied = img.copy()

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

# Canny
canny = cv.Canny(gray, 50, 150)

# 윤곽선 찾기
contours, hierachy = cv.findContours(canny, cv.RETR_CCOMP, cv.CHAIN_APPROX_NONE)

# 윤곽선 그리기
cv.drawContours(img, contours, -1, (0,255,0), 2)

cv.imshow("Contours", img)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

  • 둘 다 사용
img = cv.imread(DOG_PATH)
coppied = img.copy()

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

# 이진화
ret, binary = cv.threshold(gray, -1, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)

# Canny
canny = cv.Canny(binary, 50, 150)

# 윤곽선 찾기
contours, hierachy = cv.findContours(canny, cv.RETR_CCOMP, cv.CHAIN_APPROX_NONE)

# 윤곽선 그리기
cv.drawContours(img, contours, -1, (0,255,0), 2)

cv.imshow("Contours", img)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

boundingRect

  • 윤곽선을 둘러싼 사각형
img = cv.imread(DOG_PATH)
coppied = img.copy()

gray = cv.cvtColor(coppied, cv.COLOR_BGR2GRAY)

ret, binary = cv.threshold(gray, -1, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)

contours, hierachy = cv.findContours(binary, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)

for contour in contours:
    x, y, width, height = cv.boundingRect(contour)
    cv.rectangle(img, (x,y), (x+width, y+height), (255,0,0), 2, cv.LINE_AA)

cv.imshow("Bounding Rect", img)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

contourArea

  • contour의 면적 계산
img = cv.imread(CAT_PATH)
coppied = img.copy()

gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

ret, binary = cv.threshold(gray, -1, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)

contours, hierachy = cv.findContours(binary, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)

for contour in contours:
    # contour의 면적이 1000보다 큰 영역만 표시
    if cv.contourArea(contour) > 1000:
        x, y, width, height = cv.boundingRect(contour)
        cv.rectangle(img, (x,y), (x+width, y+height), (255,0,0), 2, cv.LINE_AA)

cv.imshow("Bounding Rect", img)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

  • 트랙바 적용
img = cv.imread("../images/vehicles.png")
coppied = img.copy()

gray = cv.cvtColor(coppied, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, -1, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
contours, hierachy = cv.findContours(binary, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)

name = "Index Trackbar"
cv.namedWindow(name)

filtered_contours = [c for c in contours if cv.contourArea(c) > 700]

cv.createTrackbar("index", name, 0, len(filtered_contours)-1, lambda x:x)

while True:
    coppied = img.copy()

    index = cv.getTrackbarPos("index", name)
    contour = filtered_contours[index]

    x, y, width, height = cv.boundingRect(contour)
    cv.rectangle(coppied, (x,y), (x+width, y+height), (255,0,0), 2, cv.LINE_AA)
    
    cv.imshow(name, coppied)

    if cv.waitKey(1) == ord("q"):
        break

cv.destroyAllWindows()
cv.waitKey(1)

  • 트랙바 콜백 함수 사용
img = cv.imread("../images/playing_cards.png")
coppied = img.copy()

gray = cv.cvtColor(coppied, cv.COLOR_BGR2GRAY)
ret, binary = cv.threshold(gray, -1, 255, cv.THRESH_BINARY | cv.THRESH_OTSU)
contours, hierachy = cv.findContours(binary, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)

crops = []

for idx, contour in enumerate(contours):
    if cv.contourArea(contour) > 1300:
        x, y, width, height = cv.boundingRect(contour)
        cv.rectangle(coppied, (x,y), (x+width, y+height), (0,255,0), 2, cv.LINE_AA)
        crop = img[y:y+height, x:x+width]
        crops.append(crop)

def on_trackbar(val):
    target = crops[val]
    cv.imshow("Card", target)

name = "Original"
cv.namedWindow(name)

cv.createTrackbar("index", name, 0, len(crops)-1, on_trackbar)

cv.imshow(name, coppied)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

히스토그램

img = cv.imread(CAT_PATH)

b = cv.calcHist([img], [0], None, [256], [0, 256])
g = cv.calcHist([img], [1], None, [256], [0, 256])
r = cv.calcHist([img], [2], None, [256], [0, 256])

plt.plot(b, color="b")
plt.plot(g, color="g")
plt.plot(r, color="r")

plt.title("Image Histogram")

plt.xlabel("Pixel Value")
plt.ylabel("Frequency")

plt.show()

팽창과 침식, 열림과 닫힘

팽창

  • 흰색 영역을 확장하여 검은 영역을 채움
img = cv.imread(DILATE)

kernel = np.ones((3,3), dtype=np.uint8)

dilate_1 = cv.dilate(img, kernel, iterations=1)
dilate_2 = cv.dilate(img, kernel, iterations=2)
dilate_3 = cv.dilate(img, kernel, iterations=3)

cv.imshow("img", img)
cv.imshow("dilate_1", dilate_1)
cv.imshow("dilate_2", dilate_2)
cv.imshow("dilate_3", dilate_3)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

침식

  • 흰색 영역의 외곽을 검은색으로 변경
img = cv.imread(ERODE)

kernel = np.ones((3,3), dtype=np.uint8)

erode_1 = cv.erode(img, kernel, iterations=1)
erode_2 = cv.erode(img, kernel, iterations=2)
erode_3 = cv.erode(img, kernel, iterations=3)

cv.imshow("img", img)
cv.imshow("erode_1", erode_1)
cv.imshow("erode_2", erode_2)
cv.imshow("erode_3", erode_3)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

열림

  • 침식 후 팽창
  • 노이즈 제거 후 복원
img = cv.imread(ERODE)

kernel = np.ones((3,3), dtype=np.uint8)
erode = cv.erode(img, kernel, iterations=3)
dilate = cv.dilate(erode, kernel, iterations=3)

cv.imshow("img", img)
cv.imshow("open", dilate)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

닫힘

  • 팽창 후 침식
  • 구멍을 메운 후 복원
img = cv.imread(DILATE)

kernel = np.ones((3,3), dtype=np.uint8)
dilate = cv.dilate(img, kernel, iterations=3)
erode = cv.erode(dilate, kernel, iterations=3)

cv.imshow("img", img)
cv.imshow("close", erode)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

얼굴인식 (Haar Cascade)

Haar 특징

  • 밝고 어두운 영역 간의 차이를 활용해 객체를 감지하는 규칙
  • 알고리즘이 단계별(계단식) 구조로 작동
    1. 특정 크기(예: 24x24 픽셀)의 창을 이미지 위에서 이동시키면서 이미지를 검사
    2. 각 창에서 Haar 특징(밝고 어두운 패턴)을 비교
    3. 창 안의 밝기 차이가 특정 조건을 만족하면 "여기에 얼굴이 있을 가능성이 있다"고 판단
    4. 얼굴일 가능성이 높은 위치를 Cascade 구조를 통해 더 정밀하게 검사

cv2.detectMultiScale

  • scaleFactor : 이미지 크기를 얼마나 줄여가며 검출을 진행할지를 결정, 기본적으로 1.1로 많이 사용
  • minNeighbors : 검출된 객체 주변에 얼마나 많은 "이웃" 검출이 있어야 최종적으로 객체로 간주할지 결정
  • minSize / maxSize : 검출할 객체의 최소 / 최대 크기, 해당 사이즈보다 작거나 큰 객체는 제외
# 얼굴인식
face_cascade = cv.CascadeClassifier(FACE_CASCADE)

img = cv.imread(FACE)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

faces = face_cascade.detectMultiScale(
    gray, scaleFactor=1.1, minNeighbors=10, minSize=(10,10)
)

if len(faces):
    for face in faces:
        x, y, width, height = face
        cv.rectangle(img, (x,y), (x+width, y+height), (0,255,0), 2, cv.LINE_AA)

cv.imshow("img", img)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)
# 눈인식
eye_cascade = cv.CascadeClassifier(EYE_CASCADE)

img = cv.imread(FACE)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

eyes = eye_cascade.detectMultiScale(
    gray, scaleFactor=1.1, minNeighbors=15, minSize=(10,10)
)

if len(eyes):
    for eye in eyes:
        x, y, width, height = eye
        cv.rectangle(img, (x,y), (x+width, y+height), (0,255,0), 2, cv.LINE_AA)

cv.imshow("img", img)

cv.waitKey(0)
cv.destroyAllWindows()
cv.waitKey(1)

마치며

얼굴 인식을 간단히 살펴보고 수업이 마무리되었는데 꽤 재미있는 파트인 것 같다. 그리고 트랙바를 구현할 때 while문 대신 콜백 함수를 사용하는 방법이 더 직관적이고 편한 것 같다고 느꼈다. 윈도우를 하나만 표시할 때는 최초에 함수를 따로 실행해줘야 하는 귀찮은 부분이 있긴 하지만, getTrackbarPos()와 while문을 사용하는 것보다는 더 편한 것 같아서 계속 사용해보면서 장단점을 더 느껴봐야겠다.

NOTION

MY NOTION (OpenCV. 04)

profile
Hello I'm TaeHyunAn, Currently Studying Data Analysis

0개의 댓글