์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿค– ์บก์ฐจ ์ž๋™ ์ธ์‹ ํ”„๋กœ๊ทธ๋žจ ์†Œ์Šค ์™„์ „ ์ •๋ณต

๐Ÿค– ์บก์ฐจ ์ž๋™ ์ธ์‹ ํ”„๋กœ๊ทธ๋žจ ์†Œ์Šค ์™„์ „ ์ •๋ณต

์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ธฐ์ˆ ๋กœ ํ’€์–ด๋ณด๋Š” ์บก์ฐจ ์ธ์‹์˜ ์„ธ๊ณ„ ๐Ÿ”

A7K9 CAPTCHA Image Processing โ€ข Noise Reduction โ€ข OCR Recognition ๐Ÿ“ท โš™๏ธ โœ… ๐Ÿค–

๐ŸŽฏ ์บก์ฐจ๊ฐ€ ๋ญ๊ธธ๋ž˜ ์ด๋ ‡๊ฒŒ ๋ณต์žกํ•ด?

์•ผ, ์ธํ„ฐ๋„ท ํ•˜๋‹ค ๋ณด๋ฉด ์ง„์งœ ์งœ์ฆ ๋‚˜๋Š” ๊ฒŒ ํ•˜๋‚˜ ์žˆ์ž–์•„? ๋ฐ”๋กœ ๊ทธ ์บก์ฐจ(CAPTCHA)๋ผ๋Š” ๊ฑฐ! ๐Ÿ˜ค

"์ €๋Š” ๋กœ๋ด‡์ด ์•„๋‹™๋‹ˆ๋‹ค" ์ฒดํฌํ•˜๊ณ , ์‹ ํ˜ธ๋“ฑ ์ฐพ๊ณ , ํšก๋‹จ๋ณด๋„ ์ฐพ๊ณ ... ๊ฐ€๋”์€ "์ด๊ฒŒ ์‹ ํ˜ธ๋“ฑ์ธ๊ฐ€ ํ‘œ์ง€ํŒ์ธ๊ฐ€?" ๊ณ ๋ฏผํ•˜๋‹ค๊ฐ€ ํ‹€๋ฆฌ๊ธฐ๋„ ํ•˜๊ณ  ๋ง์ด์•ผ. ๊ทผ๋ฐ ์ด๊ฒŒ ์™œ ์กด์žฌํ•˜๋Š”์ง€ ์•Œ์•„?

CAPTCHA๋Š” Completely Automated Public Turing test to tell Computers and Humans Apart์˜ ์•ฝ์ž์•ผ. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด "๋„ˆ ์‚ฌ๋žŒ์ด์•ผ? ๋กœ๋ด‡์ด์•ผ?" ๊ตฌ๋ถ„ํ•˜๋Š” ํ…Œ์ŠคํŠธ์ง€. ๐Ÿค”

์›น์‚ฌ์ดํŠธ ์ž…์žฅ์—์„œ๋Š” ๋ด‡(bot)๋“ค์ด ์ž๋™์œผ๋กœ ํšŒ์›๊ฐ€์ž…ํ•˜๊ฑฐ๋‚˜, ํ‹ฐ์ผ“์„ ์‹น์“ธ์ดํ•˜๊ฑฐ๋‚˜, ์ŠคํŒธ์„ ๋ฟŒ๋ฆฌ๋Š” ๊ฑธ ๋ง‰์•„์•ผ ํ•˜๊ฑฐ๋“ . ๊ทธ๋ž˜์„œ ์‚ฌ๋žŒ๋งŒ ํ’€ ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ๋ฅผ ๋‚ด๋Š” ๊ฑฐ์•ผ.

๐Ÿ’ก ์บก์ฐจ์˜ ์ข…๋ฅ˜๋“ค 1. ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์บก์ฐจ: ์™œ๊ณก๋œ ๊ธ€์ž๋‚˜ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜๋Š” ๋ฐฉ์‹
2. ์ด๋ฏธ์ง€ ๊ธฐ๋ฐ˜ ์บก์ฐจ: ํŠน์ • ๋ฌผ์ฒด๊ฐ€ ์žˆ๋Š” ์ด๋ฏธ์ง€๋ฅผ ์„ ํƒํ•˜๋Š” ๋ฐฉ์‹ (๊ตฌ๊ธ€ reCAPTCHA)
3. ์˜ค๋””์˜ค ์บก์ฐจ: ์Œ์„ฑ์œผ๋กœ ๋“ค๋ ค์ฃผ๋Š” ์ˆซ์ž๋‚˜ ๋ฌธ์ž๋ฅผ ์ž…๋ ฅ
4. ํผ์ฆ ์บก์ฐจ: ํผ์ฆ ์กฐ๊ฐ์„ ๋งž์ถ”๋Š” ๋ฐฉ์‹
5. ํ–‰๋™ ๊ธฐ๋ฐ˜ ์บก์ฐจ: ๋งˆ์šฐ์Šค ์›€์ง์ž„ ํŒจํ„ด์„ ๋ถ„์„ํ•˜๋Š” ๋ฐฉ์‹
๊ทผ๋ฐ ๋ฌธ์ œ๋Š” ๋ญ๋ƒ? ๊ธฐ์ˆ ์ด ๋ฐœ์ „ํ•˜๋ฉด์„œ ์ด ์บก์ฐจ๋ฅผ ์ž๋™์œผ๋กœ ํ’€์–ด๋ฒ„๋ฆฌ๋Š” ํ”„๋กœ๊ทธ๋žจ๋“ค์ด ๋‚˜ํƒ€๋‚˜๊ธฐ ์‹œ์ž‘ํ•œ ๊ฑฐ์ง€! ๐Ÿ˜ˆ

๊ทธ๋ž˜์„œ ์˜ค๋Š˜์€ ์บก์ฐจ ์ž๋™ ์ธ์‹ ํ”„๋กœ๊ทธ๋žจ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€, ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ธฐ์ˆ ์€ ์–ด๋–ป๊ฒŒ ํ™œ์šฉ๋˜๋Š”์ง€ ์™„์ „ ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ. ๋ฌผ๋ก  ์ด๊ฑด ๊ต์œก ๋ชฉ์ ์ด๊ณ , ์‹ค์ œ๋กœ ์•…์šฉํ•˜๋ฉด ์•ˆ ๋ผ! โš ๏ธ

๐Ÿ”ฌ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ์˜ ๊ธฐ๋ณธ ์›๋ฆฌ

์บก์ฐจ๋ฅผ ์ธ์‹ํ•˜๋ ค๋ฉด ๋จผ์ € ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ(Image Processing)์— ๋Œ€ํ•ด ์•Œ์•„์•ผ ํ•ด. ์ปดํ“จํ„ฐ๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ "๋ณด๋Š”์ง€" ์ดํ•ดํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด๊ฑฐ๋“ .

๐Ÿ“ธ ์ปดํ“จํ„ฐ๋Š” ์ด๋ฏธ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ๋ณผ๊นŒ?

์šฐ๋ฆฌ๊ฐ€ ๋ณด๋Š” ์ด๋ฏธ์ง€๋Š” ์‚ฌ์‹ค ์ˆ˜๋งŽ์€ ํ”ฝ์…€(pixel)๋“ค์˜ ์ง‘ํ•ฉ์ด์•ผ. ๊ฐ ํ”ฝ์…€์€ RGB(๋นจ๊ฐ•, ์ดˆ๋ก, ํŒŒ๋ž‘) ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด ๊ฐ’๋“ค์˜ ์กฐํ•ฉ์œผ๋กœ ์ƒ‰์ƒ์ด ๋งŒ๋“ค์–ด์ง€์ง€.

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ:
โ€ข ํฐ์ƒ‰ = RGB(255, 255, 255)
โ€ข ๊ฒ€์€์ƒ‰ = RGB(0, 0, 0)
โ€ข ๋นจ๊ฐ„์ƒ‰ = RGB(255, 0, 0)

์บก์ฐจ ์ด๋ฏธ์ง€๋Š” ๋ณดํ†ต ์ด๋Ÿฐ ํ”ฝ์…€ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์ž‘ํ•ด์„œ ์‚ฌ๋žŒ์€ ์ฝ์„ ์ˆ˜ ์žˆ์ง€๋งŒ ์ปดํ“จํ„ฐ๋Š” ์ฝ๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค์–ด์ ธ ์žˆ์–ด. ๊ธ€์ž๋ฅผ ์™œ๊ณก์‹œํ‚ค๊ฑฐ๋‚˜, ๋ฐฐ๊ฒฝ์— ๋…ธ์ด์ฆˆ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ์„ ์„ ๊ทธ์–ด๋†“๋Š” ์‹์œผ๋กœ ๋ง์ด์•ผ. ๐ŸŽจ

๐ŸŽ“ ์žฌ๋Šฅ๋„ท ๊ฟ€ํŒ! ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ๋‚˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ด€๋ จํ•ด์„œ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค์—๊ฒŒ 1:1 ๊ณผ์™ธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ง์ ‘ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋„ ํ•ด์ฃผ๊ณ , ํ”„๋กœ์ ํŠธ ๋ฉ˜ํ† ๋ง๋„ ํ•ด์ค€๋‹ค๊ณ . ํ˜ผ์ž ์‚ฝ์งˆํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ํšจ์œจ์ ์ด์ง€! ๐Ÿ’ช
๐Ÿ› ๏ธ ์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ (Preprocessing) ๋‹จ๊ณ„

์บก์ฐจ๋ฅผ ์ธ์‹ํ•˜๊ธฐ ์ „์— ์ด๋ฏธ์ง€๋ฅผ "๊นจ๋—ํ•˜๊ฒŒ" ๋งŒ๋“œ๋Š” ์ž‘์—…์ด ํ•„์š”ํ•ด. ์ด๊ฑธ ์ „์ฒ˜๋ฆฌ๋ผ๊ณ  ํ•˜๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ณผ์ •๋“ค์ด ํฌํ•จ๋ผ:

1๏ธโƒฃ ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋ณ€ํ™˜ (Grayscale Conversion)

์ปฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ํ‘๋ฐฑ์œผ๋กœ ๋ฐ”๊พธ๋Š” ๊ฑฐ์•ผ. ์™œ๋ƒ๊ณ ? ์ƒ‰์ƒ ์ •๋ณด๋Š” ์บก์ฐจ ์ธ์‹์— ๋ณ„๋กœ ์ค‘์š”ํ•˜์ง€ ์•Š๊ฑฐ๋“ . ์˜คํžˆ๋ ค ๋ฐ์ดํ„ฐ๋ฅผ ๋‹จ์ˆœํ™”์‹œ์ผœ์„œ ์ฒ˜๋ฆฌ ์†๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ์–ด.

import cv2
import numpy as np

# ์ด๋ฏธ์ง€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
image = cv2.imread('captcha.png')

# ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋ณ€ํ™˜
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# ๊ฒฐ๊ณผ ์ €์žฅ
cv2.imwrite('gray_captcha.png', gray)

2๏ธโƒฃ ์ด์ง„ํ™” (Binarization)

์ด๋ฏธ์ง€๋ฅผ ์™„์ „ํžˆ ํ‘๋ฐฑ(0 ๋˜๋Š” 255)์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฑฐ์•ผ. ๊ธ€์ž๋Š” ๊ฒ€์€์ƒ‰, ๋ฐฐ๊ฒฝ์€ ํฐ์ƒ‰ ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ฑฐ์ง€.

๊ฐ€์žฅ ๋งŽ์ด ์“ฐ๋Š” ๋ฐฉ๋ฒ•์ด Otsu's Binarization์ด์•ผ. ์ด๊ฑด ์ž๋™์œผ๋กœ ์ตœ์ ์˜ ์ž„๊ณ„๊ฐ’(threshold)์„ ์ฐพ์•„์„œ ์ด์ง„ํ™”ํ•ด์ฃผ๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์ด์•ผ. ๐Ÿ‘

# Otsu's ์ด์ง„ํ™”
ret, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# ๋˜๋Š” ์ ์‘ํ˜• ์ด์ง„ํ™” (Adaptive Thresholding)
adaptive = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
                                  cv2.THRESH_BINARY, 11, 2)

3๏ธโƒฃ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ (Noise Reduction)

์บก์ฐจ ์ด๋ฏธ์ง€์—๋Š” ์˜๋„์ ์œผ๋กœ ์ ์ด๋‚˜ ์„  ๊ฐ™์€ ๋…ธ์ด์ฆˆ๊ฐ€ ๋งŽ์ด ๋“ค์–ด๊ฐ€ ์žˆ์–ด. ์ด๊ฑธ ์ œ๊ฑฐํ•ด์•ผ ๊ธ€์ž๋ฅผ ์ œ๋Œ€๋กœ ์ธ์‹ํ•  ์ˆ˜ ์žˆ์ง€.

์ฃผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ฒ•๋“ค:
โ€ข ๊ฐ€์šฐ์‹œ์•ˆ ๋ธ”๋Ÿฌ(Gaussian Blur): ์ด๋ฏธ์ง€๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋งŒ๋“ค์–ด์„œ ๋…ธ์ด์ฆˆ ๊ฐ์†Œ
โ€ข ๋ฏธ๋””์•ˆ ํ•„ํ„ฐ(Median Filter): ์†Œ๊ธˆ-ํ›„์ถ” ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ์— ํšจ๊ณผ์ 
โ€ข ๋ชจํด๋กœ์ง€ ์—ฐ์‚ฐ(Morphology): ์นจ์‹(erosion)๊ณผ ํŒฝ์ฐฝ(dilation)์œผ๋กœ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ

# ๊ฐ€์šฐ์‹œ์•ˆ ๋ธ”๋Ÿฌ ์ ์šฉ
blurred = cv2.GaussianBlur(binary, (5, 5), 0)

# ๋ฏธ๋””์•ˆ ํ•„ํ„ฐ
median = cv2.medianBlur(binary, 5)

# ๋ชจํด๋กœ์ง€ ์—ฐ์‚ฐ (Opening = Erosion + Dilation)
kernel = np.ones((3,3), np.uint8)
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=1)

๐ŸŽฏ ๋ฌธ์ž ๋ถ„๋ฆฌ (Character Segmentation)

์ด๋ฏธ์ง€๋ฅผ ๊นจ๋—ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์œผ๋ฉด, ์ด์ œ ๊ฐ ๊ธ€์ž๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ถ„๋ฆฌํ•ด์•ผ ํ•ด. ์ด ๊ณผ์ •์„ ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜(Segmentation)์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ, ์ƒ๊ฐ๋ณด๋‹ค ๊นŒ๋‹ค๋กœ์šด ์ž‘์—…์ด์•ผ. ๐Ÿ˜…

๐Ÿ” ์œค๊ณฝ์„  ๊ฒ€์ถœ (Contour Detection)

OpenCV์˜ findContours ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋ฏธ์ง€์—์„œ ๊ฐ์ฒด์˜ ์œค๊ณฝ์„ ์„ ์ฐพ์„ ์ˆ˜ ์žˆ์–ด. ๊ฐ ๊ธ€์ž๊ฐ€ ํ•˜๋‚˜์˜ ์œค๊ณฝ์„ ์œผ๋กœ ์ธ์‹๋˜๋Š” ๊ฑฐ์ง€.

# ์œค๊ณฝ์„  ์ฐพ๊ธฐ
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# ๊ฐ ์œค๊ณฝ์„ ์„ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค๋กœ ๋ณ€ํ™˜
char_boxes = []
for contour in contours:
    x, y, w, h = cv2.boundingRect(contour)
    
    # ๋„ˆ๋ฌด ์ž‘๊ฑฐ๋‚˜ ํฐ ์œค๊ณฝ์„ ์€ ๋…ธ์ด์ฆˆ๋กœ ํŒ๋‹จํ•˜๊ณ  ์ œ์™ธ
    if w > 10 and h > 10 and w < 100 and h < 100:
        char_boxes.append((x, y, w, h))

# x ์ขŒํ‘œ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ (์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ ์ˆœ์„œ)
char_boxes = sorted(char_boxes, key=lambda box: box[0])

๐Ÿ“ ๋ฌธ์ž ์˜์—ญ ์ถ”์ถœ

๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ๊ฐ ๊ธ€์ž๋ฅผ ๊ฐœ๋ณ„ ์ด๋ฏธ์ง€๋กœ ์ž˜๋ผ๋‚ด๋Š” ๊ฑฐ์•ผ. ์ด๋•Œ ์ฃผ์˜ํ•  ์ ์€ ๊ธ€์ž๋“ค์ด ๋ถ™์–ด์žˆ๊ฑฐ๋‚˜ ๊ฒน์ณ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์ง€.

characters = []
for i, (x, y, w, h) in enumerate(char_boxes):
    # ๊ธ€์ž ์˜์—ญ ์ถ”์ถœ
    char_img = binary[y:y+h, x:x+w]
    
    # ํฌ๊ธฐ ์ •๊ทœํ™” (์˜ˆ: 28x28)
    char_img = cv2.resize(char_img, (28, 28))
    
    characters.append(char_img)
    
    # ๋””๋ฒ„๊น…์šฉ ์ €์žฅ
    cv2.imwrite(f'char_{i}.png', char_img)

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ ๊ธ€์ž๊ฐ€ ์„œ๋กœ ๋ถ™์–ด์žˆ๋Š” ๊ฒฝ์šฐ (์˜ˆ: "rn"์ด "m"์ฒ˜๋Ÿผ ๋ณด์ด๋Š” ๊ฒฝ์šฐ) ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜์ด ์ œ๋Œ€๋กœ ์•ˆ ๋  ์ˆ˜ ์žˆ์–ด. ์ด๋Ÿด ๋•Œ๋Š” ์ˆ˜์ง ํˆฌ์˜(Vertical Projection) ๊ธฐ๋ฒ•์ด๋‚˜ ๋”ฅ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด!
๐Ÿ“Š ์ˆ˜์ง/์ˆ˜ํ‰ ํˆฌ์˜ (Projection Profile)

์ด๊ฑด ์ข€ ๋” ์ •๊ตํ•œ ๋ฐฉ๋ฒ•์ธ๋ฐ, ์ด๋ฏธ์ง€์˜ ๊ฐ ํ–‰๊ณผ ์—ด์˜ ํ”ฝ์…€ ํ•ฉ์„ ๊ณ„์‚ฐํ•ด์„œ ๊ธ€์ž์˜ ๊ฒฝ๊ณ„๋ฅผ ์ฐพ๋Š” ๊ฑฐ์•ผ.

# ์ˆ˜์ง ํˆฌ์˜ (๊ฐ ์—ด์˜ ๊ฒ€์€ ํ”ฝ์…€ ๊ฐœ์ˆ˜ ํ•ฉ์‚ฐ)
vertical_projection = np.sum(binary == 0, axis=0)

# ์ˆ˜ํ‰ ํˆฌ์˜ (๊ฐ ํ–‰์˜ ๊ฒ€์€ ํ”ฝ์…€ ๊ฐœ์ˆ˜ ํ•ฉ์‚ฐ)
horizontal_projection = np.sum(binary == 0, axis=1)

# ํˆฌ์˜๊ฐ’์ด 0์ธ ์ง€์ ์„ ๊ธ€์ž ๊ฒฝ๊ณ„๋กœ ํŒ๋‹จ
# (์‹ค์ œ๋กœ๋Š” ์ž„๊ณ„๊ฐ’์„ ์„ค์ •ํ•ด์„œ ์‚ฌ์šฉ)
import matplotlib.pyplot as plt
plt.plot(vertical_projection)
plt.title('Vertical Projection')
plt.show()

์ด ๋ฐฉ๋ฒ•์€ ๊ธ€์ž ๊ฐ„๊ฒฉ์ด ๋ช…ํ™•ํ•  ๋•Œ ํŠนํžˆ ํšจ๊ณผ์ ์ด์•ผ. ํˆฌ์˜ ๊ทธ๋ž˜ํ”„์—์„œ ๊ณจ์งœ๊ธฐ(valley) ๋ถ€๋ถ„์ด ๊ธ€์ž ์‚ฌ์ด์˜ ๊ณต๋ฐฑ์ด๊ฑฐ๋“ ! ๐Ÿ“ˆ

๐Ÿค– ๋ฌธ์ž ์ธ์‹ (OCR - Optical Character Recognition)

์ž, ์ด์ œ ๋ถ„๋ฆฌ๋œ ๊ธ€์ž๋“ค์„ ์‹ค์ œ๋กœ "์ฝ์–ด์•ผ" ํ•˜๋Š” ๋‹จ๊ณ„์•ผ. ์ด๊ฒŒ ๋ฐ”๋กœ OCR(๊ด‘ํ•™ ๋ฌธ์ž ์ธ์‹)์ด์ง€! ๐Ÿ”ค

๐Ÿ“š ์ „ํ†ต์ ์ธ OCR ๋ฐฉ๋ฒ•๋“ค

1๏ธโƒฃ Tesseract OCR

๊ตฌ๊ธ€์ด ๊ฐœ๋ฐœํ•œ ์˜คํ”ˆ์†Œ์Šค OCR ์—”์ง„์ด์•ผ. ๊ฐ€์žฅ ์œ ๋ช…ํ•˜๊ณ  ๋งŽ์ด ์“ฐ์ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ํ•˜๋‚˜์ง€.

import pytesseract
from PIL import Image

# Tesseract ๊ฒฝ๋กœ ์„ค์ • (Windows)
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

# ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ ์ถ”์ถœ
image = Image.open('captcha.png')
text = pytesseract.image_to_string(image, config='--psm 7')  # PSM 7: ๋‹จ์ผ ํ…์ŠคํŠธ ๋ผ์ธ

print(f"์ธ์‹๋œ ํ…์ŠคํŠธ: {text}")

๊ทผ๋ฐ ์†”์งํžˆ ๋งํ•˜๋ฉด, Tesseract๋Š” ์ผ๋ฐ˜ ๋ฌธ์„œ ์Šค์บ”์—๋Š” ์ข‹์ง€๋งŒ ์™œ๊ณก๋œ ์บก์ฐจ ์ด๋ฏธ์ง€์—๋Š” ์„ฑ๋Šฅ์ด ๋ณ„๋กœ์•ผ. ๐Ÿ˜… ๊ทธ๋ž˜์„œ ์ „์ฒ˜๋ฆฌ๋ฅผ ์—„์ฒญ ์ž˜ ํ•ด์ค˜์•ผ ํ•˜๊ฑฐ๋‚˜, ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์จ์•ผ ํ•ด.

2๏ธโƒฃ ํ…œํ”Œ๋ฆฟ ๋งค์นญ (Template Matching)

๋ฏธ๋ฆฌ ์ค€๋น„๋œ ๊ธ€์ž ํ…œํ”Œ๋ฆฟ๊ณผ ๋น„๊ตํ•ด์„œ ๊ฐ€์žฅ ์œ ์‚ฌํ•œ ๊ฑธ ์ฐพ๋Š” ๋ฐฉ์‹์ด์•ผ. ๊ฐ„๋‹จํ•˜์ง€๋งŒ ํšจ๊ณผ์ ์ผ ์ˆ˜ ์žˆ์–ด!

# ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€๋“ค ๋กœ๋“œ
templates = {}
for char in '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ':
    template = cv2.imread(f'templates/{char}.png', 0)
    templates[char] = template

# ๊ฐ ๋ถ„๋ฆฌ๋œ ๊ธ€์ž์™€ ํ…œํ”Œ๋ฆฟ ๋น„๊ต
def match_template(char_img, templates):
    best_match = None
    best_score = -1
    
    for char, template in templates.items():
        # ํฌ๊ธฐ ๋งž์ถ”๊ธฐ
        resized_template = cv2.resize(template, (char_img.shape[1], char_img.shape[0]))
        
        # ํ…œํ”Œ๋ฆฟ ๋งค์นญ
        result = cv2.matchTemplate(char_img, resized_template, cv2.TM_CCOEFF_NORMED)
        score = result[0][0]
        
        if score > best_score:
            best_score = score
            best_match = char
    
    return best_match, best_score

# ์‚ฌ์šฉ ์˜ˆ์‹œ
recognized_text = ""
for char_img in characters:
    char, score = match_template(char_img, templates)
    if score > 0.7:  # ์ž„๊ณ„๊ฐ’
        recognized_text += char

print(f"์ธ์‹ ๊ฒฐ๊ณผ: {recognized_text}")

์ด ๋ฐฉ๋ฒ•์˜ ์žฅ์ ์€ ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ , ํŠน์ • ์บก์ฐจ ์Šคํƒ€์ผ์— ํŠนํ™”์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฑฐ์•ผ. ๋‹จ์ ์€ ๊ธ€์ž ํฐํŠธ๋‚˜ ํฌ๊ธฐ๊ฐ€ ์กฐ๊ธˆ๋งŒ ๋‹ฌ๋ผ์ ธ๋„ ์ธ์‹๋ฅ ์ด ๋–จ์–ด์ง„๋‹ค๋Š” ๊ฑฐ์ง€. ๐ŸŽญ

3๏ธโƒฃ ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์ธ์‹ (KNN, SVM)

์ „ํ†ต์ ์ธ ๋จธ์‹ ๋Ÿฌ๋‹ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ํŠน์ง•(feature)์„ ์ถ”์ถœํ•ด์„œ ๋ถ„๋ฅ˜๊ธฐ๋ฅผ ํ•™์Šต์‹œํ‚ค๋Š” ๊ฑฐ์ง€.

์•Œ๊ณ ๋ฆฌ์ฆ˜ ํŠน์ง• ์žฅ์  ๋‹จ์ 
KNN K-์ตœ๊ทผ์ ‘ ์ด์›ƒ ๊ตฌํ˜„ ๊ฐ„๋‹จ, ํ•™์Šต ๋น ๋ฆ„ ์˜ˆ์ธก ๋А๋ฆผ, ๋ฉ”๋ชจ๋ฆฌ ๋งŽ์ด ์‚ฌ์šฉ
SVM ์„œํฌํŠธ ๋ฒกํ„ฐ ๋จธ์‹  ๊ณ ์ฐจ์› ๋ฐ์ดํ„ฐ์— ๊ฐ•ํ•จ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ์…‹์— ๋А๋ฆผ
Random Forest ๋žœ๋ค ํฌ๋ ˆ์ŠคํŠธ ๊ณผ์ ํ•ฉ ๋ฐฉ์ง€, ์•ˆ์ •์  ํ•ด์„์ด ์–ด๋ ค์›€

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
import numpy as np

# ๋ฐ์ดํ„ฐ ์ค€๋น„ (์˜ˆ์‹œ)
X_train = []  # ์ด๋ฏธ์ง€ ํŠน์ง• ๋ฒกํ„ฐ๋“ค
y_train = []  # ๋ ˆ์ด๋ธ” (์‹ค์ œ ๊ธ€์ž)

# ๊ฐ ๊ธ€์ž ์ด๋ฏธ์ง€๋ฅผ 1์ฐจ์› ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜
for char_img, label in training_data:
    feature_vector = char_img.flatten()  # 28x28 -> 784์ฐจ์› ๋ฒกํ„ฐ
    X_train.append(feature_vector)
    y_train.append(label)

X_train = np.array(X_train)
y_train = np.array(y_train)

# KNN ๋ชจ๋ธ ํ•™์Šต
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)

# ์˜ˆ์ธก
test_char = characters[0].flatten().reshape(1, -1)
predicted_char = knn.predict(test_char)
print(f"์˜ˆ์ธก๋œ ๊ธ€์ž: {predicted_char[0]}")

๐Ÿง  ๋”ฅ๋Ÿฌ๋‹์„ ํ™œ์šฉํ•œ ์บก์ฐจ ์ธ์‹

์š”์ฆ˜์€ ๋”ฅ๋Ÿฌ๋‹์ด ๋Œ€์„ธ์ง€! ํŠนํžˆ CNN(Convolutional Neural Network)์„ ์‚ฌ์šฉํ•˜๋ฉด ์บก์ฐจ ์ธ์‹ ์ •ํ™•๋„๋ฅผ ์—„์ฒญ๋‚˜๊ฒŒ ๋†’์ผ ์ˆ˜ ์žˆ์–ด. ๐Ÿš€

๐ŸŽจ CNN์ด ๋ญ๊ธธ๋ž˜?

CNN์€ ์ด๋ฏธ์ง€ ์ธ์‹์— ํŠนํ™”๋œ ์‹ ๊ฒฝ๋ง์ด์•ผ. ์‚ฌ๋žŒ์˜ ์‹œ๊ฐ ํ”ผ์งˆ์„ ๋ชจ๋ฐฉํ•ด์„œ ๋งŒ๋“ค์–ด์กŒ๋Š”๋ฐ, ์ด๋ฏธ์ง€์˜ ํŠน์ง•์„ ์ž๋™์œผ๋กœ ํ•™์Šตํ•  ์ˆ˜ ์žˆ์–ด.

๊ธฐ๋ณธ ๊ตฌ์กฐ๋Š” ์ด๋ž˜:
โ€ข Convolution Layer: ์ด๋ฏธ์ง€์—์„œ ํŠน์ง• ์ถ”์ถœ (์—ฃ์ง€, ํ…์Šค์ฒ˜ ๋“ฑ)
โ€ข Pooling Layer: ํŠน์ง• ๋งต ํฌ๊ธฐ ์ถ•์†Œ (์ค‘์š”ํ•œ ์ •๋ณด๋งŒ ๋‚จ๊น€)
โ€ข Fully Connected Layer: ์ตœ์ข… ๋ถ„๋ฅ˜ ์ˆ˜ํ–‰

๐Ÿ”ฅ Keras/TensorFlow๋กœ CNN ๊ตฌํ˜„ํ•˜๊ธฐ

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

# CNN ๋ชจ๋ธ ์ •์˜
model = keras.Sequential([
    # ์ฒซ ๋ฒˆ์งธ Convolution Block
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    layers.MaxPooling2D((2, 2)),
    
    # ๋‘ ๋ฒˆ์งธ Convolution Block
    layers.Conv2D(64, (3, 3), activation='relu'),
    layers.MaxPooling2D((2, 2)),
    
    # ์„ธ ๋ฒˆ์งธ Convolution Block
    layers.Conv2D(64, (3, 3), activation='relu'),
    
    # Flatten & Dense Layers
    layers.Flatten(),
    layers.Dense(64, activation='relu'),
    layers.Dropout(0.5),  # ๊ณผ์ ํ•ฉ ๋ฐฉ์ง€
    layers.Dense(36, activation='softmax')  # 0-9, A-Z = 36๊ฐœ ํด๋ž˜์Šค
])

# ๋ชจ๋ธ ์ปดํŒŒ์ผ
model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

# ๋ชจ๋ธ ์š”์•ฝ
model.summary()

๐Ÿ’ก ๋ชจ๋ธ ๊ตฌ์กฐ ์„ค๋ช… โ€ข Conv2D(32, (3,3)): 3x3 ํฌ๊ธฐ์˜ ํ•„ํ„ฐ 32๊ฐœ๋กœ ํŠน์ง• ์ถ”์ถœ
โ€ข MaxPooling2D(2,2): 2x2 ์˜์—ญ์—์„œ ์ตœ๋Œ“๊ฐ’๋งŒ ์„ ํƒ (ํฌ๊ธฐ ์ ˆ๋ฐ˜์œผ๋กœ ์ถ•์†Œ)
โ€ข Dropout(0.5): ํ•™์Šต ์‹œ 50% ๋‰ด๋Ÿฐ์„ ๋žœ๋คํ•˜๊ฒŒ ๋” (๊ณผ์ ํ•ฉ ๋ฐฉ์ง€)
โ€ข Dense(36): 36๊ฐœ ํด๋ž˜์Šค(0-9, A-Z)๋กœ ๋ถ„๋ฅ˜

๐Ÿ“Š ๋ฐ์ดํ„ฐ์…‹ ์ค€๋น„ ๋ฐ ํ•™์Šต

# ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ฐ ์ „์ฒ˜๋ฆฌ
import numpy as np
from sklearn.model_selection import train_test_split

# ์ด๋ฏธ์ง€์™€ ๋ ˆ์ด๋ธ” ๋กœ๋“œ (์˜ˆ์‹œ)
X = np.array([...])  # shape: (N, 28, 28)
y = np.array([...])  # shape: (N,)

# ์ •๊ทœํ™” (0~255 -> 0~1)
X = X.astype('float32') / 255.0

# ์ฐจ์› ์ถ”๊ฐ€ (CNN์€ (height, width, channels) ํ˜•ํƒœ ํ•„์š”)
X = X.reshape(-1, 28, 28, 1)

# ํ•™์Šต/๊ฒ€์ฆ ๋ฐ์ดํ„ฐ ๋ถ„๋ฆฌ
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# ๋ชจ๋ธ ํ•™์Šต
history = model.fit(
    X_train, y_train,
    epochs=20,
    batch_size=32,
    validation_data=(X_val, y_val),
    callbacks=[
        keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True),
        keras.callbacks.ModelCheckpoint('best_model.h5', save_best_only=True)
    ]
)

# ํ•™์Šต ๊ฒฐ๊ณผ ํ™•์ธ
print(f"์ตœ์ข… ๊ฒ€์ฆ ์ •ํ™•๋„: {max(history.history['val_accuracy']):.4f}")

๐ŸŽฏ ์‹ค์ „ ์บก์ฐจ ์ธ์‹ ํŒŒ์ดํ”„๋ผ์ธ

์ด์ œ ๋ชจ๋“  ๊ฑธ ํ•ฉ์ณ์„œ ์™„์ „ํ•œ ์บก์ฐจ ์ธ์‹ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿ› ๏ธ

class CaptchaSolver:
    def __init__(self, model_path):
        self.model = keras.models.load_model(model_path)
        self.char_map = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    
    def preprocess(self, image_path):
        """์ด๋ฏธ์ง€ ์ „์ฒ˜๋ฆฌ"""
        # ์ด๋ฏธ์ง€ ๋กœ๋“œ
        img = cv2.imread(image_path)
        
        # ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋ณ€ํ™˜
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        
        # ์ด์ง„ํ™”
        _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
        
        # ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ
        kernel = np.ones((2,2), np.uint8)
        binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
        
        return binary
    
    def segment_characters(self, binary_img):
        """๋ฌธ์ž ๋ถ„๋ฆฌ"""
        contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
        char_boxes = []
        for contour in contours:
            x, y, w, h = cv2.boundingRect(contour)
            if 10 < w < 50 and 15 < h < 50:  # ํฌ๊ธฐ ํ•„ํ„ฐ๋ง
                char_boxes.append((x, y, w, h))
        
        # x ์ขŒํ‘œ ๊ธฐ์ค€ ์ •๋ ฌ
        char_boxes = sorted(char_boxes, key=lambda b: b[0])
        
        # ๋ฌธ์ž ์ด๋ฏธ์ง€ ์ถ”์ถœ
        characters = []
        for x, y, w, h in char_boxes:
            char_img = binary_img[y:y+h, x:x+w]
            char_img = cv2.resize(char_img, (28, 28))
            char_img = char_img.astype('float32') / 255.0
            char_img = char_img.reshape(1, 28, 28, 1)
            characters.append(char_img)
        
        return characters
    
    def recognize(self, image_path):
        """์บก์ฐจ ์ธ์‹ ๋ฉ”์ธ ํ•จ์ˆ˜"""
        # ์ „์ฒ˜๋ฆฌ
        binary = self.preprocess(image_path)
        
        # ๋ฌธ์ž ๋ถ„๋ฆฌ
        characters = self.segment_characters(binary)
        
        # ๊ฐ ๋ฌธ์ž ์ธ์‹
        result = ""
        for char_img in characters:
            prediction = self.model.predict(char_img, verbose=0)
            char_idx = np.argmax(prediction)
            confidence = prediction[0][char_idx]
            
            if confidence > 0.8:  # ์‹ ๋ขฐ๋„ ์ž„๊ณ„๊ฐ’
                result += self.char_map[char_idx]
            else:
                result += "?"  # ๋ถˆํ™•์‹คํ•œ ๊ฒฝ์šฐ
        
        return result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
solver = CaptchaSolver('best_model.h5')
captcha_text = solver.recognize('captcha_sample.png')
print(f"์ธ์‹๋œ ์บก์ฐจ: {captcha_text}")
์บก์ฐจ ์ธ์‹ ํ”„๋กœ์„ธ์Šค ํ”Œ๋กœ์šฐ 1. ์ด๋ฏธ์ง€ ์ž…๋ ฅ ๐Ÿ“ท CAPTCHA 2. ์ „์ฒ˜๋ฆฌ ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ์ด์ง„ํ™”, ๋…ธ์ด์ฆˆ์ œ๊ฑฐ 3. ๋ฌธ์ž ๋ถ„๋ฆฌ ์œค๊ณฝ์„  ๊ฒ€์ถœ ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜ 4. ๋ฌธ์ž ์ธ์‹ CNN ๋ชจ๋ธ ์˜ˆ์ธก ์ˆ˜ํ–‰ 5. ๊ฒฐ๊ณผ ์ถœ๋ ฅ โœ… "A7K9X2" ํ•ต์‹ฌ ๊ธฐ์ˆ  ์Šคํƒ OpenCV ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ TensorFlow ๋”ฅ๋Ÿฌ๋‹ ํ”„๋ ˆ์ž„์›Œํฌ NumPy ์ˆ˜์น˜ ์—ฐ์‚ฐ Scikit-learn ๋จธ์‹ ๋Ÿฌ๋‹ Pillow ์ด๋ฏธ์ง€ ๋กœ๋“œ/์ €์žฅ Matplotlib ์‹œ๊ฐํ™” Keras ๊ณ ์ˆ˜์ค€ API

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๊ณ ๊ธ‰ ๊ธฐ๋ฒ•

๊ธฐ๋ณธ์ ์ธ ์บก์ฐจ ์ธ์‹ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด, ์ด์ œ ์„ฑ๋Šฅ์„ ๋Œ์–ด์˜ฌ๋ฆด ์ฐจ๋ก€์•ผ! ๐Ÿ’ช

๐ŸŽฏ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• (Data Augmentation)

ํ•™์Šต ๋ฐ์ดํ„ฐ๊ฐ€ ๋ถ€์กฑํ•˜๊ฑฐ๋‚˜ ๋‹ค์–‘์„ฑ์ด ๋–จ์–ด์งˆ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ฒ•์ด์•ผ. ๊ธฐ์กด ์ด๋ฏธ์ง€๋ฅผ ๋ณ€ํ˜•ํ•ด์„œ ์ƒˆ๋กœ์šด ํ•™์Šต ๋ฐ์ดํ„ฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ์ง€.

from tensorflow.keras.preprocessing.image import ImageDataGenerator

# ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• ์„ค์ •
datagen = ImageDataGenerator(
    rotation_range=10,        # ๋žœ๋ค ํšŒ์ „ (-10๋„ ~ +10๋„)
    width_shift_range=0.1,    # ์ˆ˜ํ‰ ์ด๋™
    height_shift_range=0.1,   # ์ˆ˜์ง ์ด๋™
    shear_range=0.1,          # ์ „๋‹จ ๋ณ€ํ™˜
    zoom_range=0.1,           # ํ™•๋Œ€/์ถ•์†Œ
    fill_mode='nearest'       # ๋นˆ ๊ณต๊ฐ„ ์ฑ„์šฐ๊ธฐ
)

# ์ฆ๊ฐ•๋œ ๋ฐ์ดํ„ฐ๋กœ ํ•™์Šต
model.fit(
    datagen.flow(X_train, y_train, batch_size=32),
    epochs=30,
    validation_data=(X_val, y_val)
)

๐Ÿ”ฅ ์•™์ƒ๋ธ” (Ensemble) ๊ธฐ๋ฒ•

์—ฌ๋Ÿฌ ๋ชจ๋ธ์˜ ์˜ˆ์ธก์„ ๊ฒฐํ•ฉํ•ด์„œ ๋” ์ •ํ™•ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์–ป๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. "๋ฐฑ์ง€์žฅ๋„ ๋งž๋“ค๋ฉด ๋‚ซ๋‹ค"๋Š” ์†๋‹ด์ฒ˜๋Ÿผ! ๐Ÿ˜„

# ์—ฌ๋Ÿฌ ๋ชจ๋ธ ํ•™์Šต
model1 = create_cnn_model()  # CNN ๋ชจ๋ธ
model2 = create_resnet_model()  # ResNet ๋ชจ๋ธ
model3 = create_vgg_model()  # VGG ๋ชจ๋ธ

# ๊ฐ ๋ชจ๋ธ๋กœ ์˜ˆ์ธก
pred1 = model1.predict(test_image)
pred2 = model2.predict(test_image)
pred3 = model3.predict(test_image)

# ํˆฌํ‘œ(Voting) ๋ฐฉ์‹์œผ๋กœ ์ตœ์ข… ์˜ˆ์ธก
final_pred = np.argmax(pred1 + pred2 + pred3, axis=1)

# ๋˜๋Š” ๊ฐ€์ค‘ ํ‰๊ท 
weights = [0.4, 0.35, 0.25]  # ๊ฐ ๋ชจ๋ธ์˜ ์‹ ๋ขฐ๋„์— ๋”ฐ๋ผ ๊ฐ€์ค‘์น˜ ๋ถ€์—ฌ
final_pred = np.argmax(weights[0]*pred1 + weights[1]*pred2 + weights[2]*pred3, axis=1)

๐Ÿงช Transfer Learning (์ „์ด ํ•™์Šต)

์ด๋ฏธ ํ•™์Šต๋œ ๋ชจ๋ธ์„ ๊ฐ€์ ธ์™€์„œ ์šฐ๋ฆฌ ๋ฌธ์ œ์— ๋งž๊ฒŒ ๋ฏธ์„ธ ์กฐ์ •ํ•˜๋Š” ๊ฑฐ์•ผ. ์ฒ˜์Œ๋ถ€ํ„ฐ ํ•™์Šตํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅด๊ณ  ํšจ๊ณผ์ ์ด์ง€!

from tensorflow.keras.applications import MobileNetV2

# ์‚ฌ์ „ ํ•™์Šต๋œ ๋ชจ๋ธ ๋กœ๋“œ (ImageNet ๊ฐ€์ค‘์น˜)
base_model = MobileNetV2(
    input_shape=(28, 28, 3),
    include_top=False,  # ์ตœ์ƒ์œ„ ๋ถ„๋ฅ˜ ๋ ˆ์ด์–ด ์ œ์™ธ
    weights='imagenet'
)

# ๋ฒ ์ด์Šค ๋ชจ๋ธ ๋™๊ฒฐ (์ฒ˜์Œ์—๋Š” ํ•™์Šตํ•˜์ง€ ์•Š์Œ)
base_model.trainable = False

# ์ƒˆ๋กœ์šด ๋ถ„๋ฅ˜ ๋ ˆ์ด์–ด ์ถ”๊ฐ€
model = keras.Sequential([
    base_model,
    layers.GlobalAveragePooling2D(),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.5),
    layers.Dense(36, activation='softmax')
])

# 1๋‹จ๊ณ„: ์ƒˆ๋กœ์šด ๋ ˆ์ด์–ด๋งŒ ํ•™์Šต
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=10)

# 2๋‹จ๊ณ„: ์ „์ฒด ๋ชจ๋ธ ๋ฏธ์„ธ ์กฐ์ •
base_model.trainable = True
model.compile(optimizer=keras.optimizers.Adam(1e-5), loss='sparse_categorical_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=10)

โš™๏ธ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹

๋ชจ๋ธ ์„ฑ๋Šฅ์„ ์ตœ๋Œ€๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๋ ค๋ฉด ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ž˜ ์กฐ์ •ํ•ด์•ผ ํ•ด. ์ด๊ฑด ์ข€ ๋…ธ๊ฐ€๋‹ค ์ž‘์—…์ด๊ธด ํ•œ๋ฐ, ์ž๋™ํ™” ๋„๊ตฌ๋ฅผ ์“ฐ๋ฉด ํŽธํ•ด! ๐Ÿ› ๏ธ

from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
from sklearn.model_selection import GridSearchCV

def create_model(learning_rate=0.001, dropout_rate=0.5):
    model = keras.Sequential([
        layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(64, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        layers.Flatten(),
        layers.Dense(64, activation='relu'),
        layers.Dropout(dropout_rate),
        layers.Dense(36, activation='softmax')
    ])
    
    model.compile(
        optimizer=keras.optimizers.Adam(learning_rate),
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy']
    )
    return model

# ๊ทธ๋ฆฌ๋“œ ์„œ์น˜๋กœ ์ตœ์  ํŒŒ๋ผ๋ฏธํ„ฐ ์ฐพ๊ธฐ
model = KerasClassifier(build_fn=create_model, epochs=10, batch_size=32, verbose=0)

param_grid = {
    'learning_rate': [0.001, 0.0001, 0.00001],
    'dropout_rate': [0.3, 0.5, 0.7],
    'batch_size': [16, 32, 64]
}

grid = GridSearchCV(estimator=model, param_grid=param_grid, cv=3)
grid_result = grid.fit(X_train, y_train)

print(f"์ตœ์  ํŒŒ๋ผ๋ฏธํ„ฐ: {grid_result.best_params_}")
print(f"์ตœ๊ณ  ์ •ํ™•๋„: {grid_result.best_score_:.4f}")

๐Ÿ›ก๏ธ ์‹ค์ „ ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ๋””๋ฒ„๊น…

์‹ค์ œ๋กœ ์บก์ฐจ ์ธ์‹ ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฌธ์ œ์— ๋ถ€๋”ชํžˆ๊ฒŒ ๋ผ. ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ”ง

โŒ ๋ฌธ์ œ 1: ๋ฌธ์ž ๋ถ„๋ฆฌ๊ฐ€ ์ œ๋Œ€๋กœ ์•ˆ ๋ผ์š”!

๊ธ€์ž๋“ค์ด ์„œ๋กœ ๋ถ™์–ด์žˆ๊ฑฐ๋‚˜ ๊ฒน์ณ์žˆ์œผ๋ฉด ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜์ด ์–ด๋ ค์›Œ. ์ด๋Ÿด ๋•Œ๋Š”:

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: 1. ์นจ์‹(Erosion) ์—ฐ์‚ฐ ์ ์šฉ: ๊ธ€์ž๋ฅผ ์–‡๊ฒŒ ๋งŒ๋“ค์–ด์„œ ๋ถ„๋ฆฌ
2. ์ˆ˜์ง ํˆฌ์˜ ๋ถ„์„: ํ”ฝ์…€ ๋ฐ€๋„๊ฐ€ ๋‚ฎ์€ ์ง€์ ์„ ๊ฒฝ๊ณ„๋กœ ํŒ๋‹จ
3. ๋”ฅ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์„ธ๊ทธ๋ฉ˜ํ…Œ์ด์…˜: YOLO๋‚˜ Mask R-CNN ๊ฐ™์€ ๊ฐ์ฒด ๊ฒ€์ถœ ๋ชจ๋ธ ์‚ฌ์šฉ
4. ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ: ๊ณ ์ • ํฌ๊ธฐ ์œˆ๋„์šฐ๋ฅผ ์ด๋™์‹œํ‚ค๋ฉฐ ๊ธ€์ž ์ฐพ๊ธฐ

# ์นจ์‹ ์—ฐ์‚ฐ์œผ๋กœ ๊ธ€์ž ๋ถ„๋ฆฌ ๊ฐœ์„ 
kernel = np.ones((2,2), np.uint8)
eroded = cv2.erode(binary, kernel, iterations=1)

# ์ดํ›„ ์œค๊ณฝ์„  ๊ฒ€์ถœ
contours, _ = cv2.findContours(eroded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

โŒ ๋ฌธ์ œ 2: ์ธ์‹๋ฅ ์ด ๋„ˆ๋ฌด ๋‚ฎ์•„์š”! (50% ์ดํ•˜)

์ด๊ฑด ์—ฌ๋Ÿฌ ์›์ธ์ด ์žˆ์„ ์ˆ˜ ์žˆ์–ด:

์›์ธ ์ฆ์ƒ ํ•ด๊ฒฐ์ฑ…
ํ•™์Šต ๋ฐ์ดํ„ฐ ๋ถ€์กฑ ๊ณผ์ ํ•ฉ, ์ผ๋ฐ˜ํ™” ์‹คํŒจ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•, ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
์ „์ฒ˜๋ฆฌ ๋ถˆ๋Ÿ‰ ๋…ธ์ด์ฆˆ๊ฐ€ ๋งŽ์ด ๋‚จ์Œ ์ „์ฒ˜๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ •, ๋‹ค์–‘ํ•œ ํ•„ํ„ฐ ์‹œ๋„
๋ชจ๋ธ ๋ณต์žก๋„ ๋ถ€์กฑ ํ•™์Šต ์ •ํ™•๋„๋„ ๋‚ฎ์Œ ๋” ๊นŠ์€ ๋„คํŠธ์›Œํฌ, ๋” ๋งŽ์€ ํ•„ํ„ฐ
ํด๋ž˜์Šค ๋ถˆ๊ท ํ˜• ํŠน์ • ๊ธ€์ž๋งŒ ์ž˜ ๋ชป ๋งž์ถค ํด๋ž˜์Šค ๊ฐ€์ค‘์น˜ ์กฐ์ •, ์˜ค๋ฒ„์ƒ˜ํ”Œ๋ง

# ํด๋ž˜์Šค ๋ถˆ๊ท ํ˜• ํ•ด๊ฒฐ ์˜ˆ์‹œ
from sklearn.utils.class_weight import compute_class_weight

# ํด๋ž˜์Šค ๊ฐ€์ค‘์น˜ ๊ณ„์‚ฐ
class_weights = compute_class_weight(
    'balanced',
    classes=np.unique(y_train),
    y=y_train
)
class_weight_dict = dict(enumerate(class_weights))

# ํ•™์Šต ์‹œ ์ ์šฉ
model.fit(
    X_train, y_train,
    epochs=20,
    class_weight=class_weight_dict  # ๋ถˆ๊ท ํ˜• ๋ณด์ •
)

โŒ ๋ฌธ์ œ 3: ํ•™์Šต์€ ์ž˜ ๋˜๋Š”๋ฐ ์‹ค์ „์—์„œ ์•ˆ ๋ผ์š”!

์ด๊ฑด ์ „ํ˜•์ ์ธ ๊ณผ์ ํ•ฉ(Overfitting) ๋ฌธ์ œ์•ผ. ํ•™์Šต ๋ฐ์ดํ„ฐ์—๋งŒ ๋„ˆ๋ฌด ์ตœ์ ํ™”๋˜์–ด์„œ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ชป ๋งž์ถ”๋Š” ๊ฑฐ์ง€. ๐Ÿ˜ฐ

โš ๏ธ ๊ณผ์ ํ•ฉ ์ง•ํ›„ โ€ข ํ•™์Šต ์ •ํ™•๋„๋Š” 95%์ธ๋ฐ ๊ฒ€์ฆ ์ •ํ™•๋„๋Š” 60%
โ€ข ํ•™์Šต ์†์‹ค์€ ๊ณ„์† ๊ฐ์†Œํ•˜๋Š”๋ฐ ๊ฒ€์ฆ ์†์‹ค์€ ์ฆ๊ฐ€
โ€ข ํ•™์Šต ๋ฐ์ดํ„ฐ์™€ ์กฐ๊ธˆ๋งŒ ๋‹ค๋ฅธ ์ด๋ฏธ์ง€๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ์™„์ „ํžˆ ํ‹€๋ฆผ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

# 1. Dropout ๋น„์œจ ์ฆ๊ฐ€
layers.Dropout(0.7)  # 0.5์—์„œ 0.7๋กœ ์ฆ๊ฐ€

# 2. L2 ์ •๊ทœํ™” ์ถ”๊ฐ€
from tensorflow.keras import regularizers

layers.Dense(64, activation='relu', 
             kernel_regularizer=regularizers.l2(0.01))

# 3. Early Stopping ์ ์šฉ
early_stop = keras.callbacks.EarlyStopping(
    monitor='val_loss',
    patience=5,  # 5 ์—ํฌํฌ ๋™์•ˆ ๊ฐœ์„  ์—†์œผ๋ฉด ์ค‘๋‹จ
    restore_best_weights=True
)

model.fit(X_train, y_train, epochs=100, callbacks=[early_stop])

# 4. ๋ฐฐ์น˜ ์ •๊ทœํ™” ์ถ”๊ฐ€
model = keras.Sequential([
    layers.Conv2D(32, (3, 3), activation='relu'),
    layers.BatchNormalization(),  # ๋ฐฐ์น˜ ์ •๊ทœํ™”
    layers.MaxPooling2D((2, 2)),
    # ...
])

๐Ÿ› ๋””๋ฒ„๊น… ํŒ

๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๋ง‰ํž ๋•Œ ์œ ์šฉํ•œ ํŒ๋“ค์ด์•ผ! ๐Ÿ”

# 1. ์ค‘๊ฐ„ ๊ฒฐ๊ณผ ์‹œ๊ฐํ™”
import matplotlib.pyplot as plt

def visualize_preprocessing(image_path):
    # ์›๋ณธ
    original = cv2.imread(image_path)
    
    # ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ
    gray = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY)
    
    # ์ด์ง„ํ™”
    _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    
    # ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ
    kernel = np.ones((2,2), np.uint8)
    cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
    
    # ์‹œ๊ฐํ™”
    fig, axes = plt.subplots(1, 4, figsize=(15, 4))
    axes[0].imshow(cv2.cvtColor(original, cv2.COLOR_BGR2RGB))
    axes[0].set_title('Original')
    axes[1].imshow(gray, cmap='gray')
    axes[1].set_title('Grayscale')
    axes[2].imshow(binary, cmap='gray')
    axes[2].set_title('Binary')
    axes[3].imshow(cleaned, cmap='gray')
    axes[3].set_title('Cleaned')
    plt.show()

# 2. ์˜ˆ์ธก ์‹ ๋ขฐ๋„ ํ™•์ธ
predictions = model.predict(test_images)
for i, pred in enumerate(predictions):
    top3_idx = np.argsort(pred)[-3:][::-1]  # ์ƒ์œ„ 3๊ฐœ
    print(f"์ด๋ฏธ์ง€ {i}:")
    for idx in top3_idx:
        print(f"  {char_map[idx]}: {pred[idx]*100:.2f}%")

# 3. ํ˜ผ๋™ ํ–‰๋ ฌ(Confusion Matrix) ๋ถ„์„
from sklearn.metrics import confusion_matrix, classification_report
import seaborn as sns

y_pred = model.predict(X_test)
y_pred_classes = np.argmax(y_pred, axis=1)

cm = confusion_matrix(y_test, y_pred_classes)
plt.figure(figsize=(12, 10))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')
plt.show()

# ์–ด๋–ค ๊ธ€์ž๋ฅผ ์ž์ฃผ ํ‹€๋ฆฌ๋Š”์ง€ ํ™•์ธ
print(classification_report(y_test, y_pred_classes, target_names=list(char_map)))

๐Ÿš€ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์™„์ „ํ•œ ์บก์ฐจ ์†”๋ฒ„ ๋งŒ๋“ค๊ธฐ

์ด์ œ ๋ฐฐ์šด ๊ฑธ ๋‹ค ํ•ฉ์ณ์„œ ์‹ค์ „์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ์บก์ฐจ ์†”๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๐ŸŽ‰

๐Ÿ“ฆ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

captcha_solver/
โ”‚
โ”œโ”€โ”€ data/
โ”‚   โ”œโ”€โ”€ train/          # ํ•™์Šต ๋ฐ์ดํ„ฐ
โ”‚   โ”œโ”€โ”€ test/           # ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ
โ”‚   โ””โ”€โ”€ samples/        # ์ƒ˜ํ”Œ ์บก์ฐจ ์ด๋ฏธ์ง€
โ”‚
โ”œโ”€โ”€ models/
โ”‚   โ”œโ”€โ”€ cnn_model.h5    # ํ•™์Šต๋œ ๋ชจ๋ธ
โ”‚   โ””โ”€โ”€ model_config.json
โ”‚
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ preprocessor.py    # ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ
โ”‚   โ”œโ”€โ”€ segmenter.py       # ๋ฌธ์ž ๋ถ„๋ฆฌ ๋ชจ๋“ˆ
โ”‚   โ”œโ”€โ”€ recognizer.py      # ์ธ์‹ ๋ชจ๋“ˆ
โ”‚   โ””โ”€โ”€ trainer.py         # ํ•™์Šต ๋ชจ๋“ˆ
โ”‚
โ”œโ”€โ”€ utils/
โ”‚   โ”œโ”€โ”€ data_generator.py  # ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•
โ”‚   โ””โ”€โ”€ visualizer.py      # ์‹œ๊ฐํ™” ๋„๊ตฌ
โ”‚
โ”œโ”€โ”€ main.py             # ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ
โ”œโ”€โ”€ requirements.txt    # ์˜์กด์„ฑ ํŒจํ‚ค์ง€
โ””โ”€โ”€ README.md

๐Ÿ”ง ํ•ต์‹ฌ ๋ชจ๋“ˆ ๊ตฌํ˜„

1. preprocessor.py - ์ „์ฒ˜๋ฆฌ ๋ชจ๋“ˆ

import cv2
import numpy as np

class ImagePreprocessor:
    def __init__(self, target_size=(28, 28)):
        self.target_size = target_size
    
    def preprocess(self, image):
        """์ „์ฒด ์ „์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ"""
        # ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋ณ€ํ™˜
        if len(image.shape) == 3:
            gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        else:
            gray = image
        
        # ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ (๊ฐ€์šฐ์‹œ์•ˆ ๋ธ”๋Ÿฌ)
        blurred = cv2.GaussianBlur(gray, (3, 3), 0)
        
        # ์ด์ง„ํ™” (Otsu)
        _, binary = cv2.threshold(blurred, 0, 255, 
                                   cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
        
        # ๋ชจํด๋กœ์ง€ ์—ฐ์‚ฐ (๋…ธ์ด์ฆˆ ์ œ๊ฑฐ)
        kernel = np.ones((2, 2), np.uint8)
        cleaned = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
        cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, kernel)
        
        return cleaned
    
    def normalize(self, image):
        """์ •๊ทœํ™” (0~1 ๋ฒ”์œ„)"""
        return image.astype('float32') / 255.0
    
    def resize(self, image):
        """ํฌ๊ธฐ ์กฐ์ •"""
        return cv2.resize(image, self.target_size)

2. segmenter.py - ๋ฌธ์ž ๋ถ„๋ฆฌ ๋ชจ๋“ˆ

import cv2
import numpy as np

class CharacterSegmenter:
    def __init__(self, min_width=10, max_width=50, min_height=15, max_height=50):
        self.min_width = min_width
        self.max_width = max_width
        self.min_height = min_height
        self.max_height = max_height
    
    def segment(self, binary_image):
        """๋ฌธ์ž ๋ถ„๋ฆฌ ์ˆ˜ํ–‰"""
        # ์œค๊ณฝ์„  ๊ฒ€์ถœ
        contours, _ = cv2.findContours(binary_image, 
                                        cv2.RETR_EXTERNAL, 
                                        cv2.CHAIN_APPROX_SIMPLE)
        
        # ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ถ”์ถœ ๋ฐ ํ•„ํ„ฐ๋ง
        char_boxes = []
        for contour in contours:
            x, y, w, h = cv2.boundingRect(contour)
            
            # ํฌ๊ธฐ ํ•„ํ„ฐ๋ง
            if (self.min_width < w < self.max_width and 
                self.min_height < h < self.max_height):
                char_boxes.append((x, y, w, h))
        
        # x ์ขŒํ‘œ ๊ธฐ์ค€ ์ •๋ ฌ (์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ)
        char_boxes = sorted(char_boxes, key=lambda b: b[0])
        
        # ๋ฌธ์ž ์ด๋ฏธ์ง€ ์ถ”์ถœ
        characters = []
        for x, y, w, h in char_boxes:
            char_img = binary_image[y:y+h, x:x+w]
            characters.append(char_img)
        
        return characters, char_boxes
    
    def merge_overlapping(self, char_boxes, overlap_threshold=0.5):
        """๊ฒน์น˜๋Š” ๋ฐ•์Šค ๋ณ‘ํ•ฉ"""
        merged = []
        used = set()
        
        for i, box1 in enumerate(char_boxes):
            if i in used:
                continue
            
            x1, y1, w1, h1 = box1
            merged_box = list(box1)
            
            for j, box2 in enumerate(char_boxes[i+1:], start=i+1):
                if j in used:
                    continue
                
                x2, y2, w2, h2 = box2
                
                # ๊ฒน์นจ ๋น„์œจ ๊ณ„์‚ฐ
                overlap_x = max(0, min(x1+w1, x2+w2) - max(x1, x2))
                overlap_area = overlap_x * min(h1, h2)
                box1_area = w1 * h1
                
                if overlap_area / box1_area > overlap_threshold:
                    # ๋ณ‘ํ•ฉ
                    new_x = min(x1, x2)
                    new_y = min(y1, y2)
                    new_w = max(x1+w1, x2+w2) - new_x
                    new_h = max(y1+h1, y2+h2) - new_y
                    merged_box = [new_x, new_y, new_w, new_h]
                    used.add(j)
            
            merged.append(tuple(merged_box))
            used.add(i)
        
        return merged

3. recognizer.py - ์ธ์‹ ๋ชจ๋“ˆ

import numpy as np
from tensorflow import keras

class CaptchaRecognizer:
    def __init__(self, model_path, char_map='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'):
        self.model = keras.models.load_model(model_path)
        self.char_map = char_map
        self.preprocessor = ImagePreprocessor()
    
    def recognize_character(self, char_image, confidence_threshold=0.7):
        """๋‹จ์ผ ๋ฌธ์ž ์ธ์‹"""
        # ์ „์ฒ˜๋ฆฌ
        processed = self.preprocessor.resize(char_image)
        processed = self.preprocessor.normalize(processed)
        processed = processed.reshape(1, 28, 28, 1)
        
        # ์˜ˆ์ธก
        prediction = self.model.predict(processed, verbose=0)[0]
        
        # ๊ฐ€์žฅ ๋†’์€ ํ™•๋ฅ ์˜ ํด๋ž˜์Šค
        char_idx = np.argmax(prediction)
        confidence = prediction[char_idx]
        
        if confidence >= confidence_threshold:
            return self.char_map[char_idx], confidence
        else:
            return '?', confidence
    
    def recognize_captcha(self, captcha_image, return_confidence=False):
        """์ „์ฒด ์บก์ฐจ ์ธ์‹"""
        # ์ „์ฒ˜๋ฆฌ
        preprocessor = ImagePreprocessor()
        binary = preprocessor.preprocess(captcha_image)
        
        # ๋ฌธ์ž ๋ถ„๋ฆฌ
        segmenter = CharacterSegmenter()
        characters, boxes = segmenter.segment(binary)
        
        # ๊ฐ ๋ฌธ์ž ์ธ์‹
        result = ""
        confidences = []
        
        for char_img in characters:
            char, conf = self.recognize_character(char_img)
            result += char
            confidences.append(conf)
        
        if return_confidence:
            avg_confidence = np.mean(confidences) if confidences else 0
            return result, avg_confidence
        else:
            return result
    
    def batch_recognize(self, image_paths):
        """์—ฌ๋Ÿฌ ์บก์ฐจ ์ผ๊ด„ ์ธ์‹"""
        results = []
        
        for path in image_paths:
            image = cv2.imread(path)
            text, conf = self.recognize_captcha(image, return_confidence=True)
            results.append({
                'path': path,
                'text': text,
                'confidence': conf
            })
        
        return results

4. main.py - ๋ฉ”์ธ ์‹คํ–‰ ํŒŒ์ผ

import argparse
import cv2
from src.recognizer import CaptchaRecognizer

def main():
    parser = argparse.ArgumentParser(description='CAPTCHA Solver')
    parser.add_argument('--image', type=str, required=True, help='์บก์ฐจ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ')
    parser.add_argument('--model', type=str, default='models/cnn_model.h5', help='๋ชจ๋ธ ๊ฒฝ๋กœ')
    parser.add_argument('--visualize', action='store_true', help='๊ฒฐ๊ณผ ์‹œ๊ฐํ™”')
    
    args = parser.parse_args()
    
    # ์ธ์‹๊ธฐ ์ดˆ๊ธฐํ™”
    recognizer = CaptchaRecognizer(args.model)
    
    # ์ด๋ฏธ์ง€ ๋กœ๋“œ
    image = cv2.imread(args.image)
    
    if image is None:
        print(f"์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {args.image}")
        return
    
    # ์บก์ฐจ ์ธ์‹
    result, confidence = recognizer.recognize_captcha(image, return_confidence=True)
    
    print(f"์ธ์‹ ๊ฒฐ๊ณผ: {result}")
    print(f"์‹ ๋ขฐ๋„: {confidence*100:.2f}%")
    
    # ์‹œ๊ฐํ™”
    if args.visualize:
        import matplotlib.pyplot as plt
        
        plt.figure(figsize=(10, 4))
        plt.subplot(1, 2, 1)
        plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
        plt.title('Original Image')
        plt.axis('off')
        
        plt.subplot(1, 2, 2)
        plt.text(0.5, 0.5, result, fontsize=40, ha='center', va='center')
        plt.title(f'Result (Confidence: {confidence*100:.1f}%)')
        plt.axis('off')
        
        plt.tight_layout()
        plt.show()

if __name__ == '__main__':
    main()

๐ŸŽฎ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•

# 1. ์˜์กด์„ฑ ์„ค์น˜
pip install -r requirements.txt

# 2. ๋‹จ์ผ ์ด๋ฏธ์ง€ ์ธ์‹
python main.py --image samples/captcha1.png --model models/cnn_model.h5

# 3. ์‹œ๊ฐํ™” ํฌํ•จ
python main.py --image samples/captcha1.png --visualize

# 4. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
python batch_process.py --input_dir samples/ --output results.csv

๐Ÿ’ผ ์žฌ๋Šฅ๋„ท์—์„œ ํ”„๋กœ์ ํŠธ ๋„์›€ ๋ฐ›๊ธฐ ์ด๋Ÿฐ ํ”„๋กœ์ ํŠธ๋ฅผ ํ˜ผ์ž ์ง„ํ–‰ํ•˜๋‹ค๊ฐ€ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด? ์žฌ๋Šฅ๋„ท์—์„œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ „๋ฌธ๊ฐ€๋“ค์—๊ฒŒ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ถ€ํ„ฐ ๋ฒ„๊ทธ ์ˆ˜์ •, ์„ฑ๋Šฅ ์ตœ์ ํ™”๊นŒ์ง€ ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด 1:1๋กœ ๋„์™€์ค€๋‹ค๊ณ . ํŠนํžˆ ๋”ฅ๋Ÿฌ๋‹์ด๋‚˜ ์ปดํ“จํ„ฐ ๋น„์ „ ๋ถ„์•ผ๋Š” ์ „๋ฌธ๊ฐ€์˜ ์กฐ์–ธ์ด ์ •๋ง ํฐ ๋„์›€์ด ๋˜๊ฑฐ๋“ ! ๐ŸŽฏ

โš–๏ธ ๋ฒ•์ ยท์œค๋ฆฌ์  ๊ณ ๋ ค์‚ฌํ•ญ

์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ์œผ๋ฉด ์บก์ฐจ ์ธ์‹ ํ”„๋กœ๊ทธ๋žจ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ํ•˜์ง€๋งŒ ์ •๋ง ์ค‘์š”ํ•œ ์–˜๊ธฐ๋ฅผ ํ•ด์•ผ ํ•ด. ๐Ÿ˜

โš ๏ธ ๊ฒฝ๊ณ : ์บก์ฐจ ์šฐํšŒ๋Š” ๋ถˆ๋ฒ•์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค! ์บก์ฐจ๋Š” ์›น์‚ฌ์ดํŠธ๋ฅผ ๋ณดํ˜ธํ•˜๊ธฐ ์œ„ํ•œ ๋ณด์•ˆ ์žฅ์น˜์•ผ. ์ด๊ฑธ ๋ฌด๋‹จ์œผ๋กœ ์šฐํšŒํ•˜๋Š” ๊ฑด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฒ•์  ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์–ด:

โ€ข ์ปดํ“จํ„ฐ ์‚ฌ์šฉ ์‚ฌ๊ธฐ ๋ฐ ๋‚จ์šฉ๋ฒ• ์œ„๋ฐ˜ (๋ฏธ๊ตญ CFAA)
โ€ข ์ •๋ณดํ†ต์‹ ๋ง๋ฒ• ์œ„๋ฐ˜ (ํ•œ๊ตญ)
โ€ข ์„œ๋น„์Šค ์ด์šฉ์•ฝ๊ด€ ์œ„๋ฐ˜
โ€ข ์ €์ž‘๊ถŒ๋ฒ• ์œ„๋ฐ˜ (์ผ๋ถ€ ์บก์ฐจ๋Š” ์ €์ž‘๊ถŒ ๋ณดํ˜ธ ๋Œ€์ƒ)

โœ… ํ•ฉ๋ฒ•์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€

๊ทธ๋Ÿผ ์–ธ์ œ ์บก์ฐจ ์ธ์‹ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•ด๋„ ๋˜๋Š” ๊ฑธ๊นŒ?

1. ๊ต์œก ๋ฐ ์—ฐ๊ตฌ ๋ชฉ์ 
โ€ข ๋Œ€ํ•™ ํ”„๋กœ์ ํŠธ๋‚˜ ๋…ผ๋ฌธ ์—ฐ๊ตฌ
โ€ข ๋ณด์•ˆ ์ทจ์•ฝ์  ๋ถ„์„ (ํ™”์ดํŠธํ–‡ ํ•ดํ‚น)
โ€ข ๋จธ์‹ ๋Ÿฌ๋‹ ํ•™์Šต ๋ชฉ์ 

2. ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ
โ€ข ์‹œ๊ฐ ์žฅ์• ์ธ์„ ์œ„ํ•œ ๋ณด์กฐ ๋„๊ตฌ ๊ฐœ๋ฐœ
โ€ข ์ž๋™ํ™”๋œ ์ ‘๊ทผ์„ฑ ํ…Œ์ŠคํŠธ

3. ์ž์ฒด ์„œ๋น„์Šค ๊ฐœ์„ 
โ€ข ์ž์‹ ์ด ์šด์˜ํ•˜๋Š” ์›น์‚ฌ์ดํŠธ์˜ ์บก์ฐจ ๊ฐ•๋„ ํ…Œ์ŠคํŠธ
โ€ข ๋” ๋‚˜์€ ์บก์ฐจ ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ

4. ๋ช…์‹œ์  ํ—ˆ๊ฐ€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
โ€ข API๋ฅผ ํ†ตํ•œ ๊ณต์‹ ์ง€์›
โ€ข ์›น์‚ฌ์ดํŠธ ์šด์˜์ž์˜ ์„œ๋ฉด ๋™์˜

๐Ÿค ์œค๋ฆฌ์  ๊ฐ€์ด๋“œ๋ผ์ธ

๊ธฐ์ˆ ์„ ๋ฐฐ์šฐ๋Š” ๊ฑด ์ข‹์ง€๋งŒ, ์ฑ…์ž„๊ฐ ์žˆ๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

DO โœ… โ€ข ๊ต์œก ๋ชฉ์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ
โ€ข ์ž์‹ ์˜ ์„œ๋น„์Šค์—์„œ๋งŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ
โ€ข ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ๋ฐœ๊ฒฌํ•˜๋ฉด ์ฑ…์ž„๊ฐ ์žˆ๊ฒŒ ์‹ ๊ณ ํ•˜๊ธฐ
โ€ข ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ ์‹œ ๋ช…ํ™•ํ•œ ๋ผ์ด์„ ์Šค ๋ช…์‹œ

DON'T โŒ
โ€ข ํ‹ฐ์ผ“ํŒ… ๋งคํฌ๋กœ ์ œ์ž‘
โ€ข ์ŠคํŒธ ๋ด‡ ๊ฐœ๋ฐœ
โ€ข ๋ฌด๋‹จ ํฌ๋กค๋ง/์Šคํฌ๋ž˜ํ•‘
โ€ข DDoS ๊ณต๊ฒฉ ๋„๊ตฌ๋กœ ํ™œ์šฉ
โ€ข ํƒ€์ธ์˜ ๊ณ„์ • ๋ฌด๋‹จ ์ ‘๊ทผ

๐Ÿ”’ ๋Œ€์•ˆ์  ์ ‘๊ทผ ๋ฐฉ๋ฒ•

์บก์ฐจ๋ฅผ ์šฐํšŒํ•˜๋Š” ๋Œ€์‹  ์ด๋Ÿฐ ๋ฐฉ๋ฒ•๋“ค์„ ๊ณ ๋ คํ•ด๋ด:

1. ๊ณต์‹ API ์‚ฌ์šฉ
๋งŽ์€ ์„œ๋น„์Šค๋“ค์ด ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ API๋ฅผ ์ œ๊ณตํ•ด. ์ด๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด ํ•ฉ๋ฒ•์ ์ด๊ณ  ์•ˆ์ •์ ์œผ๋กœ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์–ด.

2. ์›น์‚ฌ์ดํŠธ ์šด์˜์ž์™€ ํ˜‘์˜
์ •๋‹นํ•œ ๋ชฉ์ ์ด ์žˆ๋‹ค๋ฉด ์ง์ ‘ ์—ฐ๋ฝํ•ด์„œ ํ—ˆ๊ฐ€๋ฅผ ๋ฐ›๋Š” ๊ฒŒ ์ตœ์„ ์ด์•ผ.

3. ์บก์ฐจ ์†”๋ฃจ์…˜ ์„œ๋น„์Šค ์ด์šฉ
2Captcha, Anti-Captcha ๊ฐ™์€ ํ•ฉ๋ฒ•์ ์ธ ์„œ๋น„์Šค๋“ค์ด ์žˆ์–ด. (๋ฌผ๋ก  ์ด๊ฒƒ๋„ ์šฉ๋„์— ๋”ฐ๋ผ ๋ฌธ์ œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜!)

4. ์ ‘๊ทผ์„ฑ ๊ธฐ๋Šฅ ํ™œ์šฉ
๋งŽ์€ ์บก์ฐจ ์‹œ์Šคํ…œ์ด ์˜ค๋””์˜ค ์บก์ฐจ๋‚˜ ๋Œ€์ฒด ์ธ์ฆ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ด. ์ด๋Ÿฐ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๋ฐฉ๋ฒ•์ด์•ผ.

๐Ÿ”ฎ ๋ฏธ๋ž˜ ์ „๋ง ๋ฐ ์ตœ์‹  ํŠธ๋ Œ๋“œ

์บก์ฐจ์™€ ์บก์ฐจ ์ธ์‹ ๊ธฐ์ˆ ์€ ๊ณ„์† ์ง„ํ™”ํ•˜๊ณ  ์žˆ์–ด. ์•ž์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋ฐœ์ „ํ• ์ง€ ์‚ดํŽด๋ณด์ž! ๐Ÿš€

๐Ÿค– AI vs AI์˜ ์ „์Ÿ

์žฌ๋ฏธ์žˆ๋Š” ๊ฑด, ์บก์ฐจ๋ฅผ ๋งŒ๋“œ๋Š” ์ชฝ๋„ AI๋ฅผ ์“ฐ๊ณ , ์บก์ฐจ๋ฅผ ํ‘ธ๋Š” ์ชฝ๋„ AI๋ฅผ ์“ด๋‹ค๋Š” ๊ฑฐ์•ผ. ์ผ์ข…์˜ ๊ตฐ๋น„ ๊ฒฝ์Ÿ์ด์ง€! ๐Ÿ˜„

โ€ข GAN (Generative Adversarial Network): ๋” ์–ด๋ ค์šด ์บก์ฐจ ์ƒ์„ฑ
โ€ข Transformer ๋ชจ๋ธ: ๋ณต์žกํ•œ ํŒจํ„ด๋„ ์ธ์‹ ๊ฐ€๋Šฅ
โ€ข Few-shot Learning: ์ ์€ ๋ฐ์ดํ„ฐ๋กœ๋„ ํ•™์Šต ๊ฐ€๋Šฅ
โ€ข Meta-Learning: ์ƒˆ๋กœ์šด ์บก์ฐจ ์Šคํƒ€์ผ์— ๋น ๋ฅด๊ฒŒ ์ ์‘

๐Ÿ†• ์ฐจ์„ธ๋Œ€ ์บก์ฐจ ๊ธฐ์ˆ 

1. ํ–‰๋™ ๊ธฐ๋ฐ˜ ์ธ์ฆ (Behavioral Biometrics)
๋งˆ์šฐ์Šค ์›€์ง์ž„, ํƒ€์ดํ•‘ ํŒจํ„ด, ํ„ฐ์น˜ ์ œ์Šค์ฒ˜ ๋“ฑ์„ ๋ถ„์„ํ•ด์„œ ์‚ฌ๋žŒ์ธ์ง€ ํŒ๋‹จํ•ด. ๊ตฌ๊ธ€์˜ reCAPTCHA v3๊ฐ€ ์ด ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์ง€.

2. ์ƒ์ฒด ์ธ์ฆ ํ†ตํ•ฉ
์ง€๋ฌธ, ์–ผ๊ตด ์ธ์‹, ํ™์ฑ„ ์Šค์บ” ๋“ฑ์„ ์บก์ฐจ์™€ ๊ฒฐํ•ฉํ•˜๋Š” ์ถ”์„ธ์•ผ.

3. ๊ฒŒ์ž„ํ™”๋œ ์บก์ฐจ (Gamified CAPTCHA)
ํผ์ฆ ๋งž์ถ”๊ธฐ, ๋ฏธ๋‹ˆ ๊ฒŒ์ž„ ๊ฐ™์€ ์žฌ๋ฏธ์žˆ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ง„ํ™” ์ค‘์ด์•ผ.

4. ๋ธ”๋ก์ฒด์ธ ๊ธฐ๋ฐ˜ ์ธ์ฆ
๋ถ„์‚ฐ ์›์žฅ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•œ ์ƒˆ๋กœ์šด ์ธ์ฆ ๋ฐฉ์‹๋„ ์—ฐ๊ตฌ๋˜๊ณ  ์žˆ์–ด.

๐Ÿ“Š ์‚ฐ์—… ๋™ํ–ฅ

๊ธฐ์ˆ  ํ˜„์žฌ ์ƒํƒœ ๋ฏธ๋ž˜ ์ „๋ง
ํ…์ŠคํŠธ ์บก์ฐจ ์ ์  ์‚ฌ์šฉ ๊ฐ์†Œ ๊ฑฐ์˜ ์‚ฌ๋ผ์งˆ ์ „๋ง
์ด๋ฏธ์ง€ ์บก์ฐจ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋จ ๋” ๋ณต์žกํ•ด์งˆ ๊ฒƒ
ํ–‰๋™ ๋ถ„์„ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅ ์ค‘ ์ฃผ๋ฅ˜๊ฐ€ ๋  ์ „๋ง
์ƒ์ฒด ์ธ์ฆ ์ดˆ๊ธฐ ๋‹จ๊ณ„ ๋ชจ๋ฐ”์ผ์—์„œ ํ™•๋Œ€

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ

๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด๋Ÿฐ ์ž๋ฃŒ๋“ค์„ ์ถ”์ฒœํ•ด:

์˜จ๋ผ์ธ ๊ฐ•์˜:
โ€ข Coursera - Deep Learning Specialization (Andrew Ng)
โ€ข Fast.ai - Practical Deep Learning for Coders
โ€ข Udacity - Computer Vision Nanodegree

๋…ผ๋ฌธ ๋ฐ ์ž๋ฃŒ:
โ€ข "Breaking Text-based CAPTCHAs with Variable Word Length" (Goodfellow et al.)
โ€ข "I'm not a human: Breaking the Google reCAPTCHA" (Sivakorn et al.)
โ€ข OpenCV ๊ณต์‹ ๋ฌธ์„œ
โ€ข TensorFlow ํŠœํ† ๋ฆฌ์–ผ

์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ:
โ€ข captcha-tensorflow (GitHub)
โ€ข simple-captcha-solver (GitHub)
โ€ข keras-ocr (GitHub)

์ปค๋ฎค๋‹ˆํ‹ฐ:
โ€ข Stack Overflow - [computer-vision] ํƒœ๊ทธ
โ€ข Reddit - r/MachineLearning, r/computervision
โ€ข Kaggle - ์บก์ฐจ ๊ด€๋ จ ๋Œ€ํšŒ ๋ฐ ๋…ธํŠธ๋ถ
โ€ข Papers with Code - ์ตœ์‹  ์—ฐ๊ตฌ ๋™ํ–ฅ

๐Ÿ’ก ๋งˆ์น˜๋ฉฐ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์บก์ฐจ ์ž๋™ ์ธ์‹ ํ”„๋กœ๊ทธ๋žจ์— ๋Œ€ํ•ด ์™„์ „ํžˆ ํŒŒํ—ค์ณ๋ดค์–ด! ๐ŸŽ‰

์šฐ๋ฆฌ๊ฐ€ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๋ฉด:

โœจ ํ•ต์‹ฌ ์š”์•ฝ

1. ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ธฐ์ดˆ
โ€ข ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋ณ€ํ™˜, ์ด์ง„ํ™”, ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ
โ€ข OpenCV๋ฅผ ํ™œ์šฉํ•œ ์ „์ฒ˜๋ฆฌ ๊ธฐ๋ฒ•
โ€ข ๋ชจํด๋กœ์ง€ ์—ฐ์‚ฐ๊ณผ ํ•„ํ„ฐ๋ง

2. ๋ฌธ์ž ๋ถ„๋ฆฌ (Segmentation)
โ€ข ์œค๊ณฝ์„  ๊ฒ€์ถœ๊ณผ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค
โ€ข ์ˆ˜์ง/์ˆ˜ํ‰ ํˆฌ์˜ ๋ถ„์„
โ€ข ๊ฒน์นœ ๊ธ€์ž ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•

3. ๋ฌธ์ž ์ธ์‹ (OCR)
โ€ข Tesseract OCR
โ€ข ํ…œํ”Œ๋ฆฟ ๋งค์นญ
โ€ข ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ๋ถ„๋ฅ˜ (KNN, SVM)
โ€ข ๋”ฅ๋Ÿฌ๋‹ (CNN) ํ™œ์šฉ

4. ์„ฑ๋Šฅ ์ตœ์ ํ™”
โ€ข ๋ฐ์ดํ„ฐ ์ฆ๊ฐ•
โ€ข ์•™์ƒ๋ธ” ๊ธฐ๋ฒ•
โ€ข ์ „์ด ํ•™์Šต
โ€ข ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹

5. ์‹ค์ „ ๊ตฌํ˜„
โ€ข ๋ชจ๋“ˆํ™”๋œ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ
โ€ข ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ
โ€ข ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ

๐ŸŽฏ ์‹ค๋ฌด ์ ์šฉ ์‹œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ์ด๊ฒƒ๋“ค์„ ํ™•์ธํ•ด๋ด:

โ˜‘๏ธ ๋ฒ•์  ๋ฌธ์ œ๋Š” ์—†๋Š”๊ฐ€? (๊ฐ€์žฅ ์ค‘์š”!)
โ˜‘๏ธ ์ถฉ๋ถ„ํ•œ ํ•™์Šต ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ ์ „์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์ด ์ž˜ ์ž‘๋™ํ•˜๋Š”๊ฐ€?
โ˜‘๏ธ ๋ชจ๋ธ ์„ฑ๋Šฅ์ด ๋ชฉํ‘œ์น˜๋ฅผ ๋‹ฌ์„ฑํ–ˆ๋Š”๊ฐ€?
โ˜‘๏ธ ์‹ค์ „ ๋ฐ์ดํ„ฐ์—์„œ๋„ ์ž˜ ์ž‘๋™ํ•˜๋Š”๊ฐ€?
โ˜‘๏ธ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ์ œ๋Œ€๋กœ ๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ ์ฝ”๋“œ๊ฐ€ ์œ ์ง€๋ณด์ˆ˜ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ์ธ๊ฐ€?

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

์ด์ œ ๊ธฐ๋ณธ์€ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋‹ˆ, ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ๋‚˜์•„๊ฐ€๋ณด์ž:

์ดˆ๊ธ‰ โ†’ ์ค‘๊ธ‰:
โ€ข ๋‹ค์–‘ํ•œ ์บก์ฐจ ์Šคํƒ€์ผ์— ๋Œ€์‘ํ•˜๊ธฐ
โ€ข ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ๊ฐœ์„ 
โ€ข ์›น ์„œ๋น„์Šค๋กœ ๋ฐฐํฌํ•˜๊ธฐ

์ค‘๊ธ‰ โ†’ ๊ณ ๊ธ‰:
โ€ข Transformer ๋ชจ๋ธ ์ ์šฉ (BERT, ViT)
โ€ข GAN์„ ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
โ€ข ๊ฐ•ํ™”ํ•™์Šต ๊ธฐ๋ฐ˜ ์ตœ์ ํ™”
โ€ข ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

๊ณ ๊ธ‰ โ†’ ์ „๋ฌธ๊ฐ€:
โ€ข ์ƒˆ๋กœ์šด ์บก์ฐจ ๋ฐฉ์–ด ๊ธฐ๋ฒ• ์—ฐ๊ตฌ
โ€ข ๋…ผ๋ฌธ ์ž‘์„ฑ ๋ฐ ํ•™ํšŒ ๋ฐœํ‘œ
โ€ข ์˜คํ”ˆ์†Œ์Šค ํ”„๋กœ์ ํŠธ ๊ธฐ์—ฌ
โ€ข ๋ณด์•ˆ ์ปจ์„คํŒ…

๐ŸŽ“ ์ตœ์ข… ์กฐ์–ธ ๊ธฐ์ˆ ์€ ๋„๊ตฌ์ผ ๋ฟ์ด์•ผ. ์ค‘์š”ํ•œ ๊ฑด ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋А๋ƒ์ง€. ์ด ๊ธ€์—์„œ ๋ฐฐ์šด ์ง€์‹์„ ๊ธ์ •์ ์ธ ๋ฐฉํ–ฅ์œผ๋กœ ํ™œ์šฉํ•ด์„œ, ๋” ๋‚˜์€ ๋ณด์•ˆ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๊ฑฐ๋‚˜, ์ ‘๊ทผ์„ฑ์„ ๊ฐœ์„ ํ•˜๊ฑฐ๋‚˜, ์ƒˆ๋กœ์šด ์—ฐ๊ตฌ์— ๊ธฐ์—ฌํ•˜๋Š” ๋ฐ ์“ฐ๊ธธ ๋ฐ”๋ผ! ๐Ÿ’ช

ํ”„๋กœ๊ทธ๋ž˜๋ฐ์€ ๋Š์ž„์—†๋Š” ํ•™์Šต์˜ ์—ฐ์†์ด์•ผ. ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์–ด๋„ ํฌ๊ธฐํ•˜์ง€ ๋ง๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์งˆ๋ฌธํ•˜๊ณ , ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›์œผ๋ฉด์„œ ์„ฑ์žฅํ•ด๋‚˜๊ฐ€์ž. ํ™”์ดํŒ…! ๐Ÿ”ฅ

ํ˜น์‹œ ์ด ๊ธ€์ด ๋„์›€์ด ๋๋‹ค๋ฉด, ์ฃผ๋ณ€ ๊ฐœ๋ฐœ์ž ์นœ๊ตฌ๋“ค์—๊ฒŒ๋„ ๊ณต์œ ํ•ด์ค˜! ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ƒˆ๋กœ์šด ์ธ์‚ฌ์ดํŠธ๋ฅผ ์–ป๊ฒŒ ๋˜๋ฉด, ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๊ณต์œ ํ•˜๋Š” ๊ฒƒ๋„ ์žŠ์ง€ ๋งˆ! ๐ŸŒŸ

๋งˆ์ง€๋ง‰์œผ๋กœ, ์ด ๋ถ„์•ผ๋Š” ์ •๋ง ๋น ๋ฅด๊ฒŒ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. ํ•ญ์ƒ ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€๊ณ , ์ƒˆ๋กœ์šด ๊ธฐ์ˆ ์„ ๋ฐฐ์šฐ๋Š” ์ž์„ธ๋ฅผ ์œ ์ง€ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์ด ๋‚ด์ผ์€ ๊ตฌ์‹์ด ๋  ์ˆ˜๋„ ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜Š

๊ทธ๋Ÿผ ๋ชจ๋‘ ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉ ๋˜๊ธธ ๋ฐ”๋ผ! Happy Coding! ๐ŸŽฎ๐Ÿ’ปโœจ
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.