jaimevera1107 commited on
Commit
d04393d
·
1 Parent(s): 69be85a

Versión final lista para Hugging Face Space

Browse files
.dockerignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ resources/logs/
4
+ resources/outputs/
5
+ resources/uploads/
6
+ *.log
7
+ *.csv
8
+ *.jpg
9
+ *.jpeg
10
+ *.png
11
+ *.mp4
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Imagen base con soporte CUDA + PyTorch
2
+ FROM pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime
3
+
4
+ # Establecer directorio de trabajo
5
+ WORKDIR /app
6
+
7
+ # Instalar utilidades necesarias (git para clonar repos incluidos en requirements)
8
+ RUN apt-get update && apt-get install -y git ffmpeg libsm6 libxext6 && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copiar requirements y dependencias
11
+ COPY requirements.txt /app/
12
+ RUN pip install --upgrade pip && pip install -r requirements.txt
13
+
14
+ # Copiar todo el proyecto
15
+ COPY . /app/
16
+
17
+ # Crear directorios necesarios en caso de que no existan
18
+ RUN mkdir -p resources/uploads resources/outputs resources/logs
19
+
20
+ # Exponer puerto para Gradio
21
+ EXPOSE 7860
22
+
23
+ # Variable obligatoria para Gradio en Spaces
24
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
25
+
26
+ # Comando de ejecución
27
+ CMD ["python", "app.py"]
HerdNet ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 7e25f482d875522c59c446dc0c78c8f6f2dd448d
app.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import warnings
2
+ import gradio as gr
3
+ from PIL import Image
4
+ import pandas as pd
5
+ import os
6
+
7
+ from inference.herdnet_infer import HerdNetInference
8
+ from inference.utils_io import load_yaml_config, mkdir
9
+ from animaloc.utils.seed import set_seed
10
+
11
+ # Ignorar advertencias no críticas
12
+ warnings.filterwarnings(
13
+ "ignore",
14
+ message="Got processor for keypoints, but no transform to process it",
15
+ )
16
+
17
+ # Fijar semilla para reproducibilidad
18
+ set_seed(9292)
19
+
20
+ # ===============================================================
21
+ # Configuración inicial
22
+ # ===============================================================
23
+ CONFIG_PATH = "resources/configs/default.yaml"
24
+ cfg = load_yaml_config(CONFIG_PATH)
25
+ mkdir(cfg["paths"]["uploads_dir"])
26
+
27
+ print("[INIT] Cargando modelo HerdNet... esto puede tardar unos segundos.")
28
+ infer_engine = HerdNetInference(CONFIG_PATH)
29
+ print("[READY] Modelo cargado y listo para inferencia.")
30
+
31
+ # Información del modelo (actualizada con tablas)
32
+ MODEL_INFO = """
33
+ ### Arquitectura y datos
34
+ - **Modelo:** HerdNet (FPN + Density Maps)
35
+ - **Dataset:** ULiège-AIR (6 especies + background)
36
+ - **Última actualización:** 07 Nov 2025
37
+
38
+ ### Desempeño general (Fine-Tuning oficial)
39
+
40
+ | Métrica | Valor |
41
+ |--------------|---------|
42
+ | F1-score | 0.8405 |
43
+ | Precision | 0.8407 |
44
+ | Recall | 0.8404 |
45
+ | MAE | 1.8023 |
46
+ | RMSE | 3.4892 |
47
+
48
+ ### Matriz de confusión (normalizada)
49
+
50
+ | Real \\ Predicha | buffalo | elephant | kob | topi | warthog | waterbuck |
51
+ |------------------|----------|-----------|------|------|----------|-------------|
52
+ | **buffalo** | 0.94 | 0.00 | 0.05 | 0.01 | 0.00 | 0.00 |
53
+ | **elephant** | 0.01 | 0.91 | 0.00 | 0.07 | 0.01 | 0.00 |
54
+ | **kob** | 0.08 | 0.00 | 0.92 | 0.00 | 0.00 | 0.00 |
55
+ | **topi** | 0.03 | 0.00 | 0.00 | 0.94 | 0.03 | 0.00 |
56
+ | **warthog** | 0.06 | 0.06 | 0.06 | 0.00 | 0.81 | 0.00 |
57
+ | **waterbuck** | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 1.00 |
58
+ """
59
+
60
+
61
+ def run_inference(image: Image.Image):
62
+ """
63
+ Ejecuta la inferencia sobre una imagen PIL y devuelve los resultados.
64
+ """
65
+ if image is None:
66
+ return None, pd.DataFrame(), None, None
67
+
68
+ annotated_img, counts = infer_engine.infer_single(image)
69
+
70
+ # Construir tabla completa de especies
71
+ all_species = list(infer_engine.classes.values())
72
+ df_counts = pd.DataFrame({
73
+ "Especie": all_species,
74
+ "Conteo": [counts.get(sp, 0) for sp in all_species],
75
+ })
76
+ df_counts.loc[len(df_counts)] = ["Total", df_counts["Conteo"].sum()]
77
+
78
+ # Guardar conteos
79
+ csv_counts_path = os.path.join(infer_engine.output_dir, "species_counts.csv")
80
+ df_counts.to_csv(csv_counts_path, index=False)
81
+
82
+ # Comprobar existencia de detecciones
83
+ detections_csv = os.path.join(infer_engine.output_dir, "detections.csv")
84
+ if not os.path.exists(detections_csv):
85
+ detections_csv = None
86
+
87
+ return annotated_img, df_counts, csv_counts_path, detections_csv
88
+
89
+
90
+ # ===============================================================
91
+ # Interfaz de Gradio (versión estética mejorada)
92
+ # ===============================================================
93
+ custom_css = """
94
+ #main-title h1 {
95
+ font-size: 2.2em !important;
96
+ color: #e0f2fe !important;
97
+ font-weight: 700 !important;
98
+ margin-bottom: 0.3em;
99
+ }
100
+
101
+ h2 {
102
+ color: #93c5fd !important;
103
+ font-weight: 600 !important;
104
+ margin-top: 1em;
105
+ margin-bottom: 0.3em;
106
+ }
107
+
108
+ .block-section {
109
+ background-color: #0f172a;
110
+ border-radius: 10px;
111
+ padding: 15px 20px;
112
+ margin-bottom: 25px;
113
+ box-shadow: 0 0 10px #00000040;
114
+ }
115
+
116
+ .img-bordered img {
117
+ border-radius: 10px;
118
+ box-shadow: 0 0 10px #00000040;
119
+ }
120
+
121
+ .data-table table {
122
+ font-size: 15px !important;
123
+ }
124
+
125
+ .download-btn {
126
+ background-color: #1e3a8a !important;
127
+ color: #f8fafc !important;
128
+ font-weight: 600 !important;
129
+ border-radius: 8px !important;
130
+ padding: 8px 14px !important;
131
+ border: 1px solid #3b82f6 !important;
132
+ transition: all 0.2s ease-in-out;
133
+ }
134
+
135
+ .download-btn:hover {
136
+ background-color: #2563eb !important;
137
+ transform: scale(1.05);
138
+ }
139
+
140
+ .big-button button {
141
+ background-color: #1d4ed8 !important;
142
+ color: #f8fafc !important;
143
+ font-weight: 700 !important;
144
+ font-size: 20px !important;
145
+ padding: 16px 28px !important;
146
+ border-radius: 10px !important;
147
+ width: 100% !important;
148
+ transition: all 0.25s ease-in-out;
149
+ }
150
+
151
+ .big-button button:hover {
152
+ background-color: #2563eb !important;
153
+ transform: scale(1.03);
154
+ }
155
+ """
156
+
157
+ with gr.Blocks(
158
+ theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),
159
+ css=custom_css,
160
+ ) as demo:
161
+ # Encabezado principal
162
+ gr.Markdown("# Detección y Conteo de Mamíferos Africanos", elem_id="main-title")
163
+
164
+ # Información del modelo (colapsable con tablas)
165
+ with gr.Accordion("Información del modelo", open=False):
166
+ gr.Markdown(MODEL_INFO)
167
+
168
+ # Bloque: resultados visuales
169
+ gr.Markdown("## Resultados de inferencia")
170
+ with gr.Row(elem_classes=["block-section"]):
171
+ image_input = gr.Image(
172
+ type="pil",
173
+ label="Subir imagen aérea",
174
+ height=380,
175
+ elem_classes=["img-bordered"],
176
+ )
177
+ image_output = gr.Image(
178
+ label="Detecciones (puntos resaltados)",
179
+ height=380,
180
+ elem_classes=["img-bordered"],
181
+ )
182
+
183
+ # Botón principal más grande y visible
184
+ btn = gr.Button(
185
+ "Ejecutar detección y conteo",
186
+ variant="primary",
187
+ elem_classes=["big-button"],
188
+ )
189
+
190
+ # Bloque: conteo detallado
191
+ gr.Markdown("## Conteo detallado por especie")
192
+ with gr.Column(elem_classes=["block-section"]):
193
+ counts_output = gr.Dataframe(
194
+ headers=["Especie", "Conteo"],
195
+ label="Resultados de detección",
196
+ interactive=False,
197
+ elem_classes=["data-table"],
198
+ )
199
+
200
+ with gr.Row():
201
+ download_counts = gr.File(
202
+ label="Descargar conteos (CSV)",
203
+ elem_classes=["download-btn"],
204
+ )
205
+ download_detections = gr.File(
206
+ label="Descargar anotaciones (detections.csv)",
207
+ elem_classes=["download-btn"],
208
+ )
209
+
210
+ btn.click(
211
+ fn=run_inference,
212
+ inputs=image_input,
213
+ outputs=[image_output, counts_output, download_counts, download_detections],
214
+ )
215
+
216
+ if __name__ == "__main__":
217
+ demo.launch(share=False, server_port=7860)
inference/__pycache__/herdnet_infer.cpython-312.pyc ADDED
Binary file (7.3 kB). View file
 
inference/__pycache__/postprocessing.cpython-312.pyc ADDED
Binary file (6.02 kB). View file
 
inference/__pycache__/preprocessing.cpython-312.pyc ADDED
Binary file (2.8 kB). View file
 
inference/__pycache__/utils_io.cpython-312.pyc ADDED
Binary file (5.49 kB). View file
 
inference/herdnet_infer.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import pandas as pd
4
+ import numpy as np
5
+ from torch.utils.data import DataLoader
6
+ from animaloc.models import HerdNet, LossWrapper
7
+ from animaloc.eval import HerdNetEvaluator, HerdNetStitcher
8
+ from animaloc.eval.metrics import PointsMetrics
9
+
10
+ from inference.preprocessing import (
11
+ build_normalize_transform,
12
+ build_end_transforms,
13
+ create_single_image_dataset,
14
+ )
15
+ from inference.postprocessing import (
16
+ compute_species_counts,
17
+ draw_detections_on_image,
18
+ generate_thumbnails,
19
+ save_detections,
20
+ )
21
+ from inference.utils_io import (
22
+ mkdir,
23
+ get_timestamp_dir,
24
+ init_logger,
25
+ load_yaml_config,
26
+ )
27
+
28
+
29
+ class HerdNetInference:
30
+ """
31
+ Clase envoltoria para cargar un modelo HerdNet entrenado y ejecutar
32
+ inferencias sobre imágenes individuales o carpetas completas,
33
+ incluyendo posprocesamiento y exportación de resultados.
34
+ """
35
+
36
+ def __init__(self, config_path: str = "resources/configs/default.yaml"):
37
+ self.cfg = load_yaml_config(config_path)
38
+ self.logger = init_logger(self.cfg["paths"]["logs_dir"])
39
+
40
+ # Parámetros principales
41
+ self.device = torch.device(
42
+ self.cfg["model"]["device"] if torch.cuda.is_available() else "cpu"
43
+ )
44
+ self.patch_size = self.cfg["model"]["patch_size"]
45
+ self.overlap = self.cfg["model"]["overlap"]
46
+ self.down_ratio = self.cfg["model"]["down_ratio"]
47
+ self.save_plots = self.cfg["inference"]["save_plots"]
48
+ self.save_csv = self.cfg["inference"]["save_csv"]
49
+ self.save_thumbnails = self.cfg["inference"]["save_thumbnails"]
50
+
51
+ # Directorio de salida
52
+ self.outputs_base = self.cfg["paths"]["outputs_dir"]
53
+ mkdir(self.outputs_base)
54
+ self.output_dir = get_timestamp_dir(self.outputs_base)
55
+ self.logger.info(f"[INIT] Directorio de salida: {self.output_dir}")
56
+
57
+ self._load_model()
58
+
59
+ # -----------------------------------------------------------
60
+ def _load_model(self):
61
+ """
62
+ Carga el modelo HerdNet desde el archivo .pth y lo prepara para inferencia.
63
+ """
64
+ pth_path = self.cfg["model"]["path"]
65
+ if not os.path.exists(pth_path):
66
+ raise FileNotFoundError(f"[ERROR] No se encontró el modelo → {pth_path}")
67
+
68
+ checkpoint = torch.load(pth_path, map_location=self.device)
69
+ self.classes = checkpoint["classes"]
70
+ self.num_classes = len(self.classes) + 1
71
+ self.mean = checkpoint["mean"]
72
+ self.std = checkpoint["std"]
73
+
74
+ model = HerdNet(num_classes=self.num_classes, pretrained=False)
75
+ self.model = LossWrapper(model, [])
76
+ self.model.load_state_dict(checkpoint["model_state_dict"])
77
+ self.model.to(self.device)
78
+ self.model.eval()
79
+
80
+ self.logger.info(
81
+ f"[MODEL] Modelo HerdNet cargado ({self.num_classes} clases) desde {pth_path}"
82
+ )
83
+
84
+ # -----------------------------------------------------------
85
+ def infer_single(self, image_pil):
86
+ """
87
+ Ejecuta la inferencia sobre una imagen PIL.
88
+
89
+ Retorna
90
+ -------
91
+ annotated_image : PIL.Image
92
+ Imagen anotada con las detecciones.
93
+ counts_per_species : dict
94
+ Diccionario con el conteo por especie.
95
+ """
96
+ dataset, dataloader, temp_path = create_single_image_dataset(
97
+ image_pil, mean=self.mean, std=self.std, down_ratio=self.down_ratio
98
+ )
99
+
100
+ stitcher = HerdNetStitcher(
101
+ model=self.model,
102
+ size=(self.patch_size, self.patch_size),
103
+ overlap=self.overlap,
104
+ down_ratio=self.down_ratio,
105
+ up=True,
106
+ reduction="mean",
107
+ device_name=self.device,
108
+ )
109
+
110
+ metrics = PointsMetrics(5, num_classes=self.num_classes)
111
+ evaluator = HerdNetEvaluator(
112
+ model=self.model,
113
+ dataloader=dataloader,
114
+ metrics=metrics,
115
+ device_name=self.device,
116
+ stitcher=stitcher,
117
+ work_dir=self.output_dir,
118
+ header="[INFERENCE]",
119
+ )
120
+
121
+ self.logger.info("[RUN] Iniciando inferencia sobre imagen individual...")
122
+ evaluator.evaluate(wandb_flag=False, viz=False, log_meters=False)
123
+
124
+ detections = evaluator.detections.dropna().copy()
125
+ detections["species"] = detections["labels"].map(self.classes)
126
+
127
+ # Guardar CSV con detecciones
128
+ if self.save_csv:
129
+ save_detections(detections, self.output_dir, self.logger)
130
+
131
+ counts = compute_species_counts(detections)
132
+ self.logger.info(f"[COUNTS] {counts}")
133
+
134
+ # Dibujar detecciones sobre la imagen original
135
+ annotated_image = draw_detections_on_image(
136
+ image_path=temp_path,
137
+ detections_df=detections,
138
+ )
139
+
140
+ # Generar miniaturas si está habilitado
141
+ if self.save_thumbnails:
142
+ thumbs_dir = os.path.join(self.output_dir, "thumbnails")
143
+ generate_thumbnails(temp_path, detections, thumbs_dir)
144
+
145
+ # Limpieza del archivo temporal
146
+ try:
147
+ os.remove(temp_path)
148
+ self.logger.info(f"[CLEANUP] Archivo temporal eliminado: {temp_path}")
149
+ except Exception as e:
150
+ self.logger.warning(f"[CLEANUP] No se pudo eliminar el archivo temporal: {e}")
151
+
152
+ return annotated_image, counts
inference/postprocessing.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ from PIL import Image
4
+ from animaloc.vizual import draw_points, draw_text
5
+ from inference.utils_io import mkdir, save_csv
6
+
7
+
8
+ def draw_detections_on_image(
9
+ image_path: str,
10
+ detections_df: pd.DataFrame,
11
+ output_path: str = None
12
+ ) -> Image.Image:
13
+ """
14
+ Dibuja puntos visibles sobre una imagen y añade una leyenda
15
+ con el total de detecciones y el desglose por especie.
16
+
17
+ Parámetros
18
+ ----------
19
+ image_path : str
20
+ Ruta de la imagen original.
21
+ detections_df : pd.DataFrame
22
+ DataFrame con las detecciones (columnas: x, y, species, scores, etc.).
23
+ output_path : str, opcional
24
+ Ruta donde se guardará la imagen anotada.
25
+
26
+ Retorna
27
+ -------
28
+ Image.Image
29
+ Imagen con los puntos y leyenda dibujados.
30
+ """
31
+ img = Image.open(image_path)
32
+ img_cpy = img.copy()
33
+
34
+ # Extraer coordenadas (y, x)
35
+ pts = list(detections_df[["y", "x"]].to_records(index=False))
36
+ pts = [(y, x) for y, x in pts]
37
+
38
+ # Dibujar puntos sobre la imagen
39
+ output = draw_points(img_cpy, pts, color=(255, 0, 0), size=60)
40
+
41
+ # Construir texto de leyenda
42
+ species_counts = detections_df["species"].value_counts().to_dict()
43
+ total = sum(species_counts.values())
44
+ legend = f"Detecciones: {total} | " + ", ".join(
45
+ [f"{sp}: {n}" for sp, n in species_counts.items()]
46
+ )
47
+
48
+ # Posicionar texto en parte inferior
49
+ overlay_y = img_cpy.height - int(0.08 * img_cpy.height)
50
+ output = draw_text(
51
+ output,
52
+ text=legend,
53
+ position=(20, overlay_y),
54
+ font_size=int(0.04 * img_cpy.height),
55
+ )
56
+
57
+ # Guardar imagen si se especifica ruta de salida
58
+ if output_path:
59
+ mkdir(os.path.dirname(output_path))
60
+ output.save(output_path, quality=95)
61
+
62
+ return output
63
+
64
+
65
+ def compute_species_counts(detections_df: pd.DataFrame) -> dict:
66
+ """
67
+ Calcula el número de detecciones por especie.
68
+
69
+ Retorna
70
+ -------
71
+ dict
72
+ Diccionario con las especies y sus conteos.
73
+ Retorna un diccionario vacío si no hay detecciones.
74
+ """
75
+ if detections_df.empty:
76
+ return {}
77
+ return detections_df["species"].value_counts().to_dict()
78
+
79
+
80
+ def generate_thumbnails(
81
+ image_path: str,
82
+ detections_df: pd.DataFrame,
83
+ output_dir: str,
84
+ thumb_size: int = 256
85
+ ) -> None:
86
+ """
87
+ Genera miniaturas recortadas alrededor de cada detección,
88
+ con el nombre de la especie y su puntaje de confianza.
89
+
90
+ Parámetros
91
+ ----------
92
+ image_path : str
93
+ Ruta de la imagen original.
94
+ detections_df : pd.DataFrame
95
+ DataFrame con las detecciones.
96
+ output_dir : str
97
+ Directorio donde se guardarán las miniaturas.
98
+ thumb_size : int
99
+ Tamaño (en píxeles) de cada miniatura cuadrada.
100
+ """
101
+ mkdir(output_dir)
102
+ img = Image.open(image_path)
103
+ img_cpy = img.copy()
104
+
105
+ sp_score = list(detections_df[["species", "scores"]].to_records(index=False))
106
+ pts = list(detections_df[["y", "x"]].to_records(index=False))
107
+
108
+ for i, ((y, x), (sp, score)) in enumerate(zip(pts, sp_score)):
109
+ off = thumb_size // 2
110
+ coords = (x - off, y - off, x + off, y + off)
111
+
112
+ # Recortar miniatura
113
+ thumbnail = img_cpy.crop(coords)
114
+
115
+ # Dibujar texto con especie y score
116
+ score = round(score * 100, 1)
117
+ thumbnail = draw_text(
118
+ thumbnail,
119
+ f"{sp} | {score}%",
120
+ position=(10, 5),
121
+ font_size=int(0.08 * thumb_size),
122
+ )
123
+
124
+ filename = os.path.basename(image_path)[:-4] + f"_{i}.JPG"
125
+ thumbnail.save(os.path.join(output_dir, filename))
126
+
127
+
128
+ def save_detections(
129
+ detections_df: pd.DataFrame,
130
+ output_dir: str,
131
+ logger=None
132
+ ) -> str:
133
+ """
134
+ Guarda las detecciones en formato CSV dentro del directorio de salida.
135
+
136
+ Parámetros
137
+ ----------
138
+ detections_df : pd.DataFrame
139
+ DataFrame con las detecciones.
140
+ output_dir : str
141
+ Directorio de salida.
142
+ logger : logging.Logger, opcional
143
+ Logger para registrar el proceso.
144
+
145
+ Retorna
146
+ -------
147
+ str
148
+ Ruta del archivo CSV guardado.
149
+ """
150
+ mkdir(output_dir)
151
+ csv_path = os.path.join(output_dir, "detections.csv")
152
+ save_csv(detections_df, csv_path, logger)
153
+ return csv_path
inference/preprocessing.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import albumentations as A
5
+ from torch.utils.data import DataLoader
6
+ from animaloc.datasets import CSVDataset
7
+ from animaloc.data.transforms import DownSample
8
+ from inference.utils_io import mkdir, get_temp_image_path
9
+
10
+
11
+ def build_normalize_transform(mean: list, std: list) -> A.Normalize:
12
+ """
13
+ Construye una transformación de normalización idéntica
14
+ a la utilizada durante el entrenamiento.
15
+ """
16
+ return A.Normalize(mean=mean, std=std, p=1.0)
17
+
18
+
19
+ def build_end_transforms(down_ratio: int = 2):
20
+ """
21
+ Construye el conjunto de transformaciones finales utilizadas
22
+ durante la inferencia.
23
+ """
24
+ return [DownSample(down_ratio=down_ratio, anno_type="point")]
25
+
26
+
27
+ def create_single_image_dataset(
28
+ image_pil,
29
+ mean: list,
30
+ std: list,
31
+ down_ratio: int = 2
32
+ ):
33
+ """
34
+ Crea un CSVDataset temporal y su DataLoader a partir de una única imagen PIL.
35
+ Guarda la imagen temporalmente en disco (resources/uploads) para la API de HerdNet.
36
+
37
+ Retorna
38
+ -------
39
+ dataset : CSVDataset
40
+ Dataset temporal con una sola imagen.
41
+ dataloader : DataLoader
42
+ Cargador de datos correspondiente.
43
+ temp_path : str
44
+ Ruta absoluta de la imagen guardada temporalmente.
45
+ """
46
+ # Crear directorio y archivo temporal
47
+ upload_dir = "resources/uploads"
48
+ mkdir(upload_dir)
49
+ temp_path = get_temp_image_path(upload_dir)
50
+ image_pil.save(temp_path, format="JPEG")
51
+
52
+ # Construir DataFrame para CSVDataset
53
+ df = pd.DataFrame({
54
+ "images": [os.path.basename(temp_path)],
55
+ "x": [0],
56
+ "y": [0],
57
+ "labels": [1],
58
+ })
59
+
60
+ # Normalización Albumentations
61
+ normalize = A.Normalize(mean=mean, std=std, p=1.0)
62
+ end_transforms = [DownSample(down_ratio=down_ratio, anno_type="point")]
63
+
64
+ # Crear dataset y dataloader
65
+ dataset = CSVDataset(
66
+ csv_file=df,
67
+ root_dir=os.path.dirname(temp_path),
68
+ albu_transforms=[normalize],
69
+ end_transforms=end_transforms,
70
+ )
71
+
72
+ dataloader = DataLoader(dataset, batch_size=1, shuffle=False)
73
+
74
+ return dataset, dataloader, temp_path
inference/utils_io.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import yaml
3
+ import logging
4
+ from datetime import datetime
5
+
6
+ # ===============================================================
7
+ # Utility Functions for I/O, Config, and Logging
8
+ # ===============================================================
9
+
10
+ def mkdir(path: str) -> None:
11
+ """
12
+ Creates a directory if it doesn't exist.
13
+ """
14
+ os.makedirs(path, exist_ok=True)
15
+
16
+
17
+ def load_yaml_config(path: str) -> dict:
18
+ """
19
+ Loads a YAML configuration file and returns it as a dictionary.
20
+ """
21
+ if not os.path.exists(path):
22
+ raise FileNotFoundError(f"[CONFIG] File not found: {path}")
23
+
24
+ with open(path, "r", encoding="utf-8") as file:
25
+ config = yaml.safe_load(file)
26
+
27
+ return config
28
+
29
+
30
+ def get_timestamp() -> str:
31
+ """
32
+ Returns a timestamp string for naming logs and output folders.
33
+ """
34
+ return datetime.now().strftime("%Y%m%d_%H%M%S")
35
+
36
+
37
+ def get_timestamp_dir(base_dir: str) -> str:
38
+ """
39
+ Creates a timestamped subdirectory inside the given base directory.
40
+ Example: resources/outputs/infer_20251106_233000/
41
+ """
42
+ timestamp = get_timestamp()
43
+ new_dir = os.path.join(base_dir, f"infer_{timestamp}")
44
+ mkdir(new_dir)
45
+ return new_dir
46
+
47
+
48
+ def init_logger(log_dir: str, name: str = "herdnet_infer") -> logging.Logger:
49
+ """
50
+ Initializes a logger that writes both to console and to a file.
51
+ """
52
+ mkdir(log_dir)
53
+ timestamp = get_timestamp()
54
+ log_path = os.path.join(log_dir, f"{name}_{timestamp}.log")
55
+
56
+ logger = logging.getLogger(name)
57
+ logger.setLevel(logging.INFO)
58
+ logger.propagate = False
59
+
60
+ # Avoid duplicate handlers if reloaded
61
+ if not logger.handlers:
62
+ fmt = logging.Formatter("[%(asctime)s] %(levelname)s - %(message)s")
63
+
64
+ # File handler
65
+ fh = logging.FileHandler(log_path, encoding="utf-8")
66
+ fh.setFormatter(fmt)
67
+ logger.addHandler(fh)
68
+
69
+ # Console handler
70
+ ch = logging.StreamHandler()
71
+ ch.setFormatter(fmt)
72
+ logger.addHandler(ch)
73
+
74
+ logger.info(f"[LOGGER] Initialized → {log_path}")
75
+ return logger
76
+
77
+
78
+ def save_csv(df, path: str, logger: logging.Logger = None) -> None:
79
+ """
80
+ Saves a DataFrame as CSV, logging the event.
81
+ """
82
+ df.to_csv(path, index=False)
83
+ if logger:
84
+ logger.info(f"[OUTPUT] Saved CSV → {path}")
85
+ else:
86
+ print(f"[OUTPUT] Saved CSV → {path}")
87
+
88
+
89
+ def clean_uploads(upload_dir: str, logger: logging.Logger = None) -> None:
90
+ """
91
+ Cleans temporary uploaded files in the uploads directory.
92
+ """
93
+ if not os.path.exists(upload_dir):
94
+ return
95
+
96
+ files = [f for f in os.listdir(upload_dir) if os.path.isfile(os.path.join(upload_dir, f))]
97
+ for f in files:
98
+ try:
99
+ os.remove(os.path.join(upload_dir, f))
100
+ except Exception as e:
101
+ if logger:
102
+ logger.warning(f"[CLEANUP] Could not remove {f}: {e}")
103
+
104
+ if logger:
105
+ logger.info(f"[CLEANUP] Cleared {len(files)} files from {upload_dir}")
106
+
107
+
108
+ def get_temp_image_path(upload_dir: str, filename: str = None) -> str:
109
+ """
110
+ Generates a unique temporary image path inside the uploads directory.
111
+ """
112
+ mkdir(upload_dir)
113
+ if filename is None:
114
+ filename = f"tmp_{get_timestamp()}.jpg"
115
+ return os.path.join(upload_dir, filename)
requirements.txt ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==24.1.0
2
+ albucore==0.0.24
3
+ albumentations==2.0.8
4
+ git+https://github.com/Alexandre-Delplanque/HerdNet.git@7e25f482d875522c59c446dc0c78c8f6f2dd448d#egg=animaloc
5
+ annotated-doc==0.0.3
6
+ annotated-types==0.7.0
7
+ anyio==4.11.0
8
+ brotli==1.2.0
9
+ certifi==2025.10.5
10
+ charset-normalizer==3.4.4
11
+ click==8.3.0
12
+ colorama==0.4.6
13
+ contourpy==1.3.3
14
+ cycler==0.12.1
15
+ fastapi==0.121.0
16
+ ffmpy==0.6.4
17
+ filelock==3.20.0
18
+ fonttools==4.60.1
19
+ fsspec==2025.10.0
20
+ gitdb==4.0.12
21
+ GitPython==3.1.45
22
+ gradio==5.49.1
23
+ gradio_client==1.13.3
24
+ groovy==0.1.2
25
+ h11==0.16.0
26
+ hf-xet==1.2.0
27
+ httpcore==1.0.9
28
+ httpx==0.28.1
29
+ huggingface_hub==1.1.2
30
+ idna==3.11
31
+ Jinja2==3.1.6
32
+ joblib==1.5.2
33
+ kiwisolver==1.4.9
34
+ markdown-it-py==4.0.0
35
+ MarkupSafe==3.0.3
36
+ matplotlib==3.10.7
37
+ mdurl==0.1.2
38
+ mpmath==1.3.0
39
+ networkx==3.5
40
+ numpy==2.2.6
41
+ opencv-python-headless==4.12.0.88
42
+ orjson==3.11.4
43
+ packaging==25.0
44
+ pandas==2.3.3
45
+ pillow==11.3.0
46
+ platformdirs==4.5.0
47
+ protobuf==6.33.0
48
+ pydantic==2.11.10
49
+ pydantic_core==2.33.2
50
+ pydub==0.25.1
51
+ Pygments==2.19.2
52
+ pyparsing==3.2.5
53
+ python-dateutil==2.9.0.post0
54
+ python-multipart==0.0.20
55
+ pytz==2025.2
56
+ PyYAML==6.0.3
57
+ requests==2.32.5
58
+ rich==14.2.0
59
+ ruff==0.14.4
60
+ safehttpx==0.1.7
61
+ scikit-learn==1.7.2
62
+ scipy==1.16.3
63
+ semantic-version==2.10.0
64
+ sentry-sdk==2.43.0
65
+ setuptools==80.9.0
66
+ shellingham==1.5.4
67
+ simsimd==6.5.3
68
+ six==1.17.0
69
+ smmap==5.0.2
70
+ sniffio==1.3.1
71
+ starlette==0.49.3
72
+ stringzilla==4.2.3
73
+ sympy==1.14.0
74
+ threadpoolctl==3.6.0
75
+ tomlkit==0.13.3
76
+ torch==2.9.0
77
+ torchvision==0.24.0
78
+ tqdm==4.67.1
79
+ typer==0.20.0
80
+ typer-slim==0.20.0
81
+ typing-inspection==0.4.2
82
+ typing_extensions==4.15.0
83
+ tzdata==2025.2
84
+ urllib3==2.5.0
85
+ uvicorn==0.38.0
86
+ wandb==0.22.3
87
+ websockets==15.0.1
resources/configs/default.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===========================================
2
+ # Default Configuration for HerdNet Inference
3
+ # ===========================================
4
+
5
+ model:
6
+ name: "herdnet_fase1_best"
7
+ path: "resources/models/herdnet_best.pth"
8
+ device: "cuda" # "cuda" o "cpu"
9
+ patch_size: 512
10
+ overlap: 160
11
+ down_ratio: 2
12
+
13
+ paths:
14
+ uploads_dir: "resources/uploads"
15
+ outputs_dir: "resources/outputs"
16
+ logs_dir: "resources/logs"
17
+
18
+ inference:
19
+ save_plots: true
20
+ save_csv: true
21
+ save_thumbnails: false
22
+ verbose: true
resources/models/herdnet_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bad3367c0c0b26a5392b9f654b0eae25ef205c15334108603e7085ec54905c05
3
+ size 78678824