diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..1248ce40
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+checkpoints
+venv
+data
+session_data
+/ByteTrack
diff --git a/.ipynb b/.ipynb
new file mode 100644
index 00000000..d9e55c36
--- /dev/null
+++ b/.ipynb
@@ -0,0 +1,51 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "178e912b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Is CUDA available? True\n",
+ "GPU device count: 1\n",
+ "GPU device name: NVIDIA GeForce RTX 3060\n"
+ ]
+ }
+ ],
+ "source": [
+ "import torch\n",
+ "print(f\"Is CUDA available? {torch.cuda.is_available()}\")\n",
+ "\n",
+ "# If the above is True, these will give more details:\n",
+ "if torch.cuda.is_available():\n",
+ " print(f\"GPU device count: {torch.cuda.device_count()}\")\n",
+ " print(f\"GPU device name: {torch.cuda.get_device_name(0)}\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/README.md b/README.md
index c3bb68dd..b4374376 100644
--- a/README.md
+++ b/README.md
@@ -1,201 +1,24 @@
-
-
Depth Anything V2
+🚀 Instrucciones para Ejecutar el Proyecto
+1️⃣ Crear un entorno virtual (recomendado)
+bash
-[**Lihe Yang**](https://liheyoung.github.io/)
1 · [**Bingyi Kang**](https://bingykang.github.io/)
2† · [**Zilong Huang**](http://speedinghzl.github.io/)
2
-
-[**Zhen Zhao**](http://zhaozhen.me/) · [**Xiaogang Xu**](https://xiaogang00.github.io/) · [**Jiashi Feng**](https://sites.google.com/site/jshfeng/)
2 · [**Hengshuang Zhao**](https://hszhao.github.io/)
1*
+python -m venv .venv
-
1HKU
2TikTok
-
-†project lead *corresponding author
+Activa el entorno virtual:
-

-

-

-

-
+En Windows:
+.venv\Scripts\activate
-This work presents Depth Anything V2. It significantly outperforms [V1](https://github.com/LiheYoung/Depth-Anything) in fine-grained details and robustness. Compared with SD-based models, it enjoys faster inference speed, fewer parameters, and higher depth accuracy.
+2️⃣ Instalar dependencias
-
-
-
-## News
-- **2025-01-22:** [Video Depth Anything](https://videodepthanything.github.io) has been released. It generates consistent depth maps for super-long videos (e.g., over 5 minutes).
-- **2024-12-22:** [Prompt Depth Anything](https://promptda.github.io/) has been released. It supports 4K resolution metric depth estimation when low-res LiDAR is used to prompt the DA models.
-- **2024-07-06:** Depth Anything V2 is supported in [Transformers](https://github.com/huggingface/transformers/). See the [instructions](https://huggingface.co/docs/transformers/main/en/model_doc/depth_anything_v2) for convenient usage.
-- **2024-06-25:** Depth Anything is integrated into [Apple Core ML Models](https://developer.apple.com/machine-learning/models/). See the instructions ([V1](https://huggingface.co/apple/coreml-depth-anything-small), [V2](https://huggingface.co/apple/coreml-depth-anything-v2-small)) for usage.
-- **2024-06-22:** We release [smaller metric depth models](https://github.com/DepthAnything/Depth-Anything-V2/tree/main/metric_depth#pre-trained-models) based on Depth-Anything-V2-Small and Base.
-- **2024-06-20:** Our repository and project page are flagged by GitHub and removed from the public for 6 days. Sorry for the inconvenience.
-- **2024-06-14:** Paper, project page, code, models, demo, and benchmark are all released.
-
-
-## Pre-trained Models
-
-We provide **four models** of varying scales for robust relative depth estimation:
-
-| Model | Params | Checkpoint |
-|:-|-:|:-:|
-| Depth-Anything-V2-Small | 24.8M | [Download](https://huggingface.co/depth-anything/Depth-Anything-V2-Small/resolve/main/depth_anything_v2_vits.pth?download=true) |
-| Depth-Anything-V2-Base | 97.5M | [Download](https://huggingface.co/depth-anything/Depth-Anything-V2-Base/resolve/main/depth_anything_v2_vitb.pth?download=true) |
-| Depth-Anything-V2-Large | 335.3M | [Download](https://huggingface.co/depth-anything/Depth-Anything-V2-Large/resolve/main/depth_anything_v2_vitl.pth?download=true) |
-| Depth-Anything-V2-Giant | 1.3B | Coming soon |
-
-
-## Usage
-
-### Prepraration
-
-```bash
-git clone https://github.com/DepthAnything/Depth-Anything-V2
-cd Depth-Anything-V2
pip install -r requirements.txt
-```
-
-Download the checkpoints listed [here](#pre-trained-models) and put them under the `checkpoints` directory.
-
-### Use our models
-```python
-import cv2
-import torch
-
-from depth_anything_v2.dpt import DepthAnythingV2
-
-DEVICE = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
-
-model_configs = {
- 'vits': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]},
- 'vitb': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]},
- 'vitl': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]},
- 'vitg': {'encoder': 'vitg', 'features': 384, 'out_channels': [1536, 1536, 1536, 1536]}
-}
-
-encoder = 'vitl' # or 'vits', 'vitb', 'vitg'
-
-model = DepthAnythingV2(**model_configs[encoder])
-model.load_state_dict(torch.load(f'checkpoints/depth_anything_v2_{encoder}.pth', map_location='cpu'))
-model = model.to(DEVICE).eval()
-
-raw_img = cv2.imread('your/image/path')
-depth = model.infer_image(raw_img) # HxW raw depth map in numpy
-```
-
-If you do not want to clone this repository, you can also load our models through [Transformers](https://github.com/huggingface/transformers/). Below is a simple code snippet. Please refer to the [official page](https://huggingface.co/docs/transformers/main/en/model_doc/depth_anything_v2) for more details.
-
-- Note 1: Make sure you can connect to Hugging Face and have installed the latest Transformers.
-- Note 2: Due to the [upsampling difference](https://github.com/huggingface/transformers/pull/31522#issuecomment-2184123463) between OpenCV (we used) and Pillow (HF used), predictions may differ slightly. So you are more recommended to use our models through the way introduced above.
-```python
-from transformers import pipeline
-from PIL import Image
-
-pipe = pipeline(task="depth-estimation", model="depth-anything/Depth-Anything-V2-Small-hf")
-image = Image.open('your/image/path')
-depth = pipe(image)["depth"]
-```
-
-### Running script on *images*
-
-```bash
-python run.py \
- --encoder \
- --img-path --outdir \
- [--input-size ] [--pred-only] [--grayscale]
-```
-Options:
-- `--img-path`: You can either 1) point it to an image directory storing all interested images, 2) point it to a single image, or 3) point it to a text file storing all image paths.
-- `--input-size` (optional): By default, we use input size `518` for model inference. ***You can increase the size for even more fine-grained results.***
-- `--pred-only` (optional): Only save the predicted depth map, without raw image.
-- `--grayscale` (optional): Save the grayscale depth map, without applying color palette.
-
-For example:
-```bash
-python run.py --encoder vitl --img-path assets/examples --outdir depth_vis
-```
-
-### Running script on *videos*
-
-```bash
-python run_video.py \
- --encoder \
- --video-path assets/examples_video --outdir video_depth_vis \
- [--input-size ] [--pred-only] [--grayscale]
-```
-
-***Our larger model has better temporal consistency on videos.***
-
-### Gradio demo
-
-To use our gradio demo locally:
-
-```bash
-python app.py
-```
-
-You can also try our [online demo](https://huggingface.co/spaces/Depth-Anything/Depth-Anything-V2).
-
-***Note: Compared to V1, we have made a minor modification to the DINOv2-DPT architecture (originating from this [issue](https://github.com/LiheYoung/Depth-Anything/issues/81)).*** In V1, we *unintentionally* used features from the last four layers of DINOv2 for decoding. In V2, we use [intermediate features](https://github.com/DepthAnything/Depth-Anything-V2/blob/2cbc36a8ce2cec41d38ee51153f112e87c8e42d8/depth_anything_v2/dpt.py#L164-L169) instead. Although this modification did not improve details or accuracy, we decided to follow this common practice.
-
-
-## Fine-tuned to Metric Depth Estimation
-
-Please refer to [metric depth estimation](./metric_depth).
-
-
-## DA-2K Evaluation Benchmark
-
-Please refer to [DA-2K benchmark](./DA-2K.md).
-
-
-## Community Support
-
-**We sincerely appreciate all the community support for our Depth Anything series. Thank you a lot!**
-
-- Apple Core ML:
- - https://developer.apple.com/machine-learning/models
- - https://huggingface.co/apple/coreml-depth-anything-v2-small
- - https://huggingface.co/apple/coreml-depth-anything-small
-- Transformers:
- - https://huggingface.co/docs/transformers/main/en/model_doc/depth_anything_v2
- - https://huggingface.co/docs/transformers/main/en/model_doc/depth_anything
-- TensorRT:
- - https://github.com/spacewalk01/depth-anything-tensorrt
- - https://github.com/zhujiajian98/Depth-Anythingv2-TensorRT-python
-- ONNX: https://github.com/fabio-sim/Depth-Anything-ONNX
-- ComfyUI: https://github.com/kijai/ComfyUI-DepthAnythingV2
-- Transformers.js (real-time depth in web): https://huggingface.co/spaces/Xenova/webgpu-realtime-depth-estimation
-- Android:
- - https://github.com/shubham0204/Depth-Anything-Android
- - https://github.com/FeiGeChuanShu/ncnn-android-depth_anything
-
-
-## Acknowledgement
-
-We are sincerely grateful to the awesome Hugging Face team ([@Pedro Cuenca](https://huggingface.co/pcuenq), [@Niels Rogge](https://huggingface.co/nielsr), [@Merve Noyan](https://huggingface.co/merve), [@Amy Roberts](https://huggingface.co/amyeroberts), et al.) for their huge efforts in supporting our models in Transformers and Apple Core ML.
-
-We also thank the [DINOv2](https://github.com/facebookresearch/dinov2) team for contributing such impressive models to our community.
-
-
-## LICENSE
-
-Depth-Anything-V2-Small model is under the Apache-2.0 license. Depth-Anything-V2-Base/Large/Giant models are under the CC-BY-NC-4.0 license.
+3️⃣ Levantar el servidor backend (API) con Uvicorn
-## Citation
+uvicorn api:app --host 0.0.0.0 --port 8000 --reload
-If you find this project useful, please consider citing:
+🔹 Cambia api:app por el módulo y objeto correctos según tu estructura.
-```bibtex
-@article{depth_anything_v2,
- title={Depth Anything V2},
- author={Yang, Lihe and Kang, Bingyi and Huang, Zilong and Zhao, Zhen and Xu, Xiaogang and Feng, Jiashi and Zhao, Hengshuang},
- journal={arXiv:2406.09414},
- year={2024}
-}
+4️⃣ Iniciar la aplicación Streamlit
-@inproceedings{depth_anything_v1,
- title={Depth Anything: Unleashing the Power of Large-Scale Unlabeled Data},
- author={Yang, Lihe and Kang, Bingyi and Huang, Zilong and Xu, Xiaogang and Feng, Jiashi and Zhao, Hengshuang},
- booktitle={CVPR},
- year={2024}
-}
-```
+streamlit run dashboard.py --server.port 8501
\ No newline at end of file
diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc
new file mode 100644
index 00000000..e8150b70
Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ
diff --git a/__pycache__/streamlit_depth_client.cpython-312.pyc b/__pycache__/streamlit_depth_client.cpython-312.pyc
new file mode 100644
index 00000000..970b378e
Binary files /dev/null and b/__pycache__/streamlit_depth_client.cpython-312.pyc differ
diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc
new file mode 100644
index 00000000..5985017d
Binary files /dev/null and b/backend/__pycache__/main.cpython-312.pyc differ
diff --git a/backend/__pycache__/model.cpython-312.pyc b/backend/__pycache__/model.cpython-312.pyc
new file mode 100644
index 00000000..62dcb9cf
Binary files /dev/null and b/backend/__pycache__/model.cpython-312.pyc differ
diff --git a/backend/check_gpu.py b/backend/check_gpu.py
new file mode 100644
index 00000000..c6c0ff71
--- /dev/null
+++ b/backend/check_gpu.py
@@ -0,0 +1,9 @@
+# check_gpu.py
+import torch
+print("--- Running GPU Check ---")
+print(f"Is CUDA available? {torch.cuda.is_available()}")
+if torch.cuda.is_available():
+ print(f"Device name: {torch.cuda.get_device_name(0)}")
+else:
+ print("CUDA not found by this Python interpreter.")
+print("-------------------------")
\ No newline at end of file
diff --git a/depth_anything_v2/__pycache__/dinov2.cpython-312.pyc b/depth_anything_v2/__pycache__/dinov2.cpython-312.pyc
new file mode 100644
index 00000000..8bbc2a06
Binary files /dev/null and b/depth_anything_v2/__pycache__/dinov2.cpython-312.pyc differ
diff --git a/depth_anything_v2/__pycache__/dpt.cpython-312.pyc b/depth_anything_v2/__pycache__/dpt.cpython-312.pyc
new file mode 100644
index 00000000..650a1c1e
Binary files /dev/null and b/depth_anything_v2/__pycache__/dpt.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/__init__.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..804c6a24
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/__init__.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/attention.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/attention.cpython-312.pyc
new file mode 100644
index 00000000..36d52c67
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/attention.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/block.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/block.cpython-312.pyc
new file mode 100644
index 00000000..426da444
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/block.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/drop_path.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/drop_path.cpython-312.pyc
new file mode 100644
index 00000000..4a6e97e0
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/drop_path.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/layer_scale.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/layer_scale.cpython-312.pyc
new file mode 100644
index 00000000..9b8b53de
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/layer_scale.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/mlp.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/mlp.cpython-312.pyc
new file mode 100644
index 00000000..d93a3da9
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/mlp.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/patch_embed.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/patch_embed.cpython-312.pyc
new file mode 100644
index 00000000..83860490
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/patch_embed.cpython-312.pyc differ
diff --git a/depth_anything_v2/dinov2_layers/__pycache__/swiglu_ffn.cpython-312.pyc b/depth_anything_v2/dinov2_layers/__pycache__/swiglu_ffn.cpython-312.pyc
new file mode 100644
index 00000000..ecfec566
Binary files /dev/null and b/depth_anything_v2/dinov2_layers/__pycache__/swiglu_ffn.cpython-312.pyc differ
diff --git a/depth_anything_v2/util/__pycache__/blocks.cpython-312.pyc b/depth_anything_v2/util/__pycache__/blocks.cpython-312.pyc
new file mode 100644
index 00000000..0507c268
Binary files /dev/null and b/depth_anything_v2/util/__pycache__/blocks.cpython-312.pyc differ
diff --git a/depth_anything_v2/util/__pycache__/transform.cpython-312.pyc b/depth_anything_v2/util/__pycache__/transform.cpython-312.pyc
new file mode 100644
index 00000000..da54abcb
Binary files /dev/null and b/depth_anything_v2/util/__pycache__/transform.cpython-312.pyc differ
diff --git a/depth_backend/__pycache__/image_store_depth.cpython-312.pyc b/depth_backend/__pycache__/image_store_depth.cpython-312.pyc
new file mode 100644
index 00000000..034d763d
Binary files /dev/null and b/depth_backend/__pycache__/image_store_depth.cpython-312.pyc differ
diff --git a/depth_backend/__pycache__/main.cpython-312.pyc b/depth_backend/__pycache__/main.cpython-312.pyc
new file mode 100644
index 00000000..77d3b2a7
Binary files /dev/null and b/depth_backend/__pycache__/main.cpython-312.pyc differ
diff --git a/depth_backend/__pycache__/mjpeg_generator.cpython-312.pyc b/depth_backend/__pycache__/mjpeg_generator.cpython-312.pyc
new file mode 100644
index 00000000..57fa9d1f
Binary files /dev/null and b/depth_backend/__pycache__/mjpeg_generator.cpython-312.pyc differ
diff --git a/depth_backend/image_store_depth.py b/depth_backend/image_store_depth.py
new file mode 100644
index 00000000..9cc428a1
--- /dev/null
+++ b/depth_backend/image_store_depth.py
@@ -0,0 +1,191 @@
+import os
+import time
+import threading
+import cv2
+import numpy as np
+import pandas as pd
+import torch
+import sqlite3
+from depth_anything_v2.dpt import DepthAnythingV2
+from contextlib import contextmanager
+
+class DepthStore:
+ def __init__(self, encoder="vitl", min_size=518, bins=50):
+ self._lock = threading.Lock()
+ self._frame_raw = self._frame_depth = None
+ self._last_update = 0
+ self.recording = False
+ self.raw_path = self.proc_path = None
+ self.frame_count = 0
+ self.hist_bins = bins
+ self.hist_edges = np.linspace(0, 1, bins + 1)
+ self.hist_counts = np.zeros(bins, dtype=np.int64)
+ self.encoder, self.min_size = encoder, min_size
+ self.model, self.device = self._load_model()
+ self._init_db()
+
+ def _init_db(self):
+ self.db_conn = sqlite3.connect(':memory:', check_same_thread=False)
+ self.db_conn.execute("""
+ CREATE TABLE IF NOT EXISTS depth_metrics (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ timestamp REAL,
+ min REAL,
+ max REAL,
+ mean REAL,
+ std REAL,
+ frame_count INTEGER
+ )
+ """)
+ self.db_conn.commit()
+
+ @contextmanager
+ def _db_cursor(self):
+ cursor = self.db_conn.cursor()
+ try:
+ yield cursor
+ self.db_conn.commit()
+ finally:
+ cursor.close()
+
+ def _load_model(self):
+ device = "cuda" if torch.cuda.is_available() else "cpu"
+ cfg = {
+ "encoder": self.encoder,
+ "features": 256,
+ "out_channels": [256, 512, 1024, 1024]
+ }
+ m = DepthAnythingV2(**cfg)
+ m.load_state_dict(torch.load(
+ f"checkpoints/depth_anything_v2_{self.encoder}.pth",
+ map_location=device))
+ m.to(device).eval()
+ return m, device
+
+ def _depth_inference(self, rgb):
+ return self.model.infer_image(
+ cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR),
+ self.min_size
+ )
+
+ def _process_frame(self, frame_bgr):
+ # Convert to RGB for processing
+ frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
+
+ # Depth processing
+ with torch.no_grad():
+ depth = self._depth_inference(frame_rgb)
+
+ dmin, dmax = float(depth.min()), float(depth.max())
+ norm = ((depth - dmin) / (dmax - dmin + 1e-6)).clip(0, 1)
+ vis = cv2.applyColorMap((norm * 255).astype("uint8"), cv2.COLORMAP_MAGMA)
+
+ # Histogram
+ idx, _ = np.histogram(norm.flatten(), self.hist_edges)
+
+ # Stats
+ stats = (time.time(), dmin, dmax, float(depth.mean()), float(depth.std()))
+
+ return frame_rgb, vis, stats, idx
+
+ def set_frame(self, img_bytes: bytes):
+ try:
+ frame_bgr = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR)
+ if frame_bgr is None:
+ raise ValueError("Empty frame")
+
+ # Process frame
+ frame_rgb, vis, stats, hist_idx = self._process_frame(frame_bgr)
+
+ # Update state
+ with self._lock:
+ # Encode raw frame
+ _, enc_raw = cv2.imencode(".jpg", frame_bgr)
+ self._frame_raw = enc_raw.tobytes()
+
+ # Encode depth frame
+ _, enc_depth = cv2.imencode(".jpg", vis)
+ self._frame_depth = enc_depth.tobytes()
+
+ # Update histogram
+ self.hist_counts += hist_idx
+ self._last_update = time.time()
+
+ if self.recording:
+ # Save to database
+ with self._db_cursor() as cur:
+ cur.execute(
+ "INSERT INTO depth_metrics (timestamp, min, max, mean, std, frame_count) VALUES (?, ?, ?, ?, ?, ?)",
+ (*stats, self.frame_count)
+ )
+
+ # Save images
+ cv2.imwrite(
+ os.path.join(self.raw_path, f"frame_{self.frame_count:06d}.jpg"),
+ frame_bgr
+ )
+ cv2.imwrite(
+ os.path.join(self.proc_path, f"depth_{self.frame_count:06d}.png"),
+ (norm * 255).astype("uint8")
+ )
+ self.frame_count += 1
+
+ except Exception as e:
+ print(f"Error processing frame: {e}")
+
+ def get_frame_raw(self):
+ with self._lock:
+ return self._frame_raw if (time.time() - self._last_update) < 5 else None
+
+ def get_frame_depth(self):
+ with self._lock:
+ return self._frame_depth if (time.time() - self._last_update) < 5 else None
+
+ def last_stats(self):
+ with self._db_cursor() as cur:
+ row = cur.execute(
+ "SELECT min, max, mean, std FROM depth_metrics ORDER BY timestamp DESC LIMIT 1"
+ ).fetchone()
+ return row if row else (0, 0, 0, 0)
+
+ def stats_timeseries(self):
+ with self._db_cursor() as cur:
+ return cur.execute(
+ "SELECT timestamp, min, max, mean, std FROM depth_metrics ORDER BY timestamp"
+ ).fetchall()
+
+ def hist(self):
+ with self._lock:
+ return self.hist_edges.tolist(), self.hist_counts.tolist()
+
+ def start(self):
+ ts = time.strftime("%Y%m%d_%H%M%S")
+ self.raw_path = os.path.join("data/raw", ts)
+ self.proc_path = os.path.join("data/depth", ts)
+ os.makedirs(self.raw_path, exist_ok=True)
+ os.makedirs(self.proc_path, exist_ok=True)
+
+ with self._lock:
+ self.recording = True
+ self.frame_count = 0
+ self.hist_counts[:] = 0
+
+ def stop(self):
+ with self._lock:
+ self.recording = False
+
+ def is_recording(self):
+ with self._lock:
+ return self.recording
+
+ def stats_to_csv(self):
+ with self._db_cursor() as cur:
+ df = pd.DataFrame(
+ cur.execute("SELECT * FROM depth_metrics").fetchall(),
+ columns=["id", "timestamp", "min", "max", "mean", "std", "frame_count"]
+ )
+ path = os.path.join(self.proc_path or ".", "metrics.csv")
+ df.to_csv(path, index=False)
+ return path
+
+depth_store = DepthStore()
\ No newline at end of file
diff --git a/depth_backend/main.py b/depth_backend/main.py
new file mode 100644
index 00000000..a56954cf
--- /dev/null
+++ b/depth_backend/main.py
@@ -0,0 +1,127 @@
+import numpy as np
+from fastapi import FastAPI, Request
+from fastapi.responses import JSONResponse, FileResponse, StreamingResponse
+from fastapi.middleware.cors import CORSMiddleware
+from contextlib import asynccontextmanager
+import time
+
+from .image_store_depth import depth_store
+from .mjpeg_generator import mjpeg_stream
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ # Startup
+ print("Starting DepthVision Backend")
+ yield
+ # Shutdown
+ print("Shutting down DepthVision Backend")
+
+app = FastAPI(
+ title="DepthVision Backend",
+ lifespan=lifespan
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+ expose_headers=["*"]
+)
+
+def _to_float(x):
+ if isinstance(x, np.ndarray):
+ return float(x.item())
+ if isinstance(x, (np.floating,)):
+ return float(x)
+ return x
+
+@app.post("/upload")
+async def upload(req: Request):
+ try:
+ start_time = time.time()
+ depth_store.set_frame(await req.body())
+ processing_time = (time.time() - start_time) * 1000
+ return {
+ "status": "ok",
+ "processing_time_ms": round(processing_time, 2)
+ }
+ except Exception as e:
+ return JSONResponse(
+ {"error": str(e)},
+ status_code=500
+ )
+
+@app.get("/mjpeg/raw")
+async def mjpeg_raw():
+ return StreamingResponse(
+ mjpeg_stream("raw"),
+ media_type="multipart/x-mixed-replace; boundary=frame",
+ headers={"Cache-Control": "no-cache, no-store, must-revalidate"}
+ )
+
+@app.get("/mjpeg/depth")
+async def mjpeg_depth():
+ return StreamingResponse(
+ mjpeg_stream("depth"),
+ media_type="multipart/x-mixed-replace; boundary=frame",
+ headers={"Cache-Control": "no-cache, no-store, must-revalidate"}
+ )
+
+@app.post("/record/start")
+def record_start():
+ depth_store.start()
+ return {"status": "started"}
+
+@app.post("/record/stop")
+def record_stop():
+ depth_store.stop()
+ return {"status": "stopped"}
+
+@app.get("/record/status")
+def record_status():
+ return {"recording": depth_store.is_recording()}
+
+@app.get("/metrics/latest")
+def metrics_latest():
+ mn, mx, me, sd = depth_store.last_stats()
+ return {
+ "min": _to_float(mn),
+ "max": _to_float(mx),
+ "mean": _to_float(me),
+ "std": _to_float(sd)
+ }
+
+@app.get("/metrics/timeseries")
+def metrics_timeseries():
+ data = depth_store.stats_timeseries()
+ if not data:
+ return {"t": [], "min": [], "max": [], "mean": [], "std": []}
+
+ t, mn, mx, me, sd = zip(*data)
+ return {
+ "t": [float(ts) for ts in t],
+ "min": [_to_float(v) for v in mn],
+ "max": [_to_float(v) for v in mx],
+ "mean": [_to_float(v) for v in me],
+ "std": [_to_float(v) for v in sd]
+ }
+
+@app.get("/metrics/hist")
+def metrics_hist():
+ edges, counts = depth_store.hist()
+ return {"edges": edges, "counts": counts}
+
+@app.get("/metrics/csv")
+def metrics_csv():
+ path = depth_store.stats_to_csv()
+ return FileResponse(
+ path,
+ media_type="text/csv",
+ filename="depth_metrics.csv",
+ headers={"Content-Disposition": "attachment; filename=depth_metrics.csv"}
+ )
+
+@app.get("/health")
+def health_check():
+ return {"status": "healthy", "timestamp": time.time()}
\ No newline at end of file
diff --git a/depth_backend/mjpeg_generator.py b/depth_backend/mjpeg_generator.py
new file mode 100644
index 00000000..bf1f8612
--- /dev/null
+++ b/depth_backend/mjpeg_generator.py
@@ -0,0 +1,42 @@
+import time
+from fastapi.responses import StreamingResponse
+from .image_store_depth import depth_store
+
+def mjpeg_stream(kind: str = "depth"):
+ """Generate MJPEG stream with optimized frame rate and quality."""
+ getter = depth_store.get_frame_depth if kind == "depth" else depth_store.get_frame_raw
+
+ async def generate():
+ last_frame = None
+ last_sent = 0
+ min_interval = 1/30 # 30 FPS max
+
+ while True:
+ current_time = time.time()
+ frame = getter()
+
+ if frame and (current_time - last_sent) >= min_interval:
+ if frame != last_frame: # Only send if frame changed
+ yield (
+ b"--frame\r\n"
+ b"Content-Type: image/jpeg\r\n"
+ b"Content-Length: " + str(len(frame)).encode() + b"\r\n"
+ b"\r\n" + frame + b"\r\n"
+ )
+ last_sent = current_time
+ last_frame = frame
+ else:
+ time.sleep(0.001) # Small sleep to prevent busy waiting
+ else:
+ time.sleep(0.001)
+
+ return StreamingResponse(
+ generate(),
+ media_type="multipart/x-mixed-replace; boundary=frame",
+ headers={
+ "Cache-Control": "no-cache, no-store, must-revalidate",
+ "Pragma": "no-cache",
+ "Expires": "0",
+ "X-Accel-Buffering": "no" # Disable buffering for nginx
+ }
+ )
\ No newline at end of file
diff --git a/depth_vis/demo01.png b/depth_vis/demo01.png
new file mode 100644
index 00000000..838162e5
Binary files /dev/null and b/depth_vis/demo01.png differ
diff --git a/depth_vis/demo02.png b/depth_vis/demo02.png
new file mode 100644
index 00000000..bec1e8e2
Binary files /dev/null and b/depth_vis/demo02.png differ
diff --git a/depth_vis/demo03.png b/depth_vis/demo03.png
new file mode 100644
index 00000000..729ecf3b
Binary files /dev/null and b/depth_vis/demo03.png differ
diff --git a/depth_vis/demo04.png b/depth_vis/demo04.png
new file mode 100644
index 00000000..ad65ee2c
Binary files /dev/null and b/depth_vis/demo04.png differ
diff --git a/depth_vis/demo05.png b/depth_vis/demo05.png
new file mode 100644
index 00000000..ed38faf3
Binary files /dev/null and b/depth_vis/demo05.png differ
diff --git a/depth_vis/demo06.png b/depth_vis/demo06.png
new file mode 100644
index 00000000..5217c535
Binary files /dev/null and b/depth_vis/demo06.png differ
diff --git a/depth_vis/demo07.png b/depth_vis/demo07.png
new file mode 100644
index 00000000..9d69ae4a
Binary files /dev/null and b/depth_vis/demo07.png differ
diff --git a/depth_vis/demo08.png b/depth_vis/demo08.png
new file mode 100644
index 00000000..a6306b76
Binary files /dev/null and b/depth_vis/demo08.png differ
diff --git a/depth_vis/demo09.png b/depth_vis/demo09.png
new file mode 100644
index 00000000..4abc1d8a
Binary files /dev/null and b/depth_vis/demo09.png differ
diff --git a/depth_vis/demo10.png b/depth_vis/demo10.png
new file mode 100644
index 00000000..3aafe156
Binary files /dev/null and b/depth_vis/demo10.png differ
diff --git a/depth_vis/demo11.png b/depth_vis/demo11.png
new file mode 100644
index 00000000..842f0346
Binary files /dev/null and b/depth_vis/demo11.png differ
diff --git a/depth_vis/demo12.png b/depth_vis/demo12.png
new file mode 100644
index 00000000..43133c50
Binary files /dev/null and b/depth_vis/demo12.png differ
diff --git a/depth_vis/demo13.png b/depth_vis/demo13.png
new file mode 100644
index 00000000..0dcd9449
Binary files /dev/null and b/depth_vis/demo13.png differ
diff --git a/depth_vis/demo14.png b/depth_vis/demo14.png
new file mode 100644
index 00000000..42cc56b1
Binary files /dev/null and b/depth_vis/demo14.png differ
diff --git a/depth_vis/demo15.png b/depth_vis/demo15.png
new file mode 100644
index 00000000..8517f234
Binary files /dev/null and b/depth_vis/demo15.png differ
diff --git a/depth_vis/demo16.png b/depth_vis/demo16.png
new file mode 100644
index 00000000..6deef755
Binary files /dev/null and b/depth_vis/demo16.png differ
diff --git a/depth_vis/demo17.png b/depth_vis/demo17.png
new file mode 100644
index 00000000..494c3cd3
Binary files /dev/null and b/depth_vis/demo17.png differ
diff --git a/depth_vis/demo18.png b/depth_vis/demo18.png
new file mode 100644
index 00000000..9a7d9a67
Binary files /dev/null and b/depth_vis/demo18.png differ
diff --git a/depth_vis/demo19.png b/depth_vis/demo19.png
new file mode 100644
index 00000000..c8692a1c
Binary files /dev/null and b/depth_vis/demo19.png differ
diff --git a/depth_vis/demo20.png b/depth_vis/demo20.png
new file mode 100644
index 00000000..e384e5a4
Binary files /dev/null and b/depth_vis/demo20.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..9a3e8d6e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,963 @@
+
+
+
+
+
+ DepthVision Static Processor
+
+
+
+ DepthVision Static Processor
+
+
+
+
+
+
+
+
+
Ready to select video file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Frame 0/0
+
+
+
+
+
+
Original Frame (Click to Select Points)
+
+
+
Depth Map
+
+
+
+
+
+
Volume Analysis Results
+
+
+
+
+
Point Depth Evolution
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logo.png b/logo.png
new file mode 100644
index 00000000..398346a7
Binary files /dev/null and b/logo.png differ
diff --git a/main.py b/main.py
new file mode 100644
index 00000000..708f9381
--- /dev/null
+++ b/main.py
@@ -0,0 +1,347 @@
+from fastapi import FastAPI, UploadFile, File, HTTPException, Form, Request
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
+from pydantic import BaseModel
+from io import BytesIO
+from PIL import Image
+import torch
+import numpy as np
+import cv2
+import os
+import base64
+from pathlib import Path
+from typing import List
+from datetime import datetime
+from fastapi.staticfiles import StaticFiles
+
+# Model import
+try:
+ from depth_anything_v2.dpt import DepthAnythingV2
+except ImportError:
+ raise ImportError("Could not import DepthAnythingV2 - check your model implementation")
+
+app = FastAPI()
+app.mount("/session_data", StaticFiles(directory="session_data"), name="session_data")
+
+# Configure CORS
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Global variables
+model = None
+device = None
+DATA_DIR = Path("session_data")
+DATA_DIR.mkdir(exist_ok=True)
+
+# Recording state
+RECORDING_STATE = {
+ "recording": False,
+ "frame_count": 0,
+ "session_id": "",
+}
+
+# Models
+class Point(BaseModel):
+ x: int
+ y: int
+
+class PointAnalysisOptions(BaseModel):
+ points: List[Point]
+
+class AnalysisOptions(BaseModel):
+ noise_threshold: float = 0.01
+
+# Middleware for additional CORS headers
+@app.middleware("http")
+async def add_cache_headers(request: Request, call_next):
+ response = await call_next(request)
+ if request.url.path.startswith("/session_data"):
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "0"
+ return response
+
+# Endpoints
+@app.post("/upload")
+async def upload_raw_frame(request: Request):
+ try:
+ img_bytes = await request.body()
+
+ if not RECORDING_STATE["recording"]:
+ return JSONResponse(content={"message": "Not recording. Frame ignored."}, status_code=200)
+
+ session_id = RECORDING_STATE["session_id"]
+ frame_idx = RECORDING_STATE["frame_count"]
+ session_dir = DATA_DIR / session_id / "recorded_frames"
+ os.makedirs(session_dir, exist_ok=True)
+
+ frame_path = session_dir / f"frame_{frame_idx:05d}.jpg"
+
+ # Convert and save as JPEG
+ img = Image.open(BytesIO(img_bytes))
+ img = img.convert('RGB')
+ img.save(frame_path, 'JPEG', quality=95)
+
+ RECORDING_STATE["frame_count"] += 1
+ return JSONResponse(content={"message": f"Frame {frame_idx} saved."}, status_code=200)
+
+ except Exception as e:
+ return JSONResponse(content={"error": str(e)}, status_code=500)
+
+@app.post("/start-recording")
+async def start_recording(session_id: str = Form(...)):
+ RECORDING_STATE.update(recording=True, frame_count=0, session_id=session_id)
+
+ session_dir = DATA_DIR / session_id / "recorded_frames"
+ os.makedirs(session_dir, exist_ok=True)
+ for f in session_dir.glob("frame_*.jpg"):
+ f.unlink(missing_ok=True)
+
+ # 👉 devuelve session_id
+ return {
+ "status": "success",
+ "message": f"Recording started for session {session_id}",
+ "session_id": session_id # ← ESTA LÍNEA ES IMPRESCINDIBLE
+ }
+@app.post("/stop-recording/{session_id}")
+async def stop_recording(session_id: str):
+ try:
+ if not RECORDING_STATE["recording"]:
+ return JSONResponse(
+ content={"status": "error", "message": "No hay grabación activa"},
+ status_code=400
+ )
+
+ if RECORDING_STATE["session_id"] != session_id:
+ return JSONResponse(
+ content={"status": "error", "message": "Session ID no coincide"},
+ status_code=400
+ )
+
+ RECORDING_STATE["recording"] = False
+ frame_count = RECORDING_STATE["frame_count"]
+
+ return JSONResponse(
+ content={
+ "status": "success",
+ "message": "Grabación finalizada",
+ "session_id": session_id,
+ "frame_count": frame_count,
+ "frame_base_url": f"/session_data/{session_id}/recorded_frames/frame_"
+ },
+ status_code=200
+ )
+
+ except Exception as e:
+ return JSONResponse(
+ content={"status": "error", "message": str(e)},
+ status_code=500
+ )
+
+@app.get("/session_data/{session_id}/recorded_frames/frame_{frame_number:05d}.jpg")
+async def get_frame(session_id: str, frame_number: int):
+ frame_path = DATA_DIR / session_id / "recorded_frames" / f"frame_{frame_number:05d}.jpg"
+
+ if not frame_path.exists():
+ raise HTTPException(
+ status_code=404,
+ detail=f"Frame not found at {frame_path}"
+ )
+
+ return FileResponse(
+ frame_path,
+ media_type="image/jpeg",
+ headers={"Cache-Control": "no-cache"}
+ )
+
+@app.get("/debug-frames/{session_id}")
+async def debug_frames(session_id: str):
+ frames_dir = DATA_DIR / session_id / "recorded_frames"
+
+ if not frames_dir.exists():
+ return {"error": f"Directory not found: {frames_dir}"}
+
+ frame_files = sorted(frames_dir.glob("frame_*.jpg"))
+
+ sample_frames = []
+ for i, frame_path in enumerate(frame_files[:3]):
+ sample_frames.append({
+ "number": i,
+ "name": frame_path.name,
+ "exists": frame_path.exists(),
+ "size": frame_path.stat().st_size if frame_path.exists() else 0,
+ "path": str(frame_path)
+ })
+
+ return {
+ "session_id": session_id,
+ "total_frames": len(frame_files),
+ "sample_frames": sample_frames,
+ "directory": str(frames_dir)
+ }
+
+@app.get("/get-frames/{session_id}")
+async def get_frames_info(session_id: str):
+ frames_dir = DATA_DIR / session_id / "recorded_frames"
+ if not frames_dir.exists():
+ raise HTTPException(status_code=404, detail="Session directory not found")
+
+ frame_files = sorted(frames_dir.glob("frame_*.jpg"))
+ return {
+ "frame_count": len(frame_files),
+ "frames": [f.name for f in frame_files]
+ }
+
+def load_model(encoder="vitl"):
+ global model, device
+
+ if torch.cuda.is_available():
+ device = "cuda"
+ else:
+ raise RuntimeError("GPU is required for this application, but CUDA is not available.")
+
+ print(f"Using device: {device}")
+
+ cfg = {"encoder": encoder, "features": 256, "out_channels": [256, 512, 1024, 1024]}
+ model = DepthAnythingV2(**cfg)
+
+ checkpoint_path = f"checkpoints/depth_anything_v2_{encoder}.pth"
+ if not os.path.exists(checkpoint_path):
+ raise FileNotFoundError(f"Model checkpoint not found at {checkpoint_path}")
+
+ print(f"Loading weights from {checkpoint_path}")
+ model.load_state_dict(torch.load(checkpoint_path, map_location=device))
+ model.to(device).eval()
+ print("Model loaded successfully")
+
+@app.on_event("startup")
+async def startup_event():
+ try:
+ load_model()
+ print("\n--- Directory Verification ---")
+ print(f"Data directory: {DATA_DIR.resolve()}")
+ print(f"Directory exists: {DATA_DIR.exists()}")
+ print(f"Write permissions: {os.access(DATA_DIR, os.W_OK)}")
+ except Exception as e:
+ print(f"Error loading model: {e}")
+ raise
+
+@app.post("/predict")
+async def predict_depth(
+ file: UploadFile = File(...),
+ session_id: str = Form(...),
+ frame_index: int = Form(...)
+):
+ try:
+ session_dir = DATA_DIR / session_id
+ session_dir.mkdir(exist_ok=True)
+
+ contents = await file.read()
+ image = Image.open(BytesIO(contents)).convert("RGB")
+ image_np = np.array(image)
+ image_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
+
+ with torch.no_grad():
+ depth = model.infer_image(image_bgr)
+
+ raw_depth_np = depth
+ save_path = session_dir / f"frame_{frame_index:05d}.npy"
+ np.save(save_path, raw_depth_np)
+
+ dmin, dmax = float(depth.min()), float(depth.max())
+ norm_depth = ((depth - dmin) / (dmax - dmin + 1e-6)).clip(0, 1)
+
+ return {
+ "message": f"Frame {frame_index} processed and saved to {save_path}",
+ "depth": norm_depth.tolist(),
+ "min": dmin, "max": dmax, "mean": float(depth.mean()), "std": float(depth.std())
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+@app.post("/analyze/{session_id}")
+async def analyze_session(session_id: str, options: AnalysisOptions):
+ session_dir = DATA_DIR / session_id
+ if not session_dir.exists():
+ raise HTTPException(status_code=404, detail="Session ID not found.")
+
+ frame_files = sorted(session_dir.glob("frame_*.npy"))
+ if len(frame_files) < 2:
+ raise HTTPException(status_code=400, detail="Not enough frames to perform an analysis (at least 2 required).")
+
+ reference_depth = np.load(frame_files[0])
+ analysis_results = []
+
+ for i in range(1, len(frame_files)):
+ current_depth = np.load(frame_files[i])
+ depth_difference = current_depth - reference_depth
+ significant_diff = np.where(np.abs(depth_difference) > options.noise_threshold, depth_difference, 0)
+
+ total_pixels = significant_diff.size
+ changed_pixels = np.count_nonzero(significant_diff)
+
+ frame_metrics = {
+ "frame_index": i,
+ "volume_change": float(significant_diff.sum()),
+ "added_volume": float(significant_diff[significant_diff > 0].sum()),
+ "removed_volume": float(significant_diff[significant_diff < 0].sum()),
+ "mean_depth_change": float(significant_diff.mean()),
+ "changed_area_percent": (changed_pixels / total_pixels) * 100 if total_pixels > 0 else 0
+ }
+ analysis_results.append(frame_metrics)
+
+ return {"session_id": session_id, "analysis": analysis_results}
+
+@app.post("/analyze-points/{session_id}")
+async def analyze_points(session_id: str, options: PointAnalysisOptions):
+ session_dir = DATA_DIR / session_id
+ if not session_dir.exists():
+ raise HTTPException(status_code=404, detail="Session ID not found.")
+
+ frame_files = sorted(session_dir.glob("frame_*.npy"))
+ if not frame_files:
+ raise HTTPException(status_code=400, detail="No frames found for this session.")
+
+ try:
+ all_frames_data = np.array([np.load(f) for f in frame_files])
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Error loading frame data: {e}")
+
+ results = []
+ _, height, width = all_frames_data.shape
+
+ for point in options.points:
+ if not (0 <= point.x < width and 0 <= point.y < height):
+ continue
+
+ depth_evolution = all_frames_data[:, point.y, point.x]
+
+ results.append({
+ "point": point.dict(),
+ "label": f"Point ({point.x}, {point.y})",
+ "depth_values": depth_evolution.tolist()
+ })
+
+ return {"session_id": session_id, "point_analysis": results}
+
+@app.get("/")
+async def serve_frontend():
+ html_content = """
+
+
+ DepthVision Processor
+
+
+ DepthVision Processor is running
+ Use the frontend application to interact with this service.
+
+ """
+ return HTMLResponse(content=html_content)
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/pig_depth_tracker/__init__.py b/pig_depth_tracker/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pig_depth_tracker/__pycache__/__init__.cpython-312.pyc b/pig_depth_tracker/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..02795d1c
Binary files /dev/null and b/pig_depth_tracker/__pycache__/__init__.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/app.cpython-312.pyc b/pig_depth_tracker/__pycache__/app.cpython-312.pyc
new file mode 100644
index 00000000..af0d97f9
Binary files /dev/null and b/pig_depth_tracker/__pycache__/app.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/config.cpython-312.pyc b/pig_depth_tracker/__pycache__/config.cpython-312.pyc
new file mode 100644
index 00000000..c90f49c0
Binary files /dev/null and b/pig_depth_tracker/__pycache__/config.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/model.cpython-312.pyc b/pig_depth_tracker/__pycache__/model.cpython-312.pyc
new file mode 100644
index 00000000..622ad143
Binary files /dev/null and b/pig_depth_tracker/__pycache__/model.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/processing.cpython-312.pyc b/pig_depth_tracker/__pycache__/processing.cpython-312.pyc
new file mode 100644
index 00000000..b5056158
Binary files /dev/null and b/pig_depth_tracker/__pycache__/processing.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/state.cpython-312.pyc b/pig_depth_tracker/__pycache__/state.cpython-312.pyc
new file mode 100644
index 00000000..7d91400c
Binary files /dev/null and b/pig_depth_tracker/__pycache__/state.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/utils.cpython-312.pyc b/pig_depth_tracker/__pycache__/utils.cpython-312.pyc
new file mode 100644
index 00000000..6d027a79
Binary files /dev/null and b/pig_depth_tracker/__pycache__/utils.cpython-312.pyc differ
diff --git a/pig_depth_tracker/__pycache__/visualization.cpython-312.pyc b/pig_depth_tracker/__pycache__/visualization.cpython-312.pyc
new file mode 100644
index 00000000..836f0cde
Binary files /dev/null and b/pig_depth_tracker/__pycache__/visualization.cpython-312.pyc differ
diff --git a/pig_depth_tracker/app.py b/pig_depth_tracker/app.py
new file mode 100644
index 00000000..a4fba498
--- /dev/null
+++ b/pig_depth_tracker/app.py
@@ -0,0 +1,178 @@
+
+import streamlit as st
+import cv2
+import numpy as np
+import pandas as pd
+import torch
+import os
+import tempfile
+import time
+import io
+from PIL import Image
+from utils import load_css, card
+from state import init_session_state, reset_app_state
+from model import load_model
+from processing import process_video_file, validate_video_file
+from visualization import visualize_improved_detection, render_depth_analysis, visualize_sow_detection, show_symmetry_analysis, show_silhouette_analysis, show_3d_depth_visualization, show_regional_analysis
+from config import SUPPORTED_FORMATS, DEPTH_CHANGE_THRESHOLD, MIN_PIG_AREA
+
+try:
+ from depth_anything_v2.dpt import DepthAnythingV2
+except ImportError:
+ st.error("Dependencia no encontrada: 'depth_anything_v2'. Por favor, instálala para continuar.")
+ st.code("pip install git+https://github.com/LiheYoung/Depth-Anything-V2.git")
+ st.stop()
+
+# --- Configuración de la app ---
+st.set_page_config(
+ page_title="PigDepthTracker Pro",
+ layout="wide",
+ initial_sidebar_state="collapsed",
+ menu_items={"About": "### Sistema avanzado de análisis de cerdos mediante visión por computador"}
+)
+init_session_state()
+load_css()
+
+# --- Encabezado principal ---
+st.markdown("""
+
+
PigDepthTracker Pro
+
Sistema avanzado de monitoreo porcinocéntrico
+
🔬 Modo de análisis mejorado activado
+
+""", unsafe_allow_html=True)
+
+# --- SUBIDA o RESULTADOS ---
+if not st.session_state.video_processed:
+ cols = st.columns([1, 1.5, 1])
+ with cols[1]:
+ with card("Iniciar Análisis Avanzado"):
+ tab_up, tab_live = st.tabs(["Subir Vídeo", "Grabación en Directo"])
+ with tab_up:
+ up = st.file_uploader("Selecciona un vídeo de cámara cenital", type=SUPPORTED_FORMATS, label_visibility="collapsed")
+ if up:
+ with st.expander("⚙️ Configuración Avanzada", expanded=True):
+ c1, c2 = st.columns(2)
+ with c1:
+ st.session_state.depth_change_threshold = st.slider(
+ "Umbral de Cambio de Profundidad", 0.001, 0.1, DEPTH_CHANGE_THRESHOLD, 0.001, format="%.3f"
+ )
+ with c2:
+ st.session_state.min_pig_area = st.slider(
+ "Área Mínima del Cerdo (px²)", 1000, 30000, MIN_PIG_AREA, step=500
+ )
+ if st.button("🚀 Iniciar Análisis", use_container_width=True, type="primary"):
+ with st.spinner("Procesando vídeo..."):
+ process_video_file(up)
+ with tab_live:
+ st.info("**Próximamente:** conexión directa con cámaras IP. Contáctanos para acceso anticipado.")
+else:
+ st.markdown(f"""
+
+ Resumen: {st.session_state.total_frames} frames procesados |
+ Resolución: {st.session_state.original_frames[0].shape[1]}×{st.session_state.original_frames[0].shape[0]} px |
+ Umbral: {st.session_state.depth_change_threshold:.3f} | Área mínima: {st.session_state.min_pig_area}px²
+
+ """, unsafe_allow_html=True)
+
+ if st.button("🔄 Nuevo Análisis", on_click=reset_app_state):
+ st.experimental_rerun()
+
+ tab_view, tab_analysis, tab_export = st.tabs(["Visor", "Análisis", "Exportar"])
+
+ # --- Visor de secuencia ---
+ with tab_view:
+ with card("Visor de Secuencia"):
+ c1, c2, c3, c4 = st.columns([0.1, 0.8, 0.1, 0.2])
+ c1.button("⏮️", on_click=lambda: st.session_state.update(current_frame_index=0), use_container_width=True)
+ play_pause = c2.empty()
+ if play_pause.button("▶️ Play" if not st.session_state.playing else "⏸️ Pausa", use_container_width=True, key="play_button"):
+ st.session_state.playing = not st.session_state.playing
+ c3.button("⏭️", on_click=lambda: st.session_state.update(current_frame_index=st.session_state.total_frames-1), use_container_width=True)
+ frame_slider = c4.slider("Frame", 0, st.session_state.total_frames - 1, st.session_state.current_frame_index, label_visibility="collapsed")
+ if frame_slider != st.session_state.current_frame_index:
+ st.session_state.current_frame_index = frame_slider
+ st.session_state.playing = False
+
+ idx = st.session_state.current_frame_index
+ col1, col2 = st.columns(2)
+
+ with col1:
+ frame = st.session_state.original_frames[idx]
+ depth_map = st.session_state.depth_maps_colored[idx]
+ sow_mask = st.session_state.pig_masks[idx] if idx < len(st.session_state.pig_masks) else None
+ bbox = st.session_state.pig_bboxes[idx] if idx < len(st.session_state.pig_bboxes) else None
+ centroid = st.session_state.pig_centroids[idx] if idx < len(st.session_state.pig_centroids) else None
+ analysis = st.session_state.depth_analysis[idx] if idx < len(st.session_state.depth_analysis) else None
+
+ if sow_mask is not None:
+ vis = visualize_sow_detection(
+ frame,
+ sow_mask,
+ depth_map,
+ analysis
+ )
+ st.image(vis, caption=f"Frame {idx + 1}", use_container_width=True)
+ else:
+ st.warning("No sow detected in this frame")
+ st.image(frame, caption=f"Frame {idx + 1} (No Detection)", use_container_width=True)
+
+ with col2:
+ show_silhouette_analysis(
+ frame,
+ depth_map,
+ sow_mask,
+ st.session_state.masked_depths[idx] if idx < len(st.session_state.masked_depths) else None,
+ st.session_state.anomaly_maps[idx] if idx < len(st.session_state.anomaly_maps) else None
+ )
+
+ # Add new visualizations
+ with card("Análisis de Profundidad 3D"):
+ show_3d_depth_visualization(
+ st.session_state.depth_maps_raw[idx],
+ st.session_state.pig_masks[idx]
+ )
+
+ with card("Análisis Regional"):
+ col1, col2 = st.columns(2)
+ with col1:
+ show_regional_analysis(st.session_state.depth_analysis[idx])
+ with col2:
+ show_symmetry_analysis(st.session_state.depth_analysis[idx])
+
+ with tab_analysis:
+ render_depth_analysis(st.session_state.depth_analysis)
+
+ with tab_export:
+ with card("Exportar Resultados"):
+ st.markdown("Descarga los datos procesados en formato CSV, JSON o Excel.")
+ fmt = st.radio("Formato", ["CSV", "JSON", "Excel"], horizontal=True)
+ if st.button("📤 Generar Archivo"):
+ rows = []
+ for i in range(st.session_state.total_frames):
+ rows.append({
+ "frame": i,
+ "centroid_x": st.session_state.pig_centroids[i][0],
+ "centroid_y": st.session_state.pig_centroids[i][1],
+ "bbox_area": (st.session_state.pig_bboxes[i][2] * st.session_state.pig_bboxes[i][3]) if st.session_state.pig_bboxes[i] else 0,
+ "mean_depth": st.session_state.depth_analysis[i]["mean_depth"] if st.session_state.depth_analysis[i] else None,
+ "asymmetry": st.session_state.posture_analysis_results[i]["asymmetry"] if i < len(st.session_state.posture_analysis_results) else None,
+ "depth_matrix": st.session_state.depth_segmented_matrices[i].tolist() if i < len(st.session_state.depth_segmented_matrices) else None
+ })
+ df = pd.DataFrame(rows)
+ if fmt == "CSV":
+ st.download_button("⬇️ Descargar CSV", df.to_csv(index=False).encode(), "resultados.csv", "text/csv")
+ elif fmt == "JSON":
+ st.download_button("⬇️ Descargar JSON", df.to_json(indent=2).encode(), "resultados.json", "application/json")
+ else:
+ excel = io.BytesIO()
+ with pd.ExcelWriter(excel, engine="xlsxwriter") as writer:
+ df.to_excel(writer, index=False, sheet_name="Resultados")
+ excel.seek(0)
+ st.download_button("⬇️ Descargar Excel", excel, "resultados.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+
+if st.session_state.get("playing"):
+ time.sleep(0.05)
+ st.session_state.current_frame_index = (st.session_state.current_frame_index + 1) % st.session_state.total_frames
+ st.rerun()
diff --git a/pig_depth_tracker/config.py b/pig_depth_tracker/config.py
new file mode 100644
index 00000000..c893716b
--- /dev/null
+++ b/pig_depth_tracker/config.py
@@ -0,0 +1,25 @@
+# pig_depth_tracker/config.py
+MAX_VIDEO_DURATION = 300
+MAX_VIDEO_SIZE_MB = 100
+SUPPORTED_FORMATS = ["mp4", "mov", "avi"]
+RECORDING_SERVER = "http://192.168.1.42:8000"
+FRAME_POLL_INTERVAL = 2
+MIN_PIG_AREA = 10000
+DEPTH_CHANGE_THRESHOLD = 0.02
+
+# Configuración de detección mejorada
+BACKGROUND_UPDATE_ALPHA = 0.01
+MIN_PIG_AREA = 8000
+MORPH_KERNEL_SIZE = 15
+DEPTH_CHANGE_THRESHOLD = 0.02
+
+MIN_SOW_AREA = 15000 # Minimum pixel area for valid sow detection
+SEGMENTATION_MORPH_KERNEL = 25 # Size of morphological operation kernel
+COLOR_LOWER_PINK = [140, 40, 40] # HSV lower bounds for pink tones
+COLOR_UPPER_PINK = [180, 255, 255] # HSV upper bounds for pink tones
+COLOR_LOWER_BROWN = [5, 40, 40] # HSV lower bounds for brown tones
+COLOR_UPPER_BROWN = [30, 255, 255] # HSV upper bounds for brown tones
+
+# Validation parameters
+MIN_SOW_PIXELS = 1000 # Minimum pixels to consider as valid detection
+MIN_SOLIDITY = 0.85 # Minimum contour solidity for valid detection
\ No newline at end of file
diff --git a/pig_depth_tracker/deprecation.py b/pig_depth_tracker/deprecation.py
new file mode 100644
index 00000000..92d13d60
--- /dev/null
+++ b/pig_depth_tracker/deprecation.py
@@ -0,0 +1,182 @@
+# pig_depth_tracker/depreciation.py
+import numpy as np
+from scipy import stats
+from typing import Dict, List, Optional
+from collections import deque
+import pandas as pd
+
+class DepreciationAnalyzer:
+ def __init__(self, config: Optional[Dict] = None):
+ self.config = config or {
+ 'temporal_window': 15, # frames to consider for temporal analysis
+ 'depth_change_threshold': 0.05, # significant depth change
+ 'asymmetry_threshold': 0.25, # significant asymmetry
+ 'min_consistent_frames': 5 # min frames for consistent pattern
+ }
+ self.reference_model = None
+ self.temporal_buffer = deque(maxlen=self.config['temporal_window'])
+
+ def build_reference_model(self, depth_analyses: List[Dict]):
+ """Build reference model from initial healthy frames"""
+ if not depth_analyses:
+ return None
+
+ # Collect all depth values
+ all_depths = np.concatenate([da['depth_values'] for da in depth_analyses if da])
+
+ # Calculate reference statistics
+ self.reference_model = {
+ 'mean': float(np.mean(all_depths)),
+ 'std': float(np.std(all_depths)),
+ 'percentiles': {
+ '5': float(np.percentile(all_depths, 5)),
+ '25': float(np.percentile(all_depths, 25)),
+ '50': float(np.percentile(all_depths, 50)),
+ '75': float(np.percentile(all_depths, 75)),
+ '95': float(np.percentile(all_depths, 95))
+ },
+ 'regional_means': {
+ 'head': float(np.mean([da.get('head_mean', np.nan) for da in depth_analyses])),
+ 'middle': float(np.mean([da.get('middle_mean', np.nan) for da in depth_analyses])),
+ 'rear': float(np.mean([da.get('rear_mean', np.nan) for da in depth_analyses]))
+ }
+ }
+ return self.reference_model
+
+ def detect_depreciation(self, current_analysis: Dict, frame_idx: int) -> Dict:
+ """Main detection method combining multiple analyses"""
+ if not current_analysis:
+ return {'depreciation_score': 0.0, 'confidence': 0.0}
+
+ # Update temporal buffer
+ self.temporal_buffer.append(current_analysis)
+
+ # Run individual analyses
+ depth_dev = self._analyze_depth_deviation(current_analysis)
+ temp_consistency = self._check_temporal_consistency(current_analysis, frame_idx)
+ regional = self._analyze_regions(current_analysis)
+ symmetry = self._analyze_symmetry(current_analysis)
+
+ # Combine results into overall score (0-1)
+ depreciation_score = 0.4 * depth_dev['score'] + \
+ 0.3 * temp_consistency['score'] + \
+ 0.2 * regional['score'] + \
+ 0.1 * symmetry['score']
+
+ # Build results dictionary
+ results = {
+ 'depreciation_score': float(depreciation_score),
+ 'confidence': min(1.0, len(self.temporal_buffer)/self.config['temporal_window']),
+ 'depth_deviation': depth_dev,
+ 'temporal_consistency': temp_consistency,
+ 'regional_analysis': regional,
+ 'symmetry_analysis': symmetry,
+ 'frame_index': frame_idx
+ }
+
+ return results
+
+ def _analyze_depth_deviation(self, analysis: Dict) -> Dict:
+ """Compare current depth to reference model"""
+ if not self.reference_model:
+ return {'score': 0.0, 'message': 'No reference model'}
+
+ # Overall depth comparison
+ depth_diff = abs(analysis['mean_depth'] - self.reference_model['mean'])
+ depth_score = min(1.0, depth_diff / (self.reference_model['std'] * 2))
+
+ # Percentile comparison
+ p5_diff = abs(analysis['percentiles']['5'] - self.reference_model['percentiles']['5'])
+ p95_diff = abs(analysis['percentiles']['95'] - self.reference_model['percentiles']['95'])
+ percentile_score = min(1.0, (p5_diff + p95_diff) / (self.reference_model['std'] * 3))
+
+ return {
+ 'score': max(depth_score, percentile_score),
+ 'depth_difference': depth_diff,
+ 'percentile_differences': {'p5': p5_diff, 'p95': p95_diff},
+ 'message': f"Depth deviation: {depth_diff:.3f} (ref: {self.reference_model['mean']:.3f})"
+ }
+
+ def _check_temporal_consistency(self, analysis: Dict, frame_idx: int) -> Dict:
+ """Check if anomalies persist over time"""
+ if len(self.temporal_buffer) < 2:
+ return {'score': 0.0, 'message': 'Insufficient temporal data'}
+
+ # Calculate rolling mean and std
+ means = [a['mean_depth'] for a in self.temporal_buffer if a]
+ stds = [a['std_depth'] for a in self.temporal_buffer if a]
+
+ rolling_mean = np.mean(means)
+ rolling_std = np.std(means)
+
+ # Check for consistent deviation
+ current_dev = abs(analysis['mean_depth'] - rolling_mean)
+ consistent_frames = sum(1 for m in means if abs(m - rolling_mean) > self.config['depth_change_threshold']))
+
+ consistency_score = min(1.0, consistent_frames / self.config['min_consistent_frames'])
+
+ return {
+ 'score': consistency_score,
+ 'rolling_mean': rolling_mean,
+ 'rolling_std': rolling_std,
+ 'current_deviation': current_dev,
+ 'consistent_frames': consistent_frames,
+ 'message': f"{consistent_frames} consistent anomalous frames"
+ }
+
+ def _analyze_regions(self, analysis: Dict) -> Dict:
+ """Analyze regional depth patterns"""
+ if not self.reference_model:
+ return {'score': 0.0, 'message': 'No reference model'}
+
+ regional_scores = {}
+ max_score = 0.0
+
+ for region in ['head', 'middle', 'rear']:
+ current = analysis.get(f'{region}_mean')
+ reference = self.reference_model['regional_means'].get(region)
+
+ if current is None or reference is None:
+ continue
+
+ diff = abs(current - reference)
+ score = min(1.0, diff / (self.reference_model['std'] * 1.5))
+ regional_scores[region] = score
+ max_score = max(max_score, score)
+
+ return {
+ 'score': max_score,
+ 'regional_differences': regional_scores,
+ 'message': f"Max regional difference: {max_score:.2f}"
+ }
+
+ def _analyze_symmetry(self, analysis: Dict) -> Dict:
+ """Analyze left-right symmetry"""
+ symmetry_score = analysis.get('symmetry_score', 0.0)
+ return {
+ 'score': min(1.0, symmetry_score / self.config['asymmetry_threshold']),
+ 'raw_score': symmetry_score,
+ 'message': f"Symmetry score: {symmetry_score:.3f} (threshold: {self.config['asymmetry_threshold']})"
+ }
+
+ def generate_report(self, all_results: List[Dict]) -> pd.DataFrame:
+ """Generate comprehensive report from all frame analyses"""
+ report_data = []
+
+ for result in all_results:
+ if not result:
+ continue
+
+ row = {
+ 'frame': result['frame_index'],
+ 'depreciation_score': result['depreciation_score'],
+ 'confidence': result['confidence'],
+ 'depth_deviation': result['depth_deviation']['score'],
+ 'temporal_consistency': result['temporal_consistency']['score'],
+ 'regional_analysis': result['regional_analysis']['score'],
+ 'symmetry': result['symmetry_analysis']['score'],
+ **{f"region_{k}": v for k, v in result['regional_analysis']['regional_differences'].items()}
+ }
+ report_data.append(row)
+
+ return pd.DataFrame(report_data)
\ No newline at end of file
diff --git a/pig_depth_tracker/model.py b/pig_depth_tracker/model.py
new file mode 100644
index 00000000..e2b96cd3
--- /dev/null
+++ b/pig_depth_tracker/model.py
@@ -0,0 +1,69 @@
+# pig_depth_tracker/model.py
+import streamlit as st
+import torch
+import os
+import requests
+import cv2
+import numpy as np
+from config import RECORDING_SERVER
+try:
+ from depth_anything_v2.dpt import DepthAnythingV2
+except ImportError:
+ st.error("Dependencia no encontrada: 'depth_anything_v2'. Por favor, instálala para continuar.")
+ st.code("pip install git+https://github.com/LiheYoung/Depth-Anything-V2.git")
+ st.stop()
+
+@torch.no_grad()
+@st.cache_resource(show_spinner="Cargando modelo de IA…")
+def load_model(encoder="vitl"):
+ if torch.cuda.is_available():
+ device = torch.device("cuda")
+ st.info("GPU con CUDA detectada. Usando GPU para el procesamiento.")
+ else:
+ device = torch.device("cpu")
+ st.warning("No se detectó una GPU con CUDA. El modelo se ejecutará en la CPU, lo que será considerablemente más lento.")
+
+ if not os.path.exists("checkpoints"):
+ st.error("Directorio 'checkpoints' no encontrado. Por favor, descarga los modelos y colócalos en esa carpeta.")
+ st.stop()
+
+ ckpt = f"checkpoints/depth_anything_v2_{encoder}.pth"
+ if not os.path.exists(ckpt):
+ st.error(f"Modelo no encontrado: {ckpt}. Asegúrate de que el archivo del modelo está en el directorio 'checkpoints'.")
+ st.stop()
+
+ cfg = {"encoder": encoder, "features": 256, "out_channels": [256, 512, 1024, 1024]}
+ net = DepthAnythingV2(**cfg)
+ net.load_state_dict(torch.load(ckpt, map_location=device))
+ net.to(device).eval()
+ return net, device
+
+@torch.no_grad()
+def predict_depth(model, device, image):
+ # Convertir a RGB y redimensionar si no está en tamaño adecuado
+ if image.shape[2] == 4:
+ image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB)
+ elif image.shape[2] == 1:
+ image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
+
+ # Preprocesamiento
+ img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
+ img_resized = cv2.resize(img_rgb, (640, 360))
+ img_tensor = torch.from_numpy(img_resized).permute(2, 0, 1).float().unsqueeze(0) / 255.0
+ img_tensor = img_tensor.to(device)
+
+ # Inferencia
+ depth = model(img_tensor)[0, 0].cpu().numpy()
+ depth_normalized = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8)
+
+ # Generar mapa coloreado
+ depth_colored = cv2.applyColorMap((depth_normalized * 255).astype(np.uint8), cv2.COLORMAP_MAGMA)
+
+ metrics = {
+ "min": float(depth.min()),
+ "max": float(depth.max()),
+ "mean": float(depth.mean()),
+ "std": float(depth.std()),
+ }
+
+ return depth_normalized, metrics, depth_colored
\ No newline at end of file
diff --git a/pig_depth_tracker/processing.py b/pig_depth_tracker/processing.py
new file mode 100644
index 00000000..b0021a7b
--- /dev/null
+++ b/pig_depth_tracker/processing.py
@@ -0,0 +1,401 @@
+import cv2
+import numpy as np
+import os
+import tempfile
+import torch
+import streamlit as st
+from sklearn.mixture import GaussianMixture
+from model import load_model
+from config import MAX_VIDEO_SIZE_MB, SUPPORTED_FORMATS, MAX_VIDEO_DURATION, MORPH_KERNEL_SIZE, MIN_PIG_AREA
+from state import reset_app_state
+
+from scipy import ndimage
+
+# Updated segmentation in processing.py
+# Updated segmentation in processing.py
+def segment_sow_enhanced(depth_map, rgb_frame=None, min_area=15000):
+ """
+ Enhanced segmentation specifically for overhead sow detection
+ Combines depth and color information for better accuracy
+ """
+ # Normalize depth map
+ depth_norm = cv2.normalize(depth_map, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
+
+ # Adaptive thresholding - works better for varying lighting
+ thresh = cv2.adaptiveThreshold(
+ depth_norm, 255,
+ cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
+ cv2.THRESH_BINARY_INV, 51, 7 # Increased block size for larger animals
+ )
+
+ # Morphological operations with elliptical kernel
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (25, 25)) # Larger kernel for sow size
+ cleaned = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
+ cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, kernel)
+
+ # Find contours and filter by size and solidity
+ contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+
+ # Filter contours by area and solidity (sows should be large, solid shapes)
+ valid_contours = []
+ for cnt in contours:
+ area = cv2.contourArea(cnt)
+ hull = cv2.convexHull(cnt)
+ hull_area = cv2.contourArea(hull)
+ solidity = float(area)/hull_area if hull_area > 0 else 0
+
+ if area > min_area and solidity > 0.85: # Sows should be fairly solid shapes
+ valid_contours.append(cnt)
+
+ # Create initial mask
+ mask = np.zeros_like(depth_norm, dtype=np.uint8)
+ cv2.drawContours(mask, valid_contours, -1, 255, -1)
+
+ # Refine with color information if available
+ if rgb_frame is not None:
+ mask = refine_with_color(mask, rgb_frame)
+
+ return mask, len(valid_contours) > 0 # Return mask and detection status
+
+def refine_with_color(mask, rgb_frame):
+ """Color-based refinement for sows (typically pinkish/brownish)"""
+ hsv = cv2.cvtColor(rgb_frame, cv2.COLOR_RGB2HSV)
+
+ # Color ranges for typical sow colors
+ lower_pink = np.array([140, 40, 40])
+ upper_pink = np.array([180, 255, 255])
+ lower_brown = np.array([5, 40, 40])
+ upper_brown = np.array([30, 255, 255])
+
+ # Create color masks
+ pink_mask = cv2.inRange(hsv, lower_pink, upper_pink)
+ brown_mask = cv2.inRange(hsv, lower_brown, upper_brown)
+ color_mask = cv2.bitwise_or(pink_mask, brown_mask)
+
+ # Combine with depth mask
+ refined_mask = cv2.bitwise_and(mask, color_mask)
+
+ # Final cleanup
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
+ refined_mask = cv2.morphologyEx(refined_mask, cv2.MORPH_CLOSE, kernel)
+
+ return refined_mask
+
+def analyze_sow_depth(depth_map, mask):
+ """
+ Robust depth analysis with validation checks
+ Returns None if no valid sow detected
+ """
+ if mask.sum() == 0: # Empty mask
+ return None
+
+ sow_pixels = depth_map[mask == 255]
+ if len(sow_pixels) < 1000: # Minimum pixels to consider valid
+ return None
+
+ # Calculate depth statistics
+ analysis = {
+ 'mean_depth': float(np.mean(sow_pixels)),
+ 'median_depth': float(np.median(sow_pixels)),
+ 'std_depth': float(np.std(sow_pixels)),
+ 'min_depth': float(np.min(sow_pixels)),
+ 'max_depth': float(np.max(sow_pixels)),
+ 'area_pixels': int(len(sow_pixels)),
+ 'percentiles': {
+ '5': float(np.percentile(sow_pixels, 5)),
+ '25': float(np.percentile(sow_pixels, 25)),
+ '50': float(np.percentile(sow_pixels, 50)),
+ '75': float(np.percentile(sow_pixels, 75)),
+ '95': float(np.percentile(sow_pixels, 95))
+ }
+ }
+
+ # Calculate body region depths
+ y_coords, x_coords = np.where(mask == 255)
+ height = y_coords.max() - y_coords.min()
+
+ for region, y_range in [('head', (0, 0.3)),
+ ('middle', (0.3, 0.7)),
+ ('rear', (0.7, 1.0))]:
+ region_mask = (y_coords >= y_coords.min() + y_range[0]*height) & \
+ (y_coords <= y_coords.min() + y_range[1]*height)
+ region_depths = sow_pixels[region_mask]
+
+ if len(region_depths) > 0:
+ analysis[f'{region}_mean'] = float(np.mean(region_depths))
+ analysis[f'{region}_std'] = float(np.std(region_depths))
+
+ return analysis
+
+def handle_segmentation_errors(results):
+ """Analyze and handle cases where segmentation fails"""
+ if results['detection_success']:
+ return results
+
+ # Try fallback methods when primary segmentation fails
+ if results['sow_mask'] is None:
+ # Method 1: Try with relaxed parameters
+ relaxed_mask, _ = segment_sow_enhanced(
+ results['raw_depth'],
+ results['frame'],
+ min_area=10000 # Lower minimum area
+ )
+
+ # Method 2: Try pure depth-based segmentation
+ if relaxed_mask.sum() == 0:
+ _, relaxed_mask = cv2.threshold(
+ (results['raw_depth'] * 255).astype(np.uint8),
+ 0, 255,
+ cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU
+ )
+
+ # If either method worked, re-analyze
+ if relaxed_mask.sum() > 0:
+ results['sow_mask'] = relaxed_mask
+ results['depth_analysis'] = analyze_sow_depth(results['raw_depth'], relaxed_mask)
+ results['detection_success'] = results['depth_analysis'] is not None
+
+ return results
+
+
+def validate_video_file(f):
+ if f.size > MAX_VIDEO_SIZE_MB * 1024 * 1024:
+ st.error(f"El vídeo no puede superar los {MAX_VIDEO_SIZE_MB} MB."); return False
+ if f.name.split('.')[-1].lower() not in SUPPORTED_FORMATS:
+ st.error(f"Formato no soportado. Sube {', '.join(SUPPORTED_FORMATS)}."); return False
+ return True
+
+def extract_frames(path):
+ frames=[]; cap=cv2.VideoCapture(path); fps=cap.get(cv2.CAP_PROP_FPS); n=int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
+ if fps <= 0 or n <= 0: st.error("No se pudo leer la información del vídeo."); return []
+ if n / fps > MAX_VIDEO_DURATION: st.error(f"El vídeo no puede durar más de {MAX_VIDEO_DURATION} segundos."); return []
+ prog = st.progress(0., text="Extrayendo frames...")
+ while True:
+ ok, f = cap.read()
+ if not ok: break
+ frames.append(cv2.cvtColor(f, cv2.COLOR_BGR2RGB))
+ prog.progress(len(frames) / n, text=f"Extrayendo frame {len(frames)}/{n}")
+ cap.release(); return frames
+
+def predict_depth(model, device, img_rgb):
+ with torch.no_grad():
+ raw = model.infer_image(cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR))
+ norm = cv2.normalize(raw, None, 0, 1, cv2.NORM_MINMAX, cv2.CV_32F)
+ colored = cv2.cvtColor(cv2.applyColorMap((norm*255).astype(np.uint8), cv2.COLORMAP_VIRIDIS), cv2.COLOR_BGR2RGB)
+ metrics = dict(min=float(raw.min()), max=float(raw.max()), mean=float(raw.mean()), std=float(raw.std()), median=float(np.median(raw)))
+ return raw, metrics, colored
+
+def segment_pig_fast(depth_map):
+ """Segmenta la región del cerdo con una segmentación simple basada en profundidad."""
+ median_depth = np.median(depth_map)
+ pig_mask = (depth_map < median_depth * 1.1).astype(np.uint8) * 255
+ pig_mask = cv2.morphologyEx(pig_mask, cv2.MORPH_CLOSE, np.ones((15, 15), np.uint8))
+ return pig_mask
+
+def extract_bbox_and_centroid(mask):
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ if contours:
+ c = max(contours, key=cv2.contourArea)
+ x, y, w, h = cv2.boundingRect(c)
+ M = cv2.moments(c)
+ cx = int(M["m10"] / M["m00"]) if M["m00"] != 0 else 0
+ cy = int(M["m01"] / M["m00"]) if M["m00"] != 0 else 0
+ return (x, y, w, h), (cx, cy)
+ return None, (0, 0)
+
+# Suggested improvements to analyze_depth_segmented() in processing.py
+from scipy import stats
+
+def analyze_sow_depth(depth_map, mask):
+ """Robust depth analysis that ensures required fields exist"""
+ if mask is None or mask.sum() == 0:
+ return None
+
+ sow_pixels = depth_map[mask == 255]
+ if len(sow_pixels) < 1000:
+ return None
+
+ # Ensure all required fields are included
+ analysis = {
+ 'mean_depth': float(np.mean(sow_pixels)),
+ 'std_depth': float(np.std(sow_pixels)),
+ 'min_depth': float(np.min(sow_pixels)),
+ 'max_depth': float(np.max(sow_pixels)),
+ # Ensure percentiles exist even if empty
+ 'percentiles': {
+ '5': float(np.percentile(sow_pixels, 5)),
+ '25': float(np.percentile(sow_pixels, 25)),
+ '50': float(np.percentile(sow_pixels, 50)),
+ '75': float(np.percentile(sow_pixels, 75)),
+ '95': float(np.percentile(sow_pixels, 95))
+ },
+ 'anomaly_pixels': 0.0 # Initialize with default value
+ }
+
+ # Calculate anomalies (same as before)
+ q1 = analysis['percentiles']['25']
+ q3 = analysis['percentiles']['75']
+ iqr = q3 - q1
+ lower = q1 - 1.5 * iqr
+ upper = q3 + 1.5 * iqr
+
+ anomaly_mask = np.zeros_like(mask, dtype=np.uint8)
+ anomaly_mask[(depth_map < lower) & (mask == 255)] = 255
+ anomaly_mask[(depth_map > upper) & (mask == 255)] = 255
+
+ analysis['anomaly_pixels'] = float(np.sum(anomaly_mask == 255)) / np.sum(mask == 255)
+
+ return analysis
+
+
+
+def analyze_depth_segmented(masked_depth, pig_mask, frame_shape):
+ """
+ Enhanced depth analysis with regional and symmetry metrics
+
+ Args:
+ masked_depth: Depth values within pig mask
+ pig_mask: Binary mask of pig
+ frame_shape: Shape of original frame (height, width)
+
+ Returns:
+ tuple: (analysis_dict, anomaly_mask)
+ """
+ pig_pixels = masked_depth[pig_mask == 255]
+ if pig_pixels.size == 0:
+ return None, None
+
+ # Get coordinates of all pig pixels
+ y_coords, x_coords = np.where(pig_mask == 255)
+ coords = np.column_stack((x_coords, y_coords))
+ depth_values = masked_depth[pig_mask == 255]
+
+ # Basic statistics
+ analysis = {
+ 'depth_values': depth_values.tolist(), # For visualization
+ 'mean_depth': float(np.mean(depth_values)),
+ 'std_depth': float(np.std(depth_values)),
+ 'min_depth': float(np.min(depth_values)),
+ 'max_depth': float(np.max(depth_values)),
+ 'skewness': float(stats.skew(depth_values)),
+ 'kurtosis': float(stats.kurtosis(depth_values)),
+ 'percentiles': {
+ '5': float(np.percentile(depth_values, 5)),
+ '25': float(np.percentile(depth_values, 25)),
+ '50': float(np.percentile(depth_values, 50)),
+ '75': float(np.percentile(depth_values, 75)),
+ '95': float(np.percentile(depth_values, 95)),
+ }
+ }
+
+ # Regional analysis
+ height, width = frame_shape
+ regions = {
+ 'head': (y_coords < height//3),
+ 'middle': ((y_coords >= height//3) & (y_coords < 2*height//3)),
+ 'rear': (y_coords >= 2*height//3),
+ 'left': (x_coords < width//2),
+ 'right': (x_coords >= width//2)
+ }
+
+ for region_name, region_mask in regions.items():
+ region_depths = depth_values[region_mask]
+ if len(region_depths) > 0:
+ analysis[f'{region_name}_mean'] = float(np.mean(region_depths))
+ analysis[f'{region_name}_std'] = float(np.std(region_depths))
+ analysis[f'{region_name}_min'] = float(np.min(region_depths))
+ analysis[f'{region_name}_max'] = float(np.max(region_depths))
+
+ # Symmetry analysis
+ if 'left_mean' in analysis and 'right_mean' in analysis:
+ analysis['symmetry_score'] = float(
+ np.abs(analysis['left_mean'] - analysis['right_mean']) /
+ (analysis['mean_depth'] + 1e-8) # Avoid division by zero
+ )
+
+ # Anomaly detection
+ q1 = analysis['percentiles']['25']
+ q3 = analysis['percentiles']['75']
+ iqr = q3 - q1
+ lower_bound = q1 - 1.5 * iqr
+ upper_bound = q3 + 1.5 * iqr
+
+ anomaly_mask = np.zeros_like(pig_mask, dtype=np.uint8)
+ anomaly_mask[(masked_depth < lower_bound) & (pig_mask == 255)] = 255
+ anomaly_mask[(masked_depth > upper_bound) & (pig_mask == 255)] = 255
+
+ analysis['anomaly_pixels'] = float(np.sum(anomaly_mask == 255)) / np.sum(pig_mask == 255)
+
+ return analysis, anomaly_mask
+
+# Update in processing.py
+def process_video_file(up_file):
+ if not validate_video_file(up_file):
+ return
+
+ reset_app_state()
+ model, device = load_model()
+
+ with st.status("Procesando vídeo mejorado...", expanded=True) as status:
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as tmp_file:
+ tmp_file.write(up_file.read())
+ video_path = tmp_file.name
+
+ original_frames = extract_frames(video_path)
+ frames = [cv2.resize(f, (640, 360)) for f in original_frames]
+ os.remove(video_path)
+
+ if not frames:
+ status.update(label="Error en extracción de frames", state="error")
+ return
+
+ st.session_state.total_frames = len(frames)
+ progress_bar = st.progress(0, text="Procesando frames...")
+
+ # Initialize buffers
+ originals, raws, coloreds, masks, masked_depths = [], [], [], [], []
+ bboxes, centroids, analyses, anomalies = [], [], [], []
+ segmented_matrices, confidence_scores = [], []
+
+ for i, frame in enumerate(frames):
+ # Replace the existing processing code with:
+ raw, metrics, colored = predict_depth(model, device, frame)
+
+ # Enhanced segmentation
+ sow_mask, detection_success = segment_sow_enhanced(raw, frame)
+ masked_depth = np.where(sow_mask == 255, raw, 0) if detection_success else None
+
+ # Enhanced analysis
+ analysis = analyze_sow_depth(raw, sow_mask) if detection_success else None
+ bbox, centroid = extract_bbox_and_centroid(sow_mask) if detection_success else (None, None)
+
+ # Store results
+ originals.append(original_frames[i])
+ raws.append(raw)
+ coloreds.append(colored)
+ masks.append(sow_mask if detection_success else None)
+ masked_depths.append(masked_depth)
+ bboxes.append(bbox)
+ centroids.append(centroid)
+ analyses.append(analysis)
+
+ progress_bar.progress((i + 1) / len(frames),
+ text=f"Frame {i + 1}/{len(frames)} - {'Detected' if detection_success else 'No detection'}")
+ # Save to session state
+ st.session_state.update({
+ 'original_frames': originals,
+ 'depth_maps_raw': raws,
+ 'depth_maps_colored': coloreds,
+ 'pig_masks': masks,
+ 'masked_depths': masked_depths,
+ 'pig_bboxes': bboxes,
+ 'pig_centroids': centroids,
+ 'depth_analysis': analyses,
+ 'anomaly_maps': anomalies,
+ 'depth_segmented_matrices': segmented_matrices,
+ 'segmentation_confidence': confidence_scores
+ })
+
+ st.session_state.video_processed = True
+ status.update(label="Procesamiento completado", state="complete")
+ st.rerun()
diff --git a/pig_depth_tracker/state.py b/pig_depth_tracker/state.py
new file mode 100644
index 00000000..645ffcd9
--- /dev/null
+++ b/pig_depth_tracker/state.py
@@ -0,0 +1,56 @@
+# pig_depth_tracker/state.py
+import streamlit as st
+from collections import defaultdict
+from config import MIN_PIG_AREA, DEPTH_CHANGE_THRESHOLD
+import requests
+
+def init_session_state():
+ if "initialized" in st.session_state:
+ return
+ st.session_state.update({
+ "initialized": True,
+ "video_processed": False,
+ "playing": False,
+ "current_frame_index": 0,
+ "total_frames": 0,
+ "original_frames": [],
+ "depth_maps_raw": [],
+ "depth_maps_colored": [],
+ "pig_masks": [],
+ "pig_contours": [],
+ "pig_bboxes": [],
+ "pig_centroids": [],
+ "depth_changes": [],
+ "metrics_cache": [],
+ "selected_points": [],
+ "volume_analysis_results": None,
+ "point_analysis_results": None,
+ "posture_analysis_results": [],
+ "depth_analysis": [], # <--- ¡AQUÍ!
+ "noise_threshold": 0.01,
+ "previewing": False,
+ "recording": False,
+ "recording_session_id": None,
+ "recorded_frames": 0,
+ "frame_urls": [],
+ "live_frame": None,
+ "processing_recorded": False,
+ "min_pig_area": MIN_PIG_AREA,
+ "depth_change_threshold": DEPTH_CHANGE_THRESHOLD,
+ "pig_tracks": defaultdict(list),
+ "movement_maps": [],
+ "masked_depths": [] # ✅ Añade esta línea
+ })
+
+def reset_app_state():
+ """Función independiente para resetear el estado"""
+ from config import RECORDING_SERVER # Import local para evitar circularidad
+ sid = st.session_state.get("recording_session_id")
+ for k in list(st.session_state.keys()):
+ del st.session_state[k]
+ init_session_state()
+ if sid:
+ try:
+ requests.post(f"{RECORDING_SERVER}/stop-recording/{sid}", timeout=2)
+ except Exception:
+ pass
\ No newline at end of file
diff --git a/pig_depth_tracker/utils.py b/pig_depth_tracker/utils.py
new file mode 100644
index 00000000..11ffa270
--- /dev/null
+++ b/pig_depth_tracker/utils.py
@@ -0,0 +1,138 @@
+# pig_depth_tracker/utils.py
+import streamlit as st
+from contextlib import contextmanager
+
+def load_css():
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+@contextmanager
+def card(title: str | None = None):
+ st.markdown(f"{'
'+title+'
' if title else ''}", unsafe_allow_html=True)
+ yield
+ st.markdown("
", unsafe_allow_html=True)
\ No newline at end of file
diff --git a/pig_depth_tracker/visualization.py b/pig_depth_tracker/visualization.py
new file mode 100644
index 00000000..e177251b
--- /dev/null
+++ b/pig_depth_tracker/visualization.py
@@ -0,0 +1,229 @@
+# pig_depth_tracker/visualization.py
+import cv2
+import numpy as np
+import streamlit as st
+import pandas as pd
+import plotly.express as px
+from utils import card
+
+def visualize_improved_detection(frame, pig_mask, centroid, bbox, depth_analysis):
+ overlay = frame.copy()
+
+ if pig_mask is not None:
+ contours, _ = cv2.findContours(pig_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ if contours:
+ cv2.drawContours(overlay, contours, -1, (0, 255, 0), 2)
+
+ if bbox:
+ x, y, w, h = bbox
+ cv2.rectangle(overlay, (x, y), (x + w, y + h), (255, 0, 0), 2)
+
+ if centroid:
+ cv2.circle(overlay, centroid, 5, (0, 0, 255), -1)
+
+ if depth_analysis:
+ label = f"Media: {depth_analysis['mean_depth']:.2f} | Std: {depth_analysis['std_depth']:.2f}"
+ cv2.putText(overlay, label, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
+
+ return overlay
+
+def visualize_sow_detection(frame, mask, colored_depth, depth_analysis):
+ """Create comprehensive visualization"""
+ vis = frame.copy()
+
+ if mask is not None:
+ # Draw contour
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ cv2.drawContours(vis, contours, -1, (0, 255, 0), 2)
+
+ # Draw regions
+ y_coords, x_coords = np.where(mask == 255)
+ height = y_coords.max() - y_coords.min()
+
+ for i, (name, y_range) in enumerate([('Head', 0.3), ('Middle', 0.7), ('Rear', 1.0)]):
+ y_pos = int(y_coords.min() + y_range * height)
+ cv2.line(vis,
+ (x_coords.min(), y_pos),
+ (x_coords.max(), y_pos),
+ (255, 0, 0), 1)
+ cv2.putText(vis, name,
+ (x_coords.min() + 10, y_pos - 10),
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1)
+
+ if depth_analysis:
+ # Display depth info
+ info_text = f"Mean: {depth_analysis['mean_depth']:.3f} | " \
+ f"Std: {depth_analysis['std_depth']:.3f} | " \
+ f"Area: {depth_analysis['area_pixels']}px"
+ cv2.putText(vis, info_text,
+ (10, 30), cv2.FONT_HERSHEY_SIMPLEX,
+ 0.6, (255, 255, 255), 2)
+
+ return vis
+
+def show_silhouette_analysis(frame, depth_map, pig_mask, masked_depth, anomaly_map=None):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ st.image(frame, caption="Original Frame")
+ if pig_mask is not None:
+ st.image(pig_mask, caption="Sow Segmentation Mask", clamp=True)
+ else:
+ st.warning("No sow detected in this frame")
+
+ with col2:
+ st.image(depth_map, caption="Depth Map", clamp=True)
+ if masked_depth is not None:
+ st.image(masked_depth, caption="Sow Depth Analysis", clamp=True)
+
+
+def render_depth_analysis(depth_analysis):
+ """Render depth analysis visualizations with proper error handling"""
+ if not depth_analysis or len(depth_analysis) == 0:
+ st.warning("No depth analysis data available")
+ return
+
+ # Create DataFrame with proper validation
+ valid_entries = [x for x in depth_analysis if x is not None and 'mean_depth' in x]
+ if not valid_entries:
+ st.error("No valid depth analysis entries containing 'mean_depth'")
+ return
+
+ df = pd.DataFrame(valid_entries)
+
+ with st.expander("📊 Advanced Depth Analysis", expanded=True):
+ tab1, tab2, tab3 = st.tabs(["Distributions", "Temporal Evolution", "Percentiles"])
+
+ with tab1:
+ st.subheader("Global Distribution")
+ try:
+ fig = px.histogram(df, x="mean_depth", nbins=30,
+ title="Mean Depth Distribution")
+ st.plotly_chart(fig, use_container_width=True)
+ except ValueError as e:
+ st.error(f"Could not create histogram: {str(e)}")
+ st.write("Debug data:", df.columns.tolist())
+
+ try:
+ fig2 = px.box(df, y="std_depth", title="Depth Dispersion (STD)")
+ st.plotly_chart(fig2, use_container_width=True)
+ except ValueError as e:
+ st.error(f"Could not create box plot: {str(e)}")
+
+ with tab2:
+ st.subheader("Temporal Curves")
+ try:
+ fig = px.line(df, y=["mean_depth", "min_depth", "max_depth"],
+ markers=True, title="Depth Evolution")
+ st.plotly_chart(fig, use_container_width=True)
+ except ValueError as e:
+ st.error(f"Could not create line plot: {str(e)}")
+
+ if "anomaly_pixels" in df.columns:
+ try:
+ fig2 = px.line(df, y="anomaly_pixels", markers=True,
+ title="Percentage of Anomalous Pixels")
+ st.plotly_chart(fig2, use_container_width=True)
+ except ValueError as e:
+ st.error(f"Could not create anomaly plot: {str(e)}")
+
+ with tab3:
+ st.subheader("Percentile Evolution")
+ percentiles = ["percentiles.5", "percentiles.25", "percentiles.50",
+ "percentiles.75", "percentiles.95"]
+ valid_percentiles = [p for p in percentiles if p in df.columns]
+
+ if valid_percentiles:
+ try:
+ fig = px.line(df, y=valid_percentiles, markers=True,
+ title="Depth Percentile Evolution")
+ st.plotly_chart(fig, use_container_width=True)
+ except ValueError as e:
+ st.error(f"Could not create percentile plot: {str(e)}")
+ else:
+ st.warning("No valid percentile data available")
+
+# Update visualization.py
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+
+def show_3d_depth_visualization(depth_map, pig_mask):
+ """Create interactive 3D depth visualization"""
+ y, x = np.where(pig_mask == 255)
+ z = depth_map[y, x]
+
+ fig = go.Figure(data=[go.Scatter3d(
+ x=x,
+ y=y,
+ z=z,
+ mode='markers',
+ marker=dict(
+ size=3,
+ color=z,
+ colorscale='Viridis',
+ opacity=0.8
+ )
+ )])
+
+ fig.update_layout(
+ scene=dict(
+ xaxis_title='X Position',
+ yaxis_title='Y Position',
+ zaxis_title='Depth',
+ aspectratio=dict(x=1, y=1, z=0.7)
+ ),
+ title='3D Depth Visualization of Pig',
+ height=700
+ )
+
+ st.plotly_chart(fig, use_container_width=True)
+
+def show_regional_analysis(depth_analysis):
+ """Visualize regional depth differences"""
+ if not depth_analysis:
+ return
+
+ regions = ['head', 'middle', 'rear', 'left', 'right']
+ region_data = []
+
+ for region in regions:
+ if f'{region}_mean' in depth_analysis:
+ region_data.append({
+ 'region': region,
+ 'mean': depth_analysis[f'{region}_mean'],
+ 'std': depth_analysis[f'{region}_std']
+ })
+
+ if not region_data:
+ return
+
+ df = pd.DataFrame(region_data)
+
+ fig = px.bar(df, x='region', y='mean', error_y='std',
+ title='Regional Depth Analysis',
+ labels={'mean': 'Mean Depth', 'region': 'Body Region'})
+
+ st.plotly_chart(fig, use_container_width=True)
+
+def show_symmetry_analysis(depth_analysis):
+ """Visualize left-right symmetry"""
+ if not depth_analysis or 'symmetry_score' not in depth_analysis:
+ return
+
+ fig = go.Figure()
+
+ fig.add_trace(go.Indicator(
+ mode="gauge+number",
+ value=depth_analysis['symmetry_score'],
+ title={'text': "Symmetry Score (lower is better)"},
+ gauge={'axis': {'range': [0, 1]},
+ 'steps': [
+ {'range': [0, 0.1], 'color': "green"},
+ {'range': [0.1, 0.3], 'color': "yellow"},
+ {'range': [0.3, 1], 'color': "red"}],
+ 'threshold': {'line': {'color': "black", 'width': 4},
+ 'thickness': 0.75,
+ 'value': 0.2}}))
+
+ st.plotly_chart(fig, use_container_width=True)
+
diff --git a/requirements.txt b/requirements.txt
index 8698f1bc..8da3a9ed 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,23 @@
gradio_imageslider
-gradio==4.29.0
+gradio
matplotlib
-opencv-python
torch
torchvision
+streamlit
+streamlit-drawable-canvas
+opencv-python-headless
+numpy
+Pillow
+fpdf
+plotly
+rasterio
+scikit-image
+fpdf2
+pyvista
+stpyvista
+streamlit-echarts
+requests
+streamlit-autorefresh
+streamlit-image-coordinates
+streamlit-webrtc
+streamlit-lottie
diff --git a/streamlit_app/pipeline.py b/streamlit_app/pipeline.py
new file mode 100644
index 00000000..f5749f02
--- /dev/null
+++ b/streamlit_app/pipeline.py
@@ -0,0 +1,18 @@
+import cv2
+import numpy as np
+import streamlit_depth_client as st
+import torch
+
+from .config import DEVICE
+
+
+@st.cache_data(show_spinner="Estimating depth …")
+def infer_depth(
+ _model, # leading underscore prevents Streamlit hashing
+ image_rgb: np.ndarray,
+ input_size: int,
+) -> np.ndarray:
+ img_bgr = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)
+ with torch.no_grad(), torch.cuda.amp.autocast(enabled=DEVICE == "cuda"):
+ depth = _model.infer_image(img_bgr, input_size=input_size)
+ return depth.astype("float32")
diff --git a/streamlit_depth_client.py b/streamlit_depth_client.py
new file mode 100644
index 00000000..c41f209e
--- /dev/null
+++ b/streamlit_depth_client.py
@@ -0,0 +1,679 @@
+import streamlit as st
+import cv2
+import numpy as np
+import pandas as pd
+import torch
+import os
+import tempfile
+import time
+import requests
+import io
+from contextlib import contextmanager
+from streamlit_image_coordinates import streamlit_image_coordinates
+from streamlit_echarts import st_echarts
+from PIL import Image
+from scipy import ndimage
+from sklearn.cluster import DBSCAN
+from collections import defaultdict
+
+# ───────────────────── MODELO DE PROFUNDIDAD ─────────────────────
+try:
+ from depth_anything_v2.dpt import DepthAnythingV2
+except ImportError:
+ st.error("Dependencia no encontrada: 'depth_anything_v2'. Por favor, instálala para continuar.")
+ st.code("pip install git+https://github.com/LiheYoung/Depth-Anything-V2.git")
+ st.stop()
+
+# ───────────────────── CONFIG APP ─────────────────────
+st.set_page_config(page_title="PigDepthTracker", layout="wide",
+ initial_sidebar_state="collapsed")
+
+MAX_VIDEO_DURATION = 300
+MAX_VIDEO_SIZE_MB = 100
+SUPPORTED_FORMATS = ["mp4", "mov", "avi"]
+RECORDING_SERVER = "http://192.168.1.42:8000"
+FRAME_POLL_INTERVAL = 2
+MIN_PIG_AREA = 10000
+DEPTH_CHANGE_THRESHOLD = 0.02 # Umbral para cambios significativos de profundidad
+
+# ───────────────────── UTILIDADES AVANZADAS ─────────────────────
+def load_css():
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+@contextmanager
+def card(title: str | None = None):
+ st.markdown(f"{'
'+title+'
' if title else ''}", unsafe_allow_html=True)
+ yield
+ st.markdown("
", unsafe_allow_html=True)
+
+# ───────────────────── GESTIÓN DE ESTADO ─────────────────────
+def init_session_state():
+ if "initialized" in st.session_state:
+ return
+ st.session_state.update({
+ "initialized": True,
+ "video_processed": False,
+ "playing": False,
+ "current_frame_index": 0,
+ "total_frames": 0,
+ "original_frames": [],
+ "depth_maps_raw": [],
+ "depth_maps_colored": [],
+ "pig_masks": [],
+ "pig_contours": [],
+ "pig_bboxes": [],
+ "pig_centroids": [],
+ "depth_changes": [],
+ "metrics_cache": [],
+ "selected_points": [],
+ "volume_analysis_results": None,
+ "point_analysis_results": None,
+ "posture_analysis_results": [],
+ "noise_threshold": 0.01,
+ "previewing": False,
+ "recording": False,
+ "recording_session_id": None,
+ "recorded_frames": 0,
+ "frame_urls": [],
+ "live_frame": None,
+ "processing_recorded": False,
+ "min_pig_area": MIN_PIG_AREA,
+ "depth_change_threshold": DEPTH_CHANGE_THRESHOLD,
+ "pig_tracks": defaultdict(list),
+ "movement_maps": []
+ })
+
+def reset_app_state():
+ sid = st.session_state.get("recording_session_id")
+ for k in list(st.session_state.keys()):
+ del st.session_state[k]
+ init_session_state()
+ if sid:
+ try:
+ requests.post(f"{RECORDING_SERVER}/stop-recording/{sid}", timeout=2)
+ except Exception:
+ pass
+
+# ───────────────────── MODELO DE IA ─────────────────────
+@st.cache_resource(show_spinner="Cargando modelo de IA…")
+def load_model(encoder="vitl"):
+ if torch.cuda.is_available():
+ device = torch.device("cuda")
+ st.info("GPU con CUDA detectada. Usando GPU para el procesamiento.")
+ else:
+ device = torch.device("cpu")
+ st.warning("No se detectó una GPU con CUDA. El modelo se ejecutará en la CPU, lo que será considerablemente más lento.")
+
+ if not os.path.exists("checkpoints"):
+ st.error("Directorio 'checkpoints' no encontrado. Por favor, descarga los modelos y colócalos en esa carpeta.")
+ st.stop()
+
+ ckpt = f"checkpoints/depth_anything_v2_{encoder}.pth"
+ if not os.path.exists(ckpt):
+ st.error(f"Modelo no encontrado: {ckpt}. Asegúrate de que el archivo del modelo está en el directorio 'checkpoints'.")
+ st.stop()
+
+ cfg = {"encoder": encoder, "features": 256, "out_channels": [256, 512, 1024, 1024]}
+ net = DepthAnythingV2(**cfg)
+ net.load_state_dict(torch.load(ckpt, map_location=device))
+ net.to(device).eval()
+ return net, device
+
+# ───────────────────── LÓGICA DE ANÁLISIS ─────────────────────
+def predict_depth(model, device, img_rgb):
+ with torch.no_grad():
+ raw = model.infer_image(cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR))
+ norm = cv2.normalize(raw, None, 0, 1, cv2.NORM_MINMAX, cv2.CV_32F)
+ colored = cv2.cvtColor(cv2.applyColorMap((norm*255).astype(np.uint8), cv2.COLORMAP_VIRIDIS), cv2.COLOR_BGR2RGB)
+ metrics = dict(min=float(raw.min()), max=float(raw.max()), mean=float(raw.mean()), std=float(raw.std()), median=float(np.median(raw)))
+ return raw, metrics, colored
+
+def calculate_depth_changes(depth_maps):
+ """Calcula los cambios de profundidad entre frames consecutivos"""
+ changes = []
+ for i in range(1, len(depth_maps)):
+ diff = np.abs(depth_maps[i] - depth_maps[i-1])
+ changes.append(diff)
+ return changes
+
+def detect_moving_objects(depth_change, threshold=DEPTH_CHANGE_THRESHOLD):
+ """Detecta objetos en movimiento basado en cambios de profundidad"""
+ # Umbralizar para obtener regiones con cambios significativos
+ _, thresh = cv2.threshold(depth_change, threshold, 1.0, cv2.THRESH_BINARY)
+ movement_mask = (thresh * 255).astype(np.uint8)
+
+ # Operaciones morfológicas para limpiar la máscara
+ kernel = np.ones((5, 5), np.uint8)
+ movement_mask = cv2.morphologyEx(movement_mask, cv2.MORPH_OPEN, kernel)
+ movement_mask = cv2.morphologyEx(movement_mask, cv2.MORPH_CLOSE, kernel)
+
+ return movement_mask
+
+def cluster_movement_regions(movement_mask, min_area=MIN_PIG_AREA):
+ """Agrupa regiones de movimiento para formar objetos completos"""
+ # Encontrar contornos
+ contours, _ = cv2.findContours(movement_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+
+ # Crear máscara vacía
+ object_mask = np.zeros_like(movement_mask)
+
+ # Dibujar todos los contornos significativos
+ for contour in contours:
+ if cv2.contourArea(contour) > min_area:
+ cv2.drawContours(object_mask, [contour], -1, 255, cv2.FILLED)
+
+ # Conectar regiones cercanas
+ object_mask = cv2.dilate(object_mask, np.ones((15, 15), np.uint8), iterations=1)
+ object_mask = cv2.erode(object_mask, np.ones((15, 15), np.uint8), iterations=1)
+
+ return object_mask
+
+def refine_pig_mask(object_mask, depth_map):
+ """Refina la máscara del cerdo usando información de profundidad"""
+ # Encontrar contornos en la máscara de objeto
+ contours, _ = cv2.findContours(object_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ if not contours:
+ return None, None, None
+
+ # Tomar el contorno más grande (presumiblemente el cerdo)
+ main_contour = max(contours, key=cv2.contourArea)
+
+ # Crear máscara del cerdo
+ pig_mask = np.zeros_like(object_mask)
+ cv2.drawContours(pig_mask, [main_contour], -1, 255, cv2.FILLED)
+
+ # Obtener bounding box
+ x, y, w, h = cv2.boundingRect(main_contour)
+
+ # Calcular centroide
+ M = cv2.moments(main_contour)
+ cx = int(M["m10"] / M["m00"]) if M["m00"] != 0 else 0
+ cy = int(M["m01"] / M["m00"]) if M["m00"] != 0 else 0
+
+ return pig_mask, (x, y, w, h), (cx, cy)
+
+def analyze_pig_posture(depth_map, mask):
+ """Analiza la postura del cerdo basado en la distribución de profundidad"""
+ if mask is None or depth_map is None:
+ return None
+
+ # Calcular centroide
+ moments = cv2.moments(mask.astype(np.uint8))
+ if moments["m00"] == 0:
+ return None
+
+ cx = int(moments["m10"] / moments["m00"])
+ cy = int(moments["m01"] / moments["m00"])
+
+ # Dividir en regiones izquierda/derecha
+ left_mask = mask.copy()
+ left_mask[:, cx:] = 0
+ right_mask = mask.copy()
+ right_mask[:, :cx] = 0
+
+ # Calcular profundidad media en cada región
+ left_depth = np.mean(depth_map[left_mask > 0]) if np.any(left_mask) else 0
+ right_depth = np.mean(depth_map[right_mask > 0]) if np.any(right_mask) else 0
+
+ # Calcular asimetría
+ asymmetry = abs(left_depth - right_depth)
+
+ return {
+ "centroid": (cx, cy),
+ "left_depth": left_depth,
+ "right_depth": right_depth,
+ "asymmetry": asymmetry
+ }
+
+def track_pig(centroids, frame_idx):
+ """Seguimiento del cerdo entre frames usando centroides"""
+ if frame_idx == 0:
+ return 0
+
+ last_centroid = st.session_state.pig_centroids[frame_idx-1]
+ current_centroid = centroids
+
+ # Distancia euclidiana
+ distance = np.sqrt((current_centroid[0] - last_centroid[0])**2 +
+ (current_centroid[1] - last_centroid[1])**2)
+
+ # Si la distancia es grande, podría ser un error de detección
+ if distance > 100: # Umbral de distancia máxima
+ return last_centroid # Mantener posición anterior
+
+ return current_centroid
+
+# ───────────────────── PROCESAMIENTO DE VÍDEO ─────────────────────
+def validate_video_file(f):
+ if f.size > MAX_VIDEO_SIZE_MB * 1024 * 1024:
+ st.error(f"El vídeo no puede superar los {MAX_VIDEO_SIZE_MB} MB."); return False
+ if f.name.split('.')[-1].lower() not in SUPPORTED_FORMATS:
+ st.error(f"Formato no soportado. Sube {', '.join(SUPPORTED_FORMATS)}."); return False
+ return True
+
+def extract_frames(path):
+ frames=[]; cap=cv2.VideoCapture(path); fps=cap.get(cv2.CAP_PROP_FPS); n=int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
+ if fps <= 0 or n <= 0: st.error("No se pudo leer la información del vídeo."); return []
+ if n / fps > MAX_VIDEO_DURATION: st.error(f"El vídeo no puede durar más de {MAX_VIDEO_DURATION} segundos."); return []
+ prog = st.progress(0., text="Extrayendo frames...")
+ while True:
+ ok, f = cap.read()
+ if not ok: break
+ frames.append(cv2.cvtColor(f, cv2.COLOR_BGR2RGB))
+ prog.progress(len(frames) / n, text=f"Extrayendo frame {len(frames)}/{n}")
+ cap.release(); return frames
+
+def _store_frame(model, device, img, frame_idx):
+ """Procesa y almacena un frame con detección de cerdo"""
+ raw, m, clr = predict_depth(model, device, img)
+
+ # Guardar datos básicos
+ st.session_state.original_frames.append(img)
+ st.session_state.depth_maps_raw.append(raw)
+ st.session_state.depth_maps_colored.append(clr)
+ st.session_state.metrics_cache.append(m)
+
+ # Para el primer frame, inicializar sin cerdo
+ if frame_idx == 0:
+ st.session_state.pig_masks.append(np.zeros_like(raw, dtype=np.uint8))
+ st.session_state.pig_contours.append(None)
+ st.session_state.pig_bboxes.append(None)
+ st.session_state.pig_centroids.append((0, 0))
+ return
+
+ # Calcular cambio de profundidad desde el frame anterior
+ depth_change = np.abs(raw - st.session_state.depth_maps_raw[frame_idx-1])
+ st.session_state.depth_changes.append(depth_change)
+ st.session_state.movement_maps.append(depth_change)
+
+ # Detectar movimiento
+ movement_mask = detect_moving_objects(depth_change, st.session_state.depth_change_threshold)
+
+ # Agrupar regiones de movimiento
+ object_mask = cluster_movement_regions(movement_mask, st.session_state.min_pig_area)
+
+ # Refinar máscara del cerdo
+ pig_mask, bbox, centroid = refine_pig_mask(object_mask, raw)
+
+ # Seguimiento del cerdo
+ if centroid:
+ tracked_centroid = track_pig(centroid, frame_idx)
+ else:
+ tracked_centroid = st.session_state.pig_centroids[-1] if st.session_state.pig_centroids else (0, 0)
+
+ # Guardar resultados
+ st.session_state.pig_masks.append(pig_mask if pig_mask is not None else np.zeros_like(raw, dtype=np.uint8))
+ st.session_state.pig_contours.append(None) # Se calcularán solo cuando sea necesario
+ st.session_state.pig_bboxes.append(bbox)
+ st.session_state.pig_centroids.append(tracked_centroid)
+
+ # Analizar postura
+ posture_data = analyze_pig_posture(raw, pig_mask)
+ st.session_state.posture_analysis_results.append(posture_data)
+
+def process_video_file(up_file):
+ if not validate_video_file(up_file): return
+ reset_app_state()
+ model, device = load_model()
+ with st.status("Procesando vídeo…", expanded=True) as s:
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as t:
+ t.write(up_file.read()); path = t.name
+ frames = extract_frames(path); os.remove(path)
+ if not frames: s.update(label="Extracción de frames fallida.", state="error"); return
+ h, w = frames[0].shape[:2];
+ st.session_state.total_frames = len(frames)
+ prog = st.progress(0., "Analizando profundidad y movimiento…")
+ for i, f in enumerate(frames):
+ _store_frame(model, device, f, i)
+ prog.progress((i + 1) / len(frames), f"Analizando frame {i+1}/{len(frames)}")
+ st.session_state.video_processed = True
+ s.update(label="Análisis completo.", state="complete"); st.rerun()
+
+# ───────────────────── VISUALIZACIÓN ─────────────────────
+def visualize_pig_detection(frame, pig_mask, centroid, bbox):
+ """Crea una visualización de la detección del cerdo"""
+ # Crear overlay de máscara
+ mask_rgb = cv2.cvtColor(pig_mask, cv2.COLOR_GRAY2BGR)
+ overlay = cv2.addWeighted(frame, 0.7, mask_rgb, 0.3, 0)
+
+ # Dibujar bounding box
+ if bbox:
+ x, y, w, h = bbox
+ cv2.rectangle(overlay, (x, y), (x+w, y+h), (0, 255, 0), 2)
+
+ # Dibujar centroide
+ if centroid and centroid != (0, 0):
+ cv2.circle(overlay, centroid, 8, (0, 0, 255), -1)
+
+ return overlay
+
+# ───────────────────── APLICACIÓN PRINCIPAL ─────────────────────
+init_session_state()
+load_css()
+
+st.markdown("""
+
+ Pig
+ Depth
+ Tracker
+
+
+ Detección y Análisis de Cerdos mediante Cambios de Profundidad
+
+""", unsafe_allow_html=True)
+
+# --- PANTALLA INICIAL ---
+if not st.session_state.video_processed:
+ cols = st.columns([1, 1.5, 1])
+ with cols[1]:
+ with card("Iniciar Análisis"):
+ tab_up, tab_live = st.tabs(["Subir Vídeo", "Grabación en Directo"])
+
+ with tab_up:
+ up = st.file_uploader("Selecciona un vídeo de cámara cenital",
+ type=SUPPORTED_FORMATS,
+ label_visibility="collapsed")
+
+ if up:
+ st.session_state.depth_change_threshold = st.slider(
+ "Umbral de Cambio de Profundidad",
+ min_value=0.001,
+ max_value=0.1,
+ value=DEPTH_CHANGE_THRESHOLD,
+ step=0.001,
+ format="%.3f",
+ help="Ajusta la sensibilidad para detectar cambios de profundidad"
+ )
+
+ st.session_state.min_pig_area = st.slider(
+ "Área Mínima del Cerdo (píxeles)",
+ min_value=1000,
+ max_value=30000,
+ value=MIN_PIG_AREA,
+ step=500
+ )
+
+ if st.button("Analizar Vídeo", use_container_width=True, type="primary"):
+ process_video_file(up)
+
+ with tab_live:
+ st.info("Funcionalidad de grabación en directo disponible en próximas versiones")
+ st.image("https://via.placeholder.com/600x300?text=Cámara+No+Disponible",
+ use_container_width=True)
+
+ st.markdown(f"""
+
+
Requisitos del Sistema
+
+ • GPU con CUDA • Vídeo ≤{MAX_VIDEO_DURATION}s •
+ Archivo ≤{MAX_VIDEO_SIZE_MB} MB • Visión cenital clara
+
+
+ """, unsafe_allow_html=True)
+
+# --- PANTALLA DE RESULTADOS ---
+else:
+ with st.container():
+ h1, h2 = st.columns([1, 0.2])
+ h1.markdown(f"""
+ Resultados del Análisis
+
+ {st.session_state.total_frames} frames procesados |
+ Umbral de cambio: {st.session_state.depth_change_threshold:.3f}
+
+ """, unsafe_allow_html=True)
+
+ h2.button("Nuevo Análisis", on_click=reset_app_state, use_container_width=True)
+
+ tab_view, tab_ana = st.tabs(["Visor", "Análisis"])
+
+ with tab_view:
+ if st.session_state.original_frames:
+ with card("Visor de Secuencia"):
+ c1, c2, c3 = st.columns([0.15, 1, 0.2])
+ c1.button("▶️ Play" if not st.session_state.playing else "⏸️ Pausa",
+ on_click=lambda: st.session_state.update(playing=not st.session_state.playing),
+ use_container_width=True)
+
+ val = c2.slider("Frame", 0, st.session_state.total_frames - 1,
+ st.session_state.current_frame_index,
+ label_visibility="collapsed")
+
+ if val != st.session_state.current_frame_index:
+ st.session_state.current_frame_index = val
+ st.session_state.playing = False
+
+ c3.markdown(f"{val + 1} / {st.session_state.total_frames}
",
+ unsafe_allow_html=True)
+
+ if st.session_state.total_frames > 0:
+ frame_idx = st.session_state.current_frame_index
+ frame = st.session_state.original_frames[frame_idx]
+ depth = st.session_state.depth_maps_colored[frame_idx]
+ pig_mask = st.session_state.pig_masks[frame_idx]
+ centroid = st.session_state.pig_centroids[frame_idx]
+ bbox = st.session_state.pig_bboxes[frame_idx]
+
+ # Visualización de detección
+ detection_viz = visualize_pig_detection(frame, pig_mask, centroid, bbox)
+
+ # Visualización de movimiento
+ movement_viz = None
+ if frame_idx > 0 and frame_idx < len(st.session_state.movement_maps):
+ movement_map = st.session_state.movement_maps[frame_idx-1]
+ movement_viz = cv2.normalize(movement_map, None, 0, 255, cv2.NORM_MINMAX)
+ movement_viz = cv2.applyColorMap(movement_viz.astype(np.uint8), cv2.COLORMAP_JET)
+
+ # Mostrar resultados
+ col1, col2 = st.columns(2)
+ with col1:
+ st.image(detection_viz, caption="Detección del Cerdo",
+ use_container_width=True, clamp=True)
+
+ if movement_viz is not None:
+ st.image(movement_viz, caption="Mapa de Movimiento (Cambios de Profundidad)",
+ use_container_width=True, clamp=True,
+ output_format="JPEG")
+
+ with col2:
+ st.image(depth, caption="Mapa de Profundidad",
+ use_container_width=True)
+
+ # Mostrar análisis de postura si está disponible
+ if frame_idx < len(st.session_state.posture_analysis_results):
+ posture = st.session_state.posture_analysis_results[frame_idx]
+ if posture:
+ st.markdown("**Análisis de Postura**")
+ cols = st.columns(2)
+ cols[0].metric("Asimetría", f"{posture['asymmetry']:.4f}")
+ cols[1].metric("Centroide", f"({posture['centroid'][0]}, {posture['centroid'][1]})")
+
+ with tab_ana:
+ if st.session_state.original_frames and st.session_state.posture_analysis_results:
+ # Análisis de asimetría a lo largo del tiempo
+ with card("Evolución de la Asimetría"):
+ asymmetry_data = []
+ for i, posture in enumerate(st.session_state.posture_analysis_results):
+ if posture:
+ asymmetry_data.append({
+ "frame": i,
+ "asymmetry": posture["asymmetry"],
+ "left_depth": posture["left_depth"],
+ "right_depth": posture["right_depth"]
+ })
+
+ if asymmetry_data:
+ df = pd.DataFrame(asymmetry_data)
+
+ # Calcular umbral de alerta
+ mean_asym = df["asymmetry"].mean()
+ std_asym = df["asymmetry"].std()
+ alert_threshold = mean_asym + 2 * std_asym
+
+ # Gráfico de asimetría
+ st.line_chart(df.set_index("frame")["asymmetry"])
+
+ # Gráfico de profundidades izquierda/derecha
+ st.line_chart(df.set_index("frame")[["left_depth", "right_depth"]])
+
+ # Detectar frames con posible problema
+ alert_frames = df[df["asymmetry"] > alert_threshold]
+ if not alert_frames.empty:
+ st.warning(f"Se detectaron posibles problemas de postura en {len(alert_frames)} frames")
+ st.write("Frames con asimetría significativa:")
+ st.dataframe(alert_frames)
+
+ # Análisis de trayectoria
+ with card("Trayectoria del Cerdo"):
+ if st.session_state.pig_centroids:
+ centroids = [c for c in st.session_state.pig_centroids if c != (0, 0)]
+ if centroids:
+ df_traj = pd.DataFrame(centroids, columns=["x", "y"])
+ df_traj["frame"] = df_traj.index
+
+ # Crear gráfico de trayectoria
+ st.scatter_chart(df_traj, x="x", y="y")
+
+ # Calcular distancia recorrida
+ total_distance = 0
+ for i in range(1, len(centroids)):
+ x1, y1 = centroids[i-1]
+ x2, y2 = centroids[i]
+ distance = np.sqrt((x2-x1)**2 + (y2-y1)**2)
+ total_distance += distance
+
+ st.metric("Distancia Total Recorrida", f"{total_distance:.2f} píxeles")
+
+# ───────────────────── BUCLE DE ACTUALIZACIÓN ─────────────────────
+if st.session_state.get("playing"):
+ time.sleep(0.05)
+ if st.session_state.total_frames > 0:
+ next_idx = (st.session_state.current_frame_index + 1) % st.session_state.total_frames
+ st.session_state.current_frame_index = next_idx
+ st.rerun()
\ No newline at end of file
diff --git a/video_depth_vis/basketball.mp4 b/video_depth_vis/basketball.mp4
new file mode 100644
index 00000000..1cf60f82
Binary files /dev/null and b/video_depth_vis/basketball.mp4 differ
diff --git a/video_depth_vis/ferris_wheel.mp4 b/video_depth_vis/ferris_wheel.mp4
new file mode 100644
index 00000000..5f2b0660
Binary files /dev/null and b/video_depth_vis/ferris_wheel.mp4 differ
diff --git a/vis_video_depth/tmpnobxt03_.mp4 b/vis_video_depth/tmpnobxt03_.mp4
new file mode 100644
index 00000000..f599899c
Binary files /dev/null and b/vis_video_depth/tmpnobxt03_.mp4 differ