Initial commit

This commit is contained in:
Cesa-V
2026-06-16 20:01:36 -03:00
commit e317e16cdc
91 changed files with 32046 additions and 0 deletions

10
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

BIN
SVM GUARA 802.pdf Normal file

Binary file not shown.

2931
apresentacao_svm_rgb.html Normal file

File diff suppressed because one or more lines are too long

BIN
apresentacao_svm_rgb.pptx Normal file

Binary file not shown.

367
apresentacao_svm_rgb.qmd Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

View File

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

651
outputs/features_rgb.csv Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

BIN
outputs/modelos_rgb.rds Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,6 @@
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
SVM linear,0.40153846153846157,0.3516666666666666,0.40153846153846157,0.9501282051282052,0.40929682290313046
SVM radial ajustado,0.38,0.3283333333333333,0.38,0.9483333333333333,0.3905066510222767
PCA + SVM radial,0.3676923076923077,0.31499999999999995,0.3676923076923077,0.9473076923076923,0.37836144953604534
Random Forest,0.35692307692307695,0.3033333333333333,0.35692307692307695,0.9464102564102563,0.38061565488169397
SVM radial,0.15846153846153846,0.08833333333333333,0.15846153846153846,0.9298717948717948,0.22329423850231678
1 modelo acuracia kappa sensibilidade_macro especificidade_macro f1_macro
2 SVM linear 0.40153846153846157 0.3516666666666666 0.40153846153846157 0.9501282051282052 0.40929682290313046
3 SVM radial ajustado 0.38 0.3283333333333333 0.38 0.9483333333333333 0.3905066510222767
4 PCA + SVM radial 0.3676923076923077 0.31499999999999995 0.3676923076923077 0.9473076923076923 0.37836144953604534
5 Random Forest 0.35692307692307695 0.3033333333333333 0.35692307692307695 0.9464102564102563 0.38061565488169397
6 SVM radial 0.15846153846153846 0.08833333333333333 0.15846153846153846 0.9298717948717948 0.22329423850231678

View File

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

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

View File

@@ -0,0 +1,6 @@
modelo,acuracia,kappa,sensibilidade_macro,especificidade_macro,f1_macro
SVM linear,0.40153846153846157,0.3516666666666666,0.40153846153846157,0.9501282051282052,0.40929682290313046
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
1 modelo acuracia kappa sensibilidade_macro especificidade_macro f1_macro
2 SVM linear 0.40153846153846157 0.3516666666666666 0.40153846153846157 0.9501282051282052 0.40929682290313046
3 PCA + SVM radial 0.3676923076923077 0.31499999999999995 0.3676923076923077 0.9473076923076923 0.37836144953604534
4 SVM radial ajustado 0.36153846153846153 0.3083333333333333 0.36153846153846153 0.9467948717948718 0.3667814495681257
5 Random Forest 0.3476923076923077 0.2933333333333333 0.3476923076923077 0.9456410256410256 0.35022414446407363
6 SVM radial 0.15846153846153846 0.08833333333333333 0.15846153846153846 0.9298717948717948 0.22329423850231678

View File

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

2701
trabalho_svm_rgb.html Normal file

File diff suppressed because it is too large Load Diff

1037
trabalho_svm_rgb.qmd Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,142 @@
.pagedtable {
overflow: auto;
padding-left: 8px;
padding-right: 8px;
}
.pagedtable-wrapper {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
}
.pagedtable table {
width: 100%;
max-width: 100%;
margin: 0;
}
.pagedtable th {
padding: 0 5px 0 5px;
border: none;
border-bottom: 2px solid #dddddd;
min-width: 45px;
}
.pagedtable-empty th {
display: none;
}
.pagedtable td {
padding: 0 4px 0 4px;
}
.pagedtable .even {
background-color: rgba(140, 140, 140, 0.1);
}
.pagedtable-padding-col {
display: none;
}
.pagedtable a {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pagedtable-index-nav {
cursor: pointer;
padding: 0 5px 0 5px;
float: right;
border: 0;
}
.pagedtable-index-nav-disabled {
cursor: default;
text-decoration: none;
color: #999;
}
a.pagedtable-index-nav-disabled:hover {
text-decoration: none;
color: #999;
}
.pagedtable-indexes {
cursor: pointer;
float: right;
border: 0;
}
.pagedtable-index-current {
cursor: default;
text-decoration: none;
font-weight: bold;
color: #333;
border: 0;
}
a.pagedtable-index-current:hover {
text-decoration: none;
font-weight: bold;
color: #333;
}
.pagedtable-index {
width: 30px;
display: inline-block;
text-align: center;
border: 0;
}
.pagedtable-index-separator-left {
display: inline-block;
color: #333;
font-size: 9px;
padding: 0 0 0 0;
cursor: default;
}
.pagedtable-index-separator-right {
display: inline-block;
color: #333;
font-size: 9px;
padding: 0 4px 0 0;
cursor: default;
}
.pagedtable-footer {
padding-top: 4px;
padding-bottom: 5px;
}
.pagedtable-not-empty .pagedtable-footer {
border-top: 2px solid #dddddd;
}
.pagedtable-info {
overflow: hidden;
color: #999;
white-space: nowrap;
text-overflow: ellipsis;
}
.pagedtable-header-name {
overflow: hidden;
text-overflow: ellipsis;
}
.pagedtable-header-type {
color: #999;
font-weight: 400;
}
.pagedtable-na-cell {
font-style: italic;
opacity: 0.3;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,236 @@
/* quarto syntax highlight colors */
:root {
--quarto-hl-ot-color: #003B4F;
--quarto-hl-at-color: #657422;
--quarto-hl-ss-color: #20794D;
--quarto-hl-an-color: #5E5E5E;
--quarto-hl-fu-color: #4758AB;
--quarto-hl-st-color: #20794D;
--quarto-hl-cf-color: #003B4F;
--quarto-hl-op-color: #5E5E5E;
--quarto-hl-er-color: #AD0000;
--quarto-hl-bn-color: #AD0000;
--quarto-hl-al-color: #AD0000;
--quarto-hl-va-color: #111111;
--quarto-hl-bu-color: inherit;
--quarto-hl-ex-color: inherit;
--quarto-hl-pp-color: #AD0000;
--quarto-hl-in-color: #5E5E5E;
--quarto-hl-vs-color: #20794D;
--quarto-hl-wa-color: #5E5E5E;
--quarto-hl-do-color: #5E5E5E;
--quarto-hl-im-color: #00769E;
--quarto-hl-ch-color: #20794D;
--quarto-hl-dt-color: #AD0000;
--quarto-hl-fl-color: #AD0000;
--quarto-hl-co-color: #5E5E5E;
--quarto-hl-cv-color: #5E5E5E;
--quarto-hl-cn-color: #8f5902;
--quarto-hl-sc-color: #5E5E5E;
--quarto-hl-dv-color: #AD0000;
--quarto-hl-kw-color: #003B4F;
}
/* other quarto variables */
:root {
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* syntax highlight based on Pandoc's rules */
pre > code.sourceCode > span {
color: #003B4F;
}
code.sourceCode > span {
color: #003B4F;
}
div.sourceCode,
div.sourceCode pre.sourceCode {
color: #003B4F;
}
/* Normal */
code span {
color: #003B4F;
}
/* Alert */
code span.al {
color: #AD0000;
font-style: inherit;
}
/* Annotation */
code span.an {
color: #5E5E5E;
font-style: inherit;
}
/* Attribute */
code span.at {
color: #657422;
font-style: inherit;
}
/* BaseN */
code span.bn {
color: #AD0000;
font-style: inherit;
}
/* BuiltIn */
code span.bu {
font-style: inherit;
}
/* ControlFlow */
code span.cf {
color: #003B4F;
font-weight: bold;
font-style: inherit;
}
/* Char */
code span.ch {
color: #20794D;
font-style: inherit;
}
/* Constant */
code span.cn {
color: #8f5902;
font-style: inherit;
}
/* Comment */
code span.co {
color: #5E5E5E;
font-style: inherit;
}
/* CommentVar */
code span.cv {
color: #5E5E5E;
font-style: italic;
}
/* Documentation */
code span.do {
color: #5E5E5E;
font-style: italic;
}
/* DataType */
code span.dt {
color: #AD0000;
font-style: inherit;
}
/* DecVal */
code span.dv {
color: #AD0000;
font-style: inherit;
}
/* Error */
code span.er {
color: #AD0000;
font-style: inherit;
}
/* Extension */
code span.ex {
font-style: inherit;
}
/* Float */
code span.fl {
color: #AD0000;
font-style: inherit;
}
/* Function */
code span.fu {
color: #4758AB;
font-style: inherit;
}
/* Import */
code span.im {
color: #00769E;
font-style: inherit;
}
/* Information */
code span.in {
color: #5E5E5E;
font-style: inherit;
}
/* Keyword */
code span.kw {
color: #003B4F;
font-weight: bold;
font-style: inherit;
}
/* Operator */
code span.op {
color: #5E5E5E;
font-style: inherit;
}
/* Other */
code span.ot {
color: #003B4F;
font-style: inherit;
}
/* Preprocessor */
code span.pp {
color: #AD0000;
font-style: inherit;
}
/* SpecialChar */
code span.sc {
color: #5E5E5E;
font-style: inherit;
}
/* SpecialString */
code span.ss {
color: #20794D;
font-style: inherit;
}
/* String */
code span.st {
color: #20794D;
font-style: inherit;
}
/* Variable */
code span.va {
color: #111111;
font-style: inherit;
}
/* VerbatimString */
code span.vs {
color: #20794D;
font-style: inherit;
}
/* Warning */
code span.wa {
color: #5E5E5E;
font-style: italic;
}
.prevent-inlining {
content: "</";
}
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */

View File

@@ -0,0 +1,845 @@
import * as tabsets from "./tabsets/tabsets.js";
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
detail: {},
bubbles: true,
cancelable: false,
composed: false,
});
const layoutMarginEls = () => {
// Find any conflicting margin elements and add margins to the
// top to prevent overlap
const marginChildren = window.document.querySelectorAll(
".column-margin.column-container > *, .margin-caption, .aside"
);
let lastBottom = 0;
for (const marginChild of marginChildren) {
if (marginChild.offsetParent !== null) {
// clear the top margin so we recompute it
marginChild.style.marginTop = null;
const top = marginChild.getBoundingClientRect().top + window.scrollY;
if (top < lastBottom) {
const marginChildStyle = window.getComputedStyle(marginChild);
const marginBottom = parseFloat(marginChildStyle["marginBottom"]);
const margin = lastBottom - top + marginBottom;
marginChild.style.marginTop = `${margin}px`;
}
const styles = window.getComputedStyle(marginChild);
const marginTop = parseFloat(styles["marginTop"]);
lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;
}
}
};
window.document.addEventListener("DOMContentLoaded", function (_event) {
// Recompute the position of margin elements anytime the body size changes
if (window.ResizeObserver) {
const resizeObserver = new window.ResizeObserver(
throttle(() => {
layoutMarginEls();
if (
window.document.body.getBoundingClientRect().width < 990 &&
isReaderMode()
) {
quartoToggleReader();
}
}, 50)
);
resizeObserver.observe(window.document.body);
}
const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');
const sidebarEl = window.document.getElementById("quarto-sidebar");
const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");
const marginSidebarEl = window.document.getElementById(
"quarto-margin-sidebar"
);
// function to determine whether the element has a previous sibling that is active
const prevSiblingIsActiveLink = (el) => {
const sibling = el.previousElementSibling;
if (sibling && sibling.tagName === "A") {
return sibling.classList.contains("active");
} else {
return false;
}
};
// dispatch for htmlwidgets
// they use slideenter event to trigger resize
function fireSlideEnter() {
const event = window.document.createEvent("Event");
event.initEvent("slideenter", true, true);
window.document.dispatchEvent(event);
}
const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
tabs.forEach((tab) => {
tab.addEventListener("shown.bs.tab", fireSlideEnter);
});
// dispatch for shiny
// they use BS shown and hidden events to trigger rendering
function distpatchShinyEvents(previous, current) {
if (window.jQuery) {
if (previous) {
window.jQuery(previous).trigger("hidden");
}
if (current) {
window.jQuery(current).trigger("shown");
}
}
}
// tabby.js listener: Trigger event for htmlwidget and shiny
document.addEventListener(
"tabby",
function (event) {
fireSlideEnter();
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
},
false
);
// Track scrolling and mark TOC links as active
// get table of contents and sidebar (bail if we don't have at least one)
const tocLinks = tocEl
? [...tocEl.querySelectorAll("a[data-scroll-target]")]
: [];
const makeActive = (link) => tocLinks[link].classList.add("active");
const removeActive = (link) => tocLinks[link].classList.remove("active");
const removeAllActive = () =>
[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));
// activate the anchor for a section associated with this TOC entry
tocLinks.forEach((link) => {
link.addEventListener("click", () => {
if (link.href.indexOf("#") !== -1) {
const anchor = link.href.split("#")[1];
const heading = window.document.querySelector(
`[data-anchor-id="${anchor}"]`
);
if (heading) {
// Add the class
heading.classList.add("reveal-anchorjs-link");
// function to show the anchor
const handleMouseout = () => {
heading.classList.remove("reveal-anchorjs-link");
heading.removeEventListener("mouseout", handleMouseout);
};
// add a function to clear the anchor when the user mouses out of it
heading.addEventListener("mouseout", handleMouseout);
}
}
});
});
const sections = tocLinks.map((link) => {
const target = link.getAttribute("data-scroll-target");
if (target.startsWith("#")) {
return window.document.getElementById(decodeURI(`${target.slice(1)}`));
} else {
return window.document.querySelector(decodeURI(`${target}`));
}
});
const sectionMargin = 200;
let currentActive = 0;
// track whether we've initialized state the first time
let init = false;
const updateActiveLink = () => {
// The index from bottom to top (e.g. reversed list)
let sectionIndex = -1;
if (
window.innerHeight + window.pageYOffset >=
window.document.body.offsetHeight
) {
// This is the no-scroll case where last section should be the active one
sectionIndex = 0;
} else {
// This finds the last section visible on screen that should be made active
sectionIndex = [...sections].reverse().findIndex((section) => {
if (section) {
return window.pageYOffset >= section.offsetTop - sectionMargin;
} else {
return false;
}
});
}
if (sectionIndex > -1) {
const current = sections.length - sectionIndex - 1;
if (current !== currentActive) {
removeAllActive();
currentActive = current;
makeActive(current);
if (init) {
window.dispatchEvent(sectionChanged);
}
init = true;
}
}
};
const inHiddenRegion = (top, bottom, hiddenRegions) => {
for (const region of hiddenRegions) {
if (top <= region.bottom && bottom >= region.top) {
return true;
}
}
return false;
};
const categorySelector = "header.quarto-title-block .quarto-category";
const activateCategories = (href) => {
// Find any categories
// Surround them with a link pointing back to:
// #category=Authoring
try {
const categoryEls = window.document.querySelectorAll(categorySelector);
for (const categoryEl of categoryEls) {
const categoryText = categoryEl.textContent;
if (categoryText) {
const link = `${href}#category=${encodeURIComponent(categoryText)}`;
const linkEl = window.document.createElement("a");
linkEl.setAttribute("href", link);
for (const child of categoryEl.childNodes) {
linkEl.append(child);
}
categoryEl.appendChild(linkEl);
}
}
} catch {
// Ignore errors
}
};
function hasTitleCategories() {
return window.document.querySelector(categorySelector) !== null;
}
function offsetRelativeUrl(url) {
const offset = getMeta("quarto:offset");
return offset ? offset + url : url;
}
function offsetAbsoluteUrl(url) {
const offset = getMeta("quarto:offset");
const baseUrl = new URL(offset, window.location);
const projRelativeUrl = url.replace(baseUrl, "");
if (projRelativeUrl.startsWith("/")) {
return projRelativeUrl;
} else {
return "/" + projRelativeUrl;
}
}
// read a meta tag value
function getMeta(metaName) {
const metas = window.document.getElementsByTagName("meta");
for (let i = 0; i < metas.length; i++) {
if (metas[i].getAttribute("name") === metaName) {
return metas[i].getAttribute("content");
}
}
return "";
}
async function findAndActivateCategories() {
// Categories search with listing only use path without query
const currentPagePath = offsetAbsoluteUrl(
window.location.origin + window.location.pathname
);
const response = await fetch(offsetRelativeUrl("listings.json"));
if (response.status == 200) {
return response.json().then(function (listingPaths) {
const listingHrefs = [];
for (const listingPath of listingPaths) {
const pathWithoutLeadingSlash = listingPath.listing.substring(1);
for (const item of listingPath.items) {
const encodedItem = encodeURI(item);
if (
encodedItem === currentPagePath ||
encodedItem === currentPagePath + "index.html"
) {
// Resolve this path against the offset to be sure
// we already are using the correct path to the listing
// (this adjusts the listing urls to be rooted against
// whatever root the page is actually running against)
const relative = offsetRelativeUrl(pathWithoutLeadingSlash);
const baseUrl = window.location;
const resolvedPath = new URL(relative, baseUrl);
listingHrefs.push(resolvedPath.pathname);
break;
}
}
}
// Look up the tree for a nearby linting and use that if we find one
const nearestListing = findNearestParentListing(
offsetAbsoluteUrl(window.location.pathname),
listingHrefs
);
if (nearestListing) {
activateCategories(nearestListing);
} else {
// See if the referrer is a listing page for this item
const referredRelativePath = offsetAbsoluteUrl(document.referrer);
const referrerListing = listingHrefs.find((listingHref) => {
const isListingReferrer =
listingHref === referredRelativePath ||
listingHref === referredRelativePath + "index.html";
return isListingReferrer;
});
if (referrerListing) {
// Try to use the referrer if possible
activateCategories(referrerListing);
} else if (listingHrefs.length > 0) {
// Otherwise, just fall back to the first listing
activateCategories(listingHrefs[0]);
}
}
});
}
}
if (hasTitleCategories()) {
findAndActivateCategories();
}
const findNearestParentListing = (href, listingHrefs) => {
if (!href || !listingHrefs) {
return undefined;
}
// Look up the tree for a nearby linting and use that if we find one
const relativeParts = href.substring(1).split("/");
while (relativeParts.length > 0) {
const path = relativeParts.join("/");
for (const listingHref of listingHrefs) {
if (listingHref.startsWith(path)) {
return listingHref;
}
}
relativeParts.pop();
}
return undefined;
};
const manageSidebarVisiblity = (el, placeholderDescriptor) => {
let isVisible = true;
let elRect;
return (hiddenRegions) => {
if (el === null) {
return;
}
// Find the last element of the TOC
const lastChildEl = el.lastElementChild;
if (lastChildEl) {
// Converts the sidebar to a menu
const convertToMenu = () => {
for (const child of el.children) {
child.style.opacity = 0;
child.style.overflow = "hidden";
child.style.pointerEvents = "none";
}
nexttick(() => {
const toggleContainer = window.document.createElement("div");
toggleContainer.style.width = "100%";
toggleContainer.classList.add("zindex-over-content");
toggleContainer.classList.add("quarto-sidebar-toggle");
toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom
toggleContainer.id = placeholderDescriptor.id;
toggleContainer.style.position = "fixed";
const toggleIcon = window.document.createElement("i");
toggleIcon.classList.add("quarto-sidebar-toggle-icon");
toggleIcon.classList.add("bi");
toggleIcon.classList.add("bi-caret-down-fill");
const toggleTitle = window.document.createElement("div");
const titleEl = window.document.body.querySelector(
placeholderDescriptor.titleSelector
);
if (titleEl) {
toggleTitle.append(
titleEl.textContent || titleEl.innerText,
toggleIcon
);
}
toggleTitle.classList.add("zindex-over-content");
toggleTitle.classList.add("quarto-sidebar-toggle-title");
toggleContainer.append(toggleTitle);
const toggleContents = window.document.createElement("div");
toggleContents.classList = el.classList;
toggleContents.classList.add("zindex-over-content");
toggleContents.classList.add("quarto-sidebar-toggle-contents");
for (const child of el.children) {
if (child.id === "toc-title") {
continue;
}
const clone = child.cloneNode(true);
clone.style.opacity = 1;
clone.style.pointerEvents = null;
clone.style.display = null;
toggleContents.append(clone);
}
toggleContents.style.height = "0px";
const positionToggle = () => {
// position the element (top left of parent, same width as parent)
if (!elRect) {
elRect = el.getBoundingClientRect();
}
toggleContainer.style.left = `${elRect.left}px`;
toggleContainer.style.top = `${elRect.top}px`;
toggleContainer.style.width = `${elRect.width}px`;
};
positionToggle();
toggleContainer.append(toggleContents);
el.parentElement.prepend(toggleContainer);
// Process clicks
let tocShowing = false;
// Allow the caller to control whether this is dismissed
// when it is clicked (e.g. sidebar navigation supports
// opening and closing the nav tree, so don't dismiss on click)
const clickEl = placeholderDescriptor.dismissOnClick
? toggleContainer
: toggleTitle;
const closeToggle = () => {
if (tocShowing) {
toggleContainer.classList.remove("expanded");
toggleContents.style.height = "0px";
tocShowing = false;
}
};
// Get rid of any expanded toggle if the user scrolls
window.document.addEventListener(
"scroll",
throttle(() => {
closeToggle();
}, 50)
);
// Handle positioning of the toggle
window.addEventListener(
"resize",
throttle(() => {
elRect = undefined;
positionToggle();
}, 50)
);
window.addEventListener("quarto-hrChanged", () => {
elRect = undefined;
});
// Process the click
clickEl.onclick = () => {
if (!tocShowing) {
toggleContainer.classList.add("expanded");
toggleContents.style.height = null;
tocShowing = true;
} else {
closeToggle();
}
};
});
};
// Converts a sidebar from a menu back to a sidebar
const convertToSidebar = () => {
for (const child of el.children) {
child.style.opacity = 1;
child.style.overflow = null;
child.style.pointerEvents = null;
}
const placeholderEl = window.document.getElementById(
placeholderDescriptor.id
);
if (placeholderEl) {
placeholderEl.remove();
}
el.classList.remove("rollup");
};
if (isReaderMode()) {
convertToMenu();
isVisible = false;
} else {
// Find the top and bottom o the element that is being managed
const elTop = el.offsetTop;
const elBottom =
elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;
if (!isVisible) {
// If the element is current not visible reveal if there are
// no conflicts with overlay regions
if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {
convertToSidebar();
isVisible = true;
}
} else {
// If the element is visible, hide it if it conflicts with overlay regions
// and insert a placeholder toggle (or if we're in reader mode)
if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {
convertToMenu();
isVisible = false;
}
}
}
}
};
};
const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');
for (const tabEl of tabEls) {
const id = tabEl.getAttribute("data-bs-target");
if (id) {
const columnEl = document.querySelector(
`${id} .column-margin, .tabset-margin-content`
);
if (columnEl)
tabEl.addEventListener("shown.bs.tab", function (event) {
const el = event.srcElement;
if (el) {
const visibleCls = `${el.id}-margin-content`;
// walk up until we find a parent tabset
let panelTabsetEl = el.parentElement;
while (panelTabsetEl) {
if (panelTabsetEl.classList.contains("panel-tabset")) {
break;
}
panelTabsetEl = panelTabsetEl.parentElement;
}
if (panelTabsetEl) {
const prevSib = panelTabsetEl.previousElementSibling;
if (
prevSib &&
prevSib.classList.contains("tabset-margin-container")
) {
const childNodes = prevSib.querySelectorAll(
".tabset-margin-content"
);
for (const childEl of childNodes) {
if (childEl.classList.contains(visibleCls)) {
childEl.classList.remove("collapse");
} else {
childEl.classList.add("collapse");
}
}
}
}
}
layoutMarginEls();
});
}
}
// Manage the visibility of the toc and the sidebar
const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {
id: "quarto-toc-toggle",
titleSelector: "#toc-title",
dismissOnClick: true,
});
const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {
id: "quarto-sidebarnav-toggle",
titleSelector: ".title",
dismissOnClick: false,
});
let tocLeftScrollVisibility;
if (leftTocEl) {
tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {
id: "quarto-lefttoc-toggle",
titleSelector: "#toc-title",
dismissOnClick: true,
});
}
// Find the first element that uses formatting in special columns
const conflictingEls = window.document.body.querySelectorAll(
'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'
);
// Filter all the possibly conflicting elements into ones
// the do conflict on the left or ride side
const arrConflictingEls = Array.from(conflictingEls);
const leftSideConflictEls = arrConflictingEls.filter((el) => {
if (el.tagName === "ASIDE") {
return false;
}
return Array.from(el.classList).find((className) => {
return (
className !== "column-body" &&
className.startsWith("column-") &&
!className.endsWith("right") &&
!className.endsWith("container") &&
className !== "column-margin"
);
});
});
const rightSideConflictEls = arrConflictingEls.filter((el) => {
if (el.tagName === "ASIDE") {
return true;
}
const hasMarginCaption = Array.from(el.classList).find((className) => {
return className == "margin-caption";
});
if (hasMarginCaption) {
return true;
}
return Array.from(el.classList).find((className) => {
return (
className !== "column-body" &&
!className.endsWith("container") &&
className.startsWith("column-") &&
!className.endsWith("left")
);
});
});
const kOverlapPaddingSize = 10;
function toRegions(els) {
return els.map((el) => {
const boundRect = el.getBoundingClientRect();
const top =
boundRect.top +
document.documentElement.scrollTop -
kOverlapPaddingSize;
return {
top,
bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,
};
});
}
let hasObserved = false;
const visibleItemObserver = (els) => {
let visibleElements = [...els];
const intersectionObserver = new IntersectionObserver(
(entries, _observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (visibleElements.indexOf(entry.target) === -1) {
visibleElements.push(entry.target);
}
} else {
visibleElements = visibleElements.filter((visibleEntry) => {
return visibleEntry !== entry;
});
}
});
if (!hasObserved) {
hideOverlappedSidebars();
}
hasObserved = true;
},
{}
);
els.forEach((el) => {
intersectionObserver.observe(el);
});
return {
getVisibleEntries: () => {
return visibleElements;
},
};
};
const rightElementObserver = visibleItemObserver(rightSideConflictEls);
const leftElementObserver = visibleItemObserver(leftSideConflictEls);
const hideOverlappedSidebars = () => {
marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));
sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));
if (tocLeftScrollVisibility) {
tocLeftScrollVisibility(
toRegions(leftElementObserver.getVisibleEntries())
);
}
};
window.quartoToggleReader = () => {
// Applies a slow class (or removes it)
// to update the transition speed
const slowTransition = (slow) => {
const manageTransition = (id, slow) => {
const el = document.getElementById(id);
if (el) {
if (slow) {
el.classList.add("slow");
} else {
el.classList.remove("slow");
}
}
};
manageTransition("TOC", slow);
manageTransition("quarto-sidebar", slow);
};
const readerMode = !isReaderMode();
setReaderModeValue(readerMode);
// If we're entering reader mode, slow the transition
if (readerMode) {
slowTransition(readerMode);
}
highlightReaderToggle(readerMode);
hideOverlappedSidebars();
// If we're exiting reader mode, restore the non-slow transition
if (!readerMode) {
slowTransition(!readerMode);
}
};
const highlightReaderToggle = (readerMode) => {
const els = document.querySelectorAll(".quarto-reader-toggle");
if (els) {
els.forEach((el) => {
if (readerMode) {
el.classList.add("reader");
} else {
el.classList.remove("reader");
}
});
}
};
const setReaderModeValue = (val) => {
if (window.location.protocol !== "file:") {
window.localStorage.setItem("quarto-reader-mode", val);
} else {
localReaderMode = val;
}
};
const isReaderMode = () => {
if (window.location.protocol !== "file:") {
return window.localStorage.getItem("quarto-reader-mode") === "true";
} else {
return localReaderMode;
}
};
let localReaderMode = null;
const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");
const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;
// Walk the TOC and collapse/expand nodes
// Nodes are expanded if:
// - they are top level
// - they have children that are 'active' links
// - they are directly below an link that is 'active'
const walk = (el, depth) => {
// Tick depth when we enter a UL
if (el.tagName === "UL") {
depth = depth + 1;
}
// It this is active link
let isActiveNode = false;
if (el.tagName === "A" && el.classList.contains("active")) {
isActiveNode = true;
}
// See if there is an active child to this element
let hasActiveChild = false;
for (const child of el.children) {
hasActiveChild = walk(child, depth) || hasActiveChild;
}
// Process the collapse state if this is an UL
if (el.tagName === "UL") {
if (tocOpenDepth === -1 && depth > 1) {
// toc-expand: false
el.classList.add("collapse");
} else if (
depth <= tocOpenDepth ||
hasActiveChild ||
prevSiblingIsActiveLink(el)
) {
el.classList.remove("collapse");
} else {
el.classList.add("collapse");
}
// untick depth when we leave a UL
depth = depth - 1;
}
return hasActiveChild || isActiveNode;
};
// walk the TOC and expand / collapse any items that should be shown
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
// Throttle the scroll event and walk peridiocally
window.document.addEventListener(
"scroll",
throttle(() => {
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
if (!isReaderMode()) {
hideOverlappedSidebars();
}
}, 5)
);
window.addEventListener(
"resize",
throttle(() => {
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
if (!isReaderMode()) {
hideOverlappedSidebars();
}
}, 10)
);
hideOverlappedSidebars();
highlightReaderToggle(isReaderMode());
});
tabsets.init();
function throttle(func, wait) {
let waiting = false;
return function () {
if (!waiting) {
func.apply(this, arguments);
waiting = true;
setTimeout(function () {
waiting = false;
}, wait);
}
};
}
function nexttick(func) {
return setTimeout(func, 0);
}

View File

@@ -0,0 +1,95 @@
// grouped tabsets
export function init() {
window.addEventListener("pageshow", (_event) => {
function getTabSettings() {
const data = localStorage.getItem("quarto-persistent-tabsets-data");
if (!data) {
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
return {};
}
if (data) {
return JSON.parse(data);
}
}
function setTabSettings(data) {
localStorage.setItem(
"quarto-persistent-tabsets-data",
JSON.stringify(data)
);
}
function setTabState(groupName, groupValue) {
const data = getTabSettings();
data[groupName] = groupValue;
setTabSettings(data);
}
function toggleTab(tab, active) {
const tabPanelId = tab.getAttribute("aria-controls");
const tabPanel = document.getElementById(tabPanelId);
if (active) {
tab.classList.add("active");
tabPanel.classList.add("active");
} else {
tab.classList.remove("active");
tabPanel.classList.remove("active");
}
}
function toggleAll(selectedGroup, selectorsToSync) {
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
const active = selectedGroup === thisGroup;
for (const tab of tabs) {
toggleTab(tab, active);
}
}
}
function findSelectorsToSyncByLanguage() {
const result = {};
const tabs = Array.from(
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
);
for (const item of tabs) {
const div = item.parentElement.parentElement.parentElement;
const group = div.getAttribute("data-group");
if (!result[group]) {
result[group] = {};
}
const selectorsToSync = result[group];
const value = item.innerHTML;
if (!selectorsToSync[value]) {
selectorsToSync[value] = [];
}
selectorsToSync[value].push(item);
}
return result;
}
function setupSelectorSync() {
const selectorsToSync = findSelectorsToSyncByLanguage();
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
Object.entries(tabSetsByValue).forEach(([value, items]) => {
items.forEach((item) => {
item.addEventListener("click", (_event) => {
setTabState(group, value);
toggleAll(value, selectorsToSync[group]);
});
});
});
});
return selectorsToSync;
}
const selectorsToSync = setupSelectorSync();
for (const [group, selectedName] of Object.entries(getTabSettings())) {
const selectors = selectorsToSync[group];
// it's possible that stale state gives us empty selections, so we explicitly check here.
if (selectors) {
toggleAll(selectedName, selectors);
}
}
});
}

View File

@@ -0,0 +1 @@
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,142 @@
.pagedtable {
overflow: auto;
padding-left: 8px;
padding-right: 8px;
}
.pagedtable-wrapper {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
}
.pagedtable table {
width: 100%;
max-width: 100%;
margin: 0;
}
.pagedtable th {
padding: 0 5px 0 5px;
border: none;
border-bottom: 2px solid #dddddd;
min-width: 45px;
}
.pagedtable-empty th {
display: none;
}
.pagedtable td {
padding: 0 4px 0 4px;
}
.pagedtable .even {
background-color: rgba(140, 140, 140, 0.1);
}
.pagedtable-padding-col {
display: none;
}
.pagedtable a {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pagedtable-index-nav {
cursor: pointer;
padding: 0 5px 0 5px;
float: right;
border: 0;
}
.pagedtable-index-nav-disabled {
cursor: default;
text-decoration: none;
color: #999;
}
a.pagedtable-index-nav-disabled:hover {
text-decoration: none;
color: #999;
}
.pagedtable-indexes {
cursor: pointer;
float: right;
border: 0;
}
.pagedtable-index-current {
cursor: default;
text-decoration: none;
font-weight: bold;
color: #333;
border: 0;
}
a.pagedtable-index-current:hover {
text-decoration: none;
font-weight: bold;
color: #333;
}
.pagedtable-index {
width: 30px;
display: inline-block;
text-align: center;
border: 0;
}
.pagedtable-index-separator-left {
display: inline-block;
color: #333;
font-size: 9px;
padding: 0 0 0 0;
cursor: default;
}
.pagedtable-index-separator-right {
display: inline-block;
color: #333;
font-size: 9px;
padding: 0 4px 0 0;
cursor: default;
}
.pagedtable-footer {
padding-top: 4px;
padding-bottom: 5px;
}
.pagedtable-not-empty .pagedtable-footer {
border-top: 2px solid #dddddd;
}
.pagedtable-info {
overflow: hidden;
color: #999;
white-space: nowrap;
text-overflow: ellipsis;
}
.pagedtable-header-name {
overflow: hidden;
text-overflow: ellipsis;
}
.pagedtable-header-type {
color: #999;
font-weight: 400;
}
.pagedtable-na-cell {
font-style: italic;
opacity: 0.3;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,236 @@
/* quarto syntax highlight colors */
:root {
--quarto-hl-ot-color: #003B4F;
--quarto-hl-at-color: #657422;
--quarto-hl-ss-color: #20794D;
--quarto-hl-an-color: #5E5E5E;
--quarto-hl-fu-color: #4758AB;
--quarto-hl-st-color: #20794D;
--quarto-hl-cf-color: #003B4F;
--quarto-hl-op-color: #5E5E5E;
--quarto-hl-er-color: #AD0000;
--quarto-hl-bn-color: #AD0000;
--quarto-hl-al-color: #AD0000;
--quarto-hl-va-color: #111111;
--quarto-hl-bu-color: inherit;
--quarto-hl-ex-color: inherit;
--quarto-hl-pp-color: #AD0000;
--quarto-hl-in-color: #5E5E5E;
--quarto-hl-vs-color: #20794D;
--quarto-hl-wa-color: #5E5E5E;
--quarto-hl-do-color: #5E5E5E;
--quarto-hl-im-color: #00769E;
--quarto-hl-ch-color: #20794D;
--quarto-hl-dt-color: #AD0000;
--quarto-hl-fl-color: #AD0000;
--quarto-hl-co-color: #5E5E5E;
--quarto-hl-cv-color: #5E5E5E;
--quarto-hl-cn-color: #8f5902;
--quarto-hl-sc-color: #5E5E5E;
--quarto-hl-dv-color: #AD0000;
--quarto-hl-kw-color: #003B4F;
}
/* other quarto variables */
:root {
--quarto-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* syntax highlight based on Pandoc's rules */
pre > code.sourceCode > span {
color: #003B4F;
}
code.sourceCode > span {
color: #003B4F;
}
div.sourceCode,
div.sourceCode pre.sourceCode {
color: #003B4F;
}
/* Normal */
code span {
color: #003B4F;
}
/* Alert */
code span.al {
color: #AD0000;
font-style: inherit;
}
/* Annotation */
code span.an {
color: #5E5E5E;
font-style: inherit;
}
/* Attribute */
code span.at {
color: #657422;
font-style: inherit;
}
/* BaseN */
code span.bn {
color: #AD0000;
font-style: inherit;
}
/* BuiltIn */
code span.bu {
font-style: inherit;
}
/* ControlFlow */
code span.cf {
color: #003B4F;
font-weight: bold;
font-style: inherit;
}
/* Char */
code span.ch {
color: #20794D;
font-style: inherit;
}
/* Constant */
code span.cn {
color: #8f5902;
font-style: inherit;
}
/* Comment */
code span.co {
color: #5E5E5E;
font-style: inherit;
}
/* CommentVar */
code span.cv {
color: #5E5E5E;
font-style: italic;
}
/* Documentation */
code span.do {
color: #5E5E5E;
font-style: italic;
}
/* DataType */
code span.dt {
color: #AD0000;
font-style: inherit;
}
/* DecVal */
code span.dv {
color: #AD0000;
font-style: inherit;
}
/* Error */
code span.er {
color: #AD0000;
font-style: inherit;
}
/* Extension */
code span.ex {
font-style: inherit;
}
/* Float */
code span.fl {
color: #AD0000;
font-style: inherit;
}
/* Function */
code span.fu {
color: #4758AB;
font-style: inherit;
}
/* Import */
code span.im {
color: #00769E;
font-style: inherit;
}
/* Information */
code span.in {
color: #5E5E5E;
font-style: inherit;
}
/* Keyword */
code span.kw {
color: #003B4F;
font-weight: bold;
font-style: inherit;
}
/* Operator */
code span.op {
color: #5E5E5E;
font-style: inherit;
}
/* Other */
code span.ot {
color: #003B4F;
font-style: inherit;
}
/* Preprocessor */
code span.pp {
color: #AD0000;
font-style: inherit;
}
/* SpecialChar */
code span.sc {
color: #5E5E5E;
font-style: inherit;
}
/* SpecialString */
code span.ss {
color: #20794D;
font-style: inherit;
}
/* String */
code span.st {
color: #20794D;
font-style: inherit;
}
/* Variable */
code span.va {
color: #111111;
font-style: inherit;
}
/* VerbatimString */
code span.vs {
color: #20794D;
font-style: inherit;
}
/* Warning */
code span.wa {
color: #5E5E5E;
font-style: italic;
}
.prevent-inlining {
content: "</";
}
/*# sourceMappingURL=789ad8cd35626ba772517505b42a116e.css.map */

View File

@@ -0,0 +1,845 @@
import * as tabsets from "./tabsets/tabsets.js";
const sectionChanged = new CustomEvent("quarto-sectionChanged", {
detail: {},
bubbles: true,
cancelable: false,
composed: false,
});
const layoutMarginEls = () => {
// Find any conflicting margin elements and add margins to the
// top to prevent overlap
const marginChildren = window.document.querySelectorAll(
".column-margin.column-container > *, .margin-caption, .aside"
);
let lastBottom = 0;
for (const marginChild of marginChildren) {
if (marginChild.offsetParent !== null) {
// clear the top margin so we recompute it
marginChild.style.marginTop = null;
const top = marginChild.getBoundingClientRect().top + window.scrollY;
if (top < lastBottom) {
const marginChildStyle = window.getComputedStyle(marginChild);
const marginBottom = parseFloat(marginChildStyle["marginBottom"]);
const margin = lastBottom - top + marginBottom;
marginChild.style.marginTop = `${margin}px`;
}
const styles = window.getComputedStyle(marginChild);
const marginTop = parseFloat(styles["marginTop"]);
lastBottom = top + marginChild.getBoundingClientRect().height + marginTop;
}
}
};
window.document.addEventListener("DOMContentLoaded", function (_event) {
// Recompute the position of margin elements anytime the body size changes
if (window.ResizeObserver) {
const resizeObserver = new window.ResizeObserver(
throttle(() => {
layoutMarginEls();
if (
window.document.body.getBoundingClientRect().width < 990 &&
isReaderMode()
) {
quartoToggleReader();
}
}, 50)
);
resizeObserver.observe(window.document.body);
}
const tocEl = window.document.querySelector('nav.toc-active[role="doc-toc"]');
const sidebarEl = window.document.getElementById("quarto-sidebar");
const leftTocEl = window.document.getElementById("quarto-sidebar-toc-left");
const marginSidebarEl = window.document.getElementById(
"quarto-margin-sidebar"
);
// function to determine whether the element has a previous sibling that is active
const prevSiblingIsActiveLink = (el) => {
const sibling = el.previousElementSibling;
if (sibling && sibling.tagName === "A") {
return sibling.classList.contains("active");
} else {
return false;
}
};
// dispatch for htmlwidgets
// they use slideenter event to trigger resize
function fireSlideEnter() {
const event = window.document.createEvent("Event");
event.initEvent("slideenter", true, true);
window.document.dispatchEvent(event);
}
const tabs = window.document.querySelectorAll('a[data-bs-toggle="tab"]');
tabs.forEach((tab) => {
tab.addEventListener("shown.bs.tab", fireSlideEnter);
});
// dispatch for shiny
// they use BS shown and hidden events to trigger rendering
function distpatchShinyEvents(previous, current) {
if (window.jQuery) {
if (previous) {
window.jQuery(previous).trigger("hidden");
}
if (current) {
window.jQuery(current).trigger("shown");
}
}
}
// tabby.js listener: Trigger event for htmlwidget and shiny
document.addEventListener(
"tabby",
function (event) {
fireSlideEnter();
distpatchShinyEvents(event.detail.previousTab, event.detail.tab);
},
false
);
// Track scrolling and mark TOC links as active
// get table of contents and sidebar (bail if we don't have at least one)
const tocLinks = tocEl
? [...tocEl.querySelectorAll("a[data-scroll-target]")]
: [];
const makeActive = (link) => tocLinks[link].classList.add("active");
const removeActive = (link) => tocLinks[link].classList.remove("active");
const removeAllActive = () =>
[...Array(tocLinks.length).keys()].forEach((link) => removeActive(link));
// activate the anchor for a section associated with this TOC entry
tocLinks.forEach((link) => {
link.addEventListener("click", () => {
if (link.href.indexOf("#") !== -1) {
const anchor = link.href.split("#")[1];
const heading = window.document.querySelector(
`[data-anchor-id="${anchor}"]`
);
if (heading) {
// Add the class
heading.classList.add("reveal-anchorjs-link");
// function to show the anchor
const handleMouseout = () => {
heading.classList.remove("reveal-anchorjs-link");
heading.removeEventListener("mouseout", handleMouseout);
};
// add a function to clear the anchor when the user mouses out of it
heading.addEventListener("mouseout", handleMouseout);
}
}
});
});
const sections = tocLinks.map((link) => {
const target = link.getAttribute("data-scroll-target");
if (target.startsWith("#")) {
return window.document.getElementById(decodeURI(`${target.slice(1)}`));
} else {
return window.document.querySelector(decodeURI(`${target}`));
}
});
const sectionMargin = 200;
let currentActive = 0;
// track whether we've initialized state the first time
let init = false;
const updateActiveLink = () => {
// The index from bottom to top (e.g. reversed list)
let sectionIndex = -1;
if (
window.innerHeight + window.pageYOffset >=
window.document.body.offsetHeight
) {
// This is the no-scroll case where last section should be the active one
sectionIndex = 0;
} else {
// This finds the last section visible on screen that should be made active
sectionIndex = [...sections].reverse().findIndex((section) => {
if (section) {
return window.pageYOffset >= section.offsetTop - sectionMargin;
} else {
return false;
}
});
}
if (sectionIndex > -1) {
const current = sections.length - sectionIndex - 1;
if (current !== currentActive) {
removeAllActive();
currentActive = current;
makeActive(current);
if (init) {
window.dispatchEvent(sectionChanged);
}
init = true;
}
}
};
const inHiddenRegion = (top, bottom, hiddenRegions) => {
for (const region of hiddenRegions) {
if (top <= region.bottom && bottom >= region.top) {
return true;
}
}
return false;
};
const categorySelector = "header.quarto-title-block .quarto-category";
const activateCategories = (href) => {
// Find any categories
// Surround them with a link pointing back to:
// #category=Authoring
try {
const categoryEls = window.document.querySelectorAll(categorySelector);
for (const categoryEl of categoryEls) {
const categoryText = categoryEl.textContent;
if (categoryText) {
const link = `${href}#category=${encodeURIComponent(categoryText)}`;
const linkEl = window.document.createElement("a");
linkEl.setAttribute("href", link);
for (const child of categoryEl.childNodes) {
linkEl.append(child);
}
categoryEl.appendChild(linkEl);
}
}
} catch {
// Ignore errors
}
};
function hasTitleCategories() {
return window.document.querySelector(categorySelector) !== null;
}
function offsetRelativeUrl(url) {
const offset = getMeta("quarto:offset");
return offset ? offset + url : url;
}
function offsetAbsoluteUrl(url) {
const offset = getMeta("quarto:offset");
const baseUrl = new URL(offset, window.location);
const projRelativeUrl = url.replace(baseUrl, "");
if (projRelativeUrl.startsWith("/")) {
return projRelativeUrl;
} else {
return "/" + projRelativeUrl;
}
}
// read a meta tag value
function getMeta(metaName) {
const metas = window.document.getElementsByTagName("meta");
for (let i = 0; i < metas.length; i++) {
if (metas[i].getAttribute("name") === metaName) {
return metas[i].getAttribute("content");
}
}
return "";
}
async function findAndActivateCategories() {
// Categories search with listing only use path without query
const currentPagePath = offsetAbsoluteUrl(
window.location.origin + window.location.pathname
);
const response = await fetch(offsetRelativeUrl("listings.json"));
if (response.status == 200) {
return response.json().then(function (listingPaths) {
const listingHrefs = [];
for (const listingPath of listingPaths) {
const pathWithoutLeadingSlash = listingPath.listing.substring(1);
for (const item of listingPath.items) {
const encodedItem = encodeURI(item);
if (
encodedItem === currentPagePath ||
encodedItem === currentPagePath + "index.html"
) {
// Resolve this path against the offset to be sure
// we already are using the correct path to the listing
// (this adjusts the listing urls to be rooted against
// whatever root the page is actually running against)
const relative = offsetRelativeUrl(pathWithoutLeadingSlash);
const baseUrl = window.location;
const resolvedPath = new URL(relative, baseUrl);
listingHrefs.push(resolvedPath.pathname);
break;
}
}
}
// Look up the tree for a nearby linting and use that if we find one
const nearestListing = findNearestParentListing(
offsetAbsoluteUrl(window.location.pathname),
listingHrefs
);
if (nearestListing) {
activateCategories(nearestListing);
} else {
// See if the referrer is a listing page for this item
const referredRelativePath = offsetAbsoluteUrl(document.referrer);
const referrerListing = listingHrefs.find((listingHref) => {
const isListingReferrer =
listingHref === referredRelativePath ||
listingHref === referredRelativePath + "index.html";
return isListingReferrer;
});
if (referrerListing) {
// Try to use the referrer if possible
activateCategories(referrerListing);
} else if (listingHrefs.length > 0) {
// Otherwise, just fall back to the first listing
activateCategories(listingHrefs[0]);
}
}
});
}
}
if (hasTitleCategories()) {
findAndActivateCategories();
}
const findNearestParentListing = (href, listingHrefs) => {
if (!href || !listingHrefs) {
return undefined;
}
// Look up the tree for a nearby linting and use that if we find one
const relativeParts = href.substring(1).split("/");
while (relativeParts.length > 0) {
const path = relativeParts.join("/");
for (const listingHref of listingHrefs) {
if (listingHref.startsWith(path)) {
return listingHref;
}
}
relativeParts.pop();
}
return undefined;
};
const manageSidebarVisiblity = (el, placeholderDescriptor) => {
let isVisible = true;
let elRect;
return (hiddenRegions) => {
if (el === null) {
return;
}
// Find the last element of the TOC
const lastChildEl = el.lastElementChild;
if (lastChildEl) {
// Converts the sidebar to a menu
const convertToMenu = () => {
for (const child of el.children) {
child.style.opacity = 0;
child.style.overflow = "hidden";
child.style.pointerEvents = "none";
}
nexttick(() => {
const toggleContainer = window.document.createElement("div");
toggleContainer.style.width = "100%";
toggleContainer.classList.add("zindex-over-content");
toggleContainer.classList.add("quarto-sidebar-toggle");
toggleContainer.classList.add("headroom-target"); // Marks this to be managed by headeroom
toggleContainer.id = placeholderDescriptor.id;
toggleContainer.style.position = "fixed";
const toggleIcon = window.document.createElement("i");
toggleIcon.classList.add("quarto-sidebar-toggle-icon");
toggleIcon.classList.add("bi");
toggleIcon.classList.add("bi-caret-down-fill");
const toggleTitle = window.document.createElement("div");
const titleEl = window.document.body.querySelector(
placeholderDescriptor.titleSelector
);
if (titleEl) {
toggleTitle.append(
titleEl.textContent || titleEl.innerText,
toggleIcon
);
}
toggleTitle.classList.add("zindex-over-content");
toggleTitle.classList.add("quarto-sidebar-toggle-title");
toggleContainer.append(toggleTitle);
const toggleContents = window.document.createElement("div");
toggleContents.classList = el.classList;
toggleContents.classList.add("zindex-over-content");
toggleContents.classList.add("quarto-sidebar-toggle-contents");
for (const child of el.children) {
if (child.id === "toc-title") {
continue;
}
const clone = child.cloneNode(true);
clone.style.opacity = 1;
clone.style.pointerEvents = null;
clone.style.display = null;
toggleContents.append(clone);
}
toggleContents.style.height = "0px";
const positionToggle = () => {
// position the element (top left of parent, same width as parent)
if (!elRect) {
elRect = el.getBoundingClientRect();
}
toggleContainer.style.left = `${elRect.left}px`;
toggleContainer.style.top = `${elRect.top}px`;
toggleContainer.style.width = `${elRect.width}px`;
};
positionToggle();
toggleContainer.append(toggleContents);
el.parentElement.prepend(toggleContainer);
// Process clicks
let tocShowing = false;
// Allow the caller to control whether this is dismissed
// when it is clicked (e.g. sidebar navigation supports
// opening and closing the nav tree, so don't dismiss on click)
const clickEl = placeholderDescriptor.dismissOnClick
? toggleContainer
: toggleTitle;
const closeToggle = () => {
if (tocShowing) {
toggleContainer.classList.remove("expanded");
toggleContents.style.height = "0px";
tocShowing = false;
}
};
// Get rid of any expanded toggle if the user scrolls
window.document.addEventListener(
"scroll",
throttle(() => {
closeToggle();
}, 50)
);
// Handle positioning of the toggle
window.addEventListener(
"resize",
throttle(() => {
elRect = undefined;
positionToggle();
}, 50)
);
window.addEventListener("quarto-hrChanged", () => {
elRect = undefined;
});
// Process the click
clickEl.onclick = () => {
if (!tocShowing) {
toggleContainer.classList.add("expanded");
toggleContents.style.height = null;
tocShowing = true;
} else {
closeToggle();
}
};
});
};
// Converts a sidebar from a menu back to a sidebar
const convertToSidebar = () => {
for (const child of el.children) {
child.style.opacity = 1;
child.style.overflow = null;
child.style.pointerEvents = null;
}
const placeholderEl = window.document.getElementById(
placeholderDescriptor.id
);
if (placeholderEl) {
placeholderEl.remove();
}
el.classList.remove("rollup");
};
if (isReaderMode()) {
convertToMenu();
isVisible = false;
} else {
// Find the top and bottom o the element that is being managed
const elTop = el.offsetTop;
const elBottom =
elTop + lastChildEl.offsetTop + lastChildEl.offsetHeight;
if (!isVisible) {
// If the element is current not visible reveal if there are
// no conflicts with overlay regions
if (!inHiddenRegion(elTop, elBottom, hiddenRegions)) {
convertToSidebar();
isVisible = true;
}
} else {
// If the element is visible, hide it if it conflicts with overlay regions
// and insert a placeholder toggle (or if we're in reader mode)
if (inHiddenRegion(elTop, elBottom, hiddenRegions)) {
convertToMenu();
isVisible = false;
}
}
}
}
};
};
const tabEls = document.querySelectorAll('a[data-bs-toggle="tab"]');
for (const tabEl of tabEls) {
const id = tabEl.getAttribute("data-bs-target");
if (id) {
const columnEl = document.querySelector(
`${id} .column-margin, .tabset-margin-content`
);
if (columnEl)
tabEl.addEventListener("shown.bs.tab", function (event) {
const el = event.srcElement;
if (el) {
const visibleCls = `${el.id}-margin-content`;
// walk up until we find a parent tabset
let panelTabsetEl = el.parentElement;
while (panelTabsetEl) {
if (panelTabsetEl.classList.contains("panel-tabset")) {
break;
}
panelTabsetEl = panelTabsetEl.parentElement;
}
if (panelTabsetEl) {
const prevSib = panelTabsetEl.previousElementSibling;
if (
prevSib &&
prevSib.classList.contains("tabset-margin-container")
) {
const childNodes = prevSib.querySelectorAll(
".tabset-margin-content"
);
for (const childEl of childNodes) {
if (childEl.classList.contains(visibleCls)) {
childEl.classList.remove("collapse");
} else {
childEl.classList.add("collapse");
}
}
}
}
}
layoutMarginEls();
});
}
}
// Manage the visibility of the toc and the sidebar
const marginScrollVisibility = manageSidebarVisiblity(marginSidebarEl, {
id: "quarto-toc-toggle",
titleSelector: "#toc-title",
dismissOnClick: true,
});
const sidebarScrollVisiblity = manageSidebarVisiblity(sidebarEl, {
id: "quarto-sidebarnav-toggle",
titleSelector: ".title",
dismissOnClick: false,
});
let tocLeftScrollVisibility;
if (leftTocEl) {
tocLeftScrollVisibility = manageSidebarVisiblity(leftTocEl, {
id: "quarto-lefttoc-toggle",
titleSelector: "#toc-title",
dismissOnClick: true,
});
}
// Find the first element that uses formatting in special columns
const conflictingEls = window.document.body.querySelectorAll(
'[class^="column-"], [class*=" column-"], aside, [class*="margin-caption"], [class*=" margin-caption"], [class*="margin-ref"], [class*=" margin-ref"]'
);
// Filter all the possibly conflicting elements into ones
// the do conflict on the left or ride side
const arrConflictingEls = Array.from(conflictingEls);
const leftSideConflictEls = arrConflictingEls.filter((el) => {
if (el.tagName === "ASIDE") {
return false;
}
return Array.from(el.classList).find((className) => {
return (
className !== "column-body" &&
className.startsWith("column-") &&
!className.endsWith("right") &&
!className.endsWith("container") &&
className !== "column-margin"
);
});
});
const rightSideConflictEls = arrConflictingEls.filter((el) => {
if (el.tagName === "ASIDE") {
return true;
}
const hasMarginCaption = Array.from(el.classList).find((className) => {
return className == "margin-caption";
});
if (hasMarginCaption) {
return true;
}
return Array.from(el.classList).find((className) => {
return (
className !== "column-body" &&
!className.endsWith("container") &&
className.startsWith("column-") &&
!className.endsWith("left")
);
});
});
const kOverlapPaddingSize = 10;
function toRegions(els) {
return els.map((el) => {
const boundRect = el.getBoundingClientRect();
const top =
boundRect.top +
document.documentElement.scrollTop -
kOverlapPaddingSize;
return {
top,
bottom: top + el.scrollHeight + 2 * kOverlapPaddingSize,
};
});
}
let hasObserved = false;
const visibleItemObserver = (els) => {
let visibleElements = [...els];
const intersectionObserver = new IntersectionObserver(
(entries, _observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (visibleElements.indexOf(entry.target) === -1) {
visibleElements.push(entry.target);
}
} else {
visibleElements = visibleElements.filter((visibleEntry) => {
return visibleEntry !== entry;
});
}
});
if (!hasObserved) {
hideOverlappedSidebars();
}
hasObserved = true;
},
{}
);
els.forEach((el) => {
intersectionObserver.observe(el);
});
return {
getVisibleEntries: () => {
return visibleElements;
},
};
};
const rightElementObserver = visibleItemObserver(rightSideConflictEls);
const leftElementObserver = visibleItemObserver(leftSideConflictEls);
const hideOverlappedSidebars = () => {
marginScrollVisibility(toRegions(rightElementObserver.getVisibleEntries()));
sidebarScrollVisiblity(toRegions(leftElementObserver.getVisibleEntries()));
if (tocLeftScrollVisibility) {
tocLeftScrollVisibility(
toRegions(leftElementObserver.getVisibleEntries())
);
}
};
window.quartoToggleReader = () => {
// Applies a slow class (or removes it)
// to update the transition speed
const slowTransition = (slow) => {
const manageTransition = (id, slow) => {
const el = document.getElementById(id);
if (el) {
if (slow) {
el.classList.add("slow");
} else {
el.classList.remove("slow");
}
}
};
manageTransition("TOC", slow);
manageTransition("quarto-sidebar", slow);
};
const readerMode = !isReaderMode();
setReaderModeValue(readerMode);
// If we're entering reader mode, slow the transition
if (readerMode) {
slowTransition(readerMode);
}
highlightReaderToggle(readerMode);
hideOverlappedSidebars();
// If we're exiting reader mode, restore the non-slow transition
if (!readerMode) {
slowTransition(!readerMode);
}
};
const highlightReaderToggle = (readerMode) => {
const els = document.querySelectorAll(".quarto-reader-toggle");
if (els) {
els.forEach((el) => {
if (readerMode) {
el.classList.add("reader");
} else {
el.classList.remove("reader");
}
});
}
};
const setReaderModeValue = (val) => {
if (window.location.protocol !== "file:") {
window.localStorage.setItem("quarto-reader-mode", val);
} else {
localReaderMode = val;
}
};
const isReaderMode = () => {
if (window.location.protocol !== "file:") {
return window.localStorage.getItem("quarto-reader-mode") === "true";
} else {
return localReaderMode;
}
};
let localReaderMode = null;
const tocOpenDepthStr = tocEl?.getAttribute("data-toc-expanded");
const tocOpenDepth = tocOpenDepthStr ? Number(tocOpenDepthStr) : 1;
// Walk the TOC and collapse/expand nodes
// Nodes are expanded if:
// - they are top level
// - they have children that are 'active' links
// - they are directly below an link that is 'active'
const walk = (el, depth) => {
// Tick depth when we enter a UL
if (el.tagName === "UL") {
depth = depth + 1;
}
// It this is active link
let isActiveNode = false;
if (el.tagName === "A" && el.classList.contains("active")) {
isActiveNode = true;
}
// See if there is an active child to this element
let hasActiveChild = false;
for (const child of el.children) {
hasActiveChild = walk(child, depth) || hasActiveChild;
}
// Process the collapse state if this is an UL
if (el.tagName === "UL") {
if (tocOpenDepth === -1 && depth > 1) {
// toc-expand: false
el.classList.add("collapse");
} else if (
depth <= tocOpenDepth ||
hasActiveChild ||
prevSiblingIsActiveLink(el)
) {
el.classList.remove("collapse");
} else {
el.classList.add("collapse");
}
// untick depth when we leave a UL
depth = depth - 1;
}
return hasActiveChild || isActiveNode;
};
// walk the TOC and expand / collapse any items that should be shown
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
// Throttle the scroll event and walk peridiocally
window.document.addEventListener(
"scroll",
throttle(() => {
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
if (!isReaderMode()) {
hideOverlappedSidebars();
}
}, 5)
);
window.addEventListener(
"resize",
throttle(() => {
if (tocEl) {
updateActiveLink();
walk(tocEl, 0);
}
if (!isReaderMode()) {
hideOverlappedSidebars();
}
}, 10)
);
hideOverlappedSidebars();
highlightReaderToggle(isReaderMode());
});
tabsets.init();
function throttle(func, wait) {
let waiting = false;
return function () {
if (!waiting) {
func.apply(this, arguments);
waiting = true;
setTimeout(function () {
waiting = false;
}, wait);
}
};
}
function nexttick(func) {
return setTimeout(func, 0);
}

View File

@@ -0,0 +1,95 @@
// grouped tabsets
export function init() {
window.addEventListener("pageshow", (_event) => {
function getTabSettings() {
const data = localStorage.getItem("quarto-persistent-tabsets-data");
if (!data) {
localStorage.setItem("quarto-persistent-tabsets-data", "{}");
return {};
}
if (data) {
return JSON.parse(data);
}
}
function setTabSettings(data) {
localStorage.setItem(
"quarto-persistent-tabsets-data",
JSON.stringify(data)
);
}
function setTabState(groupName, groupValue) {
const data = getTabSettings();
data[groupName] = groupValue;
setTabSettings(data);
}
function toggleTab(tab, active) {
const tabPanelId = tab.getAttribute("aria-controls");
const tabPanel = document.getElementById(tabPanelId);
if (active) {
tab.classList.add("active");
tabPanel.classList.add("active");
} else {
tab.classList.remove("active");
tabPanel.classList.remove("active");
}
}
function toggleAll(selectedGroup, selectorsToSync) {
for (const [thisGroup, tabs] of Object.entries(selectorsToSync)) {
const active = selectedGroup === thisGroup;
for (const tab of tabs) {
toggleTab(tab, active);
}
}
}
function findSelectorsToSyncByLanguage() {
const result = {};
const tabs = Array.from(
document.querySelectorAll(`div[data-group] a[id^='tabset-']`)
);
for (const item of tabs) {
const div = item.parentElement.parentElement.parentElement;
const group = div.getAttribute("data-group");
if (!result[group]) {
result[group] = {};
}
const selectorsToSync = result[group];
const value = item.innerHTML;
if (!selectorsToSync[value]) {
selectorsToSync[value] = [];
}
selectorsToSync[value].push(item);
}
return result;
}
function setupSelectorSync() {
const selectorsToSync = findSelectorsToSyncByLanguage();
Object.entries(selectorsToSync).forEach(([group, tabSetsByValue]) => {
Object.entries(tabSetsByValue).forEach(([value, items]) => {
items.forEach((item) => {
item.addEventListener("click", (_event) => {
setTabState(group, value);
toggleAll(value, selectorsToSync[group]);
});
});
});
});
return selectorsToSync;
}
const selectorsToSync = setupSelectorSync();
for (const [group, selectedName] of Object.entries(getTabSettings())) {
const selectors = selectorsToSync[group];
// it's possible that stale state gives us empty selections, so we explicitly check here.
if (selectors) {
toggleAll(selectedName, selectors);
}
}
});
}

View File

@@ -0,0 +1 @@
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}

File diff suppressed because one or more lines are too long