---
title: "Classificação de Imagens RGB com SVM"
subtitle: "CEAO-802 — Métodos de Análise de Dados"
author: "1T Generoso · 1T João Marcos · 1T Vitor Cesa"
date: today
lang: pt-BR
format:
  revealjs:
    theme: simple
    embed-resources: true
    slide-number: c/t
    progress: true
    transition: slide
    transition-speed: fast
    code-overflow: scroll
    highlight-style: github
    fig-align: center
    footer: "CEAO-802 — Classificação de Imagens RGB com SVM"
    width: 1280
    height: 720
    include-in-header:
      text: |
        <style>
          .reveal h1, .reveal h2 { color: #2C7FB8 !important; }
          .reveal h3 { color: #1a5276 !important; }
          .reveal .slide-number { color: #2C7FB8; }
          .reveal .progress { background: #E06C00 !important; }
          .destaque  { color: #E06C00; font-weight: bold; }
          .azul      { color: #2C7FB8; font-weight: bold; }
          .reveal pre { font-size: 0.72em; line-height: 1.45; }
          .reveal table { font-size: 0.78em; margin: auto; }
          .reveal table th {
            background: #2C7FB8; color: white; padding: 4px 10px;
          }
          .reveal table td { padding: 3px 10px; }
          .reveal table tr:nth-child(even) td { background: #EBF5FB; }
          .caixa-azul {
            background: #EBF5FB;
            border-left: 5px solid #2C7FB8;
            padding: 0.45em 1em;
            border-radius: 4px;
            margin-top: 0.6em;
            font-size: 0.86em;
          }
          .caixa-laranja {
            background: #FEF9E7;
            border-left: 5px solid #E06C00;
            padding: 0.45em 1em;
            border-radius: 4px;
            margin-top: 0.6em;
            font-size: 0.86em;
          }
          .reveal .footer { color: #aaa; font-size: 0.65em; }

          /* ── Redução de tamanho base (padrão simple = ~40px) ── */
          :root { --r-main-font-size: 34px; }
          .reveal { font-size: 34px; }
          .reveal h2 { font-size: 1.35em; margin-bottom: 0.25em; }
          .reveal h3 { font-size: 1.05em; margin-bottom: 0.2em; }
          .reveal ul, .reveal ol { margin: 0.25em 0 0.25em 1.2em; }
          .reveal li { margin: 0.15em 0; line-height: 1.3; }
          .reveal p  { margin: 0.3em 0; line-height: 1.35; }
          .reveal blockquote { padding: 0.3em 0.8em; margin: 0.35em 0; }
        </style>
execute:
  echo: false
  warning: false
  message: false
---

```{r}
#| label: setup
#| include: false

library(tidyverse)
library(knitr)
library(scales)
library(caret)
library(e1071)

azul    <- "#2C7FB8"
laranja <- "#E06C00"

# ── Carregar resultados do cache (sem rodar CNN nem modelos) ──────────────────
resultados_holdout <- read_csv("../outputs_parcial/resultados_modelos_rgb.csv",
                               show_col_types = FALSE) %>%
  arrange(desc(acuracia))

resultados_kfold <- read_csv("../outputs_parcial/resultados_kfold_rgb.csv",
                             show_col_types = FALSE) %>%
  arrange(desc(acuracia))

importancia_df <- read_csv("../outputs_parcial/importancia_variaveis_rgb.csv",
                           show_col_types = FALSE)

cores_modelos <- c(
  "SVM linear"          = "#2C7FB8",
  "SVM radial"          = "#AED6F1",
  "SVM radial ajustado" = "#5DADE2",
  "PCA + SVM radial"    = "#1A5276",
  "Random Forest"       = "#E06C00"
)
```

```{r}
#| label: confmat
#| include: false

# ── Matriz de confusão do SVM linear — carregada do cache (.rds) ─────────────
# Não re-treina o modelo: só carrega o objeto salvo e prediz no mesmo split.
modelos_salvos <- tryCatch(
  readRDS("../outputs_parcial/modelos_rgb.rds"),
  error = function(e) NULL
)

dados_cache <- tryCatch(
  read_csv("../outputs_parcial/features_rgb.csv", show_col_types = FALSE) %>%
    mutate(classe = factor(classe, levels = paste0("C", 1:13))),
  error = function(e) NULL
)

cm_plot <- NULL

if (!is.null(modelos_salvos) && !is.null(dados_cache)) {
  set.seed(123)
  idx_tr <- createDataPartition(dados_cache$classe, p = 0.70, list = FALSE)
  teste  <- dados_cache[-idx_tr, ]

  cols_meta <- c("arquivo", "classe", "tamanho_mb")
  x_te <- teste %>% select(-all_of(cols_meta))
  y_te <- factor(teste$classe, levels = levels(dados_cache$classe))

  # Selecionar as colunas que o preproc conhece (após nearZeroVar)
  features_usadas <- names(modelos_salvos$preproc$mean)
  x_te_sub  <- x_te[, features_usadas, drop = FALSE]
  x_te_norm <- predict(modelos_salvos$preproc, x_te_sub)

  pred   <- predict(modelos_salvos$modelo_svm_linear, x_te_norm)
  cm_tbl <- as.data.frame(table(Predicao = pred, Real = y_te))

  cm_plot <- ggplot(cm_tbl, aes(x = Real, y = Predicao, fill = Freq)) +
    geom_tile(color = "white", linewidth = 0.4) +
    geom_text(aes(label = ifelse(Freq > 0, Freq, "")),
              size = 3, color = "black") +
    scale_fill_gradient(low = "#EBF5FB", high = "#1A5276") +
    labs(x = "Classe real", y = "Classe prevista", fill = "n") +
    theme_minimal(base_size = 11) +
    theme(axis.text.x    = element_text(angle = 45, hjust = 1),
          axis.text      = element_text(size = 9),
          panel.grid     = element_blank(),
          legend.position = "right")
}
```


## Roteiro {.center}

::: {.incremental}
1. Contexto e objetivo
2. Fundamentos: Transfer Learning e SVM
3. Pipeline em R — código-chave
4. Cinco modelos avaliados
5. Comparação de resultados
6. Validação cruzada K-Fold
7. Discussão e conclusão
:::


## Contexto e Objetivo

:::: {.columns}

::: {.column width="55%"}
- Imagens RGB de cenas urbanas por **drone** — Guaratinguetá (SP)
- **13 classes** de cobertura do solo (C1–C13); dataset: RGB + TIR
- Este trabalho: apenas canal **RGB**
- Amostra: **50 imagens / classe** · **650 imagens totais** (de 1.248)
- Divisão: **70% treino** (~455) / **30% teste** (~195), estratificada
:::

::: {.column width="45%"}
**Objetivo**

> Classificação supervisionada de cenas urbanas usando Transfer Learning como extrator de atributos e SVM como classificador.

**Baseline aleatório** (13 classes):

<span class="destaque" style="font-size:1.5em">7,7%</span>

Qualquer modelo acima disso aprende algo real.
:::

::::


## As 13 Classes do Dataset

:::: {.columns}

::: {.column width="50%"}
| Classe | Cobertura |
|:------:|-----------|
| C1 | Vegetação Arbórea |
| C2 | Vegetação Rasteira |
| C3 | Solo Exposto |
| C4 | Pavimento Asfáltico |
| C5 | Solo Estabilizado |
| C6 | Telhado Cerâmico |
| C7 | Telhado de Fibrocimento |
:::

::: {.column width="50%"}
| Classe | Cobertura |
|:------:|-----------|
| C8 | Telhado Metálico |
| C9 | Placa Fotovoltaica |
| C10 | Piscina |
| C11 | Pedra Sabão |
| C12 | Caixa D'água |
| C13 | Veículos |
:::

::::

::: {.caixa-azul}
Arquivo: `C10_2023_06_M_01_RGB.tiff` → classe **C10**, junho 2023, manhã. A classe é inferida do **prefixo do nome** — sem estrutura de subpastas.
:::


## Por que Transfer Learning?

:::: {.columns}

::: {.column width="48%"}
### Abordagem tradicional

Extrair atributos estatísticos manualmente:
média, desvio-padrão, histogramas de cor

❌ Representação rasa — perde padrões espaciais e texturais complexos
:::

::: {.column width="52%"}
### Abordagem adotada

**MobileNetV2** pré-treinada no ImageNet
(>1 M imagens · 1.000 classes · Google)

- Remove a camada classificadora final
- Aplica pooling global médio
- Gera vetor de **1.280 atributos** por imagem

✅ Bordas, texturas e formas de alto nível, aprendidas em escala industrial
:::

::::

::: {.caixa-azul}
A CNN faz o trabalho de **representação**. O SVM recebe os 1.280 atributos e aprende a **fronteira de decisão** — sem re-treinar a CNN.
:::


## O que é SVM?

:::: {.columns}

::: {.column width="55%"}
**Ideia central**

Encontrar o hiperplano que **maximiza a margem** entre as classes.

**Parâmetros principais**

- `cost` — tolerância a erros; maior cost = fronteira mais rígida
- `gamma` — alcance do kernel radial; maior gamma = vizinhança menor

**Kernel trick**

Kernel radial (RBF) permite fronteiras **não-lineares** projetando os dados em espaço de alta dimensão
:::

::: {.column width="45%"}
**Dois kernels testados**

| Kernel  | Fronteira  | Parâmetros       |
|---------|-----------|------------------|
| Linear  | Hiperplano | `cost`           |
| Radial  | Não-linear | `cost` + `gamma` |

::: {.caixa-laranja}
SVM é sensível à escala → atributos **devem ser padronizados** antes do treino.
:::
:::

::::


## Pipeline Geral

<div style="display:flex; align-items:center; justify-content:center; gap:0; margin-top:80px">
  <div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">650 imagens<br>RGB</div>
  <span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
  <div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">MobileNetV2<br>1.280 atributos</div>
  <span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
  <div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">center + scale<br>nearZeroVar</div>
  <span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
  <div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">5 modelos<br>SVM · RF</div>
  <span style="font-size:2.2em; color:#999; margin:0 10px; flex-shrink:0">→</span>
  <div style="background:#2C7FB8; color:white; border-radius:50%; width:200px; height:200px; display:flex; align-items:center; justify-content:center; text-align:center; font-weight:bold; font-size:0.85em; line-height:1.4; flex-shrink:0">Holdout<br>K-Fold k=5</div>
</div>


## Código — Leitura e Amostra

```{r}
#| eval: false
#| echo: true
# Listar imagens e inferir classe do prefixo do nome
arquivos <- list.files(path = caminho_rgb,
  pattern = "\\.(jpg|jpeg|png|bmp|tif|tiff)$",
  recursive = TRUE, full.names = TRUE)

classe_inferida <- stringr::str_extract(nome_sem_extensao, "^C\\d+")

# Amostra estratificada e balanceada: 50 imagens/classe
set.seed(123)
metadados <- metadados_completo %>%
  group_by(classe) %>%
  group_modify(~ slice_sample(.x, n = min(50, nrow(.x)))) %>%
  ungroup()
# → 650 imagens totais (50 × 13 classes)
```

**O que faz:** lista os TIFFs, extrai a classe `C1`–`C13` do prefixo e sorteia 50 imagens por classe com semente fixa.


## Código — Extração com MobileNetV2

```{r}
#| eval: false
#| echo: true
# Carregar MobileNetV2 sem a camada classificadora
modelo_base <- application_mobilenet_v2(
  include_top  = FALSE,      # remove o bloco de 1.000 classes
  weights      = "imagenet", # pesos pré-treinados no ImageNet
  pooling      = "avg",      # GlobalAveragePooling → vetor 1D
  input_shape  = c(224L, 224L, 3L)
)

# Normalização específica da MobileNetV2: pixels → [-1, 1]
batch_prep <- (batch_array / 127.5) - 1.0

# features: matriz  n × 1.280  (processado em lotes de 16)
features <- predict(modelo_base, batch_prep, verbose = 0L)
```

**O que faz:** carrega MobileNetV2, normaliza pixels para [−1, 1] e extrai 1.280 atributos por imagem. Resultado salvo em cache `.rds`.


## Código — Pré-processamento e Treino

```{r}
#| eval: false
#| echo: true
# Divisão estratificada 70/30
idx_treino <- createDataPartition(dados$classe, p = 0.70, list = FALSE)

# Remover atributos de variância quase nula
variaveis_nzv <- nearZeroVar(x_treino)

# Padronizar: média 0, desvio 1 (essencial para o SVM)
preproc       <- preProcess(x_treino, method = c("center", "scale"))
x_treino_norm <- predict(preproc, x_treino)
x_teste_norm  <- predict(preproc, x_teste)

# M1 — SVM linear
modelo_svm_linear <- svm(x = x_treino_norm, y = y_treino,
  kernel = "linear", cost = 1, scale = FALSE)
```

**O que faz:** divide 70/30, padroniza e treina. O mesmo fluxo se repete para cada um dos 5 modelos (parâmetros variam).


## Os 5 Modelos

| # | Modelo | Tipo | Parâmetros |
|:--:|--------|:----:|-----------|
| 1 | **SVM linear** | Linear | `cost = 1` |
| 2 | **SVM radial** | RBF | `cost = 10` · `gamma = 0,01` (fixos) |
| 3 | **SVM radial ajustado** | RBF | `cost` e `gamma` por grade 4×4 + CV |
| 4 | **PCA + SVM radial** | RBF | PCA (≥ 95% var., máx. 30 comp.) + RBF |
| 5 | **Random Forest** | Ensemble | 500 árvores · `importance = TRUE` |

::: {.caixa-azul}
O PCA reduz os 1.280 atributos aos componentes que explicam ≥ 95% da variância antes de treinar o SVM, eliminando redundâncias.
:::


## Comparação — Holdout 70/30

```{r}
#| fig-width: 10
#| fig-height: 4.5

ggplot(resultados_holdout,
       aes(x = reorder(modelo, acuracia), y = acuracia, fill = modelo)) +
  geom_col(show.legend = FALSE, width = 0.7) +
  geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
            hjust = -0.12, size = 4.5, fontface = "bold", color = "grey20") +
  coord_flip(ylim = c(0, 0.52)) +
  scale_fill_manual(values = cores_modelos) +
  labs(x = NULL, y = "Acurácia") +
  theme_minimal(base_size = 14) +
  theme(panel.grid.major.y = element_blank(),
        panel.grid.minor   = element_blank())
```

::: {.caixa-azul}
Baseline aleatório (13 classes) = **7,7%** — SVM linear (~40%) é **5,2× melhor que o acaso**.
:::


## Melhor Modelo — SVM Linear

:::: {.columns}

::: {.column width="33%"}
| Métrica | Holdout | K-Fold |
|---------|:-------:|:------:|
| Acurácia | **40,0%** | **40,2%** |
| Kappa | 0,350 | 0,352 |
| F1 macro | 0,402 | 0,409 |

<br>

::: {.caixa-laranja}
Holdout e K-Fold **concordam** → resultado **estável**.
:::
:::

::: {.column width="67%"}
```{r}
#| fig-width: 6.8
#| fig-height: 5.2

if (!is.null(cm_plot)) {
  print(cm_plot)
} else {
  ggplot() +
    annotate("text", x = 0.5, y = 0.5, size = 5, color = "grey50",
             label = "Renderize com outputs/parcial/ no lugar certo") +
    theme_void()
}
```
:::

::::


## Validação Cruzada K-Fold (k = 5)

```{r}
#| fig-width: 10
#| fig-height: 4.2

ggplot(resultados_kfold,
       aes(x = reorder(modelo, acuracia), y = acuracia, fill = modelo)) +
  geom_col(show.legend = FALSE, width = 0.7) +
  geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
            hjust = -0.12, size = 4.5, fontface = "bold", color = "grey20") +
  coord_flip(ylim = c(0, 0.52)) +
  scale_fill_manual(values = cores_modelos) +
  labs(x = NULL, y = "Acurácia média (5 folds)") +
  theme_minimal(base_size = 14) +
  theme(panel.grid.major.y = element_blank(),
        panel.grid.minor   = element_blank())
```

> **Por que k-fold?** O holdout avalia uma única divisão aleatória. O k-fold usa **todas as imagens** para avaliação, em 5 rodadas independentes — estimativa mais robusta.


## Holdout × K-Fold

```{r}
#| fig-width: 10
#| fig-height: 5.0

bind_rows(
  resultados_holdout %>% mutate(estrategia = "Holdout 70/30"),
  resultados_kfold   %>% mutate(estrategia = "K-Fold (k=5)")
) %>%
  ggplot(aes(x = reorder(modelo, acuracia),
             y = acuracia, fill = estrategia)) +
  geom_col(position = position_dodge(0.72), width = 0.68) +
  geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
            position = position_dodge(0.72),
            hjust = -0.1, size = 4, color = "grey20") +
  coord_flip(ylim = c(0, 0.55)) +
  scale_fill_manual(values = c("Holdout 70/30" = azul,
                               "K-Fold (k=5)"  = laranja)) +
  labs(x = NULL, y = "Acurácia", fill = NULL) +
  theme_minimal(base_size = 14) +
  theme(legend.position    = "bottom",
        panel.grid.major.y = element_blank(),
        panel.grid.minor   = element_blank())
```


## Importância das Variáveis — Random Forest

```{r}
#| fig-width: 10
#| fig-height: 5.0

top10 <- importancia_df %>% slice_head(n = 10)

ggplot(top10,
       aes(x = reorder(variavel, importancia), y = importancia)) +
  geom_col(fill = azul) +
  geom_col(
    data = top10 %>% slice_head(n = 2),
    fill = laranja
  ) +
  geom_text(aes(label = round(importancia, 2)),
            hjust = -0.12, size = 4, color = "grey20") +
  coord_flip(ylim = c(0, max(top10$importancia) * 1.18)) +
  labs(x = "Atributo (neurônio MobileNetV2)",
       y = "Importância (MeanDecreaseGini)",
       subtitle = "Laranja = 2 atributos mais discriminativos") +
  theme_minimal(base_size = 13) +
  theme(panel.grid.major.y = element_blank(),
        panel.grid.minor   = element_blank())
```

::: {.fragment .caixa-azul}
`feat_0472` e `feat_0260` têm importância ~70% maior que a 3ª colocada. São índices de neurônios internos da CNN — sem interpretação visual direta.
:::


## Discussão

```{r}
#| include: false

melhor_modelo_kf <- resultados_kfold$modelo[which.max(resultados_kfold$acuracia)]
melhor_acc_kf    <- max(resultados_kfold$acuracia)
```

**Resultado principal**

- Melhor modelo: **`r melhor_modelo_kf`** com acurácia k-fold de **`r percent(melhor_acc_kf, accuracy = 0.1)`** (~5,2× acima do acaso)
- Holdout e k-fold convergiram → resultado **estável e confiável**

::: {.caixa-laranja}
**Achado mais relevante —** **SVM linear** superou o SVM radial com parâmetros fixos (14%). Os atributos da MobileNetV2 formam um espaço *quase linearmente separável* — o kernel linear é suficiente.
:::

**Trabalhos futuros**

- Outros extratores CNN: EfficientNetB0 · ResNet50 · VGG19
- Incorporar o canal **TIR** ao vetor de atributos
- Aumentar a amostra por classe (50 imagens/classe) e fazer fine-tuning da CNN


## Conclusão {.center}

<br>

> Transfer learning combina o poder representacional das CNNs com a interpretabilidade dos classificadores clássicos — **sem treinar a rede do zero**.

<br>

::: {.caixa-azul}
A abordagem é simples, reprodutível e já supera em 5× o baseline aleatório com apenas 50 imagens por classe — base sólida para extensões futuras.
:::


## {.center}

<br><br>

### Obrigado — Perguntas?

<br>

**1T Generoso · 1T João Marcos · 1T Vitor Cesa**

*CEAO-802 — Métodos de Análise de Dados*
