A threshold that finds the part usually punches holes in it: a specular highlight on a shiny flange, a white label that saturates, a shadow under a boss. Morphological closing patches those gaps but rewrites the outline you already trust. The cleaner operator for this case is fill-holes: invert the mask, flood the background starting from the image border, invert again and OR the result with the original. Interior voids close and no edge pixel moves. This guide gives the numbered steps, the pseudocode, a pixel-count test that proves the fill on your own image, and the arithmetic that turns hole pixels into square millimetres.
Why does morphological closing move edges you already trust?
Closing dilates the mask by a structuring element k pixels wide and erodes it back by the same amount, so any outline feature finer than about 2k pixels is rewritten: a 5 x 5 element can shift an irregular boundary by two pixels on each side.
Closing is dilation followed by erosion with the same element. The dilation fuses near-separate lobes and swallows thin background bays; the erosion returns the dilated shape toward its old size, not its old contour. On a part with a genuinely irregular edge, casting texture, torn trim or a perforated flange, every concave feature narrower than the element rounds over or vanishes. Some vision packages even file gap filling under their closing step, which is how the habit spreads. Closing has a real job, fusing regions that should be one; it is the wrong tool when the contour is already correct and the defect is strictly interior.
Which operation fills holes and leaves every edge pixel alone?
Invert the mask, flood the background from the image border, invert the result and OR it with the original: every pixel the flood cannot reach is a hole fully enclosed by foreground, so the pixels added are exactly the hole interiors and the outline gains zero pixels.
The reframing is the whole idea. Instead of asking which pixels belong to the part, ask which background pixels can reach the picture frame. Flood fill answers that: seeded at the border, it spreads through background-colored pixels, taking a start node, a target color and a replacement color, the three parameters of the classic flood fill. Whatever the flood never touches is background-colored but sealed inside the part: a hole by definition.
Because added pixels come only from the not-reached set, the fill cannot put a pixel on the outline or outside it. Outline pixels are foreground, and everything connected to the border stays background. Each hole closes to exactly its own extent: no rounding, no bridging, no growth.
What are the steps, in order?
Five steps do it: threshold, invert, flood the background from the border, mark unreached pixels as holes, and OR them back into the mask; every step does a bounded amount of work per pixel, so the cost grows linearly with the image.
- Threshold to a mask. One channel, one decision: part = 1, background = 0. If the mask comes out black where you expect white, fix polarity first; the threshold polarity checkpoint covers that failure.
- Invert. Set inv = 1 - mask. True background and interior holes now both read 1.
- Seed the border. Collect every border pixel where inv = 1, along all four edges. These seeds are the true background.
- Flood or label. Flood from the seeds through inv, or equivalently label the connected components of inv and keep those containing a border pixel. The reached set is bg.
- Mark the holes. holes = inv - bg: pixels that read background but the flood never reached.
- OR back. filled = mask or holes. Nothing is removed; only hole pixels are added.
mask : uint8[H, W] // 1 = part, 0 = background, from step 1
inv : uint8[H, W] // inv = 1 - mask
bg : uint8[H, W] // flood of inv seeded at border pixels where inv = 1
holes : uint8[H, W] // holes = inv - bg
filled : uint8[H, W] // filled = mask + holes, clipped to 1
test on the worked example in the next section:
count(holes) = 30
count(filled) - count(mask) = 30
count(mask - filled) = 0
Does the fill really add nothing outside the holes?
No: on a 512 x 512 mask whose part covers 164,096 pixels and holds one interior hole of 30 pixels, the filled mask counts 164,126 part pixels, with 30 hole pixels added, zero pixels changed on the outline, and the background dropping from 98,048 to 98,018.
Work the totals. The image holds 512 x 512 = 262,144 pixels. Background before the fill is 262,144 - 164,096 = 98,048 pixels. After filling, the part is 164,096 + 30 = 164,126 pixels and the background is 98,048 - 30 = 98,018, and the totals close: 164,126 + 98,018 = 262,144. The 30 added pixels are exactly the hole set, every one of them interior.
| Measure | Before | After fill |
|---|---|---|
| Part pixels | 164,096 | 164,126 |
| Background pixels (reach the border) | 98,048 | 98,018 |
| Interior hole pixels | 30 | 0 |
| Outline pixels changed | - | 0 |
The same count is the general test: count(filled) - count(mask) must equal the sum of your hole areas, and mask - filled must be empty. Pixel counting is also how blob area measurement works, so the blob-area method doubles as verification arithmetic here.
How big is a hole once it leaves pixel units?
Divide by the pixel pitch: at 23.4 um per pixel, a 48 mm field on a 2,048-pixel line, the example's 30-pixel hole covers about 16,430 um2, and a 0.10 mm2 void spec becomes a fill-below-183-pixels rule.
The pitch follows from the optics: 48 mm / 2,048 px = 0.0234 mm, call it 23.4 um. One pixel covers 23.4 x 23.4, about 548 um2. The hole covers 30 x 548, about 16,430 um2, which is 0.016 mm2; if that hole is round, its equivalent diameter is 2 x sqrt(16,430 / pi), about 145 um or 0.14 mm. The area conversion is exact; the diameter assumes a round blob.
Set the fill threshold from the spec, not from habit. If voids of 0.10 mm2 and above must fail, then 0.10 mm2 = 100,000 um2 and 100,000 / 548 gives about 183 pixels: fill holes below 183 px and hold out holes of 183 px and up as defects. Change the optics and the cutoff moves with pitch squared; double the pitch and the same 0.10 mm2 lands near 46 pixels.
The same trade, how many pixels a field of view buys for a smallest feature, is what the resolution calculator solves:
Where does hole filling go wrong?
Three traps cause nearly all failures: a one-pixel leak that connects a hole to the border, regions that genuinely touch the image edge, and mismatched connectivity, so match the fill's connectivity to the complement of your foreground's or diagonal gaps read as leaks.
- The one-pixel leak. A dropout anywhere on the outline opens a channel, the flood walks through, and the void reads as background: a hole that never fills. Fix the threshold, not the filler; nudge the level or smooth the grayscale image before thresholding. Closing would seal the channel too, and move the edges again, which defeats the point.
- Border contact. A background-colored region touching the frame is background by definition and stays open. That is correct for true background, and wrong when the part runs off the edge; the pad trick in the steps above handles that case.
- Connectivity pairing. Foreground and background need complementary connectivity: with an 8-connected part, flood with 4-connectivity. An 8-connected flood escapes through a diagonal one-pixel gap where a 4-connected flood holds, and the two conventions disagree exactly at diagonal touches.
- Every part at once. Filling fills holes of all components. If only the largest component matters, keep that region first, the same keep-what-matters discipline as when you remove spurs but keep the longest line.
Where should the fill run: in the camera or on a PC?
Either: smart cameras run threshold, fill and the pass/fail decision inside their own cycle with no PC in the loop, PC-based setups run the same five steps on frames from an area scan camera, and you pick by where the decision must close.
On-device, when the decision must close inside the camera:
- Interface
- GigE
- Resolution
- 6-10 MP
- Sensor format
- 1/2.4"
- Shutter
- Rolling
The MV-SC5060XM-08M-WBN pairs a 1/2.4" sensor with an 8 mm M12-mount lens on GigE. When the hole count feeds a longer chain of custom logic, an X86-platform model such as the LS-SMC-0017 runs your own code next to the sensor:
- Interface
- USB 2.0
- Resolution
- 6-10 MP
- Sensor format
- 1"
- Shutter
- Global
For the PC route, the MV-CU020-90GC is a 2.5 MP global-shutter GigE camera with a C-mount:
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
Browse the smart cameras and area scan cameras categories, or filter by resolution, shutter and mount in the camera finder. Once the field of view and cycle time are set, the camera configurator assembles camera, lens and light as one checked set:
The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.
What do you check before trusting the filled mask?
Four checks: the before-and-after difference equals the hole set and nothing else, nothing is removed, no added pixel sits on the image border, and the hole count per part is stable on live images at your threshold.
- Difference test. filled - mask equals the hole set: every added pixel interior, none on the outline, and the count equal to the sum of hole areas.
- Removal test. mask - filled must be empty; the fill never deletes a pixel.
- Border test. No added pixel on the first or last row or column. Border background pixels are seeds by construction, so anything appearing there means a seeding bug.
- Stability test. Overlay the found-hole mask on live parts before locking the threshold in; the live binary image preview pattern is the quickest way to watch the count hold steady.
A filled silhouette also makes a clean region mask, and masking the background first is what stabilizes pattern-matching scores; the background-masking pattern is the next read.
The decision, in one line: fill holes when the contour is final and the voids are not; keep closing for fusing lobes; set the fill threshold in pixels from your spec and pitch. The systems topic page collects the surrounding techniques, and specific questions about a line go through the contact page.
Questions
Can I fill holes and still count the holes as defects?
Yes, because the hole mask is a by-product. Keep the holes image before the OR step: each region's pixel count is the void area (30 pixels is about 0.016 mm2 at the worked example's optics), and the number of separate regions is the void count. Log both, then fill. One pass gives the filled silhouette for the presence check and hole statistics for defect logging.
Why is one hole in my mask never filled?
Almost always a leak: some background-colored path joins the void to the image border, so the flood reaches it and by definition it is background. Overlay the found-hole mask on the original image to see the channel, usually a one-pixel dropout on the outline. Nudging the threshold or smoothing the grayscale image before thresholding usually seals it. Also check the connectivity pairing: an 8-connected flood escapes through a diagonal gap that a 4-connected flood holds.
Does filling holes change the part's measured area?
It adds exactly the enclosed pixels: in the worked example, 164,096 becomes 164,126. That is what a silhouette or presence check wants. If the void itself is the measured feature, measure on the unfilled mask or subtract the hole area, keeping filled area for contour matching and unfilled area plus hole statistics for porosity and void checks.
How fast is hole filling at full camera resolution?
Hole filling is linear in pixel count, so budget per megapixel: flood- and label-based implementations on a desktop-class CPU commonly process several tens of megapixels per second, putting a 2 MP smart-camera frame in single-digit milliseconds and a 12 MP frame in the tens of milliseconds. Measure on your target hardware before fixing a cycle time.
Does this work on a grayscale image without thresholding?
The fill itself is binary: threshold first, fill the mask, and keep the grayscale image alongside for intensity measurements. Gray-scale hole filling exists as a separate operator, raising enclosed regional minima to their surround level, but for silhouette work the threshold-then-fill route is easier to predict and to test with a pixel count.


