-
Notifications
You must be signed in to change notification settings - Fork 0
Access the raw frames #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # Accessing Raw Frames in DeepStream Pipeline | ||
|
|
||
| ## Overview | ||
|
|
||
| This document describes how to access and manipulate raw video frames within a DeepStream pipeline using pad probes. This mechanism allows custom frame processing outside of standard DeepStream plugins. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Pipeline Structure | ||
|
|
||
| ``` | ||
| RTSP Source → Decode → VideoConvert → NvVideoConvert → CapsFilter → StreamMux → ... | ||
| ↑ | ||
| [Probe Point] | ||
| ``` | ||
|
|
||
| ### Key Components | ||
|
|
||
| 1. **nvvideoconvert**: Converts frames to NVMM (NVIDIA Memory Management) format for GPU processing | ||
| 2. **capsfilter**: Ensures RGBA format output (`video/x-raw(memory:NVMM), format=RGBA`) | ||
| 3. **Pad Probe**: Attached to capsfilter's src pad to intercept buffers | ||
|
|
||
| ## Implementation Details | ||
|
|
||
| ### 1. Probe Attachment Point | ||
|
|
||
| The probe is attached **after** the capsfilter element, ensuring frames are in RGBA format on GPU memory: | ||
|
|
||
| ```python | ||
| def attach_buffer_probe(elems, buffer_probe): | ||
| srcpad = elems["capsfilter"].get_static_pad("src") | ||
| srcpad.add_probe(Gst.PadProbeType.BUFFER, buffer_probe) | ||
| ``` | ||
|
|
||
| **Why this location?** | ||
| - Frames are in NVMM memory (GPU accessible) | ||
| - Format is standardized (RGBA) | ||
| - Before heavy processing elements (inference, OSD) | ||
|
|
||
| ### 2. Frame Extraction | ||
|
|
||
| The probe callback extracts frames using PyDS (DeepStream Python bindings): | ||
|
|
||
| ```python | ||
| def buffer_probe(pad, info): | ||
| buf = info.get_buffer() | ||
| if not buf: | ||
| return Gst.PadProbeReturn.OK | ||
|
|
||
| # Retrieve NvBufSurface from GPU memory | ||
| surface = pyds.get_nvds_buf_surface(hash(buf), 0) | ||
|
|
||
| # Copy to CPU as NumPy array (RGBA format) | ||
| frame = np.array(surface, copy=True, order='C') | ||
| ``` | ||
|
|
||
| **Key Functions:** | ||
| - `pyds.get_nvds_buf_surface(hash(buf), 0)`: Gets GPU surface from buffer | ||
| - `np.array(surface, copy=True)`: Copies data from GPU to CPU for manipulation | ||
|
|
||
| ### 3. Frame Manipulation | ||
|
|
||
| Once in NumPy format, frames can be manipulated using standard Python operations: | ||
|
|
||
| ```python | ||
| # Example 1: Draw red rectangle at top | ||
| h, w, _ = frame.shape | ||
| frame[0:20, 0:w] = [255, 0, 0, 255] # RGBA: red bar, 20px height | ||
|
|
||
| # Example 2: Darken entire frame | ||
| frame = (frame * 0.7).astype(np.uint8) | ||
| ``` | ||
|
|
||
| ### 4. Writing Back to Pipeline | ||
|
|
||
| After manipulation, copy the modified frame back to GPU: | ||
|
|
||
| ```python | ||
| # Copy modified frame back to GPU surface | ||
| np.copyto(np.array(surface, copy=False, order='C'), frame) | ||
|
|
||
| return Gst.PadProbeReturn.OK | ||
| ``` | ||
|
|
||
| **Important:** Use `copy=False` to get a writable view of the GPU memory. | ||
|
|
||
| ## Format Requirements | ||
|
|
||
| ### Capsfilter Configuration | ||
|
|
||
| ```python | ||
| capsfilter.set_property("caps", | ||
| Gst.Caps.from_string("video/x-raw(memory:NVMM), format=RGBA")) | ||
| ``` | ||
|
|
||
| - **Memory Type**: `NVMM` (required for `get_nvds_buf_surface`) | ||
| - **Format**: `RGBA` (4 channels, 8-bit per channel) | ||
| - **Frame Shape**: `(height, width, 4)` in NumPy | ||
|
|
||
| ## References | ||
|
|
||
| - [NVIDIA DeepStream Python Apps](https://github.com/NVIDIA-AI-IOT/deepstream_python_apps) | ||
| - [GStreamer Pad Probes](https://gstreamer.freedesktop.org/documentation/additional/design/probes.html) | ||
| - [DeepStream Python Bindings (PyDS)](https://docs.nvidia.com/metropolis/deepstream/python-api/index.html) | ||
|
|
||
| ## Summary | ||
|
|
||
| This mechanism provides a flexible way to access and manipulate raw frames in DeepStream pipelines: | ||
|
|
||
| - **Probe Location**: After capsfilter (RGBA, NVMM format) | ||
| - **Extraction**: `pyds.get_nvds_buf_surface()` + NumPy array | ||
| - **Manipulation**: Standard NumPy/OpenCV operations | ||
| - **Write-back**: `np.copyto()` to GPU surface | ||
|
|
||
| This approach enables custom frame processing while maintaining DeepStream pipeline efficiency. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| DeepStream Pipeline | ||
| RTSP → Decode → Convert → Save Images | ||
| """ | ||
|
|
||
| import gi | ||
| gi.require_version("Gst", "1.0") | ||
| from gi.repository import Gst, GLib | ||
| import os | ||
| import sys | ||
| import numpy as np | ||
| import pyds | ||
|
|
||
| RTSP_URL = "rtsp://127.0.0.1:8554/test" | ||
| OUTPUT_DIR = "/workspace/output/frames/" | ||
|
|
||
| def buffer_probe(pad, info): | ||
| buf = info.get_buffer() | ||
| if not buf: | ||
| return Gst.PadProbeReturn.OK | ||
|
|
||
| # Retrieve NvBufSurface from GPU memory | ||
| surface = pyds.get_nvds_buf_surface(hash(buf), 0) | ||
|
|
||
| # Copy to CPU (RGBA format) | ||
| frame = np.array(surface, copy=True, order='C') | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may not return a valid NumPy array — the surface might be a pointer, not the raw frame data. |
||
|
|
||
| # draw a red rectangle at the top of the image | ||
| h, w, _ = frame.shape | ||
| frame[0:20, 0:w] = [255, 0, 0, 255] # red bar, 20 pixels height | ||
|
|
||
| # Copy back to GPU | ||
| np.copyto(np.array(surface, copy=False, order='C'), frame) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copying back to GPU may fail if the surface isn’t in matching format (RGBA).
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know how that works internally, but since we were able to see the output frames manipulated, I suppose that worked properly |
||
|
|
||
| return Gst.PadProbeReturn.OK | ||
|
|
||
|
|
||
| def on_message(bus, msg, loop): | ||
| if msg.type == Gst.MessageType.ERROR: | ||
| err, debug = msg.parse_error() | ||
| print(f"ERROR: {err}, debug: {debug}") | ||
| loop.quit() | ||
| elif msg.type == Gst.MessageType.EOS: | ||
| print("End of stream") | ||
| loop.quit() | ||
|
|
||
|
|
||
| def create_elements(): | ||
| rtspsrc = Gst.ElementFactory.make("rtspsrc", "source") | ||
| depay = Gst.ElementFactory.make("rtph264depay", "depay") | ||
| parse = Gst.ElementFactory.make("h264parse", "parse") | ||
| decode = Gst.ElementFactory.make("decodebin", "decode") | ||
| convert1 = Gst.ElementFactory.make("videoconvert", "convert1") | ||
| nvvideoconvert1 = Gst.ElementFactory.make("nvvideoconvert", "nvvideoconvert1") | ||
|
|
||
| capsfilter = Gst.ElementFactory.make("capsfilter", "capsfilter") | ||
| capsfilter.set_property("caps", Gst.Caps.from_string("video/x-raw(memory:NVMM), format=RGBA")) | ||
|
|
||
| streammux = Gst.ElementFactory.make("nvstreammux", "streammux") | ||
| streammux.set_property("batch-size", 1) | ||
| streammux.set_property("width", 640) | ||
| streammux.set_property("height", 480) | ||
|
|
||
| nvvideoconvert2 = Gst.ElementFactory.make("nvvideoconvert", "nvvideoconvert2") | ||
| jpegenc = Gst.ElementFactory.make("jpegenc", "jpegenc") | ||
| sink = Gst.ElementFactory.make("multifilesink", "sink") | ||
| sink.set_property("location", os.path.join(OUTPUT_DIR, "frame_%05d.jpg")) | ||
|
|
||
| elements = [rtspsrc, depay, parse, decode, convert1, nvvideoconvert1, | ||
| capsfilter, streammux, nvvideoconvert2, jpegenc, sink] | ||
|
|
||
| if not all(elements): | ||
| print("ERROR: Failed to create one or more GStreamer elements", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| return { | ||
| "rtspsrc": rtspsrc, | ||
| "depay": depay, | ||
| "parse": parse, | ||
| "decode": decode, | ||
| "convert1": convert1, | ||
| "nvvideoconvert1": nvvideoconvert1, | ||
| "capsfilter": capsfilter, | ||
| "streammux": streammux, | ||
| "nvvideoconvert2": nvvideoconvert2, | ||
| "jpegenc": jpegenc, | ||
| "sink": sink | ||
| } | ||
|
|
||
|
|
||
| def link_pipeline_elements(pipeline, elems): | ||
| for elem in [elems["depay"], elems["parse"], elems["decode"], elems["convert1"], | ||
| elems["nvvideoconvert1"], elems["capsfilter"], elems["streammux"], | ||
| elems["nvvideoconvert2"], elems["jpegenc"], elems["sink"]]: | ||
| pipeline.add(elem) | ||
|
|
||
| elems["depay"].link(elems["parse"]) | ||
| elems["parse"].link(elems["decode"]) | ||
| elems["convert1"].link(elems["nvvideoconvert1"]) | ||
|
|
||
| elems["nvvideoconvert1"].link(elems["capsfilter"]) | ||
| sinkpad = elems["streammux"].get_request_pad("sink_0") | ||
| srcpad = elems["capsfilter"].get_static_pad("src") | ||
| srcpad.link(sinkpad) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. streammux should receive raw video, not post-RGBA capsfilter. This link order may cause performance loss.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that in this case, RGBA conversion before |
||
|
|
||
| elems["streammux"].link(elems["nvvideoconvert2"]) | ||
| elems["nvvideoconvert2"].link(elems["jpegenc"]) | ||
| elems["jpegenc"].link(elems["sink"]) | ||
|
|
||
|
|
||
| def connect_dynamic_links(elems): | ||
| def on_pad_added(src, new_pad): | ||
| sink_pad = elems["depay"].get_static_pad("sink") | ||
| if not sink_pad.is_linked(): | ||
| new_pad.link(sink_pad) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing pad type check — if RTSP includes audio, it’ll try linking it too and fail.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, yes - but here we don't care about audio stream in video |
||
|
|
||
| def on_decode_pad_added(src, new_pad): | ||
| sink_pad = elems["convert1"].get_static_pad("sink") | ||
| if not sink_pad.is_linked(): | ||
| new_pad.link(sink_pad) | ||
|
|
||
| elems["rtspsrc"].set_property("location", RTSP_URL) | ||
| elems["rtspsrc"].set_property("latency", 200) | ||
| elems["rtspsrc"].connect("pad-added", on_pad_added) | ||
| elems["decode"].connect("pad-added", on_decode_pad_added) | ||
|
|
||
|
|
||
|
|
||
| def attach_buffer_probe(elems, buffer_probe): | ||
| srcpad = elems["capsfilter"].get_static_pad("src") | ||
| srcpad.add_probe(Gst.PadProbeType.BUFFER, buffer_probe) | ||
|
|
||
| def setup_bus_and_loop(pipeline): | ||
| bus = pipeline.get_bus() | ||
| bus.add_signal_watch() | ||
| loop = GLib.MainLoop() | ||
| bus.connect("message", on_message, loop) | ||
| return loop | ||
|
|
||
|
|
||
| def main(): | ||
| os.makedirs(OUTPUT_DIR, exist_ok=True) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Create the output directory before building the pipeline, otherwise multifilesink may not find it. |
||
| Gst.init(None) | ||
|
|
||
| pipeline = Gst.Pipeline.new("simple-pipeline") | ||
| elems = create_elements() | ||
| link_pipeline_elements(pipeline, elems) | ||
| connect_dynamic_links(elems) | ||
| attach_buffer_probe(elems, buffer_probe) | ||
|
|
||
| pipeline.add(elems["rtspsrc"]) | ||
| loop = setup_bus_and_loop(pipeline) | ||
|
|
||
| print("Starting pipeline...") | ||
| pipeline.set_state(Gst.State.PLAYING) | ||
|
|
||
| try: | ||
| loop.run() | ||
| except KeyboardInterrupt: | ||
| print("\nStopping...") | ||
| finally: | ||
| pipeline.set_state(Gst.State.NULL) | ||
| print("Pipeline stopped") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using hash(buf) to access the surface. It’s unreliable across DeepStream versions and may cause invalid memory access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deepstream sample apps show that it's ok to use it:
https://github.com/NVIDIA-AI-IOT/deepstream_python_apps/blob/9b27f02ffea46a3ded2ad26b3eea27ef3e2dfded/apps/deepstream-imagedata-multistream/deepstream_imagedata-multistream.py#L121