Auto-labeling Visible Object Masks with HSV

By Ju Lin · Published July 12, 2026

Automatically detect and label 3D-printed objects in video frames using HSV color space analysis and saturation thresholding to separate printed parts from the printer bed.

65 cells1 experiment16 views0 forks

Inside this notebook

# Auto-Labeling & Segmentation Model Training — 3D Printer Bed Object Masks This notebook builds an auto-labeling pipeline for 3D-printer bed timelapse frames (from `mirdain_data.zip`) — first via HSV-threshold segmentation, then via SAM 3 prompted segmentation — to generate per-object binary masks for the printed squares. It then trains and compares conditioned U-Net models (RGB-only vs. RGB+bed-plan conditioning, with/without hue augmentation) to predict object masks directly from frames, and evaluates robustness to camera color shift. **Inputs:** `mirdain_data.zip` (print timelapse frames + bed plan/print layout). **Outputs:** per-frame label masks/overlays (`label_outputs*` folders), trained U-Net checkpoints, and branch-comparison metrics in `results.json`.

import zipfile, os, shutil, glob, json, math, random
import numpy as np
import cv2
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.colors import hsv_to_rgb
from tqdm.auto import tqdm
import torch
from pathlib import Path

os.makedirs('/home/user/extracted/images', exist_ok=True)

with zipfile.ZipFile('mirdain_data.zip', 'r') as z:
    z.extractall('/home/user/extracted')

frames = sorted(glob.glob('/home/user/extracted/images/*.jpg'))
print(f"Found {len(frames)} frames")
print(f"Sample: {frames[0]}, {frames[-1]}")
…
Found 293 frames
Sample: /home/user/extracted/images/00300.jpg, /home/user/extracted/images/00600.jpg
bed_mask shape: (256, 256, 3), dtype: uint8
bed_mask unique values: [  0 255]
Frame shape: (720, 1280, 3), dtype: uint8
# Explore bed_mask: the print plan (32 objects in BED coordinates)
bed_mask = cv2.imread('/home/user/extracted/bed_mask.png', cv2.IMREAD_GRAYSCALE)
print(f"bed_mask shape: {bed_mask.shape}")

# Find connected components (the 32 objects)
num_labels, labels_im = cv2.connectedComponents(bed_mask)
print(f"Number of connected components (including background 0): {num_labels}")
# Should be 33 (background + 32 objects)

# Show the bed mask
plt.figure(figsize=(12, 12))
plt.imshow(bed_mask, cmap='gray')
plt.title(f'Bed Mask - {num_labels-1} objects (0=background)')
plt.colorbar(label='Object ID')
plt.axis('off')
plt.show()

# Analyze each object
…
bed_mask shape: (256, 256)
Number of connected components (including background 0): 33

Obj#      Area             Centroid
    1      841 (   72.0,    35.0)
    2      841 (  109.0,    35.0)
    3      841 (  146.0,    35.0)
    4      841 (  183.0,    35.0)
    5      841 (  220.0,    35.0)
    6      841 (   35.0,    72.0)
    7      841 (   72.0,    72.0)
    8      841 (  109.0,    72.0)
    9      841 (  146.0,    72.0)
   10      841 (  183.0,    72.0)
   11      841 (  220.0,    72.0)…
# Sample a few frames to understand layout
sample_frames = frames[::60]  # one every 60 frames
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
axes = axes.ravel()

for i, fp in enumerate(sample_frames[:6]):
    img = cv2.imread(fp)
    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    axes[i].imshow(img_rgb)
    axes[i].set_title(f"Frame {Path(fp).stem}")
    axes[i].axis('off')

plt.tight_layout()
plt.show()
# Deep-dive into HSV characteristics of printed parts vs bed
sample = cv2.imread(frames[0])
hsv = cv2.cvtColor(sample, cv2.COLOR_BGR2HSV)

# Take a center crop to look at printed parts vs bare bed
h_ch, s_ch, v_ch = cv2.split(hsv)

plt.figure(figsize=(18, 10))
plt.subplot(2, 3, 1)
plt.imshow(cv2.cvtColor(sample, cv2.COLOR_BGR2RGB))
plt.title('Original')
plt.axis('off')

plt.subplot(2, 3, 2)
plt.imshow(h_ch, cmap='hsv')
plt.title('Hue')
plt.colorbar()
plt.axis('off')
…
# More careful look at HSV — pick specific printed objects vs bed
# Let's manually identify a few printed squares and bare bed
img = cv2.imread(frames[20])  # pick a frame mid-print
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(16, 8))
plt.subplot(1, 2, 1)
plt.imshow(rgb)
plt.title('Frame - pick a printed square area')
# Mark specific points to sample
plt.scatter([400, 600, 800, 200, 1000], [300, 350, 400, 100, 500], c='red', s=50)
plt.axis('off')

plt.subplot(1, 2, 2)
s_ch = hsv[:,:,1]
plt.imshow(s_ch, cmap='gray')
plt.title('Saturation channel')
…
# Systematic HSV analysis across multiple frames
# Goal: find robust saturation threshold to separate printed parts from grey bed

sample_frames_idx = [0, 30, 60, 90, 120, 150]
s_samps = []
v_samps = []
h_samps = []
bg_samp = []

fig, axes = plt.subplots(2, 3, figsize=(18, 8))
for i, idx in enumerate(sample_frames_idx):
    img = cv2.imread(frames[idx])
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    s_ch = hsv[:,:,1]
    v_ch = hsv[:,:,2]
    h_ch = hsv[:,:,0]
    
…
# Systematic HSV analysis - save figures to files
os.makedirs('/home/user/analysis', exist_ok=True)

sample_frames_idx = [0, 30, 60, 90, 120, 150]
fig, axes = plt.subplots(2, 3, figsize=(18, 8))
for i, idx in enumerate(sample_frames_idx):
    img = cv2.imread(frames[idx])
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    s_ch = hsv[:,:,1]
    ax = axes[i//3, i%3]
    im = ax.imshow(s_ch, cmap='viridis', vmin=0, vmax=255)
    ax.set_title(f'Frame {Path(frames[idx]).stem} Saturation')
    ax.axis('off')
    plt.colorbar(im, ax=ax, shrink=0.6)
plt.tight_layout()
plt.savefig('/home/user/analysis/saturation_maps.png', dpi=120)
plt.show()
print("Saved saturation_maps.png")
…
import os, glob, zipfile, numpy as np, cv2, matplotlib.pyplot as plt
from pathlib import Path
os.makedirs('/home/user/extracted/images', exist_ok=True)

# Re-extract if needed
if not os.path.exists('/home/user/extracted/images/00300.jpg'):
    with zipfile.ZipFile('mirdain_data.zip', 'r') as z:
        z.extractall('/home/user/extracted')

frames = sorted(glob.glob('/home/user/extracted/images/*.jpg'))
print(f"Found {len(frames)} frames")

os.makedirs('/home/user/analysis', exist_ok=True)

sample_frames_idx = [0, 30, 60, 90, 120, 150]
fig, axes = plt.subplots(2, 3, figsize=(18, 8))
for i, idx in enumerate(sample_frames_idx):
    img = cv2.imread(frames[idx])
…
Saved to outputs (downloadable from the file manager):
  • analysis/saturation_hist.png (45873 bytes)
  • analysis/saturation_maps.png (1453529 bytes)
  • extracted/bed_mask.png (680 bytes)
  • extracted/images/00300.jpg (149565 bytes)
  • extracted/images/00301.jpg (150461 bytes)
  • extracted/images/00302.jpg (150272 bytes)
  • extracted/images/00303.jpg (155531 bytes)
  • extracted/images/00304.jpg (151847 bytes)
  • extracted/images/00305.jpg (147888 bytes)
  • extracted/images/00306.jpg (14…

This is a preview. Open the live notebook to see all 65 cells with their charts and full outputs, or fork it into your own Clusy workspace.