scikit image
This guide defines the definitive best practices for developing with scikit-image, ensuring maintainable, performant, and robust image processing code.
المهارة نفسها
# 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
```python
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}") # False, same object modified
```
✅ GOOD: Return a new array
```python
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}") # True, new object returned
```
### 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
```python
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
```python
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))
# Usage:
# image_path = "path/to/your/image.png"
# img = load_grayscale_image(image_path)
# blurred_img = apply_gaussian_blur(img, sigma=1.5)
# final_img = normalize_intensity(blurred_img)
```
### 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.
```python
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.
"""
# Implementation details...
return image.astype(np.float64) # Simplified for example
```
## 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`
```python
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."""
# e.g., subtracting 50 from a pixel value of 20 results in 0, not -30
return filters.gaussian(image, sigma=1) # Gaussian filter expects float for proper behavior
```
✅ GOOD: Processing with `float`
```python
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) # Converts to float64, scales to [0, 1]
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.
```python
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)
# Perform processing here (e.g., convert_to_float, apply_filter, etc.)
processed_image = image # Placeholder
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
```python
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] # Assuming uint8
return inverted
```
✅ GOOD: Vectorized operation
```python
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."""
# Works for both uint8 (255 - pixel) and float (1.0 - pixel)
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
```python
import numpy as np
from skimage import io, util
float_img = np.random.rand(100, 100) # Image in [0, 1] range
io.imsave("bad_float_image.png", float_img) # Will save as uint8, clipping values
```
✅ GOOD: Converting to appropriate integer type before saving
```python
import numpy as np
from skimage import io, util
float_img = np.random.rand(100, 100)
uint8_img = util.img_as_ubyte(float_img) # Converts float [0, 1] to uint8 [0, 255]
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.
```python
from skimage import data, util, filters
import matplotlib.pyplot as plt
# Load example image
coins = data.coins()
# Process directly (BAD) vs. convert first (GOOD)
# bad_edges = filters.sobel(coins) # Sobel expects float for proper gradient calculation
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') # Displaying float [0, 1] is fine for matplotlib
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.
```bash
# Create a virtual environment
python -m venv .venv
# Activate it
# On macOS/Linux:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate
# Install scikit-image and other dependencies
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:
```bash
# After activating your virtual environment
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.
```python
import numpy as np
from numpy.testing import assert_array_equal, assert_allclose
from skimage import util
import pytest
# Assuming normalize_intensity is defined as in Section 2.1
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)
```
كيف تثبتها
1. Open the tool and start a new conversation, project or template. 2. Paste the text below wherever the tool keeps standing instructions — usually called system prompt, instructions, or persona. 3. Save it so it applies to every run rather than a single message.