๐ต๏ธ ์๋น์ค๋ก ์ผ๊ตด์ ๊ฒ์ฌํ๋ค๐
๐ ์๋ ํ์ธ์.
์ด ๋ฌธ์์์๋ ๋ค์์ ๋ณผ ์ ์์ต๋๋ค.
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).
๐ ๏ธ ๋๊ตฌ
๋ช ๊ฐ์ง ๋๊ตฌ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๐งฑ ๊ฑด์ถํ
์ด๊ฒ์ 2๊ฐ์ ๊ตฌ๊ธ ํด๋ผ์ฐ๋ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ ๊ตฌ์กฐ(App Engine+Vision API:
์ํฌํ๋ก์ฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
/analyze-image
๋จ์ ์ผ๋ก ๋ณด๋
๋๋ค./process-image
์๋ํฌ์ธํธ๋ก ์ ์กํฉ๋๋ค.๐ Python ๋ผ์ด๋ธ๋ฌ๋ฆฌ
๊ตฌ๊ธ ํด๋ผ์ฐ๋ ๋น์
๋ฒ ๊ฐ
๋ณ.
์์กดํญ
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)
๐ ํผ์ ์ฒด๋ฆฌ.๐
์ง๊ธ, ์ผ์ดํฌ ์์ ์คํ ํฌ๋ฆผ(๋๋ ์ฐ๋ฆฌ๊ฐ ํ๋์ค์ด๋ก ๋งํ๋'ํ์ด ์์ ์ฒด๋ฆฌ'):
๋ค์์ ์ ๋๋ฉ์ด์ ๋ฒ์ ์ ๋๋ค.
๋ฌผ๋ก ์ด๊ฒ์ ์ค์ ์ฌ์ง์์ ๋์ฑ ํจ๊ณผ๊ฐ ์ข๋ค.
๋ง์ง๋ง์ผ๋ก ์ฌ๊ธฐ๋ ์ฐ๋ฆฌ์ ์ ๋ช ํ ์ต๋ช ์ ๋๋ค. ์ฒ์๋ถํฐ
๐ ์์ค ์ฝ๋ ๋ฐ ๋ฐฐํฌ
๐ ์จ๋ผ์ธ ๋ฐ๋ชจ
๐ ์๋ ํ ๊ฐ์ธ์.
Feedback or questions ? ๋๋ ๋์ ์ฑ ์ ๋งค์ฐ ์ฝ๊ณ ์ถ๋ค!์ถ๊ฐ ์ ๋ณด...
โณ ์ ๋ฐ์ดํธ
Reference
์ด ๋ฌธ์ ์ ๊ดํ์ฌ(๐ต๏ธ ์๋น์ค๋ก ์ผ๊ตด์ ๊ฒ์ฌํ๋ค๐), ์ฐ๋ฆฌ๋ ์ด๊ณณ์์ ๋ ๋ง์ ์๋ฃ๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋งํฌ๋ฅผ ํด๋ฆญํ์ฌ ๋ณด์๋ค https://dev.to/googlecloud/detecting-faces-as-a-service-27nfํ ์คํธ๋ฅผ ์์ ๋กญ๊ฒ ๊ณต์ ํ๊ฑฐ๋ ๋ณต์ฌํ ์ ์์ต๋๋ค.ํ์ง๋ง ์ด ๋ฌธ์์ URL์ ์ฐธ์กฐ URL๋ก ๋จ๊ฒจ ๋์ญ์์ค.
์ฐ์ํ ๊ฐ๋ฐ์ ์ฝํ ์ธ ๋ฐ๊ฒฌ์ ์ ๋ (Collection and Share based on the CC Protocol.)
์ข์ ์นํ์ด์ง ์ฆ๊ฒจ์ฐพ๊ธฐ
๊ฐ๋ฐ์ ์ฐ์ ์ฌ์ดํธ ์์ง
๊ฐ๋ฐ์๊ฐ ์์์ผ ํ ํ์ ์ฌ์ดํธ 100์ ์ถ์ฒ ์ฐ๋ฆฌ๋ ๋น์ ์ ์ํด 100๊ฐ์ ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ๋ฐ์ ํ์ต ์ฌ์ดํธ๋ฅผ ์ ๋ฆฌํ์ต๋๋ค