Artigo 39 — Visão Computacional com OpenCV e PIL
Prof. Ricardo Matos Módulo 7 · Data Science e Machine Learning · Artigo 39 de 52
Introdução
Visão computacional é a capacidade de máquinas interpretarem e processarem imagens e vídeos. Está presente em reconhecimento facial, veículos autônomos, diagnóstico médico por imagem, controle de qualidade industrial e leitura de documentos. Python tem duas bibliotecas fundamentais para isso: Pillow (PIL) para manipulação geral de imagens e OpenCV para processamento de imagens em tempo real e visão computacional avançada.
Instalação
pip install pillow opencv-python numpy matplotlib
Pillow: Manipulação de Imagens
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
from pathlib import Path
import numpy as np
# Abrindo e inspecionando
img = Image.open("foto.jpg")
print(f"Formato: {img.format}")
print(f"Modo: {img.mode}") # RGB, RGBA, L (grayscale), etc.
print(f"Tamanho: {img.size}") # (largura, altura)
print(f"Dimensões: {img.width} × {img.height}")
# Criando imagem do zero
img_nova = Image.new("RGB", (400, 300), color=(30, 60, 120))
Operações Básicas
def operacoes_basicas(caminho: str, pasta_saida: str = "saida"):
Path(pasta_saida).mkdir(exist_ok=True)
img = Image.open(caminho)
# Redimensionamento
img_pequena = img.resize((200, 200))
img_thumb = img.copy()
img_thumb.thumbnail((300, 300)) # mantém proporção
# Recorte
largura, altura = img.size
centro_x, centro_y = largura // 2, altura // 2
crop_box = (
centro_x - 100, centro_y - 100,
centro_x + 100, centro_y + 100
)
img_crop = img.crop(crop_box)
# Rotação e espelhamento
img_rot90 = img.rotate(90, expand=True)
img_flip_h = img.transpose(Image.FLIP_LEFT_RIGHT)
img_flip_v = img.transpose(Image.FLIP_TOP_BOTTOM)
# Conversão de modo
img_gray = img.convert("L") # escala de cinza
img_rgba = img.convert("RGBA") # com canal alpha
# Salvando em diferentes formatos
img_pequena.save(f"{pasta_saida}/pequena.jpg", quality=85)
img_gray.save(f"{pasta_saida}/cinza.png")
img_crop.save(f"{pasta_saida}/recorte.jpg")
print("Operações básicas concluídas.")
return img
# Filtros
def aplicar_filtros(img: Image.Image) -> dict:
return {
"blur": img.filter(ImageFilter.BLUR),
"sharpen": img.filter(ImageFilter.SHARPEN),
"edge": img.filter(ImageFilter.FIND_EDGES),
"emboss": img.filter(ImageFilter.EMBOSS),
"smooth": img.filter(ImageFilter.SMOOTH_MORE),
"contour": img.filter(ImageFilter.CONTOUR),
"gaussian": img.filter(ImageFilter.GaussianBlur(radius=3)),
"unsharp": img.filter(ImageFilter.UnsharpMask(radius=2, percent=150)),
}
# Ajustes de imagem
def ajustar_imagem(img: Image.Image) -> Image.Image:
img = ImageEnhance.Brightness(img).enhance(1.2) # +20% brilho
img = ImageEnhance.Contrast(img).enhance(1.3) # +30% contraste
img = ImageEnhance.Sharpness(img).enhance(1.5) # +50% nitidez
img = ImageEnhance.Color(img).enhance(1.1) # +10% saturação
return img
Geração de Imagens: Marca D'água e Certificados
from PIL import Image, ImageDraw, ImageFont
import textwrap
def adicionar_marca_dagua(
caminho_img: str,
texto: str,
saida: str,
opacidade: int = 80
):
"""Adiciona marca d'água diagonal com texto transparente."""
img = Image.open(caminho_img).convert("RGBA")
larg, alt = img.size
# Camada transparente para a marca
marca = Image.new("RGBA", img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(marca)
try:
fonte = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 40)
except:
fonte = ImageFont.load_default()
# Repete o texto em grade diagonal
for x in range(-larg, larg * 2, 300):
for y in range(-alt, alt * 2, 200):
draw.text(
(x, y), texto,
font=fonte,
fill=(200, 200, 200, opacidade),
anchor="mm"
)
# Rotacionar individualmente não é direto — usamos imagem rotacionada
# Rotaciona a marca 45 graus
marca = marca.rotate(45)
resultado = Image.alpha_composite(img, marca)
resultado.convert("RGB").save(saida)
print(f"Marca d'água adicionada: {saida}")
def gerar_certificado(
nome_aluno: str,
curso: str,
data: str,
saida: str,
largura: int = 800,
altura: int = 560
):
"""Gera certificado de conclusão."""
# Fundo com gradiente simulado
img = Image.new("RGB", (largura, altura), color=(245, 245, 250))
draw = ImageDraw.Draw(img)
# Borda decorativa
for i in range(5):
cor = (30 + i * 10, 60 + i * 5, 120 + i * 5)
draw.rectangle([i, i, largura - i, altura - i], outline=cor, width=1)
# Faixa superior
draw.rectangle([0, 0, largura, 80], fill=(30, 60, 120))
try:
fonte_titulo = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32)
fonte_nome = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 36)
fonte_normal = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
fonte_pequena = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16)
except:
fonte_titulo = fonte_nome = fonte_normal = fonte_pequena = ImageFont.load_default()
# Textos
draw.text((largura // 2, 40), "CERTIFICADO DE CONCLUSÃO",
font=fonte_titulo, fill=(255, 255, 255), anchor="mm")
draw.text((largura // 2, 160), "Certificamos que",
font=fonte_normal, fill=(80, 80, 80), anchor="mm")
draw.text((largura // 2, 220), nome_aluno.upper(),
font=fonte_nome, fill=(30, 60, 120), anchor="mm")
# Linha decorativa sob o nome
draw.line([(largura//2 - 200, 250), (largura//2 + 200, 250)],
fill=(30, 60, 120), width=2)
draw.text((largura // 2, 290),
"concluiu com êxito o curso de",
font=fonte_normal, fill=(80, 80, 80), anchor="mm")
draw.text((largura // 2, 340), curso,
font=fonte_titulo, fill=(30, 60, 120), anchor="mm")
draw.text((largura // 2, 420),
f"Emitido em {data}",
font=fonte_pequena, fill=(120, 120, 120), anchor="mm")
# Linha de assinatura
draw.line([(200, 480), (400, 480)], fill=(80, 80, 80), width=1)
draw.text((300, 495), "Prof. Ricardo Matos",
font=fonte_pequena, fill=(80, 80, 80), anchor="mm")
img.save(saida, quality=95)
print(f"Certificado gerado: {saida}")
return img
# Gerando certificado
gerar_certificado(
nome_aluno="Ana Silva",
curso="Dominando o Python em 1 Ano",
data="15/03/2024",
saida="certificado_ana.jpg"
)
OpenCV: Processamento de Imagens
import cv2
import numpy as np
import matplotlib.pyplot as plt
def exibir_imagens(imagens: dict, titulo: str = ""):
"""Exibe múltiplas imagens lado a lado com matplotlib."""
n = len(imagens)
fig, axes = plt.subplots(1, n, figsize=(5 * n, 5))
if n == 1:
axes = [axes]
for ax, (nome, img) in zip(axes, imagens.items()):
if len(img.shape) == 3:
ax.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
else:
ax.imshow(img, cmap="gray")
ax.set_title(nome)
ax.axis("off")
plt.suptitle(titulo)
plt.tight_layout()
plt.savefig(f"{titulo.lower().replace(' ', '_')}.png", dpi=120)
plt.show()
# Leitura e operações básicas
img = cv2.imread("foto.jpg")
print(f"Shape: {img.shape}") # (altura, largura, canais)
print(f"Dtype: {img.dtype}")
# Conversão de espaços de cor
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Redimensionamento
img_metade = cv2.resize(img, (img.shape[1]//2, img.shape[0]//2))
img_200x200 = cv2.resize(img, (200, 200), interpolation=cv2.INTER_CUBIC)
# Recorte (numpy slicing)
img_crop = img[100:300, 150:400]
# Rotação
centro = (img.shape[1]//2, img.shape[0]//2)
matriz = cv2.getRotationMatrix2D(centro, 45, 1.0)
img_rot = cv2.warpAffine(img, matriz, (img.shape[1], img.shape[0]))
exibir_imagens({
"Original": img,
"Cinza": img_gray,
"Recorte": img_crop,
"Rotacionado": img_rot,
}, "Operações Básicas")
Detecção de Bordas e Contornos
def detectar_bordas(img_path: str):
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Suavização antes da detecção de bordas
blur = cv2.GaussianBlur(gray, (5, 5), 0)
# Canny — algoritmo clássico de detecção de bordas
canny = cv2.Canny(blur, threshold1=50, threshold2=150)
# Sobel — gradiente nas direções X e Y
sobel_x = cv2.Sobel(blur, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(blur, cv2.CV_64F, 0, 1, ksize=3)
sobel = cv2.magnitude(sobel_x, sobel_y).astype(np.uint8)
# Laplaciano
laplacian = cv2.Laplacian(blur, cv2.CV_64F)
laplacian = np.absolute(laplacian).astype(np.uint8)
exibir_imagens({
"Original": gray,
"Canny": canny,
"Sobel": sobel,
"Laplaciano": laplacian,
}, "Detecção de Bordas")
return canny
def encontrar_contornos(img_path: str):
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
_, thresh = cv2.threshold(blur, 127, 255, cv2.THRESH_BINARY)
contornos, hierarquia = cv2.findContours(
thresh,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
img_contornos = img.copy()
cv2.drawContours(img_contornos, contornos, -1, (0, 255, 0), 2)
print(f"Contornos encontrados: {len(contornos)}")
# Filtrando por área
contornos_grandes = [c for c in contornos if cv2.contourArea(c) > 500]
for c in contornos_grandes:
x, y, w, h = cv2.boundingRect(c)
area = cv2.contourArea(c)
perimetro = cv2.arcLength(c, closed=True)
cv2.rectangle(img_contornos, (x, y), (x+w, y+h), (255, 0, 0), 1)
print(f" Contorno: área={area:.0f}, perímetro={perimetro:.0f}")
return img_contornos, contornos
Detecção de Faces com Haar Cascades
def detectar_faces(img_path: str, saida: str = "faces_detectadas.jpg"):
"""Detecção de faces usando classificadores Haar."""
# Carregando classificador pré-treinado
detector_face = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
)
detector_olhos = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_eye.xml"
)
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Detectando faces
faces = detector_face.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30),
flags=cv2.CASCADE_SCALE_IMAGE
)
print(f"Faces detectadas: {len(faces)}")
for i, (x, y, w, h) in enumerate(faces):
# Desenhando retângulo ao redor da face
cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.putText(
img, f"Face {i+1}",
(x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7,
(0, 255, 0), 2
)
# Detectando olhos dentro de cada face
roi_gray = gray[y:y+h, x:x+w]
roi_img = img[y:y+h, x:x+w]
olhos = detector_olhos.detectMultiScale(roi_gray)
for (ox, oy, ow, oh) in olhos:
cv2.circle(
roi_img,
(ox + ow//2, oy + oh//2),
ow//2,
(255, 0, 0), 2
)
cv2.imwrite(saida, img)
print(f"Imagem salva: {saida}")
return img, faces
Processamento de Vídeo
def processar_video(
fonte: int | str = 0,
operacao: str = "gray",
saida: str = None
):
"""
Processa vídeo em tempo real ou arquivo.
fonte=0 usa webcam; fonte="video.mp4" usa arquivo.
"""
cap = cv2.VideoCapture(fonte)
if not cap.isOpened():
print("Não foi possível abrir a fonte de vídeo.")
return
# Configurações do vídeo
fps = cap.get(cv2.CAP_PROP_FPS)
largura = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
altura = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"Resolução: {largura}×{altura} @ {fps:.1f}fps")
# Writer para salvar
writer = None
if saida:
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter(saida, fourcc, fps, (largura, altura))
detector_face = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
)
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
frame_count += 1
# Aplicando operação escolhida
if operacao == "gray":
processado = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
processado = cv2.cvtColor(processado, cv2.COLOR_GRAY2BGR)
elif operacao == "blur":
processado = cv2.GaussianBlur(frame, (15, 15), 0)
elif operacao == "canny":
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150)
processado = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
elif operacao == "faces":
processado = frame.copy()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
faces = detector_face.detectMultiScale(gray, 1.1, 5)
for (x, y, w, h) in faces:
cv2.rectangle(processado, (x, y), (x+w, y+h), (0, 255, 0), 2)
else:
processado = frame
# Informações no frame
cv2.putText(
processado,
f"Frame: {frame_count} | {operacao.upper()}",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7,
(0, 255, 0), 2
)
if writer:
writer.write(processado)
# Exibe ao vivo (comentar em ambientes sem display)
# cv2.imshow("Video", processado)
# if cv2.waitKey(1) & 0xFF == ord("q"):
# break
# Para arquivos: limita a 100 frames no exemplo
if isinstance(fonte, str) and frame_count >= 100:
break
cap.release()
if writer:
writer.release()
cv2.destroyAllWindows()
print(f"Processamento concluído: {frame_count} frames")
OCR: Lendo Texto de Imagens
pip install pytesseract
# Instalar Tesseract: https://github.com/tesseract-ocr/tesseract
import pytesseract
from PIL import Image
import cv2
import numpy as np
def preprocessar_para_ocr(img_path: str) -> np.ndarray:
"""Prepara imagem para melhor resultado no OCR."""
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Redimensiona para melhor reconhecimento
scale = 2.0
gray = cv2.resize(gray, None, fx=scale, fy=scale,
interpolation=cv2.INTER_CUBIC)
# Remove ruído
denoised = cv2.fastNlMeansDenoising(gray, h=10)
# Binarização adaptativa
binaria = cv2.adaptiveThreshold(
denoised, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2
)
return binaria
def extrair_texto(img_path: str, idioma: str = "por") -> dict:
"""Extrai texto de imagem usando Tesseract OCR."""
img_proc = preprocessar_para_ocr(img_path)
img_pil = Image.fromarray(img_proc)
# Configuração do Tesseract
config = "--psm 6 --oem 3" # PSM 6: bloco uniforme de texto
# Texto simples
texto = pytesseract.image_to_string(
img_pil, lang=idioma, config=config
)
# Dados detalhados por palavra
dados = pytesseract.image_to_data(
img_pil, lang=idioma,
output_type=pytesseract.Output.DICT
)
# Filtra palavras com confiança > 60%
palavras_confiaveis = [
{
"texto": dados["text"][i],
"confianca": dados["conf"][i],
"x": dados["left"][i], "y": dados["top"][i],
"w": dados["width"][i], "h": dados["height"][i]
}
for i in range(len(dados["text"]))
if int(dados["conf"][i]) > 60 and dados["text"][i].strip()
]
return {
"texto": texto.strip(),
"palavras": palavras_confiaveis,
"total": len(palavras_confiaveis)
}
Exemplo Completo: Pipeline de Análise de Documentos
from PIL import Image, ImageDraw, ImageFont
import cv2
import numpy as np
from pathlib import Path
from dataclasses import dataclass
from typing import List, Tuple
import json
@dataclass
class DocumentoAnalisado:
caminho: str
largura: int
altura: int
faces_detectadas: int
contornos: int
texto_extraido: str
qualidade: float
class AnalisadorDocumentos:
def __init__(self):
self.detector_face = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
)
def avaliar_qualidade(self, img: np.ndarray) -> float:
"""Avalia qualidade da imagem pelo desvio padrão do Laplaciano."""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
return float(laplacian.var())
def detectar_faces(self, img: np.ndarray) -> List[Tuple]:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = self.detector_face.detectMultiScale(gray, 1.1, 5)
return list(faces)
def contar_contornos(self, img: np.ndarray) -> int:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
canny = cv2.Canny(blur, 50, 150)
contornos, _ = cv2.findContours(
canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
return len([c for c in contornos if cv2.contourArea(c) > 200])
def anotar_imagem(
self,
img: np.ndarray,
faces: list,
qualidade: float
) -> np.ndarray:
resultado = img.copy()
# Anotando faces
for i, (x, y, w, h) in enumerate(faces):
cv2.rectangle(resultado, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.putText(resultado, f"Face {i+1}",
(x, y-5), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (0, 255, 0), 1)
# Informações da qualidade
status_qual = "Alta" if qualidade > 500 else ("Media" if qualidade > 100 else "Baixa")
cor_qual = (0,255,0) if qualidade > 500 else ((0,165,255) if qualidade > 100 else (0,0,255))
cv2.putText(resultado, f"Qualidade: {status_qual} ({qualidade:.0f})",
(10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, cor_qual, 2)
return resultado
def analisar(self, caminho: str, salvar_anotado: bool = True) -> DocumentoAnalisado:
img = cv2.imread(caminho)
if img is None:
raise ValueError(f"Não foi possível carregar: {caminho}")
altura, largura = img.shape[:2]
qualidade = self.avaliar_qualidade(img)
faces = self.detectar_faces(img)
n_contornos = self.contar_contornos(img)
texto = "" # OCR opcional
doc = DocumentoAnalisado(
caminho= caminho,
largura= largura,
altura= altura,
faces_detectadas= len(faces),
contornos= n_contornos,
texto_extraido= texto,
qualidade= round(qualidade, 2)
)
if salvar_anotado:
anotado = self.anotar_imagem(img, faces, qualidade)
saida = caminho.replace(".", "_anotado.")
cv2.imwrite(saida, anotado)
print(f"Anotado salvo: {saida}")
return doc
def analisar_pasta(self, pasta: str) -> List[DocumentoAnalisado]:
extensoes = {".jpg", ".jpeg", ".png", ".bmp", ".tiff"}
arquivos = [
str(f) for f in Path(pasta).iterdir()
if f.suffix.lower() in extensoes
]
print(f"Analisando {len(arquivos)} imagens...")
resultados = []
for caminho in arquivos:
try:
doc = self.analisar(caminho)
resultados.append(doc)
print(f" ✓ {Path(caminho).name}: "
f"faces={doc.faces_detectadas}, "
f"qualidade={doc.qualidade:.0f}")
except Exception as e:
print(f" ✗ {Path(caminho).name}: {e}")
# Relatório
total_faces = sum(d.faces_detectadas for d in resultados)
media_qual = sum(d.qualidade for d in resultados) / len(resultados)
print(f"\nResumo: {len(resultados)} imagens processadas")
print(f" Total de faces: {total_faces}")
print(f" Qualidade média: {media_qual:.0f}")
return resultados
# Gerando imagem de teste
def criar_imagem_teste(saida: str = "teste.jpg"):
"""Cria imagem de teste com formas geométricas."""
img = np.ones((400, 600, 3), dtype=np.uint8) * 240
cv2.rectangle(img, (50, 50), (200, 200), (30, 60, 120), -1)
cv2.circle(img, (350, 150), 80, (0, 128, 0), -1)
cv2.ellipse(img, (500, 300), (80, 50),
45, 0, 360, (128, 0, 128), -1)
cv2.putText(img, "Documento Teste",
(150, 350), cv2.FONT_HERSHEY_SIMPLEX,
1.0, (0, 0, 0), 2)
cv2.imwrite(saida, img)
print(f"Imagem de teste criada: {saida}")
return saida
# Executando o pipeline
caminho_teste = criar_imagem_teste()
analisador = AnalisadorDocumentos()
resultado = analisador.analisar(caminho_teste)
print(f"\n=== Resultado da Análise ===")
print(f"Arquivo: {resultado.caminho}")
print(f"Dimensões: {resultado.largura}×{resultado.altura}")
print(f"Faces: {resultado.faces_detectadas}")
print(f"Contornos: {resultado.contornos}")
print(f"Qualidade: {resultado.qualidade}")
Resumo
- Pillow manipula imagens com API Pythonica — redimensiona, recorta, filtra e desenha
- OpenCV oferece processamento mais rápido e algoritmos avançados de visão computacional
- Canny e Sobel detectam bordas;
findContoursextrai formas geométricas - Haar Cascades detectam faces e olhos em tempo real com alta performance
VideoCaptureprocessa webcam e arquivos de vídeo frame a frame- Tesseract OCR extrai texto de imagens com precisão dependente do pré-processamento
- Qualidade de imagem pode ser medida pelo desvio padrão do Laplaciano — maior = mais nítida
- Pipelines de análise de documentos combinam detecção, OCR e anotação de forma automatizada
Referências e Leituras Complementares
- Pillow — documentação oficial — https://pillow.readthedocs.io/en/stable/
- OpenCV Python — documentação — https://docs.opencv.org/4.x/d6/d00/tutorial_py_root.html
- Tesseract OCR — https://github.com/tesseract-ocr/tesseract
- pytesseract — https://github.com/madmaze/pytesseract
- Haar Cascades pré-treinados — https://github.com/opencv/opencv/tree/master/data/haarcascades
- BRADSKI, Gary; KAEHLER, Adrian. Learning OpenCV 3. O'Reilly Media, 2016. — referência clássica e completa de OpenCV.
- SZELISKI, Richard. Computer Vision: Algorithms and Applications. 2. ed. Springer, 2022. — fundamentos teóricos de visão computacional, disponível em https://szeliski.org/Book/
Prof. Ricardo Matos — Dominando o Python em 1 Ano · Artigo 39 de 52 Próximo: Artigo 40 — Análise de Dados com Pandas Avançado e Plotly
you asked
Sim