System designTroubleshooting

findChessboardCorners: different results in Python and C++

Why findChessboardCorners gives different results in Python and C++, how patternSize order, CALIB_CB_FAST_CHECK and build differences explain it, and the fixes.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01The symptom, and what it rules out
  2. 02Cause 1: the patternSize pair is transposed
  3. 03Cause 2: CALIB_CB_FAST_CHECK rejects the board
  4. 04Cause 3: two builds behind one version number
  5. 05Worked example: one board, both bindings, all the numbers
  6. 06Give the detector more pixels per square
  7. 07Isolate the cause in fifteen minutes
  8. 08Checks before you trust the corners
  9. 09When both languages agree the board is bad
  10. 10Questions

Your Python script finds all 42 corners of a 4 x 15-square chessboard. Your C++ program, built against the same OpenCV 4.6.0, running the same three flags on the same image, returns nothing, or a partial set that looks plausible and is not. In practice the gap comes from three places: the patternSize pair passed in a different order, the CALIB_CB_FAST_CHECK pre-scan discarding a marginal board, and two differently compiled builds of the same version feeding the detector slightly different pixels. All three are fixable in an afternoon without touching your calibration math. This guide gives the test that isolates each cause, the numbers for a board that keeps the problem from coming back, and the checks to run before you trust either environment.

The symptom, and what it rules out

Both programs load the same image, convert it with COLOR_BGR2GRAY, and call findChessboardCorners with CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_FAST_CHECK + CALIB_CB_NORMALIZE_IMAGE. Python returns the full point set. C++ returns false, or true with fewer points than the board holds.

The matching version string and matching flags rule out the obvious suspects. What remains is what the two detectors actually receive: the patternSize argument, the effect of the FAST_CHECK flag, and the exact pixel values each compiled build feeds in. Chessboard detection runs on strong, even gradients between black and white squares, a method with decades of published work behind it (chessboard detection). Small input differences move a real board across the accept/reject line.

Cause 1: the patternSize pair is transposed

The patternSize argument counts inner corners, points per row first: cv::Size(points_per_row, points_per_column), which is cv::Size(columns, rows). The Python binding takes the same two numbers in the same order, so Python (3, 14) and C++ cv::Size(3, 14) are one request: 3 inner corners along each row and 14 along each column. C++ cv::Size(14, 3) describes a different board, turned a quarter turn. This argument has caught enough users to fill an OpenCV issue tracker of its own.

Board (squares) Inner corners Python C++
4 x 15 3 per row x 14 per column (3, 14) cv::Size(3, 14)
15 x 4 14 per row x 3 per column (14, 3) cv::Size(14, 3)
9 x 6 8 per row x 5 per column (8, 5) cv::Size(8, 5)

On a clean image the transposed request fails outright. On a marginal image the detector can lock onto part of the grid and return points whose order differs from Python's, and order is everything once you pair corners between views.

The test takes one minute. Draw the overlay with drawChessboardCorners in Python, count the marker rows and columns on screen, and print the tuple. Mirror those two numbers in C++:

# per_row, per_col = inner corners along a row, along a column
# gray = 8-bit grayscale image, flags = your flag combination
per_row, per_col = 3, 14
ret, corners = cv2.findChessboardCorners(gray, (per_row, per_col), flags)
assert len(corners) == per_row * per_col  # full board: 42 points
// same pair, same order: points per row, then points per column
const int per_row = 3, per_col = 14;
cv::Size pattern(per_row, per_col);
std::vector<cv::Point2f> corners;
bool found = cv::findChessboardCorners(gray, pattern, corners, flags);
// full board: found == true and corners.size() == 42

Cause 2: CALIB_CB_FAST_CHECK rejects the board

FAST_CHECK exists for speed: it runs a quick pre-scan and, when that pre-scan sees no board, the function returns false without attempting the full search. That trade suits a live preview where you poll for board presence at frame rate and most frames are empty. It does not suit calibration capture, because a board the full search would accept — a thin grid, slight defocus, uneven lighting — gets discarded by the pre-scan first. Once the pattern size matches, this flag is the most common reason one environment succeeds and the other refuses.

The test: delete CALIB_CB_FAST_CHECK from both programs, keep CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE, and re-run the same images. The call takes longer per frame; that is the full search doing the work the pre-scan skipped.

The decision: drop FAST_CHECK on every frame you calibrate from. Keep it, if at all, on the preview path where its speed was the point.

Cause 3: two builds behind one version number

A version string of 4.6.0 pins an API, not the compiled code. The pip wheel ships its own optimization paths and its own image codecs. Your C++ toolchain links a library built with different SIMD flags and possibly a different JPEG decoder. Two decoders can differ by a level or two on the same file, and a corner sitting near the adaptive threshold flips on exactly that difference.

The test: have both programs print the average gray value of the decoded image to three decimals. Different averages mean different pixels reached the detector, and no argument tuning will reconcile them. The fix is to take decoding out of the comparison: convert your captures once to 8-bit PNG, a lossless format, and feed that same file to both programs. If the averages match and the corners still differ, the remaining gap is the compiled detector itself, and the answer is to build both sides from the same source revision.

The decision: calibrate from lossless frames. A JPEG decoder is not part of a measurement chain.

Worked example: one board, both bindings, all the numbers

Take the failing board: 4 squares across and 15 down, so 3 inner corners per row and 14 per column, 3 x 14 = 42 points. The image is 1080 x 720 with the long dimension of the board spanning the full 720 px:

  • Square pitch: 720 px / 15 squares = 48 px
  • Board width: 4 squares x 48 px = 192 px
  • Subpixel window: the (3, 3) argument of cornerSubPix is a half-size, so the search spans 7 x 7 px, which is 7 / 48 or about 15 percent of the pitch

Fifteen percent of pitch is comfortable for sharply focused squares. When the edges are soft, widen the window to (5, 5), an 11 x 11 px search, and keep the same 30-iteration, 0.1 stop criteria. Then hold per_row = 3 and per_col = 14 on both sides, feed both programs the same lossless file, and leave FAST_CHECK off: both return the full point set. Confirm the start corner once in the drawn overlay of each environment before you pair anything.

Give the detector more pixels per square

Every fix above is software. The durable fix is pixels: plan for at least 20 px per square, because the subpixel stage fits a small window around each corner and needs several pixels of clean gradient on both sides of the crossing. You buy that room with focal length and working distance, from two lines of arithmetic:

m = sensor height used / board height f = WD × m / (1 + m)

Work a 200 mm board, the 15-square dimension, on a 1/1.8-inch sensor (about 7.2 x 5.4 mm as a format convention), filling 80 percent of sensor height, viewed from 500 mm:

  1. m = 4.32 mm / 200 mm = 0.0216
  2. f = 500 mm x 0.0216 / 1.0216 = 10.6 mm, so choose a 12 mm lens
  3. Check at 12 mm: m = 12 / (500 - 12) = 0.0246; field height = 5.4 mm / 0.0246 = 220 mm, so the 200 mm board fits with about 20 mm to spare
  4. One square on sensor: (200 mm / 15) x 0.0246 = 0.328 mm; at 2.0 µm pixels that is 328 / 2.0 = 164 px per square, eight times the floor
How sensor width, working distance and field of view set the focal length Rays from the edges of the sensor pass through the lens and spread to the edges of the field of view. The ratio of sensor width to field of view is the magnification m.CameraField of view (FOV)Working distance (WD)≈ fsensor width slensm = s / FOVf = WD × m / (1 + m)
How sensor width, working distance and field of view set the focal length. Rays from the edges of the sensor pass through the lens and spread to the edges of the field of view. The ratio of sensor width to field of view is the magnification m.

An 8.5 mm lens at the same station gives m = 8.5 / (500 - 8.5) = 0.0173, 115 px per square and a 312 mm field height — five times the floor with far more room to move. Take the 12 mm focal length when the stand holds distance precisely; take the shorter focal length when the board position wanders. This is the same working distance and field of view formula used to lay out inspection cells, and the lens focal length calculator runs it; its button opens the lens finder filtered to the focal length it computes.

Lens focal length, field of view and working distance
Units
Solve for
Focal length
– mm
Field of view
– mm
Working distance
– mm
Magnification
– x
Angle of view
– °
Pixel on the part
– mm

Thin-lens model: m = sensor width / field of view, f = WD × m / (1 + m). Accurate to a few percent above about 150 mm; check the lens's minimum object distance.

Find lenses with this focal length Open the full calculator

For capture, a 12 MP USB3 camera such as the LS-CMR-0166 outputs 4000 x 3000 pixels at 30 fps:

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

Two C-mount lenses bracket that station, the 8.5 mm LS-FFL-0602 and the 16 mm LS-FFL-0567:

Specification LS-FFL-0602 LS-FFL-0567
Resolution 10 Megapixel 20 Megapixel
Format 1" 1.1"
Focal length 8.5 mm 16 mm
Aperture F2.4 Manual iris F2.8-22
Filter thread M77 x P0.75 M35.5 x P0.5
Price $429.00 $679.00

All 2 share: M.O.D. 0.1 m; Mount C-mount. Values as printed in each product's data sheet; open a part for its drawing and full specifications.

And hold the station still, because vibration is blur in every corner: a slide-table stand such as the LS-MNT-0040, with 700 mm of column height and a 400 x 450 mm base, keeps working distance where you set it:

Camera standsMachine vision camera stand, professional slide-table type, height 700 mm, 400 x 450 mm base (LS-MNT-0040)LS-MNT-0040
Type
Camera stands
$629.00View partBuild a system with it

Once the numbers are fixed, browse area scan cameras, machine vision lenses and camera stands for the rest of the station.

Isolate the cause in fifteen minutes

  1. Print the pattern pair in both programs and compare position by position: Python (per_row, per_col) against C++ cv::Size(per_row, per_col).
  2. Draw the overlay in both environments and confirm the same start corner and the same row direction.
  3. Remove CALIB_CB_FAST_CHECK in both and re-run the failing captures.
  4. Feed both programs one lossless PNG and compare the printed average gray value to three decimals.
  5. Only after those four pass, touch the hardware: even up the lighting and raise the pixels per square.

Checks before you trust the corners

When both languages agree the board is bad

With the pair matched, FAST_CHECK off, and both builds reading the same lossless pixels, identical failures in both languages are the detector telling the truth. Three target faults dominate. Glossy lamination throws highlights across the squares; print matte. Squares that touch with visible seams and gaps break the quad grouping; fix them with checkerboard corners not detected: fix gaps in your target. And a board that is simply small at the working distance needs more squares or a longer lens — size it with checkerboard calibration target size: how many squares and how big. The same logic scales for distant targets, as in checkerboard calibration fails at distance: depth camera target size.

When the software matches and you want hardware that matches it, start in the configurator with your board size and working distance, or reach us through the contact page and we will run the numbers with you.

Questions

Is the Python patternSize tuple in a different order than the C++ Size?

No — the tuple is (inner corners per row, inner corners per column), the same order as cv::Size(points_per_row, points_per_column) in C++. A classic 9 x 6-square board is 8 x 5 inner corners: (8, 5) in Python, cv::Size(8, 5) in C++, 40 points. Count the squares and subtract one per axis. Keep that pair in one shared config both programs read, so the two calls can never drift apart.

What does CALIB_CB_FAST_CHECK actually change?

It runs a quick pre-scan and returns false, without the full search, when the pre-scan finds no board. The speedup shows on video streams where most frames are empty, which is the opposite of a calibration capture. There, a marginal board the full search would accept gets rejected first. Drop CALIB_CB_FAST_CHECK on every frame you calibrate from, keep CALIB_CB_ADAPTIVE_THRESH and CALIB_CB_NORMALIZE_IMAGE, and accept the slower call.

Why can two installs of OpenCV 4.6.0 disagree on the same image?

The version string pins the API, not the compiled code. A pip wheel ships its own optimization paths and its own image codecs, and a locally built C++ library can differ in both. Two decoders can differ by a level or two on one JPEG and flip corners near the threshold. Print the build information string each binding reports, and calibrate from lossless PNG frames so decoding stops being a variable between the two programs.

How many pixels should each chessboard square cover?

Plan for at least 20 px per square, and prefer double that. The subpixel stage fits a window of a few pixels around each corner and needs clean gradient on both sides of the crossing to land repeatably. At 20 px pitch a 7 x 7 px window covers about a third of a square, which is the practical floor. Angled boards and soft focus eat that margin fast, which is why the worked 48 px pitch is comfortable.

Does a square board pattern cause cross-language mismatches?

It can. A square grid matches after a 90-degree turn, so two builds can return the same points in transposed order and both be right. Keep the per-axis counts different, such as 8 x 5, and a quarter-turn cannot match at all. Only a consistent 180-degree reversal remains, and the drawn overlay exposes it at a glance because the first marker lands somewhere unexpected.