Source code for endoreg_db.models.metadata.video_meta

from django.db import models
import logging
from pathlib import Path
from typing import Optional, TYPE_CHECKING

# import endoreg_center_id from django settings
from django.conf import settings

# check if endoreg_center_id is set
if not hasattr(settings, "ENDOREG_CENTER_ID"):
    ENDOREG_CENTER_ID = 9999
else:
    ENDOREG_CENTER_ID = settings.ENDOREG_CENTER_ID

# Import the new utility function
from ...utils.ffmpeg_wrapper import get_stream_info

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from ..administration import Center
    from ..medical.hardware import EndoscopyProcessor, Endoscope


# VideoMeta
[docs] class VideoMeta(models.Model): """ Stores technical and contextual metadata related to a video file. Links to hardware (processor, endoscope), center, import details, and FFmpeg technical specs. """ processor = models.ForeignKey( "EndoscopyProcessor", on_delete=models.CASCADE, blank=True, null=True ) endoscope = models.ForeignKey( "Endoscope", on_delete=models.CASCADE, blank=True, null=True ) center = models.ForeignKey("Center", on_delete=models.CASCADE) import_meta = models.OneToOneField( "VideoImportMeta", on_delete=models.CASCADE, blank=True, null=True ) ffmpeg_meta = models.OneToOneField( "FFMpegMeta", on_delete=models.CASCADE, blank=True, null=True )
[docs] @classmethod def create_from_file( cls, video_path: Path, center: "Center", processor: Optional["EndoscopyProcessor"] = None, endoscope: Optional["Endoscope"] = None, save_instance: bool = True, ) -> "VideoMeta": """Create a new VideoMeta from a video file path, initializing FFMpegMeta.""" if not isinstance(video_path, Path): raise TypeError("video_path must be a Path object") if not video_path.exists(): raise FileNotFoundError(f"Video file not found at {video_path}") meta = cls(center=center, processor=processor, endoscope=endoscope) meta.initialize_ffmpeg_meta(video_path) if save_instance: meta.save() logger.info("Created and saved VideoMeta instance PK %s from %s", meta.pk, video_path.name) else: logger.info("Instantiated VideoMeta from %s (not saved yet)", video_path.name) return meta
def __str__(self): """Returns a string summary of the video metadata.""" processor_name = self.processor.name if self.processor is not None else "None" endoscope_name = self.endoscope.name if self.endoscope is not None else "None" center_name = self.center.name if self.center is not None else "None" ffmpeg_meta_str = self.ffmpeg_meta.__str__() import_meta_str = self.import_meta.__str__() result_html = "" result_html += f"Processor: {processor_name}\n" result_html += f"Endoscope: {endoscope_name}\n" result_html += f"Center: {center_name}\n" result_html += f"FFMpeg Meta: {ffmpeg_meta_str}\n" result_html += f"Import Meta: {import_meta_str}\n" return result_html
[docs] def save(self, *args, **kwargs): """Ensures VideoImportMeta exists before saving.""" if self.import_meta is None: self.import_meta = VideoImportMeta.objects.create() super(VideoMeta, self).save(*args, **kwargs)
[docs] def initialize_ffmpeg_meta(self, video_path: Path): """Initializes FFMpeg metadata for the video file if not already done.""" if self.ffmpeg_meta: logger.debug("FFMpegMeta already exists for VideoMeta PK %s. Skipping initialization.", self.pk) return logger.info("Initializing FFMpegMeta for VideoMeta PK %s from %s", self.pk, video_path.name) try: self.ffmpeg_meta = FFMpegMeta.create_from_file(video_path) if self.ffmpeg_meta: logger.info("Successfully created FFMpegMeta PK %s", self.ffmpeg_meta.pk) else: logger.warning("FFMpegMeta creation returned None for %s", video_path.name) except Exception as e: logger.error("Failed to create FFMpegMeta from %s: %s", video_path, e, exc_info=True)
[docs] def update_meta(self, video_path: Path): """Updates the FFMpeg metadata from the file, replacing existing data.""" logger.info("Updating FFMpegMeta for VideoMeta PK %s from %s", self.pk, video_path.name) if self.ffmpeg_meta: logger.debug("Deleting existing FFMpegMeta PK %s before update.", self.ffmpeg_meta.pk) self.ffmpeg_meta.delete() self.ffmpeg_meta = None self.initialize_ffmpeg_meta(video_path)
[docs] def get_endo_roi(self): """Retrieves the endoscope region of interest (ROI) from the associated processor.""" from ..medical.hardware import EndoscopyProcessor processor: EndoscopyProcessor = self.processor endo_roi = processor.get_roi_endoscope_image() return endo_roi
@property def fps(self) -> Optional[float]: """Returns the frame rate (FPS) from the linked FFMpegMeta.""" if not self.ffmpeg_meta: logger.warning("FFMpegMeta not linked for VideoMeta PK %s. Cannot get FPS.", self.pk) return None return self.ffmpeg_meta.fps @property def duration(self) -> Optional[float]: """Returns the duration in seconds from the linked FFMpegMeta.""" return self.ffmpeg_meta.duration if self.ffmpeg_meta else None @property def width(self) -> Optional[int]: """Returns the video width in pixels from the linked FFMpegMeta.""" return self.ffmpeg_meta.width if self.ffmpeg_meta else None @property def height(self) -> Optional[int]: """Returns the video height in pixels from the linked FFMpegMeta.""" return self.ffmpeg_meta.height if self.ffmpeg_meta else None @property def frame_count(self) -> Optional[int]: """Calculates frame count based on duration and FPS from FFMpegMeta.""" if self.ffmpeg_meta and self.ffmpeg_meta.duration is not None and self.ffmpeg_meta.fps is not None and self.ffmpeg_meta.fps > 0: return int(self.ffmpeg_meta.duration * self.ffmpeg_meta.fps) return None
[docs] class FFMpegMeta(models.Model): """ Stores technical video stream information extracted using FFmpeg (ffprobe). """ width = models.IntegerField(null=True, blank=True) height = models.IntegerField(null=True, blank=True) duration = models.FloatField(null=True, blank=True) # Duration in seconds frame_rate_num = models.IntegerField(null=True, blank=True) # Numerator for frame rate frame_rate_den = models.IntegerField(null=True, blank=True) # Denominator for frame rate codec_name = models.CharField(max_length=50, null=True, blank=True) pixel_format = models.CharField(max_length=50, null=True, blank=True) bit_rate = models.BigIntegerField(null=True, blank=True) # Bit rate in bits per second raw_probe_data = models.JSONField(null=True, blank=True) # Store the full JSON output for debugging or future use @property def fps(self) -> Optional[float]: """Calculates and returns the frames per second (FPS) if possible.""" if self.frame_rate_num is not None and self.frame_rate_den is not None and self.frame_rate_den != 0: return self.frame_rate_num / self.frame_rate_den return None
[docs] @classmethod def create_from_file(cls, file_path: Path): """ Creates an FFMpegMeta instance by running ffprobe on the given file path. """ logger.info("Running ffprobe on %s", file_path) probe_data = get_stream_info(file_path) # Use the new utility if not probe_data or "streams" not in probe_data: logger.error("Failed to get valid stream info from ffprobe for %s", file_path) return None video_stream = next((s for s in probe_data["streams"] if s.get("codec_type") == "video"), None) if not video_stream: logger.warning("No video stream found in ffprobe output for %s", file_path) return None # Extract data safely using .get() width = video_stream.get("width") height = video_stream.get("height") duration_str = video_stream.get("duration") duration = float(duration_str) if duration_str else None # Frame rate can be tricky, often represented as "num/den" frame_rate_str = video_stream.get("r_frame_rate") frame_rate_num, frame_rate_den = None, None if frame_rate_str and "/" in frame_rate_str: try: num_str, den_str = frame_rate_str.split('/') frame_rate_num = int(num_str) frame_rate_den = int(den_str) except ValueError: logger.warning("Could not parse frame rate '%s' for %s", frame_rate_str, file_path) codec_name = video_stream.get("codec_name") pixel_format = video_stream.get("pix_fmt") bit_rate_str = video_stream.get("bit_rate") bit_rate = int(bit_rate_str) if bit_rate_str else None try: instance = cls.objects.create( width=width, height=height, duration=duration, frame_rate_num=frame_rate_num, frame_rate_den=frame_rate_den, codec_name=codec_name, pixel_format=pixel_format, bit_rate=bit_rate, raw_probe_data=probe_data, # Store the raw data ) logger.info("Successfully created FFMpegMeta for %s (ID: %d)", file_path, instance.pk) return instance except Exception as e: logger.error("Error creating FFMpegMeta from %s: %s", file_path, e, exc_info=True) return None
def __str__(self): """Returns a string summary of the FFmpeg metadata.""" result_html = "" result_html += f"Width: {self.width}\n" result_html += f"Height: {self.height}\n" result_html += f"Duration: {self.duration}\n" result_html += f"Frame Rate: {self.fps}\n" result_html += f"Codec Name: {self.codec_name}\n" result_html += f"Pixel Format: {self.pixel_format}\n" result_html += f"Bit Rate: {self.bit_rate}\n" return result_html
[docs] class VideoImportMeta(models.Model): """ Stores metadata related to the import and processing status of a video. """ video_anonymized = models.BooleanField(default=False) video_patient_data_detected = models.BooleanField(default=False) outside_detected = models.BooleanField(default=False) patient_data_removed = models.BooleanField(default=False) outside_removed = models.BooleanField(default=False) def __str__(self): """Returns a string summary of the import metadata.""" result_html = "" result_html += f"Video anonymized: {self.video_anonymized}\n" result_html += ( f"Video patient data detected: {self.video_patient_data_detected}\n" ) result_html += f"Outside detected: {self.outside_detected}\n" result_html += f"Patient data removed: {self.patient_data_removed}\n" result_html += f"Outside removed: {self.outside_removed}\n" return result_html