Real-Time Video Pipelines: Techniques & Best Practices

Practical Guide to Real-Time Video Pipelines: Tools, Techniques & Optimization

Video is an extremely popular way to represent information. Indeed, sometimes, it is enough to watch a short clip instead of listening to a podcast or reading about complicated technical concepts.

Businesses also strive to gain a competitive advantage by integrating innovations like video analytics, streaming services, robotics, and AR/VR apps. To get valuable insights from raw video data, you need to design and implement efficient video pipelines.

From a user’s point of view, a video is simply a sequence of images displayed one after another with a very short inter-frame interval. Typically, it has around 30 frames per second (FPS). However, many things are left inside the box.

In this article, we focus on how to build an efficient video streaming pipeline and explore:

  • What is a video pipeline from a technical perspective.
  • Essential elements of video pipelines.
  • Ways to design and develop efficient video pipelines.
  • Share our It-Jim experience and best practices for building a video pipeline.
  • Tools, frameworks, and technologies for building video pipelines.

Let’s explore what a video pipeline means and how it is utilized in computer vision, compared to traditional image processing methods.

Understanding Video Pipelines

So, what is a video pipeline?

At its core, a video pipeline is a sequence of processing steps that takes raw video from cameras or sensors and turns it into output or actionable insight for the end user. This technology is used in computer vision systems for object detection, tracking, and monitoring.

The primary goal for developers is to maintain high video quality while optimizing storage, scalability, and seamless playback across various devices and networks.

The main components of a video pipeline are input sources, transcoding servers, content delivery networks (CDNs), and processing tools. Once these video pipeline components are aligned to work together, the system delivers a smooth viewing experience and is a reliable analytics tool.

Traditional image processing works with single, static images. There’s no pressure to process them quickly, so it’s often done offline without time constraints.

In contrast, video processing pipelines work with a stream of frames. Each frame arrives in rapid succession and is connected to the ones before and after it. For many use cases, such as live streaming, surveillance, or AR/VR, each frame must be processed in real-time or near real-time to ensure smooth playback and timely analysis.

Real-time pipelines differ from non-real-time ones in that they are designed to operate with minimal delay, often under hardware constraints. Real-time video pipelines process frames as they arrive, prioritizing low latency and smooth playback for live applications.

Non-real-time pipelines handle pre-recorded video, allowing slower, more complex processing without time constraints. Both types share the same components but differ mainly in timing and performance requirements.

In summary, either system requires a real or non-real-time video pipeline setup; the process can be complex and costly. Next, we will outline the key aspects of its careful planning and implementation.

How Video Pipelines Work

A video pipeline consists of several phases, including capture, processing, and encoding. Here is the workflow of a standard video pipeline:

  • Capture raw video through a camera.
  • Process the video (apply filters, AI models, or CV algorithms).
  • Encode it for storage or streaming.
  • Repackage frames for storage or transmission.
  • Deliver output to the user or system.

The first steps in a video pipeline are capturing raw video, uploading it to a server or cloud storage, and extracting metadata from the original video.

Whether you’re building a smart surveillance system, a robotics platform, or a live video analytics service, your video pipeline has a direct impact on the accuracy and performance of your solution.

Key Components of Video Pipelines

The main components of a video pipeline are video sources, transcoding servers, Content Delivery Networks (CDNs), and various processing tools.

Thus, most of the video pipeline consists of the following:

  • Video sources – these can include a camera, video files, or a live stream.
  • Encoding and decoding – the process of converting video into different formats for storage and transmission.
  • Transcoding – convert video from one format to another, also for different platforms or devices.
  • CDN integration – distribute video content through a network of servers to reach users faster.
  • Display elements – prepare the video for playback on various devices (e.g., TVs, computers, mobile phones).

A well-tuned video pipeline seamlessly integrates all its video pipeline components. A good understanding and implementation of the components can make a big difference to the efficiency and reliability of your video pipeline.

Next, you can check out an example with code samples on how to build a video pipeline.

Building Efficient Real-Time Video Pipelines

Have you ever written your own video player? What about a media server? Or a real-time video processing pipeline?

For most people in the world, the answer is “no”. Probably even among the readers of this blog.

Our experience suggests that many people underestimate the difficulties involved and are in for unpleasant surprises when attempting to implement computer vision (CV) in real-time.

 “Real-time” refers to the process of receiving frames from a camera or network stream, as opposed to a pre-recorded video file.

Novice computer vision engineers typically learn their craft on individual images. In rare cases, when the time dimension is required (e.g., tracking, optical flow), they usually work on pre-recorded videos.

Then they think: What can go wrong with real-time? I just get frames from the camera and apply the fancy computer vision that I usually do?

A schematic C++ code they imagine looks like this:

cv::VideoCapture cap(cv::CAP_ANY);
while (true) {
cv::Mat frame;
cap.read(frame);
process_somehow(frame);
send_somewhere(frame);
}

Is it how a good real-time computer vision system works? No! Let’s dive deeper.

Where is the Frame Loss?

When junior CV engineers try to do something with a camera, our first question is, “Where is the frame loss here?”

This question usually surprises people: “No, I do not want to lose any frames”. This is wrong. If your camera produces 30 FPS, few CV algorithms (and especially not neural networks) can process a frame in 33 milliseconds.

Then, you typically want to stream, display on screen, or record the result somewhere. This also takes time. Even if your computer is fast enough, there are always slower devices (such as embedded ones) or computers overloaded with some background tasks.

So, frame loss is inevitable. And because of the frame loss, you can never rely on a steady FPS from a camera.

Pop Quiz: Where is the frame loss in the piece of code above? Think before reading the answer.

Now, here is the answer: the frame loss is at the line “cap.read(frame);”. This is a synchronous waiting call “give me the next frame when ready”. If you take too long processing frame 1, the subsequent frames will be lost until you reach the read() call again.

Luckily for us, OpenCV VideoCapture does not try to keep multiple frames in a buffer. You can guess what would happen if it did.

Hint: Nothing good.

Again, frame loss means that there is no reliable fixed frame rate (FPS) and that the difference between the timestamps of two subsequent frames varies.

It does not matter if your CV algorithms process each frame individually. It is usually not critical for optical flow either.

However, if you perform signal processing in the time domain or use parameterized motion models, then frame loss becomes critical.

What is the solution?

You must record the original timestamp of each frame. If you need a signal with a regular frame rate (FPS), resample the original video to a desired regular timestamp grid.

Threads and Buffers to the Rescue

Does the code above work correctly? Yes. Is it efficient? Definitely not.

Note how it does all operations strictly sequentially. This includes an input (get a frame from the camera) and output (visualize the result on the screen, write it to a file, or send it to the network).

Such a sequential pipeline does not utilize (or at least does not utilize efficiently) multithreading on multiple CPU cores (and possibly a GPU also).

But the sequential pipeline has an even more striking defect.

Imagine for a moment that you want to process your frame in the cloud, and then send the result back to the edge device. Processing on the server can be very fast, but the internet connection has a lag, sometimes up to half a second or more (in two directions).

With a sequential pipeline, you will wait for half a second for the answer from the server before processing the next frame. The result is 2 FPS or less, whereas with a proper pipeline, you can achieve 30 FPS with in-cloud processing.

Figure 1: Car assembly line

Figure 1. Car assembly line (© www.freepik.com)

This is similar to a car assembly line, as shown in Figure 1.

As you can observe from the sequential pipeline in Figure 2, this means that only one car is being assembled at a given time.

Imagine an almost empty factory building with one very lonely car traveling the assembly line. Only when this car is finished can the next car start. That would not be an efficient assembly line. But we all know that is not how car factories work in real life.

In reality, multiple cars move along the assembly line, one after another. The same principle applies to serious real-time computer vision, as shown in Figure 3. Different stages in the pipeline (“actions”) take place in different threads, running on different CPU cores or possibly on a GPU.

Simple video pipeline

Figure 2. A sequential pipeline. A frame from the camera travels through the pipeline (actions 1, 2, and 3) and is finally sent to the “Output” (e.g., visualization on the screen). When this frame processing is finished, we can receive the next frame from the camera. 

Frames travel along the pipeline like cars on the assembly line, from thread to thread. While thread 3 processes frame 7 (for example), thread 2 can process frame 8 at the same time, and thread 1 can process frame 9.

The “actions” include different computer vision operations that are executed sequentially, for example, object detection and rendering of some graphics. This also includes video encoding and decoding, BGR <-> YUV conversions, CPU <-> GPU data transfer, etc.

There is, however, a subtle difference.

On the assembly line, cars travel at a fixed speed (throughput), and each assembly operation takes a standard “one step” time (or less).

In video pipelines, maintaining a consistent FPS can be challenging, and computer vision operations may take varying times on different frames. 

So, what is usually done? 

The threads on the pipeline are connected by buffers (queues). The buffers have a maximal size that they are not allowed to exceed. If the buffer is overfilled, the frame is lost (something that we would not want on a car assembly line).

Thus, if we have a bottleneck in the pipeline (thread with extended processing time), the frames are automatically lost in the buffer just before this thread. This basic pipeline architecture (threads connected with buffers) is found behind the hood in every media player or server, in YouTube, Zoom, Skype, and Netflix.

And if you want your video pipeline to work correctly, you should implement this architecture as well. Alternatively, you can use a ready-made tool (see below).

Note that buffers introduce latency. On the other hand, they ensure smooth playback. There is always a trade-off between smoothness and latency; you cannot have both.  If you want a low-latency real-time pipeline, keep your buffers as small as possible.

A multithreaded buffered pipeline

Figure 3. A multithreaded buffered pipeline. Threads are connected via buffers.

One thing is essential. Never build an unlimited buffer without a size limit. It will grow infinitely (while generating a rapidly increasing lag), fill all RAM, and eventually crash your computer.

This is not a purely theoretical possibility. Frame loss is the safety valve in your pipeline, preventing it from exploding like an overheated steam engine. When using higher-level libraries and frameworks, be aware that they may implement their buffers.

Always understand how the library functions work, and read the documentation carefully.

For example, the read() method of cv::VideoCapture provides the next camera frame, but what exactly does it mean? It is a combination of grab() and retrieve(). grab() grabs the last camera frame (or waits for the next one), while retrieve() decodes it to the BGR format if needed.

There is no buffer anywhere. Lucky for you, you cannot shoot yourself in the foot with OpenCV. But suppose some other hypothetical camera library implemented an unlimited buffer; what would then? Then, we would crash the computer by grabbing frames too slowly.

Note: The often-used logic “Send frame to the engine if the engine is available. If the engine is busy, drop the frame” can be viewed as a very rudimentary buffer with a maximum size of 0. The proper buffers are more flexible than that.

A Note on Asynchronous Programming

Asynchronous programming is a popular trend nowadays, especially in web programming, as well as in mobile and desktop GUIs.

What does it mean?

A synchronous operation means that you request some action and wait for it to finish. For example, the above-mentioned read() method of cv::VideoCapture waits for the next video frame to arrive.

An asynchronous operation means that you request something and provide a callback function that will be called when the operation is finished. This is like your boss telling you, “Do something, then text me when you are ready”. Of course, your boss will not wait for you to finish; he will do some other work.

In particular, in web and mobile, cameras and video streams typically work this way. You have to provide a callback cb(), which is called when the next frame arrives.

What does it mean?

Attentive readers may notice that this logic is mathematically not well-defined. What happens if frame 2 arrives when frame 1 is still being processed (callback did not return)?

Different libraries behave differently. Always understand how yours does. The library may lose a frame; this is good.

Or it can implement its own buffer.

Or, the callback for frame 2 will be called anyway in another CPU thread, while frame 1 is still being processed.

The last option is interesting. Many CV algorithms (optical flow, tracking, etc.) require frames to arrive purely sequentially, one after another. The algorithm will go crazy if you try to run it for two frames simultaneously in different threads, crashing, throwing an exception, or, worse, behaving erratically.

Even single-frame algorithms (like object detection neural networks) will eventually crash your device if you try to run many frames simultaneously in different threads. Such a situation happens all the time in real life when some web or mobile developer simply puts your algorithm in a callback without thinking about pipelines or buffers at all.

The correct solution is to put a buffer between the callback and algorithms. The callback should simply put a frame into the buffer, a fast operation (in general, callbacks should NOT contain any heavy operations).

At the same time, the CV algorithm in another thread reads frames from the buffer. It ensures the proper sequence of frames, and of course, the buffer should have a maximum size and frame loss, as usual. You have to implement the buffer yourself.

Decoding, Encoding and YuV

How to decode and encode videos?

At least in Linux, there are different libraries for different audio and video codecs (libx264, libvpx, etc.) with different obscure APIs.

Is there a unified approach for all codecs? Yes, there are a few options.

OpenCV uses ffmpeg under the hood and can handle simple cases, but it is vastly insufficient for serious projects. FFmpeg and GStreamer are two principal choices, at least on Linux and cross-platform. Of course, they also exist on Windows, macOS, and mobile devices. You should master these two libraries if you do video pipelines.

Most video codecs do not work with BGR or RGB images. Instead, they use various versions of YuV, including YuV420p, NV12, and NV21. If you want RGB, you will generally have to convert it yourself.

OpenCV can handle a few versions of YuV, and libswscale (a part of FFmpeg) can handle them all.

Note that YuV<->RGB conversions are pretty expensive, especially on 4K images.

You should avoid them if possible. For example, if your CV algorithm processes grayscale images, you do not need RGB and can work on YuV directly (as the grayscale frame is always a part of YuV frame).

What about hardware-accelerated encoders/decoders, such as those available on Nvidia GPUs (including the Jetson Xavier, but excluding those in laptops) and Raspberry Pi?

FFmpeg and GStreamer can generally handle those, but sometimes it requires building the library from source (e.g., FFmpeg on Raspberry Pi, which is a significant pain). 

There are also native APIs for Nvidia (NVENC/NVDEC) and for Raspberry Pi (MMAL, OpenMAX). You may encounter issues with hardware encoders and decoders.

For example, in one project, we figured out that the Nvidia H264 decoder produces only NV12 (and not the regular YuV420). Also, some hardware encoders do not repeat PPS/SPS packets (headers) of H264, which causes bad issues with streaming.

Let us cheat!

Despite your best efforts, you may find that your pipeline’s output looks ugly in terms of throughput (FPS), latency (lag), and stability.

For example, if some neural network takes 0.5 seconds per inference, you will get a 2 FPS video with over 0.5 seconds lag. Ouch.

Then, how come all commercial products, including mobile, browser, and embedded apps, all look so beautiful? First, they optimize everything that can be optimized. Second (and we are revealing to you the biggest secret in the industry), they cheat. Everybody does. By “cheating,” we mean an optimization that radically changes the entire pipeline logic to produce a visually pleasing output. 

1. Show every frame (keep full FPS), process only some of them.

In the example above, the output video of 2 FPS is very ugly. So, send only 2 frames per second to the slow neural network (which can do, e.g., object detection), but send every single input frame (30 FPS) to the output video.

This is essentially a pipeline with branches as opposed to a sequential one. A massive frame loss happens on the detection branch (as the detector is slow) but not on the visualization branch. 

But how do we visualize detected objects in every frame, when detection happens only twice per second? You can use the last detected position. Or, better, look at cheat # 2.

2. If detection is slow, interpolate, smooth or track.

When doing cheat #1, you can interpolate/extrapolate object locations between the “detection” frames or apply some kind of smooth motion models with velocity parameters. Even when detection is fast, a good motion model provides a much smoother visual representation of object motion.

Accurate tracking involves using optical flow and similar approaches to track each object as soon as it is detected.

3. Prefer zero lag and compensation.

Visible lag makes things ugly. If the camera feed on your smartphone’s screen is 0.5 seconds delayed, people tend to notice this.

Thus, when using cheat #1, it is better to visualize the frame immediately, rather than waiting for the results of object detection.

Most real-time apps, especially on mobile, work like this. Of course, this kills the synchronization between the frame and the detection result. You might notice that detection results are lagging half a second behind the frame, since they were detected on an earlier frame.

Bad. But this is a necessary evil. If the entire camera video lags, it is visually much worse than if a little bounding box lags.

What you can try is to compensate for the lag. If you have some motion model for the object, you can just go 0.5 seconds back in time. This works reasonably well, but only when the object moves predictably and not when a new object is just detected.

When you think that your app looks poor compared to existing ones, remember that true computer vision professionals are masters of cheating.

Tools and Frameworks for Video Pipeline Development 

Building a video pipeline requires using the right tools and libraries that cover different aspects of video processing.

Here are some of the most commonly used tools:

  • GStreamer: Modular, real-time streaming and processing.
  • FFmpeg: Powerful CLI/media processing toolkit.
  • OpenCV: Offers tracking, filtering, and vision utilities.
  • Nvidia DeepStream: Optimized for GPU-accelerated inference.
  • Jetson APIs, MediaPipe, VAAPI: Platform-specific hardware acceleration.

1. GStreamer

This is a multimedia framework for building graphs of media-handling components. GStreamer supports real-time audio and video processing. The tool is best for live streaming and conferencing solutions with support for custom plugins, codecs, and protocols.

2. FFmpeg

This open-source library supports video encoding, decoding, transcoding, streaming, and other related tasks. The tool supports a wide range of file formats and codecs (e.g., H.264). FFmpeg is typically used for video format conversion, frame extraction, or video compression.

3. OpenCV 

OpenCV (Open Source Computer Vision Library) focuses on real-time image and video processing. The tool is used for frame capturing, object detection and tracking and is valuable for vision-intensive tasks such as motion tracking or AR in video pipelines. 

4. Nvidia DeepStream

This is an AI streaming toolkit designed for Nvidia GPUs and real-time analytics. The technology enables high-performance deep learning inference (e.g., object detection and classification). Nvidia DeepStream is best suited for scalable, low-latency pipelines with advanced AI capabilities.

5. Hardware APIs

In addition to software libraries, hardware APIs provide access to specialized encoding and decoding capabilities to improve performance and reduce latency. For example, you can use  Nvidia’s NVENC and NVDEC or platform-specific APIs like OpenMAX. Using these APIs can greatly improve throughput and efficiency in video pipelines, especially for high-resolution or real-time applications.

Before selecting video processing tools, consider the following aspects:

  • Scalability and performance for your project.
  • Compatibility with devices to keep pipeline performance.
  • Scalability and maintenance via cloud storage solutions.

6. Integrating AI and Automation

Adding Artificial Intelligence to your video pipeline makes it more efficient, streamlines processes, and reduces manual work. AI and machine learning can:

  • Automate video indexing.
  • Generate captions or subtitles.
  • Detect inappropriate content.
  • Optimize video quality in real-time based on user preferences.
  • Improve accuracy and consistency in video processing tasks.

AI-driven techniques provide a more responsive and adaptive video pipeline that delivers high-quality content in real-time.

By using these tools, you can build efficient, scalable, and feature-rich video pipelines for your project.

Video Pipelines: Best Practices and Industry Techniques

Building a video pipeline that’s efficient and reliable requires a mix of technical knowledge, practical experience, and industry-proven methods.

Here are some best practices used in the field from real-world examples and companies like Netflix and computer vision experts like It-Jim:

  • Frame skipping and interpolation: to prevent overloads in video pipelines, process or display key frames selectively. This way, you can have smooth playback even under heavy processing loads.
  • Progressive rendering: deliver a lower-quality version of the video quickly. Then you can progressively improve the quality as more data is processed, reducing perceived buffering times.
  • Adaptive bitrate streaming: adjust video quality based on network conditions to minimize buffering, ensure smooth playback and enhance viewer’s perception of performance.
  • Preloading and caching: use preloading strategies and cache frequently accessed video segments at edge servers or CDNs to reduce latency and speed up playback start times.

You can avoid some common pitfalls in building a video pipeline by following these recommendations:

  • Managing frame loss: design real-time video pipelines to handle frame loss by recording original timestamps and resampling frames to maintain temporal consistency.
  • Buffer size control: buffers should strike a balance between latency and smooth playback. Implement strict size limits on buffers to prevent memory exhaustion and system crashes.
  • Efficient format conversions: minimize expensive video format conversions (e.g., YUV to RGB) to reduce processing overhead.
  • Multithreading and asynchronous processing: use multithreading to leverage multiple CPU cores. You can also employ asynchronous programming to avoid blocking operations and reduce latency.
  • Microservices architecture: break the pipeline into decoupled microservices to improve flexibility, scalability, and maintainability, and to speed up feature development and troubleshooting.
  • Hardware acceleration: use hardware encoders and decoders (e.g., Nvidia NVENC/NVDEC) and platform-specific APIs to reduce encoding and decoding latency and CPU load.
  • Comprehensive monitoring and analytics: implement detailed logging, quality metrics, and monitoring tools to quickly identify bottlenecks, failures or quality degradation.
  • Security: integrate security measures like Digital Rights Management (DRM) early in the pipeline to protect content without compromising performance.

By applying these best practices, engineers can build video pipelines that deliver high-quality, low-latency video experiences at scale. At the same time, these approaches enable the mitigation of typical challenges faced in production.

Efficient Video Pipelines Summary

  • Do not use sequential single-thread pipelines
  • Frame loss is inevitable
  • There is no stable, predictable FPS; if you need one, resample
  • Build a pipeline with threads and buffers
  • Never do an unlimited buffer/queue
  • Do not put heavy operations into asynchronous callbacks
  • Asynchronous callbacks must not run sequentially
  • Use FFmpeg, GStreamer, or other software for encoding/decoding
  • Codecs always use YuV, and many versions of it
  • Avoid costly YuV<->RGB conversions if possible
  • Don’t reinvent the wheel, use GStreamer!
  • Or Nvidia DeepStream for a GPU-only pipeline, which can run out of GPU RAM
  • Cheat #1: Show every frame (keep full FPS), process only some of them
  • Cheat #2: If detection is slow, interpolate, smooth, or track
  • Cheat #3: Prefer zero lag and compensate

Final Word on Video Pipelines

We have addressed some practical aspects of video processing pipelines and shed light on people who might think that this is a trivial process.

When developing video pipelines, engineers must find a proper balance between speed, accuracy, and usage of available software and hardware resources. Solutions often involve queue buffering, multithreading, and modular design of the video pipeline.

Also, incorporating AI into video pipelines can automate repetitive tasks, significantly enhancing workflow efficiency. In any case, the key to success lies in careful planning, choosing the right tools, and optimizing workflow.

Computer Vision for Faces Meetup

Long time no meetup, right? We’re fixing it with our joint online event with Sigma Software University on October 28 😉
Computer vision for faces… One of our favorite topics! Today, almost every image and video contain faces (thank you, selfies!), and computer vision and deep learning algorithms become a common thing in face processing. Join the presentation of our CTO Pavlo Vyplavin to learn more about the existing CV products for faces and get some insights from one of our cases: https://www.facebook.com/events/347493036556015.

Embedded and Single-Board Computer Vision: Running Deep Neural Nets

Deep learning (DL) and neural networks are extremely widespread in different computer vision (CV) applications. Indeed, many typical problems (like object recognition or semantic segmentation) are effectively solved by convolutional neural networks (CNNs). In this article, we are going to discuss how to utilize CNNs on embedded devices.

Neural Networks, Training and Inference

Neural networks today are ubiquitous. In particular, it is hard to imagine computer vision without them. The networks used in CV are typically convolutional (which means they rely heavily on convolutional layers) and deep (meaning the total amount of layers is large, often in the hundreds), thus “deep learning“. The architectures of the modern state of the art (SOTA) neural networks are getting more and more sophisticated, which often means they are slow and require a lot of resources to operate, although networks specially designed to be lightweight (like MobileNet series) are not uncommon.

Before we go ahead, let’s highlight the differences between the CNN training and inference. The former is typically done on powerful hardware starting from your desktop and up to the GPU cluster in the cloud (AWS, MS Azure, etc.) Technically, this is a process of optimization of hyperparameters (typically, millions). And the inference is an actual porting and running the pre-trained network. Sounds like an easy task, right? However, it’s not the case in practice.

Deploying Neural Networks in Production

So, suppose you trained a neural network in PyTorch or Tensorflow and you are happy with its performance. How do you deploy it in production (in your own C++ or Python code)? It is not that trivial, as beginner’s level DL tutorials and manuals usually avoid the issue. For example, PyTorch documentation hardly touches this at all, while Tensorflow documentation advertises some exotic commercial services on google cloud. So, the question is  “how can I infer a neural network in my own C++ or python code?”. There are many possibilities, for example:

  • Frugally-deep: C++ only, lightweight CPU-only, header-only, Eigen-based
  • Google: Tensorflow (Lite) :  C++/Python, CPU/GPU
  • Facebook: libtorch/PyTorch : C++/Python, CPU/GPU
  • Microsoft: ONNX runtime :  C++/Python, CPU/GPU
  • Nvidia: TensorRT : C++/Python (3.6 only), Nvidia GPU only

Let us discuss them in turn:

frugally-deep: This is more of a curiosity than a real thing. Frugally-deep is a header-only C++ library, which requires 3 more header-only libraries including Eigen. It infers a neural network (described as a JSON file) on the CPU using Eigen. OpenCV images can be used as input/output. There is a converter for Keras models. While not especially efficient, frugally-deep can be used for deploying lightweight models easily, or as a toy for deep learning beginners. Now let’s see what the big corporations can offer.

 

Google: Tensorflow (Lite): Tensorflow can save models in at least two formats: Keras and pure Tensorflow. For inference, it is more efficient (faster inference) to use Tensorflow Lite, an inference-only framework, which is part of a full Tensorflow but can also be installed separately. Tensorflow Lite has yet another saved model format, so your network needs conversion. Tensorflow Lite is available for both CPU and GPU, and for both C++ and Python. There is yet another C++ library “Tensorflow Lite Micro” for microcontrollers (hardcore embedded devices). Note that Tensorflow (including Lite) is notorious for requiring very particular (and outdated) CUDA and CuDNN versions (when you infer on GPU), and for the incompatibility between Tensorflow 1x and 2x.

 

Facebook: libtorch/PyTorch: Training framework PyTorch can be also used for python inference. Unlike Tensorflow, Pytorch does not build a model graph but runs model python code on each iteration. Because of that, a PyTorch “saved model” would not work without a complete model code. However, in more recent PyTorch versions there is a new subsystem named TorchScript, which is similar to Tensorflow graphs and can save models to a format that can be then inferred by someone else without the model code. TorchScript models can be also inferred in the C++ library libtorch. Compared to TensorFlow Lite, libtorch is available as a pre-build library (both CPU and GPU) and is well-documented. Both PyTorch and libtorch are rather liberal with respect to the CUDA version (compared to Tensorflow). However, libtorch size (about 1Gb) can be an issue for deployment.

 

Microsoft: ONNX runtime: It is an inference-only framework for Python and C++, CPU and Nvidia GPU, which can infer ONNX models. ONNX is the Microsoft format for neural networks, which is (in theory) framework-agnostic and can (in theory) be used to transfer any network from any training framework to any inference framework. An ONNX model can be exported from PyTorch or Tensorflow (provided that your network is good enough, see the next section). The C++ version of ONNX runtime has to be built from the source.

 

Nvidia: TensorRT : It is a C++ inference framework for Nvidia GPUs only, and the absolutely fastest inference framework for Nvidia GPUs. It should be always considered to be the default option for GPU deployment. TensorRT supports FP16 and INT8 inference and other fancy optimizations. Unlike other frameworks, it is not open-source (though free). It can import networks in ONNX, UFF and Caffe formats. I will discuss TensorRT in more detail in a separate section below. A python wrapper is advertised, but it only works on Python 3.6.

State Of The Art Curse and Model Zoos

Not all neural networks can be deployed outside of the training framework. The ones using only the most standard building blocks, such as convolution or max pooling, are usually OK, however many networks in Github do something rather exotic, or even have custom modules written in C++/CUDA.

This can be called “State Of The Art Curse”. The networks on the cutting edge of deep learning (recent arrivals on Github and top scores in PapersWithCode) are the most likely ones to use some fishy stuff and be thus undeployable. However, customers all the time fail to understand that and make demands of sorts “Take network X, which is the SoA, and deploy it on Jetson Nano using TensorRT”, which is usually impossible. Roughly speaking, there are 3 levels of “undeployable”:

  • Fully undeployable. The model cannot even be exported into ONNX, UFF or TorchScript. This usually happens when the model has a custom CUDA code or custom Python code. Note that this often can be circumvented with a lot of effort (like including the custom CUDA modules into TensorRT deployment).
  • TensorRT incompatible. Some common ONNX operations are not yet supported in TensorRT, like image resize (torch.nn.functional.interpolate). This also depends on the TensorRT version (e.g. transformers first appeared in TensorRT 7).
  • Accelerator incompatible. Deep Learning Accelerators (Nvidia DLA, Google TPU, Intel VPU) are extremely restrictive in what they can do. Nearly every modern neural network fails to deploy without modifications. More on this below.

A related concept is the “Model Zoo”. If you see “Model Zoo” somewhere, it means you have been cheated. Why? “Model Zoo” means that the company that created a DL accelerator or framework kindly provides you with a few models you are invited to use. This usually means that any other model (like a fancy SOTA network from Github) is not going to work. As DL engineers, we want to deploy any network, especially newer and better ones and do not want a very limited selection of some simple and usually outdated stuff like YOLO2.

Deep Learning on Embedded/Single Board (and other) Devices

On Embedded/Single board devices you have a choice between Nvidia Jetson devices (Fig. 1) and other devices, the latter category includes devices with DL accelerators (Fig. 2).

Fig. 1. Nvidia devices: Jetson Nano, Jetson Xavier (single-board and embedded versions).

Nvidia Jetson Devices: CUDA-Based Deep Learning and TensorRT

The Nvidia Jetson series are the only single-board/embedded devices with Nvidia GPU. Nvidia GPU means that you have CUDA and you can do deep learning in more or less the same way as on a cloud instance or a desktop Linux PC: you have Tensorflow, PyTorch, TensorRT, etc. The preferred framework for inference is TensorRT, as it is normally much faster than the alternatives. But if some network is really TensorRT-incompatible, you can always fall back to Tensorflow (Lite), PyTorch/Libtorch or ONNX runtime. This is the major advantage of having an Nvidia GPU. With DL accelerators (discussed below) you are extremely limited in what software you can use and what networks you can run. All things considered, if you have to run a network “on the edge” in production, we would suggest an Nvidia device (e.g. Xavier) to the devices with accelerators shown in Fig. 2 if the budget allows it. Note that Jetson Nano has a very low power while being equipped with an Nvidia GPU. Thus, it is better to use it for academic purposes.

In comparison, the more expensive Xavier devices have a pretty decent GPU. Once again: never ever try to train neural networks on these devices, they are only for inference.

Fig. 2. Devices with DL accelerators: Google Coral, Google Coral Stick, Intel Neural Compute Stick 2 (aka Movidius).

Now it is time to say a few more words on TensorRT. TensorRT is Nvidia’s highly optimized neural network inference framework which works on Nvidia GPUs only. It is a C++ library (python wrapper is available for python 3.6 only but not python 3.8), so you will have to know C++ well and you will have to write quite a bit of boilerplate C++ code to get your network up and running. That’s right, the command line tool trtexec is good for testing only, for deployment you will have to write your own C++ code. Probably even two pieces of C++ code: for engine creation and for inference.  The inference takes place entirely on the GPU and is using GPU RAM so you will need to know basic CUDA programming as well.

The workflow of TensorRT is like this.

  1. First, you need to create a network description, a graph consisting of standard TensorRT layers (this is not unlike the Tensorflow graph). You can create it by hand in your C++ code, but most often people import (“parse”) an existing network in ONNX, UFF or Caffe format using the respective parser, a library separate from the main TensorRT.
  2. Second, you build a TensorRT runtime engine (also known as “plan”), which optimizes your network for a particular GPU. This process can take some time (sometimes over 10 minutes). The engine can be serialized to disk (saved as a *.plan file) for later inference, but you need to keep in mind that it will not work on a different GPU model.
  3. Once you created an engine or loaded it from the *.plan file, you create a TensorRT execution context next (a rather thin wrapper around the engine). More than one context can be created from one engine. If any dimensions, including the batch size, were left unspecified (“dynamic”) in the engine, they must be fixed when the context is created.
  4. Finally, you use the execution context to run the network (inference) as many times as you like. The input and output data is stored in the GPU RAM, and the inference itself is typically enqueued in a CUDA stream.

One of the best things about TensorRT is that it can speed up your network by using FP16 and INT8 precision instead of the default FP32, provided that your GPU supports these operations (the better ones do). FP16 and INT8 use the tensor cores of the GPU, which are different from the regular CUDA cores. INT8 inference requires calibration, i.e. running the network on a bunch of input images to determine the numerical scale of each layer. TensorRT even supports the Nvidia Deep Learning Accelerator (DLA) on Xavier devices, more on that below.

TensorRT Issues

Of course, nothing is ideal. And we’ve faced a lot of challenges while using the Tensor RT engine.

Boilerplate Code:

As mentioned already, you need a lot of boilerplate C++ code. You must create a logger and manually perform the 4 steps outlined above. Also, you have to do a lot of pre-/post-processing on the images all by yourself:

  • Load images, or receive it from camera/video, convert them from BGR to RGB (if using the BGR-loving OpenCV).
  • Convert pixels from UINT8 to FP32 and normalize the image (so that the mean and standard deviation of the pixel intensities over a large dataset are approximately zero and one respectively). Most neural networks only operate on normalized images.
  • Convert from “channels last” format (where the color index is the last) to the “channels first” format of TensorRT. Suppose our image had dimensions 480x640x3, it should be converted to 3x480x640.
  • Combine many images to a batch (if batch size > 1). Using a batch size larger than 1 can speed up your inference if you need to process many images. With the batch size of 8, the final dimension of our input tensor will be 8x3x480x640.
  • Finally, copy the image from CPU to GPU RAM with the function cudaMemcpyAsync() or the like before inference.

And if the output of your neural network (inference result) is also an image, you have to perform all these steps in the reverse order on the network output.

Network Compatibility:

Many network operations are incompatible with TensorRT. The most striking example is the image resize operation. For integer upsampling only (e.g. 2x), this issue can be circumvented by clever tricks (See Nvidia RetinaNet code for details). And of course, if your model utilizes a custom CUDA code, things become more complicated. It is possible to use custom plugins in TensorRT, but there is no way such a network can be represented as ONNX, at least not the complete network. In the same Nvidia RetinaNet, only part of the network is exported as ONNX, and custom layers are added to it in the C++ code when the TensorRT network is constructed. Nvidia RetinaNet, by the way, is a pretty good (though rather advanced) TensorRT example.

ONNX Parsing and TensorRT Version Incompatibility:

You might have heard that different versions of TensorRT, especially 6x and 7x are seriously incompatible. Is it so? Yes and no. The main difference is actually not in the TensorRT itself but in the Microsoft ONNX parser (which is open-source code from Microsoft and not part of the TensorRT itself). ONNX parser is the most widely used to convert PyTorch networks to TensorRT.

The issue arises from the way TensorRT handles the batch dimension. The recommended modern way is the “explicit batch dimension”, where the batch dimension is the first dimension of all network tensors, including inputs and outputs. It can be dynamic (unspecified when constructing the engine, and fixed only when creating the execution context). However, there is also a legacy way “implicit batch dimension”, when the batch dimension is not part of the input tensor dimensions and can be set to any value at the inference time, only the maximum batch size must be specified at the engine creation time. For example, if your network processes RGB 640×480 images, the input tensor dimension will be 8x3x480x640 with an explicit batch dimension (and batch size 8), and 3x480x640 without.

Another problem might appear in the ONNX parsing. For some reason, Microsoft decided to use the implicit batch dimension only for TensorRT 6 and explicit batch dimension only for TensorRT 7. It means that the C++ code for TensorRT 6 and 7 will always be incompatible (although experienced people can get around that with C++ preprocessor directives and/or if statements)! Even exporting ONNX from PyTorch is different: for TensorRT 7, you must specify a fixed batch size explicitly or otherwise declare it as “dynamic”, while for TensorRT 6, exporting with a batch of 1 will suffice. Sounds complicated? It is. If you are a TensorRT newbie just making your first baby steps, expect spending many hours frustrated and confused with the explicit/implicit batch dimension issue.

There are many other parsing issues as well. For example, TensorRT 6 usually cannot parse ONNX files created by recent PyTorch versions, so you will have to export on an ancient PyTorch 1.2 (if your network works there). We used a separate Docker container with TensorRT 6 and PyTorch 1.2. While PyTorch networks can only be converted to TensorRT via ONNX, for Tensorflow the UFF format is more popular. Moreover, Tensorflow advertises using TensorRT directly from TensorFlow, but it requires a particular (and usually outdated) TensorRT version and is less flexible, so we suggest using UFF or ONNX instead.

FP16 and INT8 Issues:

For quite some time we could not make INT8 work at all, even on the simplest possible example. We put all the required flags in the code, and TensorRT optimizer just produced an engine with the FP32 layer. We tried everything, and it just did not work. It took us ages to realize that the problem was exactly that we used the simplest possible example. It turns out TensorRT optimizes the entire network and will switch INT8 on only for the layers where it is available and only if it speeds things up. It means in practice that you need at least two convolutional layers in a row, and with ReLU activations to see INT8.

There are many other issues with INT8 and FP16. They require tensor cores, and not all GPUs have those (Xavier devices and newer GeForces do). Network accuracy can degrade significantly compared to FP32, especially for INT8. INT8 requires calibration, and you must write your own C++ calibrator class. The good news is that you can calibrate once, save the calibration table, then use it every time you build an INT8 engine. Building an engine with FP16 and especially INT8 takes much longer time than FP32. Still, about 3x acceleration of network inference is worth it.

A Note on Docker:

On a Linux PC, you can use Docker to keep different incompatible versions of CUDA, TensorFlow, TensorRT, PyTorch, etc. on the same computer. In particular,  TensorFlow 1x and 2x are seriously incompatible, just as TensorRT 6x and 7x are, and every version of TensorFlow and TensorRT requires a very particular version of CUDA and CuDNN. On Jetson devices, you are unfortunately stuck with versions provided with your JetPack (Ubuntu-like Linux for Jetson). Different versions of JetPack have different TensorRT versions, but reinstalling JetPack is not easy (as discussed in the blog post).

We strongly recommend you export ONNX or UFF on your PC, and then use it to build the TensorRT engine on a Jetson. You should also write your C++ TensorRT code on a PC before trying it on a Jetson, optionally using a Docker container with the same TensorRT version you have on the Jetson.

A Note on Parallelization:

TensorRT optimizes an engine to infer as fast as possible. This means loading all GPU cores to a maximum and that you cannot gain anything by inferring two or more networks in parallel. And you cannot limit the number of CUDA cores used by TensorRT, it always uses them all. You might think that if one network uses FP32 (CUDA cores), while the other uses FP16/INT8 (tensor cores) they might run in parallel. We tested this a lot, it does not work either.

To summarize: Do not try to infer two or more TensorRT neural networks simultaneously.

Nvidia Deep Learning Accelerator (DLA)

You might have heard of Nvidia Deep Learning Accelerator (DLA), the open-source architecture from Nvidia for all deep learning accelerators. You can use it via the native DLA API or via TensorRT, the second option is preferred.

Sounds quite promising, isn’t it? However, there are even more problems in practice in comparison with the above considered TensorRT engine. Here are some of the bottlenecks:

  1. First of all, DLA is supported on a Jetson Xavier only. Also please note, that tensor cores and DLA are two different things!
  2. DLA supports only FP16 and INT8 (not FP32), and the accuracy of INT8 inference drastically drops for all networks we tried (compared to INT8 inference on the GPU).
  3. The word “accelerator” is misleading, DLA is in fact very slow compared to Xavier GPU, FP16 on DLA is about as slow as FP32 on GPU.
  4. As with all DL accelerators, the selection of supported layers is very limited. For example, DLA does not have Constant layers. The deconvolution layer exists in theory, but without any padding, which is unsuitable for encoder-decoder networks (semantic segmentation, optical flow, etc.), which always use deconvolutions with padding.
  5. When some layer is not available on DLA, TensorRT will run it on the GPU instead (“GPU fallback”). Expect a lot of those.
  6. You might think that you can run a network on DLA (or even two on two DLA cores of Xavier) while having the third one running on the GPU? This does not work either. DLA networks work fine (and you can run two on two DLA cores), but the GPU is almost completely blocked by them for some reason. We tested the 2 DLA networks + 1 GPU networks. Both DLA networks ran at full speed, but the GPU network was slowed down 3 times or more.

In other words, like most DL accelerators (see below), Nvidia DLA suffers from the tragic State Of The Art Curse in its utmost severity. Most existing networks, and especially those fancy 2020 SOTA networks from Github, cannot run on the pure DLA.

Deep Learning on Non-Nvidia Devices

For non-Nvidia devices, you have 3 options for neural network inference:

  • CPU inference
  • Non-Nvidia GPU inference
  • Deep learning accelerators

CPU inference is rather trivial and only suitable for very lightweight neural networks. If you do not want to write the code yourself, you can always use CPU versions of Tensorflow Lite, Libtorch or ONNX runtime, or perhaps something like Frugally Deep.

While there have been some attempts to implement neural networks on non-Nvidia GPUs (including the ones in Raspberry Pi, and also AMD and Intel), they are very experimental and often unfinished and lack the standardization and sophistication of the Nvidia-GPU frameworks like PyTorch or TensorRT.

The third option is more interesting. There are a number of devices on the market specially designed for deep learning, and they are equipped with DL accelerators. This includes Google Coral series (available as single-board and USB stick) and Intel Neural Compute Stick Series (previously known as Movidius) (Fig. 2). They are often combined with other devices, for example, a Google or Intel USB stick is plugged into a Raspberry Pi. Google Coral devices have a DL accelerator called  Edge TPU, a smaller cousin of Google TPU used in Google cloud. It is used via a special version of Tensorflow Lite, and your network needs conversion before you can run it on the TPU. Intel’s accelerator is called Intel VPU, and it is accessible through a special software named OpenVINO Toolkit. Again, network conversion is needed. Technically, two Nvidia DLA cores in Jetson Xavier is also a DL accelerator similar to TPU and VPU, and even tensor cores in modern Nvidia GPUs function in a similar way.

All DL accelerators work in precision FP16 or INT8. They do not support FP32. And the older models typically only support INT8. They are highly optimized for a few standard operations, but their instruction set is very limited. DL accelerators typically come with a “Model Zoo”, a small number of outdated neural networks. You can be pretty sure that no network from Github will work, at least not without serious modification.

It is often discussed in the Deep Learning community whether DL accelerators are a good or a bad thing. DL is a very active field of computer science at the moment, with new network architectures appearing every day. Every year or so the whole field changes beyond recognition. New building blocks (network layers) appear all the time and gradually become popular, for example, Transformers. New ideas are usually tested with custom CUDA plugins first, as they are not yet available in any framework. Frameworks like PyTorch try their best to incorporate new ideas quickly. However it is much harder to design a new piece of hardware, thus DL accelerators always fall years behind. For this reason, it is often asked if they have any usefulness for the DL community at all, at least until the field stops growing that quickly.

My answer is the following. If you want to run something very simple from the zoo (like YOLO) on the edge, and if you don’t care at all about State Of The Art, DL accelerators are for you. However, if you like to fool around with different new and fancy neural networks, then you must not stray from the Nvidia camp, and if you ever get ready for deployment, choose a Jetson Xavier or TX2 or something like it.

Summary

Deep neural nets are very exciting but you need to know how to cook them right. Especially, when your target hardware has limited capabilities and performance requirements are strict. We’ve shared some practical insights around the topic. So, what’s next?

Part 3 of the series is going to deal with video streaming and efficient video pipelines. Stay tuned!

Comparative Analysis of Classic Computer Vision Methods and Deep Convolutional Neural Networks for Floor Segmentation

In the paper, we analyze the problem of automatic room floor segmentation. For this purpose, we consider several classic computer vision algorithms as well as some of the deep convolutional neural network architectures. The segmentation results are illustrated and compared. An idea for combining two groups of methods is proposed. It is demonstrated that a proper fusion provides the best segmentation quality.

9th DataScienceUA Conference

Data Science UA will gather participants from all over the world at the 9th Data Science UA Conference which will be held online on November 20th, 2020.
The conference will last for 24 hours non-stop consisting of three significant tracks: Technical track, Workshops track, and Business track.
Speakers from TOP companies like Amazon, Facebook AI, Airbus, Nvidia, Google, IBM and others are going to share experiences and discuss as much as possible about how AI transforms the world today and what is going to be tomorrow.