Reorganiza estrutura do projeto em trabalho/ e artigo/
Move arquivos de analise, apresentacao e outputs para subpastas dedicadas; remove caches orfaos e tex nao usados. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BIN
trabalho/amostra/apresentacao/SVM GUARA - 802.pptx
Normal file
BIN
trabalho/amostra/apresentacao/SVM GUARA 802.pdf
Normal file
BIN
trabalho/amostra/apresentacao/apresentacao_svm_rgb.pptx
Normal file
588
trabalho/amostra/apresentacao/apresentacao_svm_rgb.qmd
Normal file
@@ -0,0 +1,588 @@
|
||||
---
|
||||
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*
|
||||
651
trabalho/amostra/outputs_parcial/features_rgb.csv
Normal file
1266
trabalho/amostra/outputs_parcial/importancia_variaveis_rgb.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||
SVM linear,0.40153846153846157,0.3516666666666666,0.40153846153846157,0.9501282051282052,0.40929682290313046
|
||||
SVM radial ajustado,0.38,0.3283333333333333,0.38,0.9483333333333333,0.3905066510222767
|
||||
PCA + SVM radial,0.3676923076923077,0.31499999999999995,0.3676923076923077,0.9473076923076923,0.37836144953604534
|
||||
Random Forest,0.35692307692307695,0.3033333333333333,0.35692307692307695,0.9464102564102563,0.38061565488169397
|
||||
SVM radial,0.15846153846153846,0.08833333333333333,0.15846153846153846,0.9298717948717948,0.22329423850231678
|
||||
|
@@ -0,0 +1,6 @@
|
||||
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||
SVM linear,0.4,0.35,0.4,0.95,0.401945495575665
|
||||
Random Forest,0.37435897435897436,0.3222222222222222,0.37435897435897436,0.9478632478632478,0.3688098868848946
|
||||
PCA + SVM radial,0.358974358974359,0.3055555555555555,0.358974358974359,0.9465811965811965,0.36964765748713724
|
||||
SVM radial ajustado,0.35384615384615387,0.3,0.35384615384615387,0.9461538461538461,0.35196493343650537
|
||||
SVM radial,0.14358974358974358,0.0722222222222222,0.14358974358974358,0.9286324786324787,0.192826555606469
|
||||
|
1035
trabalho/amostra/trabalho_svm_rgb.qmd
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 14 KiB |
2106
trabalho/amostra/trabalho_svm_rgb_files/libs/bootstrap/bootstrap-icons.css
vendored
Normal file
7
trabalho/amostra/trabalho_svm_rgb_files/libs/bootstrap/bootstrap.min.js
vendored
Normal file
7
trabalho/amostra/trabalho_svm_rgb_files/libs/clipboard/clipboard.min.js
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
.pagedtable {
|
||||
overflow: auto;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.pagedtable-wrapper {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pagedtable table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagedtable th {
|
||||
padding: 0 5px 0 5px;
|
||||
border: none;
|
||||
border-bottom: 2px solid #dddddd;
|
||||
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.pagedtable-empty th {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedtable td {
|
||||
padding: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
.pagedtable .even {
|
||||
background-color: rgba(140, 140, 140, 0.1);
|
||||
}
|
||||
|
||||
.pagedtable-padding-col {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedtable a {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pagedtable-index-nav {
|
||||
cursor: pointer;
|
||||
padding: 0 5px 0 5px;
|
||||
float: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pagedtable-index-nav-disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
a.pagedtable-index-nav-disabled:hover {
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.pagedtable-indexes {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pagedtable-index-current {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a.pagedtable-index-current:hover {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagedtable-index {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pagedtable-index-separator-left {
|
||||
display: inline-block;
|
||||
color: #333;
|
||||
font-size: 9px;
|
||||
padding: 0 0 0 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pagedtable-index-separator-right {
|
||||
display: inline-block;
|
||||
color: #333;
|
||||
font-size: 9px;
|
||||
padding: 0 4px 0 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pagedtable-footer {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.pagedtable-not-empty .pagedtable-footer {
|
||||
border-top: 2px solid #dddddd;
|
||||
}
|
||||
|
||||
.pagedtable-info {
|
||||
overflow: hidden;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pagedtable-header-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pagedtable-header-type {
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pagedtable-na-cell {
|
||||
font-style: italic;
|
||||
opacity: 0.3;
|
||||
}
|
||||
9
trabalho/amostra/trabalho_svm_rgb_files/libs/quarto-html/anchor.min.js
vendored
Normal file
6
trabalho/amostra/trabalho_svm_rgb_files/libs/quarto-html/popper.min.js
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
/* quarto syntax highlight colors */
|
||||
:root {
|
||||
--quarto-hl-ot-color: #003B4F;
|
||||
--quarto-hl-at-color: #657422;
|
||||
--quarto-hl-ss-color: #20794D;
|
||||
--quarto-hl-an-color: #5E5E5E;
|
||||
--quarto-hl-fu-color: #4758AB;
|
||||
--quarto-hl-st-color: #20794D;
|
||||
--quarto-hl-cf-color: #003B4F;
|
||||
--quarto-hl-op-color: #5E5E5E;
|
||||
--quarto-hl-er-color: #AD0000;
|
||||
--quarto-hl-bn-color: #AD0000;
|
||||
--quarto-hl-al-color: #AD0000;
|
||||
--quarto-hl-va-color: #111111;
|
||||
--quarto-hl-bu-color: inherit;
|
||||
--quarto-hl-ex-color: inherit;
|
||||
--quarto-hl-pp-color: #AD0000;
|
||||
--quarto-hl-in-color: #5E5E5E;
|
||||
--quarto-hl-vs-color: #20794D;
|
||||
--quarto-hl-wa-color: #5E5E5E;
|
||||
--quarto-hl-do-color: #5E5E5E;
|
||||
--quarto-hl-im-color: #00769E;
|
||||
--quarto-hl-ch-color: #20794D;
|
||||
--quarto-hl-dt-color: #AD0000;
|
||||
--quarto-hl-fl-color: #AD0000;
|
||||
--quarto-hl-co-color: #5E5E5E;
|
||||
--quarto-hl-cv-color: #5E5E5E;
|
||||
--quarto-hl-cn-color: #8f5902;
|
||||
--quarto-hl-sc-color: #5E5E5E;
|
||||
--quarto-hl-dv-color: #AD0000;
|
||||
--quarto-hl-kw-color: #003B4F;
|
||||
}
|
||||
|
||||
/* other quarto variables */
|
||||
:root {
|
||||
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* syntax highlight based on Pandoc's rules */
|
||||
pre > code.sourceCode > span {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
code.sourceCode > span {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
div.sourceCode,
|
||||
div.sourceCode pre.sourceCode {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
/* Normal */
|
||||
code span {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
/* Alert */
|
||||
code span.al {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Annotation */
|
||||
code span.an {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Attribute */
|
||||
code span.at {
|
||||
color: #657422;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* BaseN */
|
||||
code span.bn {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* BuiltIn */
|
||||
code span.bu {
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* ControlFlow */
|
||||
code span.cf {
|
||||
color: #003B4F;
|
||||
font-weight: bold;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Char */
|
||||
code span.ch {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Constant */
|
||||
code span.cn {
|
||||
color: #8f5902;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
code span.co {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* CommentVar */
|
||||
code span.cv {
|
||||
color: #5E5E5E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Documentation */
|
||||
code span.do {
|
||||
color: #5E5E5E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* DataType */
|
||||
code span.dt {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* DecVal */
|
||||
code span.dv {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Error */
|
||||
code span.er {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Extension */
|
||||
code span.ex {
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Float */
|
||||
code span.fl {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Function */
|
||||
code span.fu {
|
||||
color: #4758AB;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Import */
|
||||
code span.im {
|
||||
color: #00769E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Information */
|
||||
code span.in {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Keyword */
|
||||
code span.kw {
|
||||
color: #003B4F;
|
||||
font-weight: bold;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Operator */
|
||||
code span.op {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Other */
|
||||
code span.ot {
|
||||
color: #003B4F;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Preprocessor */
|
||||
code span.pp {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* SpecialChar */
|
||||
code span.sc {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* SpecialString */
|
||||
code span.ss {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* String */
|
||||
code span.st {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Variable */
|
||||
code span.va {
|
||||
color: #111111;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* VerbatimString */
|
||||
code span.vs {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Warning */
|
||||
code span.wa {
|
||||
color: #5E5E5E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prevent-inlining {
|
||||
content: "</";
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */
|
||||
@@ -0,0 +1,845 @@
|
||||
import * as tabsets from "./tabsets/tabsets.js";
|
||||
|
||||
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
|
||||
detail: {},
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
composed: false,
|
||||
});
|
||||
|
||||
const layoutMarginEls = () => {
|
||||
// Find any conflicting margin elements and add margins to the
|
||||
// top to prevent overlap
|
||||
const marginChildren = window.document.querySelectorAll(
|
||||
".column-margin.column-container > *, .margin-caption, .aside"
|
||||
);
|
||||
|
||||
let lastBottom = 0;
|
||||
for (const marginChild of marginChildren) {
|
||||
if (marginChild.offsetParent !== null) {
|
||||
// clear the top margin so we recompute it
|
||||
marginChild.style.marginTop = null;
|
||||
const top = marginChild.getBoundingClientRect().top + window.scrollY;
|
||||
if (top < lastBottom) {
|
||||
const marginChildStyle = window.getComputedStyle(marginChild);
|
||||
const marginBottom = parseFloat(marginChildStyle["marginBottom"]);
|
||||
const margin = lastBottom - top + marginBottom;
|
||||
marginChild.style.marginTop = `${margin}px`;
|
||||
}
|
||||
const styles = window.getComputedStyle(marginChild);
|
||||
const marginTop = parseFloat(styles["marginTop"]);
|
||||
lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.document.addEventListener("DOMContentLoaded", function (_event) {
|
||||
// Recompute the position of margin elements anytime the body size changes
|
||||
if (window.ResizeObserver) {
|
||||
const resizeObserver = new window.ResizeObserver(
|
||||
throttle(() => {
|
||||
layoutMarginEls();
|
||||
if (
|
||||
window.document.body.getBoundingClientRect().width < 990 &&
|
||||
isReaderMode()
|
||||
) {
|
||||
quartoToggleReader();
|
||||
}
|
||||
}, 50)
|
||||
);
|
||||
resizeObserver.observe(window.document.body);
|
||||
}
|
||||
|
||||
const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');
|
||||
const sidebarEl = window.document.getElementById("quarto-sidebar");
|
||||
const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");
|
||||
const marginSidebarEl = window.document.getElementById(
|
||||
"quarto-margin-sidebar"
|
||||
);
|
||||
// function to determine whether the element has a previous sibling that is active
|
||||
const prevSiblingIsActiveLink = (el) => {
|
||||
const sibling = el.previousElementSibling;
|
||||
if (sibling && sibling.tagName === "A") {
|
||||
return sibling.classList.contains("active");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// dispatch for htmlwidgets
|
||||
// they use slideenter event to trigger resize
|
||||
function fireSlideEnter() {
|
||||
const event = window.document.createEvent("Event");
|
||||
event.initEvent("slideenter", true, true);
|
||||
window.document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("shown.bs.tab", fireSlideEnter);
|
||||
});
|
||||
|
||||
// dispatch for shiny
|
||||
// they use BS shown and hidden events to trigger rendering
|
||||
function distpatchShinyEvents(previous, current) {
|
||||
if (window.jQuery) {
|
||||
if (previous) {
|
||||
window.jQuery(previous).trigger("hidden");
|
||||
}
|
||||
if (current) {
|
||||
window.jQuery(current).trigger("shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tabby.js listener: Trigger event for htmlwidget and shiny
|
||||
document.addEventListener(
|
||||
"tabby",
|
||||
function (event) {
|
||||
fireSlideEnter();
|
||||
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Track scrolling and mark TOC links as active
|
||||
// get table of contents and sidebar (bail if we don't have at least one)
|
||||
const tocLinks = tocEl
|
||||
? [...tocEl.querySelectorAll("a[data-scroll-target]")]
|
||||
: [];
|
||||
const makeActive = (link) => tocLinks[link].classList.add("active");
|
||||
const removeActive = (link) => tocLinks[link].classList.remove("active");
|
||||
const removeAllActive = () =>
|
||||
[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));
|
||||
|
||||
// activate the anchor for a section associated with this TOC entry
|
||||
tocLinks.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
if (link.href.indexOf("#") !== -1) {
|
||||
const anchor = link.href.split("#")[1];
|
||||
const heading = window.document.querySelector(
|
||||
`[data-anchor-id="${anchor}"]`
|
||||
);
|
||||
if (heading) {
|
||||
// Add the class
|
||||
heading.classList.add("reveal-anchorjs-link");
|
||||
|
||||
// function to show the anchor
|
||||
const handleMouseout = () => {
|
||||
heading.classList.remove("reveal-anchorjs-link");
|
||||
heading.removeEventListener("mouseout", handleMouseout);
|
||||
};
|
||||
|
||||
// add a function to clear the anchor when the user mouses out of it
|
||||
heading.addEventListener("mouseout", handleMouseout);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sections = tocLinks.map((link) => {
|
||||
const target = link.getAttribute("data-scroll-target");
|
||||
if (target.startsWith("#")) {
|
||||
return window.document.getElementById(decodeURI(`${target.slice(1)}`));
|
||||
} else {
|
||||
return window.document.querySelector(decodeURI(`${target}`));
|
||||
}
|
||||
});
|
||||
|
||||
const sectionMargin = 200;
|
||||
let currentActive = 0;
|
||||
// track whether we've initialized state the first time
|
||||
let init = false;
|
||||
|
||||
const updateActiveLink = () => {
|
||||
// The index from bottom to top (e.g. reversed list)
|
||||
let sectionIndex = -1;
|
||||
if (
|
||||
window.innerHeight + window.pageYOffset >=
|
||||
window.document.body.offsetHeight
|
||||
) {
|
||||
// This is the no-scroll case where last section should be the active one
|
||||
sectionIndex = 0;
|
||||
} else {
|
||||
// This finds the last section visible on screen that should be made active
|
||||
sectionIndex = [...sections].reverse().findIndex((section) => {
|
||||
if (section) {
|
||||
return window.pageYOffset >= section.offsetTop - sectionMargin;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sectionIndex > -1) {
|
||||
const current = sections.length - sectionIndex - 1;
|
||||
if (current !== currentActive) {
|
||||
removeAllActive();
|
||||
currentActive = current;
|
||||
makeActive(current);
|
||||
if (init) {
|
||||
window.dispatchEvent(sectionChanged);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inHiddenRegion = (top, bottom, hiddenRegions) => {
|
||||
for (const region of hiddenRegions) {
|
||||
if (top <= region.bottom && bottom >= region.top) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const categorySelector = "header.quarto-title-block .quarto-category";
|
||||
const activateCategories = (href) => {
|
||||
// Find any categories
|
||||
// Surround them with a link pointing back to:
|
||||
// #category=Authoring
|
||||
try {
|
||||
const categoryEls = window.document.querySelectorAll(categorySelector);
|
||||
for (const categoryEl of categoryEls) {
|
||||
const categoryText = categoryEl.textContent;
|
||||
if (categoryText) {
|
||||
const link = `${href}#category=${encodeURIComponent(categoryText)}`;
|
||||
const linkEl = window.document.createElement("a");
|
||||
linkEl.setAttribute("href", link);
|
||||
for (const child of categoryEl.childNodes) {
|
||||
linkEl.append(child);
|
||||
}
|
||||
categoryEl.appendChild(linkEl);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
};
|
||||
function hasTitleCategories() {
|
||||
return window.document.querySelector(categorySelector) !== null;
|
||||
}
|
||||
|
||||
function offsetRelativeUrl(url) {
|
||||
const offset = getMeta("quarto:offset");
|
||||
return offset ? offset + url : url;
|
||||
}
|
||||
|
||||
function offsetAbsoluteUrl(url) {
|
||||
const offset = getMeta("quarto:offset");
|
||||
const baseUrl = new URL(offset, window.location);
|
||||
|
||||
const projRelativeUrl = url.replace(baseUrl, "");
|
||||
if (projRelativeUrl.startsWith("/")) {
|
||||
return projRelativeUrl;
|
||||
} else {
|
||||
return "/" + projRelativeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// read a meta tag value
|
||||
function getMeta(metaName) {
|
||||
const metas = window.document.getElementsByTagName("meta");
|
||||
for (let i = 0; i < metas.length; i++) {
|
||||
if (metas[i].getAttribute("name") === metaName) {
|
||||
return metas[i].getAttribute("content");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function findAndActivateCategories() {
|
||||
// Categories search with listing only use path without query
|
||||
const currentPagePath = offsetAbsoluteUrl(
|
||||
window.location.origin + window.location.pathname
|
||||
);
|
||||
const response = await fetch(offsetRelativeUrl("listings.json"));
|
||||
if (response.status == 200) {
|
||||
return response.json().then(function (listingPaths) {
|
||||
const listingHrefs = [];
|
||||
for (const listingPath of listingPaths) {
|
||||
const pathWithoutLeadingSlash = listingPath.listing.substring(1);
|
||||
for (const item of listingPath.items) {
|
||||
const encodedItem = encodeURI(item);
|
||||
if (
|
||||
encodedItem === currentPagePath ||
|
||||
encodedItem === currentPagePath + "index.html"
|
||||
) {
|
||||
// Resolve this path against the offset to be sure
|
||||
// we already are using the correct path to the listing
|
||||
// (this adjusts the listing urls to be rooted against
|
||||
// whatever root the page is actually running against)
|
||||
const relative = offsetRelativeUrl(pathWithoutLeadingSlash);
|
||||
const baseUrl = window.location;
|
||||
const resolvedPath = new URL(relative, baseUrl);
|
||||
listingHrefs.push(resolvedPath.pathname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look up the tree for a nearby linting and use that if we find one
|
||||
const nearestListing = findNearestParentListing(
|
||||
offsetAbsoluteUrl(window.location.pathname),
|
||||
listingHrefs
|
||||
);
|
||||
if (nearestListing) {
|
||||
activateCategories(nearestListing);
|
||||
} else {
|
||||
// See if the referrer is a listing page for this item
|
||||
const referredRelativePath = offsetAbsoluteUrl(document.referrer);
|
||||
const referrerListing = listingHrefs.find((listingHref) => {
|
||||
const isListingReferrer =
|
||||
listingHref === referredRelativePath ||
|
||||
listingHref === referredRelativePath + "index.html";
|
||||
return isListingReferrer;
|
||||
});
|
||||
|
||||
if (referrerListing) {
|
||||
// Try to use the referrer if possible
|
||||
activateCategories(referrerListing);
|
||||
} else if (listingHrefs.length > 0) {
|
||||
// Otherwise, just fall back to the first listing
|
||||
activateCategories(listingHrefs[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hasTitleCategories()) {
|
||||
findAndActivateCategories();
|
||||
}
|
||||
|
||||
const findNearestParentListing = (href, listingHrefs) => {
|
||||
if (!href || !listingHrefs) {
|
||||
return undefined;
|
||||
}
|
||||
// Look up the tree for a nearby linting and use that if we find one
|
||||
const relativeParts = href.substring(1).split("/");
|
||||
while (relativeParts.length > 0) {
|
||||
const path = relativeParts.join("/");
|
||||
for (const listingHref of listingHrefs) {
|
||||
if (listingHref.startsWith(path)) {
|
||||
return listingHref;
|
||||
}
|
||||
}
|
||||
relativeParts.pop();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const manageSidebarVisiblity = (el, placeholderDescriptor) => {
|
||||
let isVisible = true;
|
||||
let elRect;
|
||||
|
||||
return (hiddenRegions) => {
|
||||
if (el === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the last element of the TOC
|
||||
const lastChildEl = el.lastElementChild;
|
||||
|
||||
if (lastChildEl) {
|
||||
// Converts the sidebar to a menu
|
||||
const convertToMenu = () => {
|
||||
for (const child of el.children) {
|
||||
child.style.opacity = 0;
|
||||
child.style.overflow = "hidden";
|
||||
child.style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
nexttick(() => {
|
||||
const toggleContainer = window.document.createElement("div");
|
||||
toggleContainer.style.width = "100%";
|
||||
toggleContainer.classList.add("zindex-over-content");
|
||||
toggleContainer.classList.add("quarto-sidebar-toggle");
|
||||
toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom
|
||||
toggleContainer.id = placeholderDescriptor.id;
|
||||
toggleContainer.style.position = "fixed";
|
||||
|
||||
const toggleIcon = window.document.createElement("i");
|
||||
toggleIcon.classList.add("quarto-sidebar-toggle-icon");
|
||||
toggleIcon.classList.add("bi");
|
||||
toggleIcon.classList.add("bi-caret-down-fill");
|
||||
|
||||
const toggleTitle = window.document.createElement("div");
|
||||
const titleEl = window.document.body.querySelector(
|
||||
placeholderDescriptor.titleSelector
|
||||
);
|
||||
if (titleEl) {
|
||||
toggleTitle.append(
|
||||
titleEl.textContent || titleEl.innerText,
|
||||
toggleIcon
|
||||
);
|
||||
}
|
||||
toggleTitle.classList.add("zindex-over-content");
|
||||
toggleTitle.classList.add("quarto-sidebar-toggle-title");
|
||||
toggleContainer.append(toggleTitle);
|
||||
|
||||
const toggleContents = window.document.createElement("div");
|
||||
toggleContents.classList = el.classList;
|
||||
toggleContents.classList.add("zindex-over-content");
|
||||
toggleContents.classList.add("quarto-sidebar-toggle-contents");
|
||||
for (const child of el.children) {
|
||||
if (child.id === "toc-title") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clone = child.cloneNode(true);
|
||||
clone.style.opacity = 1;
|
||||
clone.style.pointerEvents = null;
|
||||
clone.style.display = null;
|
||||
toggleContents.append(clone);
|
||||
}
|
||||
toggleContents.style.height = "0px";
|
||||
const positionToggle = () => {
|
||||
// position the element (top left of parent, same width as parent)
|
||||
if (!elRect) {
|
||||
elRect = el.getBoundingClientRect();
|
||||
}
|
||||
toggleContainer.style.left = `${elRect.left}px`;
|
||||
toggleContainer.style.top = `${elRect.top}px`;
|
||||
toggleContainer.style.width = `${elRect.width}px`;
|
||||
};
|
||||
positionToggle();
|
||||
|
||||
toggleContainer.append(toggleContents);
|
||||
el.parentElement.prepend(toggleContainer);
|
||||
|
||||
// Process clicks
|
||||
let tocShowing = false;
|
||||
// Allow the caller to control whether this is dismissed
|
||||
// when it is clicked (e.g. sidebar navigation supports
|
||||
// opening and closing the nav tree, so don't dismiss on click)
|
||||
const clickEl = placeholderDescriptor.dismissOnClick
|
||||
? toggleContainer
|
||||
: toggleTitle;
|
||||
|
||||
const closeToggle = () => {
|
||||
if (tocShowing) {
|
||||
toggleContainer.classList.remove("expanded");
|
||||
toggleContents.style.height = "0px";
|
||||
tocShowing = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get rid of any expanded toggle if the user scrolls
|
||||
window.document.addEventListener(
|
||||
"scroll",
|
||||
throttle(() => {
|
||||
closeToggle();
|
||||
}, 50)
|
||||
);
|
||||
|
||||
// Handle positioning of the toggle
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
throttle(() => {
|
||||
elRect = undefined;
|
||||
positionToggle();
|
||||
}, 50)
|
||||
);
|
||||
|
||||
window.addEventListener("quarto-hrChanged", () => {
|
||||
elRect = undefined;
|
||||
});
|
||||
|
||||
// Process the click
|
||||
clickEl.onclick = () => {
|
||||
if (!tocShowing) {
|
||||
toggleContainer.classList.add("expanded");
|
||||
toggleContents.style.height = null;
|
||||
tocShowing = true;
|
||||
} else {
|
||||
closeToggle();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Converts a sidebar from a menu back to a sidebar
|
||||
const convertToSidebar = () => {
|
||||
for (const child of el.children) {
|
||||
child.style.opacity = 1;
|
||||
child.style.overflow = null;
|
||||
child.style.pointerEvents = null;
|
||||
}
|
||||
|
||||
const placeholderEl = window.document.getElementById(
|
||||
placeholderDescriptor.id
|
||||
);
|
||||
if (placeholderEl) {
|
||||
placeholderEl.remove();
|
||||
}
|
||||
|
||||
el.classList.remove("rollup");
|
||||
};
|
||||
|
||||
if (isReaderMode()) {
|
||||
convertToMenu();
|
||||
isVisible = false;
|
||||
} else {
|
||||
// Find the top and bottom o the element that is being managed
|
||||
const elTop = el.offsetTop;
|
||||
const elBottom =
|
||||
elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;
|
||||
|
||||
if (!isVisible) {
|
||||
// If the element is current not visible reveal if there are
|
||||
// no conflicts with overlay regions
|
||||
if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {
|
||||
convertToSidebar();
|
||||
isVisible = true;
|
||||
}
|
||||
} else {
|
||||
// If the element is visible, hide it if it conflicts with overlay regions
|
||||
// and insert a placeholder toggle (or if we're in reader mode)
|
||||
if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {
|
||||
convertToMenu();
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||
for (const tabEl of tabEls) {
|
||||
const id = tabEl.getAttribute("data-bs-target");
|
||||
if (id) {
|
||||
const columnEl = document.querySelector(
|
||||
`${id} .column-margin, .tabset-margin-content`
|
||||
);
|
||||
if (columnEl)
|
||||
tabEl.addEventListener("shown.bs.tab", function (event) {
|
||||
const el = event.srcElement;
|
||||
if (el) {
|
||||
const visibleCls = `${el.id}-margin-content`;
|
||||
// walk up until we find a parent tabset
|
||||
let panelTabsetEl = el.parentElement;
|
||||
while (panelTabsetEl) {
|
||||
if (panelTabsetEl.classList.contains("panel-tabset")) {
|
||||
break;
|
||||
}
|
||||
panelTabsetEl = panelTabsetEl.parentElement;
|
||||
}
|
||||
|
||||
if (panelTabsetEl) {
|
||||
const prevSib = panelTabsetEl.previousElementSibling;
|
||||
if (
|
||||
prevSib &&
|
||||
prevSib.classList.contains("tabset-margin-container")
|
||||
) {
|
||||
const childNodes = prevSib.querySelectorAll(
|
||||
".tabset-margin-content"
|
||||
);
|
||||
for (const childEl of childNodes) {
|
||||
if (childEl.classList.contains(visibleCls)) {
|
||||
childEl.classList.remove("collapse");
|
||||
} else {
|
||||
childEl.classList.add("collapse");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layoutMarginEls();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Manage the visibility of the toc and the sidebar
|
||||
const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {
|
||||
id: "quarto-toc-toggle",
|
||||
titleSelector: "#toc-title",
|
||||
dismissOnClick: true,
|
||||
});
|
||||
const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {
|
||||
id: "quarto-sidebarnav-toggle",
|
||||
titleSelector: ".title",
|
||||
dismissOnClick: false,
|
||||
});
|
||||
let tocLeftScrollVisibility;
|
||||
if (leftTocEl) {
|
||||
tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {
|
||||
id: "quarto-lefttoc-toggle",
|
||||
titleSelector: "#toc-title",
|
||||
dismissOnClick: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Find the first element that uses formatting in special columns
|
||||
const conflictingEls = window.document.body.querySelectorAll(
|
||||
'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'
|
||||
);
|
||||
|
||||
// Filter all the possibly conflicting elements into ones
|
||||
// the do conflict on the left or ride side
|
||||
const arrConflictingEls = Array.from(conflictingEls);
|
||||
const leftSideConflictEls = arrConflictingEls.filter((el) => {
|
||||
if (el.tagName === "ASIDE") {
|
||||
return false;
|
||||
}
|
||||
return Array.from(el.classList).find((className) => {
|
||||
return (
|
||||
className !== "column-body" &&
|
||||
className.startsWith("column-") &&
|
||||
!className.endsWith("right") &&
|
||||
!className.endsWith("container") &&
|
||||
className !== "column-margin"
|
||||
);
|
||||
});
|
||||
});
|
||||
const rightSideConflictEls = arrConflictingEls.filter((el) => {
|
||||
if (el.tagName === "ASIDE") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasMarginCaption = Array.from(el.classList).find((className) => {
|
||||
return className == "margin-caption";
|
||||
});
|
||||
if (hasMarginCaption) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Array.from(el.classList).find((className) => {
|
||||
return (
|
||||
className !== "column-body" &&
|
||||
!className.endsWith("container") &&
|
||||
className.startsWith("column-") &&
|
||||
!className.endsWith("left")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const kOverlapPaddingSize = 10;
|
||||
function toRegions(els) {
|
||||
return els.map((el) => {
|
||||
const boundRect = el.getBoundingClientRect();
|
||||
const top =
|
||||
boundRect.top +
|
||||
document.documentElement.scrollTop -
|
||||
kOverlapPaddingSize;
|
||||
return {
|
||||
top,
|
||||
bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let hasObserved = false;
|
||||
const visibleItemObserver = (els) => {
|
||||
let visibleElements = [...els];
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries, _observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (visibleElements.indexOf(entry.target) === -1) {
|
||||
visibleElements.push(entry.target);
|
||||
}
|
||||
} else {
|
||||
visibleElements = visibleElements.filter((visibleEntry) => {
|
||||
return visibleEntry !== entry;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasObserved) {
|
||||
hideOverlappedSidebars();
|
||||
}
|
||||
hasObserved = true;
|
||||
},
|
||||
{}
|
||||
);
|
||||
els.forEach((el) => {
|
||||
intersectionObserver.observe(el);
|
||||
});
|
||||
|
||||
return {
|
||||
getVisibleEntries: () => {
|
||||
return visibleElements;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const rightElementObserver = visibleItemObserver(rightSideConflictEls);
|
||||
const leftElementObserver = visibleItemObserver(leftSideConflictEls);
|
||||
|
||||
const hideOverlappedSidebars = () => {
|
||||
marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));
|
||||
sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));
|
||||
if (tocLeftScrollVisibility) {
|
||||
tocLeftScrollVisibility(
|
||||
toRegions(leftElementObserver.getVisibleEntries())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.quartoToggleReader = () => {
|
||||
// Applies a slow class (or removes it)
|
||||
// to update the transition speed
|
||||
const slowTransition = (slow) => {
|
||||
const manageTransition = (id, slow) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
if (slow) {
|
||||
el.classList.add("slow");
|
||||
} else {
|
||||
el.classList.remove("slow");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
manageTransition("TOC", slow);
|
||||
manageTransition("quarto-sidebar", slow);
|
||||
};
|
||||
const readerMode = !isReaderMode();
|
||||
setReaderModeValue(readerMode);
|
||||
|
||||
// If we're entering reader mode, slow the transition
|
||||
if (readerMode) {
|
||||
slowTransition(readerMode);
|
||||
}
|
||||
highlightReaderToggle(readerMode);
|
||||
hideOverlappedSidebars();
|
||||
|
||||
// If we're exiting reader mode, restore the non-slow transition
|
||||
if (!readerMode) {
|
||||
slowTransition(!readerMode);
|
||||
}
|
||||
};
|
||||
|
||||
const highlightReaderToggle = (readerMode) => {
|
||||
const els = document.querySelectorAll(".quarto-reader-toggle");
|
||||
if (els) {
|
||||
els.forEach((el) => {
|
||||
if (readerMode) {
|
||||
el.classList.add("reader");
|
||||
} else {
|
||||
el.classList.remove("reader");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setReaderModeValue = (val) => {
|
||||
if (window.location.protocol !== "file:") {
|
||||
window.localStorage.setItem("quarto-reader-mode", val);
|
||||
} else {
|
||||
localReaderMode = val;
|
||||
}
|
||||
};
|
||||
|
||||
const isReaderMode = () => {
|
||||
if (window.location.protocol !== "file:") {
|
||||
return window.localStorage.getItem("quarto-reader-mode") === "true";
|
||||
} else {
|
||||
return localReaderMode;
|
||||
}
|
||||
};
|
||||
let localReaderMode = null;
|
||||
|
||||
const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");
|
||||
const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;
|
||||
|
||||
// Walk the TOC and collapse/expand nodes
|
||||
// Nodes are expanded if:
|
||||
// - they are top level
|
||||
// - they have children that are 'active' links
|
||||
// - they are directly below an link that is 'active'
|
||||
const walk = (el, depth) => {
|
||||
// Tick depth when we enter a UL
|
||||
if (el.tagName === "UL") {
|
||||
depth = depth + 1;
|
||||
}
|
||||
|
||||
// It this is active link
|
||||
let isActiveNode = false;
|
||||
if (el.tagName === "A" && el.classList.contains("active")) {
|
||||
isActiveNode = true;
|
||||
}
|
||||
|
||||
// See if there is an active child to this element
|
||||
let hasActiveChild = false;
|
||||
for (const child of el.children) {
|
||||
hasActiveChild = walk(child, depth) || hasActiveChild;
|
||||
}
|
||||
|
||||
// Process the collapse state if this is an UL
|
||||
if (el.tagName === "UL") {
|
||||
if (tocOpenDepth === -1 && depth > 1) {
|
||||
// toc-expand: false
|
||||
el.classList.add("collapse");
|
||||
} else if (
|
||||
depth <= tocOpenDepth ||
|
||||
hasActiveChild ||
|
||||
prevSiblingIsActiveLink(el)
|
||||
) {
|
||||
el.classList.remove("collapse");
|
||||
} else {
|
||||
el.classList.add("collapse");
|
||||
}
|
||||
|
||||
// untick depth when we leave a UL
|
||||
depth = depth - 1;
|
||||
}
|
||||
return hasActiveChild || isActiveNode;
|
||||
};
|
||||
|
||||
// walk the TOC and expand / collapse any items that should be shown
|
||||
if (tocEl) {
|
||||
updateActiveLink();
|
||||
walk(tocEl, 0);
|
||||
}
|
||||
|
||||
// Throttle the scroll event and walk peridiocally
|
||||
window.document.addEventListener(
|
||||
"scroll",
|
||||
throttle(() => {
|
||||
if (tocEl) {
|
||||
updateActiveLink();
|
||||
walk(tocEl, 0);
|
||||
}
|
||||
if (!isReaderMode()) {
|
||||
hideOverlappedSidebars();
|
||||
}
|
||||
}, 5)
|
||||
);
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
throttle(() => {
|
||||
if (tocEl) {
|
||||
updateActiveLink();
|
||||
walk(tocEl, 0);
|
||||
}
|
||||
if (!isReaderMode()) {
|
||||
hideOverlappedSidebars();
|
||||
}
|
||||
}, 10)
|
||||
);
|
||||
hideOverlappedSidebars();
|
||||
highlightReaderToggle(isReaderMode());
|
||||
});
|
||||
|
||||
tabsets.init();
|
||||
|
||||
function throttle(func, wait) {
|
||||
let waiting = false;
|
||||
return function () {
|
||||
if (!waiting) {
|
||||
func.apply(this, arguments);
|
||||
waiting = true;
|
||||
setTimeout(function () {
|
||||
waiting = false;
|
||||
}, wait);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function nexttick(func) {
|
||||
return setTimeout(func, 0);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// grouped tabsets
|
||||
|
||||
export function init() {
|
||||
window.addEventListener("pageshow", (_event) => {
|
||||
function getTabSettings() {
|
||||
const data = localStorage.getItem("quarto-persistent-tabsets-data");
|
||||
if (!data) {
|
||||
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
|
||||
return {};
|
||||
}
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
function setTabSettings(data) {
|
||||
localStorage.setItem(
|
||||
"quarto-persistent-tabsets-data",
|
||||
JSON.stringify(data)
|
||||
);
|
||||
}
|
||||
|
||||
function setTabState(groupName, groupValue) {
|
||||
const data = getTabSettings();
|
||||
data[groupName] = groupValue;
|
||||
setTabSettings(data);
|
||||
}
|
||||
|
||||
function toggleTab(tab, active) {
|
||||
const tabPanelId = tab.getAttribute("aria-controls");
|
||||
const tabPanel = document.getElementById(tabPanelId);
|
||||
if (active) {
|
||||
tab.classList.add("active");
|
||||
tabPanel.classList.add("active");
|
||||
} else {
|
||||
tab.classList.remove("active");
|
||||
tabPanel.classList.remove("active");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll(selectedGroup, selectorsToSync) {
|
||||
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
|
||||
const active = selectedGroup === thisGroup;
|
||||
for (const tab of tabs) {
|
||||
toggleTab(tab, active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findSelectorsToSyncByLanguage() {
|
||||
const result = {};
|
||||
const tabs = Array.from(
|
||||
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
|
||||
);
|
||||
for (const item of tabs) {
|
||||
const div = item.parentElement.parentElement.parentElement;
|
||||
const group = div.getAttribute("data-group");
|
||||
if (!result[group]) {
|
||||
result[group] = {};
|
||||
}
|
||||
const selectorsToSync = result[group];
|
||||
const value = item.innerHTML;
|
||||
if (!selectorsToSync[value]) {
|
||||
selectorsToSync[value] = [];
|
||||
}
|
||||
selectorsToSync[value].push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setupSelectorSync() {
|
||||
const selectorsToSync = findSelectorsToSyncByLanguage();
|
||||
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
|
||||
Object.entries(tabSetsByValue).forEach(([value, items]) => {
|
||||
items.forEach((item) => {
|
||||
item.addEventListener("click", (_event) => {
|
||||
setTabState(group, value);
|
||||
toggleAll(value, selectorsToSync[group]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return selectorsToSync;
|
||||
}
|
||||
|
||||
const selectorsToSync = setupSelectorSync();
|
||||
for (const [group, selectedName] of Object.entries(getTabSettings())) {
|
||||
const selectors = selectorsToSync[group];
|
||||
// it's possible that stale state gives us empty selections, so we explicitly check here.
|
||||
if (selectors) {
|
||||
toggleAll(selectedName, selectors);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
|
||||
2
trabalho/amostra/trabalho_svm_rgb_files/libs/quarto-html/tippy.umd.min.js
vendored
Normal file
1249
trabalho/completo/outputs_completo/features_rgb_completo.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||
SVM linear,0.36298074633403044,0.30995815445239716,0.3633603238866397,0.9469207558795383,0.3834656395997568
|
||||
SVM radial ajustado,0.35649417578146686,0.3029397087363448,0.35684210526315785,0.9463815622092584,0.3720660941606715
|
||||
Random Forest,0.3284677302142518,0.27258541671650627,0.32878542510121456,0.9440465083621263,0.3405924694392964
|
||||
PCA + SVM radial,0.32364187317626886,0.26735133180485987,0.32388663967611336,0.9436442293130114,0.35932566977715646
|
||||
SVM radial,0.14187017563340923,0.0708961467200726,0.14327935222672064,0.9285272695006503,0.17432376344764106
|
||||
|
@@ -0,0 +1,6 @@
|
||||
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||
SVM linear,0.37362637362637363,0.3214285714285714,0.37362637362637363,0.9478021978021978,0.36460524043005765
|
||||
SVM radial ajustado,0.36538461538461536,0.31249999999999994,0.36538461538461536,0.9471153846153846,0.36302238351838
|
||||
Random Forest,0.34065934065934067,0.2857142857142857,0.34065934065934067,0.945054945054945,0.3405200571756793
|
||||
PCA + SVM radial,0.31043956043956045,0.25297619047619047,0.31043956043956045,0.94253663003663,0.3160780319263731
|
||||
SVM radial,0.14835164835164835,0.07738095238095237,0.14835164835164835,0.9290293040293041,0.16727440711436561
|
||||
|
853
trabalho/completo/trabalho_svm_rgb_completo.qmd
Normal file
@@ -0,0 +1,853 @@
|
||||
---
|
||||
title: "Classificacao de Imagens RGB com SVM (Dataset Completo)"
|
||||
subtitle: "CEAO-802 - Metodos de Analise de Dados"
|
||||
author:
|
||||
- "1T Generoso"
|
||||
- "1T João Marcos"
|
||||
- "1T Vitor Cesa"
|
||||
format:
|
||||
html:
|
||||
toc: true
|
||||
toc-depth: 3
|
||||
number-sections: true
|
||||
theme: cosmo
|
||||
df-print: paged
|
||||
execute:
|
||||
warning: false
|
||||
message: false
|
||||
echo: true
|
||||
---
|
||||
|
||||
# Introducao
|
||||
|
||||
Este trabalho aplica tecnicas de classificacao supervisionada ao dataset RGB disponibilizado na disciplina CEAO-802 - Metodos de Analise de Dados.
|
||||
|
||||
O documento de datasets apresenta um problema de classificacao de cenas urbanas com imagens RGB e TIR coletadas por drone sobre Guaratingueta (SP), com 13 classes de cobertura urbana. Nesta analise sera utilizada apenas a parte RGB do dataset, armazenada localmente em `D:/Vitor/Documents/CEAO/802/HAB/DADOS CEAO 2026/RGB`.
|
||||
|
||||
**Diferenca em relacao ao trabalho original:** esta versao utiliza TODAS as imagens disponiveis por classe (sem limite de 50), para avaliar o impacto do tamanho do dataset no desempenho dos modelos.
|
||||
|
||||
A abordagem adotada segue a sugestao do material da disciplina: em vez de extrair atributos estatisticos simples (media, desvio, histogramas), serao utilizadas **redes neurais convolucionais pre-treinadas** como extratores de atributos (*transfer learning*). A rede MobileNetV2, pre-treinada no ImageNet, e carregada sem a camada de classificacao final. Cada imagem e redimensionada para `224x224` pixels e processada pela rede, que produz um vetor de 1280 atributos representando caracteristicas visuais de alto nivel aprendidas em milhoes de imagens. Esses vetores sao entao usados como entrada para os classificadores SVM e Random Forest.
|
||||
|
||||
Neste relatorio, serao avaliados:
|
||||
|
||||
1. SVM com kernel linear;
|
||||
2. SVM com kernel radial;
|
||||
3. SVM radial com ajuste simples de hiperparametros;
|
||||
4. PCA + SVM radial;
|
||||
5. Random Forest, como modelo de comparacao.
|
||||
|
||||
# Preparacao do ambiente
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 1. Instalacao e carregamento dos pacotes
|
||||
# ============================================================
|
||||
|
||||
pacotes <- c(
|
||||
"tidyverse",
|
||||
"keras3",
|
||||
"caret",
|
||||
"e1071",
|
||||
"randomForest",
|
||||
"knitr"
|
||||
)
|
||||
|
||||
pacotes_faltando <- setdiff(pacotes, rownames(installed.packages()))
|
||||
|
||||
if (length(pacotes_faltando) > 0) {
|
||||
install.packages(pacotes_faltando)
|
||||
}
|
||||
|
||||
library(tidyverse)
|
||||
library(keras3)
|
||||
library(caret)
|
||||
library(e1071)
|
||||
library(randomForest)
|
||||
library(knitr)
|
||||
|
||||
backend_ativo <- tryCatch(config_backend(), error = function(e) NA_character_)
|
||||
cat("Backend keras3:", backend_ativo, "\n")
|
||||
```
|
||||
|
||||
# Caminho do dataset
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 2. Caminho do dataset RGB
|
||||
# ============================================================
|
||||
|
||||
caminho_rgb <- "../RGB"
|
||||
|
||||
if (!dir.exists(caminho_rgb)) {
|
||||
stop("A pasta do dataset RGB nao foi encontrada. Verifique o caminho informado em caminho_rgb.")
|
||||
}
|
||||
|
||||
caminho_rgb
|
||||
```
|
||||
|
||||
# Leitura dos arquivos de imagem
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 3. Listar imagens
|
||||
# ============================================================
|
||||
|
||||
extensoes_imagem <- "\\.(jpg|jpeg|png|bmp|tif|tiff)$"
|
||||
|
||||
arquivos <- list.files(
|
||||
path = caminho_rgb,
|
||||
pattern = extensoes_imagem,
|
||||
recursive = TRUE,
|
||||
full.names = TRUE,
|
||||
ignore.case = TRUE
|
||||
)
|
||||
|
||||
if (length(arquivos) == 0) {
|
||||
stop("Nenhuma imagem foi encontrada na pasta RGB.")
|
||||
}
|
||||
|
||||
length(arquivos)
|
||||
```
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 4. Criar metadados
|
||||
# ============================================================
|
||||
|
||||
obter_classe <- function(arquivo, raiz) {
|
||||
arquivo_norm <- normalizePath(arquivo, winslash = "/", mustWork = FALSE)
|
||||
raiz_norm <- normalizePath(raiz, winslash = "/", mustWork = FALSE)
|
||||
|
||||
nome_sem_extensao <- tools::file_path_sans_ext(basename(arquivo))
|
||||
classe_inferida <- stringr::str_extract(nome_sem_extensao, "^C\\d+")
|
||||
|
||||
if (!is.na(classe_inferida)) {
|
||||
return(classe_inferida)
|
||||
}
|
||||
|
||||
caminho_relativo <- stringr::str_remove(
|
||||
arquivo_norm,
|
||||
paste0("^", stringr::fixed(raiz_norm), "/?")
|
||||
)
|
||||
|
||||
partes <- strsplit(caminho_relativo, "/", fixed = TRUE)[[1]]
|
||||
|
||||
if (length(partes) >= 2) {
|
||||
return(partes[1])
|
||||
}
|
||||
|
||||
stringr::str_extract(nome_sem_extensao, "^[^_-]+")
|
||||
}
|
||||
|
||||
metadados_completo <- tibble(
|
||||
arquivo = arquivos,
|
||||
classe = map_chr(arquivos, obter_classe, raiz = caminho_rgb)
|
||||
) %>%
|
||||
mutate(
|
||||
classe = factor(classe, levels = paste0("C", 1:13)),
|
||||
tamanho_mb = file.info(arquivo)$size / 1024^2
|
||||
) %>%
|
||||
arrange(classe, arquivo)
|
||||
|
||||
resumo_dataset <- metadados_completo %>%
|
||||
group_by(classe) %>%
|
||||
summarise(
|
||||
n_imagens = n(),
|
||||
tamanho_total_mb = sum(tamanho_mb, na.rm = TRUE),
|
||||
tamanho_medio_mb = mean(tamanho_mb, na.rm = TRUE),
|
||||
.groups = "drop"
|
||||
)
|
||||
|
||||
resumo_dataset %>%
|
||||
mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
|
||||
kable()
|
||||
```
|
||||
|
||||
# Amostra completa (sem limite por classe)
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 5. Usar TODAS as imagens disponíveis por classe
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
# Alteracao em relacao ao original: max_imagens_por_classe = 9999
|
||||
# para usar todas as imagens disponiveis
|
||||
max_imagens_por_classe <- 9999
|
||||
|
||||
metadados <- metadados_completo %>%
|
||||
group_by(classe) %>%
|
||||
group_modify(~ {
|
||||
qtd <- min(max_imagens_por_classe, nrow(.x))
|
||||
slice_sample(.x, n = qtd)
|
||||
}) %>%
|
||||
ungroup() %>%
|
||||
mutate(classe = factor(classe, levels = paste0("C", 1:13)))
|
||||
|
||||
cat("Total de imagens no dataset completo:", nrow(metadados_completo), "\n")
|
||||
cat("Total de imagens na amostra:", nrow(metadados), "\n")
|
||||
|
||||
metadados %>%
|
||||
count(classe, name = "n_imagens_amostra") %>%
|
||||
arrange(classe) %>%
|
||||
kable()
|
||||
```
|
||||
|
||||
```{r}
|
||||
ggplot(metadados, aes(x = classe)) +
|
||||
geom_bar(fill = "#2C7FB8") +
|
||||
coord_flip() +
|
||||
labs(
|
||||
title = "Distribuicao de imagens por classe (dataset completo)",
|
||||
x = "Classe",
|
||||
y = "Numero de imagens"
|
||||
) +
|
||||
theme_minimal()
|
||||
```
|
||||
|
||||
# Modelo pre-treinado (Transfer Learning)
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 6. Carregar modelo pre-treinado MobileNetV2
|
||||
# ============================================================
|
||||
|
||||
modelo_base <- application_mobilenet_v2(
|
||||
include_top = FALSE,
|
||||
weights = "imagenet",
|
||||
pooling = "avg",
|
||||
input_shape = c(224L, 224L, 3L)
|
||||
)
|
||||
|
||||
cat("Modelo carregado:", modelo_base$name, "\n")
|
||||
cat("Dimensao do vetor de features:", modelo_base$output_shape[[2]], "\n")
|
||||
```
|
||||
|
||||
# Extracao de atributos por Transfer Learning
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 7. Extracao de features (com cache separado do original)
|
||||
# ============================================================
|
||||
|
||||
pasta_saida <- "outputs_completo"
|
||||
dir.create(pasta_saida, recursive = TRUE, showWarnings = FALSE)
|
||||
|
||||
# Nomes diferentes para nao sobrescrever o cache do trabalho original
|
||||
arquivo_cache_features <- file.path(pasta_saida, "features_rgb_mobilenetv2_completo.rds")
|
||||
arquivo_cache_parcial <- file.path(pasta_saida, "features_rgb_mobilenetv2_completo_temp.rds")
|
||||
|
||||
tamanho_imagem <- 224L
|
||||
tamanho_lote <- 16L
|
||||
|
||||
extrair_features_batch <- function(arquivos, modelo, tamanho = 224L, lote = 16L) {
|
||||
n <- length(arquivos)
|
||||
resultados <- matrix(NA_real_, nrow = n, ncol = as.integer(modelo$output_shape[[2]]))
|
||||
|
||||
for (inicio in seq(1, n, by = lote)) {
|
||||
fim <- min(inicio + lote - 1L, n)
|
||||
bloco <- arquivos[inicio:fim]
|
||||
tam_bloco <- length(bloco)
|
||||
|
||||
batch_array <- array(0, dim = c(tam_bloco, tamanho, tamanho, 3L))
|
||||
|
||||
for (j in seq_len(tam_bloco)) {
|
||||
img <- image_load(bloco[j], target_size = c(tamanho, tamanho))
|
||||
batch_array[j, , , ] <- image_to_array(img)
|
||||
}
|
||||
|
||||
batch_prep <- (batch_array / 127.5) - 1.0
|
||||
features <- predict(modelo, batch_prep, verbose = 0L)
|
||||
resultados[inicio:fim, ] <- features
|
||||
}
|
||||
|
||||
resultados
|
||||
}
|
||||
|
||||
if (file.exists(arquivo_cache_features)) {
|
||||
dados <- readRDS(arquivo_cache_features)
|
||||
cat("Features carregadas do cache:", arquivo_cache_features, "\n")
|
||||
} else {
|
||||
cat("Extraindo features com MobileNetV2. Isso pode levar alguns minutos...\n")
|
||||
|
||||
erros_idx <- integer(0)
|
||||
|
||||
if (file.exists(arquivo_cache_parcial)) {
|
||||
features_matrix <- readRDS(arquivo_cache_parcial)
|
||||
linhas_prontas <- which(complete.cases(features_matrix))
|
||||
cat("Cache parcial encontrado:", length(linhas_prontas), "imagens ja processadas.\n")
|
||||
} else {
|
||||
n_features <- as.integer(modelo_base$output_shape[[2]])
|
||||
features_matrix <- matrix(NA_real_, nrow = nrow(metadados), ncol = n_features)
|
||||
linhas_prontas <- integer(0)
|
||||
}
|
||||
|
||||
linhas_pendentes <- setdiff(seq_len(nrow(metadados)), linhas_prontas)
|
||||
|
||||
if (length(linhas_pendentes) > 0) {
|
||||
for (inicio in seq(1, length(linhas_pendentes), by = tamanho_lote)) {
|
||||
fim <- min(inicio + tamanho_lote - 1L, length(linhas_pendentes))
|
||||
idxs <- linhas_pendentes[inicio:fim]
|
||||
arquivos_bloco <- metadados$arquivo[idxs]
|
||||
|
||||
resultado <- tryCatch(
|
||||
extrair_features_batch(arquivos_bloco, modelo_base, tamanho_imagem, length(idxs)),
|
||||
error = function(e) {
|
||||
message("Erro no lote ", inicio, "-", fim, ": ", conditionMessage(e))
|
||||
erros_idx <<- c(erros_idx, idxs)
|
||||
NULL
|
||||
}
|
||||
)
|
||||
|
||||
if (!is.null(resultado)) {
|
||||
features_matrix[idxs, ] <- resultado
|
||||
}
|
||||
|
||||
cat(sprintf(" Processadas: %d / %d\r", min(fim, length(linhas_pendentes)), length(linhas_pendentes)))
|
||||
|
||||
if (fim %% (tamanho_lote * 5L) == 0L) {
|
||||
saveRDS(features_matrix, arquivo_cache_parcial)
|
||||
}
|
||||
}
|
||||
cat("\n")
|
||||
saveRDS(features_matrix, arquivo_cache_parcial)
|
||||
}
|
||||
|
||||
imagens_ok <- complete.cases(features_matrix)
|
||||
n_features <- ncol(features_matrix)
|
||||
nomes_feat <- paste0("feat_", stringr::str_pad(seq_len(n_features), 4, pad = "0"))
|
||||
colnames(features_matrix) <- nomes_feat
|
||||
|
||||
dados <- bind_cols(
|
||||
metadados[imagens_ok, ],
|
||||
as_tibble(features_matrix[imagens_ok, ])
|
||||
)
|
||||
|
||||
if (length(erros_idx) > 0) {
|
||||
erros_df <- metadados[erros_idx, ] %>% mutate(erro = "falha na extracao CNN")
|
||||
arquivo_erros <- file.path(pasta_saida, "erros_processamento_rgb_completo.csv")
|
||||
write_csv(erros_df, arquivo_erros)
|
||||
cat("Imagens com erro:", length(erros_idx), "(ver", arquivo_erros, ")\n")
|
||||
}
|
||||
|
||||
saveRDS(dados, arquivo_cache_features)
|
||||
if (file.exists(arquivo_cache_parcial)) file.remove(arquivo_cache_parcial)
|
||||
|
||||
cat("Features salvas em:", arquivo_cache_features, "\n")
|
||||
}
|
||||
|
||||
cat("Imagens na base final:", nrow(dados), "\n")
|
||||
cat("Numero de atributos extraidos (MobileNetV2):", ncol(dados) - 3L, "\n")
|
||||
```
|
||||
|
||||
# Separacao entre treino e teste
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 8. Separacao treino/teste
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
idx_treino <- createDataPartition(
|
||||
y = dados$classe,
|
||||
p = 0.70,
|
||||
list = FALSE
|
||||
)
|
||||
|
||||
treino <- dados[idx_treino, ]
|
||||
teste <- dados[-idx_treino, ]
|
||||
|
||||
cat("Imagens no treino:", nrow(treino), "\n")
|
||||
cat("Imagens no teste:", nrow(teste), "\n")
|
||||
|
||||
treino %>%
|
||||
count(classe, name = "treino") %>%
|
||||
full_join(
|
||||
teste %>% count(classe, name = "teste"),
|
||||
by = "classe"
|
||||
) %>%
|
||||
arrange(classe) %>%
|
||||
kable()
|
||||
```
|
||||
|
||||
# Pre-processamento
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 9. Preparar matrizes X e vetor y
|
||||
# ============================================================
|
||||
|
||||
colunas_nao_preditoras <- c("arquivo", "classe", "tamanho_mb")
|
||||
|
||||
x_treino <- treino %>%
|
||||
select(-all_of(colunas_nao_preditoras))
|
||||
|
||||
x_teste <- teste %>%
|
||||
select(-all_of(colunas_nao_preditoras))
|
||||
|
||||
y_treino <- droplevels(treino$classe)
|
||||
y_teste <- factor(teste$classe, levels = levels(y_treino))
|
||||
|
||||
variaveis_nzv <- nearZeroVar(x_treino)
|
||||
|
||||
if (length(variaveis_nzv) > 0) {
|
||||
x_treino <- x_treino[, -variaveis_nzv, drop = FALSE]
|
||||
x_teste <- x_teste[, colnames(x_treino), drop = FALSE]
|
||||
}
|
||||
|
||||
preproc <- preProcess(
|
||||
x_treino,
|
||||
method = c("center", "scale")
|
||||
)
|
||||
|
||||
x_treino_norm <- predict(preproc, x_treino)
|
||||
x_teste_norm <- predict(preproc, x_teste)
|
||||
|
||||
cat("Preditores usados nos modelos:", ncol(x_treino_norm), "\n")
|
||||
```
|
||||
|
||||
# Modelo 1: SVM linear
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 10. SVM linear
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
modelo_svm_linear <- svm(
|
||||
x = x_treino_norm,
|
||||
y = y_treino,
|
||||
kernel = "linear",
|
||||
cost = 1,
|
||||
scale = FALSE
|
||||
)
|
||||
|
||||
pred_svm_linear <- predict(modelo_svm_linear, x_teste_norm)
|
||||
|
||||
cm_svm_linear <- confusionMatrix(pred_svm_linear, y_teste)
|
||||
|
||||
cm_svm_linear
|
||||
```
|
||||
|
||||
# Modelo 2: SVM radial
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 11. SVM radial com parametros fixos
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
modelo_svm_radial <- svm(
|
||||
x = x_treino_norm,
|
||||
y = y_treino,
|
||||
kernel = "radial",
|
||||
cost = 10,
|
||||
gamma = 0.01,
|
||||
scale = FALSE
|
||||
)
|
||||
|
||||
pred_svm_radial <- predict(modelo_svm_radial, x_teste_norm)
|
||||
|
||||
cm_svm_radial <- confusionMatrix(pred_svm_radial, y_teste)
|
||||
|
||||
cm_svm_radial
|
||||
```
|
||||
|
||||
# Modelo 3: Ajuste de hiperparametros do SVM radial
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 12. Ajuste simples de hiperparametros do SVM radial
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
k_cv <- min(5, as.integer(min(table(y_treino))))
|
||||
|
||||
if (k_cv >= 2) {
|
||||
ajuste_svm_radial <- tune.svm(
|
||||
x = x_treino_norm,
|
||||
y = y_treino,
|
||||
kernel = "radial",
|
||||
cost = c(0.1, 1, 10, 100),
|
||||
gamma = c(0.001, 0.01, 0.05, 0.1),
|
||||
tunecontrol = tune.control(cross = k_cv)
|
||||
)
|
||||
|
||||
modelo_svm_radial_ajustado <- ajuste_svm_radial$best.model
|
||||
|
||||
pred_svm_radial_ajustado <- predict(
|
||||
modelo_svm_radial_ajustado,
|
||||
x_teste_norm
|
||||
)
|
||||
|
||||
cm_svm_radial_ajustado <- confusionMatrix(
|
||||
pred_svm_radial_ajustado,
|
||||
y_teste
|
||||
)
|
||||
|
||||
ajuste_svm_radial$best.parameters %>%
|
||||
kable()
|
||||
} else {
|
||||
warning("Poucas amostras por classe para validacao cruzada.")
|
||||
ajuste_svm_radial <- NULL
|
||||
modelo_svm_radial_ajustado <- modelo_svm_radial
|
||||
pred_svm_radial_ajustado <- pred_svm_radial
|
||||
cm_svm_radial_ajustado <- cm_svm_radial
|
||||
}
|
||||
```
|
||||
|
||||
```{r}
|
||||
cm_svm_radial_ajustado
|
||||
```
|
||||
|
||||
# Modelo 4: PCA + SVM radial
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 13. PCA
|
||||
# ============================================================
|
||||
|
||||
pca <- prcomp(
|
||||
x_treino_norm,
|
||||
center = FALSE,
|
||||
scale. = FALSE
|
||||
)
|
||||
|
||||
variancia <- pca$sdev^2
|
||||
variancia_exp <- variancia / sum(variancia)
|
||||
variancia_acum <- cumsum(variancia_exp)
|
||||
|
||||
n_comp_95 <- which(variancia_acum >= 0.95)[1]
|
||||
n_comp <- min(n_comp_95, 30, ncol(x_treino_norm))
|
||||
|
||||
cat("Componentes necessarios para 95% da variancia:", n_comp_95, "\n")
|
||||
cat("Componentes usados no modelo:", n_comp, "\n")
|
||||
```
|
||||
|
||||
```{r}
|
||||
tibble(
|
||||
componente = seq_along(variancia_acum),
|
||||
variancia_acumulada = variancia_acum
|
||||
) %>%
|
||||
ggplot(aes(x = componente, y = variancia_acumulada)) +
|
||||
geom_line(color = "#2C7FB8") +
|
||||
geom_point(color = "#2C7FB8") +
|
||||
geom_hline(yintercept = 0.95, linetype = "dashed") +
|
||||
labs(
|
||||
title = "Variancia acumulada pelo PCA (dataset completo)",
|
||||
x = "Numero de componentes principais",
|
||||
y = "Variancia acumulada"
|
||||
) +
|
||||
theme_minimal()
|
||||
```
|
||||
|
||||
```{r}
|
||||
x_treino_pca <- as.data.frame(pca$x[, 1:n_comp, drop = FALSE])
|
||||
|
||||
x_teste_pca <- as.data.frame(
|
||||
predict(pca, newdata = x_teste_norm)[, 1:n_comp, drop = FALSE]
|
||||
)
|
||||
|
||||
set.seed(123)
|
||||
|
||||
modelo_svm_pca <- svm(
|
||||
x = x_treino_pca,
|
||||
y = y_treino,
|
||||
kernel = "radial",
|
||||
cost = 10,
|
||||
gamma = 0.01,
|
||||
scale = FALSE
|
||||
)
|
||||
|
||||
pred_svm_pca <- predict(modelo_svm_pca, x_teste_pca)
|
||||
|
||||
cm_svm_pca <- confusionMatrix(pred_svm_pca, y_teste)
|
||||
|
||||
cm_svm_pca
|
||||
```
|
||||
|
||||
# Modelo 5: Random Forest
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 14. Random Forest
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
modelo_rf <- randomForest(
|
||||
x = x_treino_norm,
|
||||
y = y_treino,
|
||||
ntree = 500,
|
||||
importance = TRUE
|
||||
)
|
||||
|
||||
pred_rf <- predict(modelo_rf, x_teste_norm)
|
||||
|
||||
cm_rf <- confusionMatrix(pred_rf, y_teste)
|
||||
|
||||
cm_rf
|
||||
```
|
||||
|
||||
# Comparacao dos modelos
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 15. Funcao de avaliacao dos modelos
|
||||
# ============================================================
|
||||
|
||||
avaliar_modelo <- function(nome, matriz_confusao) {
|
||||
overall <- matriz_confusao$overall
|
||||
by_class <- matriz_confusao$byClass
|
||||
|
||||
if (is.matrix(by_class)) {
|
||||
sensibilidade_macro <- mean(by_class[, "Sensitivity"], na.rm = TRUE)
|
||||
especificidade_macro <- mean(by_class[, "Specificity"], na.rm = TRUE)
|
||||
f1_macro <- mean(by_class[, "F1"], na.rm = TRUE)
|
||||
} else {
|
||||
sensibilidade_macro <- by_class["Sensitivity"]
|
||||
especificidade_macro <- by_class["Specificity"]
|
||||
f1_macro <- by_class["F1"]
|
||||
}
|
||||
|
||||
tibble(
|
||||
modelo = nome,
|
||||
acuracia = as.numeric(overall["Accuracy"]),
|
||||
kappa = as.numeric(overall["Kappa"]),
|
||||
sensibilidade_macro = as.numeric(sensibilidade_macro),
|
||||
especificidade_macro = as.numeric(especificidade_macro),
|
||||
f1_macro = as.numeric(f1_macro)
|
||||
)
|
||||
}
|
||||
|
||||
resultados <- bind_rows(
|
||||
avaliar_modelo("SVM linear", cm_svm_linear),
|
||||
avaliar_modelo("SVM radial", cm_svm_radial),
|
||||
avaliar_modelo("SVM radial ajustado", cm_svm_radial_ajustado),
|
||||
avaliar_modelo("PCA + SVM radial", cm_svm_pca),
|
||||
avaliar_modelo("Random Forest", cm_rf)
|
||||
) %>%
|
||||
arrange(desc(acuracia))
|
||||
|
||||
resultados %>%
|
||||
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
|
||||
kable(caption = "Resultados - Dataset Completo (Holdout 70/30)")
|
||||
```
|
||||
|
||||
```{r}
|
||||
ggplot(resultados, aes(x = reorder(modelo, acuracia), y = acuracia)) +
|
||||
geom_col(fill = "#2C7FB8") +
|
||||
coord_flip() +
|
||||
labs(
|
||||
title = "Comparacao da acuracia dos modelos (dataset completo)",
|
||||
x = "Modelo",
|
||||
y = "Acuracia"
|
||||
) +
|
||||
theme_minimal()
|
||||
```
|
||||
|
||||
# Importancia das variaveis
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 16. Importancia das variaveis
|
||||
# ============================================================
|
||||
|
||||
importancia <- importance(modelo_rf)
|
||||
|
||||
coluna_importancia <- if ("MeanDecreaseGini" %in% colnames(importancia)) {
|
||||
"MeanDecreaseGini"
|
||||
} else {
|
||||
colnames(importancia)[1]
|
||||
}
|
||||
|
||||
importancia_df <- tibble(
|
||||
variavel = rownames(importancia),
|
||||
importancia = importancia[, coluna_importancia]
|
||||
) %>%
|
||||
arrange(desc(importancia))
|
||||
|
||||
importancia_df %>%
|
||||
slice_head(n = 20) %>%
|
||||
kable()
|
||||
```
|
||||
|
||||
# Salvamento dos resultados
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 17. Salvar saidas (nomes diferentes do original)
|
||||
# ============================================================
|
||||
|
||||
dir.create(pasta_saida, recursive = TRUE, showWarnings = FALSE)
|
||||
|
||||
write_csv(dados, file.path(pasta_saida, "features_rgb_completo.csv"))
|
||||
write_csv(resultados, file.path(pasta_saida, "resultados_modelos_rgb_completo.csv"))
|
||||
write_csv(importancia_df, file.path(pasta_saida, "importancia_variaveis_rgb_completo.csv"))
|
||||
|
||||
saveRDS(
|
||||
list(
|
||||
preproc = preproc,
|
||||
modelo_svm_linear = modelo_svm_linear,
|
||||
modelo_svm_radial = modelo_svm_radial,
|
||||
ajuste_svm_radial = ajuste_svm_radial,
|
||||
modelo_svm_radial_ajustado = modelo_svm_radial_ajustado,
|
||||
modelo_svm_pca = modelo_svm_pca,
|
||||
modelo_rf = modelo_rf,
|
||||
pca = pca,
|
||||
resultados = resultados,
|
||||
extrator_cnn = "MobileNetV2 (ImageNet, sem topo, pooling=avg, 1280 features)"
|
||||
),
|
||||
file.path(pasta_saida, "modelos_rgb_completo.rds")
|
||||
)
|
||||
|
||||
cat("Arquivos salvos em:", pasta_saida, "\n")
|
||||
```
|
||||
|
||||
# Validacao Cruzada K-Fold
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 18. Validacao cruzada k-fold (k=5, estratificada)
|
||||
# ============================================================
|
||||
|
||||
set.seed(123)
|
||||
|
||||
k_folds <- 5L
|
||||
|
||||
folds <- createFolds(dados$classe, k = k_folds, list = TRUE, returnTrain = FALSE)
|
||||
|
||||
grade_cost_kfold <- c(1, 10, 100)
|
||||
grade_gamma_kfold <- c(0.001, 0.01, 0.05, 0.1)
|
||||
|
||||
metricas_folds <- vector("list", k_folds)
|
||||
|
||||
for (fold_i in seq_len(k_folds)) {
|
||||
idx_teste <- folds[[fold_i]]
|
||||
idx_treino <- setdiff(seq_len(nrow(dados)), idx_teste)
|
||||
|
||||
fold_treino <- dados[idx_treino, ]
|
||||
fold_teste <- dados[idx_teste, ]
|
||||
|
||||
x_fold_tr <- fold_treino %>% select(-all_of(colunas_nao_preditoras))
|
||||
x_fold_te <- fold_teste %>% select(-all_of(colunas_nao_preditoras))
|
||||
y_fold_tr <- droplevels(fold_treino$classe)
|
||||
y_fold_te <- factor(fold_teste$classe, levels = levels(y_fold_tr))
|
||||
|
||||
nzv_fold <- nearZeroVar(x_fold_tr)
|
||||
if (length(nzv_fold) > 0) {
|
||||
x_fold_tr <- x_fold_tr[, -nzv_fold, drop = FALSE]
|
||||
x_fold_te <- x_fold_te[, colnames(x_fold_tr), drop = FALSE]
|
||||
}
|
||||
|
||||
pp_fold <- preProcess(x_fold_tr, method = c("center", "scale"))
|
||||
x_fold_tr_n <- predict(pp_fold, x_fold_tr)
|
||||
x_fold_te_n <- predict(pp_fold, x_fold_te)
|
||||
|
||||
m1 <- svm(x = x_fold_tr_n, y = y_fold_tr,
|
||||
kernel = "linear", cost = 1, scale = FALSE)
|
||||
|
||||
m2 <- svm(x = x_fold_tr_n, y = y_fold_tr,
|
||||
kernel = "radial", cost = 10, gamma = 0.01, scale = FALSE)
|
||||
|
||||
k_cv_fold <- min(5L, min(table(y_fold_tr)))
|
||||
ajuste_fold <- tune.svm(
|
||||
x = x_fold_tr_n,
|
||||
y = y_fold_tr,
|
||||
kernel = "radial",
|
||||
cost = grade_cost_kfold,
|
||||
gamma = grade_gamma_kfold,
|
||||
tunecontrol = tune.control(cross = k_cv_fold)
|
||||
)
|
||||
m3 <- ajuste_fold$best.model
|
||||
|
||||
pca_fold <- prcomp(x_fold_tr_n, center = FALSE, scale. = FALSE)
|
||||
var_acum_f <- cumsum(pca_fold$sdev^2 / sum(pca_fold$sdev^2))
|
||||
n_comp_f <- min(which(var_acum_f >= 0.95)[1], 30L, ncol(x_fold_tr_n))
|
||||
x_pca_tr <- as.data.frame(pca_fold$x[, 1:n_comp_f, drop = FALSE])
|
||||
x_pca_te <- as.data.frame(predict(pca_fold, x_fold_te_n)[, 1:n_comp_f, drop = FALSE])
|
||||
m4 <- svm(x = x_pca_tr, y = y_fold_tr,
|
||||
kernel = "radial", cost = 10, gamma = 0.01, scale = FALSE)
|
||||
|
||||
m5 <- randomForest(x = x_fold_tr_n, y = y_fold_tr, ntree = 200L)
|
||||
|
||||
metricas_folds[[fold_i]] <- bind_rows(
|
||||
avaliar_modelo("SVM linear", confusionMatrix(predict(m1, x_fold_te_n), y_fold_te)),
|
||||
avaliar_modelo("SVM radial", confusionMatrix(predict(m2, x_fold_te_n), y_fold_te)),
|
||||
avaliar_modelo("SVM radial ajustado", confusionMatrix(predict(m3, x_fold_te_n), y_fold_te)),
|
||||
avaliar_modelo("PCA + SVM radial", confusionMatrix(predict(m4, x_pca_te), y_fold_te)),
|
||||
avaliar_modelo("Random Forest", confusionMatrix(predict(m5, x_fold_te_n), y_fold_te))
|
||||
) %>% mutate(fold = fold_i)
|
||||
|
||||
cat(sprintf(
|
||||
"Fold %d/%d concluido. SVM ajustado: cost = %s | gamma = %s\n",
|
||||
fold_i, k_folds,
|
||||
ajuste_fold$best.parameters$cost,
|
||||
ajuste_fold$best.parameters$gamma
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
```{r}
|
||||
resultados_kfold <- bind_rows(metricas_folds) %>%
|
||||
group_by(modelo) %>%
|
||||
summarise(across(c(acuracia, kappa, sensibilidade_macro, especificidade_macro, f1_macro), mean),
|
||||
.groups = "drop") %>%
|
||||
arrange(desc(acuracia))
|
||||
|
||||
write_csv(resultados_kfold, file.path(pasta_saida, "resultados_kfold_rgb_completo.csv"))
|
||||
|
||||
resultados_kfold %>%
|
||||
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
|
||||
kable(caption = "Acuracia media - K-Fold (k=5) - Dataset Completo")
|
||||
```
|
||||
|
||||
# Comparacao: Holdout vs K-Fold
|
||||
|
||||
```{r}
|
||||
# ============================================================
|
||||
# 19. Comparacao direta entre as duas estrategias
|
||||
# ============================================================
|
||||
|
||||
comparacao <- bind_rows(
|
||||
resultados %>% mutate(estrategia = "Holdout 70/30"),
|
||||
resultados_kfold %>% mutate(estrategia = "K-Fold (k=5)")
|
||||
) %>%
|
||||
select(estrategia, modelo, acuracia, kappa, f1_macro) %>%
|
||||
arrange(estrategia, desc(acuracia))
|
||||
|
||||
comparacao %>%
|
||||
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
|
||||
kable(caption = "Comparacao entre Holdout 70/30 e K-Fold - Dataset Completo")
|
||||
```
|
||||
|
||||
```{r}
|
||||
ggplot(comparacao, aes(x = reorder(modelo, acuracia), y = acuracia, fill = estrategia)) +
|
||||
geom_col(position = "dodge") +
|
||||
coord_flip() +
|
||||
scale_fill_manual(values = c("Holdout 70/30" = "#2C7FB8", "K-Fold (k=5)" = "#E06C00")) +
|
||||
labs(
|
||||
title = "Acuracia: Holdout 70/30 vs K-Fold (k=5) - Dataset Completo",
|
||||
x = "Modelo",
|
||||
y = "Acuracia",
|
||||
fill = "Estrategia"
|
||||
) +
|
||||
theme_minimal()
|
||||
```
|
||||
|
||||
# Conclusao
|
||||
|
||||
```{r}
|
||||
melhor_holdout <- max(resultados$acuracia)
|
||||
melhor_kf <- max(resultados_kfold$acuracia)
|
||||
|
||||
cat("Acuracia maxima - Holdout:", round(melhor_holdout, 4), "\n")
|
||||
cat("Acuracia maxima - K-Fold: ", round(melhor_kf, 4), "\n")
|
||||
cat("Melhor modelo (holdout):", resultados$modelo[1], "\n")
|
||||
cat("Melhor modelo (k-fold):", resultados_kfold$modelo[1], "\n")
|
||||
```
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 15 KiB |
2106
trabalho/completo/trabalho_svm_rgb_completo_files/libs/bootstrap/bootstrap-icons.css
vendored
Normal file
7
trabalho/completo/trabalho_svm_rgb_completo_files/libs/bootstrap/bootstrap.min.js
vendored
Normal file
7
trabalho/completo/trabalho_svm_rgb_completo_files/libs/clipboard/clipboard.min.js
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
.pagedtable {
|
||||
overflow: auto;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.pagedtable-wrapper {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pagedtable table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagedtable th {
|
||||
padding: 0 5px 0 5px;
|
||||
border: none;
|
||||
border-bottom: 2px solid #dddddd;
|
||||
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.pagedtable-empty th {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedtable td {
|
||||
padding: 0 4px 0 4px;
|
||||
}
|
||||
|
||||
.pagedtable .even {
|
||||
background-color: rgba(140, 140, 140, 0.1);
|
||||
}
|
||||
|
||||
.pagedtable-padding-col {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagedtable a {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pagedtable-index-nav {
|
||||
cursor: pointer;
|
||||
padding: 0 5px 0 5px;
|
||||
float: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pagedtable-index-nav-disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
a.pagedtable-index-nav-disabled:hover {
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.pagedtable-indexes {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pagedtable-index-current {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
a.pagedtable-index-current:hover {
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagedtable-index {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pagedtable-index-separator-left {
|
||||
display: inline-block;
|
||||
color: #333;
|
||||
font-size: 9px;
|
||||
padding: 0 0 0 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pagedtable-index-separator-right {
|
||||
display: inline-block;
|
||||
color: #333;
|
||||
font-size: 9px;
|
||||
padding: 0 4px 0 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pagedtable-footer {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.pagedtable-not-empty .pagedtable-footer {
|
||||
border-top: 2px solid #dddddd;
|
||||
}
|
||||
|
||||
.pagedtable-info {
|
||||
overflow: hidden;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pagedtable-header-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pagedtable-header-type {
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pagedtable-na-cell {
|
||||
font-style: italic;
|
||||
opacity: 0.3;
|
||||
}
|
||||
9
trabalho/completo/trabalho_svm_rgb_completo_files/libs/quarto-html/anchor.min.js
vendored
Normal file
6
trabalho/completo/trabalho_svm_rgb_completo_files/libs/quarto-html/popper.min.js
vendored
Normal file
@@ -0,0 +1,236 @@
|
||||
/* quarto syntax highlight colors */
|
||||
:root {
|
||||
--quarto-hl-ot-color: #003B4F;
|
||||
--quarto-hl-at-color: #657422;
|
||||
--quarto-hl-ss-color: #20794D;
|
||||
--quarto-hl-an-color: #5E5E5E;
|
||||
--quarto-hl-fu-color: #4758AB;
|
||||
--quarto-hl-st-color: #20794D;
|
||||
--quarto-hl-cf-color: #003B4F;
|
||||
--quarto-hl-op-color: #5E5E5E;
|
||||
--quarto-hl-er-color: #AD0000;
|
||||
--quarto-hl-bn-color: #AD0000;
|
||||
--quarto-hl-al-color: #AD0000;
|
||||
--quarto-hl-va-color: #111111;
|
||||
--quarto-hl-bu-color: inherit;
|
||||
--quarto-hl-ex-color: inherit;
|
||||
--quarto-hl-pp-color: #AD0000;
|
||||
--quarto-hl-in-color: #5E5E5E;
|
||||
--quarto-hl-vs-color: #20794D;
|
||||
--quarto-hl-wa-color: #5E5E5E;
|
||||
--quarto-hl-do-color: #5E5E5E;
|
||||
--quarto-hl-im-color: #00769E;
|
||||
--quarto-hl-ch-color: #20794D;
|
||||
--quarto-hl-dt-color: #AD0000;
|
||||
--quarto-hl-fl-color: #AD0000;
|
||||
--quarto-hl-co-color: #5E5E5E;
|
||||
--quarto-hl-cv-color: #5E5E5E;
|
||||
--quarto-hl-cn-color: #8f5902;
|
||||
--quarto-hl-sc-color: #5E5E5E;
|
||||
--quarto-hl-dv-color: #AD0000;
|
||||
--quarto-hl-kw-color: #003B4F;
|
||||
}
|
||||
|
||||
/* other quarto variables */
|
||||
:root {
|
||||
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* syntax highlight based on Pandoc's rules */
|
||||
pre > code.sourceCode > span {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
code.sourceCode > span {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
div.sourceCode,
|
||||
div.sourceCode pre.sourceCode {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
/* Normal */
|
||||
code span {
|
||||
color: #003B4F;
|
||||
}
|
||||
|
||||
/* Alert */
|
||||
code span.al {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Annotation */
|
||||
code span.an {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Attribute */
|
||||
code span.at {
|
||||
color: #657422;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* BaseN */
|
||||
code span.bn {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* BuiltIn */
|
||||
code span.bu {
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* ControlFlow */
|
||||
code span.cf {
|
||||
color: #003B4F;
|
||||
font-weight: bold;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Char */
|
||||
code span.ch {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Constant */
|
||||
code span.cn {
|
||||
color: #8f5902;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
code span.co {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* CommentVar */
|
||||
code span.cv {
|
||||
color: #5E5E5E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Documentation */
|
||||
code span.do {
|
||||
color: #5E5E5E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* DataType */
|
||||
code span.dt {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* DecVal */
|
||||
code span.dv {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Error */
|
||||
code span.er {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Extension */
|
||||
code span.ex {
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Float */
|
||||
code span.fl {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Function */
|
||||
code span.fu {
|
||||
color: #4758AB;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Import */
|
||||
code span.im {
|
||||
color: #00769E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Information */
|
||||
code span.in {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Keyword */
|
||||
code span.kw {
|
||||
color: #003B4F;
|
||||
font-weight: bold;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Operator */
|
||||
code span.op {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Other */
|
||||
code span.ot {
|
||||
color: #003B4F;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Preprocessor */
|
||||
code span.pp {
|
||||
color: #AD0000;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* SpecialChar */
|
||||
code span.sc {
|
||||
color: #5E5E5E;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* SpecialString */
|
||||
code span.ss {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* String */
|
||||
code span.st {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Variable */
|
||||
code span.va {
|
||||
color: #111111;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* VerbatimString */
|
||||
code span.vs {
|
||||
color: #20794D;
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Warning */
|
||||
code span.wa {
|
||||
color: #5E5E5E;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prevent-inlining {
|
||||
content: "</";
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */
|
||||
@@ -0,0 +1,845 @@
|
||||
import * as tabsets from "./tabsets/tabsets.js";
|
||||
|
||||
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
|
||||
detail: {},
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
composed: false,
|
||||
});
|
||||
|
||||
const layoutMarginEls = () => {
|
||||
// Find any conflicting margin elements and add margins to the
|
||||
// top to prevent overlap
|
||||
const marginChildren = window.document.querySelectorAll(
|
||||
".column-margin.column-container > *, .margin-caption, .aside"
|
||||
);
|
||||
|
||||
let lastBottom = 0;
|
||||
for (const marginChild of marginChildren) {
|
||||
if (marginChild.offsetParent !== null) {
|
||||
// clear the top margin so we recompute it
|
||||
marginChild.style.marginTop = null;
|
||||
const top = marginChild.getBoundingClientRect().top + window.scrollY;
|
||||
if (top < lastBottom) {
|
||||
const marginChildStyle = window.getComputedStyle(marginChild);
|
||||
const marginBottom = parseFloat(marginChildStyle["marginBottom"]);
|
||||
const margin = lastBottom - top + marginBottom;
|
||||
marginChild.style.marginTop = `${margin}px`;
|
||||
}
|
||||
const styles = window.getComputedStyle(marginChild);
|
||||
const marginTop = parseFloat(styles["marginTop"]);
|
||||
lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.document.addEventListener("DOMContentLoaded", function (_event) {
|
||||
// Recompute the position of margin elements anytime the body size changes
|
||||
if (window.ResizeObserver) {
|
||||
const resizeObserver = new window.ResizeObserver(
|
||||
throttle(() => {
|
||||
layoutMarginEls();
|
||||
if (
|
||||
window.document.body.getBoundingClientRect().width < 990 &&
|
||||
isReaderMode()
|
||||
) {
|
||||
quartoToggleReader();
|
||||
}
|
||||
}, 50)
|
||||
);
|
||||
resizeObserver.observe(window.document.body);
|
||||
}
|
||||
|
||||
const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');
|
||||
const sidebarEl = window.document.getElementById("quarto-sidebar");
|
||||
const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");
|
||||
const marginSidebarEl = window.document.getElementById(
|
||||
"quarto-margin-sidebar"
|
||||
);
|
||||
// function to determine whether the element has a previous sibling that is active
|
||||
const prevSiblingIsActiveLink = (el) => {
|
||||
const sibling = el.previousElementSibling;
|
||||
if (sibling && sibling.tagName === "A") {
|
||||
return sibling.classList.contains("active");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// dispatch for htmlwidgets
|
||||
// they use slideenter event to trigger resize
|
||||
function fireSlideEnter() {
|
||||
const event = window.document.createEvent("Event");
|
||||
event.initEvent("slideenter", true, true);
|
||||
window.document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("shown.bs.tab", fireSlideEnter);
|
||||
});
|
||||
|
||||
// dispatch for shiny
|
||||
// they use BS shown and hidden events to trigger rendering
|
||||
function distpatchShinyEvents(previous, current) {
|
||||
if (window.jQuery) {
|
||||
if (previous) {
|
||||
window.jQuery(previous).trigger("hidden");
|
||||
}
|
||||
if (current) {
|
||||
window.jQuery(current).trigger("shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tabby.js listener: Trigger event for htmlwidget and shiny
|
||||
document.addEventListener(
|
||||
"tabby",
|
||||
function (event) {
|
||||
fireSlideEnter();
|
||||
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Track scrolling and mark TOC links as active
|
||||
// get table of contents and sidebar (bail if we don't have at least one)
|
||||
const tocLinks = tocEl
|
||||
? [...tocEl.querySelectorAll("a[data-scroll-target]")]
|
||||
: [];
|
||||
const makeActive = (link) => tocLinks[link].classList.add("active");
|
||||
const removeActive = (link) => tocLinks[link].classList.remove("active");
|
||||
const removeAllActive = () =>
|
||||
[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));
|
||||
|
||||
// activate the anchor for a section associated with this TOC entry
|
||||
tocLinks.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
if (link.href.indexOf("#") !== -1) {
|
||||
const anchor = link.href.split("#")[1];
|
||||
const heading = window.document.querySelector(
|
||||
`[data-anchor-id="${anchor}"]`
|
||||
);
|
||||
if (heading) {
|
||||
// Add the class
|
||||
heading.classList.add("reveal-anchorjs-link");
|
||||
|
||||
// function to show the anchor
|
||||
const handleMouseout = () => {
|
||||
heading.classList.remove("reveal-anchorjs-link");
|
||||
heading.removeEventListener("mouseout", handleMouseout);
|
||||
};
|
||||
|
||||
// add a function to clear the anchor when the user mouses out of it
|
||||
heading.addEventListener("mouseout", handleMouseout);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sections = tocLinks.map((link) => {
|
||||
const target = link.getAttribute("data-scroll-target");
|
||||
if (target.startsWith("#")) {
|
||||
return window.document.getElementById(decodeURI(`${target.slice(1)}`));
|
||||
} else {
|
||||
return window.document.querySelector(decodeURI(`${target}`));
|
||||
}
|
||||
});
|
||||
|
||||
const sectionMargin = 200;
|
||||
let currentActive = 0;
|
||||
// track whether we've initialized state the first time
|
||||
let init = false;
|
||||
|
||||
const updateActiveLink = () => {
|
||||
// The index from bottom to top (e.g. reversed list)
|
||||
let sectionIndex = -1;
|
||||
if (
|
||||
window.innerHeight + window.pageYOffset >=
|
||||
window.document.body.offsetHeight
|
||||
) {
|
||||
// This is the no-scroll case where last section should be the active one
|
||||
sectionIndex = 0;
|
||||
} else {
|
||||
// This finds the last section visible on screen that should be made active
|
||||
sectionIndex = [...sections].reverse().findIndex((section) => {
|
||||
if (section) {
|
||||
return window.pageYOffset >= section.offsetTop - sectionMargin;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sectionIndex > -1) {
|
||||
const current = sections.length - sectionIndex - 1;
|
||||
if (current !== currentActive) {
|
||||
removeAllActive();
|
||||
currentActive = current;
|
||||
makeActive(current);
|
||||
if (init) {
|
||||
window.dispatchEvent(sectionChanged);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inHiddenRegion = (top, bottom, hiddenRegions) => {
|
||||
for (const region of hiddenRegions) {
|
||||
if (top <= region.bottom && bottom >= region.top) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const categorySelector = "header.quarto-title-block .quarto-category";
|
||||
const activateCategories = (href) => {
|
||||
// Find any categories
|
||||
// Surround them with a link pointing back to:
|
||||
// #category=Authoring
|
||||
try {
|
||||
const categoryEls = window.document.querySelectorAll(categorySelector);
|
||||
for (const categoryEl of categoryEls) {
|
||||
const categoryText = categoryEl.textContent;
|
||||
if (categoryText) {
|
||||
const link = `${href}#category=${encodeURIComponent(categoryText)}`;
|
||||
const linkEl = window.document.createElement("a");
|
||||
linkEl.setAttribute("href", link);
|
||||
for (const child of categoryEl.childNodes) {
|
||||
linkEl.append(child);
|
||||
}
|
||||
categoryEl.appendChild(linkEl);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
};
|
||||
function hasTitleCategories() {
|
||||
return window.document.querySelector(categorySelector) !== null;
|
||||
}
|
||||
|
||||
function offsetRelativeUrl(url) {
|
||||
const offset = getMeta("quarto:offset");
|
||||
return offset ? offset + url : url;
|
||||
}
|
||||
|
||||
function offsetAbsoluteUrl(url) {
|
||||
const offset = getMeta("quarto:offset");
|
||||
const baseUrl = new URL(offset, window.location);
|
||||
|
||||
const projRelativeUrl = url.replace(baseUrl, "");
|
||||
if (projRelativeUrl.startsWith("/")) {
|
||||
return projRelativeUrl;
|
||||
} else {
|
||||
return "/" + projRelativeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// read a meta tag value
|
||||
function getMeta(metaName) {
|
||||
const metas = window.document.getElementsByTagName("meta");
|
||||
for (let i = 0; i < metas.length; i++) {
|
||||
if (metas[i].getAttribute("name") === metaName) {
|
||||
return metas[i].getAttribute("content");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function findAndActivateCategories() {
|
||||
// Categories search with listing only use path without query
|
||||
const currentPagePath = offsetAbsoluteUrl(
|
||||
window.location.origin + window.location.pathname
|
||||
);
|
||||
const response = await fetch(offsetRelativeUrl("listings.json"));
|
||||
if (response.status == 200) {
|
||||
return response.json().then(function (listingPaths) {
|
||||
const listingHrefs = [];
|
||||
for (const listingPath of listingPaths) {
|
||||
const pathWithoutLeadingSlash = listingPath.listing.substring(1);
|
||||
for (const item of listingPath.items) {
|
||||
const encodedItem = encodeURI(item);
|
||||
if (
|
||||
encodedItem === currentPagePath ||
|
||||
encodedItem === currentPagePath + "index.html"
|
||||
) {
|
||||
// Resolve this path against the offset to be sure
|
||||
// we already are using the correct path to the listing
|
||||
// (this adjusts the listing urls to be rooted against
|
||||
// whatever root the page is actually running against)
|
||||
const relative = offsetRelativeUrl(pathWithoutLeadingSlash);
|
||||
const baseUrl = window.location;
|
||||
const resolvedPath = new URL(relative, baseUrl);
|
||||
listingHrefs.push(resolvedPath.pathname);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look up the tree for a nearby linting and use that if we find one
|
||||
const nearestListing = findNearestParentListing(
|
||||
offsetAbsoluteUrl(window.location.pathname),
|
||||
listingHrefs
|
||||
);
|
||||
if (nearestListing) {
|
||||
activateCategories(nearestListing);
|
||||
} else {
|
||||
// See if the referrer is a listing page for this item
|
||||
const referredRelativePath = offsetAbsoluteUrl(document.referrer);
|
||||
const referrerListing = listingHrefs.find((listingHref) => {
|
||||
const isListingReferrer =
|
||||
listingHref === referredRelativePath ||
|
||||
listingHref === referredRelativePath + "index.html";
|
||||
return isListingReferrer;
|
||||
});
|
||||
|
||||
if (referrerListing) {
|
||||
// Try to use the referrer if possible
|
||||
activateCategories(referrerListing);
|
||||
} else if (listingHrefs.length > 0) {
|
||||
// Otherwise, just fall back to the first listing
|
||||
activateCategories(listingHrefs[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hasTitleCategories()) {
|
||||
findAndActivateCategories();
|
||||
}
|
||||
|
||||
const findNearestParentListing = (href, listingHrefs) => {
|
||||
if (!href || !listingHrefs) {
|
||||
return undefined;
|
||||
}
|
||||
// Look up the tree for a nearby linting and use that if we find one
|
||||
const relativeParts = href.substring(1).split("/");
|
||||
while (relativeParts.length > 0) {
|
||||
const path = relativeParts.join("/");
|
||||
for (const listingHref of listingHrefs) {
|
||||
if (listingHref.startsWith(path)) {
|
||||
return listingHref;
|
||||
}
|
||||
}
|
||||
relativeParts.pop();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const manageSidebarVisiblity = (el, placeholderDescriptor) => {
|
||||
let isVisible = true;
|
||||
let elRect;
|
||||
|
||||
return (hiddenRegions) => {
|
||||
if (el === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the last element of the TOC
|
||||
const lastChildEl = el.lastElementChild;
|
||||
|
||||
if (lastChildEl) {
|
||||
// Converts the sidebar to a menu
|
||||
const convertToMenu = () => {
|
||||
for (const child of el.children) {
|
||||
child.style.opacity = 0;
|
||||
child.style.overflow = "hidden";
|
||||
child.style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
nexttick(() => {
|
||||
const toggleContainer = window.document.createElement("div");
|
||||
toggleContainer.style.width = "100%";
|
||||
toggleContainer.classList.add("zindex-over-content");
|
||||
toggleContainer.classList.add("quarto-sidebar-toggle");
|
||||
toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom
|
||||
toggleContainer.id = placeholderDescriptor.id;
|
||||
toggleContainer.style.position = "fixed";
|
||||
|
||||
const toggleIcon = window.document.createElement("i");
|
||||
toggleIcon.classList.add("quarto-sidebar-toggle-icon");
|
||||
toggleIcon.classList.add("bi");
|
||||
toggleIcon.classList.add("bi-caret-down-fill");
|
||||
|
||||
const toggleTitle = window.document.createElement("div");
|
||||
const titleEl = window.document.body.querySelector(
|
||||
placeholderDescriptor.titleSelector
|
||||
);
|
||||
if (titleEl) {
|
||||
toggleTitle.append(
|
||||
titleEl.textContent || titleEl.innerText,
|
||||
toggleIcon
|
||||
);
|
||||
}
|
||||
toggleTitle.classList.add("zindex-over-content");
|
||||
toggleTitle.classList.add("quarto-sidebar-toggle-title");
|
||||
toggleContainer.append(toggleTitle);
|
||||
|
||||
const toggleContents = window.document.createElement("div");
|
||||
toggleContents.classList = el.classList;
|
||||
toggleContents.classList.add("zindex-over-content");
|
||||
toggleContents.classList.add("quarto-sidebar-toggle-contents");
|
||||
for (const child of el.children) {
|
||||
if (child.id === "toc-title") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clone = child.cloneNode(true);
|
||||
clone.style.opacity = 1;
|
||||
clone.style.pointerEvents = null;
|
||||
clone.style.display = null;
|
||||
toggleContents.append(clone);
|
||||
}
|
||||
toggleContents.style.height = "0px";
|
||||
const positionToggle = () => {
|
||||
// position the element (top left of parent, same width as parent)
|
||||
if (!elRect) {
|
||||
elRect = el.getBoundingClientRect();
|
||||
}
|
||||
toggleContainer.style.left = `${elRect.left}px`;
|
||||
toggleContainer.style.top = `${elRect.top}px`;
|
||||
toggleContainer.style.width = `${elRect.width}px`;
|
||||
};
|
||||
positionToggle();
|
||||
|
||||
toggleContainer.append(toggleContents);
|
||||
el.parentElement.prepend(toggleContainer);
|
||||
|
||||
// Process clicks
|
||||
let tocShowing = false;
|
||||
// Allow the caller to control whether this is dismissed
|
||||
// when it is clicked (e.g. sidebar navigation supports
|
||||
// opening and closing the nav tree, so don't dismiss on click)
|
||||
const clickEl = placeholderDescriptor.dismissOnClick
|
||||
? toggleContainer
|
||||
: toggleTitle;
|
||||
|
||||
const closeToggle = () => {
|
||||
if (tocShowing) {
|
||||
toggleContainer.classList.remove("expanded");
|
||||
toggleContents.style.height = "0px";
|
||||
tocShowing = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get rid of any expanded toggle if the user scrolls
|
||||
window.document.addEventListener(
|
||||
"scroll",
|
||||
throttle(() => {
|
||||
closeToggle();
|
||||
}, 50)
|
||||
);
|
||||
|
||||
// Handle positioning of the toggle
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
throttle(() => {
|
||||
elRect = undefined;
|
||||
positionToggle();
|
||||
}, 50)
|
||||
);
|
||||
|
||||
window.addEventListener("quarto-hrChanged", () => {
|
||||
elRect = undefined;
|
||||
});
|
||||
|
||||
// Process the click
|
||||
clickEl.onclick = () => {
|
||||
if (!tocShowing) {
|
||||
toggleContainer.classList.add("expanded");
|
||||
toggleContents.style.height = null;
|
||||
tocShowing = true;
|
||||
} else {
|
||||
closeToggle();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Converts a sidebar from a menu back to a sidebar
|
||||
const convertToSidebar = () => {
|
||||
for (const child of el.children) {
|
||||
child.style.opacity = 1;
|
||||
child.style.overflow = null;
|
||||
child.style.pointerEvents = null;
|
||||
}
|
||||
|
||||
const placeholderEl = window.document.getElementById(
|
||||
placeholderDescriptor.id
|
||||
);
|
||||
if (placeholderEl) {
|
||||
placeholderEl.remove();
|
||||
}
|
||||
|
||||
el.classList.remove("rollup");
|
||||
};
|
||||
|
||||
if (isReaderMode()) {
|
||||
convertToMenu();
|
||||
isVisible = false;
|
||||
} else {
|
||||
// Find the top and bottom o the element that is being managed
|
||||
const elTop = el.offsetTop;
|
||||
const elBottom =
|
||||
elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;
|
||||
|
||||
if (!isVisible) {
|
||||
// If the element is current not visible reveal if there are
|
||||
// no conflicts with overlay regions
|
||||
if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {
|
||||
convertToSidebar();
|
||||
isVisible = true;
|
||||
}
|
||||
} else {
|
||||
// If the element is visible, hide it if it conflicts with overlay regions
|
||||
// and insert a placeholder toggle (or if we're in reader mode)
|
||||
if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {
|
||||
convertToMenu();
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');
|
||||
for (const tabEl of tabEls) {
|
||||
const id = tabEl.getAttribute("data-bs-target");
|
||||
if (id) {
|
||||
const columnEl = document.querySelector(
|
||||
`${id} .column-margin, .tabset-margin-content`
|
||||
);
|
||||
if (columnEl)
|
||||
tabEl.addEventListener("shown.bs.tab", function (event) {
|
||||
const el = event.srcElement;
|
||||
if (el) {
|
||||
const visibleCls = `${el.id}-margin-content`;
|
||||
// walk up until we find a parent tabset
|
||||
let panelTabsetEl = el.parentElement;
|
||||
while (panelTabsetEl) {
|
||||
if (panelTabsetEl.classList.contains("panel-tabset")) {
|
||||
break;
|
||||
}
|
||||
panelTabsetEl = panelTabsetEl.parentElement;
|
||||
}
|
||||
|
||||
if (panelTabsetEl) {
|
||||
const prevSib = panelTabsetEl.previousElementSibling;
|
||||
if (
|
||||
prevSib &&
|
||||
prevSib.classList.contains("tabset-margin-container")
|
||||
) {
|
||||
const childNodes = prevSib.querySelectorAll(
|
||||
".tabset-margin-content"
|
||||
);
|
||||
for (const childEl of childNodes) {
|
||||
if (childEl.classList.contains(visibleCls)) {
|
||||
childEl.classList.remove("collapse");
|
||||
} else {
|
||||
childEl.classList.add("collapse");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layoutMarginEls();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Manage the visibility of the toc and the sidebar
|
||||
const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {
|
||||
id: "quarto-toc-toggle",
|
||||
titleSelector: "#toc-title",
|
||||
dismissOnClick: true,
|
||||
});
|
||||
const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {
|
||||
id: "quarto-sidebarnav-toggle",
|
||||
titleSelector: ".title",
|
||||
dismissOnClick: false,
|
||||
});
|
||||
let tocLeftScrollVisibility;
|
||||
if (leftTocEl) {
|
||||
tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {
|
||||
id: "quarto-lefttoc-toggle",
|
||||
titleSelector: "#toc-title",
|
||||
dismissOnClick: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Find the first element that uses formatting in special columns
|
||||
const conflictingEls = window.document.body.querySelectorAll(
|
||||
'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'
|
||||
);
|
||||
|
||||
// Filter all the possibly conflicting elements into ones
|
||||
// the do conflict on the left or ride side
|
||||
const arrConflictingEls = Array.from(conflictingEls);
|
||||
const leftSideConflictEls = arrConflictingEls.filter((el) => {
|
||||
if (el.tagName === "ASIDE") {
|
||||
return false;
|
||||
}
|
||||
return Array.from(el.classList).find((className) => {
|
||||
return (
|
||||
className !== "column-body" &&
|
||||
className.startsWith("column-") &&
|
||||
!className.endsWith("right") &&
|
||||
!className.endsWith("container") &&
|
||||
className !== "column-margin"
|
||||
);
|
||||
});
|
||||
});
|
||||
const rightSideConflictEls = arrConflictingEls.filter((el) => {
|
||||
if (el.tagName === "ASIDE") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasMarginCaption = Array.from(el.classList).find((className) => {
|
||||
return className == "margin-caption";
|
||||
});
|
||||
if (hasMarginCaption) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Array.from(el.classList).find((className) => {
|
||||
return (
|
||||
className !== "column-body" &&
|
||||
!className.endsWith("container") &&
|
||||
className.startsWith("column-") &&
|
||||
!className.endsWith("left")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const kOverlapPaddingSize = 10;
|
||||
function toRegions(els) {
|
||||
return els.map((el) => {
|
||||
const boundRect = el.getBoundingClientRect();
|
||||
const top =
|
||||
boundRect.top +
|
||||
document.documentElement.scrollTop -
|
||||
kOverlapPaddingSize;
|
||||
return {
|
||||
top,
|
||||
bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let hasObserved = false;
|
||||
const visibleItemObserver = (els) => {
|
||||
let visibleElements = [...els];
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries, _observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (visibleElements.indexOf(entry.target) === -1) {
|
||||
visibleElements.push(entry.target);
|
||||
}
|
||||
} else {
|
||||
visibleElements = visibleElements.filter((visibleEntry) => {
|
||||
return visibleEntry !== entry;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasObserved) {
|
||||
hideOverlappedSidebars();
|
||||
}
|
||||
hasObserved = true;
|
||||
},
|
||||
{}
|
||||
);
|
||||
els.forEach((el) => {
|
||||
intersectionObserver.observe(el);
|
||||
});
|
||||
|
||||
return {
|
||||
getVisibleEntries: () => {
|
||||
return visibleElements;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const rightElementObserver = visibleItemObserver(rightSideConflictEls);
|
||||
const leftElementObserver = visibleItemObserver(leftSideConflictEls);
|
||||
|
||||
const hideOverlappedSidebars = () => {
|
||||
marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));
|
||||
sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));
|
||||
if (tocLeftScrollVisibility) {
|
||||
tocLeftScrollVisibility(
|
||||
toRegions(leftElementObserver.getVisibleEntries())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.quartoToggleReader = () => {
|
||||
// Applies a slow class (or removes it)
|
||||
// to update the transition speed
|
||||
const slowTransition = (slow) => {
|
||||
const manageTransition = (id, slow) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
if (slow) {
|
||||
el.classList.add("slow");
|
||||
} else {
|
||||
el.classList.remove("slow");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
manageTransition("TOC", slow);
|
||||
manageTransition("quarto-sidebar", slow);
|
||||
};
|
||||
const readerMode = !isReaderMode();
|
||||
setReaderModeValue(readerMode);
|
||||
|
||||
// If we're entering reader mode, slow the transition
|
||||
if (readerMode) {
|
||||
slowTransition(readerMode);
|
||||
}
|
||||
highlightReaderToggle(readerMode);
|
||||
hideOverlappedSidebars();
|
||||
|
||||
// If we're exiting reader mode, restore the non-slow transition
|
||||
if (!readerMode) {
|
||||
slowTransition(!readerMode);
|
||||
}
|
||||
};
|
||||
|
||||
const highlightReaderToggle = (readerMode) => {
|
||||
const els = document.querySelectorAll(".quarto-reader-toggle");
|
||||
if (els) {
|
||||
els.forEach((el) => {
|
||||
if (readerMode) {
|
||||
el.classList.add("reader");
|
||||
} else {
|
||||
el.classList.remove("reader");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setReaderModeValue = (val) => {
|
||||
if (window.location.protocol !== "file:") {
|
||||
window.localStorage.setItem("quarto-reader-mode", val);
|
||||
} else {
|
||||
localReaderMode = val;
|
||||
}
|
||||
};
|
||||
|
||||
const isReaderMode = () => {
|
||||
if (window.location.protocol !== "file:") {
|
||||
return window.localStorage.getItem("quarto-reader-mode") === "true";
|
||||
} else {
|
||||
return localReaderMode;
|
||||
}
|
||||
};
|
||||
let localReaderMode = null;
|
||||
|
||||
const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");
|
||||
const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;
|
||||
|
||||
// Walk the TOC and collapse/expand nodes
|
||||
// Nodes are expanded if:
|
||||
// - they are top level
|
||||
// - they have children that are 'active' links
|
||||
// - they are directly below an link that is 'active'
|
||||
const walk = (el, depth) => {
|
||||
// Tick depth when we enter a UL
|
||||
if (el.tagName === "UL") {
|
||||
depth = depth + 1;
|
||||
}
|
||||
|
||||
// It this is active link
|
||||
let isActiveNode = false;
|
||||
if (el.tagName === "A" && el.classList.contains("active")) {
|
||||
isActiveNode = true;
|
||||
}
|
||||
|
||||
// See if there is an active child to this element
|
||||
let hasActiveChild = false;
|
||||
for (const child of el.children) {
|
||||
hasActiveChild = walk(child, depth) || hasActiveChild;
|
||||
}
|
||||
|
||||
// Process the collapse state if this is an UL
|
||||
if (el.tagName === "UL") {
|
||||
if (tocOpenDepth === -1 && depth > 1) {
|
||||
// toc-expand: false
|
||||
el.classList.add("collapse");
|
||||
} else if (
|
||||
depth <= tocOpenDepth ||
|
||||
hasActiveChild ||
|
||||
prevSiblingIsActiveLink(el)
|
||||
) {
|
||||
el.classList.remove("collapse");
|
||||
} else {
|
||||
el.classList.add("collapse");
|
||||
}
|
||||
|
||||
// untick depth when we leave a UL
|
||||
depth = depth - 1;
|
||||
}
|
||||
return hasActiveChild || isActiveNode;
|
||||
};
|
||||
|
||||
// walk the TOC and expand / collapse any items that should be shown
|
||||
if (tocEl) {
|
||||
updateActiveLink();
|
||||
walk(tocEl, 0);
|
||||
}
|
||||
|
||||
// Throttle the scroll event and walk peridiocally
|
||||
window.document.addEventListener(
|
||||
"scroll",
|
||||
throttle(() => {
|
||||
if (tocEl) {
|
||||
updateActiveLink();
|
||||
walk(tocEl, 0);
|
||||
}
|
||||
if (!isReaderMode()) {
|
||||
hideOverlappedSidebars();
|
||||
}
|
||||
}, 5)
|
||||
);
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
throttle(() => {
|
||||
if (tocEl) {
|
||||
updateActiveLink();
|
||||
walk(tocEl, 0);
|
||||
}
|
||||
if (!isReaderMode()) {
|
||||
hideOverlappedSidebars();
|
||||
}
|
||||
}, 10)
|
||||
);
|
||||
hideOverlappedSidebars();
|
||||
highlightReaderToggle(isReaderMode());
|
||||
});
|
||||
|
||||
tabsets.init();
|
||||
|
||||
function throttle(func, wait) {
|
||||
let waiting = false;
|
||||
return function () {
|
||||
if (!waiting) {
|
||||
func.apply(this, arguments);
|
||||
waiting = true;
|
||||
setTimeout(function () {
|
||||
waiting = false;
|
||||
}, wait);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function nexttick(func) {
|
||||
return setTimeout(func, 0);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// grouped tabsets
|
||||
|
||||
export function init() {
|
||||
window.addEventListener("pageshow", (_event) => {
|
||||
function getTabSettings() {
|
||||
const data = localStorage.getItem("quarto-persistent-tabsets-data");
|
||||
if (!data) {
|
||||
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
|
||||
return {};
|
||||
}
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
function setTabSettings(data) {
|
||||
localStorage.setItem(
|
||||
"quarto-persistent-tabsets-data",
|
||||
JSON.stringify(data)
|
||||
);
|
||||
}
|
||||
|
||||
function setTabState(groupName, groupValue) {
|
||||
const data = getTabSettings();
|
||||
data[groupName] = groupValue;
|
||||
setTabSettings(data);
|
||||
}
|
||||
|
||||
function toggleTab(tab, active) {
|
||||
const tabPanelId = tab.getAttribute("aria-controls");
|
||||
const tabPanel = document.getElementById(tabPanelId);
|
||||
if (active) {
|
||||
tab.classList.add("active");
|
||||
tabPanel.classList.add("active");
|
||||
} else {
|
||||
tab.classList.remove("active");
|
||||
tabPanel.classList.remove("active");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll(selectedGroup, selectorsToSync) {
|
||||
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
|
||||
const active = selectedGroup === thisGroup;
|
||||
for (const tab of tabs) {
|
||||
toggleTab(tab, active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findSelectorsToSyncByLanguage() {
|
||||
const result = {};
|
||||
const tabs = Array.from(
|
||||
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
|
||||
);
|
||||
for (const item of tabs) {
|
||||
const div = item.parentElement.parentElement.parentElement;
|
||||
const group = div.getAttribute("data-group");
|
||||
if (!result[group]) {
|
||||
result[group] = {};
|
||||
}
|
||||
const selectorsToSync = result[group];
|
||||
const value = item.innerHTML;
|
||||
if (!selectorsToSync[value]) {
|
||||
selectorsToSync[value] = [];
|
||||
}
|
||||
selectorsToSync[value].push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setupSelectorSync() {
|
||||
const selectorsToSync = findSelectorsToSyncByLanguage();
|
||||
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
|
||||
Object.entries(tabSetsByValue).forEach(([value, items]) => {
|
||||
items.forEach((item) => {
|
||||
item.addEventListener("click", (_event) => {
|
||||
setTabState(group, value);
|
||||
toggleAll(value, selectorsToSync[group]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return selectorsToSync;
|
||||
}
|
||||
|
||||
const selectorsToSync = setupSelectorSync();
|
||||
for (const [group, selectedName] of Object.entries(getTabSettings())) {
|
||||
const selectors = selectorsToSync[group];
|
||||
// it's possible that stale state gives us empty selections, so we explicitly check here.
|
||||
if (selectors) {
|
||||
toggleAll(selectedName, selectors);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
|
||||