Your blob tool finds every circular fiducial, but the marks you draw at the reported centers sit about half a pixel up and to the left of the dots, and adding 0.5 px by hand makes them land. That systematic shift is not your threshold, your lighting or your camera: it is a mismatch between the point of the pixel your library counts from and the point your overlay draws. It shows on any sensor, from a legacy 640 × 480 camera to the current generation of area scan cameras and smart cameras, because the convention lives in the software. This guide shows you how to prove which convention you have with a synthetic image, what the bias costs in millimetres, how to correct it in one place in code, and which different signatures mean a different cause entirely.
Why is my blob centroid half a pixel off to the north-west?
Because your library indexes each pixel by its upper-left corner while your overlay marks the pixel center: the reported center of mass comes out exactly half a pixel low in x and low in y, which on a standard display reads as a shift toward the north-west.
Two conventions exist for naming a pixel with a number, and a center of mass is a weighted average of those numbers. If the integers fed to the average are corner references, the average inherits the corner reference. The pixel you call 10 covers the ground from 10 to 11, whose middle is 10.5, so the mean over a symmetric blob comes out half a pixel toward the origin in each axis.
Image coordinates place (0, 0) at the upper-left, x growing right and y growing down. Low in both axes therefore points up and left on screen, which is why adding half a pixel in x and in y pulls your mark back onto the dot.
This is a known migration path, not an anomaly: one long-established vision library referenced the upper-left corner through version 6.1 and moved to pixel-center coordinates in version 7.0. Code that drew marks at the raw values was correct before that change and half a pixel off after it, or the reverse for anyone who had already baked in a manual correction.
Which point of the pixel does a coordinate actually name?
In the pixel-center convention, integer coordinates (x, y) name the middle of a pixel, so one pixel spans x - 0.5 to x + 0.5; in the corner convention the integer names the upper-left corner, and that pixel spans x to x + 1.
A convention has two parts: the point of the first pixel that (0, 0) names, and the corner the count starts from. Most current toolkits put integers at pixel centers; the corner convention survives in older libraries and in code written against them. Every sub-pixel output — center of mass, edge position, corner coordinate — is only as good as the frame it is expressed in, which is why the same question decides how to read sub-pixel corner coordinates and their half-pixel offsets. Two tools that disagree by exactly half a pixel in both axes on the same image are telling you their origins differ, not that one of them is inaccurate.
How do I test my library's convention in one minute?
Build a synthetic two-by-two white block at columns 8 to 9 and rows 12 to 13 of a black image and run your center-of-mass function on it: a center-referenced tool returns (9.0, 13.0) and a corner-referenced tool returns (8.5, 12.5).
The block covers x from 8 to 10, so its physical middle is 9.0; the mean of the member indices, (8 + 9) / 2, is 8.5. That half-pixel gap is the whole story. The pseudocode below is illustrative; the printed values are the proof:
# Synthetic test image: 300 rows × 400 columns, all zero
test_image: uint8[300][400] = 0
test_image[12:14, 8:10] = 255 # 2 × 2 white block
(x, y) = center_of_mass(test_image) # your blob tool's call
# x = 9.0, y = 13.0 -> pixel-center convention
# x = 8.5, y = 12.5 -> corner convention: add 0.5 px per axis
Run the same image through every path that reports a position, the on-device blob tool as well as the PC library, and write the outcome beside the function name. Neighbour rules change blob membership, not the origin, so the choice covered in connectivity 4 or 8 in blob analysis for circular markers does not affect this test.
Could the offset come from thresholding, lighting or fill holes instead?
No: thresholding and lighting errors move the centroid by fractions that vary with blob size, edge gradient and position in the field, while a convention error is exactly half a pixel in both axes at every point.
The pipeline order — binarize, fill holes, filter particles, center of mass — changes which pixels belong, never the frame they are counted in. Filling holes alters membership only for ring-shaped fiducials; on a solid circle it changes nothing. A particle filter drops whole particles; it cannot slide a surviving one sideways. Connectivity toggles a few boundary pixels, worth a few hundredths of a pixel on a fiducial ten or more pixels across.
A biased threshold does shift a centroid, when one side of the circle cuts in earlier than the other. That shift grows with edge blur and differs between fiducials, so it never lands on the same half pixel in both axes at every position. The fastest discriminator is to move the part: a constant offset is the convention, and anything that changes with position or lighting is optics. Using a grayscale-weighted centroid on a dark circle shrinks the threshold's influence while you chase the rest.
How much is half a pixel in millimetres?
Take your field of view divided by your pixel count: over a 120 mm × 90 mm field of view on a 4000 × 3000 pixel image, one pixel is 0.03 mm, so the convention bias is 0.5 × 0.03 = 0.015 mm.
In full: 120 mm / 4000 px = 0.03 mm per pixel in x, and 90 mm / 3000 px = 0.03 mm per pixel in y, square pixels with convenient numbers. Half a pixel is therefore 0.015 mm, or 15 µm. A fiducial 40 px across spans 40 × 0.03 = 1.2 mm on the part, so the uncorrected bias is 1.25 % of its diameter.
Whether 15 µm matters is a tolerance question with a number, not a judgement call. A robot guidance tolerance of ±0.05 mm spends 30 % of its budget on this bias in each axis; a presence check with a ±0.5 mm margin spends 3 %. You correct it in both cases, because the correction costs one line of code.
The camera in this example is the LS-CMR-0166, a 12 MP USB3 area scan camera with a 4000 × 3000 frame.
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
To work the sizing the other way, from a smallest feature and a tolerance to a pixel count, use the camera resolution calculator, then the camera finder to filter by resolution and shutter.
How do I correct the offset in code?
Add 0.5 px to both coordinates exactly once, at the point where pixel values convert to physical units, and keep the correction in one place instead of scattering it through the application.
# Corner-referenced values returned by the blob tool
centroid_x: float = 8.5
centroid_y: float = 12.5
scale_mm_per_px: float = 0.03 # 120 mm field of view / 4000 px
x_mm: float = (centroid_x + 0.5) * scale_mm_per_px # 9.0 × 0.03 = 0.270 mm
y_mm: float = (centroid_y + 0.5) * scale_mm_per_px # 13.0 × 0.03 = 0.390 mm
If your toolkit exposes the pixel reference as a property of the image or of the measurement step, set it there and skip the arithmetic, but keep the synthetic block test in your start-up checks so an upgrade cannot silently flip it back.
Apply the correction before the value enters a distortion map, a rotation transform or a robot frame; every downstream transform inherits the origin of its input, so one corrected hand-off point protects the whole chain. Once the pixel frame is right, setting a coordinate system on a circle center shows how to carry it into part coordinates for moving parts.
When is half a pixel not a convention problem?
When the offset is not exactly half a pixel in both axes, not constant across the field, or confined to one axis: those signatures point to lens distortion, a color-plane offset or rolling-shutter skew instead.
| Signature | Likely cause | Quick test | Fix |
|---|---|---|---|
| Exactly -0.5 px in x and y, everywhere | Corner versus center pixel origin | Synthetic 2 × 2 block test | Add the half pixel, or set the pixel reference |
| Grows toward the image edges | Lens distortion | Dot grid: compare pitch at the corners with the center | Distortion calibration |
| Half a pixel in one axis only | Color-plane or chroma-subsampled input | Recompute on the green plane | Do geometry on a full-resolution plane |
| Small, 0.05 to 0.2 px, changes with lighting | Threshold asymmetry at the edge | Rotate the part 180° and recompute | Even illumination, grayscale-weighted centroid |
| Appears only when the part moves | Rolling-shutter skew | Stop the conveyor and recompute | Global shutter or strobed exposure |
On a color sensor with a Bayer pattern, the red and blue samples sit on a grid offset half a pixel from the green samples, so a centroid computed on a single color plane inherits a genuine half-pixel shift in both axes; use the green plane or the demosaiced image for geometry. The distortion row is the one worth calibration effort: lens distortion correction for pixel-to-mm accuracy covers the target and the math. For the motion row, start with strobe and exposure timing checks.
What checks prove the fix before it ships?
Run four checks on real hardware: after your correction the synthetic block test returns the block's starting index plus 0.5, the moved-part test shows no residual, the dot-grid pitch error stays under 0.05 px, and a 180° rotation shows no directional bias.
- Synthetic block test. The deployed build, not your desk copy, must return the block's starting index plus 0.5 in each axis once your correction is in place; this pins the convention the shipped software actually uses.
- Moved-part test. Put the fiducial at the center, then near each corner of the field. After correction the residual stays at zero; drift with position is distortion, not convention.
- Dot-grid pitch test. Measure the spacing between adjacent dots of a target with a known pitch. An error under 0.05 px per spacing confirms scale and origin together.
- 180° rotation test. Rotate the part, not the camera. Any remaining directional bias is lighting or threshold asymmetry, and the convention correction will not remove it.
The same four checks apply when the blob tool runs on the device. A smart camera such as the LS-SMC-0063, a 1.6 MP model with its own 16 mm lens and polarized white light, reports centers in a pixel frame with the same two possible origins, so feed its synthetic block image through the production measurement, not a preview tool.
- Interface
- GigE
- Resolution
- Up to 3 MP
The decision, one line per case. Exactly half a pixel in both axes, everywhere: pixel-origin convention, add the correction once at unit conversion. Offset growing toward the edges: distortion, calibrate it. One axis only: measure on a full-resolution color plane. Small and lighting-dependent: fix the illumination and switch to a grayscale-weighted centroid. Motion-only: shutter skew, freeze the part. When the correction is in and the microns per pixel still need to shrink, the vision system configurator starts at the camera step, and more measurement write-ups sit on the applications topic page. The contact page takes your field of view and pixel count for a check against your fixture.
Questions
Why does the half-pixel offset always point up and left?
Because image coordinates start at the upper-left corner, with x growing right and y growing down. A corner-referenced library subtracts half a pixel from the true center along both axes, which pulls the reported point toward the origin — north-west on a normal display. If your framework defines (0, 0) at a different corner, the offset follows that origin, so the shift always points toward wherever zero is defined.
Will a higher-resolution camera shrink the half-pixel error?
In millimetres, yes; in pixels, no. The pixel error stays exactly half a pixel until you correct the convention, but the physical bias scales with your mm-per-pixel value. At 0.03 mm per pixel the bias is 0.015 mm; doubling the pixel count over the same field of view makes it 0.0075 mm. Fix the convention first, then spend resolution on what remains of the error budget.
Do smart cameras and vision sensors have the same pixel-origin issue?
Yes. The convention lives in the measurement function, not the camera, so any platform that reports a center of mass in pixel units can use either origin. The synthetic block test works unchanged on a vision sensor: if the reported center equals the block's starting index instead of the index plus 0.5, apply the correction before converting to millimetres or robot coordinates.
How repeatable should a centroid be once the convention is fixed?
On a high-contrast circular fiducial with even lighting, a grayscale or binary centroid typically repeats to a few hundredths of a pixel. Residual scatter above roughly 0.1 px after the half-pixel correction points at threshold asymmetry, vibration or distortion instead. A dot target separates them: scale error grows with spacing, vibration smears in one direction, and threshold error follows the lighting.
Does the half-pixel rule apply to edges and corners too?
Yes. Every sub-pixel measurement — edge position, corner, centroid — is expressed in the same frame, so a convention mismatch shifts all of them by the same half pixel. Drawing and overlay functions carry a convention too, which is why mixing one library for measurement and another for display is the classic way the offset appears. Fix the frame once, at the hand-off between them.

