| name | scikit-image |
| description | [Applies to: **/*.py] This guide defines the definitive best practices for developing with scikit-image, ensuring maintainable, performant, and robust image processing code. |
| source | cursor_mdc |
scikit-image Best Practices
This document outlines the mandatory guidelines for all scikit-image development within our team. Adhering to these rules ensures consistency, performance, and long-term maintainability of our image processing pipelines.
1. Core Principles
scikit-image is built on NumPy. All functions must accept and return plain ndarray objects.
1.1. Immutability of Input Images
Always treat input images as immutable. Functions must return new arrays, never modify inputs in-place. This prevents unexpected side effects and simplifies debugging.
❌ BAD: In-place modification
import numpy as np
from skimage import exposure
def normalize_image_bad(image: np.ndarray) -> None:
"""❌ BAD: Modifies the input image directly."""
image[:] = exposure.rescale_intensity(image, out_range=(0, 1))
img = np.array([[0, 100], [50, 200]], dtype=np.uint8)
original_img_id = id(img)
normalize_image_bad(img)
print(f"Image ID changed? {id(img) != original_img_id}")
✅ GOOD: Return a new array
import numpy as np
from skimage import exposure
from numpy.typing import NDArray, Any
def normalize_image_good(image: NDArray[Any, Any]) -> NDArray[Any, Any]:
"""✅ GOOD: Returns a new, processed image array."""
return exposure.rescale_intensity(image, out_range=(0, 1))
img = np.array([[0, 100], [50, 200]], dtype=np.uint8)
original_img_id = id(img)
processed_img = normalize_image_good(img)
print(f"Image ID changed? {id(processed_img) != original_img_id}")
1.2. Public API Usage
Always use the documented public API. Avoid internal or private functions (prefixed with _) to ensure forward compatibility and stability.
2. Code Organization and Structure
2.1. Modular Functions
Break down complex image processing tasks into small, focused, and reusable functions. Each function should do one thing well.
❌ BAD: Monolithic function
import numpy as np
from skimage import io, filters, exposure
def process_image_bad(path: str) -> np.ndarray:
"""❌ BAD: Combines loading, filtering, and normalization."""
image = io.imread(path, as_gray=True)
blurred = filters.gaussian(image, sigma=1)
normalized = exposure.rescale_intensity(blurred, out_range=(0, 1))
return normalized
✅ GOOD: Chained, focused functions
import numpy as np
from skimage import io, filters, exposure
from numpy.typing import NDArray, Any
def load_grayscale_image(path: str) -> NDArray[Any, Any]:
"""Loads an image from path as grayscale."""
return io.imread(path, as_gray=True)
def apply_gaussian_blur(image: NDArray[Any, Any], sigma: float) -> NDArray[Any, Any]:
"""Applies a Gaussian blur to the image."""
return filters.gaussian(image, sigma=sigma)
def normalize_intensity(image: NDArray[Any, Any]) -> NDArray[Any, Any]:
"""Rescales image intensity to [0, 1]."""
return exposure.rescale_intensity(image, out_range=(0, 1))
2.2. Docstrings and Type Hints
Every function, class, and method must have a comprehensive docstring following PEP 257 and explicit type hints following PEP 484. Use numpy.typing.NDArray for NumPy arrays.
from typing import Tuple, Literal
import numpy as np
from numpy.typing import NDArray, DTypeLike
def convert_to_float(
image: NDArray[DTypeLike, Any],
*,
force_copy: bool = False
) -> NDArray[np.float64, Any]:
"""Converts an image to floating-point representation.
Parameters
----------
image : NDArray
The input image array.
force_copy : bool, optional
If True, always return a new array, even if the dtype is already float.
Defaults to False.
Returns
-------
NDArray[np.float64, Any]
The image converted to float64, with pixel values scaled to [0, 1]
if the original dtype was integer.
"""
return image.astype(np.float64)
3. Common Patterns and Anti-patterns
3.1. Image Data Types and Ranges
Always process images in floating-point format (e.g., np.float64) with values normalized to [0, 1]. Convert to uint8 or uint16 only for display or saving. Use skimage.util.img_as_float for reliable conversion.
❌ BAD: Processing with uint8
import numpy as np
from skimage import filters
def process_uint8_bad(image: np.ndarray) -> np.ndarray:
"""❌ BAD: Operations on uint8 can lead to clipping and precision loss."""
return filters.gaussian(image, sigma=1)
✅ GOOD: Processing with float
import numpy as np
from skimage import filters, util
from numpy.typing import NDArray, Any
def process_float_good(image: NDArray[Any, Any]) -> NDArray[Any, Any]:
"""✅ GOOD: Converts to float for accurate processing."""
float_image = util.img_as_float(image)
processed_float = filters.gaussian(float_image, sigma=1)
return processed_float
3.2. Channel Order
Always use (Height, Width, Channels) (HWC) order for color images to maintain consistency with matplotlib and common deep learning frameworks. scikit-image functions generally expect channels as the last dimension.
3.3. Loading and Saving Images
Always use skimage.io.imread and skimage.io.imsave. These functions handle various image formats and integrate well with scikit-image's ndarray expectations.
from skimage import io
import numpy as np
from numpy.typing import NDArray, Any
def load_and_save_image(input_path: str, output_path: str) -> None:
"""Loads an image, processes it (placeholder), and saves it."""
image: NDArray[Any, Any] = io.imread(input_path)
processed_image = image
io.imsave(output_path, processed_image)
4. Performance Considerations
4.1. Vectorized Operations
Always leverage NumPy's vectorized operations. Avoid explicit Python loops over pixel values, as they are significantly slower.
❌ BAD: Pixel-wise loop
import numpy as np
def invert_image_bad(image: np.ndarray) -> np.ndarray:
"""❌ BAD: Slow pixel-wise iteration."""
inverted = np.zeros_like(image)
for r in range(image.shape[0]):
for c in range(image.shape[1]):
inverted[r, c] = 255 - image[r, c]
return inverted
✅ GOOD: Vectorized operation
import numpy as np
from numpy.typing import NDArray, Any
def invert_image_good(image: NDArray[Any, Any]) -> NDArray[Any, Any]:
"""✅ GOOD: Fast vectorized operation."""
return image.max() - image
4.2. Memory Management
Be mindful of memory usage with large images. Operations that create many intermediate copies can consume significant RAM. Where possible, use functions that operate in-place (if the function is designed for it and explicitly documented to do so, which is rare in scikit-image's public API for image data).
5. Common Pitfalls and Gotchas
5.1. Incorrect Data Type Scaling
Forgetting to convert to float [0, 1] before processing, or failing to convert back to uint8 [0, 255] (or uint16 [0, 65535]) before saving, leads to incorrect results or corrupted images.
❌ BAD: Saving float image directly
import numpy as np
from skimage import io, util
float_img = np.random.rand(100, 100)
io.imsave("bad_float_image.png", float_img)
✅ GOOD: Converting to appropriate integer type before saving
import numpy as np
from skimage import io, util
float_img = np.random.rand(100, 100)
uint8_img = util.img_as_ubyte(float_img)
io.imsave("good_uint8_image.png", uint8_img)
5.2. Misinterpreting skimage.data
skimage.data provides example images. These are often uint8 or uint16. Always convert them to float using util.img_as_float before performing image processing operations.
from skimage import data, util, filters
import matplotlib.pyplot as plt
coins = data.coins()
good_edges = filters.sobel(util.img_as_float(coins))
fig, axes = plt.subplots(1, 2, figsize=(8, 4))
axes[0].imshow(coins, cmap='gray')
axes[0].set_title('Original (uint8)')
axes[1].imshow(good_edges, cmap='gray')
axes[1].set_title('Sobel Edges (float)')
plt.show()
6. Virtual Environments and Packaging
6.1. Virtual Environments
Always use a virtual environment (venv or conda) for scikit-image projects. This ensures reproducible environments and avoids dependency conflicts.
python -m venv .venv
source .venv/bin/activate
.venv\Scripts\activate
pip install -U scikit-image numpy scipy matplotlib
6.2. Packaging
For project-specific dependencies, use pyproject.toml or requirements.txt. For development, install in editable mode:
pip install -e .
7. Testing Approaches
7.1. Unit Tests
Every new function and significant change must be covered by unit tests using pytest. Tests should verify correctness across various inputs, edge cases, and data types. Use numpy.testing.assert_array_equal or assert_allclose for comparing image arrays.
import numpy as np
from numpy.testing import assert_array_equal, assert_allclose
from skimage import util
import pytest
def test_normalize_intensity_uint8():
img = np.array([[0, 127, 255]], dtype=np.uint8)
expected = np.array([[0.0, 0.5, 1.0]], dtype=np.float64)
result = normalize_intensity(util.img_as_float(img))
assert_allclose(result, expected)
def test_normalize_intensity_float():
img = np.array([[0.0, 0.5, 1.0]], dtype=np.float64)
expected = np.array([[0.0, 0.5, 1.0]], dtype=np.float64)
result = normalize_intensity(img)
assert_allclose(result, expected)
def test_normalize_intensity_empty_image():
img = np.array([], dtype=np.uint8).reshape(0, 0)
expected = np.array([], dtype=np.float64).reshape(0, 0)
result = normalize_intensity(util.img_as_float(img))
assert_allclose(result, expected)