Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Python image processing for microscopy and bioimage analysis. Read/write images, filter (Gaussian, median, LoG), segment (thresholding, watershed, active contours), measure region properties, detect features. SciPy/NumPy ecosystem. Use OpenCV for real-time video; CellPose for DL cell segmentation; napari for visualization.
.claude/skills/jaechang-hits-scikit-image-processing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 187% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 117% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 222% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 193% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 467% | 0% |
scikit-image is a Python library for image processing in the SciPy ecosystem. It provides algorithms for reading/writing images, filtering (noise reduction, edge detection), geometric transforms, segmentation (thresholding, watershed, active contours), object measurement (area, intensity, shape descriptors), and feature detection. Images are represented as NumPy arrays, enabling seamless integration with NumPy, SciPy, matplotlib, and pandas. Widely used for fluorescence microscopy, histology, and general bioimage analysis.
OpenCV instead for real-time video processing or GPU-accelerated operationsCellPose instead (better accuracy for touching cells)napari instead for interactive multi-dimensional image visualization and annotationPathML or histolab insteadscikit-image, numpy, scipy, matplotlibbashpip install scikit-image numpy scipy matplotlib # For reading proprietary microscopy formats pip install tifffile aicsimageio # Verify python -c "import skimage; print(skimage.__version__)"
pythonfrom skimage import io, filters, measure import numpy as np # Load → denoise → threshold → measure img = io.imread("cells.tif") img_smooth = filters.gaussian(img, sigma=1.5) threshold = filters.threshold_otsu(img_smooth) binary = img_smooth > threshold regions = measure.regionprops(measure.label(binary)) print(f"Found {len(regions)} objects") print(f"Mean area: {np.mean([r.area for r in regions]):.1f} px²")
pythonfrom skimage import io, img_as_float, img_as_uint import numpy as np # Read single image img = io.imread("nuclei.tif") print(f"Shape: {img.shape}, dtype: {img.dtype}") # (512, 512), uint16 # Read image collection from directory from skimage import io as ski_io images = ski_io.ImageCollection("data/*.tif") print(f"Loaded {len(images)} images") # Type conversions (critical for correct arithmetic) img_f = img_as_float(img) # uint16 → float64, range [0, 1] img_u8 = (img_f * 255).astype(np.uint8) # → 8-bit # Save image io.imsave("output.tif", img_u8)
python# Multi-channel fluorescence (TIFF with CZYX or ZCYX dims) import tifffile stack = tifffile.imread("multichannel.tif") # shape: (C, Z, Y, X) dapi = stack[0] # DAPI channel gfp = stack[1] # GFP channel print(f"DAPI: {dapi.shape}, GFP: {gfp.shape}") # Maximum intensity projection along Z mip = dapi.max(axis=0) io.imsave("dapi_mip.tif", mip)
pythonfrom skimage import filters, restoration import numpy as np # Gaussian blur (denoising, smoothing) from skimage.filters import gaussian smoothed = gaussian(img, sigma=2.0) # Median filter (salt-and-pepper noise removal) from skimage.filters import median from skimage.morphology import disk denoised = median(img, footprint=disk(3)) # Top-hat transform (background subtraction for uneven illumination) from skimage.morphology import white_tophat, disk background_removed = white_tophat(img, footprint=disk(50)) print(f"Background removed: range [{background_removed.min()}, {background_removed.max()}]")
python# Edge detection from skimage.filters import sobel, laplace, prewitt edges_sobel = sobel(img_as_float(img)) edges_laplace = laplace(img_as_float(img)) # Difference of Gaussians (blob-like structure detection) from skimage.filters import difference_of_gaussians blob_enhanced = difference_of_gaussians(img_as_float(img), low_sigma=1, high_sigma=3) # Contrast enhancement (CLAHE: local histogram equalization) from skimage.exposure import equalize_adapthist enhanced = equalize_adapthist(img_as_float(img), clip_limit=0.03)
pythonfrom skimage import filters, morphology, segmentation from skimage.color import label2rgb import numpy as np # Automatic thresholding methods from skimage.filters import (threshold_otsu, threshold_li, threshold_triangle, threshold_yen) img_f = img_as_float(img) print(f"Otsu: {threshold_otsu(img_f):.3f}") print(f"Li: {threshold_li(img_f):.3f}") # Apply threshold and clean binary mask binary = img_f > threshold_otsu(img_f) binary_clean = morphology.remove_small_objects(binary, min_size=50) binary_filled = morphology.remove_small_holes(binary_clean, area_threshold=100)
python# Watershed segmentation (separate touching objects) from skimage.segmentation import watershed from skimage.feature import peak_local_max from scipy import ndimage as ndi # Distance transform → local maxima → watershed distance = ndi.distance_transform_edt(binary_filled) coords = peak_local_max(distance, min_distance=20, labels=binary_filled) mask = np.zeros(distance.shape, dtype=bool) mask[tuple(coords.T)] = True markers = ndi.label(mask)[0] labels = watershed(-distance, markers, mask=binary_filled) print(f"Segmented objects: {labels.max()}") overlay = label2rgb(labels, image=img_f, bg_label=0)
pythonfrom skimage.morphology import (erosion, dilation, opening, closing, disk, ball, binary_erosion, binary_dilation) # Erosion and dilation eroded = erosion(binary, footprint=disk(3)) dilated = dilation(binary, footprint=disk(5)) # Opening: erosion then dilation (removes small objects, smooths edges) opened = opening(binary, footprint=disk(3)) # Closing: dilation then erosion (fills small holes) closed = closing(binary, footprint=disk(5)) # Skeletonization from skimage.morphology import skeletonize skeleton = skeletonize(binary) print(f"Skeleton pixels: {skeleton.sum()}")
pythonfrom skimage import measure import pandas as pd # Label connected components labeled = measure.label(binary_filled) # Extract region properties props = measure.regionprops(labeled, intensity_image=img_as_float(img)) # Convert to DataFrame data = [] for r in props: data.append({ "label": r.label, "area": r.area, "perimeter": r.perimeter, "eccentricity": r.eccentricity, "mean_intensity": r.mean_intensity, "max_intensity": r.max_intensity, "centroid_y": r.centroid[0], "centroid_x": r.centroid[1], "bbox": r.bbox, }) df = pd.DataFrame(data) print(f"Objects: {len(df)}") print(df[["area", "mean_intensity", "eccentricity"]].describe().round(2))
python# Filter by property thresholds cells = df[(df["area"] > 100) & (df["area"] < 5000) & (df["eccentricity"] < 0.9)] print(f"Valid cells: {len(cells)}") # Measure co-localization: fraction of channel-1 signal in channel-2 positive mask from skimage.measure import regionprops_table import numpy as np # For multi-channel images table = regionprops_table( labeled, intensity_image=np.stack([dapi, gfp], axis=-1), properties=["label", "area", "mean_intensity"] )
pythonfrom skimage.feature import blob_log, blob_dog, corner_harris, corner_peaks from skimage import transform # Laplacian of Gaussian blob detection (nuclei, puncta) blobs = blob_log(img_as_float(img), min_sigma=5, max_sigma=20, num_sigma=5, threshold=0.05) print(f"Blobs detected: {len(blobs)}") # blobs columns: [y, x, sigma] where radius = sqrt(2) * sigma # Difference of Gaussians (faster alternative) blobs_dog = blob_dog(img_as_float(img), min_sigma=5, max_sigma=20, threshold=0.02)
python# Geometric transforms from skimage import transform # Rescale img_small = transform.rescale(img_as_float(img), 0.5) # Rotate img_rotated = transform.rotate(img_as_float(img), angle=15, resize=True) # Affine registration (align two images) from skimage.registration import phase_cross_correlation shift, error, _ = phase_cross_correlation(ref_img, moving_img) print(f"Alignment shift: {shift} px, error: {error:.4f}")
scikit-image represents images as NumPy arrays. Shape conventions:
| Image Type | Shape | dtype | |-----------|-------|-------| | Grayscale 2D | (H, W) | uint8, uint16, float64 | | RGB color | (H, W, 3) | uint8 | | Multichannel | (H, W, C) | any | | Z-stack | (Z, H, W) | any |
dtype matters: Most algorithms expect float64 in 0, 1]. Use img_as_float(img) before processing; convert back with img_as_uint(img) for saving.
Goal: Segment DAPI-stained nuclei and measure GFP fluorescence per nucleus.
pythonfrom skimage import io, filters, morphology, measure, img_as_float from skimage.segmentation import watershed from skimage.feature import peak_local_max from scipy import ndimage as ndi import pandas as pd import numpy as np import tifffile # Load 2-channel image (DAPI=ch0, GFP=ch1) img = tifffile.imread("cells.tif") dapi = img_as_float(img[0]) gfp = img_as_float(img[1]) # Segment nuclei from DAPI channel dapi_smooth = filters.gaussian(dapi, sigma=2) threshold = filters.threshold_otsu(dapi_smooth) binary = dapi_smooth > threshold binary = morphology.remove_small_objects(binary, min_size=200) binary = morphology.remove_small_holes(binary, area_threshold=500) # Watershed to separate touching nuclei distance = ndi.distance_transform_edt(binary) coords = peak_local_max(distance, min_distance=30, labels=binary) mask = np.zeros_like(distance, dtype=bool) mask[tuple(coords.T)] = True markers = ndi.label(mask)[0] labels = watershed(-distance, markers, mask=binary) # Measure GFP per nucleus props = measure.regionprops(labels, intensity_image=gfp) df = pd.DataFrame([{ "nucleus_id": p.label, "area_px2": p.area, "gfp_mean": p.mean_intensity, "gfp_max": p.max_intensity, } for p in props]) df.to_csv("nucleus_measurements.csv", index=False) print(f"Nuclei: {len(df)}, mean GFP: {df['gfp_mean'].mean():.3f}")
Goal: Apply the same preprocessing and measurement pipeline to a folder of images.
pythonfrom pathlib import Path from skimage import io, filters, measure, img_as_float, morphology import pandas as pd results = [] for img_path in sorted(Path("data/").glob("*.tif")): img = img_as_float(io.imread(img_path)) if img.ndim == 3: img = img.mean(axis=-1) # convert RGB to grayscale # Preprocess smooth = filters.gaussian(img, sigma=1.5) thresh = filters.threshold_otsu(smooth) binary = morphology.remove_small_objects(smooth > thresh, min_size=50) # Measure labeled = measure.label(binary) props = measure.regionprops(labeled, intensity_image=img) for p in props: results.append({ "image": img_path.stem, "object_id": p.label, "area": p.area, "mean_intensity": p.mean_intensity, }) df = pd.DataFrame(results) df.to_csv("batch_results.csv", index=False) print(f"Processed {df['image'].nunique()} images, {len(df)} objects total")
| Function | Parameter | Default | Range/Options | Effect | |----------|-----------|---------|---------------|--------| | gaussian | sigma | 1.0 | 0.5–10+ | Smoothing kernel size; larger = more blur | | median | footprint | disk(1) | disk(1–10) | Median filter neighborhood | | threshold_otsu | — | — | — | Returns automatic threshold (no tuning) | | remove_small_objects | min_size | 64 | any integer | Remove objects smaller than N pixels | | watershed | — | — | — | Uses marker positions from peak_local_max | | peak_local_max | min_distance | 1 | 5–50 | Min separation between detected peaks (px) | | blob_log | min_sigma / max_sigma | 1/50 | dependent | Expected blob radius range | | regionprops | intensity_image | None | array | Image for intensity measurements |
uint8 silently clip to 0. Convert to float: img = img_as_float(img) as the first step.try_all_threshold(img) from skimage.area_um2 = area_px * pixel_size_um**2.pythonfrom skimage import io, filters, measure, img_as_float from skimage.feature import blob_log import numpy as np img = img_as_float(io.imread("spots.tif")) blobs = blob_log(img, min_sigma=2, max_sigma=8, num_sigma=5, threshold=0.05) intensities = [] for y, x, sigma in blobs: r = int(np.ceil(np.sqrt(2) * sigma)) region = img[max(0,int(y-r)):int(y+r), max(0,int(x-r)):int(x+r)] intensities.append({"y": y, "x": x, "radius": r, "mean_intensity": region.mean()}) import pandas as pd df = pd.DataFrame(intensities) print(f"Spots: {len(df)}, mean intensity: {df['mean_intensity'].mean():.4f}")
pythonfrom skimage import filters, morphology, img_as_float import numpy as np # Segment objects positive in BOTH channels ch1 = img_as_float(dapi) ch2 = img_as_float(gfp) mask1 = ch1 > filters.threshold_otsu(ch1) mask2 = ch2 > filters.threshold_otsu(ch2) combined_mask = mask1 & mask2 # objects in both channels combined_mask = morphology.binary_closing(combined_mask) print(f"Co-positive pixels: {combined_mask.sum()}")
pythonfrom skimage.color import label2rgb from skimage import io, img_as_ubyte import matplotlib.pyplot as plt overlay = label2rgb(labels, image=img_as_float(dapi), bg_label=0, alpha=0.5) fig, axes = plt.subplots(1, 2, figsize=(12, 6)) axes[0].imshow(dapi, cmap="gray") axes[0].set_title(f"DAPI (raw)") axes[1].imshow(overlay) axes[1].set_title(f"Segmented ({labels.max()} nuclei)") for ax in axes: ax.axis("off") plt.tight_layout() plt.savefig("segmentation_overlay.png", dpi=300, bbox_inches="tight") print("Saved segmentation_overlay.png")
| Problem | Cause | Solution | |---------|-------|----------| | OverflowError in arithmetic | Integer overflow from uint8/uint16 ops | Convert to float first: img = img_as_float(img) | | Otsu threshold finds one class | Bimodal distribution absent | Try threshold_li() or try_all_threshold(img) for comparison | | Watershed over-segments | Markers too close together | Increase min_distance in peak_local_max; smooth distance map | | All objects merged in binary | Threshold too low | Check plt.hist(img.ravel()) for histogram; manually adjust | | regionprops missing intensity stats | intensity_image not provided | Pass: regionprops(labels, intensity_image=img) | | 3D images processed as 2D | Z-stack not detected | Check shape: img.shape; process per slice or use 3D functions | | Tiny noise objects in binary | Threshold too aggressive | Apply morphology.remove_small_objects(binary, min_size=50) |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 19,668 | 20,015 | +2% | 1 | 1 | 0% | 3,214 | 6,989 | +117% | 0 | 0 | — |
case-02 | pass→pass | 12,369 | 8,700 | -30% | 1 | 1 | 0% | 1,973 | 6,362 | +222% | 0 | 0 | — |
case-03 | pass→pass | 15,831 | 9,990 | -37% | 1 | 1 | 0% | 2,274 | 6,657 | +193% | 0 | 0 | — |
case-04 | fail→pass | 12,723 | 6,171 | -51% | 1 | 1 | 0% | 2,070 | 5,938 | +187% | 0 | 0 | — |
case-05 | pass→pass | 5,882 | 5,042 | -14% | 1 | 1 | 0% | 1,052 | 5,962 | +467% | 0 | 0 | — |
case-06 | pass→pass | 13,320 | 8,831 | -34% | 1 | 1 | 0% | 2,336 | 6,608 | +183% | 0 | 0 | — |
case-07 | pass→pass | 4,470 | 4,641 | +4% | 1 | 1 | 0% | 806 | 5,772 | +616% | 0 | 0 | — |
case-08 | pass→pass | 4,005 | 3,282 | -18% | 1 | 1 | 0% | 713 | 5,536 | +676% | 0 | 0 | — |
case-09 | pass→pass | 3,814 | 4,102 | +8% | 1 | 1 | 0% | 618 | 5,710 | +824% | 0 | 0 | — |
case-10 | pass→pass | 8,350 | 5,411 | -35% | 1 | 1 | 0% | 1,406 | 5,956 | +324% | 0 | 0 | — |
case-11 | pass→pass | 5,790 | 5,418 | -6% | 1 | 1 | 0% | 1,206 | 5,975 | +395% | 0 | 0 | — |
case-12 | pass→pass | 4,749 | 3,809 | -20% | 1 | 1 | 0% | 764 | 5,614 | +635% | 0 | 0 | — |
case-13 | pass→pass | 8,294 | 3,711 | -55% | 1 | 1 | 0% | 771 | 5,616 | +628% | 0 | 0 | — |
case-14 | pass→pass | 4,795 | 3,409 | -29% | 1 | 1 | 0% | 830 | 5,603 | +575% | 0 | 0 | — |
case-15 | pass→pass | 11,253 | 5,750 | -49% | 1 | 1 | 0% | 2,094 | 6,164 | +194% | 0 | 0 | — |
case-16 | pass→pass | 5,823 | 5,062 | -13% | 1 | 1 | 0% | 1,119 | 5,860 | +424% | 0 | 0 | — |
case-17 | pass→pass | 4,018 | 4,001 | -0% | 1 | 1 | 0% | 667 | 5,699 | +754% | 0 | 0 | — |
case-18 | pass→pass | 7,884 | 15,370 | +95% | 1 | 1 | 0% | 1,468 | 6,399 | +336% | 0 | 0 | — |
case-19 | pass→pass | 6,211 | 5,015 | -19% | 1 | 1 | 0% | 1,138 | 5,892 | +418% | 0 | 0 | — |
case-20 | pass→pass | 4,950 | 5,419 | +9% | 1 | 1 | 0% | 863 | 5,874 | +581% | 0 | 0 | — |
case-21 | pass→pass | 6,173 | 4,466 | -28% | 1 | 1 | 0% | 1,146 | 5,715 | +399% | 0 | 0 | — |
case-22 | pass→pass | 4,970 | 3,379 | -32% | 1 | 1 | 0% | 951 | 5,641 | +493% | 0 | 0 | — |
case-23 | pass→pass | 5,414 | 19,162 | +254% | 1 | 1 | 0% | 931 | 6,072 | +552% | 0 | 0 | — |
case-24 | pass→pass | 3,974 | 4,727 | +19% | 1 | 1 | 0% | 713 | 5,807 | +714% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 24 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 24 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.