Initial commit
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Arquivos grandes e dados brutos
|
||||||
|
*.zip
|
||||||
|
RGB/
|
||||||
|
|
||||||
|
# Materiais fora do escopo deste repositorio
|
||||||
|
artigo_KC390*
|
||||||
|
artigo_promethee*
|
||||||
|
|
||||||
|
# Configuracoes locais de assistentes
|
||||||
|
.claude/
|
||||||
4
README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Analise SVM RGB CEAO 2026
|
||||||
|
|
||||||
|
Repositorio de trabalho para organizacao, processamento e documentacao das analises de classificacao de imagens RGB com SVM no contexto dos dados CEAO 2026. O projeto reune apresentacoes, artigos, referencias bibliograficas, resultados gerados e arquivos Quarto/LaTeX relacionados ao desenvolvimento metodologico e a comunicacao dos resultados.
|
||||||
|
|
||||||
47
Referencias.bib
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
% REFERÊNCIAS DO NOSSO ARTIGO
|
||||||
|
|
||||||
|
|
||||||
|
@article{toydas2021,
|
||||||
|
title={Air refueling optimisation for more agile and efficient military deployment operations},
|
||||||
|
author={Toydas, M. and Malyemez, C.},
|
||||||
|
journal={The Aeronautical Journal},
|
||||||
|
volume={126},
|
||||||
|
number={1296},
|
||||||
|
pages={365--380},
|
||||||
|
year={2022},
|
||||||
|
note={Publicado online em 19 de agosto de 2021},
|
||||||
|
doi={10.1017/aer.2021.69}
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{yamani1994,
|
||||||
|
title={A new closed form solution to determine the fuel load for a cruise mission},
|
||||||
|
author={Yamani, K.},
|
||||||
|
howpublished={AIAA Paper 1994-3503},
|
||||||
|
year={1994}
|
||||||
|
}
|
||||||
|
|
||||||
|
@manual{embraer_kc390,
|
||||||
|
title={KC-390 Millennium: Multimission Military Transport Aircraft},
|
||||||
|
author={{Embraer}},
|
||||||
|
organization={Embraer Defense {\&} Security},
|
||||||
|
address={S{\~a}o Jos{\'e} dos Campos},
|
||||||
|
year={2023},
|
||||||
|
url={https://defense.embraer.com/kc-390}
|
||||||
|
}
|
||||||
|
|
||||||
|
@manual{gams,
|
||||||
|
title={GAMS -- A User's Guide},
|
||||||
|
author={{GAMS Development Corporation}},
|
||||||
|
address={Washington DC},
|
||||||
|
year={2023},
|
||||||
|
url={https://www.gams.com}
|
||||||
|
}
|
||||||
|
|
||||||
|
@manual{r_core,
|
||||||
|
title={R: A Language and Environment for Statistical Computing},
|
||||||
|
author={{R Core Team}},
|
||||||
|
organization={R Foundation for Statistical Computing},
|
||||||
|
address={Vienna, Austria},
|
||||||
|
year={2024},
|
||||||
|
url={https://www.R-project.org/}
|
||||||
|
}
|
||||||
120
Referencias_SVM.bib
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
% ======================================================================
|
||||||
|
% Referências -- Artigo: Classificação de Imagens RGB com SVM
|
||||||
|
% e Atributos Extraídos por Transfer Learning (MobileNetV2)
|
||||||
|
% Curso CEAO-802 | Instituto Tecnológico de Aeronáutica
|
||||||
|
% ======================================================================
|
||||||
|
|
||||||
|
% -------- Referência-base do trabalho (galoa / SBSR 2023) --------
|
||||||
|
|
||||||
|
@inproceedings{lacerda2023,
|
||||||
|
author = {Lacerda, Marielcio Gon{\c{c}}alves and
|
||||||
|
Habermann, Mateus and
|
||||||
|
Lacerda, Camila Souza dos Anjos and
|
||||||
|
Roos, Daniel Rodrigues and
|
||||||
|
K{\"o}rting, Thales Sehn and
|
||||||
|
Kux, Hermann Johann Heinrich},
|
||||||
|
title = {Classifica{\c{c}}{\~a}o de Imagens Utilizando Imagens {RGB}
|
||||||
|
e Termal Obtidas por {ARPS} de Pequeno Porte},
|
||||||
|
booktitle = {Anais do XX Simp{\'o}sio Brasileiro de Sensoriamento Remoto (SBSR)},
|
||||||
|
year = {2023},
|
||||||
|
address = {Florian{\'o}polis, SC, Brasil},
|
||||||
|
pages = {3368--3371},
|
||||||
|
url = {https://proceedings.science/p/164924}
|
||||||
|
}
|
||||||
|
|
||||||
|
% -------- Teoria SVM --------
|
||||||
|
|
||||||
|
@book{vapnik1995,
|
||||||
|
author = {Vapnik, Vladimir N.},
|
||||||
|
title = {The Nature of Statistical Learning Theory},
|
||||||
|
publisher = {Springer},
|
||||||
|
address = {New York},
|
||||||
|
year = {1995},
|
||||||
|
doi = {10.1007/978-1-4757-2440-0}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{cortes1995,
|
||||||
|
author = {Cortes, Corinna and Vapnik, Vladimir},
|
||||||
|
title = {Support-Vector Networks},
|
||||||
|
journal = {Machine Learning},
|
||||||
|
volume = {20},
|
||||||
|
number = {3},
|
||||||
|
pages = {273--297},
|
||||||
|
year = {1995},
|
||||||
|
doi = {10.1007/BF00994018}
|
||||||
|
}
|
||||||
|
|
||||||
|
% -------- SVM em Sensoriamento Remoto --------
|
||||||
|
|
||||||
|
@article{melgani2004,
|
||||||
|
author = {Melgani, Farid and Bruzzone, Lorenzo},
|
||||||
|
title = {Classification of Hyperspectral Remote Sensing Images
|
||||||
|
with Support Vector Machines},
|
||||||
|
journal = {{IEEE} Transactions on Geoscience and Remote Sensing},
|
||||||
|
volume = {42},
|
||||||
|
number = {8},
|
||||||
|
pages = {1778--1790},
|
||||||
|
year = {2004},
|
||||||
|
doi = {10.1109/TGRS.2004.831865}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{campsvalls2005,
|
||||||
|
author = {Camps-Valls, Gustavo and Bruzzone, Lorenzo},
|
||||||
|
title = {Kernel-Based Methods for Hyperspectral Image Classification},
|
||||||
|
journal = {{IEEE} Transactions on Geoscience and Remote Sensing},
|
||||||
|
volume = {43},
|
||||||
|
number = {6},
|
||||||
|
pages = {1351--1362},
|
||||||
|
year = {2005},
|
||||||
|
doi = {10.1109/TGRS.2005.846154}
|
||||||
|
}
|
||||||
|
|
||||||
|
% -------- Transfer Learning / Redes Convolucionais --------
|
||||||
|
|
||||||
|
@inproceedings{sandler2018,
|
||||||
|
author = {Sandler, Mark and Howard, Andrew and Zhu, Menglong and
|
||||||
|
Zhmoginov, Andrey and Chen, Liang-Chieh},
|
||||||
|
title = {{MobileNetV2}: Inverted Residuals and Linear Bottlenecks},
|
||||||
|
booktitle = {Proceedings of the {IEEE} Conference on Computer Vision
|
||||||
|
and Pattern Recognition (CVPR)},
|
||||||
|
year = {2018},
|
||||||
|
pages = {4510--4520},
|
||||||
|
doi = {10.1109/CVPR.2018.00474}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{cheng2020,
|
||||||
|
author = {Cheng, Gong and Xie, Xingxing and Han, Junwei and
|
||||||
|
Guo, Lei and Xia, Gui-Song},
|
||||||
|
title = {Remote Sensing Image Scene Classification Meets Deep Learning:
|
||||||
|
Challenges, Methods, Benchmarks, and Opportunities},
|
||||||
|
journal = {{IEEE} Journal of Selected Topics in Applied Earth Observations
|
||||||
|
and Remote Sensing},
|
||||||
|
volume = {13},
|
||||||
|
pages = {3735--3756},
|
||||||
|
year = {2020},
|
||||||
|
doi = {10.1109/JSTARS.2020.3005403}
|
||||||
|
}
|
||||||
|
|
||||||
|
% -------- Random Forest --------
|
||||||
|
|
||||||
|
@article{breiman2001,
|
||||||
|
author = {Breiman, Leo},
|
||||||
|
title = {Random Forests},
|
||||||
|
journal = {Machine Learning},
|
||||||
|
volume = {45},
|
||||||
|
number = {1},
|
||||||
|
pages = {5--32},
|
||||||
|
year = {2001},
|
||||||
|
doi = {10.1023/A:1010933404324}
|
||||||
|
}
|
||||||
|
|
||||||
|
% -------- Linguagem R --------
|
||||||
|
|
||||||
|
@manual{r_core,
|
||||||
|
title = {R: A Language and Environment for Statistical Computing},
|
||||||
|
author = {{R Core Team}},
|
||||||
|
organization = {R Foundation for Statistical Computing},
|
||||||
|
address = {Vienna, Austria},
|
||||||
|
year = {2024},
|
||||||
|
url = {https://www.R-project.org/}
|
||||||
|
}
|
||||||
BIN
SVM GUARA - 802.pptx
Normal file
BIN
SVM GUARA 802.pdf
Normal file
2931
apresentacao_svm_rgb.html
Normal file
BIN
apresentacao_svm_rgb.pptx
Normal file
367
apresentacao_svm_rgb.qmd
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
---
|
||||||
|
title: "Classificacao de Imagens RGB com SVM"
|
||||||
|
subtitle: "CEAO-802 — Metodos de Analise de Dados"
|
||||||
|
author:
|
||||||
|
- "1T Generoso"
|
||||||
|
- "1T João Marcos"
|
||||||
|
- "1T Vitor Cesa"
|
||||||
|
format:
|
||||||
|
pptx:
|
||||||
|
fig-width: 9
|
||||||
|
fig-height: 4.5
|
||||||
|
execute:
|
||||||
|
warning: false
|
||||||
|
message: false
|
||||||
|
echo: false
|
||||||
|
---
|
||||||
|
|
||||||
|
```{r setup}
|
||||||
|
library(tidyverse)
|
||||||
|
library(knitr)
|
||||||
|
library(scales)
|
||||||
|
|
||||||
|
resultados_holdout <- read_csv("outputs/resultados_modelos_rgb.csv", show_col_types = FALSE)
|
||||||
|
resultados_kfold <- read_csv("outputs/resultados_kfold_rgb.csv", show_col_types = FALSE)
|
||||||
|
importancia_df <- read_csv("outputs/importancia_variaveis_rgb.csv", show_col_types = FALSE)
|
||||||
|
|
||||||
|
cores_modelos <- c(
|
||||||
|
"SVM linear" = "#2C7FB8",
|
||||||
|
"SVM radial" = "#7FCDBB",
|
||||||
|
"SVM radial ajustado" = "#41B6C4",
|
||||||
|
"PCA + SVM radial" = "#1D91C0",
|
||||||
|
"Random Forest" = "#225EA8"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
# Introducao e Dataset
|
||||||
|
|
||||||
|
## Contexto do Problema
|
||||||
|
|
||||||
|
- Imagens aéreas coletadas por **drone** sobre **Guaratinguetá (SP)**
|
||||||
|
- Objetivo: classificar cada recorte em uma das **13 classes de cobertura urbana**
|
||||||
|
- Dataset disponibiliza 4 modalidades: **RGB dia**, **RGB noite**, **TIR dia**, **TIR noite**
|
||||||
|
- Este trabalho utiliza somente as imagens **RGB**
|
||||||
|
- As imagens já vinham **pré-rotuladas** pelo nome do arquivo
|
||||||
|
|
||||||
|
Exemplo: `C10_2023_06_M_01_RGB.tiff` → classe **C10 (Piscina)**, junho 2023, manhã
|
||||||
|
|
||||||
|
## As 13 Classes
|
||||||
|
|
||||||
|
:::: {.columns}
|
||||||
|
|
||||||
|
::: {.column width="50%"}
|
||||||
|
| Classe | Descrição |
|
||||||
|
|:------:|-----------|
|
||||||
|
| 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 | Descrição |
|
||||||
|
|:------:|-----------|
|
||||||
|
| C8 | Telhado Metálico |
|
||||||
|
| C9 | Placa Fotovoltaica |
|
||||||
|
| C10 | Piscina |
|
||||||
|
| C11 | Pedra Sabão |
|
||||||
|
| C12 | Caixa D'água |
|
||||||
|
| C13 | Veículos |
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
||||||
|
|
||||||
|
## Amostra Balanceada
|
||||||
|
|
||||||
|
```{r dataset-plot}
|
||||||
|
tibble(
|
||||||
|
classe = factor(paste0("C", 1:13), levels = paste0("C", 1:13)),
|
||||||
|
n = 50
|
||||||
|
) |>
|
||||||
|
ggplot(aes(x = classe, y = n)) +
|
||||||
|
geom_col(fill = "#2C7FB8", width = 0.7) +
|
||||||
|
geom_text(aes(label = n), vjust = -0.5, size = 4, fontface = "bold") +
|
||||||
|
scale_y_continuous(limits = c(0, 62)) +
|
||||||
|
labs(
|
||||||
|
title = "50 imagens por classe — amostra estratificada e balanceada",
|
||||||
|
subtitle = "Dataset completo: 1.248 imagens TIFF | Amostra utilizada: 650 imagens",
|
||||||
|
x = "Classe", y = "Imagens na amostra"
|
||||||
|
) +
|
||||||
|
theme_minimal(base_size = 14) +
|
||||||
|
theme(panel.grid.major.x = element_blank())
|
||||||
|
```
|
||||||
|
|
||||||
|
# Metodologia
|
||||||
|
|
||||||
|
## Pipeline de Classificação
|
||||||
|
|
||||||
|
```{r pipeline, fig.height=3.2}
|
||||||
|
tibble(
|
||||||
|
x = 1:5,
|
||||||
|
label = c("Imagens\nRGB", "MobileNetV2\n1280 features",
|
||||||
|
"Normalização\n+ PCA (opt.)", "SVM / RF",
|
||||||
|
"Avaliação\nHoldout / K-Fold")
|
||||||
|
) |>
|
||||||
|
ggplot(aes(x = x, y = 1)) +
|
||||||
|
geom_point(size = 20, color = "#2C7FB8") +
|
||||||
|
geom_text(aes(label = label), size = 3.8, color = "white",
|
||||||
|
fontface = "bold", lineheight = 1.0) +
|
||||||
|
annotate("segment",
|
||||||
|
x = c(1.38,2.38,3.38,4.38), xend = c(1.62,2.62,3.62,4.62),
|
||||||
|
y = 1, yend = 1, linewidth = 1.4, color = "grey40",
|
||||||
|
arrow = arrow(length = unit(0.3, "cm"), type = "closed")) +
|
||||||
|
scale_x_continuous(limits = c(0.5, 5.5)) +
|
||||||
|
scale_y_continuous(limits = c(0.6, 1.4)) +
|
||||||
|
theme_void()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transfer Learning — MobileNetV2
|
||||||
|
|
||||||
|
:::: {.columns}
|
||||||
|
|
||||||
|
::: {.column width="55%"}
|
||||||
|
**O que é transfer learning?**
|
||||||
|
|
||||||
|
A rede MobileNetV2 foi treinada no **ImageNet** (1,2 milhão de imagens, 1000 categorias). Reutilizamos essa rede como **extrator de atributos**, sem retreiná-la.
|
||||||
|
|
||||||
|
- Imagem redimensionada para **224 × 224 px**
|
||||||
|
- Camada de classificação removida
|
||||||
|
- Saída: **vetor de 1280 valores** por imagem
|
||||||
|
- Cada valor representa um padrão visual aprendido
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: {.column width="45%"}
|
||||||
|
| | Tradicional | Transfer |
|
||||||
|
|--|:-----------:|:-------:|
|
||||||
|
| Nº features | ~60 | **1280** |
|
||||||
|
| Origem | Manual | CNN |
|
||||||
|
| Treinamento | Não | Pré-treinado |
|
||||||
|
| Padrões | Cor/textura | Alto nível |
|
||||||
|
|
||||||
|
Não é necessário treinar a rede do zero — apenas **reutilizamos** o que ela já aprendeu.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
||||||
|
|
||||||
|
## Código — Carregando o Modelo
|
||||||
|
|
||||||
|
```r
|
||||||
|
library(keras3)
|
||||||
|
|
||||||
|
# Carregar MobileNetV2 sem a camada de classificação
|
||||||
|
modelo_base <- application_mobilenet_v2(
|
||||||
|
include_top = FALSE, # remove a camada final de 1000 classes
|
||||||
|
weights = "imagenet", # pesos pré-treinados no ImageNet
|
||||||
|
pooling = "avg", # GlobalAveragePooling → vetor 1D
|
||||||
|
input_shape = c(224L, 224L, 3L)
|
||||||
|
)
|
||||||
|
# Saída: vetor de 1280 atributos por imagem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Código — Extraindo Features por Imagem
|
||||||
|
|
||||||
|
```r
|
||||||
|
# Para cada imagem da amostra:
|
||||||
|
img <- image_load(arquivo, target_size = c(224L, 224L))
|
||||||
|
arr <- image_to_array(img) # array 224x224x3
|
||||||
|
arr <- array_reshape(arr, c(1L, 224L, 224L, 3L)) # batch de 1 imagem
|
||||||
|
arr <- (arr / 127.5) - 1.0 # normalização padrão do MobileNetV2 [-1, 1]
|
||||||
|
|
||||||
|
feat <- predict(modelo_base, arr, verbose = 0L)
|
||||||
|
# feat: vetor de 1280 valores — atributos visuais de alto nível
|
||||||
|
```
|
||||||
|
|
||||||
|
Processamento em **lotes de 16 imagens** para eficiência, com **cache em disco** para evitar reprocessamento.
|
||||||
|
|
||||||
|
# Modelos Avaliados
|
||||||
|
|
||||||
|
## Os 5 Modelos
|
||||||
|
|
||||||
|
| # | Modelo | Tipo | Parâmetros |
|
||||||
|
|:--:|--------|:----:|-----------|
|
||||||
|
| 1 | **SVM linear** | Linear | `cost = 1` |
|
||||||
|
| 2 | **SVM radial** | RBF | `cost = 10`, `gamma = 0.01` |
|
||||||
|
| 3 | **SVM radial ajustado** | RBF | `cost` e `gamma` via grid search |
|
||||||
|
| 4 | **PCA + SVM radial** | RBF | PCA (≥ 95% variância) + RBF |
|
||||||
|
| 5 | **Random Forest** | Ensemble | 500 árvores |
|
||||||
|
|
||||||
|
**SVM** encontra o hiperplano que **maximiza a margem** entre as classes. Com kernel **RBF**, fronteiras não-lineares são possíveis via *kernel trick*.
|
||||||
|
|
||||||
|
## Código — Treinamento dos Modelos
|
||||||
|
|
||||||
|
```r
|
||||||
|
library(e1071); library(caret); library(randomForest)
|
||||||
|
|
||||||
|
# Pré-processamento: remover variância nula e normalizar
|
||||||
|
preproc <- preProcess(x_treino, method = c("center", "scale"))
|
||||||
|
x_treino_n <- predict(preproc, x_treino)
|
||||||
|
|
||||||
|
# Modelo 1: SVM linear
|
||||||
|
modelo_linear <- svm(x = x_treino_n, y = y_treino,
|
||||||
|
kernel = "linear", cost = 1, scale = FALSE)
|
||||||
|
|
||||||
|
# Modelo 3: Ajuste de hiperparâmetros (grid search com CV interna)
|
||||||
|
ajuste <- tune.svm(x = x_treino_n, 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 = 5))
|
||||||
|
|
||||||
|
# Modelo 5: Random Forest
|
||||||
|
modelo_rf <- randomForest(x = x_treino_n, y = y_treino,
|
||||||
|
ntree = 500, importance = TRUE)
|
||||||
|
```
|
||||||
|
|
||||||
|
# Avaliacao dos Modelos
|
||||||
|
|
||||||
|
## Estratégia 1 — Holdout 70/30
|
||||||
|
|
||||||
|
:::: {.columns}
|
||||||
|
|
||||||
|
::: {.column width="38%"}
|
||||||
|
**Divisão única e aleatória**
|
||||||
|
|
||||||
|
- 70% treino (~455 imagens)
|
||||||
|
- 30% teste (~195 imagens)
|
||||||
|
- Estratificada por classe
|
||||||
|
|
||||||
|
**Limitação:** com datasets menores, o resultado pode variar conforme a divisão escolhida.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: {.column width="62%"}
|
||||||
|
```{r holdout-plot}
|
||||||
|
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") +
|
||||||
|
coord_flip(ylim = c(0, 0.53)) +
|
||||||
|
scale_fill_manual(values = cores_modelos) +
|
||||||
|
labs(title = "Acurácia — Holdout 70/30", x = NULL, y = "Acurácia") +
|
||||||
|
theme_minimal(base_size = 13)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
||||||
|
|
||||||
|
## Estratégia 2 — Validação Cruzada K-Fold (k=5)
|
||||||
|
|
||||||
|
:::: {.columns}
|
||||||
|
|
||||||
|
::: {.column width="38%"}
|
||||||
|
**5 partições estratificadas**
|
||||||
|
|
||||||
|
- Cada parte usada **uma vez** como teste
|
||||||
|
- Acurácia final = **média das 5 rodadas**
|
||||||
|
- Toda imagem aparece no teste
|
||||||
|
|
||||||
|
**Vantagem:** estimativa mais **robusta** — não depende de uma única divisão.
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: {.column width="62%"}
|
||||||
|
```{r kfold-plot}
|
||||||
|
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") +
|
||||||
|
coord_flip(ylim = c(0, 0.53)) +
|
||||||
|
scale_fill_manual(values = cores_modelos) +
|
||||||
|
labs(title = "Acurácia média — K-Fold (k=5)", x = NULL, y = "Acurácia") +
|
||||||
|
theme_minimal(base_size = 13)
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
::::
|
||||||
|
|
||||||
|
## Código — Validação Cruzada K-Fold
|
||||||
|
|
||||||
|
```r
|
||||||
|
folds <- createFolds(dados$classe, k = 5, list = TRUE, returnTrain = FALSE)
|
||||||
|
|
||||||
|
for (fold_i in seq_len(5)) {
|
||||||
|
idx_teste <- folds[[fold_i]]
|
||||||
|
idx_treino <- setdiff(seq_len(nrow(dados)), idx_teste)
|
||||||
|
|
||||||
|
# Pré-processar apenas com dados do treino deste fold
|
||||||
|
pp <- preProcess(x[idx_treino, ], method = c("center", "scale"))
|
||||||
|
x_tr <- predict(pp, x[idx_treino, ])
|
||||||
|
x_te <- predict(pp, x[idx_teste, ])
|
||||||
|
y_tr <- dados$classe[idx_treino]
|
||||||
|
y_te <- dados$classe[idx_teste]
|
||||||
|
|
||||||
|
modelo <- svm(x = x_tr, y = y_tr, kernel = "linear", cost = 1)
|
||||||
|
pred <- predict(modelo, x_te)
|
||||||
|
cm <- confusionMatrix(pred, y_te)
|
||||||
|
|
||||||
|
metricas_folds[[fold_i]] <- extrair_metricas("SVM linear", cm)
|
||||||
|
}
|
||||||
|
# Média das 5 rodadas
|
||||||
|
resultados_kfold <- bind_rows(metricas_folds) |>
|
||||||
|
group_by(modelo) |> summarise(across(everything(), mean))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparação: Holdout vs K-Fold
|
||||||
|
|
||||||
|
```{r comparacao-plot, fig.height=5}
|
||||||
|
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(width = 0.72), width = 0.68) +
|
||||||
|
geom_text(aes(label = percent(acuracia, accuracy = 0.1)),
|
||||||
|
position = position_dodge(width = 0.72),
|
||||||
|
hjust = -0.1, size = 4) +
|
||||||
|
coord_flip(ylim = c(0, 0.55)) +
|
||||||
|
scale_fill_manual(values = c("Holdout 70/30" = "#2C7FB8",
|
||||||
|
"K-Fold (k=5)" = "#E06C00")) +
|
||||||
|
labs(title = "Holdout 70/30 vs K-Fold (k=5)",
|
||||||
|
x = NULL, y = "Acurácia", fill = NULL) +
|
||||||
|
theme_minimal(base_size = 13) +
|
||||||
|
theme(legend.position = "top")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variáveis Mais Importantes (Random Forest)
|
||||||
|
|
||||||
|
```{r importancia-plot, fig.height=5}
|
||||||
|
importancia_df |>
|
||||||
|
slice_head(n = 15) |>
|
||||||
|
ggplot(aes(x = reorder(variavel, importancia), y = importancia)) +
|
||||||
|
geom_col(fill = "#2C7FB8", width = 0.75) +
|
||||||
|
geom_text(aes(label = round(importancia, 2)), hjust = -0.15, size = 4) +
|
||||||
|
coord_flip(ylim = c(0, max(importancia_df$importancia[1:15]) * 1.18)) +
|
||||||
|
labs(
|
||||||
|
title = "Top 15 features mais importantes — Random Forest",
|
||||||
|
subtitle = "feat_XXXX = neurônio da camada final do MobileNetV2",
|
||||||
|
x = NULL, y = "Importância (Mean Decrease Gini)"
|
||||||
|
) +
|
||||||
|
theme_minimal(base_size = 13)
|
||||||
|
```
|
||||||
|
|
||||||
|
# Conclusao
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
```{r conclusao-calc}
|
||||||
|
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 de **`r percent(melhor_acc_kf, accuracy = 0.1)`** (k-fold)
|
||||||
|
- Acerto aleatório com 13 classes ≈ **7,7%** → resultado ~5× melhor
|
||||||
|
- Holdout e k-fold convergiram → estimativa **estável e confiável**
|
||||||
|
|
||||||
|
**Por que SVM linear ganhou do radial?**
|
||||||
|
|
||||||
|
Os 1280 atributos da MobileNetV2 já tornam as classes aproximadamente **linearmente separáveis** — o kernel RBF não agrega ganho relevante neste caso.
|
||||||
|
|
||||||
|
**Trabalhos futuros**
|
||||||
|
|
||||||
|
- Outros extratores CNN: EfficientNetB0, ResNet50, VGG19
|
||||||
|
- Incorporar canal **TIR** (térmico) ao vetor de atributos
|
||||||
|
- Aumentar amostra por classe (atualmente 50 imagens)
|
||||||
519
artigo_svm_rgb.tex
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
% ======================================================================
|
||||||
|
% Artigo -- Classificação de Imagens RGB com SVM e Transfer Learning
|
||||||
|
% Formato: SIGEport (IEEEtran, duas colunas), em português
|
||||||
|
% Compilar com: pdflatex artigo_svm_rgb.tex
|
||||||
|
% ======================================================================
|
||||||
|
|
||||||
|
\documentclass[a4paper]{SIGEport}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\usepackage[caption=false,font=footnotesize]{subfig}
|
||||||
|
\usepackage{geometry}
|
||||||
|
\usepackage{cite}
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{booktabs}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage[hyphens]{url}
|
||||||
|
\usepackage{microtype}
|
||||||
|
\usepackage{float}
|
||||||
|
\usepackage{placeins}
|
||||||
|
\usepackage{listings}
|
||||||
|
|
||||||
|
\lstset{
|
||||||
|
language=R,
|
||||||
|
basicstyle=\ttfamily\scriptsize,
|
||||||
|
breaklines=true,
|
||||||
|
frame=single,
|
||||||
|
xleftmargin=0.5em,
|
||||||
|
xrightmargin=0.5em
|
||||||
|
}
|
||||||
|
|
||||||
|
\geometry{top=20mm,bottom=20mm,left=18mm,right=12mm}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
\title{Classifica{\c{c}}{\~a}o de Imagens de Sensoriamento Remoto RGB
|
||||||
|
com SVM e Atributos Extraídos por \textit{Transfer Learning}}
|
||||||
|
|
||||||
|
\author{Jo{\~a}o Marcos de Oliveira Costa$^1$,
|
||||||
|
Eduardo Carlos Generoso J{\'u}nior$^1$,
|
||||||
|
Vitor Cesa$^1$,
|
||||||
|
Mateus Habermann$^1$
|
||||||
|
e Daniel Pamplona$^1$\\ % [VERIFICAR] afiliação de Mateus Habermann (IEAv/INPE no artigo galoa)
|
||||||
|
{\small $^1$Instituto Tecnol{\'o}gico de Aeron{\'a}utica (ITA),
|
||||||
|
S{\~a}o Jos{\'e} dos Campos/SP -- Brasil}\\
|
||||||
|
}
|
||||||
|
|
||||||
|
\maketitle
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\begin{resumo}
|
||||||
|
Este trabalho avalia o desempenho de classificadores baseados em
|
||||||
|
\textit{Support Vector Machine} (SVM) para a classificação de imagens
|
||||||
|
de sensoriamento remoto coletadas por drones sobre ambiente urbano.
|
||||||
|
A extração de atributos é realizada por meio de \textit{transfer
|
||||||
|
learning} com a rede convolucional MobileNetV2, pré-treinada no
|
||||||
|
conjunto ImageNet, produzindo vetores de 1.280 características por
|
||||||
|
imagem. O \textit{dataset} compreende 1.248 imagens TIFF organizadas em
|
||||||
|
13~classes de cobertura urbana coletadas sobre Guaratinguetá (SP).
|
||||||
|
Cinco modelos foram comparados: SVM linear, SVM radial com parâmetros
|
||||||
|
padrão, SVM radial com ajuste por busca em grade, SVM radial com
|
||||||
|
redução dimensional via Análise de Componentes Principais (PCA) e
|
||||||
|
\textit{Random Forest}. O melhor resultado foi obtido pelo SVM linear,
|
||||||
|
com acurácia de 40,00\% na partição de teste e 40,15\% na validação
|
||||||
|
cruzada \textit{k-fold} ($k=5$). O desempenho indica que a
|
||||||
|
representação RGB, mesmo enriquecida por \textit{transfer learning},
|
||||||
|
impõe um teto de discriminação para ambientes urbanos com alta
|
||||||
|
similaridade espectral entre classes, motivando a incorporação de
|
||||||
|
bandas termais em trabalhos futuros.
|
||||||
|
\end{resumo}
|
||||||
|
|
||||||
|
\begin{chave}
|
||||||
|
\textit{Support Vector Machine}; \textit{transfer learning};
|
||||||
|
MobileNetV2; sensoriamento remoto; imagens RGB;
|
||||||
|
classificação urbana; \textit{Random Forest}.
|
||||||
|
\end{chave}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\section{Introdução}
|
||||||
|
|
||||||
|
A classificação de imagens de sensoriamento remoto é tarefa central
|
||||||
|
para o monitoramento ambiental, o planejamento urbano e a inteligência
|
||||||
|
geoespacial. Com a popularização dos sistemas aéreos não tripulados
|
||||||
|
(SANT), tornou-se possível coletar imagens de alta resolução espacial
|
||||||
|
com baixo custo operacional, abrindo novas oportunidades para análises
|
||||||
|
de cobertura e uso do solo em escala local~\cite{lacerda2023}.
|
||||||
|
|
||||||
|
Imagens RGB capturadas por drones de pequeno porte oferecem três
|
||||||
|
bandas espectrais (vermelho, verde e azul), suficientes para
|
||||||
|
discriminar feições com resposta espectral visível distinta.
|
||||||
|
Em áreas urbanas, a diversidade de materiais construtivos (concreto,
|
||||||
|
asfalto, telhas cerâmicas, metálicas e de amianto, vegetação rasteira
|
||||||
|
e arbórea) produz classes com alta similaridade espectral no domínio
|
||||||
|
RGB, tornando a tarefa de classificação intrinsecamente
|
||||||
|
desafiadora~\cite{cheng2020}.
|
||||||
|
|
||||||
|
O uso de SVM em sensoriamento remoto é consolidado na literatura.
|
||||||
|
Melgani e Bruzzone~\cite{melgani2004} demonstraram que SVMs com
|
||||||
|
\textit{kernel} radial (RBF) superam classificadores clássicos em
|
||||||
|
cenários hiperespectrais. Camps-Valls e Bruzzone~\cite{campsvalls2005}
|
||||||
|
sistematizaram o emprego de métodos baseados em \textit{kernel} para
|
||||||
|
imagens hiperespectrais, mostrando que a escolha adequada do
|
||||||
|
\textit{kernel} e o ajuste de hiperparâmetros são determinantes para
|
||||||
|
o desempenho. Em imagens RGB de resolução espacial muito alta, a
|
||||||
|
escassez de informação espectral motiva o uso de descritores derivados
|
||||||
|
de redes neurais convolucionais profundas como
|
||||||
|
pré-processamento~\cite{cheng2020}.
|
||||||
|
|
||||||
|
O \textit{transfer learning} consiste em reutilizar representações
|
||||||
|
aprendidas por uma rede convolucional em uma tarefa de larga escala,
|
||||||
|
tipicamente a classificação do conjunto ImageNet, como extratores de
|
||||||
|
atributos para um domínio-alvo com menor quantidade de
|
||||||
|
dados~\cite{sandler2018}. Essa abordagem elimina a necessidade de
|
||||||
|
treinamento de ponta a ponta com dados rotulados escassos, tornando-se
|
||||||
|
atrativa para \textit{datasets} de sensoriamento remoto de médio
|
||||||
|
porte e viabilizando o uso de classificadores clássicos sobre
|
||||||
|
representações profundas.
|
||||||
|
|
||||||
|
Neste contexto, o presente trabalho aplica a rede MobileNetV2,
|
||||||
|
pré-treinada no ImageNet, como extrator de atributos para um
|
||||||
|
\textit{dataset} de imagens RGB de cobertura urbana em
|
||||||
|
Guaratinguetá (SP), avaliando cinco variantes de classificadores
|
||||||
|
SVM e \textit{Random Forest}. O trabalho complementa o estudo de
|
||||||
|
Lacerda et al.~\cite{lacerda2023}, que empregou fusão RGB\,+\,TIR
|
||||||
|
(\textit{thermal infrared}) com classificação orientada a objeto
|
||||||
|
(GEOBIA) em área urbana similar, alcançando acurácia de 88,68\%;
|
||||||
|
o presente estudo investiga o limite do canal RGB isolado com
|
||||||
|
classificadores lineares e não-lineares sobre atributos profundos.
|
||||||
|
|
||||||
|
O restante do artigo está organizado da seguinte forma: a Seção~2
|
||||||
|
apresenta a fundamentação teórica; a Seção~3 descreve a metodologia;
|
||||||
|
a Seção~4 reporta os experimentos e resultados; a Seção~5 discute os
|
||||||
|
achados; e a Seção~6 conclui o trabalho.
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\section{Fundamentação Teórica}
|
||||||
|
|
||||||
|
\subsection{\textit{Support Vector Machine}}
|
||||||
|
|
||||||
|
A SVM, introduzida por Cortes e Vapnik~\cite{cortes1995} e
|
||||||
|
formalizada em~\cite{vapnik1995}, é um classificador de margem
|
||||||
|
máxima que busca o hiperplano separador ótimo em um espaço de
|
||||||
|
características. Dado um conjunto de treinamento
|
||||||
|
$\{(\mathbf{x}_i, y_i)\}_{i=1}^n$, com $y_i \in \{-1,+1\}$,
|
||||||
|
o problema primal é:
|
||||||
|
\begin{equation}
|
||||||
|
\min_{\mathbf{w},b,\boldsymbol{\xi}} \frac{1}{2}\|\mathbf{w}\|^2
|
||||||
|
+ C\sum_{i=1}^n\xi_i,
|
||||||
|
\label{eq:svm_primal}
|
||||||
|
\end{equation}
|
||||||
|
sujeito a $y_i(\mathbf{w}^\top\phi(\mathbf{x}_i)+b)\geq 1-\xi_i$ e
|
||||||
|
$\xi_i\geq 0$, onde $C>0$ é o parâmetro de custo e $\phi(\cdot)$ é
|
||||||
|
o mapeamento induzido por um \textit{kernel}
|
||||||
|
$k(\mathbf{x},\mathbf{x}')=\phi(\mathbf{x})^\top\phi(\mathbf{x}')$.
|
||||||
|
Para problemas não-linearmente separáveis, utiliza-se o \textit{kernel}
|
||||||
|
RBF:
|
||||||
|
\begin{equation}
|
||||||
|
k(\mathbf{x},\mathbf{x}') =
|
||||||
|
\exp\!\left(-\gamma\|\mathbf{x}-\mathbf{x}'\|^2\right),
|
||||||
|
\label{eq:rbf}
|
||||||
|
\end{equation}
|
||||||
|
cujo hiperparâmetro $\gamma$ controla o raio de influência de cada
|
||||||
|
vetor de suporte. A extensão para problemas multiclasse é feita pela
|
||||||
|
estratégia \textit{one-vs-one}, que treina $\binom{K}{2}$
|
||||||
|
classificadores binários e decide por votação~\cite{melgani2004};
|
||||||
|
para $K=13$~classes, isso resulta em 78~classificadores binários.
|
||||||
|
|
||||||
|
\subsection{\textit{Random Forest}}
|
||||||
|
|
||||||
|
O \textit{Random Forest}, proposto por Breiman~\cite{breiman2001},
|
||||||
|
é um método de aprendizado por agrupamento (\textit{ensemble}) que
|
||||||
|
combina múltiplas árvores de decisão. Cada árvore é treinada sobre
|
||||||
|
uma subamostra aleatória do conjunto de treinamento
|
||||||
|
(\textit{bagging}) e utiliza, em cada divisão de nó, um subconjunto
|
||||||
|
aleatório de atributos. O resultado final é obtido por votação
|
||||||
|
majoritária entre as árvores, o que reduz a variância em relação
|
||||||
|
a um único classificador. Além da classificação, o método fornece
|
||||||
|
estimativas de importância das variáveis por meio do critério
|
||||||
|
\textit{MeanDecreaseGini}, que quantifica a contribuição média de
|
||||||
|
cada atributo na redução da impureza de Gini ao longo de todas as
|
||||||
|
árvores da floresta.
|
||||||
|
|
||||||
|
\subsection{Atributos RGB em Sensoriamento Remoto}
|
||||||
|
|
||||||
|
Imagens RGB de SANT possuem resolução espacial tipicamente inferior
|
||||||
|
a 10~cm, mas apenas três bandas espectrais, muito aquém dos sensores
|
||||||
|
hiperespectrais. Em ambientes urbanos, a discriminação de classes
|
||||||
|
espectralmente similares (como asfalto versus concreto) requer
|
||||||
|
descritores texturais ou morfológicos além da informação espectral
|
||||||
|
pura~\cite{cheng2020}. Estudos de classificação de cenas de
|
||||||
|
sensoriamento remoto com \textit{deep learning} mostram que
|
||||||
|
arquiteturas convolucionais profundas capturam essas texturas de
|
||||||
|
forma consistente mesmo quando treinadas em domínios
|
||||||
|
distintos~\cite{cheng2020}.
|
||||||
|
|
||||||
|
\subsection{\textit{Transfer Learning} e Redes Convolucionais}
|
||||||
|
|
||||||
|
Redes neurais convolucionais (CNN) profundas aprendem hierarquias de
|
||||||
|
representação, evoluindo de bordas e texturas locais nas camadas
|
||||||
|
iniciais até padrões semânticos de alto nível nas camadas finais.
|
||||||
|
Quando treinada no ImageNet, uma CNN codifica atributos genéricos
|
||||||
|
transferíveis para outros domínios sem necessidade de re-treinamento
|
||||||
|
completo~\cite{sandler2018}.
|
||||||
|
|
||||||
|
A MobileNetV2~\cite{sandler2018} é uma arquitetura eficiente que
|
||||||
|
emprega blocos de resíduo invertido com gargalo linear, com cerca de
|
||||||
|
3,4~milhões de parâmetros. Sem a camada de classificação final e
|
||||||
|
com \textit{Global Average Pooling}, produz um vetor de
|
||||||
|
1.280~dimensões por imagem de entrada $224\times224$~px.
|
||||||
|
Esses atributos alimentam um classificador externo, sem atualização
|
||||||
|
dos pesos da rede.
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\section{Metodologia}
|
||||||
|
|
||||||
|
\subsection{Conjunto de Dados}
|
||||||
|
|
||||||
|
O \textit{dataset} é composto por 1.248 imagens TIFF coletadas por
|
||||||
|
drone sobre a área urbana de Guaratinguetá (SP), organizadas em 13
|
||||||
|
classes de cobertura urbana (C1--C13), com 96~imagens por
|
||||||
|
classe. % [VERIFICAR] resolução espacial e sensor exatos
|
||||||
|
Para equalizar o impacto das classes no treinamento, foi realizada
|
||||||
|
uma amostragem estratificada de 50~imagens por classe, totalizando
|
||||||
|
650~amostras. A Figura~\ref{fig:distclasses} mostra a distribuição
|
||||||
|
das classes após a amostragem.
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-6-1.png}
|
||||||
|
\caption{Distribuição das classes no \textit{dataset} após amostragem
|
||||||
|
estratificada (50 imagens/classe, 650 no total).}
|
||||||
|
\label{fig:distclasses}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\subsection{Extração de Atributos com MobileNetV2}
|
||||||
|
|
||||||
|
Cada imagem foi redimensionada para $224\times224$~pixels e
|
||||||
|
normalizada para o intervalo $[-1, 1]$ pela transformação
|
||||||
|
$(x / 127{,}5) - 1{,}0$, conforme a pré-normalização específica
|
||||||
|
do MobileNetV2. A rede foi carregada com pesos pré-treinados no
|
||||||
|
ImageNet por meio do pacote \texttt{keras3} em~R~\cite{r_core},
|
||||||
|
sem a camada de classificação (\texttt{include\_top = FALSE}) e
|
||||||
|
com \textit{Global Average Pooling} (\texttt{pooling = "avg"}),
|
||||||
|
produzindo um vetor de 1.280~atributos por imagem:
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
modelo_base <- application_mobilenet_v2(
|
||||||
|
include_top = FALSE,
|
||||||
|
weights = "imagenet",
|
||||||
|
pooling = "avg",
|
||||||
|
input_shape = c(224L, 224L, 3L)
|
||||||
|
)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
As imagens foram processadas em lotes de 16 (\textit{batch size}),
|
||||||
|
com resultado armazenado em \textit{cache} para evitar
|
||||||
|
reprocessamento. Após a extração, atributos com variância próxima
|
||||||
|
de zero (\textit{near-zero variance}, NZV) foram removidos e os
|
||||||
|
demais padronizados (\textit{center}/\textit{scale}).
|
||||||
|
|
||||||
|
\subsection{Divisão dos Dados e Validação}
|
||||||
|
|
||||||
|
As 650~imagens foram divididas em 70\% para treinamento
|
||||||
|
(455~imagens) e 30\% para teste (195~imagens) por amostragem
|
||||||
|
estratificada (\textit{holdout}). Adicionalmente, foi aplicada
|
||||||
|
validação cruzada \textit{k-fold} com $k=5$ dobras estratificadas,
|
||||||
|
para estimativa mais robusta da generalização. Na validação cruzada,
|
||||||
|
os melhores hiperparâmetros do SVM radial ajustado, obtidos na etapa
|
||||||
|
de \textit{holdout} ($C=1$, $\gamma=0{,}001$), foram reutilizados
|
||||||
|
em cada dobra para evitar o custo computacional de re-otimização.
|
||||||
|
Para o \textit{Random Forest}, o número de árvores foi reduzido para
|
||||||
|
200 no \textit{k-fold} (contra 500 no \textit{holdout}) por razão
|
||||||
|
de eficiência computacional.
|
||||||
|
|
||||||
|
\subsection{Redução Dimensional com PCA}
|
||||||
|
|
||||||
|
A Análise de Componentes Principais foi aplicada ao conjunto de
|
||||||
|
treinamento. Para reter 95\% da variância explicada são necessários
|
||||||
|
249~componentes (Figura~\ref{fig:pca}), o que evidencia a alta
|
||||||
|
dispersão da informação ao longo das 1.280~dimensões do espaço
|
||||||
|
MobileNetV2. Optou-se por reter 30~componentes para treinar o modelo
|
||||||
|
PCA\,+\,SVM~radial, com custo computacional reduzido.
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-18-1.png}
|
||||||
|
\caption{Variância acumulada pelos componentes principais.
|
||||||
|
Para 95\% da variância, são necessários 249~componentes.
|
||||||
|
A linha vertical indica o corte em 30~componentes
|
||||||
|
utilizado no modelo PCA\,+\,SVM radial.}
|
||||||
|
\label{fig:pca}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\subsection{Modelos Avaliados}
|
||||||
|
|
||||||
|
Cinco modelos foram treinados e avaliados:
|
||||||
|
(i)~SVM linear, com $C=1$; % [VERIFICAR] valor exato de C para SVM linear
|
||||||
|
(ii)~SVM radial com parâmetros fixos ($\mathit{cost}=10$,
|
||||||
|
$\gamma=0{,}01$);
|
||||||
|
(iii)~SVM radial ajustado por busca em grade,
|
||||||
|
$C \in \{0{,}1;\;1;\;10;\;100\}$ e
|
||||||
|
$\gamma \in \{0{,}001;\;0{,}01;\;0{,}05;\;0{,}1\}$,
|
||||||
|
com melhor configuração $C=1$, $\gamma=0{,}001$;
|
||||||
|
(iv)~PCA\,+\,SVM radial, com projeção em 30~componentes seguida de
|
||||||
|
SVM radial com $\mathit{cost}=10$ e $\gamma=0{,}01$; e
|
||||||
|
(v)~\textit{Random Forest} com 500~árvores e parâmetros padrão.
|
||||||
|
Todos os modelos foram implementados em~R~\cite{r_core} com os
|
||||||
|
pacotes \texttt{e1071} (SVM), \texttt{caret} e
|
||||||
|
\texttt{randomForest}, com semente aleatória fixada em 123.
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\section{Experimentos e Resultados}
|
||||||
|
|
||||||
|
\subsection{Comparação de Desempenho}
|
||||||
|
|
||||||
|
A Tabela~\ref{tab:resultados} apresenta as métricas obtidas na
|
||||||
|
partição de teste (\textit{holdout} 70/30) e na validação cruzada
|
||||||
|
(\textit{k-fold}, $k=5$) para os cinco modelos. As métricas
|
||||||
|
reportadas são acurácia global, índice Kappa de Cohen, sensibilidade
|
||||||
|
macro-média, especificidade macro-média e F1-\textit{score}
|
||||||
|
macro-médio.
|
||||||
|
|
||||||
|
\begin{table}[h]
|
||||||
|
\centering
|
||||||
|
\caption{Métricas de desempenho (\textit{holdout} e \textit{k-fold})}
|
||||||
|
\label{tab:resultados}
|
||||||
|
{\scriptsize
|
||||||
|
\begin{tabular}{lccccc}
|
||||||
|
\toprule
|
||||||
|
Modelo & Acur. & Kappa & Sens. & Espec. & F1 \\
|
||||||
|
\midrule
|
||||||
|
\multicolumn{6}{l}{\textit{Holdout (70\%/30\%)}} \\
|
||||||
|
\midrule
|
||||||
|
SVM linear & 0,400 & 0,350 & 0,400 & 0,950 & 0,402 \\
|
||||||
|
\textit{Rand. Forest} & 0,374 & 0,322 & 0,374 & 0,948 & 0,369 \\
|
||||||
|
PCA + SVM rad. & 0,359 & 0,306 & 0,359 & 0,947 & 0,370 \\
|
||||||
|
SVM rad. aj. & 0,354 & 0,300 & 0,354 & 0,946 & 0,352 \\
|
||||||
|
SVM radial & 0,144 & 0,072 & 0,144 & 0,929 & 0,193 \\
|
||||||
|
\midrule
|
||||||
|
\multicolumn{6}{l}{\textit{K-fold} ($k = 5$)} \\
|
||||||
|
\midrule
|
||||||
|
SVM linear & 0,402 & 0,352 & 0,402 & --- & 0,409 \\
|
||||||
|
PCA + SVM rad. & 0,368 & 0,315 & 0,368 & --- & 0,378 \\
|
||||||
|
SVM rad. aj. & 0,362 & 0,308 & 0,362 & --- & 0,367 \\
|
||||||
|
\textit{Rand. Forest} & 0,348 & 0,293 & 0,348 & --- & 0,350 \\
|
||||||
|
SVM radial & 0,159 & 0,088 & 0,159 & --- & 0,223 \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}}
|
||||||
|
{\scriptsize Especificidade macro não calculada para o \textit{k-fold}.}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
O SVM linear obteve o melhor desempenho em ambas as estratégias
|
||||||
|
de avaliação. Na validação cruzada, alcançou acurácia de 40,15\% e
|
||||||
|
$\kappa=0{,}352$, indicando concordância razoável acima do acaso.
|
||||||
|
A Figura~\ref{fig:comparacao} compara as acurácias dos cinco modelos
|
||||||
|
no \textit{holdout}, e a Figura~\ref{fig:multimetrica} apresenta
|
||||||
|
as demais métricas, confirmando a consistência do SVM linear
|
||||||
|
como melhor modelo em todas as dimensões avaliadas.
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-23-1.png}
|
||||||
|
\caption{Comparação da acurácia dos cinco modelos na partição de
|
||||||
|
teste (\textit{holdout}).}
|
||||||
|
\label{fig:comparacao}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-24-1.png}
|
||||||
|
\caption{Comparação de múltiplas métricas (acurácia, Kappa,
|
||||||
|
sensibilidade e F1-\textit{score}) para os cinco modelos
|
||||||
|
(\textit{holdout}).}
|
||||||
|
\label{fig:multimetrica}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\subsection{Análise por Classe}
|
||||||
|
|
||||||
|
A Figura~\ref{fig:confusao} exibe a matriz de confusão do SVM linear
|
||||||
|
no conjunto de teste. A sensibilidade por classe varia de 13\%
|
||||||
|
(C12) a 60\% (C5 e C9), passando por valores intermediários como
|
||||||
|
C8~(53\%), C6~(53\%) e C3~(47\%). As classes C11, C12 e C13
|
||||||
|
apresentam sensibilidades inferiores a 27\%, indicando que o modelo
|
||||||
|
sistematicamente falha em discriminá-las das demais. A confusão
|
||||||
|
predominante ocorre entre classes com resposta espectral similar
|
||||||
|
no domínio visível, como diferentes tipos de telha e pavimentação.
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-26-1.png}
|
||||||
|
\caption{Matriz de confusão do SVM linear no conjunto de teste
|
||||||
|
(195 amostras, 13 classes).}
|
||||||
|
\label{fig:confusao}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\subsection{Importância das Variáveis}
|
||||||
|
|
||||||
|
A Figura~\ref{fig:importancia} mostra as 20~variáveis mais
|
||||||
|
importantes segundo o critério \textit{MeanDecreaseGini} do
|
||||||
|
\textit{Random Forest}. Os atributos mais relevantes para a
|
||||||
|
classificação são vetores de ativação específicos da camada de
|
||||||
|
\textit{Global Average Pooling} do MobileNetV2, identificados
|
||||||
|
pelos índices \texttt{feat\_XXXX}. % [VERIFICAR] identificar quais índices dominam
|
||||||
|
A distribuição da importância ao longo dos 20~principais atributos
|
||||||
|
não apresenta concentração acentuada em poucos índices, sugerindo
|
||||||
|
que a discriminação das classes urbanas depende de combinações
|
||||||
|
distribuídas das ativações profundas, em vez de um subconjunto
|
||||||
|
pequeno de características dominantes.
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-28-1.png}
|
||||||
|
\caption{As 20 variáveis mais importantes segundo o critério
|
||||||
|
\textit{MeanDecreaseGini} do \textit{Random Forest}.}
|
||||||
|
\label{fig:importancia}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\subsection{\textit{Holdout} versus \textit{K-fold}}
|
||||||
|
|
||||||
|
As métricas obtidas por \textit{k-fold} foram consistentes com as do
|
||||||
|
\textit{holdout} (Figura~\ref{fig:hkf}), com variação máxima de
|
||||||
|
0,002 pontos de acurácia para o SVM linear, indicando ausência de
|
||||||
|
sobreajuste significativo. A maior discrepância ocorreu com o
|
||||||
|
\textit{Random Forest}, cujo desempenho no \textit{holdout} (37,44\%)
|
||||||
|
superou o \textit{k-fold} (34,77\%). Parte dessa diferença é
|
||||||
|
explicada pela redução de 500 para 200 árvores no
|
||||||
|
\textit{k-fold}, adotada por razão de eficiência computacional.
|
||||||
|
|
||||||
|
\begin{figure}[h]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.98\columnwidth]{%
|
||||||
|
trabalho_svm_rgb_files/figure-html/unnamed-chunk-34-1.png}
|
||||||
|
\caption{Comparação entre as acurácias obtidas por \textit{holdout}
|
||||||
|
e por \textit{k-fold} ($k=5$) para os cinco modelos.}
|
||||||
|
\label{fig:hkf}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\section{Discussão}
|
||||||
|
|
||||||
|
A acurácia máxima de 40,15\% para 13 classes está abaixo do que
|
||||||
|
seria típico em cenários com maior diversidade espectral. O resultado
|
||||||
|
contrasta com o de Lacerda et al.~\cite{lacerda2023}, que obteve
|
||||||
|
83,02\% somente com RGB e 88,68\% com fusão RGB\,+\,TIR em um
|
||||||
|
problema de 8 classes. Três fatores explicam essa diferença.
|
||||||
|
|
||||||
|
Em primeiro lugar, o número de classes: com 13 categorias de
|
||||||
|
cobertura urbana visualmente similares, o problema é
|
||||||
|
consideravelmente mais difícil que classificações com 8 ou menos
|
||||||
|
classes.
|
||||||
|
|
||||||
|
Em segundo lugar, a informação espectral: imagens RGB não capturam
|
||||||
|
a reflectância termal (TIR), que diferencia materiais como telha
|
||||||
|
cerâmica, concreto e asfalto, cujas emissividades são distintas
|
||||||
|
mesmo quando a coloração no espectro visível é similar.
|
||||||
|
|
||||||
|
Em terceiro lugar, a representatividade do \textit{transfer
|
||||||
|
learning}: a MobileNetV2, treinada no ImageNet, aprende descritores
|
||||||
|
otimizados para objetos fotográficos do cotidiano. Sua transferência
|
||||||
|
para imagens de coberturas urbanas em ponto de vista \textit{nadir}
|
||||||
|
é parcialmente efetiva, mas não especializada para texturas de
|
||||||
|
materiais construtivos~\cite{cheng2020}.
|
||||||
|
|
||||||
|
Um achado relevante é a superioridade do SVM linear sobre o SVM
|
||||||
|
radial. Em espaços de alta dimensão (1.280 atributos), os dados
|
||||||
|
tendem a se tornar mais linearmente separáveis, de modo que o
|
||||||
|
\textit{kernel} linear encontra hiperplanos eficazes sem risco de
|
||||||
|
sobreajuste. O \textit{kernel} RBF, por sua vez, é fortemente
|
||||||
|
sensível ao parâmetro $\gamma$: com os valores padrão
|
||||||
|
($\gamma=0{,}01$, $\mathit{cost}=10$), a função de similaridade
|
||||||
|
colapsa e o modelo alcança apenas 14,36\% de acurácia. Após ajuste
|
||||||
|
por busca em grade ($\gamma=0{,}001$, $\mathit{cost}=1$), o
|
||||||
|
desempenho sobe para 35,38\%, porém ainda abaixo do SVM linear.
|
||||||
|
|
||||||
|
A aplicação de PCA antes do SVM radial com os mesmos parâmetros
|
||||||
|
padrão ($\mathit{cost}=10$, $\gamma=0{,}01$) elevou a acurácia de
|
||||||
|
14,36\% para 35,90\%, mesmo sem re-otimização dos hiperparâmetros.
|
||||||
|
Isso indica que a redução de 1.280 para 30~componentes corrige
|
||||||
|
implicitamente a escala de $\gamma$, tornando o \textit{kernel} RBF
|
||||||
|
funcional sem busca em grade. O custo, porém, é a perda de variância
|
||||||
|
explicada: os 30~componentes capturam menos de 20\% da variância
|
||||||
|
total (Figura~\ref{fig:pca}), o que limita o teto de desempenho.
|
||||||
|
|
||||||
|
% ======================================================================
|
||||||
|
\section{Conclusão}
|
||||||
|
|
||||||
|
Este trabalho avaliou cinco modelos (SVM linear, SVM radial em três
|
||||||
|
configurações e \textit{Random Forest}) para a classificação de
|
||||||
|
imagens de cobertura urbana RGB coletadas por drone, utilizando
|
||||||
|
atributos de \textit{transfer learning} extraídos pela MobileNetV2.
|
||||||
|
|
||||||
|
O SVM linear obteve o melhor desempenho, com acurácia de 40,15\%
|
||||||
|
na validação cruzada \textit{k-fold}, superando o SVM radial
|
||||||
|
ajustado (36,15\%), o \textit{Random Forest} (34,77\%) e o SVM
|
||||||
|
radial padrão (15,85\%). Os resultados confirmam que: (i)~em espaços
|
||||||
|
de alta dimensão gerados por CNN, o \textit{kernel} linear é
|
||||||
|
competitivo; (ii)~a parametrização de $\gamma$ é crítica para o
|
||||||
|
\textit{kernel} RBF; e (iii)~a redução dimensional por PCA corrige
|
||||||
|
implicitamente a escala do \textit{kernel} sem re-otimização.
|
||||||
|
|
||||||
|
Como principais direções futuras, destacam-se:
|
||||||
|
(i)~incorporação da banda termal (TIR) para fusão de dados,
|
||||||
|
conforme demonstrado em~\cite{lacerda2023};
|
||||||
|
(ii)~uso de arquiteturas CNN pré-treinadas em \textit{datasets} de
|
||||||
|
sensoriamento remoto (\textit{domain-specific transfer learning});
|
||||||
|
(iii)~ampliação do número de amostras por classe; e
|
||||||
|
(iv)~exploração de classificadores baseados em \textit{Transformers}
|
||||||
|
para dados de cena.
|
||||||
|
|
||||||
|
\bibliographystyle{IEEEtran}
|
||||||
|
\bibliography{Referencias_SVM}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
5462
datasets_2026.pdf
Normal file
BIN
galoa-proceedings--sbsr-2023--156515.pdf
Normal file
1249
outputs/completo/features_rgb_completo.csv
Normal file
BIN
outputs/completo/features_rgb_mobilenetv2_completo.rds
Normal file
1263
outputs/completo/importancia_variaveis_rgb_completo.csv
Normal file
BIN
outputs/completo/modelos_rgb_completo.rds
Normal file
6
outputs/completo/resultados_kfold_rgb_completo.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||||
|
SVM linear,0.36298074633403044,0.30995815445239716,0.3633603238866397,0.9469207558795383,0.3834656395997568
|
||||||
|
SVM radial ajustado,0.35649417578146686,0.3029397087363448,0.35684210526315785,0.9463815622092584,0.3720660941606715
|
||||||
|
Random Forest,0.3284677302142518,0.27258541671650627,0.32878542510121456,0.9440465083621263,0.3405924694392964
|
||||||
|
PCA + SVM radial,0.32364187317626886,0.26735133180485987,0.32388663967611336,0.9436442293130114,0.35932566977715646
|
||||||
|
SVM radial,0.14187017563340923,0.0708961467200726,0.14327935222672064,0.9285272695006503,0.17432376344764106
|
||||||
|
6
outputs/completo/resultados_modelos_rgb_completo.csv
Normal 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
|
||||||
|
651
outputs/features_rgb.csv
Normal file
BIN
outputs/features_rgb_mobilenetv2_50.rds
Normal file
1266
outputs/importancia_variaveis_rgb.csv
Normal file
BIN
outputs/modelos_rgb.rds
Normal file
651
outputs/parcial/features_rgb.csv
Normal file
BIN
outputs/parcial/features_rgb_mobilenetv2.rds
Normal file
1266
outputs/parcial/importancia_variaveis_rgb.csv
Normal file
BIN
outputs/parcial/modelos_rgb.rds
Normal file
6
outputs/parcial/resultados_kfold_rgb.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||||
|
SVM linear,0.40153846153846157,0.3516666666666666,0.40153846153846157,0.9501282051282052,0.40929682290313046
|
||||||
|
SVM radial ajustado,0.38,0.3283333333333333,0.38,0.9483333333333333,0.3905066510222767
|
||||||
|
PCA + SVM radial,0.3676923076923077,0.31499999999999995,0.3676923076923077,0.9473076923076923,0.37836144953604534
|
||||||
|
Random Forest,0.35692307692307695,0.3033333333333333,0.35692307692307695,0.9464102564102563,0.38061565488169397
|
||||||
|
SVM radial,0.15846153846153846,0.08833333333333333,0.15846153846153846,0.9298717948717948,0.22329423850231678
|
||||||
|
6
outputs/parcial/resultados_modelos_rgb.csv
Normal 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
|
||||||
|
BIN
outputs/pdf_images/p10_Image31.png
Normal file
BIN
outputs/pdf_images/p1_Image11.png
Normal file
40
outputs/pdf_images/p2_Image14.png
Normal file
BIN
outputs/pdf_images/p3_Image17.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
outputs/pdf_images/p4_Image20.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
outputs/pdf_images/p5_Image23.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
outputs/pdf_images/p6_Image26.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
outputs/pdf_images/p6_Image27.png
Normal file
BIN
outputs/pdf_images/p7_Image31.png
Normal file
1
outputs/pdf_images/p8_Image42.png
Normal file
BIN
outputs/pdf_images/p8_Image43.jpg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
outputs/pdf_images/p8_Image44.jpg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
outputs/pdf_images/p9_Image47.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
outputs/pdf_images/p9_Image48.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
outputs/pdf_images/p9_Image49.png
Normal file
6
outputs/resultados_kfold_rgb.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
|
||||||
|
SVM linear,0.40153846153846157,0.3516666666666666,0.40153846153846157,0.9501282051282052,0.40929682290313046
|
||||||
|
PCA + SVM radial,0.3676923076923077,0.31499999999999995,0.3676923076923077,0.9473076923076923,0.37836144953604534
|
||||||
|
SVM radial ajustado,0.36153846153846153,0.3083333333333333,0.36153846153846153,0.9467948717948718,0.3667814495681257
|
||||||
|
Random Forest,0.3476923076923077,0.2933333333333333,0.3476923076923077,0.9456410256410256,0.35022414446407363
|
||||||
|
SVM radial,0.15846153846153846,0.08833333333333333,0.15846153846153846,0.9298717948717948,0.22329423850231678
|
||||||
|
6
outputs/resultados_modelos_rgb.csv
Normal 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
|
||||||
|
2701
trabalho_svm_rgb.html
Normal file
1037
trabalho_svm_rgb.qmd
Normal file
2332
trabalho_svm_rgb_completo.html
Normal file
855
trabalho_svm_rgb_completo.qmd
Normal file
@@ -0,0 +1,855 @@
|
|||||||
|
---
|
||||||
|
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 <- "D:/Vitor/Documents/CEAO/802/HAB/DADOS CEAO 2026/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
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
dir.create("outputs", showWarnings = FALSE)
|
||||||
|
|
||||||
|
modelo_base <- application_mobilenet_v2(
|
||||||
|
include_top = FALSE,
|
||||||
|
weights = "imagenet",
|
||||||
|
pooling = "avg",
|
||||||
|
input_shape = c(224L, 224L, 3L)
|
||||||
|
)
|
||||||
|
|
||||||
|
cat("Modelo carregado:", modelo_base$name, "\n")
|
||||||
|
cat("Dimensao do vetor de features:", modelo_base$output_shape[[2]], "\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Extracao de atributos por Transfer Learning
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 7. Extracao de features (com cache separado do original)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
pasta_saida <- "outputs/completo"
|
||||||
|
dir.create(pasta_saida, recursive = TRUE, showWarnings = FALSE)
|
||||||
|
|
||||||
|
# Nomes diferentes para nao sobrescrever o cache do trabalho original
|
||||||
|
arquivo_cache_features <- file.path(pasta_saida, "features_rgb_mobilenetv2_completo.rds")
|
||||||
|
arquivo_cache_parcial <- file.path(pasta_saida, "features_rgb_mobilenetv2_completo_temp.rds")
|
||||||
|
|
||||||
|
tamanho_imagem <- 224L
|
||||||
|
tamanho_lote <- 16L
|
||||||
|
|
||||||
|
extrair_features_batch <- function(arquivos, modelo, tamanho = 224L, lote = 16L) {
|
||||||
|
n <- length(arquivos)
|
||||||
|
resultados <- matrix(NA_real_, nrow = n, ncol = as.integer(modelo$output_shape[[2]]))
|
||||||
|
|
||||||
|
for (inicio in seq(1, n, by = lote)) {
|
||||||
|
fim <- min(inicio + lote - 1L, n)
|
||||||
|
bloco <- arquivos[inicio:fim]
|
||||||
|
tam_bloco <- length(bloco)
|
||||||
|
|
||||||
|
batch_array <- array(0, dim = c(tam_bloco, tamanho, tamanho, 3L))
|
||||||
|
|
||||||
|
for (j in seq_len(tam_bloco)) {
|
||||||
|
img <- image_load(bloco[j], target_size = c(tamanho, tamanho))
|
||||||
|
batch_array[j, , , ] <- image_to_array(img)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_prep <- (batch_array / 127.5) - 1.0
|
||||||
|
features <- predict(modelo, batch_prep, verbose = 0L)
|
||||||
|
resultados[inicio:fim, ] <- features
|
||||||
|
}
|
||||||
|
|
||||||
|
resultados
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.exists(arquivo_cache_features)) {
|
||||||
|
dados <- readRDS(arquivo_cache_features)
|
||||||
|
cat("Features carregadas do cache:", arquivo_cache_features, "\n")
|
||||||
|
} else {
|
||||||
|
cat("Extraindo features com MobileNetV2. Isso pode levar alguns minutos...\n")
|
||||||
|
|
||||||
|
erros_idx <- integer(0)
|
||||||
|
|
||||||
|
if (file.exists(arquivo_cache_parcial)) {
|
||||||
|
features_matrix <- readRDS(arquivo_cache_parcial)
|
||||||
|
linhas_prontas <- which(complete.cases(features_matrix))
|
||||||
|
cat("Cache parcial encontrado:", length(linhas_prontas), "imagens ja processadas.\n")
|
||||||
|
} else {
|
||||||
|
n_features <- as.integer(modelo_base$output_shape[[2]])
|
||||||
|
features_matrix <- matrix(NA_real_, nrow = nrow(metadados), ncol = n_features)
|
||||||
|
linhas_prontas <- integer(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
linhas_pendentes <- setdiff(seq_len(nrow(metadados)), linhas_prontas)
|
||||||
|
|
||||||
|
if (length(linhas_pendentes) > 0) {
|
||||||
|
for (inicio in seq(1, length(linhas_pendentes), by = tamanho_lote)) {
|
||||||
|
fim <- min(inicio + tamanho_lote - 1L, length(linhas_pendentes))
|
||||||
|
idxs <- linhas_pendentes[inicio:fim]
|
||||||
|
arquivos_bloco <- metadados$arquivo[idxs]
|
||||||
|
|
||||||
|
resultado <- tryCatch(
|
||||||
|
extrair_features_batch(arquivos_bloco, modelo_base, tamanho_imagem, length(idxs)),
|
||||||
|
error = function(e) {
|
||||||
|
message("Erro no lote ", inicio, "-", fim, ": ", conditionMessage(e))
|
||||||
|
erros_idx <<- c(erros_idx, idxs)
|
||||||
|
NULL
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!is.null(resultado)) {
|
||||||
|
features_matrix[idxs, ] <- resultado
|
||||||
|
}
|
||||||
|
|
||||||
|
cat(sprintf(" Processadas: %d / %d\r", min(fim, length(linhas_pendentes)), length(linhas_pendentes)))
|
||||||
|
|
||||||
|
if (fim %% (tamanho_lote * 5L) == 0L) {
|
||||||
|
saveRDS(features_matrix, arquivo_cache_parcial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cat("\n")
|
||||||
|
saveRDS(features_matrix, arquivo_cache_parcial)
|
||||||
|
}
|
||||||
|
|
||||||
|
imagens_ok <- complete.cases(features_matrix)
|
||||||
|
n_features <- ncol(features_matrix)
|
||||||
|
nomes_feat <- paste0("feat_", stringr::str_pad(seq_len(n_features), 4, pad = "0"))
|
||||||
|
colnames(features_matrix) <- nomes_feat
|
||||||
|
|
||||||
|
dados <- bind_cols(
|
||||||
|
metadados[imagens_ok, ],
|
||||||
|
as_tibble(features_matrix[imagens_ok, ])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (length(erros_idx) > 0) {
|
||||||
|
erros_df <- metadados[erros_idx, ] %>% mutate(erro = "falha na extracao CNN")
|
||||||
|
arquivo_erros <- file.path(pasta_saida, "erros_processamento_rgb_completo.csv")
|
||||||
|
write_csv(erros_df, arquivo_erros)
|
||||||
|
cat("Imagens com erro:", length(erros_idx), "(ver", arquivo_erros, ")\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
saveRDS(dados, arquivo_cache_features)
|
||||||
|
if (file.exists(arquivo_cache_parcial)) file.remove(arquivo_cache_parcial)
|
||||||
|
|
||||||
|
cat("Features salvas em:", arquivo_cache_features, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
cat("Imagens na base final:", nrow(dados), "\n")
|
||||||
|
cat("Numero de atributos extraidos (MobileNetV2):", ncol(dados) - 3L, "\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Separacao entre treino e teste
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 8. Separacao treino/teste
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
idx_treino <- createDataPartition(
|
||||||
|
y = dados$classe,
|
||||||
|
p = 0.70,
|
||||||
|
list = FALSE
|
||||||
|
)
|
||||||
|
|
||||||
|
treino <- dados[idx_treino, ]
|
||||||
|
teste <- dados[-idx_treino, ]
|
||||||
|
|
||||||
|
cat("Imagens no treino:", nrow(treino), "\n")
|
||||||
|
cat("Imagens no teste:", nrow(teste), "\n")
|
||||||
|
|
||||||
|
treino %>%
|
||||||
|
count(classe, name = "treino") %>%
|
||||||
|
full_join(
|
||||||
|
teste %>% count(classe, name = "teste"),
|
||||||
|
by = "classe"
|
||||||
|
) %>%
|
||||||
|
arrange(classe) %>%
|
||||||
|
kable()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Pre-processamento
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 9. Preparar matrizes X e vetor y
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
colunas_nao_preditoras <- c("arquivo", "classe", "tamanho_mb")
|
||||||
|
|
||||||
|
x_treino <- treino %>%
|
||||||
|
select(-all_of(colunas_nao_preditoras))
|
||||||
|
|
||||||
|
x_teste <- teste %>%
|
||||||
|
select(-all_of(colunas_nao_preditoras))
|
||||||
|
|
||||||
|
y_treino <- droplevels(treino$classe)
|
||||||
|
y_teste <- factor(teste$classe, levels = levels(y_treino))
|
||||||
|
|
||||||
|
variaveis_nzv <- nearZeroVar(x_treino)
|
||||||
|
|
||||||
|
if (length(variaveis_nzv) > 0) {
|
||||||
|
x_treino <- x_treino[, -variaveis_nzv, drop = FALSE]
|
||||||
|
x_teste <- x_teste[, colnames(x_treino), drop = FALSE]
|
||||||
|
}
|
||||||
|
|
||||||
|
preproc <- preProcess(
|
||||||
|
x_treino,
|
||||||
|
method = c("center", "scale")
|
||||||
|
)
|
||||||
|
|
||||||
|
x_treino_norm <- predict(preproc, x_treino)
|
||||||
|
x_teste_norm <- predict(preproc, x_teste)
|
||||||
|
|
||||||
|
cat("Preditores usados nos modelos:", ncol(x_treino_norm), "\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Modelo 1: SVM linear
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 10. SVM linear
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
modelo_svm_linear <- svm(
|
||||||
|
x = x_treino_norm,
|
||||||
|
y = y_treino,
|
||||||
|
kernel = "linear",
|
||||||
|
cost = 1,
|
||||||
|
scale = FALSE
|
||||||
|
)
|
||||||
|
|
||||||
|
pred_svm_linear <- predict(modelo_svm_linear, x_teste_norm)
|
||||||
|
|
||||||
|
cm_svm_linear <- confusionMatrix(pred_svm_linear, y_teste)
|
||||||
|
|
||||||
|
cm_svm_linear
|
||||||
|
```
|
||||||
|
|
||||||
|
# Modelo 2: SVM radial
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 11. SVM radial com parametros fixos
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
modelo_svm_radial <- svm(
|
||||||
|
x = x_treino_norm,
|
||||||
|
y = y_treino,
|
||||||
|
kernel = "radial",
|
||||||
|
cost = 10,
|
||||||
|
gamma = 0.01,
|
||||||
|
scale = FALSE
|
||||||
|
)
|
||||||
|
|
||||||
|
pred_svm_radial <- predict(modelo_svm_radial, x_teste_norm)
|
||||||
|
|
||||||
|
cm_svm_radial <- confusionMatrix(pred_svm_radial, y_teste)
|
||||||
|
|
||||||
|
cm_svm_radial
|
||||||
|
```
|
||||||
|
|
||||||
|
# Modelo 3: Ajuste de hiperparametros do SVM radial
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 12. Ajuste simples de hiperparametros do SVM radial
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
k_cv <- min(5, as.integer(min(table(y_treino))))
|
||||||
|
|
||||||
|
if (k_cv >= 2) {
|
||||||
|
ajuste_svm_radial <- tune.svm(
|
||||||
|
x = x_treino_norm,
|
||||||
|
y = y_treino,
|
||||||
|
kernel = "radial",
|
||||||
|
cost = c(0.1, 1, 10, 100),
|
||||||
|
gamma = c(0.001, 0.01, 0.05, 0.1),
|
||||||
|
tunecontrol = tune.control(cross = k_cv)
|
||||||
|
)
|
||||||
|
|
||||||
|
modelo_svm_radial_ajustado <- ajuste_svm_radial$best.model
|
||||||
|
|
||||||
|
pred_svm_radial_ajustado <- predict(
|
||||||
|
modelo_svm_radial_ajustado,
|
||||||
|
x_teste_norm
|
||||||
|
)
|
||||||
|
|
||||||
|
cm_svm_radial_ajustado <- confusionMatrix(
|
||||||
|
pred_svm_radial_ajustado,
|
||||||
|
y_teste
|
||||||
|
)
|
||||||
|
|
||||||
|
ajuste_svm_radial$best.parameters %>%
|
||||||
|
kable()
|
||||||
|
} else {
|
||||||
|
warning("Poucas amostras por classe para validacao cruzada.")
|
||||||
|
ajuste_svm_radial <- NULL
|
||||||
|
modelo_svm_radial_ajustado <- modelo_svm_radial
|
||||||
|
pred_svm_radial_ajustado <- pred_svm_radial
|
||||||
|
cm_svm_radial_ajustado <- cm_svm_radial
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
cm_svm_radial_ajustado
|
||||||
|
```
|
||||||
|
|
||||||
|
# Modelo 4: PCA + SVM radial
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 13. PCA
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
pca <- prcomp(
|
||||||
|
x_treino_norm,
|
||||||
|
center = FALSE,
|
||||||
|
scale. = FALSE
|
||||||
|
)
|
||||||
|
|
||||||
|
variancia <- pca$sdev^2
|
||||||
|
variancia_exp <- variancia / sum(variancia)
|
||||||
|
variancia_acum <- cumsum(variancia_exp)
|
||||||
|
|
||||||
|
n_comp_95 <- which(variancia_acum >= 0.95)[1]
|
||||||
|
n_comp <- min(n_comp_95, 30, ncol(x_treino_norm))
|
||||||
|
|
||||||
|
cat("Componentes necessarios para 95% da variancia:", n_comp_95, "\n")
|
||||||
|
cat("Componentes usados no modelo:", n_comp, "\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
tibble(
|
||||||
|
componente = seq_along(variancia_acum),
|
||||||
|
variancia_acumulada = variancia_acum
|
||||||
|
) %>%
|
||||||
|
ggplot(aes(x = componente, y = variancia_acumulada)) +
|
||||||
|
geom_line(color = "#2C7FB8") +
|
||||||
|
geom_point(color = "#2C7FB8") +
|
||||||
|
geom_hline(yintercept = 0.95, linetype = "dashed") +
|
||||||
|
labs(
|
||||||
|
title = "Variancia acumulada pelo PCA (dataset completo)",
|
||||||
|
x = "Numero de componentes principais",
|
||||||
|
y = "Variancia acumulada"
|
||||||
|
) +
|
||||||
|
theme_minimal()
|
||||||
|
```
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
x_treino_pca <- as.data.frame(pca$x[, 1:n_comp, drop = FALSE])
|
||||||
|
|
||||||
|
x_teste_pca <- as.data.frame(
|
||||||
|
predict(pca, newdata = x_teste_norm)[, 1:n_comp, drop = FALSE]
|
||||||
|
)
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
modelo_svm_pca <- svm(
|
||||||
|
x = x_treino_pca,
|
||||||
|
y = y_treino,
|
||||||
|
kernel = "radial",
|
||||||
|
cost = 10,
|
||||||
|
gamma = 0.01,
|
||||||
|
scale = FALSE
|
||||||
|
)
|
||||||
|
|
||||||
|
pred_svm_pca <- predict(modelo_svm_pca, x_teste_pca)
|
||||||
|
|
||||||
|
cm_svm_pca <- confusionMatrix(pred_svm_pca, y_teste)
|
||||||
|
|
||||||
|
cm_svm_pca
|
||||||
|
```
|
||||||
|
|
||||||
|
# Modelo 5: Random Forest
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 14. Random Forest
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
modelo_rf <- randomForest(
|
||||||
|
x = x_treino_norm,
|
||||||
|
y = y_treino,
|
||||||
|
ntree = 500,
|
||||||
|
importance = TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
pred_rf <- predict(modelo_rf, x_teste_norm)
|
||||||
|
|
||||||
|
cm_rf <- confusionMatrix(pred_rf, y_teste)
|
||||||
|
|
||||||
|
cm_rf
|
||||||
|
```
|
||||||
|
|
||||||
|
# Comparacao dos modelos
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 15. Funcao de avaliacao dos modelos
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
avaliar_modelo <- function(nome, matriz_confusao) {
|
||||||
|
overall <- matriz_confusao$overall
|
||||||
|
by_class <- matriz_confusao$byClass
|
||||||
|
|
||||||
|
if (is.matrix(by_class)) {
|
||||||
|
sensibilidade_macro <- mean(by_class[, "Sensitivity"], na.rm = TRUE)
|
||||||
|
especificidade_macro <- mean(by_class[, "Specificity"], na.rm = TRUE)
|
||||||
|
f1_macro <- mean(by_class[, "F1"], na.rm = TRUE)
|
||||||
|
} else {
|
||||||
|
sensibilidade_macro <- by_class["Sensitivity"]
|
||||||
|
especificidade_macro <- by_class["Specificity"]
|
||||||
|
f1_macro <- by_class["F1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
tibble(
|
||||||
|
modelo = nome,
|
||||||
|
acuracia = as.numeric(overall["Accuracy"]),
|
||||||
|
kappa = as.numeric(overall["Kappa"]),
|
||||||
|
sensibilidade_macro = as.numeric(sensibilidade_macro),
|
||||||
|
especificidade_macro = as.numeric(especificidade_macro),
|
||||||
|
f1_macro = as.numeric(f1_macro)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultados <- bind_rows(
|
||||||
|
avaliar_modelo("SVM linear", cm_svm_linear),
|
||||||
|
avaliar_modelo("SVM radial", cm_svm_radial),
|
||||||
|
avaliar_modelo("SVM radial ajustado", cm_svm_radial_ajustado),
|
||||||
|
avaliar_modelo("PCA + SVM radial", cm_svm_pca),
|
||||||
|
avaliar_modelo("Random Forest", cm_rf)
|
||||||
|
) %>%
|
||||||
|
arrange(desc(acuracia))
|
||||||
|
|
||||||
|
resultados %>%
|
||||||
|
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
|
||||||
|
kable(caption = "Resultados - Dataset Completo (Holdout 70/30)")
|
||||||
|
```
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
ggplot(resultados, aes(x = reorder(modelo, acuracia), y = acuracia)) +
|
||||||
|
geom_col(fill = "#2C7FB8") +
|
||||||
|
coord_flip() +
|
||||||
|
labs(
|
||||||
|
title = "Comparacao da acuracia dos modelos (dataset completo)",
|
||||||
|
x = "Modelo",
|
||||||
|
y = "Acuracia"
|
||||||
|
) +
|
||||||
|
theme_minimal()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Importancia das variaveis
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 16. Importancia das variaveis
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
importancia <- importance(modelo_rf)
|
||||||
|
|
||||||
|
coluna_importancia <- if ("MeanDecreaseGini" %in% colnames(importancia)) {
|
||||||
|
"MeanDecreaseGini"
|
||||||
|
} else {
|
||||||
|
colnames(importancia)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
importancia_df <- tibble(
|
||||||
|
variavel = rownames(importancia),
|
||||||
|
importancia = importancia[, coluna_importancia]
|
||||||
|
) %>%
|
||||||
|
arrange(desc(importancia))
|
||||||
|
|
||||||
|
importancia_df %>%
|
||||||
|
slice_head(n = 20) %>%
|
||||||
|
kable()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Salvamento dos resultados
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 17. Salvar saidas (nomes diferentes do original)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
dir.create(pasta_saida, recursive = TRUE, showWarnings = FALSE)
|
||||||
|
|
||||||
|
write_csv(dados, file.path(pasta_saida, "features_rgb_completo.csv"))
|
||||||
|
write_csv(resultados, file.path(pasta_saida, "resultados_modelos_rgb_completo.csv"))
|
||||||
|
write_csv(importancia_df, file.path(pasta_saida, "importancia_variaveis_rgb_completo.csv"))
|
||||||
|
|
||||||
|
saveRDS(
|
||||||
|
list(
|
||||||
|
preproc = preproc,
|
||||||
|
modelo_svm_linear = modelo_svm_linear,
|
||||||
|
modelo_svm_radial = modelo_svm_radial,
|
||||||
|
ajuste_svm_radial = ajuste_svm_radial,
|
||||||
|
modelo_svm_radial_ajustado = modelo_svm_radial_ajustado,
|
||||||
|
modelo_svm_pca = modelo_svm_pca,
|
||||||
|
modelo_rf = modelo_rf,
|
||||||
|
pca = pca,
|
||||||
|
resultados = resultados,
|
||||||
|
extrator_cnn = "MobileNetV2 (ImageNet, sem topo, pooling=avg, 1280 features)"
|
||||||
|
),
|
||||||
|
file.path(pasta_saida, "modelos_rgb_completo.rds")
|
||||||
|
)
|
||||||
|
|
||||||
|
cat("Arquivos salvos em:", pasta_saida, "\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Validacao Cruzada K-Fold
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 18. Validacao cruzada k-fold (k=5, estratificada)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set.seed(123)
|
||||||
|
|
||||||
|
k_folds <- 5L
|
||||||
|
|
||||||
|
folds <- createFolds(dados$classe, k = k_folds, list = TRUE, returnTrain = FALSE)
|
||||||
|
|
||||||
|
grade_cost_kfold <- c(1, 10, 100)
|
||||||
|
grade_gamma_kfold <- c(0.001, 0.01, 0.05, 0.1)
|
||||||
|
|
||||||
|
metricas_folds <- vector("list", k_folds)
|
||||||
|
|
||||||
|
for (fold_i in seq_len(k_folds)) {
|
||||||
|
idx_teste <- folds[[fold_i]]
|
||||||
|
idx_treino <- setdiff(seq_len(nrow(dados)), idx_teste)
|
||||||
|
|
||||||
|
fold_treino <- dados[idx_treino, ]
|
||||||
|
fold_teste <- dados[idx_teste, ]
|
||||||
|
|
||||||
|
x_fold_tr <- fold_treino %>% select(-all_of(colunas_nao_preditoras))
|
||||||
|
x_fold_te <- fold_teste %>% select(-all_of(colunas_nao_preditoras))
|
||||||
|
y_fold_tr <- droplevels(fold_treino$classe)
|
||||||
|
y_fold_te <- factor(fold_teste$classe, levels = levels(y_fold_tr))
|
||||||
|
|
||||||
|
nzv_fold <- nearZeroVar(x_fold_tr)
|
||||||
|
if (length(nzv_fold) > 0) {
|
||||||
|
x_fold_tr <- x_fold_tr[, -nzv_fold, drop = FALSE]
|
||||||
|
x_fold_te <- x_fold_te[, colnames(x_fold_tr), drop = FALSE]
|
||||||
|
}
|
||||||
|
|
||||||
|
pp_fold <- preProcess(x_fold_tr, method = c("center", "scale"))
|
||||||
|
x_fold_tr_n <- predict(pp_fold, x_fold_tr)
|
||||||
|
x_fold_te_n <- predict(pp_fold, x_fold_te)
|
||||||
|
|
||||||
|
m1 <- svm(x = x_fold_tr_n, y = y_fold_tr,
|
||||||
|
kernel = "linear", cost = 1, scale = FALSE)
|
||||||
|
|
||||||
|
m2 <- svm(x = x_fold_tr_n, y = y_fold_tr,
|
||||||
|
kernel = "radial", cost = 10, gamma = 0.01, scale = FALSE)
|
||||||
|
|
||||||
|
k_cv_fold <- min(5L, min(table(y_fold_tr)))
|
||||||
|
ajuste_fold <- tune.svm(
|
||||||
|
x = x_fold_tr_n,
|
||||||
|
y = y_fold_tr,
|
||||||
|
kernel = "radial",
|
||||||
|
cost = grade_cost_kfold,
|
||||||
|
gamma = grade_gamma_kfold,
|
||||||
|
tunecontrol = tune.control(cross = k_cv_fold)
|
||||||
|
)
|
||||||
|
m3 <- ajuste_fold$best.model
|
||||||
|
|
||||||
|
pca_fold <- prcomp(x_fold_tr_n, center = FALSE, scale. = FALSE)
|
||||||
|
var_acum_f <- cumsum(pca_fold$sdev^2 / sum(pca_fold$sdev^2))
|
||||||
|
n_comp_f <- min(which(var_acum_f >= 0.95)[1], 30L, ncol(x_fold_tr_n))
|
||||||
|
x_pca_tr <- as.data.frame(pca_fold$x[, 1:n_comp_f, drop = FALSE])
|
||||||
|
x_pca_te <- as.data.frame(predict(pca_fold, x_fold_te_n)[, 1:n_comp_f, drop = FALSE])
|
||||||
|
m4 <- svm(x = x_pca_tr, y = y_fold_tr,
|
||||||
|
kernel = "radial", cost = 10, gamma = 0.01, scale = FALSE)
|
||||||
|
|
||||||
|
m5 <- randomForest(x = x_fold_tr_n, y = y_fold_tr, ntree = 200L)
|
||||||
|
|
||||||
|
metricas_folds[[fold_i]] <- bind_rows(
|
||||||
|
avaliar_modelo("SVM linear", confusionMatrix(predict(m1, x_fold_te_n), y_fold_te)),
|
||||||
|
avaliar_modelo("SVM radial", confusionMatrix(predict(m2, x_fold_te_n), y_fold_te)),
|
||||||
|
avaliar_modelo("SVM radial ajustado", confusionMatrix(predict(m3, x_fold_te_n), y_fold_te)),
|
||||||
|
avaliar_modelo("PCA + SVM radial", confusionMatrix(predict(m4, x_pca_te), y_fold_te)),
|
||||||
|
avaliar_modelo("Random Forest", confusionMatrix(predict(m5, x_fold_te_n), y_fold_te))
|
||||||
|
) %>% mutate(fold = fold_i)
|
||||||
|
|
||||||
|
cat(sprintf(
|
||||||
|
"Fold %d/%d concluido. SVM ajustado: cost = %s | gamma = %s\n",
|
||||||
|
fold_i, k_folds,
|
||||||
|
ajuste_fold$best.parameters$cost,
|
||||||
|
ajuste_fold$best.parameters$gamma
|
||||||
|
))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
resultados_kfold <- bind_rows(metricas_folds) %>%
|
||||||
|
group_by(modelo) %>%
|
||||||
|
summarise(across(c(acuracia, kappa, sensibilidade_macro, especificidade_macro, f1_macro), mean),
|
||||||
|
.groups = "drop") %>%
|
||||||
|
arrange(desc(acuracia))
|
||||||
|
|
||||||
|
write_csv(resultados_kfold, file.path(pasta_saida, "resultados_kfold_rgb_completo.csv"))
|
||||||
|
|
||||||
|
resultados_kfold %>%
|
||||||
|
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
|
||||||
|
kable(caption = "Acuracia media - K-Fold (k=5) - Dataset Completo")
|
||||||
|
```
|
||||||
|
|
||||||
|
# Comparacao: Holdout vs K-Fold
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
# ============================================================
|
||||||
|
# 19. Comparacao direta entre as duas estrategias
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
comparacao <- bind_rows(
|
||||||
|
resultados %>% mutate(estrategia = "Holdout 70/30"),
|
||||||
|
resultados_kfold %>% mutate(estrategia = "K-Fold (k=5)")
|
||||||
|
) %>%
|
||||||
|
select(estrategia, modelo, acuracia, kappa, f1_macro) %>%
|
||||||
|
arrange(estrategia, desc(acuracia))
|
||||||
|
|
||||||
|
comparacao %>%
|
||||||
|
mutate(across(where(is.numeric), ~ round(.x, 4))) %>%
|
||||||
|
kable(caption = "Comparacao entre Holdout 70/30 e K-Fold - Dataset Completo")
|
||||||
|
```
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
ggplot(comparacao, aes(x = reorder(modelo, acuracia), y = acuracia, fill = estrategia)) +
|
||||||
|
geom_col(position = "dodge") +
|
||||||
|
coord_flip() +
|
||||||
|
scale_fill_manual(values = c("Holdout 70/30" = "#2C7FB8", "K-Fold (k=5)" = "#E06C00")) +
|
||||||
|
labs(
|
||||||
|
title = "Acuracia: Holdout 70/30 vs K-Fold (k=5) - Dataset Completo",
|
||||||
|
x = "Modelo",
|
||||||
|
y = "Acuracia",
|
||||||
|
fill = "Estrategia"
|
||||||
|
) +
|
||||||
|
theme_minimal()
|
||||||
|
```
|
||||||
|
|
||||||
|
# Conclusao
|
||||||
|
|
||||||
|
```{r}
|
||||||
|
melhor_holdout <- max(resultados$acuracia)
|
||||||
|
melhor_kf <- max(resultados_kfold$acuracia)
|
||||||
|
|
||||||
|
cat("Acuracia maxima - Holdout:", round(melhor_holdout, 4), "\n")
|
||||||
|
cat("Acuracia maxima - K-Fold: ", round(melhor_kf, 4), "\n")
|
||||||
|
cat("Melhor modelo (holdout):", resultados$modelo[1], "\n")
|
||||||
|
cat("Melhor modelo (k-fold):", resultados_kfold$modelo[1], "\n")
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 15 KiB |
2106
trabalho_svm_rgb_completo_files/libs/bootstrap/bootstrap-icons.css
vendored
Normal file
7
trabalho_svm_rgb_completo_files/libs/bootstrap/bootstrap.min.js
vendored
Normal file
7
trabalho_svm_rgb_completo_files/libs/clipboard/clipboard.min.js
vendored
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
.pagedtable {
|
||||||
|
overflow: auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-wrapper {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable th {
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid #dddddd;
|
||||||
|
|
||||||
|
min-width: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-empty th {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable td {
|
||||||
|
padding: 0 4px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable .even {
|
||||||
|
background-color: rgba(140, 140, 140, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-padding-col {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable a {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-nav {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
float: right;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-nav-disabled {
|
||||||
|
cursor: default;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.pagedtable-index-nav-disabled:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-indexes {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-current {
|
||||||
|
cursor: default;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.pagedtable-index-current:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index {
|
||||||
|
width: 30px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-separator-left {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0 0 0 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-separator-right {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0 4px 0 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-footer {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-not-empty .pagedtable-footer {
|
||||||
|
border-top: 2px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-info {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #999;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-header-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-header-type {
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-na-cell {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
1151
trabalho_svm_rgb_completo_files/libs/pagedtable-1.1/js/pagedtable.js
Normal file
9
trabalho_svm_rgb_completo_files/libs/quarto-html/anchor.min.js
vendored
Normal file
6
trabalho_svm_rgb_completo_files/libs/quarto-html/popper.min.js
vendored
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/* quarto syntax highlight colors */
|
||||||
|
:root {
|
||||||
|
--quarto-hl-ot-color: #003B4F;
|
||||||
|
--quarto-hl-at-color: #657422;
|
||||||
|
--quarto-hl-ss-color: #20794D;
|
||||||
|
--quarto-hl-an-color: #5E5E5E;
|
||||||
|
--quarto-hl-fu-color: #4758AB;
|
||||||
|
--quarto-hl-st-color: #20794D;
|
||||||
|
--quarto-hl-cf-color: #003B4F;
|
||||||
|
--quarto-hl-op-color: #5E5E5E;
|
||||||
|
--quarto-hl-er-color: #AD0000;
|
||||||
|
--quarto-hl-bn-color: #AD0000;
|
||||||
|
--quarto-hl-al-color: #AD0000;
|
||||||
|
--quarto-hl-va-color: #111111;
|
||||||
|
--quarto-hl-bu-color: inherit;
|
||||||
|
--quarto-hl-ex-color: inherit;
|
||||||
|
--quarto-hl-pp-color: #AD0000;
|
||||||
|
--quarto-hl-in-color: #5E5E5E;
|
||||||
|
--quarto-hl-vs-color: #20794D;
|
||||||
|
--quarto-hl-wa-color: #5E5E5E;
|
||||||
|
--quarto-hl-do-color: #5E5E5E;
|
||||||
|
--quarto-hl-im-color: #00769E;
|
||||||
|
--quarto-hl-ch-color: #20794D;
|
||||||
|
--quarto-hl-dt-color: #AD0000;
|
||||||
|
--quarto-hl-fl-color: #AD0000;
|
||||||
|
--quarto-hl-co-color: #5E5E5E;
|
||||||
|
--quarto-hl-cv-color: #5E5E5E;
|
||||||
|
--quarto-hl-cn-color: #8f5902;
|
||||||
|
--quarto-hl-sc-color: #5E5E5E;
|
||||||
|
--quarto-hl-dv-color: #AD0000;
|
||||||
|
--quarto-hl-kw-color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* other quarto variables */
|
||||||
|
:root {
|
||||||
|
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* syntax highlight based on Pandoc's rules */
|
||||||
|
pre > code.sourceCode > span {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.sourceCode > span {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sourceCode,
|
||||||
|
div.sourceCode pre.sourceCode {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal */
|
||||||
|
code span {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
code span.al {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annotation */
|
||||||
|
code span.an {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute */
|
||||||
|
code span.at {
|
||||||
|
color: #657422;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BaseN */
|
||||||
|
code span.bn {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BuiltIn */
|
||||||
|
code span.bu {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ControlFlow */
|
||||||
|
code span.cf {
|
||||||
|
color: #003B4F;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Char */
|
||||||
|
code span.ch {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Constant */
|
||||||
|
code span.cn {
|
||||||
|
color: #8f5902;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comment */
|
||||||
|
code span.co {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CommentVar */
|
||||||
|
code span.cv {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Documentation */
|
||||||
|
code span.do {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DataType */
|
||||||
|
code span.dt {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DecVal */
|
||||||
|
code span.dv {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
code span.er {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extension */
|
||||||
|
code span.ex {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Float */
|
||||||
|
code span.fl {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Function */
|
||||||
|
code span.fu {
|
||||||
|
color: #4758AB;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import */
|
||||||
|
code span.im {
|
||||||
|
color: #00769E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Information */
|
||||||
|
code span.in {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyword */
|
||||||
|
code span.kw {
|
||||||
|
color: #003B4F;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Operator */
|
||||||
|
code span.op {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Other */
|
||||||
|
code span.ot {
|
||||||
|
color: #003B4F;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preprocessor */
|
||||||
|
code span.pp {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialChar */
|
||||||
|
code span.sc {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialString */
|
||||||
|
code span.ss {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* String */
|
||||||
|
code span.st {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variable */
|
||||||
|
code span.va {
|
||||||
|
color: #111111;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VerbatimString */
|
||||||
|
code span.vs {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
code span.wa {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevent-inlining {
|
||||||
|
content: "</";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */
|
||||||
845
trabalho_svm_rgb_completo_files/libs/quarto-html/quarto.js
Normal 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// grouped tabsets
|
||||||
|
|
||||||
|
export function init() {
|
||||||
|
window.addEventListener("pageshow", (_event) => {
|
||||||
|
function getTabSettings() {
|
||||||
|
const data = localStorage.getItem("quarto-persistent-tabsets-data");
|
||||||
|
if (!data) {
|
||||||
|
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTabSettings(data) {
|
||||||
|
localStorage.setItem(
|
||||||
|
"quarto-persistent-tabsets-data",
|
||||||
|
JSON.stringify(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTabState(groupName, groupValue) {
|
||||||
|
const data = getTabSettings();
|
||||||
|
data[groupName] = groupValue;
|
||||||
|
setTabSettings(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTab(tab, active) {
|
||||||
|
const tabPanelId = tab.getAttribute("aria-controls");
|
||||||
|
const tabPanel = document.getElementById(tabPanelId);
|
||||||
|
if (active) {
|
||||||
|
tab.classList.add("active");
|
||||||
|
tabPanel.classList.add("active");
|
||||||
|
} else {
|
||||||
|
tab.classList.remove("active");
|
||||||
|
tabPanel.classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll(selectedGroup, selectorsToSync) {
|
||||||
|
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
|
||||||
|
const active = selectedGroup === thisGroup;
|
||||||
|
for (const tab of tabs) {
|
||||||
|
toggleTab(tab, active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSelectorsToSyncByLanguage() {
|
||||||
|
const result = {};
|
||||||
|
const tabs = Array.from(
|
||||||
|
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
|
||||||
|
);
|
||||||
|
for (const item of tabs) {
|
||||||
|
const div = item.parentElement.parentElement.parentElement;
|
||||||
|
const group = div.getAttribute("data-group");
|
||||||
|
if (!result[group]) {
|
||||||
|
result[group] = {};
|
||||||
|
}
|
||||||
|
const selectorsToSync = result[group];
|
||||||
|
const value = item.innerHTML;
|
||||||
|
if (!selectorsToSync[value]) {
|
||||||
|
selectorsToSync[value] = [];
|
||||||
|
}
|
||||||
|
selectorsToSync[value].push(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSelectorSync() {
|
||||||
|
const selectorsToSync = findSelectorsToSyncByLanguage();
|
||||||
|
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
|
||||||
|
Object.entries(tabSetsByValue).forEach(([value, items]) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
item.addEventListener("click", (_event) => {
|
||||||
|
setTabState(group, value);
|
||||||
|
toggleAll(value, selectorsToSync[group]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return selectorsToSync;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectorsToSync = setupSelectorSync();
|
||||||
|
for (const [group, selectedName] of Object.entries(getTabSettings())) {
|
||||||
|
const selectors = selectorsToSync[group];
|
||||||
|
// it's possible that stale state gives us empty selections, so we explicitly check here.
|
||||||
|
if (selectors) {
|
||||||
|
toggleAll(selectedName, selectors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
|
||||||
2
trabalho_svm_rgb_completo_files/libs/quarto-html/tippy.umd.min.js
vendored
Normal file
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-18-1.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-20-1.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-23-1.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-24-1.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-26-1.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-28-1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-32-1.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-34-1.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
trabalho_svm_rgb_files/figure-html/unnamed-chunk-6-1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
12
trabalho_svm_rgb_files/libs/bootstrap/bootstrap-67270d1595a62d14f123c043d4fb2f18.min.css
vendored
Normal file
2106
trabalho_svm_rgb_files/libs/bootstrap/bootstrap-icons.css
vendored
Normal file
BIN
trabalho_svm_rgb_files/libs/bootstrap/bootstrap-icons.woff
Normal file
7
trabalho_svm_rgb_files/libs/bootstrap/bootstrap.min.js
vendored
Normal file
7
trabalho_svm_rgb_files/libs/clipboard/clipboard.min.js
vendored
Normal file
142
trabalho_svm_rgb_files/libs/pagedtable-1.1/css/pagedtable.css
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
.pagedtable {
|
||||||
|
overflow: auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-wrapper {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable th {
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid #dddddd;
|
||||||
|
|
||||||
|
min-width: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-empty th {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable td {
|
||||||
|
padding: 0 4px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable .even {
|
||||||
|
background-color: rgba(140, 140, 140, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-padding-col {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable a {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-nav {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
float: right;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-nav-disabled {
|
||||||
|
cursor: default;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.pagedtable-index-nav-disabled:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-indexes {
|
||||||
|
cursor: pointer;
|
||||||
|
float: right;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-current {
|
||||||
|
cursor: default;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.pagedtable-index-current:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index {
|
||||||
|
width: 30px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-separator-left {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0 0 0 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-index-separator-right {
|
||||||
|
display: inline-block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 0 4px 0 0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-footer {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-not-empty .pagedtable-footer {
|
||||||
|
border-top: 2px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-info {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #999;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-header-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-header-type {
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagedtable-na-cell {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
1151
trabalho_svm_rgb_files/libs/pagedtable-1.1/js/pagedtable.js
Normal file
9
trabalho_svm_rgb_files/libs/quarto-html/anchor.min.js
vendored
Normal file
6
trabalho_svm_rgb_files/libs/quarto-html/popper.min.js
vendored
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/* quarto syntax highlight colors */
|
||||||
|
:root {
|
||||||
|
--quarto-hl-ot-color: #003B4F;
|
||||||
|
--quarto-hl-at-color: #657422;
|
||||||
|
--quarto-hl-ss-color: #20794D;
|
||||||
|
--quarto-hl-an-color: #5E5E5E;
|
||||||
|
--quarto-hl-fu-color: #4758AB;
|
||||||
|
--quarto-hl-st-color: #20794D;
|
||||||
|
--quarto-hl-cf-color: #003B4F;
|
||||||
|
--quarto-hl-op-color: #5E5E5E;
|
||||||
|
--quarto-hl-er-color: #AD0000;
|
||||||
|
--quarto-hl-bn-color: #AD0000;
|
||||||
|
--quarto-hl-al-color: #AD0000;
|
||||||
|
--quarto-hl-va-color: #111111;
|
||||||
|
--quarto-hl-bu-color: inherit;
|
||||||
|
--quarto-hl-ex-color: inherit;
|
||||||
|
--quarto-hl-pp-color: #AD0000;
|
||||||
|
--quarto-hl-in-color: #5E5E5E;
|
||||||
|
--quarto-hl-vs-color: #20794D;
|
||||||
|
--quarto-hl-wa-color: #5E5E5E;
|
||||||
|
--quarto-hl-do-color: #5E5E5E;
|
||||||
|
--quarto-hl-im-color: #00769E;
|
||||||
|
--quarto-hl-ch-color: #20794D;
|
||||||
|
--quarto-hl-dt-color: #AD0000;
|
||||||
|
--quarto-hl-fl-color: #AD0000;
|
||||||
|
--quarto-hl-co-color: #5E5E5E;
|
||||||
|
--quarto-hl-cv-color: #5E5E5E;
|
||||||
|
--quarto-hl-cn-color: #8f5902;
|
||||||
|
--quarto-hl-sc-color: #5E5E5E;
|
||||||
|
--quarto-hl-dv-color: #AD0000;
|
||||||
|
--quarto-hl-kw-color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* other quarto variables */
|
||||||
|
:root {
|
||||||
|
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* syntax highlight based on Pandoc's rules */
|
||||||
|
pre > code.sourceCode > span {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.sourceCode > span {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sourceCode,
|
||||||
|
div.sourceCode pre.sourceCode {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normal */
|
||||||
|
code span {
|
||||||
|
color: #003B4F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert */
|
||||||
|
code span.al {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annotation */
|
||||||
|
code span.an {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute */
|
||||||
|
code span.at {
|
||||||
|
color: #657422;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BaseN */
|
||||||
|
code span.bn {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BuiltIn */
|
||||||
|
code span.bu {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ControlFlow */
|
||||||
|
code span.cf {
|
||||||
|
color: #003B4F;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Char */
|
||||||
|
code span.ch {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Constant */
|
||||||
|
code span.cn {
|
||||||
|
color: #8f5902;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comment */
|
||||||
|
code span.co {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CommentVar */
|
||||||
|
code span.cv {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Documentation */
|
||||||
|
code span.do {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DataType */
|
||||||
|
code span.dt {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DecVal */
|
||||||
|
code span.dv {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error */
|
||||||
|
code span.er {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extension */
|
||||||
|
code span.ex {
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Float */
|
||||||
|
code span.fl {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Function */
|
||||||
|
code span.fu {
|
||||||
|
color: #4758AB;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Import */
|
||||||
|
code span.im {
|
||||||
|
color: #00769E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Information */
|
||||||
|
code span.in {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyword */
|
||||||
|
code span.kw {
|
||||||
|
color: #003B4F;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Operator */
|
||||||
|
code span.op {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Other */
|
||||||
|
code span.ot {
|
||||||
|
color: #003B4F;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preprocessor */
|
||||||
|
code span.pp {
|
||||||
|
color: #AD0000;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialChar */
|
||||||
|
code span.sc {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SpecialString */
|
||||||
|
code span.ss {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* String */
|
||||||
|
code span.st {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variable */
|
||||||
|
code span.va {
|
||||||
|
color: #111111;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VerbatimString */
|
||||||
|
code span.vs {
|
||||||
|
color: #20794D;
|
||||||
|
font-style: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning */
|
||||||
|
code span.wa {
|
||||||
|
color: #5E5E5E;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevent-inlining {
|
||||||
|
content: "</";
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */
|
||||||
845
trabalho_svm_rgb_files/libs/quarto-html/quarto.js
Normal 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);
|
||||||
|
}
|
||||||
95
trabalho_svm_rgb_files/libs/quarto-html/tabsets/tabsets.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
1
trabalho_svm_rgb_files/libs/quarto-html/tippy.css
Normal 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}
|
||||||