Blob Analysis
Blob analysis (connected-component analysis) takes a binary region and breaks it into individual objects, then measures shape properties of each. The typical pipeline is:
- Segment the image into a binary region (
binarize). - Split the region into connected blobs (
components). - Measure each blob with feature functions (
area,centroid,equivalent_ellipse, …).
Connected Components
components(region, dx, dy) returns a Vector{Region} — one element per connected blob. Two runs are considered part of the same blob when their column distance is at most dx and their row gap is at most dy. The defaults dx = dy = 1 implement standard 8-connected labelling; larger values bridge small gaps between nearby objects.
julia> b1 = region_from_circle(-20, 0, 5);
julia> b2 = region_from_circle(0, 0, 5);
julia> b3 = region_from_circle(20, 0, 5);
julia> three_blobs = union(union(b1, b2), b3);
julia> comps = components(three_blobs);
julia> length(comps) # three separated circles
3
julia> all(c -> area(c) == 81, comps) # all three circles have equal area
trueThe dx parameter bridges horizontal gaps. Two boxes separated by a 3-column gap are two separate blobs at the default dx = 1, but merge into one when dx = 4:
julia> box_a = region_from_box(0, 0, 3, 5); # columns 0–3, rows 0–5
julia> box_b = region_from_box(7, 0, 10, 5); # columns 7–10, rows 0–5
julia> two_boxes = union(box_a, box_b);
julia> length(components(two_boxes)) # gap of 3 columns exceeds dx=1
2
julia> length(components(two_boxes, unsigned(4), unsigned(0))) # dx=4 bridges the gap
1Basic Shape Features
Each connected component is a Region, so all feature functions apply directly.
area returns the pixel count. width and height return the column and row extent of the bounding box. bounds_center returns the midpoint of the bounding box as (column, row). aspect_ratio returns width / height.
julia> blob = region_from_box(0, 0, 9, 1); # 10-wide × 2-tall rectangle
julia> area(blob)
20
julia> width(blob)
10
julia> height(blob)
2
julia> bounds_center(blob)
(4.5, 0.5)
julia> aspect_ratio(blob)
5.0Centroid and Equivalent Ellipse
centroid(r) returns the area-weighted centre (column, row). Unlike bounds_center it is pulled toward dense parts of the region, not just the bounding-box midpoint.
equivalent_ellipse(r) fits the ellipse that has the same area, centroid, and second-order moments as the region. It returns a named tuple (center, semi_axes, angle) where semi_axes = (major, minor) and angle is in radians from the column axis toward the row axis. The equivalent ellipse is useful for estimating the orientation and elongation of a blob.
julia> elong_box = region_from_box(0, -1, 9, 1); # 10-wide × 3-tall, centred at row 0
julia> centroid(elong_box)
(4.5, 0.0)
julia> el = equivalent_ellipse(elong_box);
julia> el.center
(4.5, 0.0)
julia> round(el.semi_axes[1]; digits=4) # major (horizontal) half-axis
5.7446
julia> round(el.angle; digits=4) # 0 — aligned with column axis
0.0Perimeter and Compactness
perimeter(r) counts exposed 4-connected boundary edges (each edge has length 1). compactness(r) normalises the perimeter by the area using the isoperimetric ratio P² / (4π·A), which equals 1 for a perfect circle and grows for more irregular or elongated shapes.
julia> c5 = region_from_circle(0, 0, 5);
julia> perimeter(c5)
44.0
julia> round(compactness(c5); digits=4) # close to 1 — nearly circular
1.902
julia> round(compactness(region_from_box(-2, -2, 2, 2)); digits=4) # 5×5 square
1.2732Convex Hull and Shape Convexity
convex_hull(r) returns the convex hull of the region as an ordered list of half-integer pixel-corner vertices. convex_area and convex_perimeter measure its area and perimeter. convexity returns area / convex_area (1 for convex shapes, < 1 when the shape has concavities). perforation returns the complementary fraction (convex_area − area) / convex_area.
julia> cross = Region([Run(-2, 0:0), Run(-1, 0:0), Run(0, -2:2), Run(1, 0:0), Run(2, 0:0)]);
julia> area(cross)
9
julia> convex_area(cross)
17.0
julia> round(convexity(cross); digits=4) # 9/17 — arms create deep concavities
0.5294
julia> round(perforation(cross); digits=4) # 47% of convex hull not filled by region
0.4706Feret Diameters
feret_diameters(r) returns (min_feret, max_feret) — the minimum and maximum caliper widths obtained by rotating calipers on the convex hull. The minimum Feret diameter is the narrowest gap a physical caliper could pass through. The maximum Feret diameter is the longest diagonal of the convex hull.
julia> blob_wide = region_from_box(0, 0, 9, 1); # 10-wide × 2-tall
julia> mn_f, mx_f = feret_diameters(blob_wide);
julia> mn_f # 2 px — fits through the short dimension
2.0
julia> round(mx_f; digits=3) # corner-to-corner diagonal
10.198Hole Features
number_of_holes(r) counts enclosed background components (the same regions that holes(r) returns). area_of_holes(r) sums their pixel areas. Both are zero for simply-connected (hole-free) regions.
julia> blob_frame = difference(region_from_box(-3, -3, 3, 3),
region_from_box(-1, -1, 1, 1)); # 7×7 frame with 3×3 hole
julia> number_of_holes(blob_frame)
1
julia> area_of_holes(blob_frame)
9Gear Analysis Example
The gear is one connected region (components returns a single element) with a rich toothed boundary. Strong erosion with a large disk SE shrinks every tooth to an isolated stub and separates the stubs. Filtering by minimum area removes erosion artefacts; the remaining blobs are the individual teeth. Shape features then characterise each tooth:
img = load("test/gear.png")
gear = binarize(img, px -> px < 0.9)
println("area: ", area(gear)) # 100431
println("holes: ", number_of_holes(gear)) # 9 — spokes + shaft bore
println("hole area: ", area_of_holes(gear)) # 58110 — enclosed voids
println("convexity: ", round(convexity(gear); digits=3)) # 0.562 — teeth cut deep concavities
println("feret: ", round.(feret_diameters(gear); digits=1)) # (473.1, 481.0) — outer diameter
se18 = region_from_circle(0, 0, 18)
eroded18 = erosion(gear, se18) # shrink until teeth disconnect
tooth_comps = components(eroded18)
teeth = filter(c -> area(c) >= 200, tooth_comps) # drop sub-pixel artefacts
println("teeth: ", length(teeth)) # 7 teeth visible in the image quadrant
for t in sort(teeth, by=area, rev=true)
cen = centroid(t)
el = equivalent_ellipse(t)
println(" area=$(area(t)) centroid=($(round(Int,cen[1])), $(round(Int,cen[2])))",
" major=$(round(el.semi_axes[1]; digits=1))")
endThe convexity of 0.56 quantifies what is visible in the image: only about 56% of the gear's convex hull is actually filled, with the remainder being the inter-tooth concavities. The 9 holes — 4 spoke cavities and the central shaft bore — account for more than half the gear's own pixel area (58 110 out of 100 431 pixels).