Visitar URL original
Refactor VideoProcessor class by C-Achard · Pull Request #3358 · DeepLabCut/DeepLabCut · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3e47c96
Sort and stringify video list
C-Achard Jun 5, 2026
108beba
Convert video paths to strings for vp calls
C-Achard Jun 5, 2026
257a8c9
Revert "Sort and stringify video list"
C-Achard Jun 9, 2026
902aa1d
Add tests for VideoProcessorCV
C-Achard Jun 9, 2026
d3ed1b4
Turn VideoProcessor into ABC with abstract methods
C-Achard Jun 9, 2026
bffcb92
Use clip properties for height/width/fps
C-Achard Jun 9, 2026
f338f3c
Add properties and docs to video processor
C-Achard Jun 9, 2026
f18d166
Avoid str() casts when calling vp
C-Achard Jun 9, 2026
31f8985
Initialize video attrs and switch to logging
C-Achard Jun 9, 2026
203b131
Change logging to exception
C-Achard Jun 9, 2026
ef1d2e2
Test VideoProcessorCV accepts Path and stores str
C-Achard Jun 9, 2026
f0d4fd4
Add type annotations to VideoProcessor.__init__
C-Achard Jun 9, 2026
17de1f4
Address review comments
C-Achard Jun 10, 2026
09dd8b4
Refactor VideoProcessor API to video_fps/h/w
C-Achard Jun 10, 2026
5e24647
Mark legacy VideoProcessor accessors as deprecated
C-Achard Jun 10, 2026
84c60a2
Disable deprecated decorator on FPS property
C-Achard Jun 10, 2026
df89520
Comment on deprecations
C-Achard Jun 10, 2026
fbb6352
Fix incorrect property usage
C-Achard Jun 10, 2026
f07ba9e
Rename VideoProcessor.video_fps to fps
C-Achard Jun 11, 2026
06a7521
Rename VideoProcessor h/w to height/width
C-Achard Jun 12, 2026
8e86e02
Fix missing renaming of properties in make_labeled_video
C-Achard Jun 15, 2026
1496ff2
Merge branch 'main' into cy/fix-video-reader-set-error
MMathisLab Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions deeplabcut/utils/make_labeled_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ def CreateVideo(
if displaycropped:
ny, nx = y2 - y1, x2 - x1
else:
ny, nx = clip.height(), clip.width()
ny, nx = clip.height, clip.width

fps = clip.fps()
fps = clip.fps
if isinstance(fps, float):
if fps * 1000 > 65535:
fps = round(fps)
Comment thread
C-Achard marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -252,9 +252,9 @@ def CreateVideoSlow(
if displaycropped:
ny, nx = y2 - y1, x2 - x1
else:
ny, nx = clip.height(), clip.width()
ny, nx = clip.height, clip.width

fps = clip.fps()
fps = clip.fps
if outputframerate is None: # by def. same as input rate.
outputframerate = fps

Expand Down Expand Up @@ -919,7 +919,7 @@ def proc_video(
skeleton_color=skeleton_color,
color_by=color_by,
colormap=cfg["colormap"],
fps=clip.fps(),
fps=clip.fps,
)
clip.close()
elif not fastmode:
Expand Down Expand Up @@ -1287,7 +1287,7 @@ def create_video_with_all_detections(
pcutoff = cfg["pcutoff"]
dotsize = cfg["dotsize"]
clip = vp(fname=video, sname=outputname, codec="mp4v")
ny, nx = clip.height(), clip.width()
ny, nx = clip.height, clip.width

bboxes_pcutoff = (
metadata.get("data", {})
Expand Down
167 changes: 147 additions & 20 deletions deeplabcut/utils/video_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,40 @@
i.e. 'XVID'
"""

import logging
from abc import ABC, abstractmethod

import cv2
import numpy as np

logger = logging.getLogger(__name__)


class VideoProcessor:
class VideoProcessor(ABC):
"""Base class for a video processing unit, implementation is required for video
loading and saving.

sh and sw are the output height and width respectively.
"""

def __init__(self, fname="", sname="", nframes=-1, fps=None, codec="X264", sh="", sw=""):
def __init__(
self,
fname: str = "",
Comment thread
C-Achard marked this conversation as resolved.
Resolved
sname: str = "",
nframes: int = -1,
fps: float = None,
Comment thread
C-Achard marked this conversation as resolved.
Resolved
codec: str = "X264",
sh: int = "",
sw: int = "",
Comment thread
C-Achard marked this conversation as resolved.
Resolved
):
self._fname = None
self._sname = None
self.FPS = None
self.vid = None
self.svid = None
self.sh = 0
self.sw = 0
###
Comment thread
C-Achard marked this conversation as resolved.
Resolved
Comment thread
C-Achard marked this conversation as resolved.
Resolved
self.fname = fname
Comment thread
C-Achard marked this conversation as resolved.
Resolved
self.sname = sname
self.nframes = nframes
Expand All @@ -58,9 +80,11 @@ def __init__(self, fname="", sname="", nframes=-1, fps=None, codec="X264", sh=""
self.svid = self.create_video()

except Exception as ex:
print("Error: %s", ex)
logger.exception("VideoProcessor initialization failed: %s", ex)

if fps is not None: # Overwrite the video's FPS
# NOTE @C-Achard 2026-06-09 improving checks here might break old API
# same for raising on missing FPS
self.FPS = fps

def load_frame(self):
Expand All @@ -69,81 +93,184 @@ def load_frame(self):
self.i += 1
return frame

@property
def fname(self):
return self._fname

@fname.setter
def fname(self, value):
self._fname = str(value)
Comment thread
C-Achard marked this conversation as resolved.
Resolved

@property
def sname(self):
return self._sname

@sname.setter
def sname(self, value):
self._sname = str(value)

@property
def height(self):
return self.h

@property
def width(self):
return self.w

@property
def fps(self):
return self.FPS

@property
def counter(self):
return self.i

@property
def frame_count(self):
return self.nframes

Comment thread
C-Achard marked this conversation as resolved.
Resolved
@abstractmethod
def get_video(self):
"""Implement your own."""
pass
raise NotImplementedError("Implement your own get_video method.")

@abstractmethod
def get_info(self):
"""Implement your own."""
pass
raise NotImplementedError("Implement your own get_info method.")

@abstractmethod
def create_video(self):
"""Implement your own."""
pass
raise NotImplementedError("Implement your own create_video method.")

@abstractmethod
def _read_frame(self):
"""Implement your own."""
pass
raise NotImplementedError("Implement your own _read_frame method.")

@abstractmethod
def save_frame(self, frame):
"""Implement your own."""
pass
raise NotImplementedError("Implement your own save_frame method.")

@abstractmethod
def close(self):
"""Implement your own."""
pass
raise NotImplementedError("Implement your own close method.")


class VideoProcessorCV(VideoProcessor):
"""OpenCV implementation of VideoProcessor requires opencv-python==3.4.0.12."""
"""OpenCV-backed video reader and writer.

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
This implementation uses cv2.VideoCapture for reading videos and
cv2.VideoWriter for writing videos. Frames returned by
`load_frame` are converted from OpenCV's native BGR channel order to
RGB channel order. Frames passed to `save_frame` are expected to be in
RGB order and are converted back to BGR before writing.

Attributes:
fname (str): Path to the input video. If empty, no input video is opened.
sname (str): Path to the output video. If empty, no output video is created.
nframes (int): Number of frames to process. If initialized as ``-1``,
it is replaced by the total number of frames reported by OpenCV.
codec (str): FourCC codec string used when creating the output video.
h (int): Input video height in pixels.
w (int): Input video width in pixels.
nc (int): Number of channels. This implementation uses ``3``.
i (int): Number of frames successfully loaded through ``load_frame()``.
FPS (float): Frames per second reported by OpenCV, or the user-provided
override.
sh (int): Output video height in pixels.
sw (int): Output video width in pixels.
vid (cv2.VideoCapture | None): OpenCV video reader.
svid (cv2.VideoWriter | None): OpenCV video writer.
"""

def get_video(self):
"""Open the input video with OpenCV.

Returns:
cv2.VideoCapture: OpenCV video capture object for ``self.fname``.
"""
return cv2.VideoCapture(self.fname)

def get_info(self):
"""Populate metadata from the OpenCV video reader.

Sets:
self.w: Frame width in pixels.
self.h: Frame height in pixels.
self.nframes: Number of frames to process.
self.FPS: Frames per second reported by OpenCV.
self.nc: Number of channels, always ``3``.

Notes:
If ``self.nframes`` is ``-1`` or greater than the total number of
frames reported by OpenCV, it is replaced by OpenCV's frame count.
"""
self.w = int(self.vid.get(cv2.CAP_PROP_FRAME_WIDTH))
self.h = int(self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT))
all_frames = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
self.FPS = self.vid.get(cv2.CAP_PROP_FPS)
self.nc = 3

if self.nframes == -1 or self.nframes > all_frames:
self.nframes = all_frames

def create_video(self):
"""Create an OpenCV video writer.

Returns:
cv2.VideoWriter: OpenCV video writer for ``self.sname``.

Notes:
``self.sw`` and ``self.sh`` are expected to be set by the base class
before this method is called. The codec is interpreted as a FourCC
string, preserving the historical OpenCV behavior.
"""
fourcc = cv2.VideoWriter_fourcc(*self.codec)
return cv2.VideoWriter(self.sname, fourcc, self.FPS, (self.sw, self.sh), True)

def _read_frame(self): # return RGB (rather than BGR)!
# return cv2.cvtColor(np.flip(self.vid.read()[1],2), cv2.COLOR_BGR2RGB)
def _read_frame(self):
"""Read the next video frame.

Returns:
numpy.ndarray | None: The next frame in RGB channel order, or
``None`` if no frame could be read.

Notes:
OpenCV returns BGR frames. This method converts them to RGB using
``np.flip(frame, 2)`` to preserve legacy behavior.
"""
if self.vid is None:
return None

success, frame = self.vid.read()
if not success:
return frame

return np.flip(frame, 2)

def save_frame(self, frame):
"""Write one RGB frame to the output video.

Args:
frame (numpy.ndarray | None): RGB frame to write. ``None`` is ignored.

Notes:
This method preserves the historical behavior of silently ignoring
``None`` frames. Non-``None`` frames are converted from RGB to BGR
before being passed to OpenCV.
"""
if frame is not None:
self.svid.write(np.flip(frame, 2))

Comment thread
C-Achard marked this conversation as resolved.
Resolved
def close(self):
"""Release OpenCV reader and writer resources.

This method is safe to call multiple times. After release, ``self.svid``
and ``self.vid`` are set to ``None`` to avoid accidental reuse of closed
OpenCV handles.
"""
if hasattr(self, "svid") and self.svid is not None:
self.svid.release()
self.svid = None

if hasattr(self, "vid") and self.vid is not None:
self.vid.release()
self.vid = None
Loading
Loading