[딥러닝] 10분안에 이미지 캡쳐 탐지 서비스 만들기

chrisjune
12 min readFeb 25, 2023

--

딥러닝 라이브러리를 활용해 간단한 이미지 이미지의 캡쳐 여부를 판단할 수 있는 서비스를 만들어보고 실무에서 이를 활용했던 후기를 공유하도록 하겠습니다.

Requirements

준비물은 Python3.6버전 이상과 딥러닝 라이브러리가 필요합니다.

여기서 딥러닝라이브러리는 파이토치를 사용하도록 하겠습니다. 꼭 파이토치일 필요는 없으며 텐서플로, 케라스, FastAI 모두 흐름은 동일합니다.

pip install "numpy==1.23.*" "requests==2.28.*" "torch==1.12.*" "torchvision==0.13.*" "requests==2.28.*"

캡쳐 이미지 탐지 코드

세가지 단계로 캡쳐한 이미지인지 판별할 수 있습니다. 1) 딥러닝 이미지 모델 불러오기 2) 비교할 이미지를 모델에 넣어 결과값 받기 3) 결과값을 비교해서 캡쳐한것인지 판단하기

1.딥러닝 이미지 모델 불러오기

from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights
from torchvision.models.feature_extraction import create_feature_extractor

// EfficientNet의 학습된 파라미터를 불러옵니다.
weights = EfficientNet_B0_Weights.DEFAULT

// 학습된 파라미터를 사용한 모델을 정의합니다. 실행시 다운로드 받는데 시간이 소요됩니다.
model = efficientnet_b0(weights=weights)
model = create_feature_extractor(model, return_nodes={'avgpool': 'avgpool'})
model.eval()

중간에 return_node에 avgpool dictionary를 선언한 이유는, 모델 그래프에서 중간 계산중 마지막 분류Layer인 classifier직전 값을 따로 메모리에 저장하기 위함입니다. 해당 모델에 이미지를 넣으면 어떤이미지인지 라벨링을 하기 때문에 라벨링 직전 Feature를 뽑아낸 값이 필요합니다.

print(model)을 해보면 classifier layer 직전이 avgpool layer이기 때문에 이 layer의 결과값을 받을 수 있도록 하였습니다.

print(model)을 했을 때 결과

2. 이미지를 모델에 넣어 결과값 받기

import requests
import torchvision.transforms as T
from PIL import Image

// 학습모델에 이미지를 Input data로 넣기 위한 사이즈로 resize를 해줍니다.
def image_resize(image_url):
image = Image.open(requests.get(image_url, stream=True).raw)
rgb_image = image.convert('RGB')
preprocess = T.Compose([
T.Resize(256, interpolation=T.InterpolationMode.BICUBIC),
T.CenterCrop(224),
T.ToTensor()]
)
return preprocess(rgb_image).unsqueeze(0)

// 이미지URL을 Resize-> 추론 ->1차원 백터로 변환해주는 로직입니다.
def predict(image_url):
resized_image = image_resize(image_url)
predicted_result = model(resized_image)
image_feature = torch.flatten(predicted_result['avgpool'])
return image_feature.detach().numpy()

3. 비교할 이미지를 모델에 넣어 결과값 받기

from numpy import dot
from numpy.linalg import norm

// 두 백터가 유사한지 검증하기 위한 cosine 유사도함수를 정의해줍니다.
def cos_sim(A, B):
return dot(A, B) / (norm(A) * norm(B))

결과

지금까지 한 작업을 종합해보면 이미지를 두개의 숫자배열로 만들고, 두개의 배열이 얼마나 가까운지 계산하는 것이었습니다.

두 백터(배열)가 유사한지 계산은 Cosine 유사도로 계산이 가능하고, 결과 백터의 값들이 양수이기 때문에 0이상 1이하의 값을 가지며 0에 가까울 수록 서로 다른이미지, 1에 가까울 수록 비슷한 이미지라고 판단할 수 있습니다.

Oreilly

비슷한 사진 테스트

두개의 비슷하지만 약간 다른 이미지를 비교했을 때 결과는 0.9로 1에 매우 근접한 것으로 유사하다고 판단할 수 있습니다.

source_url = "https://drive.google.com/uc?export=download&id=15SFZlK07iWIDUn1NRU7njl_7pU-AlLY1"
target_url = "https://drive.google.com/uc?export=download&id=1Z3CtQSqsuaSEv3aeEaLtcYOf2_iJxdNK"

source_embedding = predict(source_url)
target_embedding = predict(target_url)
print(cos_sim(source_embedding, target_embedding))

// 0.9064

다른 사진 테스트

비슷한 로봇팔이지만 분명 캡쳐이미지는 아닌 사진이지요. 유사도는 0.5로 캡쳐이미지는 아니라고 판단할 수 있습니다. 여러 이미지로 테스트해보며 휴리스틱하게 캡쳐의 기준을 정할 수 있습니다. 만약 로봇팔과 관련없는 아예 다른 이미지였다면 유사도는 0에 더 가까울 것 입니다.

source_url = "https://drive.google.com/uc?export=download&id=15SFZlK07iWIDUn1NRU7njl_7pU-AlLY1"
not_same_target_url = "https://drive.google.com/uc?export=download&id=119biFlAhymgClAAxGTzEQyuhgVtWiXNn"

source_embedding = predict(source_url)
not_same_target_embedding = predict(not_same_target_url)
print(cos_sim(source_embedding, not_same_target_embedding))

// 0.5067

전체코드. 50줄 이내에 코드부터 테스트까지 해볼 수 있었습니다.

from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights
from torchvision.models.feature_extraction import create_feature_extractor

weights = EfficientNet_B0_Weights.DEFAULT

model = efficientnet_b0(weights=weights)
model = create_feature_extractor(model, return_nodes={'avgpool': 'avgpool'})
model.eval()

import requests
import torchvision.transforms as T
from PIL import Image

source_url = "https://drive.google.com/uc?export=download&id=15SFZlK07iWIDUn1NRU7njl_7pU-AlLY1"
target_url = "https://drive.google.com/uc?export=download&id=1Z3CtQSqsuaSEv3aeEaLtcYOf2_iJxdNK"
not_same_target_url = "https://drive.google.com/uc?export=download&id=119biFlAhymgClAAxGTzEQyuhgVtWiXNn"


def image_resize(image_url):
image = Image.open(requests.get(image_url, stream=True).raw)
rgb_image = image.convert('RGB')
preprocess = T.Compose([
T.Resize(256, interpolation=T.InterpolationMode.BICUBIC),
T.CenterCrop(224),
T.ToTensor()]
)
return preprocess(rgb_image).unsqueeze(0)


from numpy import dot
from numpy.linalg import norm
import torch


def cos_sim(A, B):
return dot(A, B) / (norm(A) * norm(B))


def predict(image_url):
resized_image = image_resize(image_url)
predicted_result = model(resized_image)
image_feature = torch.flatten(predicted_result['avgpool'])
return image_feature.detach().numpy()


source_embedding = predict(source_url)
target_embedding = predict(target_url)

print(cos_sim(source_embedding, target_embedding))
not_same_target_embedding = predict(not_same_target_url)
print(cos_sim(source_embedding, not_same_target_embedding))

실무에 적용하며 느꼈던 점

1. 실무에서 SOTA모델이 제일 좋은 것은 아닐 수 있다.

실제 업무에서는 EfficientNet이 아닌 ResNet을 활용하였습니다. 논문에서 EfficientNet이 파라미터 수 대비 정확도나 추론 속도가 매우 빠른것을 확인할 수 있습니다. 따라서 2019년 SOTA(State-of-the-Art) 모델이 되었습니다.

ImageNet에서 기존 ConvNet보다 8.4배 작으면서 6.1배 빠르고 더 높은 정확도를 갖는다고 합니다.

하지만 실제 업무에서 캡쳐된 이미지를 활용하여 POC를 진행할 때 ResNet과 파라미터 용량과 추론 속도면에서 큰 차이를 느끼지 못하였습니다. 몇천개의 리뷰 이미지로 테스트해보면서 EfficientNet의 Embedding으로 유사도를 비교할 때 제대로 탐지하는 못하는 엣지 케이스가 생겨 ResNet을 활용하였습니다. 다양한 모델중에서도 ResNet이 유사도 비교시 큰 편차없이 잘 탐지해냈습니다.

2. 독립적인 서비스로 운영하는 것이 효율적이다.

실제 적용시엔 메인서비스에 구현하지 않고 독립 서비스로 구현하는 것이 좋습니다.

서비스를 분리했을 때 장점은 메인서비스가 딥러닝과 관련된 의존성을 포함하지 않아도 되고, 캡쳐 탐지 서비스의 성능을 보장하기 위해 독립적으로 스케일아웃 할 수 있기 때문입니다.

3. 모델 API 서빙

모델을 REST API로 만들어 서빙하기 위한 방법은 다양합니다. 서비스 상황과 규모에 맞는 다양한 조합을 시도해보고 그 중에서 선택해보시는 것을 추천합니다. 시간이 없고 우선 빨리 개발을 해야한다면 익숙한 라이브러리를 사용하여 먼저 만들어보고 성능테스트후 변경을 해보는 것이 좋습니다.

후보: Torchserve, Sagemaker, WSGI Library (Django, Flask), ASGI Library (FastAPI, BentoML)

4. 캡쳐 여부의 Threshold

서비스와 도메인마다 이미지가 유사한 이미지인지 판단하는 기준이 다릅니다. 왜냐하면 사용할 이미지가 배경이 있는지, 물건이 복잡한지, 사람이 포함되어있는지 등등 여러 변수가 많기 때문입니다. 따라서, POC를 통해 기준을정하고 서비스에 배포후 값을 조절해 보는 것이 좋습니다.

지금까지 매우 간단한 딥러닝 지식을 바탕으로 캡쳐 탐지 로직을 나누었습니다. 조금이나마 도움이 되었으면 좋겠습니다. 감사합니다

--

--