Search
Duplicate

Vector Search - 영화 속 얼굴 검색

문서번호 : 11-1680343

Document Information

최초 작성일 : 2023.10.10
최종 수정일 : 2023.10.25
이 문서는 아래 버전을 기준으로 작성되었습니다.
SinglestoreDB : 8.1.2

Goal

SingleStoreDB 의 vector 함수인 dot_product 를 활용하여 이미지에서 인식한 얼굴의 유사도 비교

Step

1.
영화 영상 파일에서 프레임을 추출하여, 프레임(이미지)을 SingleStoreDB 에 저장
2.
추출한 프레임에서 얼굴 인식 모델을 활용하여 인식한 얼굴 이미지를 임베딩하여 vector 데이터로 변환 후 DB에 저장
3.
flask 로 해당하는 얼굴과 가장 유사한 얼굴 이미지를 유사도 순위로 웹페이지에 표현
SingleStoreDB 버전 : 8.1.20 CPU : 4 MEMORY : 8 GB Node : MA - 1개 LEAF - 1개 Partition : 4 Python 3.8

DDL 및 DML

DDL 및 DML 쿼리는 아래 파이썬 코드 (img2vec.py, flask_image.py) 에 포함되어 있습니다.
1.
img2vec.py : 프레임 추출 및 임베딩 그리고 테이블에 data ingestion 하는 코드
2.
flask_image.py : 얼굴 유사도 비교 결과를 flask 를 이용하여 웹에 표현하는 코드

프레임 추출 및 얼굴 인식 후 DB 에 저장

1. 영화 영상 파일에서 프레임 추출

저작권이 만료된 영화 2편에서 각 25프레임 단위로 프레임 추출
1.
Teachers_pet2.mkv (선생님의 애완동물 part2) (흑백 영화)
2.
Gone_with_the_Wind_part1.mkv (바람과 함께 사라지다 part1)

2. 프레임에서 얼굴 인식

facenet_pytorch 의 얼굴 인식 모델인 MTCNN 을 사용하여 추출한 프레임에서 얼굴 인식하여 얼굴 좌표 획득 및 임베딩하여 vector 데이터로 변환
pytorch 기반 라이브러리로 보다 사용이 용이하고, 한 이미지에서 여러 얼굴 인식 및 얼굴 좌표 (랜드마크) 추출이 가능하며, dlib 그리고 기존 MTCNN 보다 얼굴 인식 측면에서 속도가 더 빠른 pytorch로 구현된 facenet_pytorch 의 MTCNN 을 사용하여 얼굴 인식.
vggface2 데이터 셋으로 사전 학습한 InceptionResnetV1 모델로 이미지 임베딩 수행

파이썬 코드 : img2vec.py

import os import cv2 import numpy as np import torch from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image import singlestoredb as s2 import traceback import io import json # 데이터베이스 연결 정보 HOST = 'IP' PORT = 3306 USER = 'DB 유저' PASSWORD = '비밀번호' DATABASE = '데이터 베이스' frm_tbl = 'img_frame_fx25' fc_tbl = 'face2vec_fx25' # 데이터베이스 연결 conn = s2.connect( host=HOST, user=USER, password=PASSWORD, database=DATABASE ) # create table frame_tbl = f"""create table if not exists {frm_tbl} ( img_id int(11) not null, frame_nm varchar(255), image longblob, key (id) using hash, shard key(frame_nm), sort key(id) );""" face_tbl = f"""create table if not exists {fc_tbl} ( face_id int(11) not null, frame_nm varchar(255), face json, vector blob, key (id) using hash, shard key(frame_nm), sort key(id) );""" cur = conn.cursor() cur.execute(frame_tbl) cur.execute(face_tbl) # MTCNN과 InceptionResnetV1 모델 로드 mtcnn = MTCNN(keep_all=True, min_face_size=120) resnet = InceptionResnetV1(pretrained='vggface2').eval() frame_sql = f"INSERT INTO {frm_tbl} (img_id , frame_nm, image) VALUES (%s, %s, %s)" face_sql = f"INSERT INTO {fc_tbl} (face_id , frame_nm, face, vector) VALUES (%s, %s, %s, json_array_pack(%s))" # nomalize vector (벡터 정규화) def normalize_vector(vector): norm = torch.norm(vector, p=2, dim=1, keepdim=True) normalized_vector = vector / norm return normalized_vector #id cnt = 0 fcnt = 1 # vectorize def extract_frames(video_path, frame_interval, batch_size): global cnt global fcnt # Load video cap = cv2.VideoCapture(video_path) # check the file loaded if not cap.isOpened(): print("Error: can't load the file.") return batch = [] batch_number = 1 frame_count = 0 while True: # read frames ret, frame = cap.read() # finished the read if not ret: break # save frames by interval if frame_count % frame_interval == 0: batch.append(frame) if len(batch) == batch_size: print('batch length :', len(batch)) for frame in batch: frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) img_cv2 = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR) frame_title = f"{filename}_{cnt}" faces = mtcnn(frame) # 얼굴 좌표 및 확률 추출 boxes, probs = mtcnn.detect(frame) if boxes is not None and len (boxes) > 0: for idx, face in enumerate(faces): prob = probs[idx] if prob > 0.99 : # resnet 모델을 사용하여 face 이미지를 임베딩하여 벡터 데이터로 변환 img_embedding = resnet(face.unsqueeze(0)) #위에서 정의한 정규화 함수를 이용하여 벡터 데이터 정규화 img_embedding = normalize_vector(img_embedding) # 데이터 베이스에 벡터 데이터를 저장하기 위해 타입 변경 # Numpy 배열인 임베딩된 벡터 데이터를 flatten 하여 1차원 배열로 변환하고, 리스트 -> 문자열로 변환 img_embedding = str(img_embedding.flatten().tolist()) box = boxes[idx] box_list = box.tolist() box_json = json.dumps(box_list) face_data =(fcnt, frame_title, box_json, img_embedding) cur.execute(face_sql, face_data) fcnt += 1 cnt += 1 # 이미지를 바이너리 데이터로 변환 byte_arr = io.BytesIO() frame.save(byte_arr, format='PNG') image_data = byte_arr.getvalue() frame_data = (cnt, frame_title, image_data) cur.execute(frame_sql, frame_data) conn.commit() batch=[] batch_number += 1 frame_count += 1 if batch: for frame in batch: frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) img_cv2 = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR) frame_title = f"{filename}_{cnt}" faces = mtcnn(frame) boxes, probs = mtcnn.detect(frame) if boxes is not None and len (boxes) > 0: for idx, face in enumerate(faces): prob = probs[idx] if prob > 0.99 : img_embedding = resnet(face.unsqueeze(0)) img_embedding = normalize_vector(img_embedding) img_embedding = str(img_embedding.flatten().tolist()) box = boxes[idx] box_list = box.tolist() box_json = json.dumps(box_list) face_data =(fcnt, frame_title, box_json, img_embedding) cur.execute(face_sql, face_data) fcnt += 1 cnt += 1 # 이미지를 바이너리 데이터로 변환 byte_arr = io.BytesIO() frame.save(byte_arr, format='PNG') image_data = byte_arr.getvalue() frame_data = (cnt, frame_title, image_data) cur.execute(frame_sql, frame_data) conn.commit() # Close video reader cap.release() frame_count = int(frame_count / frame_interval) print(f"Finished loading frames. Total {frame_count} saved.") # Set variables input_directory = "/home/opc/mv_frame/movies" # mkv 파일 디렉토리 경로 frame_interval = 25 # 일정한 간격으로 프레임 저장 (25 프레임) batch_size = 10 # Iterate through mkv files in the input directory try : for filename in os.listdir(input_directory): if filename.endswith(".mkv"): video_path = os.path.join(input_directory, filename) extract_frames(video_path, frame_interval, batch_size) except Exception as e: traceback.print_exc() finally: conn.close()
Python
복사

Ingestion 확인

singlestore> select img_id, frame_nm, sha1(image) from img_frame_fx25 where img_id < 6 order by 1; +--------+---------------------+------------------------------------------+ | img_id | frame_nm | sha1(image) | +--------+---------------------+------------------------------------------+ | 1 | Teachers_pet2.mkv_0 | 4e163e6e3158e76aa9b9dad47de93c65c96370dd | | 2 | Teachers_pet2.mkv_1 | c404dedc08c73da67b21350afe3ae48ea5886770 | | 3 | Teachers_pet2.mkv_2 | abb0467097181b86954d17da0acb1bb7cc406096 | | 4 | Teachers_pet2.mkv_3 | a6d28afc96ae0c5da6f2a595ed97b9305822f740 | | 5 | Teachers_pet2.mkv_4 | 8e2dcf463baef83e854e5505854a339e3b80ba63 | +--------+---------------------+------------------------------------------+ 5 rows in set (0.04 sec) singlestore> select face_id, frame_nm, face, sha1(vector) from face2vec_fx25 where face_id <6 order by 1; +---------+----------------------+-----------------------------------------------------------------------------+------------------------------------------+ | face_id | frame_nm | face | sha1(vector) | +---------+----------------------+-----------------------------------------------------------------------------+------------------------------------------+ | 1 | Teachers_pet2.mkv_1 | [841.9851684570312,142.64962768554688,963.5681762695312,308.54241943359375] | 6acd6e0617eae9830c9276eeb48aded66d51f89b | | 2 | Teachers_pet2.mkv_2 | [784.7305297851562,143.9194793701172,912.7191772460938,314.43682861328125] | 9bdabe286cb3d43c251fc66ba3cf0ade9c184cde | | 3 | Teachers_pet2.mkv_3 | [747.0670166015625,140.2769317626953,874.9608764648438,310.2846374511719] | 1f3e35c6a64a23cf37b864d1cba346271ea59c3d | | 4 | Teachers_pet2.mkv_13 | [332.0965881347656,391.7582092285156,439.3731689453125,583.3365478515625] | cc0130da4c89f5dd9d91893cc78806fa9e5920f8 | | 5 | Teachers_pet2.mkv_14 | [1507.8375244140625,449.70953369140625,1641.097412109375,637.3809814453125] | 2d03b19ba5c98f6b4b640455b5bfd148f678aa64 | +---------+----------------------+-----------------------------------------------------------------------------+------------------------------------------+ 5 rows in set (0.04 sec)
SQL
복사

특정 얼굴 유사도 표현

Flask로 웹 페이지에 이미지와 얼굴 표현

특정 face_id 를 지정하여, 해당하는 vector 값과 가장 유사한 얼굴 이미지를 포함하는 프레임을 유사도 순위로 표현.
DOT_PRODUCT 를 이용하여 얼굴 유사도를 구하고 높은 값부터 20개를 정렬하여 선택. (Top 20 KNN)
위에서 선택한 얼굴 20개를 EUCLIDEAN_DISTANCE 이용하여 서로 다른 프레임 간 유클리디안 거리가 0.3 미만이면 중복된 얼굴로 인식하여 보다 작은 크기의 사이즈로 이미지 표시. (중복 제거는 data에 따라 다양한 알고리즘 선정 필요함. 예시를 위해 vector간 거리를 사용함)

dot_product 함수를 이용한 유사도 top4 조회 예시

# face_id 가 100인 얼굴의 이미지 유사도 비교 top4 조회 쿼리 WITH target AS ( SELECT vector as v2 FROM face2vec_fx25 WHERE face_id = 100 # 기준 face_id ) SELECT v.face_id, i.frame_nm, dot_product(v.vector, target.v2) as score, substring(json_array_unpack(v.vector),1, 110) as vector FROM img_frame_fx25 i INNER JOIN face2vec_fx25 v ON i.frame_nm = v.frame_nm INNER JOIN target ORDER BY score DESC LIMIT 5; # top4 조회 결과 및 vector 데이터 확인 +---------+-----------------------+--------------------+----------------------------------------------------------------------------------------------------------------+ | face_id | frame_nm | score | vector | +---------+-----------------------+--------------------+----------------------------------------------------------------------------------------------------------------+ | 100 | Teachers_pet2.mkv_104 | 1 | [-0.0192908347,-0.0224674344,0.0060667675,-0.0262606274,0.0808363631,0.0225105044,0.0154247871,-0.00753672561, | | 101 | Teachers_pet2.mkv_105 | 0.9452268481254578 | [-0.0211227089,-0.00780974748,0.00126591069,-0.0703039244,0.0416311659,0.0327570438,0.00976078026,-0.013043608 | | 99 | Teachers_pet2.mkv_103 | 0.9119338989257812 | [0.00312176137,-0.00704365829,0.00900752097,-0.0737070814,0.0437549539,0.0156371333,-0.0124957021,0.0157040302 | | 92 | Teachers_pet2.mkv_88 | 0.8868334889411926 | [0.00233438541,-0.00629964471,0.00138104905,-0.0759592205,0.0319078825,0.0234120488,0.0157014783,-0.0069763497 | | 103 | Teachers_pet2.mkv_111 | 0.8767971992492676 | [0.00145188696,-0.00690847356,-0.00848316774,-0.0913699418,0.037625622,0.0291228425,0.0242910013,-0.0098315281 | +---------+-----------------------+--------------------+----------------------------------------------------------------------------------------------------------------+ 5 rows in set (0.11 sec)
SQL
복사

파이썬 코드 : flask_image.py

from flask import Flask, render_template, Response import singlestoredb as s2 import cv2 import numpy as np import ast import base64 import traceback app = Flask(__name__) # Database connection settings HOST = 'IP' PORT = 3306 USER = 'DB 유저' PASSWORD = '비밀번호' DATABASE = '데이터 베이스' # Define a route to display images @app.route('/display_image/<int:_id>') def display_image(_id): # Database connection conn = s2.connect( host=HOST, user=USER, password=PASSWORD, database=DATABASE ) cur = conn.cursor() try: # Execute the query to fetch image and face data query = """ with basis_face as ( select face_id, frame_nm, face, vector from face2vec_fx25 where face_id = %s), similar_faces as ( select b.face_id, b.frame_nm, b.face, b.vector, dot_product(a.vector, b.vector) similarity from basis_face a, face2vec_fx25 b ), face_set as ( select face_id, frame_nm, similarity, face, vector from similar_faces order by similarity desc limit 20), face_distance as ( select a.face_id face_id_a, b.face_id face_id_b, EUCLIDEAN_DISTANCE(a.vector, b.vector) distance from face_set a, face_set b where a.face_id != b.face_id order by 3), dup_faces as ( select row_number() over (order by similarity desc) order_id, face_id_a, json_agg(face_id_b) dup_face, similarity from face_distance, face_set where distance < 0.3 and face_distance.face_id_a = face_set.face_id group by face_id_a), dup_faces_result as ( select a.order_id order_id, a.face_id_a face_id , a.dup_face dup_face , case when sum(json_array_contains_double(b.dup_face, a.face_id_a)) > 0 then 'Y' else 'N' end dup_exists from dup_faces a, dup_faces b where a.order_id >= b.order_id group by a.order_id, a.face_id_a ) select y.*, z.image from ( select a.face_id, a.frame_nm, face, a.similarity, dup_face from face_set a left join dup_faces_result b on a.face_id = b.face_id where dup_exists is null or dup_exists = 'N' ) y, img_frame_fx25 z where y.frame_nm = z.frame_nm order by similarity desc; """ cur.execute(query, (_id)) # Fetch the results results = cur.fetchall() # 이미지 데이터를 저장할 리스트 images = [] for result in results: id, frame_nm, face, score, dup, image_blob = result if dup is not None : flag = 0 # Convert image_blob to numpy array img_array = np.frombuffer(image_blob, np.uint8) image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) # Draw a rectangle around the face face_coords = face x1, y1, x2, y2 = face_coords cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) image = cv2.resize(image, (550, 343)) # Encode image to base64 _, img_encoded = cv2.imencode('.jpg', image) img_base64 = base64.b64encode(img_encoded).decode('utf-8') # image info img_info = { "Face ID": id, "Frame Name": frame_nm, "Similarity Score": score, } images.append((flag, img_base64, img_info)) for dup_face in dup : flag = 0 dup_sql = f""" with dp_face as ( select face_id from face2vec_fx25 where face_id = {dup_face} ) select a.*, b.image from ( select f.face_id, f.face, i.frame_nm from face2vec_fx25 f inner join img_frame_fx25 i on f.frame_nm = i.frame_nm inner join dp_face on f.face_id = dp_face.face_id) a, img_frame_fx25 b where a.frame_nm = b.frame_nm; """ cur.execute(dup_sql) dup_results = cur.fetchone() dp_face_id, dp_face, dp_frame_nm, dp_image = dup_results # Convert image_blob to numpy array img_array = np.frombuffer(dp_image, np.uint8) image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) # Draw a rectangle around the face face_coords = dp_face x1, y1, x2, y2 = face_coords cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) image = cv2.resize(image, (400, 203)) # Encode image to base64 _, img_encoded = cv2.imencode('.jpg', image) img_base64 = base64.b64encode(img_encoded).decode('utf-8') # image info img_info = { "Face ID": dp_face_id, "Frame Name": dp_frame_nm, } images.append((flag, img_base64, img_info)) else : flag = 1 # Convert image_blob to numpy array img_array = np.frombuffer(image_blob, np.uint8) image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) # Draw a rectangle around the face face_coords = face x1, y1, x2, y2 = face_coords cv2.rectangle(image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) image = cv2.resize(image, (550, 343)) # Encode image to base64 _, img_encoded = cv2.imencode('.jpg', image) img_base64 = base64.b64encode(img_encoded).decode('utf-8') # image info img_info = { "Face ID": id, "Frame Name": frame_nm, "Similarity Score": score, } images.append((flag, img_base64, img_info)) # 이미지 데이터를 HTML 템플릿으로 전달하고 이미지를 표시 return render_template('display_image.html', images=images) except Exception as e: traceback.print_exc() return str(e), 500 finally: cur.close() conn.close() if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)
Python
복사

HTML: display_image.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Image Display</title> <style> @media (max-width: 600px) { .image-container { flex-direction: column; } .image-wrapper { width: 100%; } } </style> </head> <body> <div class="image-container"> <div class="image-column"> {% set is_last_1 = true %} {% for is_1, img_base64, img_info in images %} {% if is_1 %} {% if not is_last_1 %} </div> <!-- 1인 이미지 세로 배치를 마친 경우 닫기 --> <br><br> <!-- 1인 이미지 사이에 빈 줄 추가 --> {% endif %} <div class="image-column"> <!-- 1인 이미지 세로 배치 시작 --> <div class="image-wrapper"> <!-- 1인 이미지 세로 배치 시작 --> <img src="data:image/jpeg;base64,{{ img_base64 }}" alt="Image"> <p>Face ID: {{ img_info['Face ID'] }}</p> <p>Frame Name: {{ img_info['Frame Name'] }}</p> <p>Similarity Score: {{ img_info['Similarity Score'] }}</p> </div> </div> {% set is_last_1 = true %} {% else %} <div class="image-wrapper" style="display: inline-block;"> <!-- 0인 이미지 가로 배치 --> <img src="data:image/jpeg;base64,{{ img_base64 }}" alt="Image"> <p>Face ID: {{ img_info['Face ID'] }}</p> <p>Frame Name: {{ img_info['Frame Name'] }}</p> <p>Similarity Score: {{ img_info['Similarity Score'] }}</p> </div> {% set is_last_1 = false %} {% endif %} {% endfor %} </div> </div> </body> </html>
HTML
복사

Flask 웹 페이지 확인

왼쪽 가장 위에 있는 얼굴 이미지 기준(예시 1의 Facd ID : 4262)으로 유사도 분석
기준 얼굴 이미지 바로 아래에 세로로 유사도가 높은 얼굴 이미지들을 차례로 나열
가로로 나열된 크기가 보다 작은 이미지들은 가장 왼쪽의 이미지에서 중복된 얼굴로 인식된 이미지를 표현

예시 1

예시 2

References

History

일자
작성자
비고
2023.10.10
min
2023.10.25
min
설명 및 쿼리 추가