VPI - Vision Programming Interface

4.0 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 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.

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/vpi4/samples/assets/fisheye directory.

Note
Fisheye python sample only works for OpenCV version <= 4.9

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 command line parameters are:

-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

Here's one invocation example:

  • C++
    ./vpi_sample_11_fisheye -c 10,7 -s 22 ../assets/fisheye/*.jpg
  • Python
    python3 main.py -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.

Language:
27 import sys
28 import vpi
29 import numpy as np
30 from argparse import ArgumentParser
31 import cv2
32 
33 # ============================
34 # Parse command line arguments
35 
36 parser = ArgumentParser()
37 parser.add_argument('-c', metavar='W,H', required=True,
38  help='Checkerboard with WxH squares')
39 
40 parser.add_argument('-s', metavar='win', type=int,
41  help='Search window width around checkerboard verted used in refinement, default is 0 (disable refinement)')
42 
43 parser.add_argument('images', nargs='+',
44  help='Input images taken with a fisheye lens camera')
45 
46 args = parser.parse_args();
47 
48 # Parse checkerboard size
49 try:
50  cbSize = np.array([int(x) for x in args.c.split(',')])
51 except ValueError:
52  exit("Error parsing checkerboard information")
53 
54 # Use predefined calibration values
55 imgSize = None
56 coeffs = np.array([-0.073234, 0.121251, -0.120230, 0.038480])
57 camMatrix = np.array([
58  [440.956750, 0.000000, 659.822931],
59  [0.000000, 439.322113, 451.685233],
60  [0.000000, 0.000000, 1.000000]
61 ])
62 
63 # Load first image to get image size
64 img = cv2.imread(args.images[0])
65 imgSize = (img.shape[1], img.shape[0])
66 
67 # Print out calibration results
68 print("Fisheye coefficients: {}".format(coeffs))
69 print("Camera matrix:")
70 print(camMatrix)
71 
72 # ======================
73 # Undistort input images
74 
75 # Create an uniform grid
76 grid = vpi.WarpGrid(imgSize)
77 
78 # Create undistort warp map from the calibration parameters and the grid
79 undist_map = vpi.WarpMap.fisheye_correction(grid,
80  K=camMatrix[0:2,:], X=np.eye(3,4), coeffs=coeffs,
81  mapping=vpi.FisheyeMapping.EQUIDISTANT)
82 
83 # Go through all input images,
84 idx=0
85 for imgName in args.images:
86  # Load input image and do some sanity check
87  img = cv2.imread(imgName)
88 
89  # Using the CUDA backend,
90  with vpi.Backend.CUDA:
91  # Convert image to NV12_ER, apply the undistortion map and convert image back to RGB8
92  imgCorrected = vpi.asimage(img).convert(vpi.Format.NV12_ER).remap(undist_map, interp=vpi.Interp.CATMULL_ROM).convert(vpi.Format.RGB8)
93 
94  # Write undistorted image to disk
95  cv2.imwrite("undistort_python{}_{:03d}.jpg".format(sys.version_info[0],idx), imgCorrected.cpu())
96  idx += 1
29 #include <opencv2/core/version.hpp>
30 
31 #if CV_MAJOR_VERSION >= 3
32 # include <opencv2/imgcodecs.hpp>
33 #else
34 # include <opencv2/highgui/highgui.hpp>
35 #endif
36 
37 #include <opencv2/calib3d/calib3d.hpp>
38 #include <opencv2/imgproc/imgproc.hpp>
39 #include <vpi/OpenCVInterop.hpp>
40 
41 #include <vpi/Image.h>
43 #include <vpi/Status.h>
44 #include <vpi/Stream.h>
46 #include <vpi/algo/Remap.h>
47 
48 #include <iostream>
49 #include <sstream>
50 
51 #define CHECK_STATUS(STMT) \
52  do \
53  { \
54  VPIStatus status = (STMT); \
55  if (status != VPI_SUCCESS) \
56  { \
57  char buffer[VPI_MAX_STATUS_MESSAGE_LENGTH]; \
58  vpiGetLastStatusMessage(buffer, sizeof(buffer)); \
59  std::ostringstream ss; \
60  ss << vpiStatusGetName(status) << ": " << buffer; \
61  throw std::runtime_error(ss.str()); \
62  } \
63  } while (0);
64 
65 static void PrintUsage(const char *progname, std::ostream &out)
66 {
67  out << "Usage: " << progname << " <-c W,H> [-s win] <image1> [image2] [image3] ...\n"
68  << " where,\n"
69  << " W,H\tcheckerboard with WxH squares\n"
70  << " win\tsearch window width around checkerboard vertex used\n"
71  << "\tin refinement, default is 0 (disable refinement)\n"
72  << " imageN\tinput images taken with a fisheye lens camera" << std::endl;
73 }
74 
75 static char *my_basename(char *path)
76 {
77 #ifdef WIN32
78  char *name = strrchr(path, '\\');
79 #else
80  char *name = strrchr(path, '/');
81 #endif
82  if (name != NULL)
83  {
84  return name;
85  }
86  else
87  {
88  return path;
89  }
90 }
91 
92 struct Params
93 {
94  cv::Size vtxCount; // Number of internal vertices the checkerboard has
95  int searchWinSize; // search window size around the checkerboard vertex for refinement.
96  std::vector<const char *> images; // input image names.
97 };
98 
99 static Params ParseParameters(int argc, char *argv[])
100 {
101  Params params = {};
102 
103  cv::Size cbSize;
104 
105  for (int i = 1; i < argc; ++i)
106  {
107  if (argv[i][0] == '-')
108  {
109  if (strlen(argv[i] + 1) == 1)
110  {
111  switch (argv[i][1])
112  {
113  case 'h':
114  PrintUsage(my_basename(argv[0]), std::cout);
115  return {};
116 
117  case 'c':
118  if (i == argc - 1)
119  {
120  throw std::invalid_argument("Option -c must be followed by checkerboard width and height");
121  }
122 
123  if (sscanf(argv[++i], "%d,%d", &cbSize.width, &cbSize.height) != 2)
124  {
125  throw std::invalid_argument("Error parsing checkerboard information");
126  }
127 
128  // OpenCV expects number of interior vertices in the checkerboard,
129  // not number of squares. Let's adjust for that.
130  params.vtxCount.width = cbSize.width - 1;
131  params.vtxCount.height = cbSize.height - 1;
132  break;
133 
134  case 's':
135  if (i == argc - 1)
136  {
137  throw std::invalid_argument("Option -s must be followed by search window size");
138  }
139  if (sscanf(argv[++i], "%d", &params.searchWinSize) != 1)
140  {
141  throw std::invalid_argument("Error parsing search window size");
142  }
143  if (params.searchWinSize < 0)
144  {
145  throw std::invalid_argument("Search window size must be >= 0");
146  }
147  break;
148 
149  default:
150  throw std::invalid_argument(std::string("Option -") + (argv[i] + 1) + " not recognized");
151  }
152  }
153  else
154  {
155  throw std::invalid_argument(std::string("Option -") + (argv[i] + 1) + " not recognized");
156  }
157  }
158  else
159  {
160  params.images.push_back(argv[i]);
161  }
162  }
163 
164  if (params.images.empty())
165  {
166  throw std::invalid_argument("At least one image must be defined");
167  }
168 
169  if (cbSize.width <= 3 || cbSize.height <= 3)
170  {
171  throw std::invalid_argument("Checkerboard size must have at least 3x3 squares");
172  }
173 
174  if (params.searchWinSize == 1)
175  {
176  throw std::invalid_argument("Search window size must be 0 (default) or >= 2");
177  }
178 
179  return params;
180 }
181 
182 int main(int argc, char *argv[])
183 {
184  // OpenCV image that will be wrapped by a VPIImage.
185  // Define it here so that it's destroyed *after* wrapper is destroyed
186  cv::Mat cvImage;
187 
188  // VPI objects that will be used
189  VPIStream stream = NULL;
190  VPIPayload remap = NULL;
191  VPIImage tmpIn = NULL, tmpOut = NULL;
192  VPIImage vimg = nullptr;
193 
194  int retval = 0;
195 
196  try
197  {
198  // First parse command line paramers
199  Params params = ParseParameters(argc, argv);
200  if (params.images.empty()) // user just wanted the help message?
201  {
202  return 0;
203  }
204 
205  // Store image size from first image
206  cv::Size imgSize = {};
207  cv::Mat firstImg = cv::imread(params.images[0]);
208  if (firstImg.empty())
209  {
210  throw std::runtime_error("Can't read " + std::string(params.images[0]));
211  }
212  imgSize = firstImg.size();
213 
214  // Pre-defined calibration values
215  using Mat3 = cv::Matx<double, 3, 3>;
216  Mat3 camMatrix = Mat3::eye();
217  std::vector<double> coeffs(4);
218 
219  // Set fisheye coefficients
220  coeffs[0] = -0.073234;
221  coeffs[1] = 0.121251;
222  coeffs[2] = -0.120230;
223  coeffs[3] = 0.038480;
224 
225  // Set camera matrix
226  camMatrix(0, 0) = 440.956750;
227  camMatrix(0, 2) = 659.822931;
228  camMatrix(1, 1) = 439.322113;
229  camMatrix(1, 2) = 451.685233;
230  camMatrix(2, 2) = 1.0;
231 
232  // Output calibration values being used
233  printf("Using pre-defined calibration values:\n");
234  printf("Fisheye coefficients: %lf %lf %lf %lf\n", coeffs[0], coeffs[1], coeffs[2], coeffs[3]);
235  printf("Camera matrix:\n");
236  printf("[%lf %lf %lf; %lf %lf %lf; %lf %lf %lf]\n", camMatrix(0, 0), camMatrix(0, 1), camMatrix(0, 2),
237  camMatrix(1, 0), camMatrix(1, 1), camMatrix(1, 2), camMatrix(2, 0), camMatrix(2, 1), camMatrix(2, 2));
238 
239  // Now use VPI to undistort the input images:
240  // Allocate a dense map.
241  VPIWarpMap map = {};
242  map.grid.numHorizRegions = 1;
243  map.grid.numVertRegions = 1;
244  map.grid.regionWidth[0] = imgSize.width;
245  map.grid.regionHeight[0] = imgSize.height;
246  map.grid.horizInterval[0] = 1;
247  map.grid.vertInterval[0] = 1;
248  CHECK_STATUS(vpiWarpMapAllocData(&map));
249 
250  // Initialize the fisheye lens model with the coefficients given by calibration procedure.
251  VPIFisheyeLensDistortionModel distModel = {};
252  distModel.mapping = VPI_FISHEYE_EQUIDISTANT;
253  distModel.k1 = coeffs[0];
254  distModel.k2 = coeffs[1];
255  distModel.k3 = coeffs[2];
256  distModel.k4 = coeffs[3];
257 
258  // Fill up the camera intrinsic parameters given by camera calibration procedure.
260  for (int i = 0; i < 2; ++i)
261  {
262  for (int j = 0; j < 3; ++j)
263  {
264  K[i][j] = camMatrix(i, j);
265  }
266  }
267 
268  // Camera extrinsics is be identity.
269  VPICameraExtrinsic X = {};
270  X[0][0] = X[1][1] = X[2][2] = 1;
271 
272  // Generate a warp map to undistort an image taken from fisheye lens with
273  // given parameters calculated above.
274  vpiWarpMapGenerateFromFisheyeLensDistortionModel(K, X, K, &distModel, &map);
275 
276  // Create the Remap payload for undistortion given the map generated above.
277  CHECK_STATUS(vpiCreateRemap(VPI_BACKEND_CUDA, &map, &remap));
278 
279  // Now that the remap payload is created, we can destroy the warp map.
280  vpiWarpMapFreeData(&map);
281 
282  // Create a stream where operations will take place. We're using CUDA
283  // processing.
284  CHECK_STATUS(vpiStreamCreate(VPI_BACKEND_CUDA, &stream));
285 
286  // Temporary input and output images in NV12 format.
287  CHECK_STATUS(vpiImageCreate(imgSize.width, imgSize.height, VPI_IMAGE_FORMAT_NV12_ER, 0, &tmpIn));
288  CHECK_STATUS(vpiImageCreate(imgSize.width, imgSize.height, VPI_IMAGE_FORMAT_NV12_ER, 0, &tmpOut));
289 
290  // For each input image,
291  for (unsigned i = 0; i < params.images.size(); ++i)
292  {
293  // Read it from disk.
294  cvImage = cv::imread(params.images[i]);
295  assert(!cvImage.empty());
296 
297  // Wrap it into a VPIImage
298  if (vimg == nullptr)
299  {
300  // Now create a VPIImage that wraps it.
301  CHECK_STATUS(vpiImageCreateWrapperOpenCVMat(cvImage, 0, &vimg));
302  }
303  else
304  {
305  CHECK_STATUS(vpiImageSetWrappedOpenCVMat(vimg, cvImage));
306  }
307 
308  // Convert BGR -> NV12
309  CHECK_STATUS(vpiSubmitConvertImageFormat(stream, VPI_BACKEND_CUDA, vimg, tmpIn, NULL));
310 
311  // Undistorts the input image.
312  CHECK_STATUS(vpiSubmitRemap(stream, VPI_BACKEND_CUDA, remap, tmpIn, tmpOut, VPI_INTERP_CATMULL_ROM,
313  VPI_BORDER_ZERO, 0));
314 
315  // Convert the result NV12 back to BGR, writing back to the input image.
316  CHECK_STATUS(vpiSubmitConvertImageFormat(stream, VPI_BACKEND_CUDA, tmpOut, vimg, NULL));
317 
318  // Wait until conversion finishes.
319  CHECK_STATUS(vpiStreamSync(stream));
320 
321  // Since vimg is wrapping the OpenCV image, the result is already there.
322  // We just have to save it to disk.
323  char buf[64];
324  snprintf(buf, sizeof(buf), "undistort_%03d.jpg", i);
325  imwrite(buf, cvImage);
326  }
327  }
328  catch (std::exception &e)
329  {
330  std::cerr << "Error: " << e.what() << std::endl;
331  PrintUsage(my_basename(argv[0]), std::cerr);
332 
333  retval = 1;
334  }
335 
336  vpiStreamDestroy(stream);
337  vpiPayloadDestroy(remap);
338  vpiImageDestroy(tmpIn);
339  vpiImageDestroy(tmpOut);
340  vpiImageDestroy(vimg);
341 
342  return retval;
343 }
Declares functions that handle image format conversion.
#define VPI_IMAGE_FORMAT_NV12_ER
YUV420sp 8-bit pitch-linear format with full range.
Definition: ImageFormat.h:222
Functions and structures for dealing with VPI images.
Declares functions to generate warp maps based on common lens distortion models.
Functions for handling OpenCV interoperability with VPI.
Declares functions that implement the Remap algorithm.
Declaration of VPI status codes handling functions.
Declares functions dealing with VPI streams.
VPIStatus vpiSubmitConvertImageFormat(VPIStream stream, uint64_t backend, VPIImage input, VPIImage output, const VPIConvertImageFormatParams *params)
Converts the image contents to the desired format, with optional scaling and offset.
void vpiImageDestroy(VPIImage img)
Destroy an image instance.
struct VPIImageImpl * VPIImage
A handle to an image.
Definition: Types.h:254
VPIStatus vpiImageCreate(int32_t width, int32_t height, VPIImageFormat fmt, uint64_t flags, VPIImage *img)
Create an empty image instance with the specified flags.
VPIFisheyeMapping mapping
Mapping between pixel angle and pixel distance to image center.
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.
float VPICameraExtrinsic[3][4]
Camera extrinsic matrix.
Definition: Types.h:672
float VPICameraIntrinsic[2][3]
Camera intrinsic matrix.
Definition: Types.h:659
@ VPI_FISHEYE_EQUIDISTANT
Specifies the equidistant fisheye mapping.
Holds coefficients for fisheye lens distortion model.
VPIStatus vpiImageCreateWrapperOpenCVMat(const cv::Mat &mat, VPIImageFormat fmt, uint64_t flags, VPIImage *img)
Wraps a cv::Mat in an VPIImage with the given image format.
VPIStatus vpiImageSetWrappedOpenCVMat(VPIImage img, const cv::Mat &mat)
Redefines the wrapped cv::Mat of an existing VPIImage wrapper.
struct VPIPayloadImpl * VPIPayload
A handle to an algorithm payload.
Definition: Types.h:266
void vpiPayloadDestroy(VPIPayload payload)
Deallocates the payload object and all associated resources.
VPIStatus vpiSubmitRemap(VPIStream stream, uint64_t backend, VPIPayload payload, VPIImage input, VPIImage output, VPIInterpolationType interp, VPIBorderExtension border, uint64_t flags)
Submits a Remap operation to the stream.
VPIStatus vpiCreateRemap(uint64_t backends, const VPIWarpMap *warpMap, VPIPayload *payload)
Create a payload for Remap algorithm.
struct VPIStreamImpl * VPIStream
A handle to a stream.
Definition: Types.h:248
VPIStatus vpiStreamSync(VPIStream stream)
Blocks the calling thread until all submitted commands in this stream queue are done (queue is empty)...
void vpiStreamDestroy(VPIStream stream)
Destroy a stream instance and deallocate all HW resources.
VPIStatus vpiStreamCreate(uint64_t flags, VPIStream *stream)
Create a stream instance.
@ VPI_BACKEND_CUDA
CUDA backend.
Definition: Types.h:93
@ VPI_BORDER_ZERO
All pixels outside the image are considered to be zero.
Definition: Types.h:276
@ VPI_INTERP_CATMULL_ROM
Catmull-Rom cubic interpolation.
int8_t numHorizRegions
Number of regions horizontally.
Definition: WarpGrid.h:159
VPIWarpGrid grid
Warp grid control point structure definition.
Definition: WarpMap.h:91
int16_t horizInterval[VPI_WARPGRID_MAX_HORIZ_REGIONS_COUNT]
Horizontal spacing between control points within a given region.
Definition: WarpGrid.h:174
int8_t numVertRegions
Number of regions vertically.
Definition: WarpGrid.h:162
int16_t vertInterval[VPI_WARPGRID_MAX_VERT_REGIONS_COUNT]
Vertical spacing between control points within a given region.
Definition: WarpGrid.h:180
int16_t regionWidth[VPI_WARPGRID_MAX_HORIZ_REGIONS_COUNT]
Width of each region.
Definition: WarpGrid.h:165
int16_t regionHeight[VPI_WARPGRID_MAX_VERT_REGIONS_COUNT]
Height of each region.
Definition: WarpGrid.h:168
void vpiWarpMapFreeData(VPIWarpMap *warpMap)
Deallocates the warp map control points allocated by vpiWarpMapAllocData.
VPIStatus vpiWarpMapAllocData(VPIWarpMap *warpMap)
Allocates the warp map's control point array for a given warp grid.
Defines the mapping between input and output images' pixels.
Definition: WarpMap.h:88