Mercurial > repos > imgteam > cv2_extract_frames
changeset 0:d2919cd7e314 draft default tip
planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tools/cv2 commit 15c13526f0bf7274b785e4f03e7b6ad9049e5bae
| author | imgteam |
|---|---|
| date | Wed, 18 Mar 2026 09:15:00 +0000 |
| parents | |
| children | |
| files | creators.xml cv2_extract_frames.py cv2_extract_frames.xml test-data/frame_0.tiff test-data/frame_1.tiff test-data/frame_2.tiff test-data/frame_3.tiff test-data/frame_4.tiff test-data/frame_5.tiff test-data/frame_6.tiff test-data/frame_7.tiff test-data/input1.mp4 test-data/input2.avi tests.xml |
| diffstat | 14 files changed, 407 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/creators.xml Wed Mar 18 09:15:00 2026 +0000 @@ -0,0 +1,43 @@ +<macros> + + <xml name="creators/bmcv"> + <organization name="Biomedical Computer Vision Group, Heidelberg Universtiy" alternateName="BMCV" url="http://www.bioquant.uni-heidelberg.de/research/groups/biomedical_computer_vision.html" /> + <yield /> + </xml> + + <xml name="creators/kostrykin"> + <person givenName="Leonid" familyName="Kostrykin"/> + <yield/> + </xml> + + <xml name="creators/rmassei"> + <person givenName="Riccardo" familyName="Massei"/> + <yield/> + </xml> + + <xml name="creators/alliecreason"> + <person givenName="Allison" familyName="Creason"/> + <yield/> + </xml> + + <xml name="creators/bugraoezdemir"> + <person givenName="Bugra" familyName="Oezdemir"/> + <yield/> + </xml> + + <xml name="creators/thawn"> + <person givenName="Till" familyName="Korten"/> + <yield/> + </xml> + + <xml name="creators/pavanvidem"> + <person givenName="Pavan" familyName="Videm"/> + <yield/> + </xml> + + <xml name="creators/tuncK"> + <person givenName="Tunc" familyName="Kayikcioglu"/> + <yield/> + </xml> + +</macros>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cv2_extract_frames.py Wed Mar 18 09:15:00 2026 +0000 @@ -0,0 +1,127 @@ +import argparse +import os +from pathlib import Path +from typing import Optional, Union + +import cv2 + + +def _normalize_frame_number( + frame_count: int, + frame: int, +) -> int: + """ + Translate negative frame numbers into positives by counting from the end. + + Raises: + ------- + ValueError: The given frame is beyond the end of the sequence. + + Returns: + -------- + Integer number between 0 and num_frames - 1. + """ + if frame >= frame_count: + raise ValueError( + f"Frame {frame} is beyond the end of the sequence ({frame_count} frames).", + ) + return frame % frame_count + + +def extract_frames( + output_dir: Union[str, Path], + video_path: Union[str, Path], + start_time: float, + end_time: Optional[float], + convert_to_grey: str = "false", +) -> Path: + """ + Extract frames from a video within a specified time range in seconds + + Parameters + ---------- + video_path: Path to the input video file + + start_time: Start time in seconds + + end_time: End time in seconds + + output_dir: Directory where extracted frames will be saved + + convert_to_gray: Whether to convert frames to grayscale + + Returns: + --------- + Path to the directory containing the extracted frames. + """ + + try: + video = cv2.VideoCapture(video_path) + + # get the video frames per second + fps = video.get(cv2.CAP_PROP_FPS) + print('Frames per second:', fps) + # get the video total frames + frame_count = video.get(cv2.CAP_PROP_FRAME_COUNT) + print('Total frames:', frame_count) + + # determine first and last frame of the sequence to extract + start_frame = _normalize_frame_number( + frame_count, + round(start_time * fps), + ) + end_frame = ( + _normalize_frame_number( + frame_count, + round(end_time * fps), + ) + if end_time is not None + else frame_count - 1 + ) + if start_frame > end_frame: + raise ValueError( + f"Start frame {start_frame} is beyond the end frame {end_frame}.", + ) + else: + print( + f'Starting extracting from frame {start_frame} until {end_frame}...', + ) + + video.set(cv2.CAP_PROP_POS_FRAMES, start_frame) + current_frame = start_frame + + while current_frame <= end_frame: + ret, frame = video.read() + if not ret: + break + + # Convert to single-channel grayscale + output_path = os.path.join(output_dir, f"frame_{int(current_frame):d}.tiff") + if convert_to_grey == "true": + cv2.imwrite(output_path, cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY), [cv2.IMWRITE_TIFF_COMPRESSION, 1]) + else: + cv2.imwrite(output_path, frame, [cv2.IMWRITE_TIFF_COMPRESSION, 1]) + current_frame += 1 + + video.release() + + print('Extraction was successfully executed. Enjoy your frames.') + + except IOError: + exit("Cannot open video file.") + + except ValueError as err: + exit(err.args[0]) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract frames from video files") + parser.add_argument('output_dir', help="Name of the output folder") + parser.add_argument('-v', '--video_path', required=True, help="Path to the video to convert") + parser.add_argument('-s', '--start_time', required=True, type=float, help="Start time in seconds") + parser.add_argument('-e', '--end_time', type=float, help="End time in seconds") + parser.add_argument('-c', '--convert_to_grey', required=False, type=str, help="Convert the file to grayscale") + + args = parser.parse_args() + + extract_frames(args.output_dir, video_path=args.video_path, start_time=args.start_time, end_time=args.end_time, convert_to_grey=args.convert_to_grey)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cv2_extract_frames.xml Wed Mar 18 09:15:00 2026 +0000 @@ -0,0 +1,142 @@ +<tool id="cv2_extract_frames" name="Extract video frames" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="21.05"> + <description>with cv2</description> + <macros> + <token name="@TOOL_VERSION@">4.13.0</token> + <token name="@VERSION_SUFFIX@">0</token> + <token name="@HELP_SECONDS@">Use a negative value to specify the time from the end of the video (e.g., a value of -1 corresponds to "1 second before the end of the video").</token> + <import>tests.xml</import> + <import>creators.xml</import> + </macros> + <creator> + <expand macro="creators/rmassei"/> + </creator> + <edam_operations> + <edam_operation>operation_3443</edam_operation> + </edam_operations> + <xrefs> + <xref type="biii">opencv</xref> + </xrefs> + <requirements> + <requirement type="package" version="4.13.0">opencv</requirement> + </requirements> + <required_files> + <include type="literal" path="cv2_extract_frames.py"/> + </required_files> + <command detect_errors="aggressive"> + <![CDATA[ + mkdir ./output_frames && + + python '$__tool_directory__/cv2_extract_frames.py' output_frames + -v '$video_path' + -s '$start_time' + #if str($end_time) != "" + -e '$end_time' + #end if + -c '$convert_to_grey' + + && ls -l ./output_frames + ]]> + </command> + <inputs> + <param name="video_path" type="data" optional="False" format="mp4,avi" label="Input video to convert"/> + <param name="start_time" type="float" value="0" optional="false" label="Start time" + help="Start time in seconds. @HELP_SECONDS@"/> + <param name="end_time" type="float" optional="true" label="End time" + help="End time in seconds (optional). Leave empty to extract the sequence until the end of the video. @HELP_SECONDS@"/> + <param name="convert_to_grey" type="boolean" label="Convert output to single-channel grayscale?" + help="Convert the file to grayscale (will be RGB otherwise)."/> + </inputs> + <outputs> + <collection name="frames" type="list" label="Output frames"> + <discover_datasets directory="output_frames" format="tiff" pattern="__name__"/> + </collection> + </outputs> + <tests> + <test> + <!-- test MP4 w/o grayscale conversion --> + <param name="video_path" value="input1.mp4"/> + <param name="start_time" value="0"/> + <param name="end_time" value="0.1"/> + <param name="convert_to_grey" value="False"/> + <output_collection name="frames" type="list"> + <expand macro="tests/intensity_image_diff/element" name="frame_0.tiff" value="frame_0.tiff" ftype="tiff"/> + <expand macro="tests/intensity_image_diff/element" name="frame_1.tiff" value="frame_1.tiff" ftype="tiff"/> + </output_collection> + </test> + <test> + <!-- test MP4 w grayscale conversion --> + <param name="video_path" value="input1.mp4"/> + <param name="start_time" value="0"/> + <param name="end_time" value="0.1"/> + <param name="convert_to_grey" value="True"/> + <output_collection name="frames" type="list"> + <expand macro="tests/intensity_image_diff/element" name="frame_2.tiff" value="frame_2.tiff" ftype="tiff"/> + <expand macro="tests/intensity_image_diff/element" name="frame_3.tiff" value="frame_3.tiff" ftype="tiff"/> + </output_collection> + </test> + <test> + <!-- test AVI w/o grayscale conversion --> + <param name="video_path" value="input2.avi"/> + <param name="start_time" value="0"/> + <param name="end_time" value="1"/> + <param name="convert_to_grey" value="False"/> + <output_collection name="frames" type="list"> + <expand macro="tests/intensity_image_diff/element" name="frame_4.tiff" value="frame_4.tiff" ftype="tiff"/> + <expand macro="tests/intensity_image_diff/element" name="frame_5.tiff" value="frame_5.tiff" ftype="tiff"/> + </output_collection> + </test> + <test> + <!-- test AVI w grayscale conversion --> + <param name="video_path" value="input2.avi"/> + <param name="start_time" value="0"/> + <param name="end_time" value="1"/> + <param name="convert_to_grey" value="True"/> + <output_collection name="frames" type="list"> + <expand macro="tests/intensity_image_diff/element" name="frame_6.tiff" value="frame_6.tiff" ftype="tiff"/> + <expand macro="tests/intensity_image_diff/element" name="frame_7.tiff" value="frame_7.tiff" ftype="tiff"/> + </output_collection> + </test> + <test> + <!-- test AVI with endtime 0 --> + <param name="video_path" value="input2.avi"/> + <param name="start_time" value="0"/> + <param name="end_time" value="0"/> + <param name="convert_to_grey" value="True"/> + <output_collection name="frames" type="list" count="1"/> + </test> + <test> + <!-- test MP4 with endtime negative--> + <param name="video_path" value="input1.mp4"/> + <param name="start_time" value="0"/> + <param name="end_time" value="-1"/> + <param name="convert_to_grey" value="True"/> + <output_collection name="frames" type="list" count="238"/> + </test> + </tests> + <help> + + **Extract a sequence of image frames as TIFFs from MP4 and AVI files.** + + Frames captured within a specified time interval (in seconds) will be extracted from the video file as individual TIFF images. + Optionally, the extracted frames can be converted to grayscale. + + **Codecs Support:** + The tool was tested with H.264 and H.265 (HEVC) video codecs. + The tool may support other codecs, though this has not been tested yet. + If you have successfully converted files using alternative video formats or codecs, we would be grateful for your feedback at https://github.com/BMCV/galaxy-image-analysis. + + </help> + <citations> + <citation type="bibtex">@article{opencv_library, + author = {Bradski, G.}, + citeulike-article-id = {2236121}, + journal = {Dr. Dobb's Journal of Software Tools}, + keywords = {bibtex-import}, + posted-at = {2008-01-15 19:21:54}, + priority = {4}, + title = {{The OpenCV Library}}, + year = {2000} + }</citation> + </citations> +</tool> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests.xml Wed Mar 18 09:15:00 2026 +0000 @@ -0,0 +1,95 @@ +<macros> + + <!-- Macros for verification of image outputs --> + + <xml + name="tests/binary_image_diff" + tokens="name,value,ftype,metric,eps" + token_metric="mae" + token_eps="0.01"> + + <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="0"> + <assert_contents> + <has_image_n_labels n="2"/> + <yield/> + </assert_contents> + </output> + + </xml> + + <xml + name="tests/label_image_diff" + tokens="name,value,ftype,metric,eps,pin_labels" + token_metric="iou" + token_eps="0.01" + token_pin_labels="0"> + + <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="@PIN_LABELS@"> + <assert_contents> + <yield/> + </assert_contents> + </output> + + </xml> + + <xml + name="tests/intensity_image_diff" + tokens="name,value,ftype,metric,eps" + token_metric="rms" + token_eps="0.01"> + + <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@"> + <assert_contents> + <yield/> + </assert_contents> + </output> + + </xml> + + <!-- Variants of the above for verification of collection elements --> + + <xml + name="tests/binary_image_diff/element" + tokens="name,value,ftype,metric,eps" + token_metric="mae" + token_eps="0.01"> + + <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="0"> + <assert_contents> + <has_image_n_labels n="2"/> + <yield/> + </assert_contents> + </element> + + </xml> + + <xml + name="tests/label_image_diff/element" + tokens="name,value,ftype,metric,eps" + token_metric="iou" + token_eps="0.01" + token_pin_labels="0"> + + <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="@PIN_LABELS@"> + <assert_contents> + <yield/> + </assert_contents> + </element> + + </xml> + + <xml + name="tests/intensity_image_diff/element" + tokens="name,value,ftype,metric,eps" + token_metric="rms" + token_eps="0.01"> + + <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@"> + <assert_contents> + <yield/> + </assert_contents> + </element> + + </xml> + +</macros>
