ovicore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ovicore-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Ferreira Schopf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ prune tests
ovicore-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,316 @@
1
+ Metadata-Version: 2.4
2
+ Name: ovicore
3
+ Version: 0.1.0
4
+ Summary: Avaliação padronizada e reprodutível de detectores de objetos treinados em diferentes frameworks
5
+ Author: Daniel Ferreira Schopf
6
+ License-Expression: MIT
7
+ Project-URL: Repositorio, https://github.com/UFSM-Fiocruz/hybrid-core
8
+ Keywords: object-detection,evaluation,reproducibility,detectron2,ultralytics,huggingface
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pandas>=2.2
13
+ Requires-Dist: numpy>=1.26
14
+ Provides-Extra: ultralytics
15
+ Requires-Dist: ultralytics>=8.3; extra == "ultralytics"
16
+ Requires-Dist: torch>=2.1; extra == "ultralytics"
17
+ Provides-Extra: sahi
18
+ Requires-Dist: sahi<0.13,>=0.11.19; extra == "sahi"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == "dev"
21
+ Requires-Dist: build; extra == "dev"
22
+ Requires-Dist: twine; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # ovicore
26
+
27
+ Biblioteca para avaliar e comparar modelos de detecção de ovos de *Aedes aegypti* em imagens de ovitrampas, independente do framework usado.
28
+
29
+ Projeto UFSM–Fiocruz. TCC em Sistemas de Informação, por Daniel Schopf, sob orientação da Profa. Andrea Schwertner Charão.
30
+
31
+ ## O problema
32
+
33
+ O projeto usa vários frameworks de detecção: Detectron2, Ultralytics e HuggingFace. Cada um treina do seu jeito, salva o resultado no seu formato e calcula as métricas com sua própria regra.
34
+
35
+ Isso torna a comparação entre eles pouco confiável. Quando dois números vêm de códigos diferentes, com convenções diferentes, comparar um com o outro não diz muita coisa.
36
+
37
+ ## O que a biblioteca faz
38
+
39
+ O ovicore não muda a forma como cada framework treina. Ele parte de uma entrada única e padroniza o que sai.
40
+
41
+ ```
42
+ ┌─────────────────┐
43
+ ENTRADA ────► │ O FRAMEWORK │ ────► SAÍDA
44
+ (fonte única │ (não mexemos) │ (padronizada)
45
+ em COCO) │ │
46
+ └─────────────────┘
47
+ Detectron2
48
+ Ultralytics
49
+ HuggingFace
50
+ ```
51
+
52
+ A entrada é uma fonte única de anotações em COCO. Converter essa fonte para o formato de cada framework está fora desta versão e fica como trabalho futuro. Na saída, as predições de qualquer modelo passam pelo mesmo cálculo de métricas. A diferença entre dois resultados passa a ser a diferença entre os modelos, não entre os códigos que os mediram.
53
+
54
+ ## Instalação
55
+
56
+ ```bash
57
+ pip install ovicore
58
+ pip install "ovicore[sahi]"
59
+ ```
60
+
61
+ O segundo comando instala também o SAHI, para a inferência fatiada. A versão em desenvolvimento pode ser instalada direto do GitHub, com acesso ao repositório: `pip install git+https://github.com/UFSM-Fiocruz/hybrid-core.git@biblioteca-pip`.
62
+
63
+ Para ler o orçamento de checkpoints do Ultralytics, que exige `torch`, use o extra `[ultralytics]`. Para a inferência fatiada com SAHI, use o extra `[sahi]`. O resto da biblioteca só depende de `pandas` e `numpy`.
64
+
65
+ No Google Colab:
66
+
67
+ ```python
68
+ %pip install -q ovicore
69
+ from ovicore.metrics import avaliar
70
+ ```
71
+
72
+ Para desenvolver:
73
+
74
+ ```bash
75
+ git clone https://github.com/UFSM-Fiocruz/hybrid-core.git
76
+ cd hybrid-core
77
+ pip install -e ".[dev]"
78
+ ```
79
+
80
+ As dependências da biblioteca vêm do `pyproject.toml`. O `requirements.txt` continua com as versões fixas usadas no Colab.
81
+
82
+ O notebook [`notebooks/exemplo_avaliacao.ipynb`](notebooks/exemplo_avaliacao.ipynb) mostra o uso completo: instala, importa e avalia.
83
+
84
+ ## Uso
85
+
86
+ ### Calcular as métricas
87
+
88
+ ```python
89
+ from ovicore.metrics import avaliar
90
+
91
+ resultado = avaliar(
92
+ pred_bboxes, pred_scores, pred_labels,
93
+ true_bboxes, true_labels,
94
+ score_threshold=0.40,
95
+ iou_threshold=0.45,
96
+ id2label={0: 'egg', 1: 'cluster'},
97
+ )
98
+
99
+ for classe, m in resultado.items():
100
+ print(classe, m)
101
+ ```
102
+
103
+ ```
104
+ geral {'vp': 229, 'fp': 163, 'fn': 157, 'precisao': 0.584, 'recall': 0.593, 'f1': 0.589}
105
+ egg {'vp': 219, 'fp': 156, 'fn': 150, 'precisao': 0.584, 'recall': 0.594, 'f1': 0.589}
106
+ cluster {'vp': 10, 'fp': 7, 'fn': 7, 'precisao': 0.588, 'recall': 0.588, 'f1': 0.588}
107
+ ```
108
+
109
+ As caixas devem estar em `[x1, y1, x2, y2]`. Se o dataset usa o formato COCO (`[x, y, largura, altura]`), converta antes com `BBox.from_coco()`.
110
+
111
+ ### Salvar os resultados
112
+
113
+ ```python
114
+ from ovicore.io import montar_metadata, gravar_predicoes
115
+
116
+ metadata = montar_metadata(
117
+ framework="ultralytics", arquitetura="yolov8n",
118
+ dataset="coco-dataset-v2", fold="fold-01-416",
119
+ n_epochs=500, batch_size=16, learning_rate=0.01,
120
+ score_threshold=0.10, iou_threshold=0.45, device="cuda",
121
+ evaluation_score_threshold=0.40,
122
+ )
123
+
124
+ csv_path, json_path = gravar_predicoes(
125
+ df, output_dir="saida", framework="ultralytics",
126
+ ambiente="colab", fold="fold-01-416", metadata=metadata,
127
+ )
128
+ ```
129
+
130
+ Gera dois arquivos:
131
+
132
+ ```
133
+ predictions-ultralytics-colab-fold-01-416.csv as predições
134
+ predictions-ultralytics-colab-fold-01-416.meta.json como foram geradas
135
+ ```
136
+
137
+ O JSON acompanha o CSV sempre. Quem recebe os dois consegue refazer o cálculo sem o notebook e sem adivinhar qual parâmetro foi usado.
138
+
139
+ ## Do notebook para a biblioteca
140
+
141
+ O que os notebooks repetem (verdade-terreno, SAHI e montagem do CSV) já está na biblioteca. Com o extra `[sahi]` instalado, o fluxo de avaliação de um modelo treinado fica assim, com `metadata` montado como na seção anterior:
142
+
143
+ ```python
144
+ from ovicore.anotacoes import ler_anotacoes_coco
145
+ from ovicore.contracts import SlicingConfig
146
+ from ovicore.fatiamento import carregar_modelo_sahi, inferir_com_sahi
147
+ from ovicore.io import montar_predicoes, gravar_predicoes
148
+ from ovicore.relatorio import resumo_execucoes
149
+
150
+ anotacoes = ler_anotacoes_coco("test/test.json")
151
+ modelo = carregar_modelo_sahi("ultralytics", "best.pt", None, score_threshold=0.10, device="cuda")
152
+ deteccoes = [inferir_com_sahi(modelo, f"test/{nome}", SlicingConfig()) for nome in anotacoes]
153
+ df = montar_predicoes(list(anotacoes), deteccoes, anotacoes)
154
+ gravar_predicoes(df, "saida", "ultralytics", "colab", "fold-01-416", metadata)
155
+ resumo_execucoes("saida")
156
+ ```
157
+
158
+ A verdade-terreno sai em `[x, y, x + w, y + h]`, sem o erro da caixa quadrada (E0). Os rótulos do modelo e os da anotação precisam estar no mesmo espaço de ids. Confira os ids antes de avaliar. No Faster R-CNN de referência, predições e `test.json` já usam 0 = egg e 1 = cluster, então o deslocamento é 0 (o padrão). Para reproduzir esses notebooks, use também `SlicingConfig(class_agnostic=True)`.
159
+
160
+ Antes e depois, só na parte de inferência e avaliação:
161
+
162
+ | Modelo | Antes | Células de código | Linhas de código | Depois | Células de código | Linhas de código |
163
+ |---|---|---|---|---|---|---|
164
+ | Faster R-CNN | notebook do grupo | 22 | 371 | `avaliacao_faster_rcnn_detectron2.ipynb` | 8 | 66 |
165
+ | Mask R-CNN | notebook do grupo | 22 | 373 | `avaliacao_mask_rcnn_detectron2.ipynb` | 10 | 94 |
166
+ | DETR | notebook do grupo | 10 | 123 | `avaliacao_detr_huggingface.ipynb` | 9 | 97 |
167
+ | YOLOv8n | exportação `.py` do Untitled2 | sem registro | 173 | `avaliacao_yolov8n_ultralytics.ipynb` | 8 | 69 |
168
+
169
+ Como foi contado:
170
+
171
+ - Linhas de código são as linhas não vazias que não são comentário.
172
+ - Nos notebooks do grupo, a parte de inferência e avaliação vai da seção "Avaliação" até o fim. Nos novos, vai da seção "3. Dados e modelo" até o fim, então inclui também a leitura da verdade-terreno, do orçamento e do modelo.
173
+ - A exportação `.py` do YOLOv8n entra inteira, sem a instalação e sem as células de texto. Ela não guarda os limites das células, então o número de células fica sem registro.
174
+ - No Mask R-CNN, o notebook novo faz duas avaliações: a reprodução, com as caixas da máscara, e o resultado oficial, com as caixas da cabeça de detecção.
175
+ - No DETR, a diferença é menor por dois motivos. A avaliação do notebook do grupo não chega ao fim: a função `compute_metrics` nunca é chamada. E o notebook novo tem uma célula de 22 linhas que confere a escala dos scores (E10); sem ela, seriam 75 linhas.
176
+
177
+ ## Relatório de execuções
178
+
179
+ `resumo_execucoes()` lê todos os pares CSV + `.meta.json` de uma pasta e monta uma linha por execução, com a configuração e as métricas calculadas pelo mesmo código. CSV sem `.meta.json` fica de fora.
180
+
181
+ ```python
182
+ from ovicore.relatorio import resumo_execucoes, divergencias_de_configuracao
183
+
184
+ resumo = resumo_execucoes("saida")
185
+ print(resumo[["framework", "arquitetura", "n_epochs", "score_threshold", "f1"]])
186
+ print(divergencias_de_configuracao(resumo))
187
+ ```
188
+
189
+ A saída abaixo vem de dados simulados, as duas execuções de demonstração do notebook de exemplo. Não é resultado de nenhum modelo real.
190
+
191
+ ```
192
+ framework arquitetura n_epochs score_threshold f1
193
+ detectron2 faster_rcnn_R_50_FPN_3x 100 0.5 0.5
194
+ ultralytics yolov8n 500 0.5 0.5
195
+ {'n_epochs': [100, 500], 'batch_size': [8, 16], 'learning_rate': [0.0001, 0.01]}
196
+ ```
197
+
198
+ `divergencias_de_configuracao()` lista os campos que variam entre as execuções. No exemplo, o F1 é igual, mas o orçamento de treino não, e isso fica registrado ao lado do resultado. O que conta como orçamento e os valores reais de cada modelo estão em [`docs/ORCAMENTO.md`](docs/ORCAMENTO.md).
199
+
200
+ Para o limiar de confiança, `varrer_limiares()` calcula precisão, revocação e F1 em vários limiares, e `escolher_limiar()` escolhe o de maior F1 na partição de validação. O limiar escolhido é aplicado depois ao teste.
201
+
202
+ Para os quatro modelos do trabalho, `scripts/relatorio_execucoes.py` lê a pasta `resultados/` baixada do Drive, roda `resumo_execucoes()` e `divergencias_de_configuracao()` sobre os CSVs de teste e grava [`docs/RELATORIO_EXECUCOES.md`](docs/RELATORIO_EXECUCOES.md). A tabela mostra o `dataset`, o `fold`, a `versao_dataset` e o número de caixas de cada teste, porque cada modelo foi avaliado num conjunto diferente. Os CSVs de predições não entram no repositório, só o relatório.
203
+
204
+ ```bash
205
+ python scripts/relatorio_execucoes.py caminho/para/resultados
206
+ ```
207
+
208
+ ## Estrutura
209
+
210
+ ```
211
+ hybrid-core/
212
+ ├── ovicore/
213
+ │ ├── metrics.py IoU, comparação e métricas
214
+ │ ├── io.py gravação e leitura padronizadas
215
+ │ ├── relatorio.py resumo de execuções e escolha do limiar
216
+ │ ├── anotacoes.py verdade-terreno a partir do COCO
217
+ │ ├── fatiamento.py inferência com SAHI (extra [sahi])
218
+ │ ├── contracts.py tipos e interfaces do core
219
+ │ └── backends/ adaptadores por framework
220
+ ├── tests/
221
+ │ ├── test_aceitacao.py metrics.py, io.py e contracts.py
222
+ │ ├── test_backend_*.py um por backend
223
+ │ ├── test_relatorio.py relatorio.py, com dados simulados
224
+ │ ├── test_anotacoes.py anotacoes.py, com um JSON pequeno
225
+ │ ├── test_fatiamento.py fatiamento.py, com um SAHI falso
226
+ │ ├── test_montar_predicoes.py
227
+ │ └── fixture_* dados reais usados nos testes
228
+ ├── notebooks/
229
+ │ ├── exemplo_avaliacao.ipynb
230
+ │ ├── avaliacao_faster_rcnn_detectron2.ipynb
231
+ │ ├── avaliacao_mask_rcnn_detectron2.ipynb
232
+ │ ├── treino_detr_huggingface.ipynb notebook do framework, fora da biblioteca
233
+ │ ├── avaliacao_detr_huggingface.ipynb
234
+ │ └── avaliacao_yolov8n_ultralytics.ipynb
235
+ ├── scripts/
236
+ │ └── relatorio_execucoes.py relatório dos quatro modelos a partir de resultados/
237
+ ├── docs/
238
+ │ ├── EVIDENCIAS.md os problemas que motivaram cada regra
239
+ │ ├── CONTRATO.md formato do CSV e do JSON
240
+ │ ├── ORCAMENTO.md o que é orçamento de treino e o de cada modelo
241
+ │ └── RELATORIO_EXECUCOES.md gerado por scripts/relatorio_execucoes.py
242
+ ├── .github/workflows/ testes a cada envio; tag v* publica no TestPyPI, Run workflow na tag publica no PyPI
243
+ ├── LICENSE MIT
244
+ ├── pyproject.toml
245
+ └── requirements.txt versões fixas para o Colab
246
+ ```
247
+
248
+ `metrics.py` e `io.py` estão prontos e testados. Os backends estão no começo: `Detectron2Backend`, `UltralyticsBackend` e `HuggingFaceBackend` só implementam `extrair_budget()`. `carregar_modelo()` e `inferir()` levantam `NotImplementedError`. `preparar_dataset()` também, porque a conversão na entrada está fora do escopo desta versão.
249
+
250
+ ## Resultados obtidos
251
+
252
+ Três modelos avaliados com o mesmo módulo de avaliação, sem alterar código. O Faster R-CNN e o YOLOv8n usam as 6 imagens de teste do `fold-01-416` atual, com recorte SAHI de 416px. Para o YOLOv8n esse teste tem vazamento: 4 das 6 imagens estavam no treino dele, e o F1 não é comparável. Por isso ele é avaliado no teste limpo do `fold01_416` antigo, que tem só 2 imagens de origem (21 fatias, 148 caixas; ver E11). O Mask R-CNN foi treinado numa versão anterior do conjunto de dados e é avaliado no teste original dele, com sobreposição de 50% (ver E10 em [`docs/EVIDENCIAS.md`](docs/EVIDENCIAS.md)).
253
+
254
+ | | Faster R-CNN | Mask R-CNN | YOLOv8n |
255
+ |---|---|---|---|
256
+ | Framework | Detectron2 | Detectron2 | Ultralytics |
257
+ | F1 (limiar ajustado) | 0,829 | 0,825 (limiar 0,75) | 0,609 no teste limpo (limiar 0,40); o 0,589 da referência não é comparável: vazamento |
258
+ | Origem do limiar | 0,75 herdado do grupo | 0,75 herdado do grupo | 0,40 escolhido no teste com vazamento |
259
+ | Limiar escolhido na validação | 0,60 (F1 0,799 no teste) | n/a | n/a |
260
+ | Prevê a classe `cluster`? | Não | Sim | Sim |
261
+ | Tempo de instalação | ~10 min | ~10 min | 9 s |
262
+
263
+ Só o limiar do Faster R-CNN foi escolhido na validação (37 fatias, 387 caixas): 0,60. No teste, ele dá VP 333, FP 115, FN 53 e F1 0,799, contra 0,829 com o 0,75 herdado. A validação é formada por fatias das mesmas imagens do treino, e o modelo vai melhor nela (F1 0,858 em 0,75, contra 0,829 no teste), o que provavelmente puxa o limiar para baixo. Nos outros modelos, o limiar é o 0,75 herdado do grupo (Mask R-CNN), o 0,40 escolhido no teste (YOLOv8n) e o 0,2 da demonstração (DETR). Ver E10.
264
+
265
+ O Mask R-CNN tem duas medições com os mesmos pesos, no mesmo teste e no mesmo limiar:
266
+
267
+ | Mask R-CNN | VP | FP | FN | F1 |
268
+ |---|---|---|---|---|
269
+ | Resultado oficial, caixas da cabeça de detecção | 341 | 94 | 51 | 0,825 |
270
+ | Reprodução de 24/11/2024, caixas da máscara | 231 | 213 | 161 | 0,553 |
271
+
272
+ Só a origem das caixas muda entre as duas linhas: com a máscara ligada, o SAHI monta a caixa pelo contorno da máscara, em pixels inteiros, e isso baixa o F1 em 0,27. O resultado do Mask R-CNN não se compara diretamente com o do Faster R-CNN, porque foi medido em outro teste, sobre outra versão dos dados e com sobreposição de 0,5.
273
+
274
+ O YOLOv8n, no teste limpo, com limiar 0,40, sem referência verificável:
275
+
276
+ | YOLOv8n | VP | FP | FN | F1 |
277
+ |---|---|---|---|---|
278
+ | Teste limpo, `fold01_416` antigo | 92 | 62 | 56 | 0,609 |
279
+ | Referência de 22/08/2026, teste com vazamento | 229 | 163 | 157 | 0,589 |
280
+
281
+ As duas linhas não se comparam: a referência foi medida em outro teste, em que 4 das 6 imagens estavam no treino do modelo. O teste limpo tem só 2 imagens de origem, o que limita a variedade das imagens e dos aglomerados (6). O limiar de 0,40 foi escolhido no teste com vazamento e é provisório.
282
+
283
+ Os três modelos não foram treinados nas mesmas condições. As épocas, o learning rate e o otimizador são diferentes entre eles (ver [`docs/ORCAMENTO.md`](docs/ORCAMENTO.md)). Os números servem para verificar que a padronização funciona nos três, não para comparar a qualidade dos modelos. A biblioteca registra o orçamento de cada execução e aponta quando ele difere entre as execuções comparadas.
284
+
285
+ Um quarto framework, HuggingFace/DETR (`conditional-detr-resnet-50`), também passou pelo mesmo pipeline sem alterar código. Retreinado em 08/11/2026 no `fold-01-256` e avaliado no teste dele (as mesmas 6 imagens do `fold-01-416` atual), com limiar provisório de 0,2: VP 275, FP 1.733, FN 111, F1 0,230, sem referência verificável (ver E10). Não entra na tabela porque foi treinado num fold diferente dos outros três e com orçamento reduzido de propósito (30 épocas, só para validar o contrato). O F1 dele não é comparável com os de cima.
286
+
287
+ ## De onde vêm as regras
288
+
289
+ As regras da biblioteca vieram de problemas encontrados ao rodar os frameworks. O arquivo [`docs/EVIDENCIAS.md`](docs/EVIDENCIAS.md) traz cada um com os números.
290
+
291
+ | Regra | Motivo |
292
+ |---|---|
293
+ | Coordenadas em `float` | `np.float32` corrompe o CSV sem dar erro na hora |
294
+ | Não truncar coordenadas | Com a máscara, o SAHI devolve as caixas do Mask R-CNN em pixels inteiros, o que distorce a medição |
295
+ | Considerar a classe ao comparar | Sem isso, o problema da classe `cluster` fica invisível |
296
+ | Ordenar por confiança | Os notebooks comparam na ordem do arquivo |
297
+ | Ajustar o limiar por framework | Com 0,75 fixo, o F1 do YOLOv8n cai de 0,589 para 0,234, no teste com vazamento (ver E11) |
298
+ | Salvar os metadados junto | O modelo salvo não bate com o que o notebook diz |
299
+ | Registrar o treino usado | Mask R-CNN, Faster R-CNN (referência do grupo, não o retreinado), DETR e YOLOv8n têm cerca de 183, cerca de 100, 30 e 45 épocas (o YOLOv8n de 500 configuradas, com parada antecipada) |
300
+ | Registrar a granularidade | Um dataset guarda a imagem inteira, outro guarda recortes |
301
+
302
+ ## Testes
303
+
304
+ ```bash
305
+ pytest -q
306
+ ```
307
+
308
+ Rodam localmente com `pytest` e no GitHub Actions a cada envio, em Python 3.10, 3.11 e 3.12 (`.github/workflows/testes.yml`). Nenhum precisa de GPU nem de framework de detecção.
309
+
310
+ Os dados são de dois tipos: simulados, montados dentro do próprio teste, e arquivos pequenos de exemplo em `tests/` (um CSV real de predições do YOLOv8n, o `metrics.json` do Mask R-CNN e o `train_args` do YOLOv8n). Além de conferir os cálculos, os testes verificam se a biblioteca recusa os problemas conhecidos: tipos errados, rótulos em texto e arquivos sem metadados.
311
+
312
+ Treino e inferência não são testados aqui. Eles ficam nos notebooks de cada modelo. O único teste que abre um checkpoint `.pt` real é pulado quando faltam `torch`, `ultralytics` ou o próprio arquivo, que não é versionado.
313
+
314
+ ## Licença
315
+
316
+ MIT. Ver [`LICENSE`](LICENSE).
@@ -0,0 +1,292 @@
1
+ # ovicore
2
+
3
+ Biblioteca para avaliar e comparar modelos de detecção de ovos de *Aedes aegypti* em imagens de ovitrampas, independente do framework usado.
4
+
5
+ Projeto UFSM–Fiocruz. TCC em Sistemas de Informação, por Daniel Schopf, sob orientação da Profa. Andrea Schwertner Charão.
6
+
7
+ ## O problema
8
+
9
+ O projeto usa vários frameworks de detecção: Detectron2, Ultralytics e HuggingFace. Cada um treina do seu jeito, salva o resultado no seu formato e calcula as métricas com sua própria regra.
10
+
11
+ Isso torna a comparação entre eles pouco confiável. Quando dois números vêm de códigos diferentes, com convenções diferentes, comparar um com o outro não diz muita coisa.
12
+
13
+ ## O que a biblioteca faz
14
+
15
+ O ovicore não muda a forma como cada framework treina. Ele parte de uma entrada única e padroniza o que sai.
16
+
17
+ ```
18
+ ┌─────────────────┐
19
+ ENTRADA ────► │ O FRAMEWORK │ ────► SAÍDA
20
+ (fonte única │ (não mexemos) │ (padronizada)
21
+ em COCO) │ │
22
+ └─────────────────┘
23
+ Detectron2
24
+ Ultralytics
25
+ HuggingFace
26
+ ```
27
+
28
+ A entrada é uma fonte única de anotações em COCO. Converter essa fonte para o formato de cada framework está fora desta versão e fica como trabalho futuro. Na saída, as predições de qualquer modelo passam pelo mesmo cálculo de métricas. A diferença entre dois resultados passa a ser a diferença entre os modelos, não entre os códigos que os mediram.
29
+
30
+ ## Instalação
31
+
32
+ ```bash
33
+ pip install ovicore
34
+ pip install "ovicore[sahi]"
35
+ ```
36
+
37
+ O segundo comando instala também o SAHI, para a inferência fatiada. A versão em desenvolvimento pode ser instalada direto do GitHub, com acesso ao repositório: `pip install git+https://github.com/UFSM-Fiocruz/hybrid-core.git@biblioteca-pip`.
38
+
39
+ Para ler o orçamento de checkpoints do Ultralytics, que exige `torch`, use o extra `[ultralytics]`. Para a inferência fatiada com SAHI, use o extra `[sahi]`. O resto da biblioteca só depende de `pandas` e `numpy`.
40
+
41
+ No Google Colab:
42
+
43
+ ```python
44
+ %pip install -q ovicore
45
+ from ovicore.metrics import avaliar
46
+ ```
47
+
48
+ Para desenvolver:
49
+
50
+ ```bash
51
+ git clone https://github.com/UFSM-Fiocruz/hybrid-core.git
52
+ cd hybrid-core
53
+ pip install -e ".[dev]"
54
+ ```
55
+
56
+ As dependências da biblioteca vêm do `pyproject.toml`. O `requirements.txt` continua com as versões fixas usadas no Colab.
57
+
58
+ O notebook [`notebooks/exemplo_avaliacao.ipynb`](notebooks/exemplo_avaliacao.ipynb) mostra o uso completo: instala, importa e avalia.
59
+
60
+ ## Uso
61
+
62
+ ### Calcular as métricas
63
+
64
+ ```python
65
+ from ovicore.metrics import avaliar
66
+
67
+ resultado = avaliar(
68
+ pred_bboxes, pred_scores, pred_labels,
69
+ true_bboxes, true_labels,
70
+ score_threshold=0.40,
71
+ iou_threshold=0.45,
72
+ id2label={0: 'egg', 1: 'cluster'},
73
+ )
74
+
75
+ for classe, m in resultado.items():
76
+ print(classe, m)
77
+ ```
78
+
79
+ ```
80
+ geral {'vp': 229, 'fp': 163, 'fn': 157, 'precisao': 0.584, 'recall': 0.593, 'f1': 0.589}
81
+ egg {'vp': 219, 'fp': 156, 'fn': 150, 'precisao': 0.584, 'recall': 0.594, 'f1': 0.589}
82
+ cluster {'vp': 10, 'fp': 7, 'fn': 7, 'precisao': 0.588, 'recall': 0.588, 'f1': 0.588}
83
+ ```
84
+
85
+ As caixas devem estar em `[x1, y1, x2, y2]`. Se o dataset usa o formato COCO (`[x, y, largura, altura]`), converta antes com `BBox.from_coco()`.
86
+
87
+ ### Salvar os resultados
88
+
89
+ ```python
90
+ from ovicore.io import montar_metadata, gravar_predicoes
91
+
92
+ metadata = montar_metadata(
93
+ framework="ultralytics", arquitetura="yolov8n",
94
+ dataset="coco-dataset-v2", fold="fold-01-416",
95
+ n_epochs=500, batch_size=16, learning_rate=0.01,
96
+ score_threshold=0.10, iou_threshold=0.45, device="cuda",
97
+ evaluation_score_threshold=0.40,
98
+ )
99
+
100
+ csv_path, json_path = gravar_predicoes(
101
+ df, output_dir="saida", framework="ultralytics",
102
+ ambiente="colab", fold="fold-01-416", metadata=metadata,
103
+ )
104
+ ```
105
+
106
+ Gera dois arquivos:
107
+
108
+ ```
109
+ predictions-ultralytics-colab-fold-01-416.csv as predições
110
+ predictions-ultralytics-colab-fold-01-416.meta.json como foram geradas
111
+ ```
112
+
113
+ O JSON acompanha o CSV sempre. Quem recebe os dois consegue refazer o cálculo sem o notebook e sem adivinhar qual parâmetro foi usado.
114
+
115
+ ## Do notebook para a biblioteca
116
+
117
+ O que os notebooks repetem (verdade-terreno, SAHI e montagem do CSV) já está na biblioteca. Com o extra `[sahi]` instalado, o fluxo de avaliação de um modelo treinado fica assim, com `metadata` montado como na seção anterior:
118
+
119
+ ```python
120
+ from ovicore.anotacoes import ler_anotacoes_coco
121
+ from ovicore.contracts import SlicingConfig
122
+ from ovicore.fatiamento import carregar_modelo_sahi, inferir_com_sahi
123
+ from ovicore.io import montar_predicoes, gravar_predicoes
124
+ from ovicore.relatorio import resumo_execucoes
125
+
126
+ anotacoes = ler_anotacoes_coco("test/test.json")
127
+ modelo = carregar_modelo_sahi("ultralytics", "best.pt", None, score_threshold=0.10, device="cuda")
128
+ deteccoes = [inferir_com_sahi(modelo, f"test/{nome}", SlicingConfig()) for nome in anotacoes]
129
+ df = montar_predicoes(list(anotacoes), deteccoes, anotacoes)
130
+ gravar_predicoes(df, "saida", "ultralytics", "colab", "fold-01-416", metadata)
131
+ resumo_execucoes("saida")
132
+ ```
133
+
134
+ A verdade-terreno sai em `[x, y, x + w, y + h]`, sem o erro da caixa quadrada (E0). Os rótulos do modelo e os da anotação precisam estar no mesmo espaço de ids. Confira os ids antes de avaliar. No Faster R-CNN de referência, predições e `test.json` já usam 0 = egg e 1 = cluster, então o deslocamento é 0 (o padrão). Para reproduzir esses notebooks, use também `SlicingConfig(class_agnostic=True)`.
135
+
136
+ Antes e depois, só na parte de inferência e avaliação:
137
+
138
+ | Modelo | Antes | Células de código | Linhas de código | Depois | Células de código | Linhas de código |
139
+ |---|---|---|---|---|---|---|
140
+ | Faster R-CNN | notebook do grupo | 22 | 371 | `avaliacao_faster_rcnn_detectron2.ipynb` | 8 | 66 |
141
+ | Mask R-CNN | notebook do grupo | 22 | 373 | `avaliacao_mask_rcnn_detectron2.ipynb` | 10 | 94 |
142
+ | DETR | notebook do grupo | 10 | 123 | `avaliacao_detr_huggingface.ipynb` | 9 | 97 |
143
+ | YOLOv8n | exportação `.py` do Untitled2 | sem registro | 173 | `avaliacao_yolov8n_ultralytics.ipynb` | 8 | 69 |
144
+
145
+ Como foi contado:
146
+
147
+ - Linhas de código são as linhas não vazias que não são comentário.
148
+ - Nos notebooks do grupo, a parte de inferência e avaliação vai da seção "Avaliação" até o fim. Nos novos, vai da seção "3. Dados e modelo" até o fim, então inclui também a leitura da verdade-terreno, do orçamento e do modelo.
149
+ - A exportação `.py` do YOLOv8n entra inteira, sem a instalação e sem as células de texto. Ela não guarda os limites das células, então o número de células fica sem registro.
150
+ - No Mask R-CNN, o notebook novo faz duas avaliações: a reprodução, com as caixas da máscara, e o resultado oficial, com as caixas da cabeça de detecção.
151
+ - No DETR, a diferença é menor por dois motivos. A avaliação do notebook do grupo não chega ao fim: a função `compute_metrics` nunca é chamada. E o notebook novo tem uma célula de 22 linhas que confere a escala dos scores (E10); sem ela, seriam 75 linhas.
152
+
153
+ ## Relatório de execuções
154
+
155
+ `resumo_execucoes()` lê todos os pares CSV + `.meta.json` de uma pasta e monta uma linha por execução, com a configuração e as métricas calculadas pelo mesmo código. CSV sem `.meta.json` fica de fora.
156
+
157
+ ```python
158
+ from ovicore.relatorio import resumo_execucoes, divergencias_de_configuracao
159
+
160
+ resumo = resumo_execucoes("saida")
161
+ print(resumo[["framework", "arquitetura", "n_epochs", "score_threshold", "f1"]])
162
+ print(divergencias_de_configuracao(resumo))
163
+ ```
164
+
165
+ A saída abaixo vem de dados simulados, as duas execuções de demonstração do notebook de exemplo. Não é resultado de nenhum modelo real.
166
+
167
+ ```
168
+ framework arquitetura n_epochs score_threshold f1
169
+ detectron2 faster_rcnn_R_50_FPN_3x 100 0.5 0.5
170
+ ultralytics yolov8n 500 0.5 0.5
171
+ {'n_epochs': [100, 500], 'batch_size': [8, 16], 'learning_rate': [0.0001, 0.01]}
172
+ ```
173
+
174
+ `divergencias_de_configuracao()` lista os campos que variam entre as execuções. No exemplo, o F1 é igual, mas o orçamento de treino não, e isso fica registrado ao lado do resultado. O que conta como orçamento e os valores reais de cada modelo estão em [`docs/ORCAMENTO.md`](docs/ORCAMENTO.md).
175
+
176
+ Para o limiar de confiança, `varrer_limiares()` calcula precisão, revocação e F1 em vários limiares, e `escolher_limiar()` escolhe o de maior F1 na partição de validação. O limiar escolhido é aplicado depois ao teste.
177
+
178
+ Para os quatro modelos do trabalho, `scripts/relatorio_execucoes.py` lê a pasta `resultados/` baixada do Drive, roda `resumo_execucoes()` e `divergencias_de_configuracao()` sobre os CSVs de teste e grava [`docs/RELATORIO_EXECUCOES.md`](docs/RELATORIO_EXECUCOES.md). A tabela mostra o `dataset`, o `fold`, a `versao_dataset` e o número de caixas de cada teste, porque cada modelo foi avaliado num conjunto diferente. Os CSVs de predições não entram no repositório, só o relatório.
179
+
180
+ ```bash
181
+ python scripts/relatorio_execucoes.py caminho/para/resultados
182
+ ```
183
+
184
+ ## Estrutura
185
+
186
+ ```
187
+ hybrid-core/
188
+ ├── ovicore/
189
+ │ ├── metrics.py IoU, comparação e métricas
190
+ │ ├── io.py gravação e leitura padronizadas
191
+ │ ├── relatorio.py resumo de execuções e escolha do limiar
192
+ │ ├── anotacoes.py verdade-terreno a partir do COCO
193
+ │ ├── fatiamento.py inferência com SAHI (extra [sahi])
194
+ │ ├── contracts.py tipos e interfaces do core
195
+ │ └── backends/ adaptadores por framework
196
+ ├── tests/
197
+ │ ├── test_aceitacao.py metrics.py, io.py e contracts.py
198
+ │ ├── test_backend_*.py um por backend
199
+ │ ├── test_relatorio.py relatorio.py, com dados simulados
200
+ │ ├── test_anotacoes.py anotacoes.py, com um JSON pequeno
201
+ │ ├── test_fatiamento.py fatiamento.py, com um SAHI falso
202
+ │ ├── test_montar_predicoes.py
203
+ │ └── fixture_* dados reais usados nos testes
204
+ ├── notebooks/
205
+ │ ├── exemplo_avaliacao.ipynb
206
+ │ ├── avaliacao_faster_rcnn_detectron2.ipynb
207
+ │ ├── avaliacao_mask_rcnn_detectron2.ipynb
208
+ │ ├── treino_detr_huggingface.ipynb notebook do framework, fora da biblioteca
209
+ │ ├── avaliacao_detr_huggingface.ipynb
210
+ │ └── avaliacao_yolov8n_ultralytics.ipynb
211
+ ├── scripts/
212
+ │ └── relatorio_execucoes.py relatório dos quatro modelos a partir de resultados/
213
+ ├── docs/
214
+ │ ├── EVIDENCIAS.md os problemas que motivaram cada regra
215
+ │ ├── CONTRATO.md formato do CSV e do JSON
216
+ │ ├── ORCAMENTO.md o que é orçamento de treino e o de cada modelo
217
+ │ └── RELATORIO_EXECUCOES.md gerado por scripts/relatorio_execucoes.py
218
+ ├── .github/workflows/ testes a cada envio; tag v* publica no TestPyPI, Run workflow na tag publica no PyPI
219
+ ├── LICENSE MIT
220
+ ├── pyproject.toml
221
+ └── requirements.txt versões fixas para o Colab
222
+ ```
223
+
224
+ `metrics.py` e `io.py` estão prontos e testados. Os backends estão no começo: `Detectron2Backend`, `UltralyticsBackend` e `HuggingFaceBackend` só implementam `extrair_budget()`. `carregar_modelo()` e `inferir()` levantam `NotImplementedError`. `preparar_dataset()` também, porque a conversão na entrada está fora do escopo desta versão.
225
+
226
+ ## Resultados obtidos
227
+
228
+ Três modelos avaliados com o mesmo módulo de avaliação, sem alterar código. O Faster R-CNN e o YOLOv8n usam as 6 imagens de teste do `fold-01-416` atual, com recorte SAHI de 416px. Para o YOLOv8n esse teste tem vazamento: 4 das 6 imagens estavam no treino dele, e o F1 não é comparável. Por isso ele é avaliado no teste limpo do `fold01_416` antigo, que tem só 2 imagens de origem (21 fatias, 148 caixas; ver E11). O Mask R-CNN foi treinado numa versão anterior do conjunto de dados e é avaliado no teste original dele, com sobreposição de 50% (ver E10 em [`docs/EVIDENCIAS.md`](docs/EVIDENCIAS.md)).
229
+
230
+ | | Faster R-CNN | Mask R-CNN | YOLOv8n |
231
+ |---|---|---|---|
232
+ | Framework | Detectron2 | Detectron2 | Ultralytics |
233
+ | F1 (limiar ajustado) | 0,829 | 0,825 (limiar 0,75) | 0,609 no teste limpo (limiar 0,40); o 0,589 da referência não é comparável: vazamento |
234
+ | Origem do limiar | 0,75 herdado do grupo | 0,75 herdado do grupo | 0,40 escolhido no teste com vazamento |
235
+ | Limiar escolhido na validação | 0,60 (F1 0,799 no teste) | n/a | n/a |
236
+ | Prevê a classe `cluster`? | Não | Sim | Sim |
237
+ | Tempo de instalação | ~10 min | ~10 min | 9 s |
238
+
239
+ Só o limiar do Faster R-CNN foi escolhido na validação (37 fatias, 387 caixas): 0,60. No teste, ele dá VP 333, FP 115, FN 53 e F1 0,799, contra 0,829 com o 0,75 herdado. A validação é formada por fatias das mesmas imagens do treino, e o modelo vai melhor nela (F1 0,858 em 0,75, contra 0,829 no teste), o que provavelmente puxa o limiar para baixo. Nos outros modelos, o limiar é o 0,75 herdado do grupo (Mask R-CNN), o 0,40 escolhido no teste (YOLOv8n) e o 0,2 da demonstração (DETR). Ver E10.
240
+
241
+ O Mask R-CNN tem duas medições com os mesmos pesos, no mesmo teste e no mesmo limiar:
242
+
243
+ | Mask R-CNN | VP | FP | FN | F1 |
244
+ |---|---|---|---|---|
245
+ | Resultado oficial, caixas da cabeça de detecção | 341 | 94 | 51 | 0,825 |
246
+ | Reprodução de 24/11/2024, caixas da máscara | 231 | 213 | 161 | 0,553 |
247
+
248
+ Só a origem das caixas muda entre as duas linhas: com a máscara ligada, o SAHI monta a caixa pelo contorno da máscara, em pixels inteiros, e isso baixa o F1 em 0,27. O resultado do Mask R-CNN não se compara diretamente com o do Faster R-CNN, porque foi medido em outro teste, sobre outra versão dos dados e com sobreposição de 0,5.
249
+
250
+ O YOLOv8n, no teste limpo, com limiar 0,40, sem referência verificável:
251
+
252
+ | YOLOv8n | VP | FP | FN | F1 |
253
+ |---|---|---|---|---|
254
+ | Teste limpo, `fold01_416` antigo | 92 | 62 | 56 | 0,609 |
255
+ | Referência de 22/08/2026, teste com vazamento | 229 | 163 | 157 | 0,589 |
256
+
257
+ As duas linhas não se comparam: a referência foi medida em outro teste, em que 4 das 6 imagens estavam no treino do modelo. O teste limpo tem só 2 imagens de origem, o que limita a variedade das imagens e dos aglomerados (6). O limiar de 0,40 foi escolhido no teste com vazamento e é provisório.
258
+
259
+ Os três modelos não foram treinados nas mesmas condições. As épocas, o learning rate e o otimizador são diferentes entre eles (ver [`docs/ORCAMENTO.md`](docs/ORCAMENTO.md)). Os números servem para verificar que a padronização funciona nos três, não para comparar a qualidade dos modelos. A biblioteca registra o orçamento de cada execução e aponta quando ele difere entre as execuções comparadas.
260
+
261
+ Um quarto framework, HuggingFace/DETR (`conditional-detr-resnet-50`), também passou pelo mesmo pipeline sem alterar código. Retreinado em 08/11/2026 no `fold-01-256` e avaliado no teste dele (as mesmas 6 imagens do `fold-01-416` atual), com limiar provisório de 0,2: VP 275, FP 1.733, FN 111, F1 0,230, sem referência verificável (ver E10). Não entra na tabela porque foi treinado num fold diferente dos outros três e com orçamento reduzido de propósito (30 épocas, só para validar o contrato). O F1 dele não é comparável com os de cima.
262
+
263
+ ## De onde vêm as regras
264
+
265
+ As regras da biblioteca vieram de problemas encontrados ao rodar os frameworks. O arquivo [`docs/EVIDENCIAS.md`](docs/EVIDENCIAS.md) traz cada um com os números.
266
+
267
+ | Regra | Motivo |
268
+ |---|---|
269
+ | Coordenadas em `float` | `np.float32` corrompe o CSV sem dar erro na hora |
270
+ | Não truncar coordenadas | Com a máscara, o SAHI devolve as caixas do Mask R-CNN em pixels inteiros, o que distorce a medição |
271
+ | Considerar a classe ao comparar | Sem isso, o problema da classe `cluster` fica invisível |
272
+ | Ordenar por confiança | Os notebooks comparam na ordem do arquivo |
273
+ | Ajustar o limiar por framework | Com 0,75 fixo, o F1 do YOLOv8n cai de 0,589 para 0,234, no teste com vazamento (ver E11) |
274
+ | Salvar os metadados junto | O modelo salvo não bate com o que o notebook diz |
275
+ | Registrar o treino usado | Mask R-CNN, Faster R-CNN (referência do grupo, não o retreinado), DETR e YOLOv8n têm cerca de 183, cerca de 100, 30 e 45 épocas (o YOLOv8n de 500 configuradas, com parada antecipada) |
276
+ | Registrar a granularidade | Um dataset guarda a imagem inteira, outro guarda recortes |
277
+
278
+ ## Testes
279
+
280
+ ```bash
281
+ pytest -q
282
+ ```
283
+
284
+ Rodam localmente com `pytest` e no GitHub Actions a cada envio, em Python 3.10, 3.11 e 3.12 (`.github/workflows/testes.yml`). Nenhum precisa de GPU nem de framework de detecção.
285
+
286
+ Os dados são de dois tipos: simulados, montados dentro do próprio teste, e arquivos pequenos de exemplo em `tests/` (um CSV real de predições do YOLOv8n, o `metrics.json` do Mask R-CNN e o `train_args` do YOLOv8n). Além de conferir os cálculos, os testes verificam se a biblioteca recusa os problemas conhecidos: tipos errados, rótulos em texto e arquivos sem metadados.
287
+
288
+ Treino e inferência não são testados aqui. Eles ficam nos notebooks de cada modelo. O único teste que abre um checkpoint `.pt` real é pulado quando faltam `torch`, `ultralytics` ou o próprio arquivo, que não é versionado.
289
+
290
+ ## Licença
291
+
292
+ MIT. Ver [`LICENSE`](LICENSE).
@@ -0,0 +1,20 @@
1
+ """
2
+ Núcleo de avaliação padronizada para comparação de frameworks de detecção
3
+ de objetos.
4
+
5
+ Projeto UFSM-Fiocruz: detecção de ovos de Aedes aegypti em ovitrampas.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from ovicore.metrics import avaliar, compute_iou, compute_metrics
11
+ from ovicore.io import (
12
+ gravar_predicoes, carregar_predicoes, montar_metadata,
13
+ validar_contrato, padronizar_labels_para_int,
14
+ )
15
+
16
+ __all__ = [
17
+ "avaliar", "compute_iou", "compute_metrics",
18
+ "gravar_predicoes", "carregar_predicoes", "montar_metadata",
19
+ "validar_contrato", "padronizar_labels_para_int",
20
+ ]