Metadata-Version: 2.4
Name: ovicore
Version: 0.1.0
Summary: Avaliação padronizada e reprodutível de detectores de objetos treinados em diferentes frameworks
Author: Daniel Ferreira Schopf
License-Expression: MIT
Project-URL: Repositorio, https://github.com/UFSM-Fiocruz/hybrid-core
Keywords: object-detection,evaluation,reproducibility,detectron2,ultralytics,huggingface
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pandas>=2.2
Requires-Dist: numpy>=1.26
Provides-Extra: ultralytics
Requires-Dist: ultralytics>=8.3; extra == "ultralytics"
Requires-Dist: torch>=2.1; extra == "ultralytics"
Provides-Extra: sahi
Requires-Dist: sahi<0.13,>=0.11.19; extra == "sahi"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# ovicore

Biblioteca para avaliar e comparar modelos de detecção de ovos de *Aedes aegypti* em imagens de ovitrampas, independente do framework usado.

Projeto UFSM–Fiocruz. TCC em Sistemas de Informação, por Daniel Schopf, sob orientação da Profa. Andrea Schwertner Charão.

## O problema

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.

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.

## O que a biblioteca faz

O ovicore não muda a forma como cada framework treina. Ele parte de uma entrada única e padroniza o que sai.

```
                    ┌─────────────────┐
   ENTRADA  ────►   │   O FRAMEWORK   │   ────►  SAÍDA
 (fonte única       │  (não mexemos)  │      (padronizada)
   em COCO)         │                 │
                    └─────────────────┘
                     Detectron2
                     Ultralytics
                     HuggingFace
```

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.

## Instalação

```bash
pip install ovicore
pip install "ovicore[sahi]"
```

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`.

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`.

No Google Colab:

```python
%pip install -q ovicore
from ovicore.metrics import avaliar
```

Para desenvolver:

```bash
git clone https://github.com/UFSM-Fiocruz/hybrid-core.git
cd hybrid-core
pip install -e ".[dev]"
```

As dependências da biblioteca vêm do `pyproject.toml`. O `requirements.txt` continua com as versões fixas usadas no Colab.

O notebook [`notebooks/exemplo_avaliacao.ipynb`](notebooks/exemplo_avaliacao.ipynb) mostra o uso completo: instala, importa e avalia.

## Uso

### Calcular as métricas

```python
from ovicore.metrics import avaliar

resultado = avaliar(
    pred_bboxes, pred_scores, pred_labels,
    true_bboxes, true_labels,
    score_threshold=0.40,
    iou_threshold=0.45,
    id2label={0: 'egg', 1: 'cluster'},
)

for classe, m in resultado.items():
    print(classe, m)
```

```
geral    {'vp': 229, 'fp': 163, 'fn': 157, 'precisao': 0.584, 'recall': 0.593, 'f1': 0.589}
egg      {'vp': 219, 'fp': 156, 'fn': 150, 'precisao': 0.584, 'recall': 0.594, 'f1': 0.589}
cluster  {'vp': 10,  'fp': 7,   'fn': 7,   'precisao': 0.588, 'recall': 0.588, 'f1': 0.588}
```

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()`.

### Salvar os resultados

```python
from ovicore.io import montar_metadata, gravar_predicoes

metadata = montar_metadata(
    framework="ultralytics", arquitetura="yolov8n",
    dataset="coco-dataset-v2", fold="fold-01-416",
    n_epochs=500, batch_size=16, learning_rate=0.01,
    score_threshold=0.10, iou_threshold=0.45, device="cuda",
    evaluation_score_threshold=0.40,
)

csv_path, json_path = gravar_predicoes(
    df, output_dir="saida", framework="ultralytics",
    ambiente="colab", fold="fold-01-416", metadata=metadata,
)
```

Gera dois arquivos:

```
predictions-ultralytics-colab-fold-01-416.csv        as predições
predictions-ultralytics-colab-fold-01-416.meta.json  como foram geradas
```

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.

## Do notebook para a biblioteca

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:

```python
from ovicore.anotacoes import ler_anotacoes_coco
from ovicore.contracts import SlicingConfig
from ovicore.fatiamento import carregar_modelo_sahi, inferir_com_sahi
from ovicore.io import montar_predicoes, gravar_predicoes
from ovicore.relatorio import resumo_execucoes

anotacoes = ler_anotacoes_coco("test/test.json")
modelo = carregar_modelo_sahi("ultralytics", "best.pt", None, score_threshold=0.10, device="cuda")
deteccoes = [inferir_com_sahi(modelo, f"test/{nome}", SlicingConfig()) for nome in anotacoes]
df = montar_predicoes(list(anotacoes), deteccoes, anotacoes)
gravar_predicoes(df, "saida", "ultralytics", "colab", "fold-01-416", metadata)
resumo_execucoes("saida")
```

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)`.

Antes e depois, só na parte de inferência e avaliação:

| Modelo | Antes | Células de código | Linhas de código | Depois | Células de código | Linhas de código |
|---|---|---|---|---|---|---|
| Faster R-CNN | notebook do grupo | 22 | 371 | `avaliacao_faster_rcnn_detectron2.ipynb` | 8 | 66 |
| Mask R-CNN | notebook do grupo | 22 | 373 | `avaliacao_mask_rcnn_detectron2.ipynb` | 10 | 94 |
| DETR | notebook do grupo | 10 | 123 | `avaliacao_detr_huggingface.ipynb` | 9 | 97 |
| YOLOv8n | exportação `.py` do Untitled2 | sem registro | 173 | `avaliacao_yolov8n_ultralytics.ipynb` | 8 | 69 |

Como foi contado:

- Linhas de código são as linhas não vazias que não são comentário.
- 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.
- 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.
- 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.
- 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.

## Relatório de execuções

`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.

```python
from ovicore.relatorio import resumo_execucoes, divergencias_de_configuracao

resumo = resumo_execucoes("saida")
print(resumo[["framework", "arquitetura", "n_epochs", "score_threshold", "f1"]])
print(divergencias_de_configuracao(resumo))
```

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.

```
  framework             arquitetura  n_epochs  score_threshold  f1
 detectron2 faster_rcnn_R_50_FPN_3x       100              0.5 0.5
ultralytics                 yolov8n       500              0.5 0.5
{'n_epochs': [100, 500], 'batch_size': [8, 16], 'learning_rate': [0.0001, 0.01]}
```

`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).

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.

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.

```bash
python scripts/relatorio_execucoes.py caminho/para/resultados
```

## Estrutura

```
hybrid-core/
├── ovicore/
│   ├── metrics.py      IoU, comparação e métricas
│   ├── io.py           gravação e leitura padronizadas
│   ├── relatorio.py    resumo de execuções e escolha do limiar
│   ├── anotacoes.py    verdade-terreno a partir do COCO
│   ├── fatiamento.py   inferência com SAHI (extra [sahi])
│   ├── contracts.py    tipos e interfaces do core
│   └── backends/       adaptadores por framework
├── tests/
│   ├── test_aceitacao.py       metrics.py, io.py e contracts.py
│   ├── test_backend_*.py       um por backend
│   ├── test_relatorio.py       relatorio.py, com dados simulados
│   ├── test_anotacoes.py       anotacoes.py, com um JSON pequeno
│   ├── test_fatiamento.py      fatiamento.py, com um SAHI falso
│   ├── test_montar_predicoes.py
│   └── fixture_*               dados reais usados nos testes
├── notebooks/
│   ├── exemplo_avaliacao.ipynb
│   ├── avaliacao_faster_rcnn_detectron2.ipynb
│   ├── avaliacao_mask_rcnn_detectron2.ipynb
│   ├── treino_detr_huggingface.ipynb       notebook do framework, fora da biblioteca
│   ├── avaliacao_detr_huggingface.ipynb
│   └── avaliacao_yolov8n_ultralytics.ipynb
├── scripts/
│   └── relatorio_execucoes.py   relatório dos quatro modelos a partir de resultados/
├── docs/
│   ├── EVIDENCIAS.md   os problemas que motivaram cada regra
│   ├── CONTRATO.md     formato do CSV e do JSON
│   ├── ORCAMENTO.md    o que é orçamento de treino e o de cada modelo
│   └── RELATORIO_EXECUCOES.md   gerado por scripts/relatorio_execucoes.py
├── .github/workflows/  testes a cada envio; tag v* publica no TestPyPI, Run workflow na tag publica no PyPI
├── LICENSE             MIT
├── pyproject.toml
└── requirements.txt    versões fixas para o Colab
```

`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.

## Resultados obtidos

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)).

| | Faster R-CNN | Mask R-CNN | YOLOv8n |
|---|---|---|---|
| Framework | Detectron2 | Detectron2 | Ultralytics |
| 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 |
| Origem do limiar | 0,75 herdado do grupo | 0,75 herdado do grupo | 0,40 escolhido no teste com vazamento |
| Limiar escolhido na validação | 0,60 (F1 0,799 no teste) | n/a | n/a |
| Prevê a classe `cluster`? | Não | Sim | Sim |
| Tempo de instalação | ~10 min | ~10 min | 9 s |

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.

O Mask R-CNN tem duas medições com os mesmos pesos, no mesmo teste e no mesmo limiar:

| Mask R-CNN | VP | FP | FN | F1 |
|---|---|---|---|---|
| Resultado oficial, caixas da cabeça de detecção | 341 | 94 | 51 | 0,825 |
| Reprodução de 24/11/2024, caixas da máscara | 231 | 213 | 161 | 0,553 |

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.

O YOLOv8n, no teste limpo, com limiar 0,40, sem referência verificável:

| YOLOv8n | VP | FP | FN | F1 |
|---|---|---|---|---|
| Teste limpo, `fold01_416` antigo | 92 | 62 | 56 | 0,609 |
| Referência de 22/08/2026, teste com vazamento | 229 | 163 | 157 | 0,589 |

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.

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.

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.

## De onde vêm as regras

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.

| Regra | Motivo |
|---|---|
| Coordenadas em `float` | `np.float32` corrompe o CSV sem dar erro na hora |
| 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 |
| Considerar a classe ao comparar | Sem isso, o problema da classe `cluster` fica invisível |
| Ordenar por confiança | Os notebooks comparam na ordem do arquivo |
| 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) |
| Salvar os metadados junto | O modelo salvo não bate com o que o notebook diz |
| 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) |
| Registrar a granularidade | Um dataset guarda a imagem inteira, outro guarda recortes |

## Testes

```bash
pytest -q
```

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.

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.

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.

## Licença

MIT. Ver [`LICENSE`](LICENSE).
