How Vigiovo grades eggs without AI
Vigiovo finds each egg by colour, splits touching eggs with watershed, measures dirt against a shading surface fitted to each egg and finds cracks as thin, deep lines. Fixed thresholds decide the class. On video, each egg is measured three times as it passes and is rejected only if the defect repeats.
Updated

1. Colour mask
The image is converted to HSV. A pixel is shell when it has a warm hue (red to yellow), saturation of at least 45 and brightness of at least 120 on a 0–255 scale. Conveyor rollers are grey and dark, so they're left out. A lookup table does the hue test in a single pass.
Morphological opening and closing remove noise and close small holes. Components smaller than 150 pixels are dropped.
2. Camera-independent scale
On the first frame, Vigiovo estimates the typical egg radius from isolated blobs and downscales until that radius is ~12 px. Outlines don't need detail, and this cuts the pixels processed by 4–10×.
Every later limit is a multiple of the radius r, so the same configuration works with cameras of different resolutions.
3. Splitting touching eggs
Touching eggs form a single blob. To split them, Vigiovo computes the distance transform (each pixel's distance to the edge), finds one peak per egg with h-maxima via morphological reconstruction and runs watershed from those peaks.
The neck between two eggs is shallow, so the split is stable. Blobs the size of a single egg skip this step, which keeps the cost low.
4. Clean outline and unverified eggs
Each egg has its holes filled, is opened with a ~1 r disk and is clipped to its fitted ellipse. This removes shadows between eggs and thin appendages.
An egg is marked unverified, and never graded, when it:
- touches the image border;
- has an area outside 0.35–2.5 times that of a typical egg;
- has solidity below 0.90 (a concave shape, a sign of overlap);
- doesn't look like an ellipse or has an axis ratio above 1.8.
5. Measuring dirt
A clean shell isn't evenly bright: the dome of the egg creates a gradient. Vigiovo fits a quadratic surface to each egg's brightness by least squares, discarding outliers, and measures how far each pixel falls below it. A stain is measured against that egg, not against a global value.
In parallel it measures colour deviation in Lab space, which catches stains of similar brightness but different colour. Specular highlights are excluded. If stains cover 3% or more of the visible shell, the egg is dirty.
6. Measuring cracks
Black-hat and top-hat filters highlight lines darker or lighter than their surroundings. A line only counts as a crack if it is:
- elongated: at least 5 times longer than wide;
- long: at least 0.35 r;
- deep: it reaches at least 0.4 r into the shell instead of following the rim.
These rules separate cracks from round stains and from the natural shading at the egg's edge. If the total length is 0.8 r or more, the egg is cracked.
7. Decision and scores
The class comes from a threshold rule: dirty if the dirty fraction passes its limit, cracked if the crack length passes its limit, clean otherwise. Ties go to the more severe defect.
The displayed scores look like probabilities but are a fixed formula: the softmax of [1, dirt ÷ limit, crack ÷ limit] divided by a temperature. The highest value always matches the threshold rule, so nobody has to trust a black box.
8. Real-time video
Segmentation is cheap and runs on every frame. Defect measurement is the expensive part, so each egg is measured only 3 times, at least half a radius of travel apart, like the 1/3, 2/3 and 3/3 checks on a quality panel.
Eggs are tracked between frames by nearest neighbour with a constant-velocity prediction and deterministic tie-breaking. An egg is rejected only when the defect shows up in 2 of the 3 checks, which removes one-frame false positives such as glare. In practice that's about 2 measurements per frame with ~45 eggs on screen.
9. What comes out
For each egg, Vigiovo emits a record with the class, the scores, a readable reason and the raw measurements. Below is a real cracked egg from the demo image:
{
"id": 3,
"label": "cracked",
"scores": { "clean": 0.1809, "dirty": 0.0104, "cracked": 0.8087 },
"reasons": ["crack length 1.22r >= 0.80r"],
"measurements": {
"dirt_fraction": 0.0,
"crack_length_r": 1.2193,
"solidity": 0.979,
"ellipse_fill": 0.9973,
"axis_ratio": 1.2178,
"touches_border": false
}
}