Depth estimation

Depth estimation with event cameras is possible by applying the same approach of disparity calculation on a calibrated stereo camera rig. The straightforward approach is to accumulate frames from events on both cameras and use the same disparity estimation algorithm. This approach might have some limitations, since accumulating events might result in suboptimal results due to low texture available in an accumulated frame.

The dv-processing library provides the dv::camera::StereoGeometry and a few disparity estimation algorithms that, in combination, can be used to build a depth estimation pipeline.

Semi-dense stereo block matching

Dense block matching here refers to the most straightforward approach: accumulating full frames and running a conventional disparity estimation on top to estimate depth. Since the accumulated frames only contain limited texture due to pixels reacting to brightness changes - this approach is referred to as semi-dense. The SemiDenseStereoMatcher class wraps the disparity estimation part, where estimated disparity can be used to calculate depth with dv::camera::StereoGeometry.

Following sample code show the use of SemiDenseStereoMatcher with dv::camera::StereoGeometry to run a real-time depth estimation pipeline on a calibration stereo camera.

  1#include <dv-processing/camera/calibration_set.hpp>
  2#include <dv-processing/core/stereo_event_stream_slicer.hpp>
  3#include <dv-processing/depth/semi_dense_stereo_matcher.hpp>
  4#include <dv-processing/io/camera/discovery.hpp>
  5#include <dv-processing/noise/background_activity_noise_filter.hpp>
  6
  7#include <opencv2/highgui.hpp>
  8
  9int main() {
 10    using namespace std::chrono_literals;
 11
 12    // Path to a stereo calibration file, replace with a file path on your local file system
 13    const std::string calibrationFilePath = "path/to/calibration.json";
 14
 15    // Load the calibration file
 16    auto calibration = dv::camera::CalibrationSet::LoadFromFile(calibrationFilePath);
 17
 18    // It is expected that calibration file will have "C0" as the leftEventBuffer camera
 19    auto leftCameraCalib = calibration.getCameraCalibration("C0").value();
 20
 21    // The second camera is assumed to be rightEventBuffer-side camera
 22    auto rightCameraCalib = calibration.getCameraCalibration("C1").value();
 23
 24    // Open the stereo camera with camera names from calibration
 25    auto leftCamera  = dv::io::camera::openSync(leftCameraCalib.name);
 26    auto rightCamera = dv::io::camera::openSync(rightCameraCalib.name);
 27
 28    dv::io::camera::synchronizeAnyTwo(leftCamera, rightCamera);
 29
 30    // Make sure both cameras support event stream output, throw an error otherwise
 31    if (!leftCamera->isEventStreamAvailable() || !rightCamera->isEventStreamAvailable()) {
 32        throw dv::exceptions::RuntimeError("Input camera does not provide an event stream.");
 33    }
 34
 35    // Initialize a stereo block matcher with a stereo geometry from calibration and the preconfigured SGBM instance
 36    dv::SemiDenseStereoMatcher blockMatcher(
 37        std::make_unique<dv::camera::StereoGeometry>(leftCameraCalib, rightCameraCalib));
 38
 39    // Initialization of a stereo event sliver
 40    dv::StereoEventStreamSlicer slicer;
 41
 42    // Initialize a window to show previews of the output
 43    cv::namedWindow("Preview", cv::WINDOW_NORMAL);
 44
 45    // Local event buffers to implement overlapping window of events for accumulation
 46    dv::EventStore leftEventBuffer, rightEventBuffer;
 47
 48    // Use one third of the resolution as count of events per accumulated frame
 49    const size_t eventCount = static_cast<size_t>(leftCameraCalib.resolution.area()) / 3;
 50
 51    // Register a callback to be done at 30Hz
 52    slicer.doEveryTimeInterval(33ms, [&blockMatcher, &leftEventBuffer, &rightEventBuffer, eventCount](
 53                                         const auto &leftEvents, const auto &rightEvents) {
 54        // Push input events into the local buffers
 55        leftEventBuffer.add(leftEvents);
 56        rightEventBuffer.add(rightEvents);
 57
 58        // If the number of events is above the count, just keep the latest events
 59        if (leftEventBuffer.size() > eventCount) {
 60            leftEventBuffer = leftEventBuffer.sliceBack(eventCount);
 61        }
 62        if (rightEventBuffer.size() > eventCount) {
 63            rightEventBuffer = rightEventBuffer.sliceBack(eventCount);
 64        }
 65
 66        // Pass these events into block matcher and estimate disparity, the matcher will accumulate frames
 67        // internally. The disparity output is 16-bit integer, that has sub-pixel precision.
 68        const auto disparity = blockMatcher.computeDisparity(leftEventBuffer, rightEventBuffer);
 69
 70        // Convert disparity into 8-bit integers with scaling and normalize the output for a nice preview.
 71        // This loses the actual numeric value of the disparity, but it's a nice way to visualize the disparity.
 72        cv::Mat disparityU8, disparityColored;
 73        disparity.convertTo(disparityU8, CV_8UC1, 1.0 / 16.0);
 74        cv::normalize(disparityU8, disparityU8, 0, 255, cv::NORM_MINMAX);
 75
 76        // Convert the accumulated frames into colored images for preview.
 77        std::vector<cv::Mat> images(3);
 78        cv::cvtColor(blockMatcher.getLeftFrame().image, images[0], cv::COLOR_GRAY2BGR);
 79        cv::cvtColor(blockMatcher.getRightFrame().image, images[1], cv::COLOR_GRAY2BGR);
 80
 81        // Apply color-mapping to the disparity image, this will encode depth with color: red - close; blue - far.
 82        cv::applyColorMap(disparityU8, images[2], cv::COLORMAP_JET);
 83
 84        // Concatenate images and show them in a window
 85        cv::Mat preview;
 86        cv::hconcat(images, preview);
 87        cv::imshow("Preview", preview);
 88    });
 89
 90    // Buffer input events in these variables to synchronize inputs
 91    std::optional<dv::EventStore> leftEvents  = std::nullopt;
 92    std::optional<dv::EventStore> rightEvents = std::nullopt;
 93
 94    // Run the processing loop while both cameras are connected
 95    while (leftCamera->isRunning() && rightCamera->isRunning()) {
 96        // Read events from respective left / right cameras
 97        if (!leftEvents.has_value()) {
 98            leftEvents = leftCamera->getNextEventBatch();
 99        }
100        if (!rightEvents.has_value()) {
101            rightEvents = rightCamera->getNextEventBatch();
102        }
103
104        // Feed the data into the slicer and reset the buffer
105        if (leftEvents && rightEvents) {
106            slicer.accept(*leftEvents, *rightEvents);
107            leftEvents  = std::nullopt;
108            rightEvents = std::nullopt;
109        }
110
111        // Wait for a small amount of time to avoid CPU overhaul
112        cv::waitKey(1);
113    }
114
115    return 0;
116}
_images/semi-dense.png

Expected result of semi-dense disparity estimation. The output provides two accumulated frames and color-coded disparity map.

Note

Disparity map yields results only in areas with visible texture, areas without texture contain speckle noise.

Sparse disparity estimation

The semi-dense appraoch is a straightforward to stereo disparity estimation. Another approach is to perform disparity estimation on sparse selected regions within accumulated image. Sparse estimation approach allows the implementation to select regions with enough texture to be selected for the disparity, reducing computational complexity and improving quality. The sparse approach takes point coordinates of where the disparity needs to be estimated, performs sparse accumulation only in the regions where disparity matching actually needs to happen and runs correlation based template matching of left image patches on the right camera image. Each template is matched against the other image on a horizontal line using normalized correlation coefficient (Pearson correlation) and the best scoring match is considered to be the correct match and according disparity is assigned to that point.

The following sample code shows the use of sparse disparity block matcher with a live calibrated stereo camera:

  1#include <dv-processing/camera/calibration_set.hpp>
  2#include <dv-processing/cluster/mean_shift/event_store_adaptor.hpp>
  3#include <dv-processing/core/stereo_event_stream_slicer.hpp>
  4#include <dv-processing/data/utilities.hpp>
  5#include <dv-processing/depth/sparse_event_block_matcher.hpp>
  6#include <dv-processing/io/camera/discovery.hpp>
  7#include <dv-processing/visualization/colors.hpp>
  8
  9#include <opencv2/highgui.hpp>
 10
 11int main() {
 12    using namespace std::chrono_literals;
 13
 14    // Path to a stereo calibration file, replace with a file path on your local file system
 15    const std::string calibrationFilePath = "path/to/calibration.json";
 16
 17    // Load the calibration file
 18    auto calibration = dv::camera::CalibrationSet::LoadFromFile(calibrationFilePath);
 19
 20    // It is expected that calibration file will have "C0" as the leftEventBuffer camera
 21    auto leftCameraCalib = calibration.getCameraCalibration("C0").value();
 22
 23    // The second camera is assumed to be rightEventBuffer-side camera
 24    auto rightCameraCalib = calibration.getCameraCalibration("C1").value();
 25
 26    // Open the stereo camera with camera names from calibration
 27    auto leftCamera  = dv::io::camera::openSync(leftCameraCalib.name);
 28    auto rightCamera = dv::io::camera::openSync(rightCameraCalib.name);
 29
 30    dv::io::camera::synchronizeAnyTwo(leftCamera, rightCamera);
 31
 32    // Make sure both cameras support event stream output, throw an error otherwise
 33    if (!leftCamera->isEventStreamAvailable() || !rightCamera->isEventStreamAvailable()) {
 34        throw dv::exceptions::RuntimeError("Input camera does not provide an event stream.");
 35    }
 36
 37    // Matching window size for the block matcher
 38    const cv::Size window(24, 24);
 39    // Minimum disparity value to measure
 40    const int minDisparity = 0;
 41    // Maximum disparity value
 42    const int maxDisparity = 40;
 43    // Minimum z-score value that a valid match can have
 44    const float minScore = 0.0f;
 45
 46    // Initialize the block matcher with rectification
 47    auto matcher
 48        = dv::SparseEventBlockMatcher(std::make_unique<dv::camera::StereoGeometry>(leftCameraCalib, rightCameraCalib),
 49            window, maxDisparity, minDisparity, minScore);
 50
 51    // Initialization of a stereo event sliver
 52    dv::StereoEventStreamSlicer slicer;
 53
 54    // Initialize a window to show previews of the output
 55    cv::namedWindow("Preview", cv::WINDOW_NORMAL);
 56
 57    // Local event buffers to implement overlapping window of events for accumulation
 58    dv::EventStore leftEventBuffer, rightEventBuffer;
 59
 60    // Use one third of the resolution as count of events per accumulated frame
 61    const size_t eventCount = static_cast<size_t>(leftCameraCalib.resolution.area()) / 3;
 62
 63    // Register a callback to be done at 50Hz
 64    slicer.doEveryTimeInterval(20ms, [&matcher, &leftEventBuffer, &rightEventBuffer, eventCount, &window](
 65                                         const auto &leftEvents, const auto &rightEvents) {
 66        // Push input events into the local buffers
 67        leftEventBuffer.add(leftEvents);
 68        rightEventBuffer.add(rightEvents);
 69
 70        // If the number of events is above the count, just keep the latest events
 71        if (leftEventBuffer.size() > eventCount) {
 72            leftEventBuffer = leftEventBuffer.sliceBack(eventCount);
 73        }
 74        if (rightEventBuffer.size() > eventCount) {
 75            rightEventBuffer = rightEventBuffer.sliceBack(eventCount);
 76        }
 77
 78        // Number of clusters to extract
 79        constexpr int numClusters = 100;
 80
 81        // Initialize the mean-shift clustering algorithm
 82        dv::cluster::mean_shift::MeanShiftEventStoreAdaptor meanShift(leftEventBuffer, 10.f, 1.0f, 20, numClusters);
 83
 84        // Find cluster centers which are going to be used for disparity estimation
 85        auto centers = meanShift.findClusterCentres<dv::cluster::mean_shift::kernel::Epanechnikov>();
 86
 87        // Run disparity estimation, the output will contain a disparity estimate for each of the given points.
 88        const std::vector<dv::SparseEventBlockMatcher::PixelDisparity> estimates
 89            = matcher.computeDisparitySparse(leftEventBuffer, rightEventBuffer, dv::data::convertToCvPoints(centers));
 90
 91        // Convert the accumulated frames into colored images for preview.
 92        std::vector<cv::Mat> images(2);
 93        cv::cvtColor(matcher.getLeftFrame().image, images[0], cv::COLOR_GRAY2BGR);
 94        cv::cvtColor(matcher.getRightFrame().image, images[1], cv::COLOR_GRAY2BGR);
 95
 96        // Visualize the matched blocks
 97        int32_t index = 0;
 98        for (const auto &point : estimates) {
 99            // If point estimation is invalid, do not show a preview of it
100            if (!point.valid) {
101                continue;
102            }
103
104            // The rest of the code performs drawing of the match according to the disparity value on the
105            // preview images.
106            const cv::Scalar color = dv::visualization::colors::someNeonColor(index++);
107            // Draw some nice colored markers and rectangles.
108            cv::drawMarker(images[1], *point.matchedPosition, color, cv::MARKER_CROSS, 7);
109            cv::rectangle(images[1],
110                cv::Rect(point.matchedPosition->x - (window.width / 2), point.matchedPosition->y - (window.height / 2),
111                    window.width, window.height),
112                color);
113            cv::rectangle(images[0],
114                cv::Rect(point.templatePosition->x - (window.width / 2),
115                    point.templatePosition->y - (window.height / 2), window.width, window.height),
116                color);
117        }
118
119        // Concatenate images and show them in a window
120        cv::Mat preview;
121        cv::hconcat(images, preview);
122        cv::imshow("Preview", preview);
123    });
124
125    // Buffer input events in these variables to synchronize inputs
126    std::optional<dv::EventStore> leftEvents  = std::nullopt;
127    std::optional<dv::EventStore> rightEvents = std::nullopt;
128
129    // Run the processing loop while both cameras are connected
130    while (leftCamera->isRunning() && rightCamera->isRunning()) {
131        // Read events from respective left / right cameras
132        if (!leftEvents.has_value()) {
133            leftEvents = leftCamera->getNextEventBatch();
134        }
135        if (!rightEvents.has_value()) {
136            rightEvents = rightCamera->getNextEventBatch();
137        }
138
139        // Feed the data into the slicer and reset the buffer
140        if (leftEvents && rightEvents) {
141            slicer.accept(*leftEvents, *rightEvents);
142            leftEvents  = std::nullopt;
143            rightEvents = std::nullopt;
144        }
145
146        // Wait for a small amount of time to avoid CPU overhaul
147        cv::waitKey(1);
148    }
149
150    return 0;
151}
_images/sparse-disparity.png

Expected result of sparse disparity estimation. The colored rectangles represent sparse blocks that are matched on the right side image. Block colors are matched on both images. Note that frame are sparse as well - the accumulation happens only in relevant areas around points of interest. The points of interest are selected on high density event areas as per mean-shift cluster extraction.