VPI - Vision Programming Interface

0.3.7 Release

Fisheye Distortion Correction

Overview

This sample application performs a fisheye lens calibration using input images taken with the same camera/lens. Then it uses Image Remap and the calibration data to correct fisheye lens distortion of these images and save the result to disk. The mapping used for distortion correction is VPI_FISHEYE_EQUIDISTANT, which maps straight lines in the scene to straight lines in the corrected image.

This sample shows the following:

Lens Calibration

Lens calibration uses a set of images taken by the same camera/lens, each one showing a checkerboard pattern in a different position, so that taken collectively, the checkerboard appears in almost entire field of view. The more images, the more accurate the calibration will be, but typically 10 to 15 images suffice.

VPI samples include a set of input images that can be used. They are found in /opt/nvidia/vpi-0.3/samples/assets/fisheye directory.

To create a set of calibration images for a given lens, do the following:

  1. Print a checkerboard pattern on a piece of paper. VPI provides in samples' assets directory one 10x7 checkerboard file that can be used, named checkerboard_10x7.pdf.
  2. Mount the fisheye lens on a camera.
  3. With the camera in a fixed position, take several pictures showing the checkerboard in different positions, covering a good part of the field of view.

Instructions

The usage is:

./vpi_sample_11_fisheye -c W,H [-s win] <image1> [image2] [image3] ...

where

  • -c W,H: specifies the number of squares the checkerboard pattern has horizontally (W) and vertically (H).
  • -s win: (optional) the width of a window around each internal vertex of the checkerboard (point where 4 squares meet) to be used in a vertex position refinement stage. The actual vertex position will be searched within this window. If this parameter is omitted, the refinement stage will be skipped.
  • imageN: set of calibration images
Note
Since currently only the PVA backend implements Image Remap, and only on Jetson Xavier series, this sample can be run solely in these devices.

Here's one invocation example:

./vpi_sample_11_fisheye -c 10,7 -s 22 ../assets/fisheye/*.jpg

This will correct the included set of calibration images, all captured using the checkerboard pattern also included. It's using a 22x22 window around each checkerboard internal vertex to refine the vertex position.

Results

Here are some input and output images produced by the sample application:

InputCorrected

Source code

For convenience, here's the code that is also installed in the samples directory.

/*
* Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of NVIDIA CORPORATION nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <opencv2/core/version.hpp>
#if CV_MAJOR_VERSION >= 3
# include <opencv2/imgcodecs.hpp>
#else
# include <opencv2/highgui/highgui.hpp>
#endif
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <string.h> // for basename(3) that doesn't modify its argument
#include <unistd.h> // for getopt
#include <vpi/Context.h>
#include <vpi/Event.h>
#include <vpi/Image.h>
#include <vpi/Stream.h>
#include <iostream>
#define CHECK_STATUS(STMT) \
do \
{ \
VPIStatus status = (STMT); \
if (status != VPI_SUCCESS) \
{ \
throw std::runtime_error(vpiStatusGetName(status)); \
} \
} while (0);
static void PrintUsage(const char *progname, std::ostream &out)
{
out << "Usage: " << progname << " <-c W,H> [-s win] <image1> [image2] [image3] ...\n"
<< " where,\n"
<< " W,H\tcheckerboard with WxH squares\n"
<< " win\tsearch window width around checkerboard vertex used\n"
<< "\tin refinement, default is 0 (disable refinement)\n"
<< " imageN\tinput images taken with a fisheye lens camera" << std::endl;
}
struct Params
{
cv::Size vtxCount; // Number of internal vertices the checkerboard has
int searchWinSize; // search window size around the checkerboard vertex for refinement.
std::vector<const char *> images; // input image names.
};
static Params ParseParameters(int argc, char *argv[])
{
Params params = {};
cv::Size cbSize;
opterr = 0;
int opt;
while ((opt = getopt(argc, argv, "hc:s:")) != -1)
{
switch (opt)
{
case 'h':
PrintUsage(basename(argv[0]), std::cout);
return {};
case 'c':
if (sscanf(optarg, "%u,%u", &cbSize.width, &cbSize.height) != 2)
{
throw std::invalid_argument("Error parsing checkerboard information");
}
// OpenCV expects number of interior vertices in the checkerboard,
// not number of squares. Let's adjust for that.
params.vtxCount.width = cbSize.width - 1;
params.vtxCount.height = cbSize.height - 1;
break;
case 's':
if (sscanf(optarg, "%d", &params.searchWinSize) != 1)
{
throw std::invalid_argument("Error parseing search window size");
}
if (params.searchWinSize < 0)
{
throw std::invalid_argument("Search window size must be >= 0");
}
break;
case '?':
throw std::invalid_argument(std::string("Option -") + (char)optopt + " not recognized");
}
}
for (int i = optind; i < argc; ++i)
{
params.images.push_back(argv[i]);
}
if (params.images.empty())
{
throw std::invalid_argument("At least one image must be defined");
}
if (cbSize.width <= 3 || cbSize.height <= 3)
{
throw std::invalid_argument("Checkerboard size must have at least 3x3 squares");
}
if (params.searchWinSize == 1)
{
throw std::invalid_argument("Search window size must be 0 (default) or >= 2");
}
return params;
}
int main(int argc, char *argv[])
{
// We'll create all vpi objects under this context, so that
// we don't have to track what objects to destroy. Just destroying
// the context will destroy all objects.
VPIContext ctx = 0;
try
{
// First parse command line paramers
Params params = ParseParameters(argc, argv);
if (params.images.empty()) // user just wanted the help message?
{
return 0;
}
// Where to store checkerboard 2D corners of each input image.
std::vector<std::vector<cv::Point2f>> corners2D;
// Store image size. All input images must have same size.
cv::Size imgSize = {};
for (unsigned i = 0; i < params.images.size(); ++i)
{
// Load input image and do some sanity check
cv::Mat img = cv::imread(params.images[i]);
if (img.empty())
{
throw std::runtime_error("Can't read " + std::string(params.images[i]));
}
if (imgSize == cv::Size{})
{
imgSize = img.size();
}
else if (imgSize != img.size())
{
throw std::runtime_error("All images must have same size");
}
// Find the checkerboard pattern on the image, saving the 2D
// coordinates of checkerboard vertices in cbVertices.
// Vertex is the point where 4 squares (2 white and 2 black) meet.
std::vector<cv::Point2f> cbVertices;
if (findChessboardCorners(img, params.vtxCount, cbVertices,
cv::CALIB_CB_ADAPTIVE_THRESH + cv::CALIB_CB_NORMALIZE_IMAGE))
{
// Needs to perform further corner refinement?
if (params.searchWinSize >= 2)
{
cv::Mat gray;
cvtColor(img, gray, cv::COLOR_BGR2GRAY);
cornerSubPix(gray, cbVertices, cv::Size(params.searchWinSize / 2, params.searchWinSize / 2),
cv::Size(-1, -1),
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.0001));
}
// save this image's 2D vertices in vector
corners2D.push_back(std::move(cbVertices));
}
else
{
std::cerr << "Warning: checkerboard pattern not found in image " << params.images[i] << std::endl;
}
}
// Create the vector that stores 3D coordinates for each checkerboard pattern on a space
// where X and Y are orthogonal and run along the checkerboard sides, and Z==0 in all points on
// checkerboard.
std::vector<cv::Point3f> initialCheckerboard3DVertices;
for (int i = 0; i < params.vtxCount.height; ++i)
{
for (int j = 0; j < params.vtxCount.width; ++j)
{
// since we're not interested in extrinsic camera parameters,
// we can assume that checkerboard square size is 1x1.
initialCheckerboard3DVertices.emplace_back(j, i, 0);
}
}
// Initialize a vector with initial checkerboard positions for all images
std::vector<std::vector<cv::Point3f>> corners3D(corners2D.size(), initialCheckerboard3DVertices);
// Camera intrinsic parameters, initially identity (will be estimated by calibration process).
using Mat3 = cv::Matx<double, 3, 3>;
Mat3 camMatrix = Mat3::eye();
// stores the fisheye model coefficients.
std::vector<double> coeffs(4);
// VPI currently doesn't support skew parameter on camera matrix, make sure
// calibration process fixes it to 0.
int flags = cv::fisheye::CALIB_FIX_SKEW;
// Run calibration
{
cv::Mat rvecs, tvecs; // stores rotation and translation for each camera, not needed now.
double rms = cv::fisheye::calibrate(corners3D, corners2D, imgSize, camMatrix, coeffs, rvecs, tvecs, flags);
printf("rms error: %lf\n", rms);
}
// Output calibration result.
printf("Fisheye coefficients: %lf %lf %lf %lf\n", coeffs[0], coeffs[1], coeffs[2], coeffs[3]);
printf("Camera matrix:\n");
printf("[%lf %lf %lf; %lf %lf %lf; %lf %lf %lf]\n", camMatrix(0, 0), camMatrix(0, 1), camMatrix(0, 2),
camMatrix(1, 0), camMatrix(1, 1), camMatrix(1, 2), camMatrix(2, 0), camMatrix(2, 1), camMatrix(2, 2));
// Now use VPI to undistort the input images:
// Allocate a dense map.
VPIWarpMap map = {};
map.grid.regionWidth[0] = imgSize.width;
map.grid.regionHeight[0] = imgSize.height;
map.grid.horizInterval[0] = 1;
map.grid.vertInterval[0] = 1;
CHECK_STATUS(vpiWarpMapAllocData(&map));
// Initialize the fisheye lens model with the coefficients given by calibration procedure.
distModel.k1 = coeffs[0];
distModel.k2 = coeffs[1];
distModel.k3 = coeffs[2];
distModel.k4 = coeffs[3];
// Fill up the camera intrinsic parameters given by camera calibration procedure.
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 3; ++j)
{
K[i][j] = camMatrix(i, j);
}
}
// Camera extrinsics is be identity.
X[0][0] = X[1][1] = X[2][2] = 1;
// Generate a warp map to undistort an image taken from fisheye lens with
// given parameters calculated above.
// Create out a vpi context to store all vpi objects we'll create.
CHECK_STATUS(vpiContextCreate(0, &ctx));
// Activate it. From now on all created objects will be owned by it.
CHECK_STATUS(vpiContextSetCurrent(ctx));
// Create a stream for remap operation. Currently it's implemented only on PVA.
VPIStream stream;
CHECK_STATUS(vpiStreamCreate(VPI_DEVICE_TYPE_PVA, &stream));
// remap expects NV12 input/output images. OpenCV gives us images in BGR.
// We'll use VPI to perform color space conversion on CUDA. Let's create a
// stream for it.
VPIStream streamCUDA;
CHECK_STATUS(vpiStreamCreate(VPI_DEVICE_TYPE_CUDA, &streamCUDA));
// Create the ImageRemap payload for undistortion given the map generated above.
VPIPayload remap;
CHECK_STATUS(vpiCreateImageRemap(stream, &map, &remap));
// Temporary input and output images in NV12 format.
VPIImage tmpIn;
CHECK_STATUS(vpiImageCreate(imgSize.width, imgSize.height, VPI_IMAGE_TYPE_NV12, 0, &tmpIn));
VPIImage tmpOut;
CHECK_STATUS(vpiImageCreate(imgSize.width, imgSize.height, VPI_IMAGE_TYPE_NV12, 0, &tmpOut));
// We'll need 3 events for synchronization between the two streams above.
VPIEvent ev1, ev2;
// For each input image,
for (unsigned i = 0; i < params.images.size(); ++i)
{
// Read it from disk.
cv::Mat img = cv::imread(params.images[i]);
assert(!img.empty());
// Wrap it into a VPIImage
VPIImage vimg;
{
VPIImageData imgData;
memset(&imgData, 0, sizeof(imgData));
assert(img.type() == CV_8UC3);
// First fill VPIImageData with the, well, image data...
imgData.numPlanes = 1;
imgData.planes[0].width = img.cols;
imgData.planes[0].height = img.rows;
imgData.planes[0].rowStride = img.step[0];
imgData.planes[0].data = img.data;
// Now create a VPIImage that wraps it.
CHECK_STATUS(vpiImageWrapHostMem(&imgData, 0, &vimg));
}
// Convert BGR -> NV12
CHECK_STATUS(vpiSubmitImageFormatConverter(streamCUDA, vimg, tmpIn, VPI_CONVERSION_CLAMP, 1, 0));
// Make sure remap starts only after conversion finishes.
CHECK_STATUS(vpiEventRecord(ev1, streamCUDA));
CHECK_STATUS(vpiStreamWaitFor(stream, ev1));
// Finaly undistorts the input image.
CHECK_STATUS(vpiSubmitImageRemap(remap, tmpIn, tmpOut, VPI_INTERP_CATMULL_ROM, VPI_BOUNDARY_COND_ZERO));
// Make sure final color space conversion starts only after remap operation finishes.
CHECK_STATUS(vpiEventRecord(ev2, stream));
CHECK_STATUS(vpiStreamWaitFor(streamCUDA, ev2));
// Convert the result NV12 back to BGR, writing back to the input image.
CHECK_STATUS(vpiSubmitImageFormatConverter(streamCUDA, tmpOut, vimg, VPI_CONVERSION_CLAMP, 1, 0));
// Wait until conversion finishes.
CHECK_STATUS(vpiStreamSync(streamCUDA));
// Since vimg is wrapping the OpenCV image, the result is already there.
// We just have to save it to disk.
char buf[64];
snprintf(buf, sizeof(buf), "undistort_%03d.jpg", i);
imwrite(buf, img);
}
}
catch (std::exception &e)
{
std::cerr << "Error: " << e.what() << std::endl;
PrintUsage(basename(argv[0]), std::cerr);
if (ctx != nullptr)
{
}
return 1;
}
if (ctx != nullptr)
{
}
return 0;
}
VPIWarpGrid::numHorizRegions
uint8_t numHorizRegions
Number of regions horizontally.
Definition: WarpGrid.h:158
VPIImagePlane::height
uint32_t height
Height of this plane in pixels.
Definition: Image.h:137
VPIContext
struct VPIContextImpl * VPIContext
A handle to a context.
Definition: Types.h:165
VPIWarpGrid::horizInterval
uint16_t horizInterval[VPI_WARPGRID_MAX_HORIZ_REGIONS_COUNT]
Horizontal spacing between control points within a given region.
Definition: WarpGrid.h:163
vpiEventCreate
VPIStatus vpiEventCreate(uint32_t flags, VPIEvent *event)
Create an event instance with the specified flags.
VPIFisheyeLensDistortionModel::k4
float k4
Definition: LensDistortionModels.h:142
VPIImagePlane::width
uint32_t width
Width of this plane in pixels.
Definition: Image.h:136
VPI_IMAGE_TYPE_NV12
@ VPI_IMAGE_TYPE_NV12
8-bit NV12.
Definition: Types.h:212
VPI_FISHEYE_EQUIDISTANT
@ VPI_FISHEYE_EQUIDISTANT
Specifies the equidistant fisheye mapping.
Definition: LensDistortionModels.h:86
VPI_IMAGE_TYPE_BGR8
@ VPI_IMAGE_TYPE_BGR8
8-bit BGR, B having the lowest address.
Definition: Types.h:217
vpiContextCreate
VPIStatus vpiContextCreate(uint32_t flags, VPIContext *ctx)
Create a context instance.
vpiWarpMapAllocData
VPIStatus vpiWarpMapAllocData(VPIWarpMap *warpMap)
Allocates the warp map's control point array for a given warp grid.
vpiEventRecord
VPIStatus vpiEventRecord(VPIEvent event, VPIStream stream)
Captures in the event the contents of the stream command queue at the time of this call.
VPIWarpGrid::regionHeight
uint16_t regionHeight[VPI_WARPGRID_MAX_VERT_REGIONS_COUNT]
Height of each region.
Definition: WarpGrid.h:162
vpiContextSetCurrent
VPIStatus vpiContextSetCurrent(VPIContext ctx)
Sets the context for the calling thread.
VPIImagePlane::rowStride
uint32_t rowStride
Difference in bytes of beginning of one row and the beginning of the previous.
Definition: Image.h:138
VPIWarpGrid::vertInterval
uint16_t vertInterval[VPI_WARPGRID_MAX_VERT_REGIONS_COUNT]
Vertical spacing between control points within a given region.
Definition: WarpGrid.h:165
vpiStreamCreate
VPIStatus vpiStreamCreate(VPIDeviceType devType, VPIStream *stream)
Create a stream instance.
LensDistortionModels.h
vpiStreamSync
VPIStatus vpiStreamSync(VPIStream stream)
Blocks the calling thread until all submitted commands in this stream queue are done (queue is empty)...
VPIWarpGrid::numVertRegions
uint8_t numVertRegions
Number of regions vertically.
Definition: WarpGrid.h:159
VPIImageData
Stores information about image characteristics and content.
Definition: Image.h:155
VPIStream
struct VPIStreamImpl * VPIStream
A handle to a stream.
Definition: Types.h:177
vpiContextDestroy
void vpiContextDestroy(VPIContext ctx)
Destroy a context instance as well as all resources it owns.
VPI_DEVICE_TYPE_PVA
@ VPI_DEVICE_TYPE_PVA
PVA backend.
Definition: Types.h:558
ImageRemap.h
VPIImageData::planes
VPIImagePlane planes[VPI_MAX_PLANE_COUNT]
Data of all image planes.
Definition: Image.h:162
VPICameraExtrinsic
float VPICameraExtrinsic[3][4]
Camera extrinsic matrix.
Definition: Types.h:587
VPIWarpMap::grid
VPIWarpGrid grid
Warp grid control point structure definition.
Definition: WarpMap.h:90
VPIFisheyeLensDistortionModel::k3
float k3
Definition: LensDistortionModels.h:142
VPI_DEVICE_TYPE_CUDA
@ VPI_DEVICE_TYPE_CUDA
CUDA backend.
Definition: Types.h:557
vpiCreateImageRemap
VPIStatus vpiCreateImageRemap(VPIStream stream, const VPIWarpMap *warpMap, VPIPayload *payload)
Create a payload for Image Remap algorithm.
Image.h
VPI_INTERP_CATMULL_ROM
@ VPI_INTERP_CATMULL_ROM
Alias to fast Catmull-Rom cubic interpolator.
Definition: Types.h:373
Event.h
VPIEvent
struct VPIEventImpl * VPIEvent
A handle to an event.
Definition: Types.h:171
ImageFormatConverter.h
vpiImageWrapHostMem
VPIStatus vpiImageWrapHostMem(const VPIImageData *hostData, uint32_t flags, VPIImage *img)
Create an image object by wrapping around an existing host-memory block.
VPIImage
struct VPIImageImpl * VPIImage
A handle to an image.
Definition: Types.h:183
VPI_CONVERSION_CLAMP
@ VPI_CONVERSION_CLAMP
Clamps input to output's type range.
Definition: Types.h:395
VPIImageData::numPlanes
int32_t numPlanes
Number of planes.
Definition: Image.h:157
VPIWarpGrid::regionWidth
uint16_t regionWidth[VPI_WARPGRID_MAX_HORIZ_REGIONS_COUNT]
Width of each region.
Definition: WarpGrid.h:161
VPI_EVENT_DISABLE_TIMESTAMP
#define VPI_EVENT_DISABLE_TIMESTAMP
disable time-stamping of event signaled state-change for better performance
Definition: Event.h:111
VPI_BOUNDARY_COND_ZERO
@ VPI_BOUNDARY_COND_ZERO
All pixels outside the image are considered to be zero.
Definition: Types.h:270
vpiSubmitImageFormatConverter
VPIStatus vpiSubmitImageFormatConverter(VPIStream stream, VPIImage input, VPIImage output, VPIConversionPolicy convPolicy, float scale, float offset)
Converts the image contents to the desired format, with optional scaling and offset.
VPIPayload
struct VPIPayloadImpl * VPIPayload
A handle to an algorithm payload.
Definition: Types.h:195
VPIImageData::type
VPIImageType type
Image type.
Definition: Image.h:156
vpiImageCreate
VPIStatus vpiImageCreate(uint32_t width, uint32_t height, VPIImageType type, uint32_t flags, VPIImage *img)
Create an empty image instance with the specified flags.
vpiStreamWaitFor
VPIStatus vpiStreamWaitFor(VPIStream stream, VPIEvent event)
Pushes a command that blocks the processing of all future commands by the backend until the event is ...
VPIFisheyeLensDistortionModel
Holds coefficients for fisheye lens distortion model.
Definition: LensDistortionModels.h:138
vpiSubmitImageRemap
VPIStatus vpiSubmitImageRemap(VPIPayload payload, VPIImage input, VPIImage output, VPIInterpolationType interp, VPIBoundaryCond bcond)
Submits the Image Remap operation to the stream associated with the payload.
vpiWarpMapGenerateFromFisheyeLensDistortionModel
VPIStatus vpiWarpMapGenerateFromFisheyeLensDistortionModel(const VPICameraIntrinsic Kin, const VPICameraExtrinsic X, const VPICameraIntrinsic Kout, const VPIFisheyeLensDistortionModel *distModel, VPIWarpMap *warpMap)
Generates a mapping that corrects image distortions caused by fisheye lenses.
Stream.h
VPIImagePlane::data
void * data
Pointer to the first row of this plane.
Definition: Image.h:146
VPIWarpMap
Defines the mapping between input and output images' pixels.
Definition: WarpMap.h:87
VPIFisheyeLensDistortionModel::k2
float k2
Definition: LensDistortionModels.h:142
VPIFisheyeLensDistortionModel::k1
float k1
Definition: LensDistortionModels.h:142
VPICameraIntrinsic
float VPICameraIntrinsic[2][3]
Camera intrinsic matrix.
Definition: Types.h:574
VPIFisheyeLensDistortionModel::mapping
VPIFisheyeMapping mapping
Mapping between pixel angle and pixel distance to image center.
Definition: LensDistortionModels.h:139
Context.h