๐Ÿ•ต๏ธ ์„œ๋น„์Šค๋กœ ์–ผ๊ตด์„ ๊ฒ€์‚ฌํ•˜๋‹ค๐Ÿ

๐Ÿ‘‹ ์•ˆ๋…•ํ•˜์„ธ์š”.


์ด ๋ฌธ์„œ์—์„œ๋Š” ๋‹ค์Œ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์‚ฌ์ง„ ์† ์‚ฌ๋žŒ์˜ ์–ผ๊ตด์„ ์–ด๋–ป๊ฒŒ ๊ฒ€์ถœํ•˜๋Š”์ง€,
  • ์–ด๋–ป๊ฒŒ ์ž๋™์œผ๋กœ ์ต๋ช…, ์žฌ๋‹จ... ์‚ฌ๋žŒ์˜ ์–ผ๊ตด์ด ๋‹ด๊ธด ์‚ฌ์ง„,
  • ์„œ๋ฒ„ ์—†๋Š” ์˜จ๋ผ์ธ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์œผ๋กœ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•
  • 300์ค„ ๋ฏธ๋งŒ์˜ Python ์ฝ”๋“œ์— ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๊ฒƒ์€ ์ €๋ช…ํ•œ ์–ผ๊ตด๋กœ ์ž๋™ ์ต๋ช…๊ณผ ์žฌ๋‹จ๋˜์—ˆ๋‹ค.์ด๊ฒŒ ๋ˆ„๊ตฐ์ง€ ์•Œ์•„๋งžํ˜€ ๋ด?

    Note: We're talking about face detection, not face recognition. Though technically possible, face recognition can have harmful applications. Responsible companies have established AI principles and avoid exposing such potentially harmful technologies (e.g. Google AI Principles).


    ๐Ÿ› ๏ธ ๋„๊ตฌ


    ๋ช‡ ๊ฐ€์ง€ ๋„๊ตฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜๋Š” ๊ธฐ๊ณ„ ํ•™์Šต ๋ชจ๋ธ,
  • ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ,
  • ์›น ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ํ”„๋ ˆ์ž„์›Œํฌ
  • ๊ฐ€์žฅ ์ €๋ ดํ•œ ๋น„์šฉ์œผ๋กœ ์—ฐ์ค‘๋ฌดํœด 24์‹œ๊ฐ„ ํ”„๋ ˆ์  ํ…Œ์ด์…˜์„ ์ œ๊ณตํ•˜๋Š” ์„œ๋ฒ„ ์—†๋Š” ์†”๋ฃจ์…˜
  • ๐Ÿงฑ ๊ฑด์ถ•ํ•™


    ์ด๊ฒƒ์€ 2๊ฐœ์˜ ๊ตฌ๊ธ€ ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ตฌ์กฐ(App Engine+Vision API:

    ์›Œํฌํ”Œ๋กœ์šฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์—ด๊ธฐ: ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ์—”์ง„์ด ํ™ˆํŽ˜์ด์ง€์— ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ์…€์นด: ํ”„๋ŸฐํŠธ์—”๋“œ์—์„œ /analyze-image๋‹จ์ ์œผ๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  • ๋ฐฑ์—”๋“œ์—์„œ Vision API์— ์š”์ฒญ: ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜๊ณ  ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค(์ฐธ๊ณ ).
  • ๋ฐฑ์—”๋“œ์—์„œ ์ฃผ์„๊ณผ ๊ฒ€์ถœ๋œ ๋ฉด์˜ ์ˆ˜๋ฅผ ๋˜๋Œ๋ ค์ค๋‹ˆ๋‹ค. (์›น ํŽ˜์ด์ง€์— ์ง์ ‘ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.)
  • ํ”„๋ŸฐํŠธ์—”๋“œ์—์„œ ์ด๋ฏธ์ง€, ์ฃผ์„ ๋ฐ ์ฒ˜๋ฆฌ ์˜ต์…˜์„ /process-image ์—”๋“œํฌ์ธํŠธ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฐฑ์—”๋“œ์—์„œ ์ฃผ์–ด์ง„ ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€๋ฅผ ๋˜๋Œ๋ ค์ค๋‹ˆ๋‹ค.
  • ์˜ต์…˜ ๋ณ€๊ฒฝ: 5๋‹จ๊ณ„์™€ 6๋‹จ๊ณ„๋ฅผ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.
  • ์ƒˆ ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • ์ด๊ฒƒ์€ ๋งŽ์€ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ ์ค‘์˜ ํ•˜๋‚˜๋‹ค.์ด ๋ฐฉ๋ฒ•์˜ ์žฅ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.
  • ์›น ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ ์…€์นด์™€ ์ฃผ์„: ์ €์žฅ๊ณผ ๊ด€๋ จ์ด ์—†๊ณ  ๊ตฌ๋ฆ„ ์–ด๋Š ๊ณณ์—๋„ ๊ฐœ์ธ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฏธ์ง€๋‹น Vision API๋Š” ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • ๐Ÿ Python ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ


    ๊ตฌ๊ธ€ ํด๋ผ์šฐ๋“œ ๋น„์ „

  • Vision API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋ด‰์ธํ•ฉ๋‹ˆ๋‹ค.
  • https://pypi.org/project/google-cloud-vision
  • ๋ฒ ๊ฐœ

  • ๋งค์šฐ ์œ ํ–‰ํ•˜๋Š” ์ด๋ฏธ์ง€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋‚ด์šฉ์ด ๊ด‘๋ฒ”์œ„ํ•˜๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์‰ฝ๋‹ค.
  • https://pypi.org/project/Pillow
  • ๋ณ‘.

  • ๊ฐ€์žฅ ์œ ํ–‰ํ•˜๋Š” ์›น ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ํ”„๋ ˆ์ž„์›Œํฌ ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค.
  • https://pypi.org/project/Flask
  • ์˜์กดํ•ญ

    requirements.txt ํŒŒ์ผ์—์„œ ์ข…์†์„ฑ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    google-cloud-vision==2.4.4
    
    Pillow==8.3.2
    
    Flask==2.0.2
    

    Notes:

    • As a best practice, also specify the dependency versions. This freezes your production environment in a known state and prevents newer versions from potentially breaking future deployments.
    • App Engine will automatically deploy these dependencies.

    ๐Ÿง  ์ด๋ฏธ์ง€ ๋ถ„์„


    Vision API


    Vision API๋Š” ์ด๋ฏธ์ง€ ๋ถ„์„์— ์‚ฌ์šฉ๋˜๋Š” ๊ฐ€์žฅ ์„ ์ง„์ ์ธ ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํŠน์ง• ์ค‘ ํ•˜๋‚˜๋Š” ์–ผ๊ตด ๊ฒ€์‚ฌ๋‹ค.๋‹ค์Œ์€ ์ด๋ฏธ์ง€์—์„œ ์‚ฌ๋žŒ์˜ ์–ผ๊ตด์„ ๊ฐ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
    from google.cloud import vision_v1 as vision
    
    Annotations = vision.AnnotateImageResponse
    MAX_DETECTED_FACES = 50
    
    def detect_faces(image_bytes: bytes) -> Annotations:
        client = vision.ImageAnnotatorClient()
        api_image = vision.Image(content=image_bytes)
        return client.face_detection(api_image, max_results=MAX_DETECTED_FACES)
    

    ๋ฐฑ์—”๋“œ ๋์ 


    Flask๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ API ๋์ ์„ ๊ณต๊ฐœํ•˜๋ฉด ๋ผ์šฐํŒ… ํŒจํ‚ค์ง€ ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.๋‹ค์Œ์€ ๊ฐ€๋Šฅํ•œ POST ๋์ ์ž…๋‹ˆ๋‹ค.
    import base64
    
    import flask
    
    app = flask.Flask(__name__)
    
    @app.post("/analyze-image")
    def analyze_image():
        image_file = flask.request.files.get("image")
        annotations = detect_faces(image_file.read())
    
        return flask.jsonify(
            faces_detected=len(annotations.face_annotations),
            annotations=encode_annotations(annotations),
        )
    
    def encode_annotations(annotations: Annotations) -> str:
        binary_data = Annotations.serialize(annotations)
        base64_data = base64.urlsafe_b64encode(binary_data)
        base64_annotations = base64_data.decode("ascii")
        return base64_annotations
    

    ํ”„๋ŸฐํŠธ์—”๋“œ ์š”์ฒญ


    ๋‹ค์Œ์€ ํ”„๋ŸฐํŠธ์—”๋“œ์—์„œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” javascript ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
    async function analyzeImage(imageBlob) {
      const formData = new FormData();
      formData.append("image", imageBlob);
      const params = { method: "POST", body: formData };
      const response = await fetch("/analyze-image", params);
    
      return response.json();
    }
    

    ๐ŸŽจ ํ™”์ƒ ์ฒ˜๋ฆฌ


    ๋ฉด ๊ฒฝ๊ณ„ ์ƒ์ž ๋ฐ ์ง€ํ‘œ


    ์‹œ๊ฐ API๋Š” ๊ฒ€์ถœ๋œ ์‚ฌ๋žŒ์˜ ์–ผ๊ตด์˜ ๊ฒฝ๊ณ„ ์ƒ์ž์™€ 30์—ฌ ๊ฐœ์˜ ์–ผ๊ตด ํ‘œ์ง€์ (์ž…, ์ฝ”, ๋ˆˆ ๋“ฑ)์˜ ์œ„์น˜๋ฅผ ์ œ๊ณตํ•œ๋‹ค.๋‹ค์Œ์€ ๋ฒ ๊ฐœ(PIL)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
    from PIL import Image, ImageDraw
    
    PilImage = Image.Image
    ANNOTATION_COLOR = "#00FF00"
    ANNOTATION_LANDMARK_DIM_PERMIL = 8
    ANNOTATION_LANDMARK_DIM_MIN = 4
    
    def draw_face_landmarks(image: PilImage, annotations: Annotations):
        r_half = min(image.size) * ANNOTATION_LANDMARK_DIM_PERMIL // 1000
        r_half = max(r_half, ANNOTATION_LANDMARK_DIM_MIN) // 2
        border = max(r_half // 2, 1)
    
        draw = ImageDraw.Draw(image)
        for face in annotations.face_annotations:
            v = face.bounding_poly.vertices
            r = (v[0].x, v[0].y, v[2].x + 1, v[2].y + 1)
            draw.rectangle(r, outline=ANNOTATION_COLOR, width=border)
    
            for landmark in face.landmarks:
                x = int(landmark.position.x + 0.5)
                y = int(landmark.position.y + 0.5)
                r = (x - r_half, y - r_half, x + r_half + 1, y + r_half + 1)
                draw.rectangle(r, outline=ANNOTATION_COLOR, width=border)
    

    ์ต๋ช…


    ๋‹ค์Œ์€ ๊ฒฝ๊ณ„์„ ์„ ํ†ตํ•ด ์–ผ๊ตด ์ต๋ช…์„ ์‹คํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
    ANONYMIZATION_PIXELS=13
    
    def anonymize_faces(image: PilImage, annotations: Annotations):
        for face in annotations.face_annotations:
            v = face.bounding_poly.vertices
            face = image.crop((v[0].x, v[0].y, v[2].x + 1, v[2].y + 1))
    
            face1_w, face1_h = face.size
            pixel_dim = max(face1_w, face1_h) // ANONYMIZATION_PIXELS
            face2_w, face2_h = face1_w // pixel_dim, face1_h // pixel_dim
            face = face.resize((face2_w, face2_h), Image.NEAREST)
            face = face.resize((face1_w, face1_h), Image.NEAREST)
    
            image.paste(face, (v[0].x, v[0].y))
    

    ๋ฉด์žฌ๋‹จ


    ์ด์™€ ์œ ์‚ฌํ•˜๊ฒŒ ๊ฒ€์ถœ๋œ ๋ฉด์— ์ดˆ์ ์„ ๋งž์ถ”๊ธฐ ์œ„ํ•ด ๋ฉด ์ฃผ์œ„์˜ ๋ชจ๋“  ๋‚ด์šฉ์„ ์žฌ๋‹จํ•  ์ˆ˜ ์žˆ๋‹ค.
    CROP_MARGIN_PERCENT = 10
    
    def crop_faces(image: PilImage, annotations: Annotations) -> PilImage:
        mask = Image.new("L", image.size, 0x00)
        draw = ImageDraw.Draw(mask)
        for face in annotations.face_annotations:
            draw.ellipse(face_crop_box(face), fill=0xFF)
        image.putalpha(mask)
        return image
    
    def face_crop_box(face_annotation) -> tuple[int, int, int, int]:
        v = face_annotation.bounding_poly.vertices
        x1, y1, x2, y2 = v[0].x, v[0].y, v[2].x + 1, v[2].y + 1
        w, h = x2 - x1, y2 - y1
        hx, hy = x1 + w / 2, y1 + h / 2
        m = max(w, h) * (100 + CROP_MARGIN_PERCENT) / 100 / 2
        return int(hx - m + 0.5), int(hy - m + 0.5), int(hx + m + 1.5), int(hy + m + 1.5)
    

    ๐Ÿ’ ํ”ผ์˜ ์ฒด๋ฆฌ.๐Ÿ


    ์ง€๊ธˆ, ์ผ€์ดํฌ ์œ„์˜ ์„คํƒ• ํฌ๋ฆผ(๋˜๋Š” ์šฐ๋ฆฌ๊ฐ€ ํ”„๋ž‘์Šค์–ด๋กœ ๋งํ•˜๋Š”'ํŒŒ์ด ์œ„์˜ ์ฒด๋ฆฌ'):
  • ์—ฌ๋Ÿฌ ์˜ต์…˜์„ ๋™์‹œ์— ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋Š” ๋…๋ฆฝ์ ์ธ ๋ Œ๋”๋ง ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ชจ๋“  ๋ฉด์˜ ๊ฒฝ๊ณ„ ์ƒ์ž๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ์ตœ์†Œ ๊ฒฝ๊ณ„ ์ƒ์ž๋กœ ์žฌ๋‹จํ•  ์ˆ˜ ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ฝ”์™€ ์ž…์˜ ์œ„์น˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ์ฝง์ˆ˜์—ผ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ•จ์ˆ˜์— ๋‹จ์ผ ํ”„๋ ˆ์ž„์„ ๋ Œ๋”๋งํ•˜๋Š” ๋งค๊ฐœ ๋ณ€์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด ๋ช‡ ์ค„ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Flask ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ๋กœ์ปฌ์—์„œ ์‹คํ–‰๋˜๋ฉด ๊ฐ€์žฅ ๋‚ฎ์€ ๋น„์šฉ์œผ๋กœ 24์‹œ๊ฐ„ ๋ฐฐ์น˜ํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋‹ค์Œ์€ ์œ ๋ช…ํ•œ ์‚ฌ์ง„๊ธ‰ ์‹ค๊ฐ ํšŒํ™”์—์„œ ๋ฐœ๊ฒฌํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.
  • ๋ฏธ๊ตญ ๊ณ ๋”• ์–‘์‹
  • ์ง„์ฃผ ๊ท€๊ฑธ์ด๋ฅผ ํ•œ ์—ฌ์ž
  • ์…ฐ์ต์Šคํ”ผ์–ด

  • ๋‹ค์Œ์€ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค.
  • ๋ฏธ๊ตญ ๊ณ ๋”• ์–‘์‹
  • ์ง„์ฃผ ๊ท€๊ฑธ์ด๋ฅผ ํ•œ ์—ฌ์ž
  • ์…ฐ์ต์Šคํ”ผ์–ด

  • ๋ฌผ๋ก  ์ด๊ฒƒ์€ ์‹ค์ œ ์‚ฌ์ง„์—์„œ ๋”์šฑ ํšจ๊ณผ๊ฐ€ ์ข‹๋‹ค.
  • ๊ฐœ์ธ์‚ฌ์ง„(2์„ธ๋ถ€ํ„ฐ 44์„ธ๊นŒ์ง€)
  • ๋„ค, ์ˆ˜์—ผ์„ ๊ธฐ๋ฅธ ์ง€ 42๋…„์ด ๋์–ด์š”. ์ œ ์—ฌ๋™์ƒ๋„์š”.)

  • ๋งˆ์ง€๋ง‰์œผ๋กœ ์—ฌ๊ธฐ๋Š” ์šฐ๋ฆฌ์˜ ์œ ๋ช…ํ•œ ์ต๋ช…์ž…๋‹ˆ๋‹ค. ์ฒ˜์Œ๋ถ€ํ„ฐ
  • ๋ชจ๋‚˜๋ฆฌ์ž

  • ๐Ÿš€ ์†Œ์Šค ์ฝ”๋“œ ๋ฐ ๋ฐฐํฌ

  • ๋ฐฑ์—”๋“œ์˜ Python ์†Œ์Šค ์ฝ”๋“œ๋Š” 300์ค„ ๋ฏธ๋งŒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ด ํ”„๋ ˆ์  ํ…Œ์ด์…˜์€ 4 ๋ถ„ ์ด๋‚ด์— ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ฐธ์กฐ"Deploying from scratch".
  • ๐ŸŽ‰ ์˜จ๋ผ์ธ ๋ฐ๋ชจ

  • ์ง์ ‘ ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ์ˆ˜ํ–‰:
  • โžก๏ธ https://face-detection.lolo.dev โฌ…๏ธ
  • ๋ฏธ๋ฆฌ๋ณด๊ธฐ:

  • ๐Ÿ–– ์•ˆ๋…•ํžˆ ๊ฐ€์„ธ์š”.


    Feedback or questions ? ๋‚˜๋Š” ๋„ˆ์˜ ์ฑ…์„ ๋งค์šฐ ์ฝ๊ณ  ์‹ถ๋‹ค!์ถ”๊ฐ€ ์ •๋ณด...

    โณ ์—…๋ฐ์ดํŠธ

  • 2021 10์›” 8์ผ: ์ตœ์‹  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ + Python 3.7 ์—…๋ฐ์ดํŠธโ†’ 3.9
  • ์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ