optimization

This commit is contained in:
Arman Jangmiri
2026-07-14 10:52:55 +03:30
parent f1bfc8d321
commit 09b5bdea7e
2 changed files with 67 additions and 15 deletions
+62 -14
View File
@@ -69,8 +69,11 @@ class FirstFrameLastFrameExtractor:
# --- Path A: image batch from another node ---
if images is not None:
first_frame = images[0].unsqueeze(0) # [1, H, W, C]
last_frame = images[-1].unsqueeze(0) # [1, H, W, C]
# .clone() creates small independent tensors so the giant
# parent batch can be garbage-collected immediately after
# this node runs, instead of being kept alive by the views.
first_frame = images[0].unsqueeze(0).clone() # [1, H, W, C]
last_frame = images[-1].unsqueeze(0).clone() # [1, H, W, C]
return (first_frame, last_frame)
# --- Path B: read video file from disk ---
@@ -115,8 +118,12 @@ class FirstFrameLastFrameExtractor:
"""
Read the first and last frames from an open cv2.VideoCapture.
Uses seeking first; falls back to sequential reading if the codec
does not support seeking to the last frame.
Strategy for the last frame:
1. Seek directly to (total_frames - 1).
2. If that fails, binary-search backwards from the end to find
the highest seekable frame (avoids reading every frame).
3. Final fallback: scan forward from the start (only reached
on truly broken containers).
"""
# ---- First frame ----
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
@@ -128,17 +135,23 @@ class FirstFrameLastFrameExtractor:
# ---- Last frame ----
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
last_frame = None
if total_frames > 0:
# Try seeking directly to the last frame
if total_frames > 1:
# Attempt 1: seek straight to last frame
cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
ret, last_frame = cap.read()
if not ret:
last_frame = None
ret, frame = cap.read()
if ret and frame is not None:
last_frame = frame
# Fallback: read sequentially until the end
# Attempt 2: binary-search backwards for highest readable
if last_frame is None:
last_frame = FirstFrameLastFrameExtractor._binary_seek_last(
cap, 0, total_frames - 1
)
# Attempt 3 (rare): sequential scan — only when total_frames
# is unreliable (0 or 1) or binary search also failed.
if last_frame is None:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
while True:
@@ -154,6 +167,28 @@ class FirstFrameLastFrameExtractor:
return first_frame, last_frame
@staticmethod
def _binary_seek_last(cap, lo, hi):
"""
Binary-search for the highest frame index in [lo, hi] that
OpenCV can successfully seek to and decode.
Returns the decoded frame (np.ndarray) or None.
"""
best_frame = None
while lo <= hi:
mid = (lo + hi) // 2
cap.set(cv2.CAP_PROP_POS_FRAMES, mid)
ret, frame = cap.read()
if ret and frame is not None:
best_frame = frame
lo = mid + 1 # try higher
else:
hi = mid - 1 # come back lower
return best_frame
@staticmethod
def _cv2_to_tensor(cv2_frame):
"""
@@ -161,8 +196,21 @@ class FirstFrameLastFrameExtractor:
Returns a torch.float32 tensor with shape [1, H, W, C], values in
[0.0, 1.0], RGB channel order.
Minimizes memory copies:
- In-place BGR→RGB channel swap (no allocation).
- Single float32 conversion via torch (no intermediate numpy copy).
"""
rgb = cv2.cvtColor(cv2_frame, cv2.COLOR_BGR2RGB)
normalized = rgb.astype(np.float32) / 255.0
tensor = torch.from_numpy(normalized).unsqueeze(0) # [1, H, W, C]
# Flip BGR → RGB in-place (reverses last axis, zero-copy view)
rgb = cv2_frame[:, :, ::-1]
# np.ascontiguousarray is needed because the slice above creates
# a non-contiguous view; torch.from_numpy requires contiguous data.
# This is still only ONE allocation (the contiguous copy).
rgb = np.ascontiguousarray(rgb)
# Build tensor directly from uint8 numpy, then convert to float
# in one fused operation — avoids a separate float32 numpy array.
tensor = torch.from_numpy(rgb).unsqueeze(0).float().div_(255.0)
return tensor
+5 -1
View File
@@ -1,6 +1,10 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "comfyui-firstframe-lastframe-extractor"
version = "1.0.2"
version = "1.0.3"
description = "Extract the first and last frame from any video file or image batch in ComfyUI."
license = { text = "MIT" }
requires-python = ">=3.8"