EasyOCR

Import Library

import re
import cv2
import easyocr
import os
from glob import glob
import numpy as np
from PIL import Image, ImageDraw, ImageFont
# import torch
  • re: ์ •๊ทœ์‹ ๊ฒ€์‚ฌ(์ˆซ์ž ํŒจํ„ด ํƒ์ง€)
  • cv2: (OpenCV): ์ด๋ฏธ์ง€ ์ž…์ถœ๋ ฅ, ํด๋ฆฌ๊ณค(polygon) ๋งˆ์Šคํฌ, ๋ฆฌ์‚ฌ์ด์ฆˆ, ๋ธ”๋Ÿฌ ๋“ฑ
  • easyocr: OCR ์—”์ง„ (readtext โ†’ [(bbox, text, confidence),...])
  • numpy: ์ขŒํ‘œ/๋ฐฐ์—ด ์—ฐ์‚ฐ
  • PIL(Pillow): ํ…์ŠคํŠธ ๋Œ€์ฒด ๋ชจ๋“œ(replace_text)์—์„œ ๊ธ€์ž ๋ Œ๋”๋ง ๋ฐ ํšŒ์ „ ํ•ฉ์„ฑ
  • glob, os: ํŒŒ์ผ ์ž…์ถœ๋ ฅ/ํด๋” ์ฒ˜๋ฆฌ
  • torch: gpu ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ (ํ•„์ˆ˜ x)

์‚ฌ์ „ ์„ค์ •

# EasyOCR ์„ค์ •๊ฐ’
OCR_LANGS = ['ko', 'en']
 
# ์ •๊ทœ์‹ (3๊ฐ€์ง€ ์œ ํ˜• ์ปค๋ฒ„)
rrn_combined_regex = re.compile(r"\b\d{6}\s*-\s*\d{7}\b")
rrn_nohyphen_regex = re.compile(r"\b\d{13}\b")
rrn_front_regex = re.compile(r"\b\d{6}\b")
rrn_back_regex = re.compile(r"\b\d{7}\b")
  • OCR_LANGS = ['ko', 'en']: EasyOCR์ด ์‚ฌ์šฉํ•  ์–ธ์–ด
  • rrn_combined_regex : \b\d{6}\s*-\s*\d{7}\b (6์ž๋ฆฌ, ํ•˜์ดํ”ˆ(์˜ต์…˜ ๊ณต๋ฐฑ) ํ›„ 7์ž๋ฆฌ)
  • rrn_nohyphen_regex : \b\d{13}\b (13์ž๋ฆฌ ๋ถ™์–ด์žˆ์„ ๋•Œ)
  • rrn_front_regex : \b\d{6}\b (์•ž 6์ž๋ฆฌ)
  • rrn_back_regex : \b\d{7}\b (๋’ค 7์ž๋ฆฌ)

โš ๏ธํ˜„์žฌ ์ •๊ทœ์‹ ํŒจํ„ด์˜ ํ•œ๊ณ„

  • ์ (.), ์Šฌ๋ž˜์‹œ(/) ๋“ฑ ๋‹ค๋ฅธ ๊ตฌ๋ถ„์ž์™€ ํŠน์ˆ˜๋ฌธ์ž, OCR ์˜คํƒ€(์˜ˆ: oโ†’0, lโ†’1)๋Š” ํŒจํ„ด์—์„œ ์ œ์™ธ๋จ.
# ๊ฒฐํ•ฉ ์ž„๊ณ„๊ฐ’ ์„ค์ •
MAX_X_DISTANCE = 50
MAX_Y_DIFFERENCE = 10
 
# ๋น„์‹๋ณ„ํ™” ๋ฐฉ์‹ ๋ฐ ์„ค์ •
MASK_METHOD = "black" # "black", "mosaic", "blur", "replace_text"
REPLACE_TEXT = "[ REDACTED ]"
MOSAIC_SCALE = 0.05
BLUR_KERNEL = (51, 51)
OCR_LOW_TEXT = 0.4
  • MAX_X_DISTANCE = 50: ์•ž์˜ ์ˆซ์ž(6์ž๋ฆฌ)์™€ ๋’ค์˜ ์ˆซ์ž(7์ž๋ฆฌ) ์‚ฌ์ด x ๊ฑฐ๋ฆฌ ์ตœ๋Œ€ ํ—ˆ์šฉ (ํ”ฝ์…€ ๋‹จ์œ„)

  • MAX_Y_DIFFERENCE = 10: ์ค‘์‹ฌ์„  y ์ฐจ์ด ์ตœ๋Œ€ ํ—ˆ์šฉ

  • MASK_METHOD: ๋งˆ์Šคํ‚น ๋ฐฉ์‹. โ€œblackโ€, โ€œmosaicโ€, โ€œblurโ€, โ€œreplace_textโ€์œผ๋กœ ์ด 4๊ฐ€์ง€

  • MOSAIC_SCALE, BLUR_KERNEL, OCR_LOW_TEXT: ๋ณด์ • ํŒŒ๋ผ๋ฏธํ„ฐ


๋งˆ์Šคํ‚น ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜

mask_bbox ํ•จ์ˆ˜

def mask_bbox(img, bbox, method=MASK_METHOD):
 
    pts = np.array(bbox, dtype=np.int32)
    
    ...
    
    return img  

โ„น๏ธ ๊ธฐ๋Šฅ

  • ์ด๋ฏธ์ง€ ์•ˆ์—์„œ ํŠน์ • ๊ธ€์ž ์˜์—ญ๋งŒ ๊ฐ€๋ฆฌ๊ฑฐ๋‚˜ ๋งˆ์Šคํ‚นํ•˜๋Š” ํ•จ์ˆ˜
  • EasyOCR์˜ readtext()์—์„œ ๋ฐ˜ํ™˜๋œ **๊ธ€์ž์˜ ์œ„์น˜(bbox)**๋ฅผ ์ž…๋ ฅ ๋ฐ›์•„ ์ฒ˜๋ฆฌ

  • pts = np.array(bbox, dtype=np.int32)

    bbox์€ ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐ’์„ ๊ฐ–๋Š”๋‹ค.

    bbox = [[np.int32(296), np.int32(188)], [np.int32(640), np.int32(188)], [np.int32(640), np.int32(264)], [np.int32(296), np.int32(264)]]
    
  • OpenCV์—์„œ ๋„ํ˜• ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜(cv2.fillPoly)๋Š” numpy ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ์ขŒํ‘œ๋ฅผ ๋ฐ›์•„์•ผ ํ•œ๋‹ค. EasyOCR์—์„œ๋Š” ์ขŒํ‘œ๊ฐ’์„ numpy(np)์˜ int32 ํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ dtype=np.in32๋กœ ์ง€์ •ํ•œ๋‹ค.

    ๊ฒฐ๋ก ์ ์œผ๋กœ pts๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ 2์ฐจ์› numpy array๊ฐ€ ๋œ๋‹ค.

    [[296 188]
     [640 188]
     [640 264]
     [296 264]]
    

๊ฒ€์ •์ƒ‰ ๋งˆ์Šคํ‚น(black)

    if method == "black":  
        cv2.fillPoly(img, [pts], (0, 0, 0))  
  • cv2.fillPoly(img, [pts], (0, 0, 0))
    • ์ž…๋ ฅ ๋ฐ›์€ ์ด๋ฏธ์ง€(img)์—์„œ ์ง€์ •๋œ ๋‹ค๊ฐํ˜• ์˜์—ญ([pts])์„ ๊ฒ€์ •์ƒ‰((0, 0, 0))์œผ๋กœ ์ฑ„์šฐ๋Š” ํ•จ์ˆ˜
    • pts ์ขŒํ‘œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฏธ์ง€์— ๋‹ค๊ฐํ˜• ์˜์—ญ์„ ์ •์˜ํ•˜๊ณ , ๋‹ค๊ฐํ˜• ๋‚ด๋ถ€ ํ”ฝ์…€๊ฐ’์„ ๊ฒ€์ •์ƒ‰์œผ๋กœ ์ฑ„์šด๋‹ค.
    • ๊ณต์‹ ๋ฌธ์„œ ์ฐธ์กฐ

โ“ ์™œ [pts]์ฒ˜๋Ÿผ ๋ฆฌ์ŠคํŠธ ์•ˆ์— ๋„ฃ์„๊นŒ?

  • cv2.fillPoly()๋Š” ์—ฌ๋Ÿฌ ๊ฐœ ๋‹ค๊ฐํ˜•์„ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค!!
  • ์˜ˆ๋ฅผ ๋“ค์–ด, ์—ฌ๋Ÿฌ ๋‹ค๊ฐํ˜•์„ ํ•œ ๋ฒˆ์— ๊ทธ๋ฆฌ๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋ฉด ๋œ๋‹ค.
    cv2.fillPoly(img, [pts1, pts2, pts3], (0,0,0))
  • ์ฆ‰, ๋‹จ์ผ ๋‹ค๊ฐํ˜•์ด๋ผ๋„ ๋ฆฌ์ŠคํŠธ ์•ˆ์— ๋„ฃ์–ด์•ผ ํ•จ์ˆ˜ ๊ทœ๊ฒฉ์— ๋งž๋Š”๋‹ค.

๋ชจ์ž์ดํฌ ๋งˆ์Šคํ‚น(mosaic)

    elif method == "mosaic":  
        """ ์ž‘์€ ์ด๋ฏธ์ง€๋กœ ์ถ•์†Œ ํ›„ ๋‹ค์‹œ ํ™•๋Œ€ํ•˜์—ฌ ํ”ฝ์…€ํ™” (๋ชจ์ž์ดํฌ) """  
        mask = np.zeros(img.shape[:2], dtype=np.uint8)  
        cv2.fillPoly(mask, [pts], 255)  
        x, y, w, h = cv2.boundingRect(pts) # bbox ํฌ๊ธฐ ๊ณ„์‚ฐ  
        roi = img[y:y+h, x:x+w]  
        mask_roi = mask[y:y+h, x:x+w]  
        if roi.size != 0:  
            small_w = max(1, int(w * MOSAIC_SCALE))  
            small_h = max(1, int(h * MOSAIC_SCALE))  
            small = cv2.resize(roi, (small_w, small_h))  
            mosaic = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)  
            roi[mask_roi==255] = mosaic[mask_roi==255]
  • ๋ชจ์ž์ดํฌ๋Š” **๋Œ€์ƒ ์˜์—ญ(๋‹ค๊ฐํ˜•)**๋งŒ ๋”ฐ๋กœ ์ž˜๋ผ์„œ, ๊ทธ ๋ถ€๋ถ„์„ ์•„์ฃผ ์ž‘๊ฒŒ ์ถ•์†Œํ–ˆ๋‹ค๊ฐ€(์ •๋ณด ์†์‹ค) ๋‹ค์‹œ ์›๋ž˜ ํฌ๊ธฐ๋กœ ํ™•๋Œ€ํ•ด์„œ ํ”ฝ์…€์„ ๋ญ‰๊ฐœ๋Š” ๋ฐฉ์‹. ์œ„์˜ ์ฝ”๋“œ์—์„œ np.zeros๋กœ ๋งŒ๋“  **๋งˆ์Šคํฌ(mask)**๋Š” โ€œ์–ด๋””์— ๋ชจ์ž์ดํฌ๋ฅผ ์ ์šฉํ• ์ง€โ€๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋„ํ™”์ง€ ์—ญํ• ์„ ํ•œ๋‹ค.


  • mask = np.zeros(img.shape[:2], dtype=np.unit8)

    • img.shape[:2]๋Š” ์ž…๋ ฅ ๋ฐ›์€ ์ด๋ฏธ์ง€์˜ (height, width)๋ฅผ ๋œปํ•œ๋‹ค. ์ฐธ๊ณ ๋กœ OpenCV์˜ ์ด๋ฏธ์ง€๋Š” (height, width, channels)์˜ ํ˜•ํƒœ๋ฅผ ๊ฐ–๋Š”๋‹ค.
    • np.zeros(shape, dtype=None, order='C')๋Š” ์ฃผ์–ด์ง„ ํ˜•ํƒœ(shape)์˜ ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•˜๋ฉฐ, ๋ชจ๋“  ์š”์†Œ๋ฅผ 0์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ํ•จ์ˆ˜๋‹ค. shape์€ ์ •์ˆ˜ ๋˜๋Š” ์ •์ˆ˜๋“ค์˜ ํŠœํ”Œ ํ˜•ํƒœ๋กœ ์ž…๋ ฅ๋œ๋‹ค.
    • ์ฆ‰, mask = np.zeros(...)๋Š” ๋ชจ๋“  ์š”์†Œ๋ฅผ 0์œผ๋กœ ๊ฐ–๋Š” 2์ฐจ์› ๋ฐฐ์—ด์ด๋ฉฐ, OpenCV ์ด๋ฏธ์ง€์˜ ๊ด€์ ์—์„œ๋Š” ์ž…๋ ฅ ๋ฐ›์€ ์ด๋ฏธ์ง€์™€ ๊ฐ™์€ ํฌ๊ธฐ์˜ ๋‹จ์ผ ์ฑ„๋„(2D) ์ด๋ฏธ์ง€๋‹ค.
  • cv2.fillPoly(mask, [pts], 255)

    • mask ์ด๋ฏธ์ง€ ์•ˆ์—์„œ pts(๊ด€์‹ฌ ์˜์—ญ์˜ ๊ผญ์ง“์  ์ขŒํ‘œ๋“ค)๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ๋ถ€๋ถ„์„ ํฐ์ƒ‰(255)์œผ๋กœ ์ฑ„์›€.
    • ๊ฒฐ๊ณผ์ ์œผ๋กœ ์ด ๋ถ€๋ถ„์ด ๋ชจ์ž์ดํฌ ๋Œ€์ƒ(ํฐ์ƒ‰)์ด๋‹คโ€ ๋ผ๋Š” ์ •๋ณด๊ฐ€ mask ์•ˆ์— ๋‹ด๊ธด๋‹ค.
  • x, y, w, h = cv2.boundingRect(pts)

    • boundingRect()๋Š” ์ฃผ์–ด์ง„ ์ ์„ ๊ฐ์‹ธ๋Š” ์ตœ์†Œ ํฌ๊ธฐ์˜ ์‚ฌ๊ฐํ˜•(๋ฐ”์šด๋”ฉ ๋ฐ•์Šค)์„ ๋ฐ˜ํ™˜ํ•˜๋Š” OpenCV์˜ ์™ธ๊ฐ์„  ํ•จ์ˆ˜๋‹ค. (x, y ์ขŒ์ƒ๋‹จ, ๋„ˆ๋น„, ๋†’์ด) cv2.boundingRect_explain
  • roi = img[y:y+h, x:x+w] mask_roi = mask[y:y+h, x:x+w]

    • roi๋Š” ์›๋ณธ ์ด๋ฏธ์ง€์—์„œ ๊ด€์‹ฌ ์˜์—ญ๋งŒ ์ž˜๋ผ๋‚ธ ๋ถ€๋ถ„(๋†’์ด h, ๋„ˆ๋น„ w, ์ฑ„๋„ 3) โ†’ ์—ฌ๊ธฐ์„œ ๋ชจ์ž์ดํฌ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.
    • mask_roi๋Š” ๋งˆ์Šคํฌ์—์„œ roi์™€ ๋™์ผํ•œ ์˜์—ญ์„ ์ž˜๋ผ๋‚ธ ๊ฒƒ(๋†’์ด h, ๋„ˆ๋น„ w, ๋‹จ์ผ ์ฑ„๋„)

    ์ „์ฒด ํฐ ์ด๋ฏธ์ง€์—์„œ ์ž‘์—…ํ•˜๋Š” ๋Œ€์‹ , ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ด€์‹ฌ ์˜์—ญ(ROI)๋งŒ ์ž˜๋ผ์„œ ์—ฐ์‚ฐํ•˜๋ฉด ์†๋„์™€ ๋ฉ”๋ชจ๋ฆฌ ๋ฉด์—์„œ ํšจ์œจ์ ์ด๋‹ค.

  • small_w = max(1, int(w * MOSAIC_SCALE)) small_h = max(1, int(h * MOSAIC_SCALE))

    • MOSAIC_SCALE์€ ์›๋ณธ ROI๋ฅผ ์–ผ๋งˆ๋‚˜ ์ž‘๊ฒŒ ๋งŒ๋“ค์ง€ ์ •ํ•˜๋Š” ๋น„์œจ ์˜ˆ: w=200, MOSAIC_SCALE=0.05 โ†’ small_w=10
    • max(1, ...)๋Š” ROI๊ฐ€ ์•„์ฃผ ์ž‘์•„์„œ 0์ด ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•จ(0์ด๋ฉด resize ์—๋Ÿฌ๋‚จ)
    • ์ž‘๊ฒŒ ์ค„์ผ์ˆ˜๋ก ๋ชจ์ž์ดํฌ๊ฐ€ ๋”์šฑ ์‹๋ณ„ ๋ถˆ๊ฐ€๋Šฅํ•ด์ง
  • small = cv2.resize(roi, (small_w, small_h))

    • roi(์›๋ณธ ์นผ๋ผ ์ด๋ฏธ์ง€)๋ฅผ (small_w, small_h) ํฌ๊ธฐ๋กœ ์ถ•์†Œ.
  • mosaic = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)

    • ๋‹ค์‹œ ์›๋ž˜ ํฌ๊ธฐ (w,h)๋กœ ํ™•๋Œ€.
    • ๋ณด๊ฐ„๋ฒ•(interpolation)์œผ๋กœ๋Š” INTER_NEAREST, INTER_AREA, INTER_CUBIC, INTER_LINEAR(๊ธฐ๋ณธ๊ฐ’) ๋“ฑ ๋‹ค์–‘ํ•˜๋‹ค. INTER_NEAREST๋Š” ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ํ”ฝ์…€์„ ๋ณต์ œํ•˜๋Š” ๋ณด๊ฐ„๋ฒ•์ด๋‹ค.
  • roi[mask_roi==255] = mosaic[mask_roi==255]

    • ์ง์‚ฌ๊ฐํ˜• roi ์•ˆ์—์„œ **๋งˆ์Šคํฌ๊ฐ€ ํฐ์ƒ‰(255)**์ธ ํ”ฝ์…€๋“ค๋งŒ ๊ณจ๋ผ์„œ, ๊ทธ ํ”ฝ์…€๋“ค์„ ๊ฐ™์€ ์œ„์น˜์˜ mosaic ๊ฐ’์œผ๋กœ ๊ต์ฒด

๋ธ”๋Ÿฌ ๋งˆ์Šคํ‚น(blur)

    elif method == "blur":  
        """ ๊ฐ€์šฐ์‹œ์•ˆ ๋ธ”๋Ÿฌ ์ ์šฉ """ 
        mask = np.zeros(img.shape[:2], dtype=np.uint8)  
        cv2.fillPoly(mask, [pts], 255)  
        x, y, w, h = cv2.boundingRect(pts)  
        roi = img[y:y+h, x:x+w]  
        mask_roi = mask[y:y+h, x:x+w]  
        if roi.size != 0:  
            blur = cv2.GaussianBlur(roi, BLUR_KERNEL, 0)  
            roi[mask_roi==255] = blur[mask_roi==255] 
  • cv2.GaussianBlur โ†’ ์ง€์ •ํ•œ ์ปค๋„๋งŒํผ ์ด๋ฏธ์ง€๋ฅผ ํ๋ฆฌ๊ฒŒ ์ฒ˜๋ฆฌ
  • ๋ชจ์ž์ดํฌ(mosaic)์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ mask ์˜์—ญ๋งŒ ์ ์šฉ

ํ…์ŠคํŠธ ๊ต์ฒด ๋งˆ์Šคํ‚น(replace_text)

โญ ์ด ํ•จ์ˆ˜๊ฐ€ ํ•˜๋Š” ์ผ

  • ํด๋ฆฌ๊ณค(bbox) ์˜์—ญ์„ ํฐ์ƒ‰ ๋ฐ•์Šค๋กœ ๋ฎ์–ด ์›๋ž˜ ๊ธ€์ž๋ฅผ ์ง€์šด๋‹ค. (๊ฒ€์ •์ƒ‰ ๋งˆ์Šคํ‚น๊ณผ ์›๋ฆฌ ๋™์ผ)
  • ๊ทธ ์˜์—ญ์— ๋นจ๊ฐ„์ƒ‰ ๋Œ€์ฒด ํ…์ŠคํŠธ ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฐ๋‹ค.
  • ํด๋ฆฌ๊ณค ์˜์—ญ์ด ๊ธฐ์šธ์–ด์ ธ ์žˆ๋‹ค๋ฉด ํ…์ŠคํŠธ ์ด๋ฏธ์ง€๋„ ๊ทธ ๊ธฐ์šธ๊ธฐ์— ๋งž์ถฐ ๊ฐ™์€ ๊ฐ๋„๋กœ ํšŒ์ „์‹œ์ผœ ๋„ฃ๋Š”๋‹ค.
  • PIL(RGBA)๋ฅผ ์ด์šฉํ•ด ์ด๋ฏธ์ง€ ํ•ฉ์„ฑ ํ›„ OpenCV(BGR) ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ด์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
    elif method == "replace_text":  
        cv2.fillPoly(img, [pts], (255, 255, 255))
        x, y, w, h = cv2.boundingRect(pts)  
        center_x = x + w // 2  
        center_y = y + h // 2    
        max_len = 0  
        angle = 0  
        for i in range(4):  
            dx = pts[(i+1)%4][0] - pts[i][0]  
            dy = pts[(i+1)%4][1] - pts[i][1]  
            length = np.hypot(dx, dy)  
            if length > max_len:  
                max_len = length  
                angle = -np.degrees(np.arctan2(dy, dx))   
        pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))  
        draw = ImageDraw.Draw(pil_img)  
        diagonal = np.hypot(w, h)  
        font_size = min(max(int(diagonal * 0.1), 12), h)   
        try:  
            font = ImageFont.truetype("malgunbd.ttf", font_size) # ๋ง‘์€ ๊ณ ๋”• bold
        except:  
            font = ImageFont.load_default()  
        bbox_text = draw.textbbox((0, 0), REPLACE_TEXT, font=font)  
        text_w = bbox_text[2] - bbox_text[0]  
        text_h = bbox_text[3] - bbox_text[1]  
        offset = 5 # ํ…์ŠคํŠธ ์œ„๋กœ ์˜ฌ๋ฆด ํ”ฝ์…€ ์ˆ˜  
        text_x = center_x - text_w / 2  
        text_y = center_y - text_h / 2 - offset  
        text_layer = Image.new("RGBA", pil_img.size, (255, 255, 255, 0))  
        text_draw = ImageDraw.Draw(text_layer)  
        text_draw.text((text_x, text_y), REPLACE_TEXT, font=font, fill=(255, 0, 0, 255))   
        rotated_layer = text_layer.rotate(angle, center=(center_x, center_y), resample=Image.BICUBIC)  
        pil_img = Image.alpha_composite(pil_img.convert("RGBA"), rotated_layer)  
        img[:] = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)

๊ฐœ๋นก์„ธ๋‹ค

  • cv2.fillPoly(img, [pts], (255, 255, 255))
    • ์ž…๋ ฅ ๋ฐ›์€ ์ด๋ฏธ์ง€(img)์—์„œ ์ง€์ •๋œ ๋‹ค๊ฐํ˜• ์˜์—ญ([pts])์„ ํฐ์ƒ‰((255, 255, 255))์œผ๋กœ ์ฑ„์šฐ๋Š” ํ•จ์ˆ˜.
    • ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฐ๊ฒฝ
  • center_x = x + w // 2 center_y = y + h // 2
    • bbox ๊ฐ€์šด๋ฐ ์ขŒํ‘œ ๊ณ„์‚ฐ โ†’ ํ…์ŠคํŠธ๋ฅผ ์ค‘์•™์— ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•จ.

max_len = 0  
angle = 0  
for i in range(4):  
    dx = pts[(i+1)%4][0] - pts[i][0]  
    dy = pts[(i+1)%4][1] - pts[i][1]  
    length = np.hypot(dx, dy)  
    if length > max_len:  
        max_len = length  
        angle = -np.degrees(np.arctan2(dy, dx))
  • ๋งˆ์Šคํ‚นํ•ด์•ผ ํ•˜๋Š” ํ…์ŠคํŠธ๊ฐ€ ๊ธฐ์šธ์–ด์ ธ ์žˆ์„ ๋•Œ ๋Œ€์ฒดํ•  ํ…์ŠคํŠธ ํšŒ์ „์„ ์œ„ํ•ด ๊ฐ๋„๋ฅผ ๊ณ„์‚ฐํ•˜๊ธฐ ์œ„ํ•œ ๋ฃจํ”„
  • ์œ„ ์ฝ”๋“œ๋Š” **๊ฐ ๋ณ€(์—ฐ์†๋œ ์  ์Œ)**์— ๋Œ€ํ•ด (dx, dy)์™€ ๊ทธ ๊ธธ์ด length๋ฅผ ๊ตฌํ•˜๊ณ , ๊ฐ€์žฅ ๊ธด ๋ณ€์„ ์ฐพ์€ ๋’ค ๊ทธ ๋ณ€์˜ ๋ฐฉํ–ฅ์„ angle๋กœ ์ง€์ •ํ•˜๋Š” ๋ฃจํ”„๋‹ค.

numpy.hypot(x1, x2)

์ง๊ฐ์‚ผ๊ฐํ˜•์˜ ๋‘ ๋ณ€์˜ ๊ธธ์ด๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ๋น—๋ณ€์˜ ๊ธธ์ด๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ํ•จ์ˆ˜. ์ฆ‰, numpy.hypot() = ๊ธธ์ด(length)๋ฅผ ๊ตฌํ•  ๋•Œ, ์ด ๊ณต์‹์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ **๋‘ ์  ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ ๊ณต์‹ **์— ๊ทผ๊ฑฐํ•œ๋‹ค.

numpy.arctan2(y, x)

arctan2(y, x)๋Š” 2์ฐจ์› ๋ฒกํ„ฐ (x, y)๊ฐ€ **x์ถ•๊ณผ ์ด๋ฃจ๋Š” ๋ฐฉํ–ฅ๊ฐ(๊ฐ๋„)**์„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ณ„์‚ฐํ•ด์ฃผ๋Š” ํ•จ์ˆ˜.

โ“ ์™œ โ€˜๊ฐ€์žฅ ๊ธด ๋ณ€โ€™์˜ ๊ฐ๋„๋ฅผ ์“ฐ๋Š”๊ฐ€?

  • ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ์˜ ๊ฒฝ์šฐ, ๋ณดํ†ต ์ƒ์„ฑ๋˜๋Š” bounding box๋Š” ๊ฐ€๋กœ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ๊ฒŒ ์ƒ์„ฑ๋œ๋‹ค๊ณ  ๊ฐ€์ •.
  • ๋”ฐ๋ผ์„œ ๊ฐ€์žฅ ๊ธด ๋ณ€์„ ํ…์ŠคํŠธ์˜ ๊ธฐ์ค€์„ ์œผ๋กœ ๋ณด๊ณ , ๊ทธ ๋ณ€์˜ ๋ฐฉํ–ฅ์œผ๋กœ ๋Œ€์ฒด ํ…์ŠคํŠธ๋ฅผ ํšŒ์ „์‹œํ‚ค๋ฉด ๋œ๋‹ค๋Š” ๋…ผ๋ฆฌ!
  • angle = -np.degrees(np.arctan2(dy, dx))
    • pillow(PIL)์˜ rotate ๋ฉ”์„œ๋“œ๋Š” ์–‘์ˆ˜(+) ๊ฐ๋„๋ฅผ ์ž…๋ ฅ ๋ฐ›์œผ๋ฉด, ๋ฐ˜์‹œ๊ณ„ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ angle์— ์ ์ ˆํ•œ ๋ถ€ํ˜ธ๋ฅผ ๋งž์ถฐ์ค˜์•ผ ํ…์ŠคํŠธ๊ฐ€ ๋™์ผ ๋ฐฉํ–ฅ์œผ๋กœ ๋†“์ธ๋‹ค.
    • ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฐ•์Šค๊ฐ€ ์™ผ์ชฝ์œผ๋กœ 15ยฐ ๊ธฐ์šธ์–ด์กŒ๋‹ค๋ฉด, np.degrees(np.arctan2(dy, dx))๋Š” ์•ฝ -15ยฐ๋กœ ๋‚˜์˜ค๊ณ , angle = 15๊ฐ€ ๋œ๋‹ค. ์ด๋•Œ pillow(PIL)์˜ rotate์— ์˜ํ•ด ๋Œ€์ฒด ํ…์ŠคํŠธ๊ฐ€ ๋ฐ˜์‹œ๊ณ„ ๋ฐฉํ–ฅ์œผ๋กœ 15ยฐ ํšŒ์ „ํ•˜๊ฒŒ ๋˜๋ฉด์„œ ๋ฐ•์Šค์™€ ๋Œ€์ฒด ํ…์ŠคํŠธ๊ฐ€ ๋™์ผ ๋ฐฉํ–ฅ์œผ๋กœ ๋†“์ด๊ฒŒ ๋œ๋‹ค.

โš ๏ธ ๊ฐ๋„ ๊ณ„์‚ฐ ๋ฃจํ”„์˜ ํ•œ๊ณ„

  • ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ ์œ„ ๋ฃจํ”„๋Š” ์‹คํŒจํ•œ๋‹ค.
    • ์„ธ๋กœ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ๊ฒŒ ๋‚˜์—ด๋œ ํ…์ŠคํŠธ(ํ•˜์ง€๋งŒ ์• ์ดˆ์— EasyOCR์ด ์„ธ๋กœ ๋ฐฉํ–ฅ ํ…์ŠคํŠธ ์ธ์‹์— ์•ฝํ•˜๋‹ค.)
    • ๋น„์ •ํ˜• polygon(๋„ค ์ ์ด ์‚ฌ๊ฐํ˜• ์ˆœ์„œ๋กœ ์˜ค์ง€ ์•Š๋Š” ๊ฒฝ์šฐ)
    • ์ •์‚ฌ๊ฐํ˜• polygon (๊ฐ€๋Šฅ์„ฑ์ด ๊ฑฐ์˜ ์—†๊ธด ํ•˜๋‹ค)
  • ๋” ์ข‹์€ ๋ฐฉ์‹์„ ์ƒ๊ฐํ•ด๋ณผ ํ•„์š”๊ฐ€ ์žˆ๋‹ค..

PIL ๋ณ€ํ™˜ ๋ฐ ํ…์ŠคํŠธ ๋ Œ๋”๋ง
# PIL ์ด๋ฏธ์ง€ ์ƒ์„ฑ  
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_img)  
  
diagonal = np.hypot(w, h)  
font_size = min(max(int(diagonal * 0.1), 12), h)  
  
# ํฐํŠธ ์„ ํƒ๊ณผ ๋กœ๋“œ
try:  
    font = ImageFont.truetype("malgunbd.ttf", font_size) # ๋ง‘์€ ๊ณ ๋”• bold
except:  
    font = ImageFont.load_default() 
  • pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    • OpenCV๋Š” img๋Š” numpy.ndarray์ด๊ณ  ์ƒ‰ ์ฑ„๋„ ์ˆœ์„œ๋Š” BGR, dtype์€ ๋ณดํ†ต unit8
    • PIL(pillow)์€ ์ƒ‰ ์ฑ„๋„ ์ˆœ์„œ๊ฐ€ RGB(๊ทธ๋ฆฌ๊ณ  RGBA)๋‹ค.
    • ๋”ฐ๋ผ์„œ OpenCV โ†’ PIL ๋ณ€ํ™˜ ์‹œ ๋ฐ˜๋“œ์‹œ cv2.cvtColor(img, cv2.COLOR_BGR2RGB)๋กœ BGR โ†’ RGB ๋ณ€ํ™˜์„ ํ•˜๊ณ , numpy ๋ฐฐ์—ด์„ Image.fromarray()๋ฅผ ํ†ตํ•ด PIL ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ์ƒ‰์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณด์ธ๋‹ค.
  • draw = ImageDraw.Draw(pil_img)
    • ๋Œ€์ฒด ํ…์ŠคํŠธ ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ PIL ImageDraw ๊ฐ์ฒด ์ƒ์„ฑ.
    • Image.fromarray(...)์˜ ๊ณผ์ •์€ numpy.ndarray ์ด๋ฏธ์ง€๋ฅผ Pillow๊ฐ€ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” PIL.Image ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์ด์—ˆ๋‹ค๋ฉด, ImageDraw.Draw()๋Š” ๊ทธ ์ด๋ฏธ์ง€ ์œ„์— ๋‹ค๋ฅธ ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
  • diagonal = np.hypot(w, h) font_size = min(max(int(diagonal * 0.1), 12), h)
    • diagonal์€ ํด๋ฆฌ๊ณค ์˜์—ญ์˜ ๋Œ€๊ฐ์„  ๊ธธ์ด
    • int(diagonal * 0.1): ๋Œ€๊ฐ์„  ๊ธธ์ด์˜ 10%๋ฅผ ํฐํŠธ ํฌ๊ธฐ๋กœ.
    • max(..., 12): ์•„๋ฌด๋ฆฌ ์ž‘์€ ์ด๋ฏธ์ง€๋ผ๋„ ์ตœ์†Œ 12px์ด์ƒ ํฐํŠธ ํฌ๊ธฐ ์œ ์ง€.
    • min(..., h): ํฐํŠธ ํฌ๊ธฐ๊ฐ€ ์ด๋ฏธ์ง€ ๋†’์ด๋ณด๋‹ค ์ปค์ง€๋Š” ๊ฑธ ๋ฐฉ์ง€.

ํ…์ŠคํŠธ ํฌ๊ธฐ ๊ณ„์‚ฐ
bbox_text = draw.textbbox((0, 0), REPLACE_TEXT, font=font)  
text_w = bbox_text[2] - bbox_text[0]  
text_h = bbox_text[3] - bbox_text[1]

ImageDraw.textbbox((x,y), text, font, ...) ์ฐธ์กฐ

๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋ ธ์„ ๋•Œ์˜ ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค ์ขŒํ‘œ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋ฐ˜ํ™˜๊ฐ’์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ 4-ํŠœํ”Œ ํ˜•ํƒœ๋‹ค.

(x0, y0, x1, y1)
  • (x0, y0): ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค์˜ ์™ผ์ชฝ ์œ„ ์ขŒํ‘œ

  • (x1, y1): ๋ฐ”์šด๋”ฉ ๋ฐ•์Šค์˜ ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ์ขŒํ‘œ

  • ๊ณต์‹ ๋ฌธ์„œ์—์„œ๋Š” โ€œ(left,ย top,ย right,ย bottom)ย bounding boxโ€ ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค๊ณ  ์„ค๋ช…ํ•œ๋‹ค.

  • ImageDraw.textbbox()๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๊ฐ’์„ ๋ฐ”ํƒ•์œผ๋กœ ํ…์ŠคํŠธ์˜ ๋„ˆ๋น„์™€ ๋†’์ด๋ฅผ ๊ตฌํ•œ๋‹ค.

ํ…์ŠคํŠธ ์œ„์น˜ ๊ณ„์‚ฐ
offset = 5 # ํ…์ŠคํŠธ ์œ„๋กœ ์˜ฌ๋ฆด ํ”ฝ์…€ ์ˆ˜
text_x = center_x - text_w / 2
text_y = center_y - text_h / 2 - offset
  • center_x, center_y๋Š” bbox ์ค‘์‹ฌ์˜ ์ขŒํ‘œ.
  • text_x = center_x - text_w / 2
    • ํ…์ŠคํŠธ๋ฅผ ์ˆ˜ํ‰ ์ค‘์•™ ์ •๋ ฌ: bbox์˜ ์ค‘์‹ฌ์—์„œ ํ…์ŠคํŠธ ๋„ˆ๋น„์˜ ์ ˆ๋ฐ˜๋งŒํผ ์™ผ์ชฝ์— ๋†“์Œ.
  • text_y = center_y - text_h / 2 - offset
    • ํ…์ŠคํŠธ๋ฅผ ์ˆ˜์ง ์ค‘์•™ ์ •๋ ฌํ›„, offset๋งŒํผ ์œ„๋กœ ์˜ฌ๋ฆผ.
    • ์ด๋ฏธ์ง€ ์ขŒํ‘œ๊ณ„์—์„œ y๊ฐ’์€ ์•„๋ž˜๋กœ ์ฆ๊ฐ€ํ•˜๋ฏ€๋กœ -offset์€ ์‹œ๊ฐ์ ์œผ๋กœ ํ…์ŠคํŠธ๋ฅผ ์œ„๋กœ ์˜ฌ๋ฆฌ๋Š” ๋™์ž‘์ด๋‹ค.
    • ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ฐ”์šด๋”ฉ๋ฐ•์Šค์˜ ์ค‘์‹ฌ์— ๋”ฑ ๋งž์ถฐ ๋„ฃ์œผ๋ฉด ํ…์ŠคํŠธ๊ฐ€ bbox์˜ ์ค‘์•™๋ณด๋‹ค ์‚ด์ง ์•„๋ž˜์— ์œ„์น˜ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— offset์œผ๋กœ ํ…์ŠคํŠธ๋ฅผ ์กฐ๊ธˆ ์œ„๋กœ ์ด๋™์‹œํ‚ค๋Š” ๊ฒƒ.

ํ…์ŠคํŠธ ๋ ˆ์ด์–ด ์ƒ์„ฑ ๋ฐ ํšŒ์ „
# ํ…์ŠคํŠธ ๋ ˆ์ด์–ด ์ƒ์„ฑ
text_layer = Image.new("RGBA", pil_img.size, (255, 255, 255, 0))
text_draw = ImageDraw.Draw(text_layer)
text_draw.text((text_x, text_y), REPLACE_TEXT, font=font, fill=(255, 0, 0, 255))
 
# ํ…์ŠคํŠธ ๋ ˆ์ด์–ด ํšŒ์ „
rotated_layer = text_layer.rotate(angle, center=(center_x, center_y), resample=Image.BICUBIC)

โ“ ์™œ ํ…์ŠคํŠธ ๋ ˆ์ด์–ด๋ฅผ ๋งŒ๋“œ๋‚˜?

  • ๋ชฉ์ : ์›๋ณธ ์ด๋ฏธ์ง€์— ์ง์ ‘ ํ…์ŠคํŠธ๋ฅผ ๊ทธ๋ฆฌ์ง€ ์•Š๊ณ , โ€œํˆฌ๋ช…ํ•œ ์บ”๋ฒ„์Šค(RGBA)โ€ ์œ„์— ํ…์ŠคํŠธ๋งŒ ๊ทธ๋ฆฌ๊ธฐ ์œ„ํ•ด.
  • ์ด์œ :
    • PIL ์ด๋ฏธ์ง€๋ฅผ ์ด์šฉํ•˜๋Š” ๊ทผ๋ณธ์ ์ธ ์ด์œ ๋Š”
      1. OpenCV๋ฅผ ์ด์šฉํ•  ๊ฒฝ์šฐ, ํ•œ๊ธ€ ํฐํŠธ ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅ.
      2. OpenCV๋ฅผ ์ด์šฉํ•œ ๋Œ€์ฒด ํ…์ŠคํŠธ ์ƒ์„ฑ ์‹œ, ํ…์ŠคํŠธ์˜ ํ™”์งˆ์ด ๋งค์šฐ ์ข‹์ง€ ์•Š์€ ๊ฒƒ์ด ํ™•์ธ๋จ.
    • ๋”ฐ๋ผ์„œ ํ…์ŠคํŠธ ๋ ˆ์ด์–ด๋ฅผ ์ƒ์„ฑ ํ›„, ํ•ฉ์„ฑํ•˜๋Š” ๋ฐฉ์‹์„ ํ†ตํ•ด ํ•œ๊ธ€ ํฐํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๋Œ€๋น„ํ•˜๊ณ , ํ…์ŠคํŠธ ํ™”์งˆ ์ €ํ•˜ ๋ฌธ์ œ๋„ ํ•ด๊ฒฐ.

  • text_layer = Image.new("RGBA", pil_img.size, (255, 255, 255, 0))
    • RGBA: A = ์•ŒํŒŒ ์ฑ„๋„ ํˆฌ๋ช…๋„
    • (255, 255, 255, 0)์€ ์™„์ „ํ•œ ํˆฌ๋ช…์ƒ‰์„ ์˜๋ฏธ - ์ฆ‰ ์•„๋ฌด ๊ฒƒ๋„ ๊ทธ๋ ค์ง€์ง€ ์•Š์€ ํˆฌ๋ช… ์บ”๋ฒ„์Šค
  • text_draw.text((text_x, text_y), REPLACE_TEXT, font=font, fill=(255, 0, 0, 255))
    • fill=(255, 0, 0, 255)์—์„œ ๋งˆ์ง€๋ง‰ ๊ฐ’ 255๋Š” ์•ŒํŒŒ=๋ถˆํˆฌ๋ช…(์™„์ „ ํ‘œ์‹œ). ์ฆ‰, ํ…์ŠคํŠธ๋Š” ๋ถˆํˆฌ๋ช…ํ•œ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ๊ทธ๋ ค์ง.
  • rotated_layer = text_layer.rotate(angle, center=(center_x, center_y), resample=Image.BICUBIC)
    • angle: ํ…์ŠคํŠธ ํšŒ์ „ ๊ฐ๋„ ๊ณ„์‚ฐ ๋ฃจํ”„๋ฅผ ํ†ตํ•ด ๊ตฌํ•œ ๊ฐ๋„.
    • center=(center_x, center_y): ํšŒ์ „์˜ ์ค‘์‹ฌ์ 
    • resample: ํšŒ์ „ ์‹œ ์‚ฌ์šฉํ•  ๋ณด๊ฐ„๋ฒ•์„ ์ง€์ •ํ•˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ. ์ฃผ์š” ์˜ต์…˜์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
      • Image.NEAREST : ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ํ”ฝ์…€ ๋ณต์ œ (๊ณ„์‚ฐ ๋น ๋ฆ„, ์—์ผ๋ฆฌ์–ด์‹ฑ ์‹ฌํ•จ)
      • Image.BILINEAR : 2ร—2 ์„ ํ˜• ๋ณด๊ฐ„ (๋ถ€๋“œ๋Ÿฌ์›€ โ†‘)
      • Image.BICUBIC : 4ร—4 ๋น„ํ๋น… ๋ณด๊ฐ„ (๋” ๋ถ€๋“œ๋Ÿฝ๊ณ  ํ’ˆ์งˆ ์ข‹์Œ)
      • Image.LANCZOS : ๊ณ ๊ธ‰ ํ•„ํ„ฐ (๋ณดํ†ต ์ถ•์†Œ์‹œ์— ์ข‹์Œ)
    • ํ…์ŠคํŠธ๋ฅผ ํšŒ์ „ํ•˜๋ฉด ๊ณ„๋‹จํ˜„์ƒ(aliasing)์ด ์ƒ๊ธฐ๊ธฐ ์‰ฌ์›€. BICUBIC์€ ํ”ฝ์…€ ๊ฒฝ๊ณ„๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณด๊ฐ„ํ•ด ํšŒ์ „ ํ›„ ํ…์ŠคํŠธ๊ฐ€ ์ข€ ๋” ์ผ๊ธฐ ์ข‹๊ฒŒ ๋ณด์ด๋„๋ก ํ•ด์ฃผ๋ฏ€๋กœ ํ•ด๋‹น ์˜ต์…˜ ์‚ฌ์šฉ.

์ด๋ฏธ์ง€ ํ•ฉ์„ฑ
pil_img = Image.alpha_composite(pil_img.convert("RGBA"), rotated_layer)
img[:] = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)

Image.alpha_composite(base, overlay) ์ž‘๋™ ์›๋ฆฌ

  • alpha_composite๋Š” ๋‘ RGBA ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์•„์„œ ์ „๊ฒฝ(over) ์•ŒํŒŒ ์ฑ„๋„์— ๋”ฐ๋ผ ํ”ฝ์…€ ๋‹จ์œ„๋กœ ํ•ฉ์„ฑ ์†”์งํžˆ ์ž˜ ๋ชจ๋ฆ„
  • ์ „์ œ ์กฐ๊ฑด: ๋‘ ์ด๋ฏธ์ง€์˜ ํฌ๊ธฐ๊ฐ€ ๊ฐ™์•„์•ผ ํ•จ(pil_img.size์™€ ๋™์ผ ํฌ๊ธฐ์˜ text_layer๋ฅผ ๋งŒ๋“  ์ด์œ )
  • pil_img.convert("RGBA"):
    • ์›๋ณธ PIL ์ด๋ฏธ์ง€๊ฐ€ RGB์ด๋ฉด ์•ŒํŒŒ ์ฑ„๋„์ด ์—†์œผ๋ฏ€๋กœ RGBA๋กœ ๋ฐ”๊ฟ”์•ผ ํ•ฉ์„ฑ ๊ฐ€๋Šฅ. convert("RGBA")๋กœ ์•ŒํŒŒ=255(๋ถˆํˆฌ๋ช…)์ธ base๊ฐ€ ๋งŒ๋“ค์–ด์ง.
  • img[:] = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)
    • pil_img.convert("RGB"): RGBA โ†’ RGB๋กœ ๋ฐ”๊พผ ๋‹ค์Œ numpy๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
    • cv2.cvtColor(..., cv2.COLOR_RGB2BGR) : Pillow๋Š” RGB, OpenCV๋Š” BGR ์ด๋ฏ€๋กœ ์ฑ„๋„ ์ˆœ์„œ๋ฅผ ๋งž์ถฐ์ค€๋‹ค.
    • img[:] = ... : ์›๋ณธ img(NumPy ๋ฐฐ์—ด)๋ฅผ ์ „์ฒด ๋ฐ”๊พผ๋‹ค.

combine_bboxes ํ•จ์ˆ˜

def combine_bboxes(bbox1, bbox2):  
    xs1 = [p[0] for p in bbox1]  
    ys1 = [p[1] for p in bbox1]  
    xs2 = [p[0] for p in bbox2]  
    ys2 = [p[1] for p in bbox2]  
  
    x_min = min(min(xs1), min(xs2))  
    x_max = max(max(xs1), max(xs2))  
    y_min = min(min(ys1), min(ys2))  
    y_max = max(max(ys1), max(ys2))  
  
    return [[x_min, y_min], [x_max, y_min], [x_max, y_max], [x_min, y_max]]  

โ„น๏ธ ๊ธฐ๋Šฅ

  • EasyOCR์ด ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ๋ฅผ ๋‘ ์กฐ๊ฐ์œผ๋กœ ์ธ์‹ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜

๐Ÿงฉ ์ƒํ™ฉ ๋ฐฐ๊ฒฝ: EasyOCR์˜ ๋ถ„๋ฆฌ ์ธ์‹ ๋ฌธ์ œ ์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋ฏธ์ง€ ์•ˆ์— ์•„๋ž˜์™€ ๊ฐ™์€ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•˜์ž.

123456-7891011

EasyOCR์ด ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ๋ฅผ ์ธ์‹ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ธ์‹ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค.

์ธ์‹๋œ ํ…์ŠคํŠธbbox(์‚ฌ๊ฐํ˜• ์ขŒํ‘œ)์„ค๋ช…
123456bbox1์•ž 6์ž๋ฆฌ๋งŒ ์ธ์‹
7891011bbox2๋’ค 7์ž๋ฆฌ๋งŒ ์ธ์‹

์ด๋ ‡๊ฒŒ ๋‚˜๋ˆ„์–ด ์ธ์‹ํ•˜๊ฒŒ ๋˜๋ฉด ํ•ด๋‹น ํ…์ŠคํŠธ๊ฐ€ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ์ธ์ง€ ์ •๊ทœ์‹์œผ๋กœ ๊ฑธ๋Ÿฌ๋‚ผ ์ˆ˜๊ฐ€ ์—†๋‹ค. ๋”ฐ๋ผ์„œ

  1. ์•ž์ž๋ฆฌ ํ›„๋ณด(front_parts)์™€ ๋’ท์ž๋ฆฌ ํ›„๋ณด(back_parts)๋ฅผ ๊ฐ๊ฐ ๋”ฐ๋กœ ์ˆ˜์ง‘
  2. x์ถ• ๊ฑฐ๋ฆฌ(MAX_X_DISTANCE)์™€ y์ถ• ๊ฑฐ๋ฆฌ(MAX_Y_DISTANCE)๋ฅผ ๊ธฐ์ค€์œผ๋กœ โ€œ๋ถ™์–ด ์žˆ๋Š”์ง€โ€ ํŒ๋ณ„ (์•ž์ž๋ฆฌ ํ›„๋ณด์™€ ๋’ท์ž๋ฆฌ ํ›„๋ณด๊ฐ€ ๋„ˆ๋ฌด ๋ฉ€๋ฆฌ ๋–จ์–ด์ ธ ์žˆ๋‹ค๋ฉด ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ๊ฐ€ ์•„๋‹ˆ๋ผ๊ณ  ํŒ์ •)
  3. โ€œ๋ถ™์–ด ์žˆ๋‹คโ€๊ณ  ํŒ๋‹จ๋˜๋ฉด ๋‘ bbox๋ฅผ ํ•˜๋‚˜๋กœ ๊ฒฐํ•ฉ(combine_bboxes(bbox1, bbox2) ์‚ฌ์šฉ)

โš™๏ธ ์ž‘๋™ ๋ฐฉ์‹

def combine_bboxes(bbox1, bbox2):  
    xs1 = [p[0] for p in bbox1]  
    ys1 = [p[1] for p in bbox1]  
    xs2 = [p[0] for p in bbox2]  
    ys2 = [p[1] for p in bbox2]
  • ๊ฐ bbox์˜ x, y ์ขŒํ‘œ๋“ค์„ ๊ฐ๊ฐ ๋ถ„๋ฆฌํ•ด ๋ฆฌ์ŠคํŠธ๋กœ ์ถ”์ถœ
x_min = min(min(xs1), min(xs2))  
x_max = max(max(xs1), max(xs2))  
y_min = min(min(ys1), min(ys2))  
y_max = max(max(ys1), max(ys2)) 
  • ๋‘ bbox์˜ ๋ชจ๋“  ์ ์„ ๋น„๊ตํ•˜์—ฌ, x_min, x_max, y_min, y_max ๊ฐ’์„ ๊ตฌํ•œ๋‹ค.
  • ๋‘ ํ…์ŠคํŠธ๋ฅผ ์™„์ „ํžˆ ๋ฎ๋Š” ์ตœ์†Œ ํฌ๊ธฐ์˜ ์ง์‚ฌ๊ฐํ˜•์˜ ๊ผญ์ง€์  ์ขŒํ‘œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
return [[x_min, y_min], [x_max, y_min], [x_max, y_max], [x_min, y_max]]

๋ฉ”์ธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜

process_image ํ•จ์ˆ˜

def process_image(image_path, reader):

โญ ์ด ํ•จ์ˆ˜๊ฐ€ ํ•˜๋Š” ์ผ

  1. ์ด๋ฏธ์ง€๋ฅผ ์ž…๋ ฅ ๋ฐ›์•„(image_path) EasyOCR๋กœ ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ฝ๊ณ 
  2. ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ(์ „์ฒด ์ธ์‹๋˜๊ฑฐ๋‚˜, ๋ถ„๋ฆฌ ์ธ์‹๋œ ๊ฒƒ)๋ฅผ ์ •๊ทœ์‹์œผ๋กœ ์ฐพ์•„
  3. ํ•ด๋‹น ๋ถ€๋ถ„์„ ๋งˆ์Šคํ‚น(mask_bbox()) ์ฒ˜๋ฆฌํ•œ ๋’ค
  4. ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๋Š” ํ•ต์‹ฌ ํ•จ์ˆ˜

์ด๋ฏธ์ง€ ์ฝ๊ธฐ ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

image = cv2.imread(image_path)  
if image is None or image.size == 0:  
    print(f"[ERROR] ์ด๋ฏธ์ง€๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์Œ: {image_path}")  
    return 0
  • image = cv2.imread() โ†’ OpenCV๋กœ ์ž…๋ ฅ ์ด๋ฏธ์ง€ ์ฝ๊ธฐ
  • ํŒŒ์ผ์ด ๊นจ์ ธ ์žˆ๊ฑฐ๋‚˜ ์—†๋Š” ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ.
    • cv2.imread()๋Š” ์‹คํŒจ ์‹œ None์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด ์กฐ๊ฑด๋ฌธ์œผ๋กœ ์•ˆ์ •์„ฑ ํ™•๋ณด.
    • ์ด ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’(found)์€ โ€œํƒ์ง€๋œ ์ฃผ๋ฏผ๋ฒˆํ˜ธ ์ˆ˜โ€์ด๋ฏ€๋กœ, 0์€ โ€˜์—†์Œโ€™์œผ๋กœ ์˜๋ฏธ ์žˆ๊ฒŒ ์ž‘๋™

์ด๋ฏธ์ง€ ์ฝ๊ธฐ ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

try:
    ocr_results = reader.readtext(image, low_text=OCR_LOW_TEXT)
except Exception as e:
    print(f"[ERROR] EasyOCR ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
    return 0
  • EasyOCR ๋ชจ๋ธ(reader.readtext())์„ ์ด์šฉํ•ด ์ด๋ฏธ์ง€ ๋‚ด์˜ ๋ชจ๋“  ํ…์ŠคํŠธ ๋ฐ•์Šค์™€ ์ธ์‹ ๊ฒฐ๊ณผ ์–ป์Œ.
  • ocr_results๋Š” ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ์ด๋ฉฐ, ๊ฐ ํ•ญ๋ชฉ์€ (bbox, text, confidence) ํŠœํ”Œ.
  • ์˜ค๋ฅ˜๊ฐ€ ๋‚  ๊ฐ€๋Šฅ์„ฑ์„ ๋Œ€๋น„ํ•˜๊ธฐ ์œ„ํ•ด try-except๋กœ ๊ฐ์‹ธ ์•ˆ์ „์„ฑ ํ™•๋ณด.

์ดˆ๊ธฐ ๋ณ€์ˆ˜ ์„ค์ •

found = 0  
masked_bboxes_coords = [] 
  • found: ํ˜„์žฌ ์ด๋ฏธ์ง€์—์„œ ๋งˆ์Šคํ‚น๋œ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ์˜ ๊ฐœ์ˆ˜ ์นด์šดํŠธ
  • masked_bboxes_coords: ๋‚˜์ค‘์— ๋งˆ์Šคํ‚น๋œ bbox๋“ค์˜ ์ขŒํ‘œ๋ฅผ ์ €์žฅํ•  ๋ฆฌ์ŠคํŠธ

์™„์ „ํ•œ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ํŒจํ„ด ํƒ์ง€

for bbox, text, conf in ocr_results:
    m_combined = rrn_combined_regex.search(text)
    if m_combined:
         image = mask_bbox(image, bbox, method=MASK_METHOD)
         found += 1
         masked_bboxes_coords.append(bbox)
         print(f"[MASKED] Combined RRN='{m_combined.group()}' | conf={conf:.2f}")
         continue
  
    m_nohyphen = rrn_nohyphen_regex.search(text)
    if m_nohyphen:
         image = mask_bbox(image, bbox, method=MASK_METHOD)
         found += 1
         masked_bboxes_coords.append(bbox)
         print(f"[MASKED] NoHyphen RRN='{m_nohyphen.group()}' | conf={conf:.2f}")
         continue

re.search(pattern, text(string), flags=0)

text๊ฐ€ pattern(์ •๊ทœ ํ‘œํ˜„์‹ ํŒจํ„ด)๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ. flags๋Š” ์„ ํƒ์  ์ธ์ˆ˜๋กœ ์ •๊ทœ ํ‘œํ˜„์‹์˜ ๋™์ž‘์„ ์ˆ˜์ •ํ•˜๋Š” ํ”Œ๋ž˜๊ทธ. ์ •๊ทœํ‘œํ˜„์‹๊ณผ ํŒจํ„ด์ด ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ์— re.match ๊ฐ์ฒด ๋ฐ˜ํ™˜, ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด None ๋ฐ˜ํ™˜.

โ„น๏ธ ๊ธฐ๋Šฅ

  • EasyOCR ๊ฒฐ๊ณผ ์ค‘ โ€œ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ์ „์ฒด(6์ž๋ฆฌ-7์ž๋ฆฌ)โ€œ๊ฐ€ ๋“ค์–ด ์žˆ๋Š” ํ…์ŠคํŠธ ๊ฒ€์ƒ‰.
  • 2๊ฐœ์˜ ์ •๊ทœ์‹ ์‚ฌ์šฉ
    • rrn_combined_regex โ†’ -
    • rrn_nohyphen_regex โ†’ (ํ•˜์ดํ”ˆ ์—†๋Š” 13์ž๋ฆฌ)

  • None์ด ์•„๋‹Œ re.match ๊ฐ์ฒด ๋ฐ˜ํ™˜ ์‹œ mask_bbox() ํ˜ธ์ถœ โ†’ ์ง€์ •๋œ ๋ฐฉ์‹(๊ฒ€์ •, ๋ชจ์ž์ดํฌ, ๋ธ”๋Ÿฌ, ํ…์ŠคํŠธ ๊ต์ฒด)๋กœ ๋งˆ์Šคํ‚น ์ˆ˜ํ–‰
  • continue๋ฅผ ์จ์„œ ํ•œ ๋ฒˆ ๋งˆ์Šคํ‚น๋œ bbox๋Š” ๋‹ค์Œ ๊ฒ€์‚ฌ ์ƒ๋žต โ†’ ์ค‘๋ณต ๋งˆ์Šคํ‚น ๋ฐฉ์ง€ (์•„๋งˆ java์˜ continue์™€ ๊ธฐ๋Šฅ์ ์œผ๋กœ ์ผ์น˜)

    continue๋Š” ๋ฐ˜๋ณต๋ฌธ(for, while)์—์„œ ์•„๋ž˜์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ ์‹œํ‚ค์ง€ ์•Š๊ณ , ๋‹ค์Œ ๋ฐ˜๋ณต ๊ตฌ๋ฌธ์œผ๋กœ ๋„˜์–ด๊ฐ€๋„๋ก ํ•˜๋Š” ์ œ์–ด์ž


๋ถ„๋ฆฌ ์ธ์‹๋œ ์ฃผ๋ฏผ๋ฒˆํ˜ธ ํƒ์ง€

front_parts, back_parts = [], []
 
for bbox, text, conf in ocr_results:
    clean_text = re.sub(r'[- ]', '', text)
    if rrn_front_regex.search(clean_text) and len(clean_text) == 6:
        front_parts.append({'bbox': bbox, 'text': text, 'conf': conf})
    elif rrn_back_regex.search(clean_text) and len(clean_text) == 7:
        back_parts.append({'bbox': bbox, 'text': text, 'conf': conf})

re.sub(pattern, replace, text)

text ์ค‘ pattern์— ํ•ด๋‹นํ•˜๋Š” ๋ถ€๋ถ„์„ replace๋กœ ๋Œ€์ฒดํ•˜๋Š” ๋ฉ”์„œ๋“œ.

  • clean_text = re.sub(r'[- ]', '', text): ํ•˜์ดํ”ˆ(-)์ด๋‚˜ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐํ•ด์„œ ์ˆซ์ž๋งŒ ๋‚จ๊น€ โ†’ "123456 -"์™€ ๊ฐ™์€ ํ…์ŠคํŠธ๋ฅผ "123456"์œผ๋กœ ๋ณ€ํ™˜
  • **rrn_front_regex(6์ž๋ฆฌ)์™€ rrn_back_regex(7์ž๋ฆฌ)**์— ์˜ํ•ด ์ˆซ์ž ๊ธธ์ด์™€ ํŒจํ„ด์„ ๋งŒ์กฑํ•˜๋Š” ํ•ญ๋ชฉ์„ ๊ฐ๊ฐ front_parts์™€ back_parts์— ๋ณด๊ด€
  • ๊ฐ ํ•ญ๋ชฉ์€ {'bbox': bbox, 'text': text, 'conf': conf} ํ˜•ํƒœ๋กœ ์ €์žฅ๋˜์–ด ์ขŒํ‘œ์™€ ์›๋ฌธ์„ ์œ ์ง€

์•ž/๋’ค ํŽ˜์–ด๋ง ๋ฐ bbox ๊ฒฐํ•ฉ

masked_fronts = set()
masked_backs = set()
 
for i, front in enumerate(front_parts):
    if i in masked_fronts:
        continue
        
    front_xs = [p[0] for p in front['bbox']]
    front_ys = [p[1] for p in front['bbox']]
    front_x_max = max(front_xs)
    front_y_center = (min(front_ys) + max(front_ys)) / 2
    
    for j, back in enumerate(back_parts):
        if j in masked_backs:
            continue
            
        back_xs = [p[0] for p in back['bbox']]
        back_ys = [p[1] for p in back['bbox']]
        back_x_min = min(back_xs)
        back_y_center = (min(back_ys) + max(back_ys)) / 2
        
        x_distance = back_x_min - front_x_max
        y_diff = abs(front_y_center - back_y_center)
        
        if 0 <= x_distance <= MAX_X_DISTANCE and y_diff <= MAX_Y_DIFFERENCE:  
            combined_bbox = combine_bboxes(front['bbox'], back['bbox'])
            image = mask_bbox(image, combined_bbox, method=MASK_METHOD)
            found += 1
            # ์ค‘๋ณต ๋ฐฉ์ง€  
            masked_fronts.add(i)
            masked_backs.add(j)
            
            combined_text = f"{front['text']}-{back['text']}"  
            print(f"[MASKED] Combined (Split) RRN='{combined_text}' | dist={x_distance:.1f} | y_diff={y_diff:.1f}")
            break

โ„น๏ธ ๊ธฐ๋Šฅ

  • โ€œ์•ž ์ˆซ์ž ๋ฐ•์Šค์˜ ์˜ค๋ฅธ์ชฝ ๋๊ณผ, ๋’ค ์ˆซ์ž ๋ฐ•์Šค์˜ ์™ผ์ชฝ ์‹œ์ž‘์ด ๊ฐ€๊น๊ณ (์ˆ˜ํ‰ ๊ฑฐ๋ฆฌ), ๋‘ ๋ฐ•์Šค์˜ ์„ธ๋กœ ์ค‘์‹ฌ์ด ๊ฑฐ์˜ ๊ฐ™์€ ๊ฒฝ์šฐ(๊ฐ™์€ ํ–‰)โ€, ์ด๋ฅผ ํ•˜๋‚˜์˜ ์ฃผ๋ฏผ๋ฒˆํ˜ธ๋กœ ํŒ๋‹จํ•œ๋‹ค.

set()

Python ๋‚ด์žฅ ํ•จ์ˆ˜๋กœ, ์ง‘ํ•ฉ์— ๊ด€๋ จ๋œ ๊ฒƒ์„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

abs(x)

Python ๋‚ด์žฅ ํ•จ์ˆ˜๋กœ, x์˜ ์ ˆ๋Œ“๊ฐ’ ๋ฐ˜ํ™˜


  • front_x_max = max(fronts_xs): ์•ž ๋ฐ•์Šค์˜ ์˜ค๋ฅธ์ชฝ ๋(x ์ขŒํ‘œ)
  • back_x_min = min(back_xs): ๋’ค ๋ฐ•์Šค์˜ ์™ผ์ชฝ ์‹œ์ž‘(x์ขŒํ‘œ)
  • x_distance = back_x_min - front_x_max
    • ์ด ๊ฐ’์ด 0 ์ด์ƒ์ด๋ฉด ๋’ค ๋ฐ•์Šค๊ฐ€ ์•ž ๋ฐ•์Šค ์˜ค๋ฅธ์ชฝ์— ์œ„์น˜(๋…ผ๋ฆฌ์  ๋™์น˜)
    • โ‡ MAX_X_DISTANCE ์ด๋ฉด ๋ถ™์–ด ์žˆ๋‹ค(ํ•˜์ดํ”ˆ์ด๋‚˜ ๊ณต๋ฐฑ ์ •๋„์˜ ๊ฑฐ๋ฆฌ).
  • front_y_center, back_y_center: ๊ฐ ๋ฐ•์Šค์˜ ์ˆ˜์ง ์ค‘์‹ฌ.
    • y_diff = abs(...)๊ฐ€ โ‡ MAX_Y_DISTANCE์ด๋ฉด ๊ฐ™์€ ํ–‰์— ์žˆ๋‹ค๊ณ  ์ทจ๊ธ‰.

๊ฒฐ๊ณผ ์ €์žฅ

fname = os.path.basename(image_path)
out_path = os.path.join(OUTPUT_DIR, f"EasyOCR_masked_{MASK_METHOD}_{fname}")
cv2.imwrite(out_path, image)
print(f"์™„๋ฃŒ: {out_path} (์ด {found}๊ฐœ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ์ฒ˜๋ฆฌ)")
return fou
  • ๋งˆ์Šคํ‚น ์™„๋ฃŒ๋œ ์ด๋ฏธ์ง€๋ฅผ OUTPUT_DIR์— ์ €์žฅ
  • ํŒŒ์ผ๋ช…์— MASK_METHOD๋ฅผ ํฌํ•จ์‹œ์ผœ ๊ตฌ๋ถ„

๋ฉ”์ธ ํ•จ์ˆ˜

main ํ•จ์ˆ˜

def main():

โญ ์ด ํ•จ์ˆ˜๊ฐ€ ํ•˜๋Š” ์ผ

  1. OCR Reader ์ดˆ๊ธฐํ™”๋ถ€ํ„ฐ ์ด๋ฏธ์ง€ ํด๋” ์ „์ฒด ์ˆœํšŒ
  2. process_image() ํ˜ธ์ถœ
  3. ํ†ต๊ณ„ ์ถœ๋ ฅ๊นŒ์ง€ ์ „์ฒด ๋งˆ์Šคํ‚น ํŒŒ์ดํ”„๋ผ์ธ์„ ์ œ์–ดํ•˜๋Š” ์ง„์ž…์ (entry point) ํ•จ์ˆ˜

EasyOCR Reader ์ดˆ๊ธฐํ™”

reader = easyocr.Reader(OCR_LANGS, gpu=True)
  • EasyOCR์˜ Reader ๊ฐ์ฒด ์ดˆ๊ธฐํ™”

์ด๋ฏธ์ง€ ๋ชฉ๋ก ์ˆ˜์ง‘

img_files = []  
for ext in ("*.jpg", "*.jpeg", "*.png"):  
    img_files.extend(glob(os.path.join(INPUT_DIR, ext)))
    
if not img_files:  
    print(f"[INFO] ์ž…๋ ฅ ํด๋”์— ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค: {INPUT_DIR}")  
    return
  • ์ง€์ •๋œ INPUT_DIR์—์„œ ์ด๋ฏธ์ง€ ํŒŒ์ผ ๋ชจ๋‘ ์ˆ˜์ง‘
  • JPG, JPEG, PNG ํ™•์žฅ์ž๋งŒ ์ธ์‹ ๋Œ€์ƒ์œผ๋กœ ํฌํ•จ.
  • ์ž…๋ ฅ ํด๋”์— ์ด๋ฏธ์ง€๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ์กฐ๊ธฐ ์ข…๋ฃŒ

์ „์ฒด ํŒŒ์ผ ๋ฃจํ”„ ๋ฐ ์ฒ˜๋ฆฌ

total_masked = 0
for img_path in img_files:  
    print(f"\n์ฒ˜๋ฆฌ ์ค‘: {img_path}")  
    total_masked += process_image(img_path, reader)  
 
print(f"\n=== ์ „์ฒด ์™„๋ฃŒ ===\n์ด {len(img_files)}์žฅ ์ฒ˜๋ฆฌ, {total_masked}๊ฐœ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ์ฒ˜๋ฆฌ")
  • ์ˆ˜์ง‘ํ•œ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋“ค์„ ํ•˜๋‚˜์”ฉ ์ˆœํšŒํ•˜๋ฉฐ ์ฒ˜๋ฆฌ
  • process_image() ํ•จ์ˆ˜์— ๊ฐ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ์™€ reader ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ
  • ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•œ โ€œ๋งˆ์Šคํ‚น๋œ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ๊ฐœ์ˆ˜โ€(found)๋ฅผ ๋ˆ„์ 
  • ๋งˆ์ง€๋ง‰์œผ๋กœ ์ „์ฒด ์š”์•ฝ ๊ฒฐ๊ณผ๋ฅผ ์ฝ˜์†”์— ์ถœ๋ ฅ

์Šคํฌ๋ฆฝํŠธ ์ง„์ž…์ 

if __name__ == "__main__":  
    main()

๋!


PaddleOCR

Import Library

import re
import os
import cv2
import numpy as np
from paddleocr import PaddleOCR
from PIL import Image, ImageDraw, ImageFont
# import paddle
  • paddleocr.PaddleOCR: OCR ๋ชจ๋ธ ๋กœ๋“œยท์˜ˆ์ธก
  • paddle: GPU ๋ฉ”๋ชจ๋ฆฌ ์กฐํšŒํ•˜๋ ค๋ฉด paddle ํ•„์š” (ํ•„์ˆ˜ x)

(์ฃผ์„ ์ฒ˜๋ฆฌ๋œ) GPU ๋ฉ”๋ชจ๋ฆฌ ์ถœ๋ ฅ ์ฝ”๋“œ

# GPU ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ
# mem_alloc = paddle.device.cuda.memory_allocated()
# mem_reserved = paddle.device.cuda.max_memory_reserved()
# print(f"์‹œ์ž‘ ์ „ ๋ฉ”๋ชจ๋ฆฌ: {mem_alloc/1024**2:.2f} MB")
# print(f"์ตœ๋Œ€ ์˜ˆ์•ฝ๋œ ๋ฉ”๋ชจ๋ฆฌ: {mem_reserved / 1024**2:.2f} MB")
  • GPU ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ถœ๋ ฅํ•˜๋Š” ์ฝ”๋“œ(์ฃผ์„ ์ฒ˜๋ฆฌ๋จ).

OCR ์ดˆ๊ธฐํ™”

ocr = PaddleOCR(
    lang='korean',
    use_doc_orientation_classify=False,
    use_doc_unwarping=False,
    use_textline_orientation=False,
    device='gpu:0', # 'cpu' ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
)
  • PaddleOCR ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ชจ๋ธ์„ ๋กœ๋“œํ•œ๋‹ค.

paddleocr.PaddleOCR()

  • lang
  • use_doc_orientation_classify
    • ๋ฌธ์„œ ๋ฐฉํ–ฅ ๋ถ„๋ฅ˜ ๋ชจ๋“ˆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€. ๊ธฐ๋ณธ๊ฐ’์€ True.
    • ์Šค์บ”ํ•œ ๋ฌธ์„œ ์ด๋ฏธ์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€(ํšŒ์ „๋˜์–ด ์žˆ๋Š”์ง€) ์ž๋™์œผ๋กœ ํŒ๋ณ„.
  • use_doc_unwarping
    • text image unwarping ๋ชจ๋“ˆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€. ๊ธฐ๋ณธ๊ฐ’์€ True.
    • โ€œtext image unwarpingโ€์ด๋ž€ ๊ตฌ๋ถ€๋Ÿฌ์ง€๊ฑฐ๋‚˜ ์™œ๊ณก๋œ ๋ฌธ์„œ ์ด๋ฏธ์ง€๋ฅผ ํ‰ํ‰ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ํ˜•ํƒœ๋กœ ๋ณต์›ํ•˜๋Š” ๊ธฐ์ˆ ์„ ๋œปํ•œ๋‹ค.
  • use_textline_orientation
    • ํ…์ŠคํŠธ ๋ฐฉํ–ฅ ๋ถ„๋ฅ˜ ๋ชจ๋“ˆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€. ๊ธฐ๋ณธ๊ฐ’์€ True
    • ์ด๋ฏธ์ง€ ์† ํ…์ŠคํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€(ํšŒ์ „๋˜์–ด ์žˆ๋Š”์ง€) ์ž๋™์œผ๋กœ ํŒ๋ณ„.

๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•œ ๋” ์ž์„ธํ•œ ๋‚ด์šฉ์€ PaddleOCR Documentation ์ฐธ์กฐ.

  • use_doc_orientation_classify
    • ๋ฌธ์„œ ๋ฐฉํ–ฅ ๋ถ„๋ฅ˜ ๋ชจ๋“ˆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜. ๊ธฐ๋ณธ๊ฐ’์€ True.
    • ์Šค์บ”ํ•œ ๋ฌธ์„œ ์ด๋ฏธ์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€(ํšŒ์ „๋˜์–ด ์žˆ๋Š”์ง€) ์ž๋™์œผ๋กœ ํŒ๋ณ„.
  • use_doc_unwarping
    • text image unwarping ๋ชจ๋“ˆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜. ๊ธฐ๋ณธ๊ฐ’์€ True.
    • โ€œtext image unwarpingโ€์ด๋ž€ ๊ตฌ๋ถ€๋Ÿฌ์ง€๊ฑฐ๋‚˜ ์™œ๊ณก๋œ ๋ฌธ์„œ ์ด๋ฏธ์ง€๋ฅผ ํ‰ํ‰ํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅธ ํ˜•ํƒœ๋กœ ๋ณต์›ํ•˜๋Š” ๊ธฐ์ˆ ์„ ๋œปํ•œ๋‹ค.
  • use_textline_orientation
    • ํ…์ŠคํŠธ ๋ฐฉํ–ฅ ๋ถ„๋ฅ˜ ๋ชจ๋“ˆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜. ๊ธฐ๋ณธ๊ฐ’์€ True
    • ์ด๋ฏธ์ง€ ์† ํ…์ŠคํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์ธ์ง€(ํšŒ์ „๋˜์–ด ์žˆ๋Š”์ง€) ์ž๋™์œผ๋กœ ํŒ๋ณ„.

์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ์ •๊ทœ์‹

patterns = [
    re.compile(r"\b\d{6}\s*-\s*\d{7}\b"),
    re.compile(r"\b\d{13}\b")
]

๋งˆ์Šคํ‚น ํ•จ์ˆ˜๋“ค

paddleocr.PaddleOCR.predict๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” dt_polys(ํ…์ŠคํŠธ ๋ฐ”์šด๋”” ๋ฐ•์Šค ์ขŒํ‘œ, 4๊ฐœ ๊ผญ์ง“์ ) ํฌ๋งท์„ ๊ทธ๋Œ€๋กœ ๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์•„๋ž˜์— ํ›„์ˆ ํ•  ๊ฐ ํ•จ์ˆ˜๋Š” ํด๋ฆฌ๊ณค์„ ๋งˆ์Šคํฌ๋กœ ๋งŒ๋“ค๊ณ  ๊ทธ ๋‚ด๋ถ€๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋งˆ์Šคํ‚น ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋ฉฐ, EasyOCR์˜ ๋งˆ์Šคํ‚น ํ•จ์ˆ˜๋“ค๊ณผ ๊ฑฐ์˜ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค.


๊ฒ€์ •์ƒ‰ ์ฑ„์šฐ๊ธฐ

def mask_black(img, box):
    pts = np.array(box, dtype=np.int32)
    cv2.fillPoly(img, [pts], color=(0,0,0))
    return img
  • ์ฃผ์–ด์ง„ box ํด๋ฆฌ๊ณค ๋‚ด๋ถ€๋ฅผ (B,G,R)=(0,0,0)์œผ๋กœ ์ฑ„์›€.

๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ

def mask_blur(img, box):
    pts = np.array(box, dtype=np.int32)
    mask = np.zeros(img.shape[:2], dtype=np.uint8)
    cv2.fillPoly(mask, [pts], 255)
    x, y, w, h = cv2.boundingRect(pts)
    roi = img[y:y+h, x:x+w]
    roi_mask = mask[y:y+h, x:x+w]
    if roi.size > 0:
        kernel = (51, 51) # kernel: ๋ธ”๋Ÿฌ ๊ฐ•๋„ ์กฐ์ ˆ (ํ™€์ˆ˜๊ฐ’ ์ž…๋ ฅ, ๊ฐ’์ด ํด์ˆ˜๋ก ๊ฐ•๋„ โ†‘)
        blurred = cv2.GaussianBlur(roi, kernel, 30)
        roi[roi_mask==255] = blurred[roi_mask==255]
        img[y:y+h, x:x+w] = roi
    return img

EasyOCR์˜ ๋ธ”๋Ÿฌ ๋งˆ์Šคํ‚น๊ณผ ๋™์ผ

  • mask๋ฅผ ๋งŒ๋“ค์–ด ROI์—๋งŒ ๋ธ”๋Ÿฌ๋ฅผ ์ ์šฉ.
  • kernel์€ ๋ฐ˜๋“œ์‹œ ํ™€์ˆ˜. ๊ฐ’์ด ํด์ˆ˜๋ก ๋ธ”๋Ÿฌ ๊ฐ•ํ•ด์ง. (51์€ ๋งค์šฐ ๊ฐ•ํ•จ)

๋ชจ์ž์ดํฌ ์ฒ˜๋ฆฌ

def mask_mosaic(img, box, ratio=0.05): # ratio: ๋ชจ์ž์ดํฌ ๊ฐ•๋„ ์กฐ์ ˆ (๊ฐ’์ด ๋‚ฎ์„์ˆ˜๋ก ๊ฐ•๋„ โ†‘)
    pts = np.array(box, dtype=np.int32)
    mask = np.zeros(img.shape[:2], dtype=np.uint8)
    cv2.fillPoly(mask, [pts], 255)
    x, y, w, h = cv2.boundingRect(pts)
    roi = img[y:y+h, x:x+w]
    roi_mask = mask[y:y+h, x:x+w]
    if roi.size > 0:
        small = cv2.resize(roi, (max(1,int(w*ratio)), max(1,int(h*ratio))), interpolation=cv2.INTER_LINEAR)
        mosaic = cv2.resize(small, (w,h), interpolation=cv2.INTER_NEAREST)
        roi[roi_mask==255] = mosaic[roi_mask==255]
        img[y:y+h, x:x+w] = roi
    return img

EasyOCR์˜ ๋ชจ์ž์ดํฌ ๋งˆ์Šคํ‚น๊ณผ ๋™์ผ

ํ…์ŠคํŠธ๋กœ ๋Œ€์ฒด (ํšŒ์ „ ๋ฐ˜์˜)

def mask_replace_text(img, box, text="[ REDACTED ]"):
        pts = np.array(box, dtype=np.int32)
        x, y, w, h = cv2.boundingRect(pts)
        center_x = x + w // 2
        center_y = y + h // 2
        cv2.fillPoly(img, [pts], (255, 255, 255))
        max_len = 0
        angle = 0
        for i in range(4):
            dx = pts[(i+1)%4][0] - pts[i][0]
            dy = pts[(i+1)%4][1] - pts[i][1]
            length = np.hypot(dx, dy)
            if length > max_len:
                max_len = length
                angle = -np.degrees(np.arctan2(dy, dx))
        pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        draw = ImageDraw.Draw(pil_img)
        diagonal = np.hypot(w, h)
        font_size = min(max(int(diagonal * 0.1), 12), h)
        try:
            font = ImageFont.truetype("malgunbd.ttf", font_size) # ๋ง‘์€ ๊ณ ๋”• bold
        except:
            font = ImageFont.load_default()
        bbox_text = draw.textbbox((0, 0), text, font=font)
        text_w = bbox_text[2] - bbox_text[0]
        text_h = bbox_text[3] - bbox_text[1]
        offset = 5 # ํ…์ŠคํŠธ ์œ„๋กœ ์˜ฌ๋ฆด ํ”ฝ์…€ ์ˆ˜
        text_x = center_x - text_w / 2
        text_y = center_y - text_h / 2 - offset
        text_layer = Image.new("RGBA", pil_img.size, (255, 255, 255, 0))
        text_draw = ImageDraw.Draw(text_layer)
        text_draw.text((text_x, text_y), text, font=font, fill=(255, 0, 0, 255))
        rotated_layer = text_layer.rotate(angle, center=(center_x, center_y), resample=Image.BICUBIC)
        pil_img = Image.alpha_composite(pil_img.convert("RGBA"), rotated_layer)
        img[:] = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)
 
        return img

EasyOCR์˜ ํ…์ŠคํŠธ ๊ต์ฒด ๋งˆ์Šคํ‚น๊ณผ ๋™์ผ


๋งˆ์Šคํ‚น ๋ฉ”์„œ๋“œ ๋งคํ•‘

masking_methods = {
    "black": mask_black,
    "blur": mask_blur,
    "mosaic": mask_mosaic,
    "replace_text": mask_replace_text
}
  • ๋ฌธ์ž์—ด๋กœ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์„ ํƒ์„ ์‰ฝ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด ๋”•์…”๋„ˆ๋ฆฌ ๋งคํ•‘(dictionary mapping).
  • ํ›„์ˆ ํ•  masking ์ธ์ž๊ฐ’ ๊ฐ„ํŽธํ•˜๊ฒŒ ์„ ํƒ ๊ฐ€๋Šฅ.

์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜

def process_image(img_path, save_path, masking="black"):
    img = cv2.imread(img_path)
    if img is None:
        print(f"์ด๋ฏธ์ง€๋ฅผ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {img_path}")
        return
 
    result = ocr.predict(img_path)[0]  # result[0] ์‚ฌ์šฉ
    rec_texts = result['rec_texts']
    #print(rec_texts) # OCRํ•œ ํ…์ŠคํŠธ๋ฅผ ์ถœ๋ ฅํ•ด๋ณด๊ณ  ์‹ถ์œผ๋ฉด ์ฃผ์„์„ ์ง€์šฐ์„ธ์šฉ
    dt_polys = result['dt_polys']
 
    for text, box in zip(rec_texts, dt_polys):
        if any(p.search(text) for p in patterns):
            if masking in masking_methods:
                img = masking_methods[masking](img, box)
 
    cv2.imwrite(save_path, img)
    print(f"์ €์žฅ ์™„๋ฃŒ: {save_path}")
  • cv2.imread(img_path): ์ด๋ฏธ์ง€ ๋กœ๋“œ (BGR numpy array).

  • if img is None: ํŒŒ์ผ ์ฝ๊ธฐ ์‹คํŒจ ์ฒดํฌ.

  • result = ocr.predict(img_path)[0]: OCR ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ. (์‚ฌ์šฉํ•˜์‹  ๋ฒ„์ „์—์„œ predict๊ฐ€ path๋ฅผ ๋ฐ›์•„์„œ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค๊ณ  ๊ฐ€์ •)

  • rec_texts = result['rec_texts'], dt_polys = result['dt_polys'] : ํ…์ŠคํŠธ ๋ชฉ๋ก๊ณผ ๊ฐ ํ…์ŠคํŠธ์˜ ํด๋ฆฌ๊ณค ์ขŒํ‘œ ์ถ”์ถœ.

  • for text, box in zip(...): ํ…์ŠคํŠธ์™€ ์ขŒํ‘œ๋ฅผ ๋ฌถ์–ด ์ˆœํšŒ.

  • if any(p.search(text) for p in patterns): ์ •๊ทœ์‹์œผ๋กœ ์ฃผ๋ฏผ๋“ฑ๋ก๋ฒˆํ˜ธ ํŒจํ„ด์„ ์ฐพ์œผ๋ฉด true.

  • img = masking_methods[masking](img, box): ์„ ํƒํ•œ ๋งˆ์Šคํ‚น ํ•จ์ˆ˜๋กœ ์ด๋ฏธ์ง€ ๊ฐฑ์‹ .

  • cv2.imwrite(save_path, img): ๊ฒฐ๊ณผ ์ €์žฅ.


๋ฉ”์ธ ์‹คํ–‰ ๋ธ”๋ก

if __name__ == "__main__":
    input_dir = "idc_samples"
    output_dir = "idc_results"
    os.makedirs(output_dir, exist_ok=True)
 
    # ์„ ํƒ: "black", "blur", "mosaic", "replace_text"
    masking_type = "black"
 
    for file in os.listdir(input_dir):
        if file.lower().endswith((".jpg", ".jpeg", ".png")):
            input_path = os.path.join(input_dir, file)
            output_path = os.path.join(output_dir, f"PaddleOCR_{masking_type}_{file}")
            process_image(input_path, output_path, masking=masking_type)
  • ์ž…๋ ฅ/์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ์ง€์ • ๋ฐ ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ.

  • masking_type์œผ๋กœ ๋งˆ์Šคํ‚น ๋ฐฉ์‹ ์„ ํƒ.

  • ์ž…๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ์˜ ์ด๋ฏธ์ง€๋“ค์„ ์ˆœํšŒํ•˜๋ฉด์„œ process_image ํ˜ธ์ถœ.