Visão Computacional: do Desenvolvimento ao Deploy
- Vinicius Goia

- 13 de abr. de 2024
- 9 min de leitura

Este post tem o objetivo de demonstrar todo o processo de desenvolvimento e deploy de uma aplicação de visão computacional. O aplicativo será hospedado através da plataforma Streamlit, disponibilizado através de um link e o processamento mais pesado será realizado dentro da plataforma AWS. A função principal a ser executada será a detecção e desfoque de rostos, técnica comumente utilizada para manter a privacidade de pessoas.
Pré-requisitos
VSCode - Editor de código utilizado durante o desenvolvimento. Disponível para Windows, macOS e Linux. Instalação oficial do VSCode.
Pyenv - Ferramenta para gerenciar múltiplas versões do Python. A versão recomendada do Python para este projeto é a `3.11.3`. Instruções oficiais de instalação do Pyenv.
Poetry - Ferramenta de gerenciamento de dependências em Python. Instruções oficiais de instalação do Poetry.
Git - Ferramenta de controle de versão distribuído. Instruções oficiais de instalação do Git.
GitHub - Plataforma de hospedagem de código. É essencial ter uma conta para interagir com os repositórios. Como criar uma conta no GitHub.
Docker - Necessário para criar um ambiente isolado que simula uma função Lambda para testes locais. Para instalar o Docker, siga as instruções em Install Docker.
Funcionamento
Como faríamos para desfocar rostos de pessoas de maneira automática em uma foto? Pensemos em todas as etapas envolvidas no processo:
Carregamento de uma imagem qualquer com pessoas;
Detecção dos rostos das pessoas e criação de demarcações;
Utilização dessas demarcações para criação de uma área de interesse na foto (ROI);
Recorte da área de interesse e aplicação de filtros de desfoque;
Inserção do recorte modificado novamente na imagem;
Disponibilização da imagem completa alterada.
Esses seriam os passos para a execução local de nossa aplicação. Como a ideia é realizar o processamento em nuvem, temos que pensar nos seguintes pontos:
Como iremos enviar a imagem para a aplicação na nuvem?
Como essa aplicação vai ser hospedada na nuvem?
Como essa aplicação retorna a imagem modificada?
Como hospedar a aplicação inteira em um link?
Visualizando esse cenário, definimos que utilizaremos o processamento principal em nuvem através de funções lambda disponibilizadas na plataforma AWS. Essa função lambda será criada a partir de um arquivo Docker e, através de uma outra função, iremos requisitar sua ação. Vamos ao desenvolvimento.
Desenvolvimento
Antes de iniciarmos o desenvolvimento do código, vamos configurar nosso ambiente visando as melhores práticas. Siga os seguintes passos para a configuração:
No VSCode, utilize o shell para se dirigir à pasta do projeto, e dentro dela, execute o comando poetry new "nome_do_projeto" para criar uma estrutura de arquivos baseada no poetry;
Entrando na nova pasta criada, execute o comando poetry config virtualenvs.in-project true para configurar o poetry na criação de ambientes virtuais;
Execute o comando pyenv install 3.11.3 para utilizar a versão do python correta;
Execute o comando poetry shell para iniciar o ambiente virtual.
Função Lambda
Como comentado anteriormente, esse script será inserido na plataforma AWS através de um arquivo Docker. Primeiramente, vamos observar a sua lógica:
# Importação de bibliotecas necessárias
import cv2
import base64
import numpy as np
import dlib
import imutils
from imutils import face_utils
from scipy.ndimage import filters
from PIL import Imagedef encode_image_to_base64(
img: np.ndarray
) -> str:
"""
Codifica uma imagem (array da NumPy) diretamente para a representação em string Base64,
salvando temporariamente em um arquivo.
Args:
img (np.ndarray): A imagem como um array da NumPy para ser codificada.
Returns:
str: A representação da imagem codificada em Base64.
"""
# Caminho temporário para salvar a imagem
aux_path = '/tmp/tmp_image.png'
# Salva a imagem no caminho temporário
cv2.imwrite(aux_path, img)
# Abre a imagem salva, codifica em Base64, e retorna a string decodificada
with open(aux_path, "rb") as f:
encoded_string = base64.b64encode(f.read()).decode("utf-8")
return encoded_stringA função encode_image_to_base64 recebe uma imagem e codifica em base64, que nada mais é do que uma sequência de caracteres que representam a imagem. Dessa maneira, conseguimos enviar a informação de forma mais efetiva.
def decode_base64_to_image(
encoded_string: str,
img_path: str = "/tmp/decoded_img.png"
) -> np.ndarray:
"""
Decodifica uma string Base64 para uma imagem e salva como arquivo, então lê a imagem do arquivo e retorna como um array da NumPy.
Args:
encoded_string (str): String Base64 da imagem a ser decodificada.
img_path (str): Caminho do arquivo onde a imagem decodificada será salva. Modificado para /tmp/decoded_img.png
Returns:
np.ndarray: A imagem decodificada como um array da NumPy.
"""
img_data = base64.b64decode(encoded_string)
with open(img_path, "wb") as image_file:
image_file.write(img_data)
image = cv2.imread(img_path)
return imageA função decode_base64_to_image decodifica a imagem em base64 para o formato numpy array para que todo o restante do processo seja executado na imagem.
def blur_faces(image):
"""
Identifica e desfoca as faces de pessoas.
Parâmetros:
image (numpy.ndarray): Imagem para desfocar.
Retorna:
numpy.ndarray: Imagem desfocada.
"""
#Instanciamento de métodos de detecção facial
face_detector = dlib.get_frontal_face_detector()
predict_points = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
#Leitura da Imagem
img = image
#Conversão da imagem em escala de cinza
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#Conversão da imagem para padrão PIL
pil_im = Image.fromarray(img)
#Aplicação do método na imagem em escala de cinza
rects = face_detector(gray_image, 1)
#Listas de coordenadas vazias
x1=[]
y1=[]
w1=[]
h1=[]
#Iteração nos dados adquiridos
for (i, rect) in enumerate(rects):
reference_points= predict_points(gray_image, rect)
reference_points = face_utils.shape_to_np(reference_points)
# (x, y, w, h)
(x, y, w, h) = face_utils.rect_to_bb(rect)
x1.append(x)
y1.append(y)
w1.append(w)
h1.append(h)
#Definição de área para recorte
box = (x, y, (x+w),(y+h))
#Recorte da imagem
region = pil_im.crop(box)
#Aplicação de filtro gaussiano para desfoque
gauss_face = filters.gaussian_filter(region,10)
#Transformação para padrão PIL
gauss_face = Image.fromarray(gauss_face)
#Colagem das faces desfocadas na imagem original
pil_im.paste(gauss_face,box) #usar
#Tranformação em Array
pil_im = np.array(pil_im)
#Desenho da caixa de demarcação
for i,n in enumerate(x1):
cv2.rectangle(pil_im, (x1[i], y1[i]), (x1[i] + w1[i], y1[i] + h1[i]), (0, 150, 200), 2)
return pil_imA função blur_faces realiza o processamento pesado da imagem. Para isso, é utilizado a biblioteca dlib para detecção de rostos e, através de seus resultados, é possível encontrar pontos de referências e criar uma área de interesse (ROI). Através do ROI, cortamos a imagem onde os rostos se encontram, executamos os filtros referentes ao desfoque e colocamos a imagem recortada e desfocada novamente na imagem original.
def lambda_handler(event, context):
# Exemplo de como obter a string base64 da imagem a partir do evento
base64_string = event['body']
# Decodifica a string base64 para uma imagem
image = decode_base64_to_image(base64_string)
# Aplica a correção de inclinação na imagem
processed_image = blur_faces(image)
# Codifica a imagem processada em Base64
processed_image_base64 = encode_image_to_base64(processed_image)
# Retorna a imagem processada codificada em Base64
return {
'statusCode': 200,
'isBase64Encoded': True,
'headers': {'Content-Type': 'image/png'},
'body': processed_image_base64
}A função lambda_handler é responsável por acionar as funções descritas anteriormente na plataforma AWS, através de disparo de um evento.
Arquivo Dockerfile
Dentro da plataforma AWS, iremos carregar nossa função lambda através de um arquivo Dockerfile. O Docker é responsável pela conteinerização de scripts. Isso garante que os arquivos e bibliotecas necessários para o funcionamento de nossa aplicação sejam instalados e disponibilizados. Para isso, o seguinte arquivo Dockerfile foi criado:
# Usando a imagem base oficial do Python 3.11 para AWS Lambda
FROM public.ecr.aws/lambda/python:3.11
# Copia arquivos pertinentes
COPY shape_predictor_68_face_landmarks.dat ./
# Instala as dependências necessárias
RUN pip install numpy requests opencv-python-headless Pillow imutils scipy dlib-bin
# Copia o código da função para o contêiner
COPY lamb_func.py ./
# Define o comando para executar a função lambda
CMD ["lamb_func.lambda_handler"]Antes de continuarmos, vamos verificar a estrutura de arquivos de nossa pasta:
Observações:
O arquivo __init__.py foi criado automaticamente pelo poetry;
O arquivo shape_predictor_68_face_landmarks.dat é utilizado nas predições para detecção de faces. Tenha certeza de tê-lo nessa estrutura.
O arquivo trust-policy.json é um arquivo de configuração da AWS que nos dá acesso à certas ações dentro da plataforma, uma regra. Seu conteúdo segue abaixo:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}Configuração AWS
Antes de mais nada, necessitamos de uma conta na plataforma da Amazon Web Services. Depois de criada a conta, procuramos a seção referente a IAM e realizamos as configurações de usuários necessárias, como a identificação de dois fatores. Com tudo configurado, criamos uma Access Key, a qual gerará em conjunto uma Secret Access Key. Certifique-se de salvar sua Account ID para utilizarmos futuramente. Também necessitamos da instalação e configuração do AWS CLI, para que possamos executar os comandos de configuração diretamente do shell do VSCode.
Com essas configurações realizadas, podemos executar o Docker para montar nosso arquivo e marcá-lo para utilização:
docker build -t <nome> .docker tag lambda-blur:latest <account_id>.dkr.ecr.<region>.amazonaws.com/<nome_repo>:latestObservações:
<nome> Colocar nome do projeto;
<account_id> Colocar Account ID da AWS do seu usuário;
<region> Colocar região utilizada na plataforma;
<nome_repo> Colocar nome do repositório.
Após essas etapas, fazemos o login no ECR da AWS e criamos o repositório ECR com os seguintes comandos:
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account_id>.dkr.ecr.<region>.amazonaws.comaws ecr create-repository --repository-name <nome_repo> --region <region>O comando acima retorna uma sequência de informações. Vamos anotar a informação referente ao 'repositoryUri'.
Agora vamos enviar as informações do Docker para o repositório ECR:
docker push <account_id>.dkr.ecr.<region>.amazonaws.'com/<nome_repo>:latestNeste momento, iremos criar a regra para IAM utilizando o arquivo trust-policy.json para podermos utilizar os recursos de uma função lambda na plataforma, e o anexaremos na IAM Role com os seguintes comandos:
aws iam create-role --role-name LambdaExecutionRole --assume-role-policy-document file://trust-policy.jsonaws iam attach-role-policy --role-name LambdaExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRoleCom essas configurações realizadas, criaremos a função lambda:
aws lambda create-function --function-name <function_name> --package-type Image --code ImageUri=<repositoryUri>:latest --role arn:aws:iam::<account_id>:role/LambdaExecutionRole --region <region>Aumentaremos também o tempo de execução da função para 60 segundos uma vez que as tarefas de processamento de imagem podem ser mais pesadas e demandar mais tempo.
aws lambda update-function-configuration --function-name <function_name> --timeout 60 --region <region>No console da AWS, verificaremos a função lambda criada e testaremos seu funcionamento:
Conforme a imagem acima, para testar a função lambda, temos que criar um evento, nomeá-lo e, na seção Event JSON, devemos colocar a imagem no formato base64, com a palavra 'body' (No site https://base64.guru/ podemos gerar nossa imagem nesse formato).
Com os testes realizados, iremos na seção API Gateway do console e criaremos uma New Rest API. Criaremos um método Post, o qual é destinado a enviar uma informação e retornar outra. Selecionaremos Lambda Function, marcaremos a opção Proxy Integration, verificaremos a região utilizada e selecionaremos a função criada anteriormente. Deixaremos o Timeout padrão, criaremos a API e realizaremos o deploy da API. Em API Settings, na seção Manage Media Type, colocaremos '*/*' e realizaremos o deploy novamente. Ao final de todo esse processo, utilizaremos o Default endpoint para o desenvolvimento de nossa interface de interação.
Interface de interação com Streamlit
Através das funções abaixo, criaremos uma interface de interação com acesso via API de nossa função lambda para o processamento da imagem enviada.
import cv2
import numpy as np
import streamlit as st
import requests
from PIL import Image
import io
import base64
def get_image_download_link(img,filename,text):
"""Função para link de download de imagem"""
buffered = io.BytesIO()
img.save(buffered, format="JPEG")
img_str = base64.b64encode(buffered.getvalue()).decode()
href = f'<a href="data:file/txt;base64,{img_str}" download="{filename}">{text}</a>'
return href
def blur_faces_api(img: np.ndarray) -> np.ndarray:
"""Função para envio e recebimento de informação via API"""
API_ENDPOINT = "https://nti7y6pgge.execute-api.us-east-2.amazonaws.com/desenvolvimento"
# Codifica a imagem para formato PNG em bytes
is_success, im_buf_arr = cv2.imencode(".png", img)
if not is_success:
raise Exception("Não foi possível codificar a imagem para o formato PNG")
byte_im = im_buf_arr.tobytes()
# Envia a imagem codificada para a API e recebe a resposta
response = requests.post(url=API_ENDPOINT, data=byte_im)
if response.status_code != 200:
raise Exception(f"Falha na requisição à API: {response.status_code}")
# Lê a imagem diretamente da resposta como um array NumPy
img_corrected = np.frombuffer(response.content, np.uint8)
img_corrected = cv2.imdecode(img_corrected, cv2.IMREAD_COLOR)
return img_corrected
def convert_uploaded_file_to_cv2_image(uploaded_file):
"""Função para conversão de imagem"""
file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
return img
def main():
"""Função principal de layout streamlit"""
# Definição de colunas para layout
left_co, cent_co,last_co = st.columns(3)
# Imagem principal da página
st.image(Image.open('Streamlit/Logo Site.jpg'))
#Título principal
st.title("Aplicativo de Desfoque de Rostos")
# Texto descritivo abaixo do título
st.markdown('<div style="text-align: justify;">Este é um aplicativo projetado para ser utilizado como tarefa final da Especialização em Visão Computacional, ministrado por Carlos Melo, através da Escola Sigmoidal. Nele encontram-se conceitos de detecção de rostos, filtros, manipulação de imagens, bem como toda configuração para uso de uma Função Lambda via API Gateway. Está disponibilizado no link do repositório bem como descrito detalhadamente no post do blog Natural Engines.</div>', unsafe_allow_html=True)
# Adiciona o informações na sidebar
st.sidebar.image(Image.open('Streamlit/teste.png'))
st.sidebar.link_button('Estimativa de velocidade de carros com Yolo', 'https://www.naturalengines.com/post/estimativa-de-velocidade-de-carros-com-yolo')
st.sidebar.link_button('Treinamento de Modelo para Classificação de Imagens', 'https://www.naturalengines.com/post/treinamento-de-modelo-para-classifica%C3%A7%C3%A3o-de-imagens')
st.sidebar.link_button('Manuseio e Processamento de Imagens', 'https://www.naturalengines.com/post/manuseio-e-processamento-b%C3%A1sico-de-imagens')
st.sidebar.link_button('Geometria Primitiva e Transformações 2D com Python', 'https://www.naturalengines.com/post/geometria-primitiva-e-transforma%C3%A7%C3%B5es-2d-com-python')
st.sidebar.link_button('Python e Técnica de Keying', 'https://www.naturalengines.com/post/python-e-t%C3%A9cnica-de-keying')
st.sidebar.link_button('LinkedIn', 'https://www.linkedin.com/in/vinicius-goia-75a403234')
st.sidebar.link_button('Repositório', 'https://github.com/vinigoia/blur_Images_api')
# Barra de carregamento de imagem
uploaded_file = st.file_uploader("Escolha uma imagem para desfoque de rostos (max 2,5MB)", type=["png", "jpg", "jpeg"])
# Gif principal
st.image(Image.open('Streamlit/teste2_gif.gif'))
if uploaded_file is not None:
# Convertendo a imagem carregada para um formato que a API pode processar
img_original = convert_uploaded_file_to_cv2_image(uploaded_file)
# Aplicando o desfoque de rostos
try:
img_deskewed = blur_faces_api(img_original)
# Convertendo imagens de BGR para RGB para exibição
img_original_rgb = cv2.cvtColor(img_original, cv2.COLOR_BGR2RGB)
img_blurred = cv2.cvtColor(img_deskewed, cv2.COLOR_BGR2RGB)
# Exibindo imagens lado a lado
col1, col2 = st.columns(2)
with col1:
st.image(img_original_rgb, caption="Imagem Original", use_column_width=True)
with col2:
st.image(img_blurred, caption="Imagem Desfocada", use_column_width=True)
## Original image came from cv2 format, fromarray convert into PIL format
result = Image.fromarray(img_blurred)
st.markdown(get_image_download_link(result,'img_blurred.png','Download img_blurred'), unsafe_allow_html=True)
except Exception as e:
st.error(f"Erro ao processar a imagem: {e}")
if __name__ == "__main__":
main()Para a hospedagem no site do Streamlit, devemos fazer uma conexão com o repositório no Github e indicar qual arquivo refere-se a aplicação. É de extrema importância que, em nossa estrutura de arquivos, o documento requirements.txt descreva as bibliotecas a serem utilizadas:
opencv_python_headless
numpy
streamlit
Pillow
requestsAtravés deste link podemos testar a aplicação rodando em nuvem.

Interface

Imagem teste original

Imagem processada
Conclusão
Através do desenvolvimento desta aplicação, realizamos todas as etapas que englobam a entrega de um projeto de visão computacional com interface de usuário. Devemos, no entanto, sempre nos atentar ao objetivo e escopo do projeto. Dependendo dos requisitos e tarefas a serem executadas, algumas configurações adicionais devem ser realizadas com o intuito de aumentar espaços de armazenamento, processamento, etc. Devemos ter em mente que alguns processos podem ser realizados via API para consultas e retornos rápidos, mas dependendo do processamento, outras alternativas devem ser analisadas.












Comentários