Skip to content

Commit 6bd9392

Browse files
committed
Merge branch 'Tobias-Fischer-patch-2'
add comprehensive fixup of the small blob moment issue
2 parents 8968867 + bad2e13 commit 6bd9392

File tree

2 files changed

+112
-7
lines changed

2 files changed

+112
-7
lines changed

src/machinevisiontoolbox/ImageBlobs.py

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def __str__(self):
100100
:return: compact string representation
101101
:rtype: str
102102
"""
103-
return f"Blob[{self.id}](area={self._moments.m00:.2g}, color={self.color}, parent={self.parent.id if self.parent else None}"
103+
return f"Blob[{self.id}](area={self.moments.m00:.2g}, color={self.color}, parent={self.parent.id if self.parent else None}"
104104

105105
def __repr__(self):
106106
return str(self)
@@ -302,13 +302,110 @@ def __init__(self, image=None, kulpa=True, **kwargs):
302302

303303
blob.contourpoint = blob.perimeter[:, 0]
304304

305-
## For a single set pixel OpenCV returns all moments as zero, skip such blobs
306-
## TODO handle this situation by setting m00=1, m10=x, m01=y etc.
305+
## For a single set pixel OpenCV returns all moments as zero, let's fix it
307306
if blob._moments["m00"] == 0:
308307
runts += 1
309-
else:
310-
self.data.append(blob)
308+
309+
# Raw moments
310+
for uv in contour:
311+
for p, q in [
312+
(0, 0),
313+
(1, 0),
314+
(0, 1),
315+
(2, 0),
316+
(1, 1),
317+
(0, 2),
318+
(3, 0),
319+
(2, 1),
320+
(1, 2),
321+
(0, 3),
322+
]:
323+
u = uv[0]
324+
v = uv[1]
325+
blob._moments[f"m{p}{q}"] += u**p * v**q
326+
327+
# Central moments
328+
blob._moments["mu10"] = blob._moments["mu01"] = 0
329+
blob._moments["mu20"] = (
330+
blob._moments["m20"]
331+
- blob._moments["m10"] ** 2 / blob._moments["m00"]
332+
)
333+
blob._moments["mu02"] = (
334+
blob._moments["m02"]
335+
- blob._moments["m01"] ** 2 / blob._moments["m00"]
336+
)
337+
blob._moments["mu11"] = (
338+
blob._moments["m11"]
339+
- blob._moments["m10"] * blob._moments["m01"] / blob._moments["m00"]
340+
)
341+
342+
# Third-order central moments
343+
blob._moments["mu30"] = (
344+
blob._moments["m30"]
345+
- 3
346+
* blob._moments["m20"]
347+
* blob._moments["m10"]
348+
/ blob._moments["m00"]
349+
+ 2 * (blob._moments["m10"] ** 3) / (blob._moments["m00"] ** 2)
350+
)
351+
blob._moments["mu03"] = (
352+
blob._moments["m03"]
353+
- 3
354+
* blob._moments["m02"]
355+
* blob._moments["m01"]
356+
/ blob._moments["m00"]
357+
+ 2 * (blob._moments["m01"] ** 3) / (blob._moments["m00"] ** 2)
358+
)
359+
blob._moments["mu21"] = (
360+
blob._moments["m21"]
361+
- 2
362+
* blob._moments["m11"]
363+
* blob._moments["m10"]
364+
/ blob._moments["m00"]
365+
- blob._moments["m20"] * blob._moments["m01"] / blob._moments["m00"]
366+
+ 2
367+
* (blob._moments["m10"] ** 2 * blob._moments["m01"])
368+
/ (blob._moments["m00"] ** 2)
369+
)
370+
blob._moments["mu12"] = (
371+
blob._moments["m12"]
372+
- 2
373+
* blob._moments["m11"]
374+
* blob._moments["m01"]
375+
/ blob._moments["m00"]
376+
- blob._moments["m02"] * blob._moments["m10"] / blob._moments["m00"]
377+
+ 2
378+
* (blob._moments["m01"] ** 2 * blob._moments["m10"])
379+
/ (blob._moments["m00"] ** 2)
380+
)
381+
382+
# Normalised central moments
383+
blob._moments["nu20"] = blob._moments["mu20"] / (
384+
blob._moments["m00"] ** (1 + (2 / 2))
385+
)
386+
blob._moments["nu02"] = blob._moments["mu02"] / (
387+
blob._moments["m00"] ** (1 + (2 / 2))
388+
)
389+
blob._moments["nu11"] = blob._moments["mu11"] / (
390+
blob._moments["m00"] ** (1 + (2 / 2))
391+
)
392+
393+
# Third-order normalised moments
394+
blob._moments["nu30"] = blob._moments["mu30"] / (
395+
blob._moments["m00"] ** (1 + (3 / 2))
396+
)
397+
blob._moments["nu03"] = blob._moments["mu03"] / (
398+
blob._moments["m00"] ** (1 + (3 / 2))
399+
)
400+
blob._moments["nu21"] = blob._moments["mu21"] / (
401+
blob._moments["m00"] ** (1 + (3 / 2))
402+
)
403+
blob._moments["nu12"] = blob._moments["mu12"] / (
404+
blob._moments["m00"] ** (1 + (3 / 2))
405+
)
406+
311407
allblobs.append(blob)
408+
self.data.append(blob)
312409

313410
## second pass: equivalent ellipse
314411

tests/test_blobs.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ def test_blobs1(self):
3030

3131
self.assertEqual(len(blobs), 1)
3232
self.assertEqual(blobs[0].moments.m00, 1)
33+
self.assertEqual(blobs[0].u, 4.5)
34+
self.assertEqual(blobs[0].v, 3.5)
3335
self.assertEqual(blobs[0].perimeter_length, 3)
3436

3537
def test_blobs2(self):
@@ -51,7 +53,10 @@ def test_blobs2(self):
5153
)
5254
blobs = im.blobs()
5355

54-
self.assertEqual(len(blobs), 0)
56+
self.assertEqual(len(blobs), 1)
57+
self.assertEqual(blobs[0].area, 1)
58+
self.assertEqual(blobs[0].u, 4.0)
59+
self.assertEqual(blobs[0].v, 3.0)
5560

5661
def test_blobs3(self):
5762
# 1 blob: single pixel width line
@@ -73,7 +78,10 @@ def test_blobs3(self):
7378
)
7479
blobs = im.blobs()
7580

76-
self.assertEqual(len(blobs), 0)
81+
self.assertEqual(len(blobs), 1)
82+
self.assertEqual(blobs[0].area, 4)
83+
self.assertEqual(blobs[0].u, 4.0)
84+
self.assertEqual(blobs[0].v, 3.0)
7785

7886
def test_blobs4(self):
7987
# 2 blobs: 3x3 and 4x4

0 commit comments

Comments
 (0)