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>
This commit is contained in:
Cesa-V
2026-06-17 10:35:57 -03:00
parent 77b7ba11b7
commit 953758497d
92 changed files with 644 additions and 10336 deletions

Binary file not shown.

Binary file not shown.

View 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 (C1C13); 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*

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View 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
1 modelo acuracia kappa sensibilidade_macro especificidade_macro f1_macro
2 SVM linear 0.40153846153846157 0.3516666666666666 0.40153846153846157 0.9501282051282052 0.40929682290313046
3 SVM radial ajustado 0.38 0.3283333333333333 0.38 0.9483333333333333 0.3905066510222767
4 PCA + SVM radial 0.3676923076923077 0.31499999999999995 0.3676923076923077 0.9473076923076923 0.37836144953604534
5 Random Forest 0.35692307692307695 0.3033333333333333 0.35692307692307695 0.9464102564102563 0.38061565488169397
6 SVM radial 0.15846153846153846 0.08833333333333333 0.15846153846153846 0.9298717948717948 0.22329423850231678

View File

@@ -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
1 modelo acuracia kappa sensibilidade_macro especificidade_macro f1_macro
2 SVM linear 0.4 0.35 0.4 0.95 0.401945495575665
3 Random Forest 0.37435897435897436 0.3222222222222222 0.37435897435897436 0.9478632478632478 0.3688098868848946
4 PCA + SVM radial 0.358974358974359 0.3055555555555555 0.358974358974359 0.9465811965811965 0.36964765748713724
5 SVM radial ajustado 0.35384615384615387 0.3 0.35384615384615387 0.9461538461538461 0.35196493343650537
6 SVM radial 0.14358974358974358 0.0722222222222222 0.14358974358974358 0.9286324786324787 0.192826555606469

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 */

View File

@@ -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);
}

View File

@@ -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);
}
}
});
}

View File

@@ -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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View 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
1 modelo acuracia kappa sensibilidade_macro especificidade_macro f1_macro
2 SVM linear 0.36298074633403044 0.30995815445239716 0.3633603238866397 0.9469207558795383 0.3834656395997568
3 SVM radial ajustado 0.35649417578146686 0.3029397087363448 0.35684210526315785 0.9463815622092584 0.3720660941606715
4 Random Forest 0.3284677302142518 0.27258541671650627 0.32878542510121456 0.9440465083621263 0.3405924694392964
5 PCA + SVM radial 0.32364187317626886 0.26735133180485987 0.32388663967611336 0.9436442293130114 0.35932566977715646
6 SVM radial 0.14187017563340923 0.0708961467200726 0.14327935222672064 0.9285272695006503 0.17432376344764106

View File

@@ -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
1 modelo acuracia kappa sensibilidade_macro especificidade_macro f1_macro
2 SVM linear 0.37362637362637363 0.3214285714285714 0.37362637362637363 0.9478021978021978 0.36460524043005765
3 SVM radial ajustado 0.36538461538461536 0.31249999999999994 0.36538461538461536 0.9471153846153846 0.36302238351838
4 Random Forest 0.34065934065934067 0.2857142857142857 0.34065934065934067 0.945054945054945 0.3405200571756793
5 PCA + SVM radial 0.31043956043956045 0.25297619047619047 0.31043956043956045 0.94253663003663 0.3160780319263731
6 SVM radial 0.14835164835164835 0.07738095238095237 0.14835164835164835 0.9290293040293041 0.16727440711436561

View 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")
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 */

View File

@@ -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);
}

View File

@@ -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);
}
}
});
}

View File

@@ -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}

File diff suppressed because one or more lines are too long