cv-dot-annotation-blob-diff-extraction
SkillMediaRecover (x, y, class) point labels from color-coded dot-annotation image pairs via absdiff + blackout masking + Laplacian-of-Gaussian blob detection + center-pixel RGB classification
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the cv-dot-annotation-blob-diff-extraction skill
What this skill tells your AI
The instructions your AI receives, as published by wenmin-wu/ds-skills in skills/cv/dot-annotation-blob-diff-extraction/SKILL.md and read by ahel’s review.
Overview
Many wildlife/cell/object-counting datasets ship annotations as a second copy of the image with colored dots painted over each instance. You have to recover the point list yourself. The canonical recipe is four steps: cv2.absdiff(dotted, raw) to isolate the dots, bitwise-mask out any blacked-out exclusion regions present in either image, run skimage.feature.blob_log with tight sigma bounds matched to the known dot radius, then classify each blob by reading the centroid pixel color from the dotted image (not the diff — the diff desaturates the color). Used in NOAA Steller Sea Lion Population Count top kernels.
Quick Start
import cv2, numpy as np, skimage.feature
img_raw = cv2.imread(raw_path)
img_dot = cv2.imread(dotted_path)
diff = cv2.absdiff(img_dot, img_raw)
# mask out blacked-out regions (annotator exclusions) from either image
m1 = cv2.cvtColor(img_dot, cv2.COLOR_BGR2GRAY); m1[m1 < 20] = 0; m1[m1 > 0] = 255
m2 = cv2.cvtColor(img_raw, cv2.COLOR_BGR2GRAY); m2[m2 < 20] = 0; m2[m2 > 0] = 255
diff = cv2.bitwise_or(diff, diff, mask=m1)
diff = cv2.bitwise_or(diff, diff, mask=m2)
gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
blobs = skimage.feature.blob_log(gray, min_sigma=3, max_sigma=4, num_sigma=1, threshold=0.02)
points = []
for y, x, _ in blobs:
b, g, r = img_dot[int(y), int(x)]
if r > 200 and b < 50 and g < 50: cls = 'adult_male'
elif r > 200 and b > 200 and g < 50: cls = 'subadult_male'
elif r < 100 and g > 100 and b < 100: cls = 'juvenile'
elif r < 100 and g < 100 and 150 < b < 200: cls = 'pup'
elif r < 150 and g < 50 and b < 100: cls = 'adult_female'
else: continue
points.append((int(x), int(y), cls))
Workflow
- Compute
cv2.absdiff(dotted, raw)to leave only the dot pixels - Build blackout masks from both images by thresholding grayscale < 20 and bitwise-OR the diff through them
- Convert to grayscale and run
blob_logwithmin/max_sigmabracketing the expected dot radius andnum_sigma=1for speed - For each blob centroid, sample the BGR pixel from the original dotted image (preserves saturation)
- Route the pixel through a hand-tuned RGB decision tree; drop fall-throughs to an
errorbucket to quantify noise
Key Decisions
- absdiff, not signed subtract: color-agnostic and symmetric across image order.
- Mask from BOTH images: annotators black out regions in either copy; missing either mask leaks false blobs.
num_sigma=1with tight sigma range: single-scale LoG is fast and precise when dot radius is fixed.- Sample color from
img_dot, notdiff: the diff fades saturated colors, wrecking the decision tree. - Explicit
errorfallback: catches out-of-gamut dots and quantifies labeling noise rather than silently mislabeling.
References
Signals
- GitHub stars
- 60
- Forks
- 4
- Last commit
- Apr 2026
Advanced
- Catalog kind
- skill
- Gateway key
cv-dot-annotation-blob-diff-extraction- Source
- github.com/wenmin-wu/ds-skills