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: -Paper PDF -Project Page - -Benchmark -
+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 -![teaser](assets/teaser.png) - - -## 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
+ + + + + + +
+ +
+
+

Original Frame (Click to Select Points)

+ +
+

Depth Map

+
+
Minimum Depth
0.0000
+
Maximum Depth
0.0000
+
Mean Depth
0.0000
+
Standard Deviation
0.0000
+
+

Depth Distribution

+
+ + + + + + + + + \ 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