9  Deep Learning para Visão Computacional

🚧 Em construção!

Ao longo desta parte do livro, um padrão se repetiu com insistência. No Capítulo 7, descritores de cor, textura (LBP) e forma (HOG) foram projetados manualmente, e suas limitações — especificidade, dependência de hiperparâmetros, incapacidade de capturar semântica de alto nível — foram discutidas abertamente. No Capítulo 8, o mesmo padrão se repetiu com o ORB (descritor binário fixo) e o Haar Cascade (filtros retangulares fixos), cujo falso positivo real, observado experimentalmente, expôs de forma concreta o custo de depender de características genéricas e não-aprendidas.

Este capítulo final da Parte II apresenta a resposta da área a essa limitação recorrente: as Redes Neurais Convolucionais (Convolutional Neural Networks — CNNs). A mudança de paradigma é conceitualmente simples de enunciar, ainda que profunda em suas consequências: em vez de um especialista humano decidir, a priori, quais filtros aplicar a uma imagem (bordas de Sobel no Capítulo 3, blocos de contraste do LBP, pares de pixels do BRIEF, retângulos do Haar Cascade), os próprios filtros se tornam parâmetros aprendidos, ajustados automaticamente a partir de milhares de exemplos, para minimizar o erro na tarefa de interesse.

O capítulo caminha por três etapas: primeiro, os fundamentos de convolução e pooling aprendidos, e como eles se conectam à convolução fixa já estudada no Capítulo 3; segundo, o treinamento de uma CNN do zero e a técnica de transferência de aprendizado, que reaproveita características já aprendidas para acelerar o aprendizado de novas tarefas; por fim, o livro se encerra integrando tudo o que foi estudado — geometria computacional (homografia, correspondência, calibração) e aprendizado profundo — em três aplicações reais de Visão Computacional: realidade aumentada, fotogrametria e visão estereoscópica.

9.1 Objetivos do Capítulo

Ao final deste capítulo, o estudante deverá ser capaz de:

  • Compreender a convolução e o pooling como operações aprendidas, relacionando-as à convolução de kernels fixos já estudada no Capítulo 3;
  • Descrever a arquitetura típica de uma CNN (blocos de convolução, ativação e pooling, seguidos de camadas totalmente conectadas);
  • Implementar e treinar uma CNN do zero para uma tarefa de classificação de imagens, comparando seu desempenho ao dos classificadores clássicos do Capítulo 7;
  • Compreender e aplicar a técnica de transferência de aprendizado, identificando quando ela é vantajosa e quais são suas limitações;
  • Reconhecer, em linhas gerais, como modelos pré-treinados de grande escala são utilizados em classificação, detecção e segmentação;
  • Integrar geometria computacional e aprendizado profundo em aplicações reais: realidade aumentada, fotogrametria (medição a partir de imagens) e visão estereoscópica (estimativa de profundidade).

9.2 Panorama: Classificação, Detecção e Segmentação

Antes de introduzir as redes neurais convolucionais (CNNs), convém consolidar a terminologia clássica construída ao longo da Parte II. Cada capítulo tratou uma tarefa distinta de Visão Computacional, diferenciada essencialmente pela granularidade exigida na resposta. A Tabela 9.1 organiza essa taxonomia.

Tabela 9.1: Comparativo de granularidade e escopo das tarefas fundamentais de Visão Computacional.
Tarefa Pergunta Respondida Granularidade da Saída Capítulo de Estudo
Classificação “Qual é a classe desta imagem?” Um único rótulo para a imagem inteira Capítulo 7
Detecção de objetos “Quais objetos existem e onde estão?” Classe e caixa delimitadora (bounding box) por instância Capítulo 8
Segmentação semântica “A que classe pertence cada pixel?” Um rótulo de classe por pixel, sem distinguir indivíduos Capítulo 8
Segmentação de instâncias “Quais pixels formam cada objeto individual?” Um rótulo por pixel, identificando instâncias da mesma classe Capítulo 8
Segmentação panóptica Combinação das duas anteriores Classe e identificador de instância para cada pixel Capítulo 8

Não há concorrência entre tais conceitos; cada formulação atende a uma necessidade específica do pipeline de análise. Contar elementos demanda detecção ou segmentação de instâncias; delimitar uma região contínua de fundo (como céu ou estrada) requer segmentação semântica; rotular uma imagem de forma global, como ocorre no reconhecimento de dígitos, é plenamente resolvido pela classificação.

O diferencial a partir deste capítulo não reside na taxonomia — que permanece idêntica —, mas na origem das características utilizadas para resolver cada tarefa. A tabela de modelos modernos apresentada no Capítulo 8 (Tabela 8.2) antecipou o mapeamento entre tarefa e arquitetura profunda: a aplicação de modelos como YOLO, Faster R-CNN e SSD resolve a detecção; a rede U-Net atende à segmentação semântica; o modelo Mask R-CNN estende o processo à segmentação de instâncias; e o SAM generaliza o isolamento de objetos via prompts. Toda essa gama de arquiteturas compartilha o mesmo pilar: a extração de atributos por meio de filtros aprendidos diretamente dos dados, superando os descritores projetados manualmente, como HOG, ORB ou os filtros retangulares de Haar.

9.3 Configuração do Ambiente

Além das bibliotecas utilizadas nos capítulos anteriores, este capítulo introduz o PyTorch, um dos principais frameworks para aprendizado profundo, utilizado na construção e no treinamento de redes neurais convolucionais. O código a seguir verifica a disponibilidade das dependências necessárias e, caso o PyTorch ainda não esteja instalado, instala automaticamente a versão mais adequada ao hardware do computador: com suporte à GPU NVIDIA quando disponível ou, caso contrário, a versão otimizada para execução em CPU.

import importlib, subprocess, sys, shutil, os, urllib.request

for mod, pkg in {
    "cv2": "opencv-python",
    "skimage": "scikit-image",
    "numpy": "numpy",
    "sklearn": "scikit-learn",
    "matplotlib": "matplotlib",
}.items():
    try:
        importlib.import_module(mod)
    except ImportError:
        subprocess.run([sys.executable, "-m", "pip", "install", "-q", pkg], check=True)

try:
    import torch
except ImportError:
    args = (
        ["torch", "torchvision"]
        if shutil.which("nvidia-smi")
        else ["--index-url", "https://download.pytorch.org/whl/cpu",
              "torch", "torchvision"]
    )
    subprocess.run([sys.executable, "-m", "pip", "install", "-q", *args], check=True)
    import torch

import cv2
import numpy as np
import matplotlib.pyplot as plt
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from skimage import data as skdata

torch.manual_seed(42)

if not os.path.exists("morph.py"):
    urllib.request.urlretrieve(
        "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/morph.py",
        "morph.py",
    )

import morph
from morph import mm

device = "cuda" if torch.cuda.is_available() else "cpu"
gpu = f" ({torch.cuda.get_device_name(0)})" if device == "cuda" else ""

print(
    f"✅ Ambiente pronto. morph {getattr(morph,'__version__','local_file')} | "
    f"PyTorch {torch.__version__} | {device}{gpu}"
)
✅ Ambiente pronto. morph 1.1.5 | PyTorch 2.12.1+cpu | cpu

9.4 Da Convolução Fixa à Convolução Aprendida

O Capítulo 3 introduziu a convolução espacial com kernels fixos, como os operadores de Sobel, definidos manualmente para realçar bordas em uma direção específica. Os Capítulos 7 e 8 estenderam essa mesma lógica de “padrão fixo, definido por um especialista” aos descritores HOG, LBP, ORB e aos filtros retangulares do Haar Cascade. Em todos os casos, um humano decidiu, antecipadamente, o que procurar na imagem.

Uma Rede Neural Convolucional aplica a mesma operação matemática de convolução — a soma ponderada de uma vizinhança de pixels por um kernel — mas com uma diferença decisiva: os valores do kernel deixam de ser fixados manualmente e passam a ser parâmetros aprendidos, ajustados automaticamente por um algoritmo de otimização (descida de gradiente) de forma a minimizar o erro do modelo em uma tarefa específica, a partir de exemplos de treinamento rotulados.

9.4.1 Convolução como Camada Aprendida

Formalmente, a saída de uma camada convolucional na posição \((i,j)\), para um kernel \(K\) de tamanho \(k \times k\), é:

\[ F(i,j) = \sum_{u=0}^{k-1}\sum_{v=0}^{k-1} K(u,v) \cdot I(i+u,\, j+v), \]

exatamente a mesma operação do Capítulo 3 — a diferença está em como \(K\) é obtido: em vez de ser definido a priori, seus valores começam aleatórios e são ajustados a cada iteração de treinamento. Uma camada convolucional tipicamente aprende múltiplos kernels simultaneamente (múltiplos “filtros”), cada um produzindo um mapa de características (feature map) diferente — um pode aprender a responder a bordas verticais, outro a texturas específicas, outro a manchas de cor, sem que nenhum humano tenha especificado isso explicitamente.

Duas propriedades tornam essa ideia particularmente eficiente:

  • Compartilhamento de pesos (weight sharing): o mesmo kernel é aplicado a todas as posições da imagem, o que reduz drasticamente o número de parâmetros em comparação a uma camada totalmente conectada, e garante equivariância à translação — se o padrão se deslocar na imagem de entrada, sua detecção se desloca correspondentemente no mapa de características;
  • Hierarquia de características: camadas convolucionais empilhadas aprendem representações progressivamente mais abstratas — as primeiras camadas tendem a responder a bordas e texturas simples (de forma análoga aos filtros de Sobel e LBP), enquanto camadas mais profundas combinam essas respostas em padrões cada vez mais complexos e específicos da tarefa.

9.4.2 Pooling

Entre blocos de convolução, camadas de pooling reduzem a resolução espacial dos mapas de características, tipicamente pela operação de max-pooling, que retém o maior valor em cada janela local:

\[ P(i,j) = \max_{(u,v)\, \in\, \text{janela}(i,j)} F(u,v). \]

O pooling cumpre dois papéis: reduz o custo computacional das camadas seguintes, e confere certa invariância a pequenas translações — um padrão detectado ligeiramente deslocado ainda ativa a mesma região após o agrupamento.

9.4.3 Arquitetura Típica

Uma CNN de classificação organiza esses blocos em uma pilha:

Entrada → [Convolução → Ativação (ReLU) → Pooling] × N → Flatten → Camada(s) Totalmente Conectada(s) → Softmax

As camadas convolucionais iniciais extraem características cada vez mais abstratas da imagem; ao final, essas características são “achatadas” (flatten) em um vetor e passadas a camadas totalmente conectadas — a mesma estrutura de classificador com a qual o k-NN do Capítulo 7 operava, mas agora recebendo características aprendidas, e não artesanais.

O simulador a seguir tornar concreta a operação de convolução: uma imagem \(8\times8\) (com uma borda vertical e um bloco mais claro, propositalmente construída para tornar visíveis os diferentes efeitos de cada kernel) é percorrida por uma janela deslizante \(3\times3\). Experimente os diferentes kernels e avance passo a passo para observar como cada posição do mapa de características é calculada.

🎯 Simulador: Convolução Passo a Passo kernel 3×3, stride 1, sem preenchimento
Kernel:
Posição atual: (0, 0) de 6×6
Entrada (8×8) — janela atual destacada
Kernel (3×3)
Mapa de Características (6×6)
Figura 9.1: Simulador interativo de convolução: escolha um kernel e avance passo a passo (ou calcule tudo de uma vez) para observar a construção do mapa de características.
Nota🧠 Por que funciona? — Compartilhamento de pesos e hierarquia

Observe, no simulador, que o mesmo kernel de bordas é aplicado a todas as 36 posições da imagem — é exatamente essa reutilização (compartilhamento de pesos) que torna as CNNs muito mais econômicas em parâmetros do que seria uma camada totalmente conectada equivalente, e que garante que uma borda seja detectada da mesma forma independentemente de sua posição na imagem.

Também vale notar a diferença entre o kernel de bordas e o de realce: o primeiro tem resposta próxima de zero em regiões homogêneas e alta magnitude (positiva ou negativa) em transições de intensidade — o mesmo comportamento do operador de Sobel do Capítulo 3. O de realce, por sua vez, amplia o contraste local sem eliminar a intensidade de base. Em uma CNN real, esses kernels não são escolhidos manualmente como aqui — eles emergem do treinamento, mas frequentemente as primeiras camadas aprendem, de forma independente, filtros notavelmente semelhantes a detectores de borda e realce como estes.

9.5 Projeto Prático 1: Uma CNN do Zero para os Dígitos do Capítulo 7

Para tornar a comparação com o Capítulo 7 direta, esta CNN é treinada sobre a mesma base load_digits utilizada naquele capítulo — a diferença está inteiramente na forma como as características são obtidas: antes, pixels brutos ou HOG, calculados manualmente; agora, filtros convolucionais, aprendidos durante o treinamento.

digits = load_digits()
X = digits.images.astype(np.float32) / 16.0
y = digits.target

X_treino, X_teste, y_treino, y_teste = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

X_treino_t = torch.tensor(X_treino).unsqueeze(1)   # (N, 1, 8, 8)
y_treino_t = torch.tensor(y_treino, dtype=torch.long)
X_teste_t = torch.tensor(X_teste).unsqueeze(1)
y_teste_t = torch.tensor(y_teste, dtype=torch.long)

class CNNDigitos(nn.Module):
    # CNN simples: duas camadas convolucionais + pooling, seguidas de camadas densas.
    def __init__(self, n_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 8, kernel_size=3, padding=1)   # 8x8 -> 8x8, 8 filtros
        self.conv2 = nn.Conv2d(8, 16, kernel_size=3, padding=1)  # 4x4 -> 4x4, 16 filtros
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(16 * 2 * 2, 32)
        self.fc2 = nn.Linear(32, n_classes)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))  # 8x8 -> 4x4
        x = self.pool(self.relu(self.conv2(x)))  # 4x4 -> 2x2
        x = x.view(x.size(0), -1)
        x = self.relu(self.fc1(x))
        return self.fc2(x)

modelo_cnn = CNNDigitos()
print(f"Parâmetros treináveis: {sum(p.numel() for p in modelo_cnn.parameters())}")

otimizador = optim.Adam(modelo_cnn.parameters(), lr=1e-2)
criterio = nn.CrossEntropyLoss()

n = X_treino_t.size(0)
tam_lote = 32
epocas = 60
historico_perda, historico_acc = [], []

for epoca in range(epocas):
    modelo_cnn.train()
    perm = torch.randperm(n)
    perda_epoca = 0.0
    for i in range(0, n, tam_lote):
        idx = perm[i:i + tam_lote]
        otimizador.zero_grad()
        saida = modelo_cnn(X_treino_t[idx])
        perda = criterio(saida, y_treino_t[idx])
        perda.backward()
        otimizador.step()
        perda_epoca += perda.item() * len(idx)

    modelo_cnn.eval()
    with torch.no_grad():
        acc_teste = (modelo_cnn(X_teste_t).argmax(dim=1) == y_teste_t).float().mean().item()
    historico_perda.append(perda_epoca / n)
    historico_acc.append(acc_teste)

acc_final_cnn = historico_acc[-1]
print(f"Acurácia final da CNN no conjunto de teste: {acc_final_cnn:.4f}")

fig, ax1 = plt.subplots(figsize=(6, 4))
ax1.plot(historico_perda, color="#dc2626", label="Perda (treino)")
ax1.set_xlabel("Época")
ax1.set_ylabel("Perda", color="#dc2626")
ax2 = ax1.twinx()
ax2.plot(historico_acc, color="#2563eb", label="Acurácia (teste)")
ax2.set_ylabel("Acurácia (teste)", color="#2563eb")
plt.title("Treinamento da CNN — Base de Dígitos")
fig.tight_layout()
Parâmetros treináveis: 3658
Acurácia final da CNN no conjunto de teste: 0.9644
Figura 9.2: Curva de treinamento de uma CNN simples na base de dígitos: perda de treinamento e acurácia no conjunto de teste ao longo das épocas.

9.5.1 Comparando com o Capítulo 7

A tabela a seguir reúne os resultados obtidos na mesma base de dados, com as três abordagens estudadas ao longo do livro:

# Valores obtidos no Capítulo 7 (k-NN, k=3), reproduzidos aqui para comparação direta
ACC_KNN_PIXELS_CAP7 = 0.9844
ACC_KNN_HOG_CAP7 = 0.7578

metodos = ["k-NN\n(pixels brutos)", "k-NN\n(HOG)", "CNN\n(este capítulo)"]
acuracias = [ACC_KNN_PIXELS_CAP7, ACC_KNN_HOG_CAP7, acc_final_cnn]

plt.figure(figsize=(5, 4))
cores = ["#6366f1", "#f97316", "#16a34a"]
plt.bar(metodos, acuracias, color=cores)
plt.ylim(0, max(acuracias) + 0.08)
plt.ylabel("Acurácia (conjunto de teste)")
plt.title("Cap. 7 vs. Cap. 9 — Base de Dígitos")
for i, v in enumerate(acuracias):
    plt.text(i, v + 0.02, f"{v:.3f}", ha="center")
plt.tight_layout()
Figura 9.3: Comparação de acurácia entre os classificadores clássicos do Capítulo 7 (pixels brutos e HOG com k-NN) e a CNN treinada neste capítulo, na mesma base de dígitos.
Nota🧠 Por que funciona? — E por que a CNN nem sempre “ganha”

O resultado observado aqui repete o padrão já visto no Capítulo 7: a CNN, apesar de aprender automaticamente suas características, não supera necessariamente o k-NN com pixels brutos nesta base específica. A explicação é a mesma: load_digits é uma base pequena (menos de 1800 exemplos), com imagens já centralizadas, normalizadas e de baixíssima resolução (\(8\times8\)) — condições em que a comparação direta de intensidades já é altamente informativa, e há poucos dados para que a rede aprenda filtros verdadeiramente superiores aos descritores simples.

O verdadeiro diferencial das CNNs aparece em cenários que descritores artesanais e classificadores simples não conseguem endereçar: imagens maiores e mais realistas, com milhares de categorias, variação substancial de pose, iluminação e fundo, e conjuntos de treinamento massivos — exatamente o regime em que os modelos apresentados na seção “Aplicações em Larga Escala”, mais adiante, foram treinados. A lição pedagógica que atravessa os Capítulos 7, 8 e 9 deste livro é consistente: a sofisticação de um método deve ser proporcional à complexidade do problema — usar uma CNN para um problema que um k-NN resolve igualmente bem é desperdício de recursos computacionais, não uma virtude.

9.6 Transferência de Aprendizado

Treinar uma CNN do zero, como no projeto anterior, exige dados suficientes para ajustar todos os seus parâmetros de forma confiável — uma limitação severa quando se dispõe de poucos exemplos rotulados para a tarefa de interesse. A transferência de aprendizado (transfer learning) contorna essa limitação reaproveitando um extrator de características já treinado em uma tarefa relacionada:

  1. Treina-se (ou obtém-se já treinada) uma CNN em uma tarefa de origem, tipicamente com muitos dados disponíveis;
  2. As camadas convolucionais iniciais — que tendem a aprender características gerais, como bordas e texturas, pouco específicas da tarefa de origem — são congeladas (seus pesos deixam de ser atualizados);
  3. Apenas as camadas finais (geralmente a(s) camada(s) totalmente conectada(s)) são substituídas e treinadas do zero para a nova tarefa, com muito menos dados e muito menos tempo de treinamento do que seriam necessários para treinar a rede inteira.

Esse princípio é o motivo pelo qual modelos pré-treinados em bases massivas, como a ImageNet (mais de um milhão de imagens em mil categorias), tornaram-se o ponto de partida padrão de praticamente qualquer projeto moderno de visão computacional, mesmo quando a tarefa final é bastante diferente da classificação de objetos genéricos.

# Divide as classes de dígitos em dois domínios disjuntos:
# Domínio A (0-4): usado para treinar o extrator de características "pré-treinado"
# Domínio B (5-9): tarefa de destino, com poucos dados disponíveis
classes_A, classes_B = [0, 1, 2, 3, 4], [5, 6, 7, 8, 9]
mask_A, mask_B = np.isin(y, classes_A), np.isin(y, classes_B)
XA, yA = X[mask_A], y[mask_A]
XB, yB = X[mask_B], y[mask_B] - 5  # reindexa para 0..4

XA_tr, XA_te, yA_tr, yA_te = train_test_split(XA, yA, test_size=0.25, random_state=42, stratify=yA)
XB_tr, XB_te, yB_tr, yB_te = train_test_split(XB, yB, test_size=0.25, random_state=42, stratify=yB)

# Simula escassez de dados no domínio de destino: apenas 20 exemplos rotulados
rng = np.random.default_rng(0)
idx_poucos = rng.choice(len(XB_tr), size=20, replace=False)
XB_tr_poucos, yB_tr_poucos = XB_tr[idx_poucos], yB_tr[idx_poucos]

def para_tensor(Ximg, yarr):
    return torch.tensor(Ximg).unsqueeze(1), torch.tensor(yarr, dtype=torch.long)

XA_tr_t, yA_tr_t = para_tensor(XA_tr, yA_tr)
XA_te_t, yA_te_t = para_tensor(XA_te, yA_te)
XB_tr_t, yB_tr_t = para_tensor(XB_tr_poucos, yB_tr_poucos)
XB_te_t, yB_te_t = para_tensor(XB_te, yB_te)

class ExtratorConv(nn.Module):
    # Bloco convolucional reaproveitável entre tarefas.
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
        self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.pool(self.relu(self.conv1(x)))
        x = self.pool(self.relu(self.conv2(x)))
        return x.view(x.size(0), -1)

class CNNCompleta(nn.Module):
    def __init__(self, n_classes):
        super().__init__()
        self.extrator = ExtratorConv()
        self.fc1 = nn.Linear(64, 32)
        self.fc2 = nn.Linear(32, n_classes)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.extrator(x)
        x = self.relu(self.fc1(x))
        return self.fc2(x)

def treinar(modelo, X_t, y_t, epocas, lr, tam_lote=16, parametros=None):
    params = parametros if parametros is not None else modelo.parameters()
    otim = optim.Adam(params, lr=lr)
    crit = nn.CrossEntropyLoss()
    n_amostras = X_t.size(0)
    for _ in range(epocas):
        perm = torch.randperm(n_amostras)
        for i in range(0, n_amostras, tam_lote):
            idx = perm[i:i + tam_lote]
            otim.zero_grad()
            perda = crit(modelo(X_t[idx]), y_t[idx])
            perda.backward()
            otim.step()

def calcular_acuracia(modelo, X_t, y_t):
    modelo.eval()
    with torch.no_grad():
        pred = modelo(X_t).argmax(dim=1)
    return (pred == y_t).float().mean().item()

# 1) Treina o modelo "de origem" no domínio A
modelo_origem = CNNCompleta(n_classes=5)
treinar(modelo_origem, XA_tr_t, yA_tr_t, epocas=40, lr=1e-2)
print(f"Acurácia no domínio de origem A (dígitos 0-4): {calcular_acuracia(modelo_origem, XA_te_t, yA_te_t):.4f}")

# 2) Transferência: reaproveita o extrator (congelado), treina apenas a nova cabeça
modelo_transferencia = CNNCompleta(n_classes=5)
modelo_transferencia.extrator.load_state_dict(modelo_origem.extrator.state_dict())
for p in modelo_transferencia.extrator.parameters():
    p.requires_grad = False

treinar(
    modelo_transferencia, XB_tr_t, yB_tr_t, epocas=40, lr=1e-2,
    parametros=list(modelo_transferencia.fc1.parameters()) + list(modelo_transferencia.fc2.parameters())
)
acc_transferencia = calcular_acuracia(modelo_transferencia, XB_te_t, yB_te_t)

# 3) Do zero: mesmo pouco dado, mesmas épocas, mas sem reaproveitar nada
modelo_do_zero = CNNCompleta(n_classes=5)
treinar(modelo_do_zero, XB_tr_t, yB_tr_t, epocas=40, lr=1e-2)
acc_do_zero = calcular_acuracia(modelo_do_zero, XB_te_t, yB_te_t)

print(f"Domínio de destino B (dígitos 5-9), apenas {len(XB_tr_poucos)} exemplos de treino:")
print(f"  Com transferência (extrator congelado): {acc_transferencia:.4f}")
print(f"  Treinando do zero (mesmos dados/épocas): {acc_do_zero:.4f}")

plt.figure(figsize=(4.5, 4))
plt.bar(["Do zero", "Transferência"], [acc_do_zero, acc_transferencia], color=["#dc2626", "#16a34a"])
plt.ylim(0, max([acc_do_zero, acc_transferencia]) + 0.08)
plt.ylabel("Acurácia no domínio B (teste)")
plt.title(f"Efeito da Transferência ({len(XB_tr_poucos)} exemplos de treino)")
for i, v in enumerate([acc_do_zero, acc_transferencia]):
    plt.text(i, v + 0.02, f"{v:.3f}", ha="center")
plt.tight_layout()
Acurácia no domínio de origem A (dígitos 0-4): 1.0000
Domínio de destino B (dígitos 5-9), apenas 20 exemplos de treino:
  Com transferência (extrator congelado): 0.7277
  Treinando do zero (mesmos dados/épocas): 0.6786
Figura 9.4: Comparação de acurácia entre treinar do zero e reaproveitar (transferir) um extrator de características já treinado, ambos com o mesmo pequeno número de exemplos rotulados na tarefa de destino.
Nota🧠 Por que funciona? — E quando não funciona

Com apenas 20 exemplos rotulados no domínio de destino, o modelo treinado do zero mal consegue superar um classificador aleatório — não há dados suficientes para ajustar todos os parâmetros da rede de forma confiável. O modelo com transferência de aprendizado, por outro lado, já parte de um extrator de características capaz de identificar bordas, curvas e traços característicos de dígitos manuscritos em geral (aprendidos a partir dos dígitos 0–4) — restando à nova cabeça apenas a tarefa, muito mais simples, de aprender a combinar essas características já úteis para distinguir os dígitos 5–9.

Quando a transferência ajuda: quando a tarefa de origem e a de destino compartilham estatísticas visuais de baixo e médio nível — o que é razoável esperar entre dígitos manuscritos diferentes, como neste experimento, ou, mais amplamente, entre a maioria das imagens naturais e uma tarefa de nicho.

Quando a transferência falha ou até prejudica (fenômeno conhecido como transferência negativa): quando o domínio de origem é excessivamente diferente do domínio de destino — por exemplo, transferir um extrator treinado em fotografias naturais para radiografias médicas ou imagens de satélite pode não trazer benefício algum, e em alguns casos até atrapalhar, se as características de baixo nível relevantes forem muito distintas entre os dois domínios. Nesses casos, um ajuste fino (fine-tuning) parcial das camadas convolucionais — em vez de congelá-las por completo — costuma produzir melhores resultados.

9.7 Aplicações em Larga Escala com Modelos Pré-treinados

Os experimentos anteriores utilizaram CNNs pequenas, treinadas em minutos em uma base de poucas centenas de exemplos. Na prática profissional, é extremamente comum utilizar diretamente modelos já treinados em bases massivas — como a ImageNet, com mais de um milhão de imagens em mil categorias — como ponto de partida (via transferência de aprendizado) ou mesmo diretamente, sem qualquer re-treinamento.

Importante

As células desta seção não são executadas automaticamente neste notebook: o carregamento desses modelos exige o download de pesos pré-treinados (centenas de megabytes) diretamente dos servidores do PyTorch, o que requer conexão à internet no ambiente de execução. O código é apresentado tal como seria executado em um ambiente com conectividade, para fins de referência.

9.7.1 Classificação de Imagens

O padrão de uso é sempre o mesmo: carregar a arquitetura com pesos pré-treinados e, para uma nova tarefa, substituir apenas a camada final, como discutido na seção de transferência de aprendizado.

import torchvision.models as models

# Carrega uma ResNet-18 pré-treinada na ImageNet (requer internet)
modelo_resnet = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)

# Transferência de aprendizado: congela o extrator de características
for parametro in modelo_resnet.parameters():
    parametro.requires_grad = False

# Substitui a camada final (originalmente 1000 classes da ImageNet)
# por uma nova camada adequada à tarefa de destino (ex.: 3 classes)
n_classes_destino = 3
modelo_resnet.fc = nn.Linear(modelo_resnet.fc.in_features, n_classes_destino)

# A partir daqui, apenas `modelo_resnet.fc` seria treinada com os dados da nova tarefa

9.7.2 Detecção de Objetos

Modelos como o Faster R-CNN, apresentado no Capítulo 8, estão disponíveis prontos para uso, já treinados para detectar até 91 categorias de objetos comuns (base COCO):

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
from torchvision.models.detection import fasterrcnn_resnet50_fpn
from torchvision.transforms.functional import to_tensor

from PIL import Image
from urllib.request import urlopen

img = Image.open(urlopen(
    "https://raw.githubusercontent.com/pytorch/hub/master/images/dog.jpg"
)).convert("RGB")
modelo = fasterrcnn_resnet50_fpn(weights="DEFAULT").eval()

with torch.no_grad():
    pred = modelo([to_tensor(img)])[0]

fig, ax = plt.subplots(figsize=(6, 4))
ax.imshow(img)

for box, score in zip(pred["boxes"], pred["scores"]):
    if score < 0.8:
        continue
    x1, y1, x2, y2 = box.numpy()
    ax.add_patch(patches.Rectangle((x1, y1), x2-x1, y2-y1,
                                   fill=False, edgecolor="red", linewidth=2))

ax.set_axis_off()
plt.show()

9.7.3 Segmentação Semântica

De forma análoga, modelos de segmentação semântica pré-treinados retornam diretamente um mapa de classes por pixel:

import torch
import matplotlib.pyplot as plt
from PIL import Image
from urllib.request import urlopen
from torchvision.transforms.functional import to_tensor
from torchvision.models.segmentation import deeplabv3_resnet50

img = Image.open(urlopen(
    "https://raw.githubusercontent.com/pytorch/hub/master/images/dog.jpg"
)).convert("RGB")

modelo = deeplabv3_resnet50(weights="DEFAULT").eval()

with torch.no_grad():
    mapa = modelo(to_tensor(img).unsqueeze(0))["out"].argmax(1).squeeze()

plt.imshow(mapa)
plt.axis("off")
plt.show()

9.8 Projeto Prático 2: Detecção de Objetos com YOLO e Transferência de Aprendizado

A subseção anterior mostrou, por meio do Faster R-CNN, um modelo de detecção pré-treinado sendo usado “como está”, sem qualquer re-treinamento. O Faster R-CNN pertence à família de detectores de dois estágios: uma primeira rede propõe regiões candidatas a conter um objeto (region proposals) e uma segunda rede classifica e refina cada região — a mesma lógica de “propor, depois verificar” já discutida no Capítulo 8 a propósito do Haar Cascade em cascata.

A família YOLO (You Only Look Once) segue uma filosofia distinta, de um único estágio: a imagem é dividida conceitualmente em uma grade, e uma única rede convolucional prevê, em uma única passagem direta, as caixas delimitadoras, a confiança de haver um objeto e a classe de cada região — sem uma etapa separada de proposta de regiões. Essa simplificação é o que torna a família YOLO especialmente adequada à detecção em tempo real, citada na seção de encerramento deste capítulo como uma das direções naturais de aprofundamento.

Nesta seção, o princípio de transferência de aprendizado — já demonstrado para classificação, com uma CNN pequena treinada nos dígitos do Capítulo 7 — é aplicado a uma tarefa de detecção: partindo de um YOLO pré-treinado na base COCO (fotografias naturais, 80 categorias), o modelo é ajustado (fine-tuned) para uma tarefa de detecção bastante diferente — localizar e classificar objetos geométricos sintéticos (triângulos, quadrados, estrelas, etc.), variando em cor, tamanho e rotação.

9.8.1 Gerando um Conjunto de Dados Sintético de Objetos Geométricos

O cenário reproduzido aqui é inspirado em uma avaliação prática comum em disciplinas de Visão Computacional: dado um conjunto de imagens contendo objetos geométricos gerados aleatoriamente, treinar um detector capaz de localizar e classificar cada objeto em nove classes:

classes = ['Triangle', 'Square', 'Pentagon', 'Hexagon',
           'Heptagon', 'Circle', 'Ellipse', 'Star', 'Cross']

Cada objeto varia aleatoriamente em cor, tamanho e rotação, e cada imagem pode conter um ou mais objetos. Para cada imagem, um arquivo TXT associado descreve, em uma linha por objeto, a classe e a caixa delimitadora no formato YOLO: a classe seguida de quatro valores normalizados entre 0 e 1 — centro em \(x\), centro em \(y\), largura e altura, todos expressos como fração da largura ou da altura da imagem, por exemplo:

0 0.2895 0.2007 0.3750 0.2862

Essa é exatamente a convenção de anotação esperada pelas bibliotecas de treinamento de YOLO (como a ultralytics, usada a seguir) — de modo que, caso o leitor tenha acesso a um conjunto de imagens reais anotado neste mesmo formato, basta organizá-lo na estrutura de pastas descrita adiante para reutilizar todo o restante do código sem qualquer alteração.

Como não há, neste notebook, acesso a um conjunto de imagens reais, o código a seguir gera sinteticamente um pequeno conjunto de dados com as mesmas características do cenário descrito: formas geométricas com cor, tamanho e rotação aleatórios, desenhadas com o OpenCV, já anotadas no formato YOLO.

import os, random

CLASSES = ['Triangle', 'Square', 'Pentagon', 'Hexagon',
           'Heptagon', 'Circle', 'Ellipse', 'Star', 'Cross']
N_LADOS = {'Triangle': 3, 'Square': 4, 'Pentagon': 5, 'Hexagon': 6, 'Heptagon': 7}


def poligono_regular(cx, cy, r, n_lados, rot_graus):
    ang0 = np.deg2rad(rot_graus - 90)
    angs = ang0 + 2 * np.pi * np.arange(n_lados) / n_lados
    return np.stack([cx + r * np.cos(angs), cy + r * np.sin(angs)], axis=1)


def poligono_estrela(cx, cy, r_externo, rot_graus, n_pontas=5):
    r_interno = r_externo * 0.45
    ang0 = np.deg2rad(rot_graus - 90)
    angs = ang0 + np.pi * np.arange(2 * n_pontas) / n_pontas
    raios = np.where(np.arange(2 * n_pontas) % 2 == 0, r_externo, r_interno)
    return np.stack([cx + raios * np.cos(angs), cy + raios * np.sin(angs)], axis=1)


def poligono_cruz(cx, cy, r, rot_graus, espessura_rel=0.35):
    w = r * espessura_rel
    base = np.array([
        (-w, -r), (w, -r), (w, -w), (r, -w), (r, w), (w, w),
        (w, r), (-w, r), (-w, w), (-r, w), (-r, -w), (-w, -w),
    ])
    theta = np.deg2rad(rot_graus)
    R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
    return base @ R.T + np.array([cx, cy])


def desenha_objeto(img, classe_idx, cx, cy, tamanho, rotacao, cor):
    nome = CLASSES[classe_idx]
    if nome in N_LADOS:
        pts = poligono_regular(cx, cy, tamanho, N_LADOS[nome], rotacao)
        cv2.fillPoly(img, [pts.astype(np.int32)], cor)
        xs, ys = pts[:, 0], pts[:, 1]
    elif nome == 'Star':
        pts = poligono_estrela(cx, cy, tamanho, rotacao)
        cv2.fillPoly(img, [pts.astype(np.int32)], cor)
        xs, ys = pts[:, 0], pts[:, 1]
    elif nome == 'Cross':
        pts = poligono_cruz(cx, cy, tamanho, rotacao)
        cv2.fillPoly(img, [pts.astype(np.int32)], cor)
        xs, ys = pts[:, 0], pts[:, 1]
    elif nome == 'Circle':
        cv2.circle(img, (int(cx), int(cy)), int(tamanho), cor, -1)
        xs, ys = np.array([cx - tamanho, cx + tamanho]), np.array([cy - tamanho, cy + tamanho])
    else:  # Ellipse
        eixo = (int(tamanho), int(tamanho * 0.6))
        cv2.ellipse(img, (int(cx), int(cy)), eixo, rotacao, 0, 360, cor, -1)
        ang = np.deg2rad(rotacao)
        dx = np.hypot(eixo[0] * np.cos(ang), eixo[1] * np.sin(ang))
        dy = np.hypot(eixo[0] * np.sin(ang), eixo[1] * np.cos(ang))
        xs, ys = np.array([cx - dx, cx + dx]), np.array([cy - dy, cy + dy])
    return xs.min(), ys.min(), xs.max(), ys.max()


def gera_imagem(tam_img=160, n_objetos=(1, 3), rng=None):
    rng = rng or random.Random()
    img = np.full((tam_img, tam_img, 3), 255, dtype=np.uint8)
    anotacoes = []
    for _ in range(rng.randint(*n_objetos)):
        classe_idx = rng.randrange(len(CLASSES))
        tamanho = rng.randint(tam_img // 10, tam_img // 5)
        cx = rng.randint(tamanho + 2, tam_img - tamanho - 2)
        cy = rng.randint(tamanho + 2, tam_img - tamanho - 2)
        rotacao = rng.uniform(0, 360)
        cor = tuple(rng.sample(range(30, 226), 3))
        x0, y0, x1, y1 = desenha_objeto(img, classe_idx, cx, cy, tamanho, rotacao, cor)
        x0, y0 = max(x0, 0), max(y0, 0)
        x1, y1 = min(x1, tam_img), min(y1, tam_img)
        xc, yc = (x0 + x1) / 2 / tam_img, (y0 + y1) / 2 / tam_img
        w, h = (x1 - x0) / tam_img, (y1 - y0) / tam_img
        anotacoes.append((classe_idx, xc, yc, w, h))
    return img, anotacoes


# Monta o conjunto de dados no formato esperado pela biblioteca `ultralytics`:
# shapes_dataset/{images,labels}/{train,val}/*.{jpg,txt} + data.yaml
base_dir = "shapes_dataset"
rng_global = random.Random(42)
for split, n_imgs in [("train", 90), ("val", 20)]:
    os.makedirs(f"{base_dir}/images/{split}", exist_ok=True)
    os.makedirs(f"{base_dir}/labels/{split}", exist_ok=True)
    for i in range(n_imgs):
        img, anotacoes = gera_imagem(rng=rng_global)
        cv2.imwrite(f"{base_dir}/images/{split}/{i:04d}.jpg", img)
        with open(f"{base_dir}/labels/{split}/{i:04d}.txt", "w") as f:
            for c, xc, yc, w, h in anotacoes:
                f.write(f"{c} {xc:.4f} {yc:.4f} {w:.4f} {h:.4f}\n")

with open(f"{base_dir}/data.yaml", "w") as f:
    f.write(f"path: {os.path.abspath(base_dir)}\ntrain: images/train\nval: images/val\nnames:\n")
    for i, nome in enumerate(CLASSES):
        f.write(f"  {i}: {nome}\n")

print("Dataset sintético gerado: 90 imagens de treino, 20 de validação.")

# Visualiza três amostras com as caixas delimitadoras sobrepostas
fig, axs = plt.subplots(1, 3, figsize=(10, 3.5))
cores_plot = plt.cm.tab10(np.linspace(0, 1, len(CLASSES)))
for ax, i in zip(axs, [0, 1, 2]):
    img = cv2.cvtColor(cv2.imread(f"{base_dir}/images/train/{i:04d}.jpg"), cv2.COLOR_BGR2RGB)
    ax.imshow(img)
    tam_img = img.shape[0]
    with open(f"{base_dir}/labels/train/{i:04d}.txt") as f:
        for linha in f:
            c, xc, yc, w, h = map(float, linha.split())
            c = int(c)
            x0, y0 = (xc - w / 2) * tam_img, (yc - h / 2) * tam_img
            ax.add_patch(plt.Rectangle((x0, y0), w * tam_img, h * tam_img,
                                        fill=False, edgecolor=cores_plot[c], linewidth=2))
            ax.text(x0, max(y0 - 3, 0), CLASSES[c], color=cores_plot[c], fontsize=8, weight="bold")
    ax.set_axis_off()
plt.tight_layout()
plt.show()
Dataset sintético gerado: 90 imagens de treino, 20 de validação.
Figura 9.5: Amostras do conjunto de dados sintético de objetos geométricos, com as caixas delimitadoras no formato YOLO sobrepostas.

9.8.2 Ajuste Fino de um YOLO Pré-treinado

A biblioteca ultralytics fornece uma interface de alto nível para carregar arquiteturas YOLO com pesos pré-treinados na base COCO e ajustá-las (fine-tuning) a uma nova tarefa — exatamente o padrão de transferência de aprendizado já discutido para classificação, mas agora aplicado à detecção: em vez de apenas substituir a última camada, a cabeça de detecção é reconfigurada para as novas classes (nove formas geométricas em vez de 80 categorias da COCO), e o treinamento é conduzido por poucas épocas sobre o conjunto de dados sintético.

Importante

A célula a seguir não é executada automaticamente neste notebook: o carregamento do modelo pré-treinado exige o download de seus pesos a partir dos servidores da Ultralytics/GitHub, o que requer conexão à internet no ambiente de execução, além da instalação da biblioteca ultralytics. Em um ambiente com GPU, o treinamento abaixo (15 épocas, ~110 imagens de \(160\times160\)) leva menos de um minuto; em CPU, alguns minutos.

try:
    from ultralytics import YOLO
except ImportError:
    subprocess.run([sys.executable, "-m", "pip", "install", "-q", "ultralytics"], check=True)
    from ultralytics import YOLO

# Carrega o YOLOv8-nano com pesos pré-treinados na base COCO (transferência de aprendizado)
modelo_yolo = YOLO("yolov8n.pt")

# Ajuste fino: `freeze=10` mantém congeladas as 10 primeiras camadas do backbone
# (características genéricas de baixo nível, como bordas e contornos), treinando
# apenas as camadas finais e a cabeça de detecção para as novas 9 classes —
# o mesmo princípio de congelamento parcial discutido no Exercício 3.
resultados = modelo_yolo.train(
    data="shapes_dataset/data.yaml",
    epochs=15,
    imgsz=160,
    batch=8,
    freeze=10,
    verbose=False,
    plots=False,
)

metricas = modelo_yolo.val()
print(f"mAP50 no conjunto de validação: {metricas.box.map50:.3f}")
New https://pypi.org/project/ultralytics/8.4.96 available 😃 Update with 'pip install -U ultralytics'

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

engine/trainer: agnostic_nms=False, amp=True, angle=1.0, augment=False, auto_augment=randaugment, batch=8, bgr=0.0, box=7.5, cache=False, cfg=None, classes=None, close_mosaic=10, cls=0.5, cls_pw=0.0, compile=False, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=shapes_dataset/data.yaml, degrees=0.0, deterministic=True, device=, dfl=1.5, dis=6.0, distill_model=None, dnn=False, dropout=0.0, dynamic=False, embed=None, end2end=None, epochs=15, erasing=0.4, exist_ok=False, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=10, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=160, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolov8n.pt, momentum=0.937, mosaic=1.0, multi_scale=0.0, name=train-9, nbs=64, nms=False, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=False, pose=12.0, pretrained=True, profile=False, project=None, quantize=None, rect=False, resume=False, retina_masks=False, rle=1.0, save=True, save_conf=False, save_crop=False, save_dir=/home/fz/fz/VSCode/pdi-vc/runs/detect/train-9, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=tracktrack.yaml, translate=0.1, val=True, verbose=False, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=8, workspace=None

Overriding model.yaml nc=80 with nc=9



                   from  n    params  module                                       arguments                     

  0                  -1  1       464  ultralytics.nn.modules.conv.Conv             [3, 16, 3, 2]                 

  1                  -1  1      4672  ultralytics.nn.modules.conv.Conv             [16, 32, 3, 2]                

  2                  -1  1      7360  ultralytics.nn.modules.block.C2f             [32, 32, 1, True]             

  3                  -1  1     18560  ultralytics.nn.modules.conv.Conv             [32, 64, 3, 2]                

  4                  -1  2     49664  ultralytics.nn.modules.block.C2f             [64, 64, 2, True]             

  5                  -1  1     73984  ultralytics.nn.modules.conv.Conv             [64, 128, 3, 2]               

  6                  -1  2    197632  ultralytics.nn.modules.block.C2f             [128, 128, 2, True]           

  7                  -1  1    295424  ultralytics.nn.modules.conv.Conv             [128, 256, 3, 2]              

  8                  -1  1    460288  ultralytics.nn.modules.block.C2f             [256, 256, 1, True]           

  9                  -1  1    164608  ultralytics.nn.modules.block.SPPF            [256, 256, 5]                 

 10                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 11             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 12                  -1  1    148224  ultralytics.nn.modules.block.C2f             [384, 128, 1]                 

 13                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 14             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 15                  -1  1     37248  ultralytics.nn.modules.block.C2f             [192, 64, 1]                  

 16                  -1  1     36992  ultralytics.nn.modules.conv.Conv             [64, 64, 3, 2]                

 17            [-1, 12]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 18                  -1  1    123648  ultralytics.nn.modules.block.C2f             [192, 128, 1]                 

 19                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              

 20             [-1, 9]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 21                  -1  1    493056  ultralytics.nn.modules.block.C2f             [384, 256, 1]                 

 22        [15, 18, 21]  1    753067  ultralytics.nn.modules.head.Detect           [9, 16, None, [64, 128, 256]] 

Model summary: 130 layers, 3,012,603 parameters, 3,012,587 gradients, 8.2 GFLOPs



Transferred 319/355 items from pretrained weights

Freezing layer 'model.0.conv.weight'

Freezing layer 'model.0.bn.weight'

Freezing layer 'model.0.bn.bias'

Freezing layer 'model.1.conv.weight'

Freezing layer 'model.1.bn.weight'

Freezing layer 'model.1.bn.bias'

Freezing layer 'model.2.cv1.conv.weight'

Freezing layer 'model.2.cv1.bn.weight'

Freezing layer 'model.2.cv1.bn.bias'

Freezing layer 'model.2.cv2.conv.weight'

Freezing layer 'model.2.cv2.bn.weight'

Freezing layer 'model.2.cv2.bn.bias'

Freezing layer 'model.2.m.0.cv1.conv.weight'

Freezing layer 'model.2.m.0.cv1.bn.weight'

Freezing layer 'model.2.m.0.cv1.bn.bias'

Freezing layer 'model.2.m.0.cv2.conv.weight'

Freezing layer 'model.2.m.0.cv2.bn.weight'

Freezing layer 'model.2.m.0.cv2.bn.bias'

Freezing layer 'model.3.conv.weight'

Freezing layer 'model.3.bn.weight'

Freezing layer 'model.3.bn.bias'

Freezing layer 'model.4.cv1.conv.weight'

Freezing layer 'model.4.cv1.bn.weight'

Freezing layer 'model.4.cv1.bn.bias'

Freezing layer 'model.4.cv2.conv.weight'

Freezing layer 'model.4.cv2.bn.weight'

Freezing layer 'model.4.cv2.bn.bias'

Freezing layer 'model.4.m.0.cv1.conv.weight'

Freezing layer 'model.4.m.0.cv1.bn.weight'

Freezing layer 'model.4.m.0.cv1.bn.bias'

Freezing layer 'model.4.m.0.cv2.conv.weight'

Freezing layer 'model.4.m.0.cv2.bn.weight'

Freezing layer 'model.4.m.0.cv2.bn.bias'

Freezing layer 'model.4.m.1.cv1.conv.weight'

Freezing layer 'model.4.m.1.cv1.bn.weight'

Freezing layer 'model.4.m.1.cv1.bn.bias'

Freezing layer 'model.4.m.1.cv2.conv.weight'

Freezing layer 'model.4.m.1.cv2.bn.weight'

Freezing layer 'model.4.m.1.cv2.bn.bias'

Freezing layer 'model.5.conv.weight'

Freezing layer 'model.5.bn.weight'

Freezing layer 'model.5.bn.bias'

Freezing layer 'model.6.cv1.conv.weight'

Freezing layer 'model.6.cv1.bn.weight'

Freezing layer 'model.6.cv1.bn.bias'

Freezing layer 'model.6.cv2.conv.weight'

Freezing layer 'model.6.cv2.bn.weight'

Freezing layer 'model.6.cv2.bn.bias'

Freezing layer 'model.6.m.0.cv1.conv.weight'

Freezing layer 'model.6.m.0.cv1.bn.weight'

Freezing layer 'model.6.m.0.cv1.bn.bias'

Freezing layer 'model.6.m.0.cv2.conv.weight'

Freezing layer 'model.6.m.0.cv2.bn.weight'

Freezing layer 'model.6.m.0.cv2.bn.bias'

Freezing layer 'model.6.m.1.cv1.conv.weight'

Freezing layer 'model.6.m.1.cv1.bn.weight'

Freezing layer 'model.6.m.1.cv1.bn.bias'

Freezing layer 'model.6.m.1.cv2.conv.weight'

Freezing layer 'model.6.m.1.cv2.bn.weight'

Freezing layer 'model.6.m.1.cv2.bn.bias'

Freezing layer 'model.7.conv.weight'

Freezing layer 'model.7.bn.weight'

Freezing layer 'model.7.bn.bias'

Freezing layer 'model.8.cv1.conv.weight'

Freezing layer 'model.8.cv1.bn.weight'

Freezing layer 'model.8.cv1.bn.bias'

Freezing layer 'model.8.cv2.conv.weight'

Freezing layer 'model.8.cv2.bn.weight'

Freezing layer 'model.8.cv2.bn.bias'

Freezing layer 'model.8.m.0.cv1.conv.weight'

Freezing layer 'model.8.m.0.cv1.bn.weight'

Freezing layer 'model.8.m.0.cv1.bn.bias'

Freezing layer 'model.8.m.0.cv2.conv.weight'

Freezing layer 'model.8.m.0.cv2.bn.weight'

Freezing layer 'model.8.m.0.cv2.bn.bias'

Freezing layer 'model.9.cv1.conv.weight'

Freezing layer 'model.9.cv1.bn.weight'

Freezing layer 'model.9.cv1.bn.bias'

Freezing layer 'model.9.cv2.conv.weight'

Freezing layer 'model.9.cv2.bn.weight'

Freezing layer 'model.9.cv2.bn.bias'

Freezing layer 'model.22.dfl.conv.weight'

train: Fast image access ✅ (ping: 0.0±0.0 ms, read: 142.8±50.5 MB/s, size: 3.3 KB)

train: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/train.cache... 90 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 90/90 29.0Mit/s 0.0s

val: Fast image access ✅ (ping: 0.0±0.0 ms, read: 137.2±69.3 MB/s, size: 2.9 KB)

val: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/val.cache... 20 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 20/20 9.3Mit/s 0.0s

optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 

optimizer: AdamW(lr=0.000769, momentum=0.9) with parameter groups 57 weight(decay=0.0), 64 weight(decay=0.0005), 63 bias(decay=0.0)

Image sizes 160 train, 160 val

Using 0 dataloader workers

Logging results to /home/fz/fz/VSCode/pdi-vc/runs/detect/train-9

Starting training for 15 epochs...



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       1/15         0G      1.097       4.44      1.051          9        160: 100% ━━━━━━━━━━━━ 12/12 2.2it/s 5.5s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 3.8it/s 0.5s1.1s

                   all         20         42    0.00112     0.0509    0.00213   0.000523



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       2/15         0G     0.9418      4.196     0.9975          7        160: 100% ━━━━━━━━━━━━ 12/12 2.4it/s 5.0s0.2s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 2.7it/s 0.7s1.8s

                   all         20         42    0.00109     0.0509    0.00149   0.000385



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       3/15         0G     0.8698      3.995     0.9579         14        160: 100% ━━━━━━━━━━━━ 12/12 4.1it/s 3.0s0.4s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 9.0it/s 0.2s0.5s

                   all         20         42    0.00111     0.0509    0.00207   0.000805



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       4/15         0G     0.8784      3.791     0.9763          7        160: 100% ━━━━━━━━━━━━ 12/12 5.8it/s 2.1s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 7.1it/s 0.3s0.7s

                   all         20         42    0.00527      0.378       0.15      0.128



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       5/15         0G     0.8578      3.608      0.968          8        160: 100% ━━━━━━━━━━━━ 12/12 6.0it/s 2.0s0.2s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 8.6it/s 0.2s0.5s

                   all         20         42    0.00882      0.761      0.336      0.236

Closing dataloader mosaic



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       6/15         0G     0.7412      3.033     0.9074          5        160: 100% ━━━━━━━━━━━━ 12/12 6.2it/s 1.9s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 8.9it/s 0.2s0.5s

                   all         20         42      0.714      0.112      0.374      0.311



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       7/15         0G     0.6899      2.889      0.875          4        160: 100% ━━━━━━━━━━━━ 12/12 5.4it/s 2.2s0.1s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 7.1it/s 0.3s0.7s

                   all         20         42      0.626      0.235      0.408      0.341



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       8/15         0G     0.7244      2.696     0.9041          3        160: 100% ━━━━━━━━━━━━ 12/12 4.5it/s 2.7s0.4s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 9.8it/s 0.2s0.5s

                   all         20         42      0.481      0.374        0.4      0.334



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

       9/15         0G     0.7013      2.519     0.9164          4        160: 100% ━━━━━━━━━━━━ 12/12 8.1it/s 1.5s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 9.5it/s 0.2s0.5s

                   all         20         42      0.283      0.547       0.41      0.356



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

      10/15         0G     0.6925      2.314     0.8889          5        160: 100% ━━━━━━━━━━━━ 12/12 6.7it/s 1.8s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 6.0it/s 0.3s0.8s

                   all         20         42      0.421      0.496      0.425      0.361



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

      11/15         0G     0.7052      2.325     0.8966          6        160: 100% ━━━━━━━━━━━━ 12/12 5.6it/s 2.1s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 10.4it/s 0.2s.5s

                   all         20         42      0.514      0.473      0.458      0.389



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

      12/15         0G     0.6623      2.184     0.8962          6        160: 100% ━━━━━━━━━━━━ 12/12 7.6it/s 1.6s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 8.8it/s 0.2s0.5s

                   all         20         42      0.498      0.505       0.52      0.455



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

      13/15         0G     0.6442      2.145      0.898          4        160: 100% ━━━━━━━━━━━━ 12/12 5.0it/s 2.4s0.4s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 7.7it/s 0.3s0.6s

                   all         20         42      0.517      0.526       0.48      0.418



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

      14/15         0G     0.6811      2.083     0.9132          4        160: 100% ━━━━━━━━━━━━ 12/12 6.1it/s 2.0s0.3s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 4.9it/s 0.4s0.9s

                   all         20         42      0.476      0.569      0.497      0.439



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

      15/15         0G     0.7058      2.037     0.9087          5        160: 100% ━━━━━━━━━━━━ 12/12 5.4it/s 2.2s0.4s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 8.8it/s 0.2s0.6s

                   all         20         42      0.489      0.583      0.502       0.45



15 epochs completed in 0.013 hours.

Optimizer stripped from /home/fz/fz/VSCode/pdi-vc/runs/detect/train-9/weights/last.pt, 6.2MB

Optimizer stripped from /home/fz/fz/VSCode/pdi-vc/runs/detect/train-9/weights/best.pt, 6.2MB



Validating /home/fz/fz/VSCode/pdi-vc/runs/detect/train-9/weights/best.pt...

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

Model summary (fused): 73 layers, 3,007,403 parameters, 0 gradients, 8.1 GFLOPs

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 7.6it/s 0.3s0.6s

                   all         20         42      0.499      0.505       0.52      0.455

Speed: 0.1ms preprocess, 10.8ms inference, 0.0ms loss, 0.5ms postprocess per image

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

Model summary (fused): 73 layers, 3,007,403 parameters, 0 gradients, 8.1 GFLOPs

val: Fast image access ✅ (ping: 0.0±0.0 ms, read: 230.4±101.0 MB/s, size: 3.8 KB)

val: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/val.cache... 20 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 20/20 7.6Mit/s 0.0s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 2/2 7.3it/s 0.3s0.4s

                   all         20         42      0.499      0.505       0.52      0.455

              Triangle          2          2          0          0       0.13      0.119

                Square          6          6      0.269      0.333      0.436      0.372

              Pentagon          4          5      0.168        0.2      0.226       0.21

               Hexagon          2          2          1          0      0.133     0.0422

              Heptagon          1          1      0.379          1      0.995      0.895

                Circle          7          7      0.725      0.386      0.588      0.503

               Ellipse          2          2      0.313          1      0.398      0.368

                  Star          8          9      0.799          1      0.995      0.876

                 Cross          7          8      0.833      0.624       0.78      0.709

Speed: 0.1ms preprocess, 10.9ms inference, 0.0ms loss, 0.5ms postprocess per image

Results saved to /home/fz/fz/VSCode/pdi-vc/runs/detect/val-5

mAP50 no conjunto de validação: 0.520
resultado = modelo_yolo.predict("shapes_dataset/images/val/0000.jpg", conf=0.4, verbose=False)[0]

fig, ax = plt.subplots(figsize=(4, 4))
ax.imshow(cv2.cvtColor(resultado.orig_img, cv2.COLOR_BGR2RGB))
for box in resultado.boxes:
    x0, y0, x1, y1 = box.xyxy[0].tolist()
    classe, conf = CLASSES[int(box.cls)], float(box.conf)
    ax.add_patch(plt.Rectangle((x0, y0), x1 - x0, y1 - y0,
                                fill=False, edgecolor="red", linewidth=2))
    ax.text(x0, max(y0 - 3, 0), f"{classe} {conf:.2f}", color="red", fontsize=8, weight="bold")
ax.set_axis_off()
plt.show()
Figura 9.6: Detecções do YOLO ajustado por transferência de aprendizado em uma imagem de validação não vista durante o treinamento.
import os
from ultralytics import YOLO

# 1. Definição das classes do problema geométrico
CLASSES_GEOMETRICAS = [
    'Triangle', 'Square', 'Pentagon', 'Hexagon', 
    'Heptagon', 'Circle', 'Ellipse', 'Star', 'Cross'
]

# 2. Configuração do arquivo de dados (dataset.yaml) exigido pelo YOLO
yaml_content = """
train: ./shapes_dataset/images/train
val: ./shapes_dataset/images/val

nc: 9
names: ['Triangle', 'Square', 'Pentagon', 'Hexagon', 'Heptagon', 'Circle', 'Ellipse', 'Star', 'Cross']
"""

with open("dataset.yaml", "w") as f:
    f.write(yaml_content)

# 3. Carrega um modelo YOLO nano pré-treinado (Extrator de características congelado/inicializado)
# O sufixo 'n' indica a versão mais leve, ideal para restrições computacionais
modelo_yolo = YOLO("yolov8n.pt") 

# 4. Transferência de Aprendizado / Ajuste Fino (Fine-tuning)
# O framework adapta automaticamente a camada de saída para 9 classes
resultados = modelo_yolo.train(
    data="dataset.yaml",
    epochs=4,
    imgsz=640,
    batch=16,
    device=device # Reaproveita a variável 'device' configurada no início do capítulo
)
New https://pypi.org/project/ultralytics/8.4.96 available 😃 Update with 'pip install -U ultralytics'

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

engine/trainer: agnostic_nms=False, amp=True, angle=1.0, augment=False, auto_augment=randaugment, batch=16, bgr=0.0, box=7.5, cache=False, cfg=None, classes=None, close_mosaic=10, cls=0.5, cls_pw=0.0, compile=False, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=dataset.yaml, degrees=0.0, deterministic=True, device=cpu, dfl=1.5, dis=6.0, distill_model=None, dnn=False, dropout=0.0, dynamic=False, embed=None, end2end=None, epochs=4, erasing=0.4, exist_ok=False, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=None, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=640, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolov8n.pt, momentum=0.937, mosaic=1.0, multi_scale=0.0, name=train-10, nbs=64, nms=False, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=True, pose=12.0, pretrained=True, profile=False, project=None, quantize=None, rect=False, resume=False, retina_masks=False, rle=1.0, save=True, save_conf=False, save_crop=False, save_dir=/home/fz/fz/VSCode/pdi-vc/runs/detect/train-10, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=tracktrack.yaml, translate=0.1, val=True, verbose=True, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=8, workspace=None

Overriding model.yaml nc=80 with nc=9



                   from  n    params  module                                       arguments                     

  0                  -1  1       464  ultralytics.nn.modules.conv.Conv             [3, 16, 3, 2]                 

  1                  -1  1      4672  ultralytics.nn.modules.conv.Conv             [16, 32, 3, 2]                

  2                  -1  1      7360  ultralytics.nn.modules.block.C2f             [32, 32, 1, True]             

  3                  -1  1     18560  ultralytics.nn.modules.conv.Conv             [32, 64, 3, 2]                

  4                  -1  2     49664  ultralytics.nn.modules.block.C2f             [64, 64, 2, True]             

  5                  -1  1     73984  ultralytics.nn.modules.conv.Conv             [64, 128, 3, 2]               

  6                  -1  2    197632  ultralytics.nn.modules.block.C2f             [128, 128, 2, True]           

  7                  -1  1    295424  ultralytics.nn.modules.conv.Conv             [128, 256, 3, 2]              

  8                  -1  1    460288  ultralytics.nn.modules.block.C2f             [256, 256, 1, True]           

  9                  -1  1    164608  ultralytics.nn.modules.block.SPPF            [256, 256, 5]                 

 10                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 11             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 12                  -1  1    148224  ultralytics.nn.modules.block.C2f             [384, 128, 1]                 

 13                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 14             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 15                  -1  1     37248  ultralytics.nn.modules.block.C2f             [192, 64, 1]                  

 16                  -1  1     36992  ultralytics.nn.modules.conv.Conv             [64, 64, 3, 2]                

 17            [-1, 12]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 18                  -1  1    123648  ultralytics.nn.modules.block.C2f             [192, 128, 1]                 

 19                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              

 20             [-1, 9]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 21                  -1  1    493056  ultralytics.nn.modules.block.C2f             [384, 256, 1]                 

 22        [15, 18, 21]  1    753067  ultralytics.nn.modules.head.Detect           [9, 16, None, [64, 128, 256]] 

Model summary: 130 layers, 3,012,603 parameters, 3,012,587 gradients, 8.2 GFLOPs



Transferred 319/355 items from pretrained weights

Freezing layer 'model.22.dfl.conv.weight'

train: Fast image access ✅ (ping: 0.0±0.0 ms, read: 157.3±47.3 MB/s, size: 3.3 KB)

train: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/train.cache... 90 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 90/90 34.3Mit/s 0.0s

val: Fast image access ✅ (ping: 0.0±0.0 ms, read: 135.0±74.2 MB/s, size: 2.9 KB)

val: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/val.cache... 20 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 20/20 7.0Mit/s 0.0s

optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 

optimizer: AdamW(lr=0.000769, momentum=0.9) with parameter groups 57 weight(decay=0.0), 64 weight(decay=0.0005), 63 bias(decay=0.0)

Plotting labels to /home/fz/fz/VSCode/pdi-vc/runs/detect/train-10/labels.jpg... 

Image sizes 640 train, 640 val

Using 0 dataloader workers

Logging results to /home/fz/fz/VSCode/pdi-vc/runs/detect/train-10

Starting training for 4 epochs...



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        1/4         0G     0.8733      3.844      1.118         49        640: 100% ━━━━━━━━━━━━ 6/6 12.1s/it 1:1314.4ss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 7.8s/it 7.8s

                   all         20         42     0.0134      0.941      0.103     0.0785



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        2/4         0G     0.7482      3.601      1.073         45        640: 100% ━━━━━━━━━━━━ 6/6 9.6s/it 57.5s8.5s5s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 3.6s/it 3.6s

                   all         20         42     0.0116          1      0.135      0.116



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        3/4         0G     0.7126       3.38       1.08         38        640: 100% ━━━━━━━━━━━━ 6/6 8.1s/it 48.9s<10.4s

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 5.8s/it 5.8s

                   all         20         42     0.0107          1      0.186      0.163



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        4/4         0G     0.6787      3.236      1.045         34        640: 100% ━━━━━━━━━━━━ 6/6 5.1s/it 30.4s4.8ss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 2.6s/it 2.6s

                   all         20         42     0.0109      0.984      0.224        0.2



4 epochs completed in 0.065 hours.

Optimizer stripped from /home/fz/fz/VSCode/pdi-vc/runs/detect/train-10/weights/last.pt, 6.2MB

Optimizer stripped from /home/fz/fz/VSCode/pdi-vc/runs/detect/train-10/weights/best.pt, 6.2MB



Validating /home/fz/fz/VSCode/pdi-vc/runs/detect/train-10/weights/best.pt...

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

Model summary (fused): 73 layers, 3,007,403 parameters, 0 gradients, 8.1 GFLOPs

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 3.7s/it 3.7s

                   all         20         42     0.0109      0.984      0.227      0.203

              Triangle          2          2    0.00971          1      0.133      0.109

                Square          6          6    0.00819          1      0.158      0.141

              Pentagon          4          5     0.0025          1      0.429      0.413

               Hexagon          2          2    0.00213          1     0.0463     0.0274

              Heptagon          1          1     0.0034          1     0.0415     0.0415

                Circle          7          7     0.0249      0.857      0.499      0.445

               Ellipse          2          2    0.00536          1       0.21      0.206

                  Star          8          9     0.0335          1      0.248      0.191

                 Cross          7          8     0.0085          1      0.282      0.255

Speed: 1.7ms preprocess, 146.1ms inference, 0.0ms loss, 34.3ms postprocess per image

Results saved to /home/fz/fz/VSCode/pdi-vc/runs/detect/train-10

# 5. Validação e Inferência em uma imagem de teste
# Exemplo de URL de teste enviada em exames: https://dropbox.com/s/ekjbzp14jt90bfq/00004.jpg
imagem_teste = "./shapes_dataset/images/val/0001.jpg"
predicoes = modelo_yolo(imagem_teste)

# Exibe o resultado visual com as caixas sobrepostas
predicoes[0].show()

image 1/1 /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/images/val/0001.jpg: 640x640 (no detections), 1071.0ms
Speed: 7.4ms preprocess, 1071.0ms inference, 28.3ms postprocess per image at shape (1, 3, 640, 640)
# --- 1. Instalação e Importação das Bibliotecas ---
# O Ultralytics YOLO é uma biblioteca popular para treino e inferência.
# !pip install ultralytics -q

from ultralytics import YOLO
import matplotlib.pyplot as plt
import cv2
import numpy as np
from pathlib import Path
import torch

# Verificar dispositivo
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"Dispositivo usado: {device}")

# --- 2. Carregar o Modelo YOLO Pré-treinado ---
# 'yolov8n.pt' é a versão "nano" (mais leve e rápida), pré-treinada no COCO.
# Usaremos seus pesos como ponto de partida (transferência de aprendizado).
modelo = YOLO('yolov8n.pt')
print("Modelo YOLOv8n carregado com sucesso!")

# --- 3. Preparar o Dataset de Objetos Geométricos ---
# Assumimos que você tem uma estrutura de pastas pronta para o YOLO:
# dataset_geometrico/
#   images/
#       00004.jpg, 00005.jpg, ...
#   labels/
#       00004.txt, 00005.txt, ...
#
# O arquivo de label deve estar no formato YOLO:
# <class_id> <x_center> <y_center> <width> <height>
# (todos normalizados entre 0 e 1)

# Exemplo de caminho para o dataset (substitua pelo seu caminho)
caminho_dataset = Path("./shapes_dataset")
caminho_imagens = caminho_dataset / "images"
caminho_rotulos = caminho_dataset / "labels"

# Crie um arquivo data.yaml para descrever o dataset
data_yaml_content = f"""
path: {caminho_dataset.resolve()}
train: images
val: images  # Se você tiver uma pasta separada, use 'images/val'

nc: 9  # Número de classes
names: ['Triangle', 'Square', 'Pentagon', 'Hexagon', 'Heptagon', 'Circle', 'Ellipse', 'Star', 'Cross']
"""

# Cria o arquivo data.yaml (se não existir)
data_yaml_path = caminho_dataset / "data.yaml"
if not data_yaml_path.exists():
    data_yaml_path.write_text(data_yaml_content)
    print(f"Arquivo data.yaml criado em: {data_yaml_path}")
else:
    print("Arquivo data.yaml já existe.")

# --- 4. Treinamento com Transferência de Aprendizado ---
# A chave aqui é que os pesos da cabeça de detecção (camadas finais) serão
# re-inicializados, enquanto o extrator de características (backbone) é ajustado.
resultados = modelo.train(
    data=str(data_yaml_path),   # Caminho para o arquivo data.yaml
    epochs=5,                  # Número de épocas (ajuste fino com poucos dados)
    imgsz=640,                  # Tamanho da imagem de entrada
    batch=16,                   # Tamanho do lote
    device=device,              # 'cpu' ou 'cuda'
    project='runs/detect',      # Pasta para salvar resultados
    name='geometricos_yolov8n', # Nome do experimento
    exist_ok=True,              # Sobrescreve se existir
    pretrained=True,            # Usar pesos pré-treinados (já é padrão)
)

print("Treinamento concluído!")
melhor_modelo_path = Path(resultados.save_dir) / "weights" / "best.pt"

# --- 5. Avaliação e Inferência em uma Imagem de Exemplo ---
if melhor_modelo_path.exists():
    # Carrega o melhor modelo treinado
    melhor_modelo = YOLO(str(melhor_modelo_path))

    # Realiza inferência em uma imagem de teste (ex: '00004.jpg')
    caminho_teste = caminho_imagens / "00004.jpg"
    if caminho_teste.exists():
        resultados_pred = melhor_modelo(caminho_teste, conf=0.25)

        # Plota a imagem com as detecções
        for r in resultados_pred:
            # A imagem com as caixas é renderizada no OpenCV
            img_plot = r.plot()  # Retorna um array BGR
            img_plot_rgb = cv2.cvtColor(img_plot, cv2.COLOR_BGR2RGB)

            plt.figure(figsize=(8, 8))
            plt.imshow(img_plot_rgb)
            plt.axis('off')
            plt.title("Detecções YOLO (Transferência)")
            plt.show()

            # Exibe as detecções como texto
            print("\nObjetos Detectados:")
            for box in r.boxes:
                cls = int(box.cls[0])
                conf = float(box.conf[0])
                nome_classe = modelo.names[cls]
                print(f"  - Classe: {nome_classe} (Confiança: {conf:.2f})")
    else:
        print(f"Imagem de teste '{caminho_teste}' não encontrada.")
else:
    print("Modelo treinado não encontrado. Verifique o diretório de saída do treinamento.")
Dispositivo usado: cpu

Modelo YOLOv8n carregado com sucesso!

Arquivo data.yaml já existe.

New https://pypi.org/project/ultralytics/8.4.96 available 😃 Update with 'pip install -U ultralytics'

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

engine/trainer: agnostic_nms=False, amp=True, angle=1.0, augment=False, auto_augment=randaugment, batch=16, bgr=0.0, box=7.5, cache=False, cfg=None, classes=None, close_mosaic=10, cls=0.5, cls_pw=0.0, compile=False, conf=None, copy_paste=0.0, copy_paste_mode=flip, cos_lr=False, cutmix=0.0, data=shapes_dataset/data.yaml, degrees=0.0, deterministic=True, device=cpu, dfl=1.5, dis=6.0, distill_model=None, dnn=False, dropout=0.0, dynamic=False, embed=None, end2end=None, epochs=5, erasing=0.4, exist_ok=True, fliplr=0.5, flipud=0.0, format=torchscript, fraction=1.0, freeze=None, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, imgsz=640, iou=0.7, keras=False, kobj=1.0, line_width=None, lr0=0.01, lrf=0.01, mask_ratio=4, max_det=300, mixup=0.0, mode=train, model=yolov8n.pt, momentum=0.937, mosaic=1.0, multi_scale=0.0, name=geometricos_yolov8n, nbs=64, nms=False, opset=None, optimize=False, optimizer=auto, overlap_mask=True, patience=100, perspective=0.0, plots=True, pose=12.0, pretrained=True, profile=False, project=runs/detect, quantize=None, rect=False, resume=False, retina_masks=False, rle=1.0, save=True, save_conf=False, save_crop=False, save_dir=/home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n, save_frames=False, save_json=False, save_period=-1, save_txt=False, scale=0.5, seed=0, shear=0.0, show=False, show_boxes=True, show_conf=True, show_labels=True, simplify=True, single_cls=False, source=None, split=val, stream_buffer=False, task=detect, time=None, tracker=tracktrack.yaml, translate=0.1, val=True, verbose=True, vid_stride=1, visualize=False, warmup_bias_lr=0.1, warmup_epochs=3.0, warmup_momentum=0.8, weight_decay=0.0005, workers=8, workspace=None

Overriding model.yaml nc=80 with nc=9



                   from  n    params  module                                       arguments                     

  0                  -1  1       464  ultralytics.nn.modules.conv.Conv             [3, 16, 3, 2]                 

  1                  -1  1      4672  ultralytics.nn.modules.conv.Conv             [16, 32, 3, 2]                

  2                  -1  1      7360  ultralytics.nn.modules.block.C2f             [32, 32, 1, True]             

  3                  -1  1     18560  ultralytics.nn.modules.conv.Conv             [32, 64, 3, 2]                

  4                  -1  2     49664  ultralytics.nn.modules.block.C2f             [64, 64, 2, True]             

  5                  -1  1     73984  ultralytics.nn.modules.conv.Conv             [64, 128, 3, 2]               

  6                  -1  2    197632  ultralytics.nn.modules.block.C2f             [128, 128, 2, True]           

  7                  -1  1    295424  ultralytics.nn.modules.conv.Conv             [128, 256, 3, 2]              

  8                  -1  1    460288  ultralytics.nn.modules.block.C2f             [256, 256, 1, True]           

  9                  -1  1    164608  ultralytics.nn.modules.block.SPPF            [256, 256, 5]                 

 10                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 11             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 12                  -1  1    148224  ultralytics.nn.modules.block.C2f             [384, 128, 1]                 

 13                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          

 14             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 15                  -1  1     37248  ultralytics.nn.modules.block.C2f             [192, 64, 1]                  

 16                  -1  1     36992  ultralytics.nn.modules.conv.Conv             [64, 64, 3, 2]                

 17            [-1, 12]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 18                  -1  1    123648  ultralytics.nn.modules.block.C2f             [192, 128, 1]                 

 19                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              

 20             [-1, 9]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           

 21                  -1  1    493056  ultralytics.nn.modules.block.C2f             [384, 256, 1]                 

 22        [15, 18, 21]  1    753067  ultralytics.nn.modules.head.Detect           [9, 16, None, [64, 128, 256]] 

Model summary: 130 layers, 3,012,603 parameters, 3,012,587 gradients, 8.2 GFLOPs



Transferred 319/355 items from pretrained weights

Freezing layer 'model.22.dfl.conv.weight'

train: Fast image access ✅ (ping: 0.0±0.0 ms, read: 108.9±31.9 MB/s, size: 3.3 KB)

train: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/train.cache... 90 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 90/90 34.3Mit/s 0.0s

val: Fast image access ✅ (ping: 0.0±0.0 ms, read: 179.0±78.7 MB/s, size: 2.9 KB)

val: Scanning /home/fz/fz/VSCode/pdi-vc/all/cap09/shapes_dataset/labels/val.cache... 20 images, 0 backgrounds, 0 corrupt: 100% ━━━━━━━━━━━━ 20/20 12.0Mit/s 0.0s

optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 

optimizer: AdamW(lr=0.000769, momentum=0.9) with parameter groups 57 weight(decay=0.0), 64 weight(decay=0.0005), 63 bias(decay=0.0)

Plotting labels to /home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n/labels.jpg... 

Image sizes 640 train, 640 val

Using 0 dataloader workers

Logging results to /home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n

Starting training for 5 epochs...



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        1/5         0G     0.8733      3.844      1.118         49        640: 100% ━━━━━━━━━━━━ 6/6 8.8s/it 52.7s7.8sss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 2.8s/it 2.8s

                   all         20         42     0.0134      0.941      0.103     0.0785



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        2/5         0G     0.7489      3.595      1.073         45        640: 100% ━━━━━━━━━━━━ 6/6 4.4s/it 26.6s4.3ss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 2.8s/it 2.8s

                   all         20         42     0.0111          1      0.139       0.12



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        3/5         0G     0.7121      3.353      1.081         38        640: 100% ━━━━━━━━━━━━ 6/6 5.1s/it 30.4s5.5ss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 3.7s/it 3.7s

                   all         20         42    0.00979      0.984      0.186      0.163



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        4/5         0G     0.6732      3.179      1.039         34        640: 100% ━━━━━━━━━━━━ 6/6 4.8s/it 28.8s5.0ss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 3.4s/it 3.4s

                   all         20         42     0.0104      0.984      0.211      0.188



      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size

        5/5         0G     0.6735      3.087      1.073         47        640: 100% ━━━━━━━━━━━━ 6/6 4.8s/it 28.9s5.0ss

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 2.7s/it 2.7s

                   all         20         42     0.0106      0.984      0.213      0.192



5 epochs completed in 0.051 hours.

Optimizer stripped from /home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n/weights/last.pt, 6.2MB

Optimizer stripped from /home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n/weights/best.pt, 6.2MB



Validating /home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n/weights/best.pt...

Ultralytics 8.4.87 🚀 Python-3.13.5 torch-2.12.1+cpu CPU (Intel Core i7-4870HQ 2.50GHz)

Model summary (fused): 73 layers, 3,007,403 parameters, 0 gradients, 8.1 GFLOPs

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100% ━━━━━━━━━━━━ 1/1 2.7s/it 2.7s

                   all         20         42     0.0105      0.984      0.213      0.193

              Triangle          2          2    0.00893          1     0.0796     0.0716

                Square          6          6    0.00842          1      0.175      0.168

              Pentagon          4          5    0.00261          1      0.367      0.338

               Hexagon          2          2    0.00189          1     0.0394     0.0275

              Heptagon          1          1    0.00342          1     0.0433     0.0433

                Circle          7          7     0.0224      0.857      0.501      0.456

               Ellipse          2          2    0.00464          1      0.171      0.167

                  Star          8          9     0.0328          1      0.275      0.214

                 Cross          7          8    0.00969          1      0.271       0.25

Speed: 1.8ms preprocess, 111.0ms inference, 0.0ms loss, 18.2ms postprocess per image

Results saved to /home/fz/fz/VSCode/pdi-vc/runs/detect/runs/detect/geometricos_yolov8n

Treinamento concluído!

Imagem de teste 'shapes_dataset/images/00004.jpg' não encontrada.
Nota🧠 Um domínio muito mais distante que o dos dígitos

No experimento de transferência de aprendizado entre dígitos manuscritos (domínios A e B), a tarefa de origem e a de destino compartilhavam estatísticas visuais muito próximas: ambas eram traços em tons de cinza sobre fundo uniforme. Aqui, a distância entre domínios é bem maior — o YOLO foi pré-treinado em fotografias naturais coloridas da COCO (pessoas, animais, veículos, objetos do cotidiano), e a tarefa de destino consiste em formas geométricas sintéticas, de cor sólida e contorno bem definido, sem textura, iluminação ou fundo complexo.

Ainda assim, a transferência de aprendizado tende a ajudar: as camadas iniciais de um detector treinado na COCO aprendem filtros muito genéricos — detectores de bordas, cantos e regiões de contraste — que continuam úteis para delimitar o contorno de um triângulo ou de uma estrela, mesmo que o conteúdo visual final seja muito diferente. É por isso que o congelamento (freeze=10) é aplicado apenas às primeiras camadas do backbone: as camadas mais profundas, que na COCO aprenderam a reconhecer partes de objetos complexos do mundo real (rodas, focinhos, faces), têm pouca serventia direta para formas geométricas simples e se beneficiam de serem reajustadas.

Este é o mesmo compromisso discutido no Exercício 3 deste capítulo — entre congelar totalmente o extrator, ajustar parcialmente algumas camadas, ou treinar tudo do zero — agora observado em uma tarefa de detecção, e com um domínio de origem e destino visivelmente mais distantes do que o observado entre os dois grupos de dígitos.

9.8.3 Usando um Conjunto de Dados Real

O pipeline acima foi construído inteiramente em torno do formato de anotação YOLO (classe, \(x_{centro}\), \(y_{centro}\), largura, altura, normalizados pela largura e altura da imagem) exatamente para que ele possa ser reaproveitado sem alterações caso o leitor tenha acesso a um conjunto de imagens reais anotado da mesma forma — por exemplo, um conjunto de imagens de objetos geométricos fotografados ou renderizados, cada um com um arquivo .txt correspondente no mesmo formato usado aqui. Para isso, bastaria:

  1. Organizar as imagens reais em shapes_dataset/images/train e shapes_dataset/images/val, e os arquivos .txt de anotação correspondentes nas pastas labels/train e labels/val (um arquivo de anotação por imagem, mesmo nome-base, extensão .txt);
  2. Ajustar o arquivo data.yaml caso o número ou os nomes das classes sejam diferentes;
  3. Executar as mesmas células de treinamento, ajuste fino e visualização já apresentadas, sem qualquer outra modificação de código.

Essa separação entre geração/organização dos dados e treinamento do modelo é, na prática, o motivo pelo qual formatos de anotação padronizados (como o do YOLO) são tão amplamente adotados: eles permitem trocar o conjunto de dados de entrada — sintético por real, um domínio por outro — mantendo inalterado todo o restante do pipeline de transferência de aprendizado.

9.9 Integrando Geometria e Aprendizado Profundo

O livro se encerra retomando um fio condutor que atravessou toda a Parte II: a geometria projetiva (transformações de perspectiva e homografia, estudadas nos Capítulos 6 e 8) e, agora, o aprendizado profundo, combinam-se para resolver problemas de Visão Computacional com aplicação direta no mundo real. Três aplicações ilustram essa integração.

9.9.1 Realidade Aumentada

A realidade aumentada sobrepõe conteúdo virtual a uma cena real, alinhado geometricamente com ela. O princípio é exatamente a homografia do Capítulo 8: detecta-se um marcador (ou, em sistemas modernos, características naturais da cena, aprendidas por uma rede), estima-se a transformação de perspectiva entre suas coordenadas conhecidas e sua posição na imagem, e essa mesma transformação é usada para deformar e posicionar um conteúdo virtual sobre a cena.

dic = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
aruco = cv2.cvtColor(cv2.aruco.generateImageMarker(dic, 7, 200), cv2.COLOR_GRAY2BGR)

src = np.float32([[0,0],[200,0],[200,200],[0,200]])
dst = np.float32([[150,120],[430,90],[460,350],[140,380]])

cena = np.full((480,640,3), 200, np.uint8)
H, _ = cv2.findHomography(src, dst)

mask = cv2.warpPerspective(np.full((200,200),255,np.uint8), H, (640,480))
cena[mask>0] = cv2.warpPerspective(aruco, H, (640,480))[mask>0]

det = cv2.aruco.ArucoDetector(dic, cv2.aruco.DetectorParameters())
corners, ids, _ = det.detectMarkers(cena)

virtual = cv2.resize(cv2.cvtColor(skdata.astronaut(), cv2.COLOR_RGB2BGR), (200,200))
H, _ = cv2.findHomography(src, corners[0][0])

ra = cena.copy()
mask = cv2.warpPerspective(np.full((200,200),255,np.uint8), H, (640,480))
ra[mask>0] = cv2.warpPerspective(virtual, H, (640,480))[mask>0]

mm.show(
    [cv2.cvtColor(cena, cv2.COLOR_BGR2RGB),
     cv2.cvtColor(ra, cv2.COLOR_BGR2RGB)],
    titles=["Marcador detectado", "Overlay via homografia"],
    cols=2, figsize=(9,4)
)
Figura 9.7: Realidade aumentada baseada em marcador: um marcador ArUco é detectado na cena e sua homografia é reaproveitada para posicionar uma imagem virtual sobre ele.

Nota: sistemas modernos de realidade aumentada (como o ARKit da Apple ou o ARCore do Google) substituem marcadores explícitos por odometria visual-inercial baseada em aprendizado profundo, capaz de rastrear características naturais da cena (sem a necessidade de um marcador impresso) e estimar a pose da câmera em tempo real — mas o princípio geométrico subjacente de alinhar conteúdo virtual à cena real por meio de uma transformação estimada permanece o mesmo.

9.9.2 Fotogrametria e Referência de Escala

Uma pergunta recorrente em aplicações práticas é: dado um objeto medido apenas em pixels em uma fotografia, qual é seu tamanho real? A resposta clássica utiliza um objeto de referência de tamanho conhecido presente na mesma imagem, na mesma distância aproximada da câmera:

COR_REFERENCIA = (200, 200, 200)  # cartão de referência: cinza claro
COR_OBJETO = (60, 60, 220)        # objeto a medir: vermelho

# Cena original: os objetos são desenhados PREENCHIDOS, como apareceriam de fato
# em uma foto -- e não apenas como contornos de caixa delimitadora.
cena_medicao = np.full((300, 500, 3), 255, dtype=np.uint8)
cv2.rectangle(cena_medicao, (30, 200), (30 + 140, 200 + 88), COR_REFERENCIA, -1)   # referência conhecida
cv2.rectangle(cena_medicao, (250, 100), (250 + 220, 100 + 150), COR_OBJETO, -1)    # objeto a medir

def caixa_delimitadora_por_cor(imagem_bgr, cor_bgr, tolerancia=40):
    diferenca = np.abs(imagem_bgr.astype(int) - np.array(cor_bgr)).sum(axis=2)
    mascara = (diferenca < tolerancia).astype(np.uint8) * 255
    contornos, _ = cv2.findContours(mascara, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    maior_contorno = max(contornos, key=cv2.contourArea)
    return cv2.boundingRect(maior_contorno)  # (x, y, largura, altura) em pixels

x_ref, y_ref, w_ref_px, h_ref_px = caixa_delimitadora_por_cor(cena_medicao, COR_REFERENCIA)
x_obj, y_obj, w_obj_px, h_obj_px = caixa_delimitadora_por_cor(cena_medicao, COR_OBJETO)

LARGURA_REFERENCIA_CM = 8.56  # largura padrão de um cartão (ISO/IEC 7810)
razao_cm_por_px = LARGURA_REFERENCIA_CM / w_ref_px
largura_obj_cm = w_obj_px * razao_cm_por_px
altura_obj_cm = h_obj_px * razao_cm_por_px

# Painel de resultado: a MESMA cena, agora com as caixas delimitadoras detectadas
# (em verde, obtidas por segmentação de cor) e as medições estimadas sobrepostas.
resultado = cena_medicao.copy()
cv2.rectangle(resultado, (x_ref, y_ref), (x_ref + w_ref_px, y_ref + h_ref_px), (0, 180, 0), 2)
cv2.rectangle(resultado, (x_obj, y_obj), (x_obj + w_obj_px, y_obj + h_obj_px), (0, 180, 0), 2)

cv2.putText(resultado, f"{LARGURA_REFERENCIA_CM:.2f} cm",
            (x_ref, y_ref - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 120, 0), 2)

cv2.putText(resultado,
            f"{largura_obj_cm:.1f} x {altura_obj_cm:.1f} cm",
            (x_obj, y_obj - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 120, 0), 2)

mm.show(
    [cv2.cvtColor(cena_medicao, cv2.COLOR_BGR2RGB),
     cv2.cvtColor(resultado, cv2.COLOR_BGR2RGB)],
    titles=[
        "Cena original",
        "Caixas delimitadoras detectadas + medição por referência de escala"
    ],
    cols=2, figsize=(9,4)
)
Figura 9.8: Medição de um objeto a partir de uma referência de escala conhecida (um cartão de 8,56 cm de largura).

Essa é a base da fotogrametria clássica — a mesma lógica se estende, com maior sofisticação geométrica (calibração de câmera, correção de distorção de lente, múltiplas vistas), a aplicações como levantamento topográfico por drones e reconstrução 3D de objetos a partir de fotografias.

9.9.3 Visão Estereoscópica

Enquanto a fotogrametria acima assume uma única imagem e um objeto de referência conhecido, a visão estereoscópica estima profundidade diretamente a partir de duas câmeras (ou duas fotografias da mesma cena, tiradas de posições ligeiramente diferentes) — o mesmo princípio da visão binocular humana. A ideia central é a disparidade: um ponto próximo à câmera se desloca mais, entre as duas imagens, do que um ponto distante.

Formalmente, a profundidade \(Z\) de um ponto se relaciona à disparidade \(d\) (a diferença de posição horizontal do mesmo ponto nas duas imagens) por:

\[ Z = \frac{f \cdot B}{d}, \]

em que \(f\) é a distância focal da câmera e \(B\) é a linha de base (baseline) — a distância entre os dois pontos de captura. Quanto maior a disparidade, menor a profundidade (objeto mais próximo).

img_base = cv2.cvtColor(skdata.astronaut(), cv2.COLOR_RGB2GRAY)

DESLOC_FUNDO = 4    # pequeno deslocamento: objeto distante
DESLOC_OBJETO = 22  # grande deslocamento: objeto próximo à câmera

def deslocar(imagem, dx):
    M = np.float32([[1, 0, dx], [0, 1, 0]])
    return cv2.warpAffine(imagem, M, (imagem.shape[1], imagem.shape[0]))

img_esquerda = img_base.copy()
img_direita = deslocar(img_base, -DESLOC_FUNDO)

# Simula um objeto em primeiro plano com deslocamento (disparidade) maior
y0, y1, x0, x1 = 180, 340, 200, 360
img_direita[y0:y1, x0:x1] = deslocar(img_base, -DESLOC_OBJETO)[y0:y1, x0:x1]

correspondencia_estereo = cv2.StereoBM_create(numDisparities=32, blockSize=15)
mapa_disparidade = correspondencia_estereo.compute(img_esquerda, img_direita).astype(np.float32) / 16.0

disp_fundo = mapa_disparidade[:150, :150].mean()
disp_objeto = mapa_disparidade[200:320, 220:340].mean()
print(f"Disparidade média no fundo: {disp_fundo:.2f} px")
print(f"Disparidade média no objeto em primeiro plano: {disp_objeto:.2f} px")
print("→ Disparidade maior = objeto mais próximo da câmera, como previsto pela equação Z = f·B/d.")

mm.show(
    [img_esquerda, img_direita, mapa_disparidade],
    titles=["Imagem Esquerda", "Imagem Direita", "Mapa de Disparidade"],
    cols=3, figsize=(10, 3.5)
)
Disparidade média no fundo: 2.58 px
Disparidade média no objeto em primeiro plano: 21.63 px
→ Disparidade maior = objeto mais próximo da câmera, como previsto pela equação Z = f·B/d.
Figura 9.9: Par estéreo sintético (um objeto em primeiro plano deslocado mais do que o fundo) e o mapa de disparidade calculado — regiões mais claras indicam maior disparidade (objetos mais próximos).

Nota: métodos clássicos de correspondência estéreo, como o utilizado acima, comparam diretamente blocos de pixels entre as duas imagens — uma tarefa que sofre em regiões de textura pouco distintiva (paredes lisas, céu). Redes neurais de estimativa de profundidade monocular (a partir de uma única imagem) e de correspondência estéreo aprendida superam essa limitação aprendendo priors visuais sobre a estrutura de cenas do mundo real a partir de grandes volumes de dados — um caso de uso direto das CNNs estudadas neste capítulo aplicado diretamente ao problema geométrico com o qual o livro se encerra.

9.10 Resumo

Este capítulo, que encerra a Parte II do livro, apresentou:

  • Convolução e pooling aprendidos: a mesma operação matemática de convolução do Capítulo 3, mas com kernels tratados como parâmetros ajustados por treinamento, em vez de definidos manualmente;
  • Compartilhamento de pesos e hierarquia de características como as propriedades que tornam as CNNs eficientes e capazes de aprender representações cada vez mais abstratas em camadas sucessivas;
  • Treinamento de uma CNN do zero, com desempenho comparável — e não necessariamente superior — aos classificadores clássicos do Capítulo 7 em uma base pequena e simples, reforçando que a escolha do método deve ser proporcional à complexidade real do problema;
  • Transferência de aprendizado, demonstrada experimentalmente como uma estratégia eficaz para tarefas com poucos dados rotulados, reaproveitando um extrator de características já treinado em uma tarefa relacionada — e suas limitações, na forma da transferência negativa entre domínios muito distintos;
  • Aplicações em larga escala com modelos pré-treinados de classificação, detecção (Faster R-CNN) e segmentação (DeepLabV3), seguindo o mesmo princípio de transferência de aprendizado em escala industrial;
  • Transferência de aprendizado aplicada à detecção de objetos, ajustando um YOLO pré-treinado na COCO para localizar e classificar objetos geométricos sintéticos, ilustrando o mesmo princípio de congelamento parcial em um domínio de origem e destino ainda mais distantes entre si;
  • A integração de geometria computacional (homografia, calibração) e aprendizado profundo em três aplicações reais que encerram o livro: realidade aumentada, fotogrametria e visão estereoscópica.

9.11 🤖 Uso do Gemini Notebook como Tutor Complementar

Nesta edição, o uso do Gemini Notebook é incentivado como ferramenta complementar de aprendizagem. Baseado em inteligência artificial, o sistema utiliza exclusivamente os documentos fornecidos pelo autor como fonte de conhecimento, produzindo respostas alinhadas ao conteúdo e à abordagem adotada ao longo deste capítulo.

Importante🎓 Estude com o Tutor Inteligente

🚀 ACESSAR Gemini Notebook: CAPÍTULO 09

⚠️ Aviso sobre Conteúdo Gerado por IA

Embora seja uma ferramenta valiosa de apoio aos estudos, o Gemini Notebook pode eventualmente produzir respostas incompletas, imprecisas ou incorretas. Recomenda-se validar as informações consultando o material do capítulo, livros, artigos científicos e outras fontes acadêmicas confiáveis. Sempre que possível, execute e experimente os exemplos práticos apresentados ao longo do texto para consolidar a compreensão dos conceitos.

9.12 Exercícios Práticos

9.12.1 Exercício 1: Profundidade da Rede (Básico)

Contexto: A CNN do Projeto Prático 1 utiliza duas camadas convolucionais (8 e 16 filtros, respectivamente).

Desafio: Adicione uma terceira camada convolucional (por exemplo, 32 filtros) antes da camada totalmente conectada, ajustando as dimensões necessárias. Treine a nova rede nas mesmas condições (60 épocas, mesma divisão treino/teste) e compare sua acurácia final e seu número de parâmetros com a rede original de duas camadas.

Saída esperada: O número de parâmetros de cada rede, suas acurácias finais, e uma frase concluindo se a camada adicional trouxe benefício mensurável nesta base de dados específica.

Dica: Como a imagem de entrada é pequena (\(8\times8\)), o pooling repetido pode reduzir demais a resolução espacial antes da terceira convolução; considere remover um dos poolings ou usar padding para manter dimensões compatíveis.

9.12.2 Exercício 2: Limiar de Transferência de Dados (Intermediário)

Contexto: O experimento de transferência de aprendizado utilizou apenas 20 exemplos de treino no domínio de destino (dígitos 5–9).

Desafio: Repita o experimento variando o número de exemplos de treino disponíveis no domínio B para \(\{5, 10, 20, 40, 80\}\), comparando, para cada quantidade, a acurácia obtida com transferência de aprendizado e treinando do zero.

Saída esperada: Um gráfico com duas curvas (transferência e do zero), mostrando a acurácia em função do número de exemplos de treino, e uma conclusão sobre em que ponto (se houver) treinar do zero se torna tão eficaz quanto a transferência de aprendizado.

Dica: Espera-se que a vantagem da transferência de aprendizado diminua à medida que mais dados se tornam disponíveis no domínio de destino — eventualmente, dados suficientes tornam o treinamento do zero competitivo.

9.12.3 Exercício 3: Ajuste Fino Parcial (Fine-Tuning) (Intermediário/Desafio)

Contexto: O experimento de transferência de aprendizado congelou totalmente o extrator de características, treinando apenas a cabeça de classificação.

Desafio: Implemente uma terceira estratégia: descongele apenas a segunda camada convolucional (conv2) do extrator (mantendo conv1 congelada), permitindo que ela seja ajustada durante o treinamento no domínio B, junto com a nova cabeça de classificação. Compare a acurácia resultante às duas estratégias já implementadas (transferência totalmente congelada e treinamento do zero).

Saída esperada: Uma tabela ou gráfico de barras com as três estratégias (congelado, parcialmente ajustado, do zero) e suas respectivas acurácias no domínio B.

Dica: Para descongelar apenas conv2, itere sobre modelo.extrator.conv2.parameters() e defina requires_grad = True antes de incluí-los na lista de parâmetros passada ao otimizador.

9.12.4 Exercício 5: Congelamento do Backbone no YOLO (Intermediário/Desafio)

Contexto: No Projeto Prático 2, o ajuste fino do YOLO pré-treinado congelou as 10 primeiras camadas do backbone (freeze=10), deixando as camadas finais e a cabeça de detecção livres para se adaptar às 9 classes de objetos geométricos.

Desafio: Repita o treinamento variando o número de camadas congeladas para \(\{0, 5, 10, 15\}\) (freeze=0 corresponde a um ajuste fino completo, sem nenhuma camada congelada), mantendo fixos o número de épocas e o conjunto de dados. Para cada configuração, registre o mAP50 obtido no conjunto de validação.

Saída esperada: Uma tabela ou gráfico relacionando o número de camadas congeladas ao mAP50 final, e uma conclusão sobre se, para esta tarefa específica (domínio de origem e destino bastante distintos), congelar mais ou menos camadas produziu melhores resultados.

Dica: Como o conjunto de dados sintético é pequeno, espera-se alguma variância entre execuções; se possível, repita cada configuração com mais de uma semente aleatória na geração dos dados e reporte a média.

9.12.5 Exercício 4: Disparidade e Distância de Base (Desafio)

Contexto: O exemplo de visão estereoscópica utilizou deslocamentos fixos (4 e 22 pixels) para simular um objeto distante e um objeto próximo, respectivamente.

Desafio: Utilizando a fórmula \(Z = f \cdot B / d\), e assumindo uma distância focal fictícia \(f = 700\) pixels e uma linha de base \(B = 6\) cm (aproximando a distância entre os olhos humanos), calcule a profundidade estimada \(Z\), em centímetros, para o fundo e para o objeto em primeiro plano do experimento, a partir das disparidades médias já calculadas (disp_fundo e disp_objeto). Em seguida, gere um novo par estéreo com um terceiro deslocamento intermediário (por exemplo, 12 pixels) para uma segunda região da imagem, e calcule sua profundidade estimada.

Saída esperada: As três profundidades estimadas (fundo, objeto original, novo objeto intermediário), em centímetros, e uma verificação de que a relação entre disparidade e profundidade estimada é inversa, como previsto pela fórmula.

Dica: Tome cuidado com disparidades muito próximas de zero — a fórmula \(Z = f \cdot B / d\) produz valores extremamente grandes (ou indefinidos) nesse caso, refletindo a dificuldade real de estimar profundidade para objetos muito distantes por triangulação estéreo.

9.13 Encerramento da Parte II

Este capítulo conclui a Parte II do livro — e, com ela, a jornada que começou no Capítulo 6 com a formalização de “detectar, descrever e corresponder” características em imagens. Ao longo do caminho, um mesmo tema conceitual se repetiu sob formas cada vez mais sofisticadas: a tensão entre características projetadas manualmente (Sobel, LBP, HOG, ORB, Haar Cascade) e características aprendidas automaticamente (as CNNs deste capítulo) — e a constatação experimental, repetida em três capítulos consecutivos, de que nenhuma das duas abordagens é universalmente superior: a escolha correta depende da complexidade real do problema, da quantidade de dados disponível e das restrições computacionais da aplicação.

Para o leitor interessado em aprofundar os tópicos aqui apresentados apenas em suas ideias centrais, algumas direções naturais de estudo incluem:

  • Arquiteturas modernas de CNN (ResNet, EfficientNet, Vision Transformers) e as inovações que permitiram treinar redes cada vez mais profundas;
  • Detecção e segmentação em tempo real, com foco nas famílias YOLO e nos modelos de segmentação promptable como o Segment Anything;
  • Reconstrução 3D e SLAM (Simultaneous Localization and Mapping), que estendem a visão estereoscópica deste capítulo a cenários dinâmicos, com câmeras em movimento;
  • Modelos generativos para imagens (GANs, modelos de difusão), capazes de sintetizar, e não apenas interpretar, conteúdo visual.

Os fundamentos consolidados nesta Parte II — desde a extração de características mais simples até as redes neurais convolucionais — são a base sobre a qual todas essas direções mais avançadas se apoiam.


9.14 💻 Parte Prática com Exercícios de Programação

🚧 Em construção!

A presente lista de exercícios de programação (EP) consolida as formulações teóricas apresentadas ao longo do Capítulo 9 — Deep Learning para Visão Computacional — por meio de uma trilha prática aplicada. Diferentemente do treinamento de redes neurais completas com PyTorch, que exige tempo de execução e, por vezes, GPU, os EPs deste capítulo isolam as grandezas intermediárias de um pipeline real de aprendizado profundo — a saída de uma única camada convolucional, o resultado de uma operação de pooling, a contagem de parâmetros treináveis de uma arquitetura, a sobreposição entre caixas delimitadoras candidatas e o filtro de supressão de não-máximos — permitindo validar manualmente cada etapa do raciocínio sem depender de bibliotecas de aprendizado de máquina nem de treinamento real.

O encadeamento dos exercícios reproduz o fluxo conceitual do capítulo: inicia-se com o cálculo manual da saída de uma camada convolucional aprendida, a partir de um kernel e um viés já treinados; avança-se para a operação de pooling (máximo e média), que reduz a resolução espacial entre blocos convolucionais; prossegue-se com a contagem de parâmetros treináveis de uma arquitetura CNN completa, evidenciando por que o compartilhamento de pesos torna essas redes tão mais econômicas que uma camada totalmente conectada equivalente; aprofunda-se no cálculo de Interseção sobre União (IoU) e na Supressão de Não-Máximos (NMS), etapa de pós-processamento comum a detectores como o Faster R-CNN e o YOLO; e conclui-se com um pipeline integrado, unindo a saída de um detector de objetos (após NMS) a uma medição do mundo real por referência de escala — o mesmo princípio da fotogrametria estudada na integração final do capítulo.

ImportanteDiretrizes para a Resolução dos Exercícios de Programação

Em todos os exercícios deste capítulo, as etapas de discretização ou arredondamento numérico devem empregar o arredondamento padrão para o inteiro mais próximo (round half away from zero), mitigando ambiguidades em valores com fração exatamente igual a \(0{,}5\). Salvo indicação explícita em contrário: (i) a operação de “convolução” segue a convenção adotada pelos frameworks de aprendizado profundo — correlação cruzada, sem inversão espacial do kernel, exatamente como apresentado na Seção “Convolução como Camada Aprendida”; (ii) o preenchimento (padding) é feito com zeros; (iii) caixas delimitadoras são especificadas no formato canto-a-canto \((x_1, y_1, x_2, y_2)\), com \(x_1 < x_2\) e \(y_1 < y_2\); e (iv) vetores/matrizes seguem indexação a partir de \(0\), com a convenção [linha][coluna] para estruturas bidimensionais.

🎯 Objetivo deste Caderno

O caderno permite desenvolver, validar, organizar e testar soluções de Exercícios de Programação (EPs) em ambientes interativos, como o Colab, com os mesmos casos de teste do Moodle, copiando para lá apenas na hora de registrar a nota oficial.

Download

Baixe morph.py e testsuite.py executando a célula abaixo:

import os, sys, importlib, inspect, urllib.request

# URLs do repositório
BASE_URL = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph"
for f in ["morph.py", "testsuite.py"]:
    if not os.path.exists(f):
        urllib.request.urlretrieve(f"{BASE_URL}/{f}", f)

import morph, testsuite
importlib.reload(morph); importlib.reload(testsuite)
from morph import mm
from testsuite import TestSuite

print(f"✅ Ambiente pronto. Morph: {morph.__version__} | TestSuite: {testsuite.__version__}")
✅ Ambiente pronto. Morph: 1.1.2 | TestSuite: 1.1.2

Executando os Testes

Para avaliar os testes, execute TestSuite("EP09_01.extensão").run() numa nova célula, trocando a extensão pela da linguagem usada (.py, .java, .c, .cpp, .js ou .r). O sistema baixa os casos de teste do GitHub, executa o programa e calcula a nota automaticamente.

Para testar código Python diretamente, sem salvar arquivo, use run_code(codigo) passando o código como string numa variável codigo:

codigo = """
# ... seu código aqui ...
"""
TestSuite("EP09_01").run_code(codigo)

9.14.1 EP09_01 🟢 Convolução 2D Manual (Forward de uma Camada Aprendida)

O PyTorch, apresentado neste capítulo, executa nn.Conv2d(x) em uma única chamada — mas por trás dessa chamada está apenas a operação de correlação cruzada entre um kernel (já treinado) e uma vizinhança da entrada, seguida da soma de um viés e de uma ativação, exatamente como formalizado na Seção “Convolução como Camada Aprendida”. A diferença essencial em relação à convolução fixa do Capítulo 3 é que, aqui, os valores do kernel e do viés já vêm prontos (como se tivessem sido aprendidos por gradiente), e cabe a você reproduzir manualmente a passagem direta (forward pass) que o framework executa internamente.

Antes de treinar uma CNN de verdade, você foi encarregado de implementar essa passagem direta do zero, para uma única camada convolucional com um único canal de entrada e um único filtro de saída, incluindo suporte a padding e stride arbitrários.

9.14.1.1 📋 Diretrizes de Implementação

  1. Entrada: Ler as dimensões \(H \times W\) do mapa de características de entrada e, em seguida, seus \(H \times W\) valores reais.
  2. Kernel e viés: Ler as dimensões \(k_h \times k_w\) do kernel (já treinado), seus valores reais, e o viés \(b\) (real, escalar).
  3. Hiperparâmetros: Ler o padding \(p\) (inteiro, número de zeros adicionados em cada borda) e o stride \(s\) (inteiro, passo do deslizamento).
  4. Preenchimento: Adicionar \(p\) zeros em cada uma das quatro bordas do mapa de entrada antes da correlação.
  5. Correlação cruzada: Para cada posição de saída \((i, j)\), calcular \[ z(i,j) = b + \sum_{u=0}^{k_h-1} \sum_{v=0}^{k_w-1} K(u,v) \cdot X_{pad}(i \cdot s + u,\; j \cdot s + v), \] percorrendo a entrada sem inverter o kernel (convenção dos frameworks de aprendizado profundo, diferente da convolução matemática clássica).
  6. Ativação: Aplicar ReLU a cada valor: \(a(i,j) = \max(0, z(i,j))\).
  7. Dimensões de saída: \(O_h = \lfloor (H + 2p - k_h)/s \rfloor + 1\) e \(O_w = \lfloor (W + 2p - k_w)/s \rfloor + 1\).
  8. Saída: Imprimir \(O_h\) e \(O_w\) na primeira linha, seguidos de \(O_h\) linhas com \(O_w\) valores reais cada (o mapa de características de saída, já com ReLU aplicada), formatados com 4 casas decimais.

9.14.1.2 📌 Restrições Computacionais

  • Um canal de entrada, um filtro de saída: não é necessário lidar com múltiplos canais ou múltiplos filtros nesta versão simplificada.
  • Sem inversão do kernel: implemente correlação cruzada, não a convolução matemática clássica com kernel invertido — é essa a operação que o PyTorch (e a maioria dos frameworks) chama de “convolução”.
  • Preenchimento por zeros: os \(p\) pixels adicionados em cada borda valem sempre \(0\).
  • Formatação: todos os valores de saída devem ter exatamente 4 casas decimais, mesmo quando o valor é inteiro (ex.: 2.0000).

9.14.1.3 🧠 Fundamentação Teórica

Elemento Papel na camada convolucional
Kernel \(K\) Parâmetros aprendidos por gradiente, análogos aos filtros de Sobel do Capítulo 3, mas ajustados a partir de dados
Viés \(b\) Parâmetro aprendido, somado à saída de cada janela
Padding \(p\) Controla a dimensão espacial da saída; \(p=0\) (“valid”) reduz a resolução, \(p\) maior preserva-a
Stride \(s\) Passo do deslizamento; \(s>1\) sub-amostra a saída, reduzindo custo computacional
ReLU Ativação não-linear aplicada após cada convolução, sem a qual camadas empilhadas colapsariam em uma única transformação linear

9.14.1.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiros \(H\) e \(W\).
  • Próximas \(H\) linhas: \(W\) valores reais cada (mapa de entrada).
  • Próxima linha: Inteiros \(k_h\) e \(k_w\).
  • Próximas \(k_h\) linhas: \(k_w\) valores reais cada (kernel).
  • Próxima linha: Um valor real \(b\) (viés).
  • Próxima linha: Inteiros \(p\) e \(s\) (padding e stride).

Saída:

  • Linha 1: Inteiros \(O_h\) e \(O_w\).
  • Próximas \(O_h\) linhas: \(O_w\) valores reais cada, formatados com 4 casas decimais.

9.14.1.5 📌 Exemplos

Entrada Saída Observação
3 3
1 2 0
0 1 2
1 0 1
2 2
1 1
1 1
-2
0 1
2 2
2.0000 3.0000
0.0000 2.0000
Padding 0, stride 1: saída \(2\times2\) sem preenchimento.
3 3
1 2 0
0 1 2
1 0 1
2 2
1 0
0 1
0
1 2
2 2
1.0000 0.0000
1.0000 2.0000
Padding 1, stride 2: entrada preenchida com zeros antes da correlação.
🎮 Simulador: Convolução 2D Manual 🟢 correlação cruzada + viés + ReLU

Entrada 4×4 fixa, kernel 2×2 fixo (destacado em azul), padding=0, stride=1 — deslize para escolher a posição de saída.

(0,0)
Entrada X (4×4)
Kernel K (2×2)
Figura 9.10: Simulador: Convolução 2D Manual (correlação cruzada + viés + ReLU)
%%writefile EP09_01.py
# Código Python
Writing EP09_01.py
TestSuite("EP09_01.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/casos/EP09_01.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/cap9/EP9_1.cases
❌ Não foi possível baixar EP09_01.cases

9.14.2 EP09_02 🟢 Pooling Manual (Máximo e Média)

Entre blocos convolucionais, a arquitetura típica de uma CNN intercala camadas de pooling, que reduzem a resolução espacial do mapa de características sem introduzir novos parâmetros treináveis — ao contrário da convolução, o pooling não tem pesos: ele apenas resume cada janela da entrada a um único valor, por um máximo ou por uma média.

Você foi encarregado de implementar essa operação a partir de uma janela deslizante quadrada, sem sobreposição parcial nas bordas (apenas janelas completas), suportando os dois tipos mais comuns: max (preserva o valor mais saliente, tipicamente usado para reter bordas e texturas fortes) e avg (suaviza a região, preservando informação de intensidade média).

9.14.2.1 📋 Diretrizes de Implementação

  1. Entrada: Ler as dimensões \(H \times W\) do mapa de características de entrada e seus \(H \times W\) valores reais.
  2. Janela: Ler os inteiros \(k\) (tamanho da janela quadrada \(k \times k\)) e \(s\) (stride).
  3. Tipo: Ler uma string, max ou avg, indicando o tipo de pooling.
  4. Sem preenchimento: Esta operação não utiliza padding; janelas que ultrapassariam a borda da entrada são descartadas.
  5. Cálculo: Para cada posição de saída \((i,j)\), calcular o máximo ou a média dos \(k \times k\) valores da janela correspondente, iniciando em \((i \cdot s,\, j \cdot s)\).
  6. Dimensões de saída: \(O_h = \lfloor (H - k)/s \rfloor + 1\) e \(O_w = \lfloor (W - k)/s \rfloor + 1\).
  7. Saída: Imprimir \(O_h\) e \(O_w\) na primeira linha, seguidos de \(O_h\) linhas com \(O_w\) valores reais cada, formatados com 4 casas decimais.

9.14.2.2 📌 Restrições Computacionais

  • Janela quadrada: \(k \times k\), sem suporte a janelas retangulares nesta versão.
  • Sem padding: apenas janelas inteiramente contidas na entrada são consideradas.
  • avg usa divisão real: a média é sempre \(\text{soma}/k^2\), mesmo quando o resultado tem muitas casas decimais — arredonde apenas na formatação final, conforme a diretriz geral do capítulo.
  • Formatação: todos os valores de saída com exatamente 4 casas decimais.

9.14.2.3 🧠 Fundamentação Teórica

Elemento Papel na arquitetura
Pooling máximo Preserva a ativação mais forte da janela; comum após camadas convolucionais para reter bordas e texturas salientes
Pooling médio Suaviza a região, preservando a intensidade média; comum em camadas finais (global average pooling)
Ausência de parâmetros Diferencia o pooling da convolução: reduz resolução espacial sem custo adicional de treinamento
Redução de resolução Contribui para a invariância a pequenas translações e para a redução do custo computacional das camadas seguintes

9.14.2.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiros \(H\) e \(W\).
  • Próximas \(H\) linhas: \(W\) valores reais cada.
  • Próxima linha: Inteiros \(k\) e \(s\).
  • Próxima linha: max ou avg.

Saída:

  • Linha 1: Inteiros \(O_h\) e \(O_w\).
  • Próximas \(O_h\) linhas: \(O_w\) valores reais cada, formatados com 4 casas decimais.

9.14.2.5 📌 Exemplos

Entrada Saída Observação
4 4
1 3 2 4
5 6 1 2
2 1 0 3
4 2 5 1
2 2
max
2 2
6.0000 4.0000
4.0000 5.0000
Pooling máximo, janela \(2\times2\), stride 2.
4 4
1 3 2 4
5 6 1 2
2 1 0 3
4 2 5 1
2 2
avg
2 2
3.7500 2.2500
2.2500 2.2500
Pooling médio sobre as mesmas janelas.
🎮 Simulador: Pooling Manual 🟢 janela 2×2, stride 2

Escolha o quadrante e o tipo de pooling para ver o valor resultante.

(0,0)
Figura 9.11: Simulador: Pooling Manual (máximo vs. média)
%%writefile EP09_02.py
# Código Python
Writing EP09_02.py
TestSuite("EP09_02.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/casos/EP09_02.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/cap9/EP9_2.cases
❌ Não foi possível baixar EP09_02.cases

9.14.3 EP09_03 🟡 Contagem de Parâmetros Treináveis de uma CNN

O Exercício 1 deste capítulo pediu para adicionar uma terceira camada convolucional à CNN do Projeto Prático 1 e comparar o número de parâmetros da rede resultante. Este EP formaliza esse cálculo: dada a descrição textual de uma arquitetura — uma sequência de camadas convolucionais, de pooling e totalmente conectadas — determine, para cada camada, o número de parâmetros treináveis e o total da rede.

O ponto central deste exercício é notar que a contagem de parâmetros de uma camada convolucional depende apenas do tamanho do kernel e do número de canais de entrada/saída — nunca das dimensões espaciais (\(H \times W\)) do mapa de características, graças ao compartilhamento de pesos: o mesmo kernel desliza por toda a imagem, seja ela \(28\times28\) ou \(280\times280\). É exatamente esse compartilhamento que torna as CNNs muito mais econômicas em parâmetros do que uma camada totalmente conectada equivalente, na qual cada pixel de entrada tem uma conexão (e um peso) independente para cada neurônio de saída.

9.14.3.1 📋 Diretrizes de Implementação

  1. Entrada: Ler o inteiro \(L\) (número de camadas da arquitetura, na ordem em que são aplicadas).
  2. Camadas: Ler \(L\) linhas, cada uma descrevendo uma camada em um dos três formatos:
    • CONV kh kw cin cout bias — camada convolucional com kernel \(k_h \times k_w\), cin canais de entrada, cout canais (filtros) de saída, e bias (0 ou 1) indicando se há viés por filtro;
    • POOL — camada de pooling (máximo ou médio; não possui parâmetros treináveis);
    • FC in out bias — camada totalmente conectada com in entradas, out saídas, e bias (0 ou 1) indicando se há viés por neurônio.
  3. Parâmetros de uma camada CONV: \(k_h \cdot k_w \cdot c_{in} \cdot c_{out} + c_{out} \cdot \text{bias}\).
  4. Parâmetros de uma camada FC: \(\text{in} \cdot \text{out} + \text{out} \cdot \text{bias}\).
  5. Parâmetros de uma camada POOL: sempre \(0\).
  6. Saída: Para cada camada, na ordem de leitura, imprimir Camada i: P, onde \(i\) começa em \(1\) e \(P\) é o número de parâmetros daquela camada. Ao final, imprimir Total: T, com \(T\) igual à soma de parâmetros de todas as camadas.

9.14.3.2 📌 Restrições Computacionais

  • Independência da dimensão espacial: a entrada não informa \(H \times W\) — a contagem de uma camada CONV não depende disso, apenas de kh kw cin cout.
  • bias sempre 0 ou 1: multiplique diretamente o termo de viés por esse valor, sem tratamento condicional especial.
  • Camadas POOL sem argumentos adicionais: a linha contém apenas a palavra POOL.
  • Todos os valores de entrada e saída são inteiros não-negativos.

9.14.3.3 🧠 Fundamentação Teórica

Elemento Papel na contagem de parâmetros
Compartilhamento de pesos O mesmo kernel \(k_h \times k_w\) é reutilizado em toda a extensão espacial da entrada — por isso a contagem independe de \(H \times W\)
Canais de entrada/saída Cada um dos cout filtros possui um conjunto de pesos por canal de entrada, daí o fator \(c_{in} \cdot c_{out}\)
Viés Um único escalar por filtro (CONV) ou por neurônio (FC), independente do tamanho do kernel ou da entrada
Camada FC Cada uma das in entradas conecta-se a cada uma das out saídas — sem compartilhamento, o que explica seu custo tipicamente muito maior em número de parâmetros

9.14.3.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(L\).
  • Próximas \(L\) linhas: descrição de cada camada, no formato CONV kh kw cin cout bias, POOL, ou FC in out bias.

Saída:

  • \(L\) linhas no formato Camada i: P.
  • Última linha: Total: T.

9.14.3.5 📌 Exemplos

Entrada Saída Observação
3
CONV 3 3 1 8 1
POOL
FC 1352 10 1
Camada 1: 80
Camada 2: 0
Camada 3: 13530
Total: 13610
Rede com uma convolução, um pooling e uma camada final.
4
CONV 3 3 1 8 1
CONV 3 3 8 16 1
POOL
FC 64 32 0
Camada 1: 80
Camada 2: 1168
Camada 3: 0
Camada 4: 2048
Total: 3296
Rede com duas convoluções empilhadas, como no Projeto Prático 1; a última camada não usa viés.
🎮 Simulador: Contagem de Parâmetros 🟡 compartilhamento de pesos

Ajuste o kernel e os canais de uma camada CONV e compare com uma camada FC equivalente, para uma entrada hipotética de 32×32 pixels.

3
1
8
Figura 9.12: Simulador: Contagem de Parâmetros — Convolução vs. Camada Totalmente Conectada
%%writefile EP09_03.py
# Código Python
Writing EP09_03.py
TestSuite("EP09_03.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/casos/EP09_03.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/cap9/EP9_3.cases
❌ Não foi possível baixar EP09_03.cases

9.14.4 EP09_04 🟡 Interseção sobre União (IoU) e Supressão de Não-Máximos (NMS)

Tanto o Faster R-CNN quanto o YOLO, apresentados na seção “Aplicações em Larga Escala” e no Projeto Prático 2 deste capítulo, produzem internamente muito mais caixas delimitadoras candidatas do que objetos realmente existem na imagem — várias delas cobrindo, com pequenas variações, o mesmo objeto. A etapa de pós-processamento responsável por eliminar essas redundâncias é a Supressão de Não-Máximos (NMS), e sua peça fundamental é a métrica de Interseção sobre União (IoU), que mede o quanto duas caixas se sobrepõem — a mesma métrica usada, por exemplo, para definir o limiar de acerto do mAP50 calculado no Projeto Prático 2 (IoU \(\ge 0{,}5\) com a caixa verdadeira).

Você foi encarregado de implementar o algoritmo de NMS do zero: dado um conjunto de caixas candidatas com suas pontuações de confiança, filtrar as redundantes e manter apenas as detecções mais confiáveis e suficientemente distintas entre si.

9.14.4.1 📋 Diretrizes de Implementação

  1. Entrada: Ler o inteiro \(N\) (número de caixas candidatas) e o limiar real \(\tau\) (limiar de IoU para supressão).
  2. Caixas: Ler \(N\) linhas, cada uma com cinco valores reais: \(x_1, y_1, x_2, y_2, \text{score}\) (canto superior-esquerdo, canto inferior-direito, pontuação de confiança).
  3. Interseção sobre União: Para duas caixas \(A\) e \(B\), \[ \text{IoU}(A,B) = \frac{\text{área}(A \cap B)}{\text{área}(A) + \text{área}(B) - \text{área}(A \cap B)}, \] onde a área de interseção é calculada pela sobreposição dos intervalos em \(x\) e em \(y\) (zero se não houver sobreposição).
  4. Algoritmo guloso de NMS:
    1. Ordene as caixas por score decrescente (em caso de empate, mantenha a ordem de leitura original — ordenação estável).
    2. Selecione a caixa de maior pontuação entre as restantes; adicione-a ao conjunto de saída e remova-a da lista.
    3. Calcule o IoU dessa caixa selecionada com todas as caixas ainda restantes; remova (suprima) qualquer caixa com \(\text{IoU} > \tau\).
    4. Repita os passos (b)–(c) até que não restem caixas.
  5. Saída: Para cada caixa mantida, na ordem em que foi selecionada, imprimir seu índice original (posição de leitura, começando em \(0\)) e seu score, formatado com 4 casas decimais. Ao final, imprimir Total mantidas: X.

9.14.4.2 📌 Restrições Computacionais

  • Supressão estrita: apenas caixas com \(\text{IoU} > \tau\) são suprimidas; caixas com \(\text{IoU}\) exatamente igual a \(\tau\) são mantidas.
  • Índices originais: a saída referencia a posição em que cada caixa foi lida na entrada (começando em \(0\)), não sua posição após a ordenação.
  • Retângulos alinhados aos eixos: todas as caixas são especificadas por dois cantos, com \(x_1 < x_2\) e \(y_1 < y_2\) garantidos na entrada.

9.14.4.3 🧠 Fundamentação Teórica

Elemento Papel no pós-processamento de detecção
IoU Quantifica a sobreposição espacial entre duas caixas; \(\text{IoU}=1\) para caixas idênticas, \(\text{IoU}=0\) para caixas disjuntas
Limiar \(\tau\) Controla a agressividade da supressão: \(\tau\) baixo elimina até detecções pouco sobrepostas; \(\tau\) alto preserva quase todas
Ordenação por confiança Garante que, entre caixas redundantes, sempre sobrevive a de maior pontuação
mAP50 (Projeto Prático 2) Usa exatamente o mesmo limiar de IoU (\(0{,}5\)) para decidir se uma detecção final “acerta” a caixa verdadeira

9.14.4.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Inteiro \(N\) e real \(\tau\).
  • Próximas \(N\) linhas: cinco reais \(x_1\ y_1\ x_2\ y_2\ \text{score}\).

Saída:

  • Uma linha por caixa mantida, na ordem de seleção: índice score (score com 4 casas decimais).
  • Última linha: Total mantidas: X.

9.14.4.5 📌 Exemplos

Entrada Saída Observação
3 0.5
0 0 10 10 0.9
1 1 11 11 0.75
50 50 60 60 0.8
0 0.9000
2 0.8000
Total mantidas: 2
A caixa 1 é suprimida por sobrepor fortemente a caixa 0 (IoU≈0,68 > 0,5).
2 0.5
0 0 10 10 0.9
0 0 10 6 0.95
1 0.9500
Total mantidas: 1
A caixa 0 é suprimida por sobrepor demais a vencedora (IoU=0,6 > 0,5), mesmo tendo menor score de origem que a caixa 1, aqui a de maior score.
🎮 Simulador: IoU e Supressão de Não-Máximos 🟡 duas caixas candidatas

Ajuste a sobreposição entre a caixa vencedora (azul, score maior) e a candidata (vermelha) e o limiar de supressão.

3
0.50
Figura 9.13: Simulador: IoU e Supressão de Não-Máximos
%%writefile EP09_04.py
# Código Python
Writing EP09_04.py
TestSuite("EP09_04.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/casos/EP09_04.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/cap9/EP9_4.cases
❌ Não foi possível baixar EP09_04.cases

9.14.5 EP09_05 🔴 Pipeline Integrado: Da Detecção à Medição do Mundo Real

Este exercício final integra os dois exercícios anteriores ao princípio de fotogrametria apresentado na seção “Integrando Geometria e Aprendizado Profundo” — exatamente o mesmo cálculo implementado na figura de medição por referência de escala deste capítulo. O cenário reproduz uma situação realista: um detector (Faster R-CNN ou YOLO) gera várias caixas candidatas sobrepostas para o mesmo objeto de interesse; após filtrá-las por NMS, a caixa sobrevivente de maior confiança é usada, junto de uma caixa de referência de largura real conhecida (como o cartão de \(8{,}56\) cm), para estimar as dimensões reais do objeto detectado.

9.14.5.1 📋 Diretrizes de Implementação

  1. Referência conhecida: Ler o valor real \(L_{ref}\) (largura real do objeto de referência, em cm) e, em seguida, os quatro reais \(x_1\ y_1\ x_2\ y_2\) de sua caixa delimitadora em pixels (já conhecida, sem necessidade de detecção).
  2. Candidatas do objeto a medir: Ler o inteiro \(N\) (número de caixas candidatas produzidas pelo detector para o objeto de interesse) e o limiar real \(\tau\); em seguida, ler as \(N\) linhas de caixas candidatas, cada uma com \(x_1\ y_1\ x_2\ y_2\ \text{score}\).
  3. Etapa 1 — NMS: Aplique exatamente o algoritmo de Supressão de Não-Máximos do EP09_04 às \(N\) caixas candidatas, usando o limiar \(\tau\), para eliminar detecções redundantes do mesmo objeto.
  4. Etapa 2 — Seleção da caixa final: Após o NMS, a caixa de maior score entre as mantidas é a detecção final do objeto (a entrada garante que todas as caixas candidatas correspondem a um único objeto físico, portanto a primeira caixa selecionada pelo NMS já é o resultado final).
  5. Etapa 3 — Medição por referência de escala: Calcule a razão \(\text{cm/pixel} = L_{ref} / \text{largura da referência em pixels}\) e aplique-a tanto à largura quanto à altura (em pixels) da caixa final do objeto, obtendo suas dimensões reais estimadas em centímetros.
  6. Saída: Primeiro, uma linha por caixa mantida após o NMS (mesmo formato do EP09_04): índice score. Em seguida, a linha Total mantidas: X. Por fim, a linha Objeto: L x A cm, onde \(L\) e \(A\) são a largura e a altura estimadas do objeto, cada uma com 2 casas decimais.

9.14.5.2 📌 Restrições Computacionais

  • Reaproveite o NMS do EP09_04 integralmente — mesma regra de desempate, mesmo critério de supressão (\(\text{IoU} > \tau\)).
  • A referência não passa por NMS: sua caixa é dada diretamente, sem candidatas concorrentes.
  • Razão única para largura e altura: assim como na figura de fotogrametria do capítulo, a mesma razão cm/pixel (derivada da largura da referência) é aplicada tanto à largura quanto à altura do objeto — não há calibração vertical separada.

9.14.5.3 🧠 Fundamentação Teórica

Etapa Conceito do capítulo
Múltiplas caixas candidatas Saída bruta de um detector como o Faster R-CNN ou o YOLO, antes do pós-processamento
NMS (EP09_04) Filtra a redundância, mantendo apenas a detecção mais confiável do objeto
Referência de escala conhecida Mesmo princípio da fotogrametria: um objeto de dimensão real conhecida converte pixels em centímetros
Razão cm/pixel Fator de conversão único, assumindo que a câmera está aproximadamente perpendicular à cena (sem correção de perspectiva)

9.14.5.4 📦 Especificação de Entrada e Saída (VPL)

Entrada:

  • Linha 1: Real \(L_{ref}\).
  • Linha 2: Quatro reais \(x_1\ y_1\ x_2\ y_2\) (caixa de referência).
  • Linha 3: Inteiro \(N\) e real \(\tau\).
  • Próximas \(N\) linhas: cinco reais \(x_1\ y_1\ x_2\ y_2\ \text{score}\) (caixas candidatas do objeto).

Saída:

  • Uma linha por caixa mantida após NMS: índice score (score com 4 casas decimais).
  • Linha Total mantidas: X.
  • Linha final: Objeto: L x A cm (2 casas decimais cada).

9.14.5.5 📌 Exemplos

Entrada Saída Observação
8.56
30 200 170 288
3 0.5
250 100 470 250 0.92
255 105 468 245 0.88
600 600 650 650 0.40
0 0.9200
2 0.4000
Total mantidas: 2
Objeto: 13.45 x 9.17 cm
A caixa 1 é suprimida por sobrepor fortemente a caixa 0; a detecção final do objeto é a caixa 0.
🎮 Simulador: Pipeline Integrado 🔴 detecção + fotogrametria

A caixa cinza é a referência de largura real conhecida; ajuste-a e veja a medição do objeto (vermelho) recalculada — a mesma razão cm/pixel se aplica à largura e à altura.

8.56
referência
objeto detectado
Figura 9.14: Simulador: Pipeline Integrado — NMS + Medição por Referência de Escala
%%writefile EP09_05.py
# Código Python
Writing EP09_05.py
TestSuite("EP09_05.py").run()
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/casos/EP09_05.cases
📥 Tentando: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap09/cap9/EP9_5.cases
❌ Não foi possível baixar EP09_05.cases