CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-pyvips

Python binding for the libvips image processing library with high-performance streaming architecture

Pending
Overview
Eval results
Files

array-integration.mddocs/

Array Integration

Seamless integration with Python arrays and NumPy for data exchange, enabling easy interoperability with the scientific Python ecosystem. PyVips provides efficient conversion between images and arrays without unnecessary data copying.

Capabilities

Array Export

Convert PyVips images to Python arrays and NumPy arrays for analysis and processing with other libraries.

def tolist(self) -> list:
    """
    Convert single-band image to Python list of lists.
    
    IMPORTANT: Only works for single-band images. Multi-band images will raise NotImplementedError.
    
    Returns:
    List of lists representing image rows: [[row1_pixels], [row2_pixels], ...]
    - Each inner list contains pixel values for one row
    - Complex format images return complex numbers (a + bj)
    
    Raises:
    NotImplementedError: If image has more than one band
    """

def numpy(self, dtype=None):
    """
    Convert image to NumPy array (convenience method for method chaining).
    
    This mimics PyTorch behavior: image.op1().op2().numpy()
    
    Parameters:
    - dtype: str or numpy dtype, target data type (optional, uses image format if None)
    
    Returns:
    NumPy array with shape:
    - (height, width) for single-band images (channel axis removed)
    - (height, width, bands) for multi-band images
    - Single-pixel single-band images become 0D arrays
    
    Requires numpy as runtime dependency.
    """

def __array__(self, dtype=None):
    """
    NumPy array interface implementation for np.array() compatibility.
    
    Parameters:
    - dtype: str or numpy dtype, target data type (optional, uses image format if None)
    
    Returns:
    NumPy array with same shape rules as numpy() method
    
    Requires numpy as runtime dependency.
    """

Example usage:

import numpy as np

# Convert single-band image to Python list
gray_image = image.colourspace('b-w')  # Convert to single-band first
gray_list = gray_image.tolist()  # Returns [[row1_pixels], [row2_pixels], ...]
print(f"List type: {type(gray_list)}")
print(f"Number of rows: {len(gray_list)}")
print(f"Pixels in first row: {len(gray_list[0])}")

# tolist() only works for single-band images
try:
    # This will fail for multi-band images
    rgb_list = image.tolist()  # NotImplementedError for multi-band
except NotImplementedError:
    print("tolist() only works for single-band images")
    # Use numpy() instead for multi-band images
    array = image.numpy()
    pixel_list = array.tolist()  # Convert numpy array to list

# Convert to NumPy array
array = image.numpy()
print(f"Array shape: {array.shape}")
print(f"Array dtype: {array.dtype}")

# Specify target dtype
float_array = image.numpy(dtype=np.float32)
uint16_array = image.numpy(dtype=np.uint16)

# Use NumPy array interface
array = np.array(image)  # Equivalent to image.numpy()
array = np.asarray(image, dtype=np.float64)

# Access array properties
height, width = array.shape[:2]
if len(array.shape) == 3:
    bands = array.shape[2]
    print(f"Shape: {height}x{width}x{bands}")
else:
    print(f"Shape: {height}x{width} (grayscale)")

Array Import

Create PyVips images from Python arrays and NumPy arrays for processing with PyVips operations.

@classmethod
def new_from_list(cls, array: list, scale: float = 1.0, 
                  offset: float = 0.0) -> 'Image':
    """
    Create image from Python list.
    
    Parameters:
    - array: 2D list of pixel values [[row1], [row2], ...]
    - scale: float, scale factor applied to pixel values
    - offset: float, offset added to pixel values
    
    Returns:
    Image object created from list data
    """

@classmethod
def new_from_array(cls, array, scale: float = 1.0, offset: float = 0.0, 
                   interpretation: str = None) -> 'Image':
    """
    Create image from array (list or NumPy array).
    
    Parameters:
    - array: 2D array of pixel values
    - scale: float, scale factor 
    - offset: float, offset value
    - interpretation: str, color space interpretation
    
    Returns:
    Image object created from array data
    """

Example usage:

# Create from Python list
pixel_data = [
    [255, 128, 0],    # Row 1: Red, Half-intensity, Black
    [128, 255, 128],  # Row 2: Half-intensity, Green, Half-intensity
    [0, 128, 255]     # Row 3: Black, Half-intensity, Blue
]
image_from_list = pyvips.Image.new_from_list(pixel_data)

# Create from NumPy array
import numpy as np

# RGB image from array
rgb_array = np.random.randint(0, 256, (100, 100, 3), dtype=np.uint8)
image_from_array = pyvips.Image.new_from_array(rgb_array)

# Grayscale image
gray_array = np.random.randint(0, 256, (100, 100), dtype=np.uint8)
gray_image = pyvips.Image.new_from_array(gray_array)

# Float array with scaling
float_array = np.random.random((50, 50)).astype(np.float32)
float_image = pyvips.Image.new_from_array(float_array, scale=255.0)

# Specify color space interpretation
srgb_array = np.zeros((100, 100, 3), dtype=np.uint8)
srgb_array[:, :, 0] = 255  # Red channel
srgb_image = pyvips.Image.new_from_array(srgb_array, interpretation='srgb')

# Complex data
complex_array = np.random.random((50, 50)) + 1j * np.random.random((50, 50))
# Note: Complex arrays need special handling - convert to real representation
real_part = complex_array.real
imag_part = complex_array.imag
combined = np.stack([real_part, imag_part], axis=-1)
complex_image = pyvips.Image.new_from_array(combined, interpretation='complex')

NumPy Interoperability

Seamless integration with NumPy ecosystem for scientific computing workflows.

import numpy as np
import matplotlib.pyplot as plt
import scipy.ndimage

# PyVips to NumPy workflow
image = pyvips.Image.new_from_file('photo.jpg')
array = image.numpy()

# NumPy operations
blurred_array = scipy.ndimage.gaussian_filter(array, sigma=2.0)
edges_array = scipy.ndimage.sobel(array.mean(axis=2))  # Edge detection on grayscale

# Visualization with matplotlib
plt.figure(figsize=(15, 5))

plt.subplot(131)
plt.imshow(array)
plt.title('Original')
plt.axis('off')

plt.subplot(132)
plt.imshow(blurred_array)
plt.title('Blurred (SciPy)')
plt.axis('off')

plt.subplot(133)
plt.imshow(edges_array, cmap='gray')
plt.title('Edges (SciPy)')
plt.axis('off')

plt.tight_layout()
plt.show()

# Convert back to PyVips
blurred_image = pyvips.Image.new_from_array(blurred_array)
edges_image = pyvips.Image.new_from_array(edges_array)

# Continue processing with PyVips
result = (blurred_image
    .resize(0.5)
    .sharpen()
    .colourspace('srgb'))

# Combine PyVips and NumPy processing
def hybrid_processing(pyvips_image):
    # PyVips preprocessing
    preprocessed = (pyvips_image
        .thumbnail_image(800)
        .gaussblur(0.5))
    
    # Convert to NumPy for custom algorithm
    array = preprocessed.numpy()
    
    # Custom NumPy processing
    # Apply custom filter
    kernel = np.array([[-1, -1, -1], 
                       [-1,  9, -1], 
                       [-1, -1, -1]])
    if len(array.shape) == 3:
        filtered = np.zeros_like(array)
        for i in range(array.shape[2]):
            filtered[:, :, i] = scipy.ndimage.convolve(array[:, :, i], kernel)
    else:
        filtered = scipy.ndimage.convolve(array, kernel)
    
    # Convert back to PyVips
    result_image = pyvips.Image.new_from_array(filtered)
    
    # PyVips postprocessing
    final = (result_image
        .linear([1.1, 1.1, 1.1], [0, 0, 0])  # Slight contrast boost
        .colourspace('srgb'))
    
    return final

# Use hybrid processing
processed = hybrid_processing(image)

Data Type Handling

Proper handling of different data types between PyVips and NumPy.

import numpy as np

# PyVips format to NumPy dtype mapping
format_to_dtype = {
    'uchar': np.uint8,
    'char': np.int8,
    'ushort': np.uint16,
    'short': np.int16,
    'uint': np.uint32,
    'int': np.int32,
    'float': np.float32,
    'double': np.float64,
    'complex': np.complex64,
    'dpcomplex': np.complex128
}

def get_numpy_dtype(image):
    """Get appropriate NumPy dtype for PyVips image."""
    return format_to_dtype.get(image.format, np.float32)

# Convert with appropriate dtype
image = pyvips.Image.new_from_file('photo.jpg')
appropriate_dtype = get_numpy_dtype(image)
array = image.numpy(dtype=appropriate_dtype)

# Handle different bit depths
def normalize_to_float(image):
    """Normalize image to 0-1 float range regardless of input format."""
    array = image.numpy()
    
    if image.format == 'uchar':
        return array.astype(np.float32) / 255.0
    elif image.format == 'ushort':
        return array.astype(np.float32) / 65535.0
    elif image.format in ['float', 'double']:
        return array.astype(np.float32)
    else:
        # For other formats, use image statistics
        min_val = array.min()
        max_val = array.max()
        if max_val > min_val:
            return (array.astype(np.float32) - min_val) / (max_val - min_val)
        else:
            return array.astype(np.float32)

# Convert back with proper scaling
def array_to_image_format(array, target_format='uchar'):
    """Convert NumPy array to specific PyVips format with proper scaling."""
    if target_format == 'uchar':
        # Scale to 0-255
        scaled = np.clip(array * 255.0, 0, 255).astype(np.uint8)
    elif target_format == 'ushort':
        # Scale to 0-65535
        scaled = np.clip(array * 65535.0, 0, 65535).astype(np.uint16)
    elif target_format == 'float':
        scaled = array.astype(np.float32)
    else:
        scaled = array
    
    return pyvips.Image.new_from_array(scaled)

# Example usage
normalized = normalize_to_float(image)
# Process in float space
processed_array = normalized * 1.2  # Brighten
# Convert back to original format
result = array_to_image_format(processed_array, target_format=image.format)

Batch Processing with Arrays

Efficient batch processing combining PyVips and NumPy for handling multiple images.

import numpy as np
from pathlib import Path

def process_image_batch(image_paths, output_dir):
    """Process multiple images with combined PyVips/NumPy operations."""
    results = []
    
    for path in image_paths:
        # Load with PyVips
        image = pyvips.Image.new_from_file(str(path))
        
        # Standardize size with PyVips
        standard = image.thumbnail_image(512)
        
        # Convert to NumPy for batch operations
        array = standard.numpy()
        
        # Collect for batch processing
        results.append({
            'path': path,
            'array': array,
            'original': image
        })
    
    # Batch NumPy operations
    all_arrays = np.array([r['array'] for r in results])
    
    # Compute batch statistics
    batch_mean = np.mean(all_arrays, axis=0)
    batch_std = np.std(all_arrays, axis=0)
    
    # Apply batch normalization
    normalized_arrays = []
    for i, array in enumerate(all_arrays):
        normalized = (array - batch_mean) / (batch_std + 1e-8)
        normalized = np.clip(normalized * 50 + 128, 0, 255).astype(np.uint8)
        normalized_arrays.append(normalized)
    
    # Convert back to PyVips and save
    for i, result in enumerate(results):
        normalized_image = pyvips.Image.new_from_array(normalized_arrays[i])
        
        # Final PyVips processing
        final = (normalized_image
            .colourspace('srgb')
            .sharpen())
        
        # Save with original filename
        output_path = Path(output_dir) / f"processed_{result['path'].name}"
        final.write_to_file(str(output_path))

# Machine learning data preparation
def prepare_ml_dataset(image_dir, target_size=(224, 224)):
    """Prepare images for machine learning with PyVips and NumPy."""
    image_paths = list(Path(image_dir).glob('*.jpg'))
    
    dataset = []
    labels = []
    
    for path in image_paths:
        # Load and preprocess with PyVips
        image = pyvips.Image.new_from_file(str(path))
        
        # Standardize with smart cropping
        processed = (image
            .thumbnail_image(target_size[0], crop='attention')
            .colourspace('srgb'))
        
        # Ensure exact size (pad if necessary)
        if processed.width != target_size[0] or processed.height != target_size[1]:
            # Center crop or pad
            left = (processed.width - target_size[0]) // 2
            top = (processed.height - target_size[1]) // 2
            
            if left >= 0 and top >= 0:
                processed = processed.crop(left, top, target_size[0], target_size[1])
            else:
                # Pad with black
                processed = processed.gravity('centre', target_size[0], target_size[1])
        
        # Convert to NumPy for ML framework
        array = processed.numpy().astype(np.float32) / 255.0
        
        dataset.append(array)
        
        # Extract label from filename or directory
        label = path.stem.split('_')[0]  # Assuming format: "class_image.jpg"
        labels.append(label)
    
    return np.array(dataset), labels

# Use ML preparation
X, y = prepare_ml_dataset('training_images/')
print(f"Dataset shape: {X.shape}")
print(f"Labels: {set(y)}")

Performance Considerations

Memory Efficiency

# Efficient array conversion for large images
def efficient_array_processing(large_image):
    """Process large images efficiently with arrays."""
    
    # For very large images, process in tiles
    if large_image.width * large_image.height > 50_000_000:  # 50MP threshold
        # Tile-based processing
        tile_size = 2048
        result_tiles = []
        
        for y in range(0, large_image.height, tile_size):
            for x in range(0, large_image.width, tile_size):
                # Extract tile
                w = min(tile_size, large_image.width - x)
                h = min(tile_size, large_image.height - y)
                tile = large_image.crop(x, y, w, h)
                
                # Process tile with NumPy
                tile_array = tile.numpy()
                processed_array = your_numpy_function(tile_array)
                processed_tile = pyvips.Image.new_from_array(processed_array)
                
                result_tiles.append((x, y, processed_tile))
        
        # Reconstruct image from tiles
        # (Implementation depends on specific needs)
        
    else:
        # Direct processing for smaller images
        array = large_image.numpy()
        processed_array = your_numpy_function(array)
        return pyvips.Image.new_from_array(processed_array)

# Avoid unnecessary copies
def zero_copy_when_possible(image):
    """Minimize memory copies during array operations."""
    
    # Check if array conversion is really needed
    if can_use_pyvips_directly():
        return image.some_pyvips_operation()
    
    # Convert to array only when necessary
    array = image.numpy()
    
    # Modify array in-place when possible
    array *= 1.1  # In-place multiplication
    array += 10   # In-place addition
    
    return pyvips.Image.new_from_array(array)

Type Optimization

# Choose appropriate data types
def optimize_array_types(image):
    """Use appropriate data types for different operations."""
    
    if image.format == 'uchar' and needs_precision():
        # Convert to float for high-precision operations
        float_array = image.numpy(dtype=np.float32)
        processed = complex_float_operation(float_array)
        # Convert back to uint8 with proper scaling
        result_array = np.clip(processed, 0, 1) * 255
        return pyvips.Image.new_from_array(result_array.astype(np.uint8))
    else:
        # Keep original type for simple operations
        array = image.numpy()
        processed = simple_operation(array)
        return pyvips.Image.new_from_array(processed)

Install with Tessl CLI

npx tessl i tessl/pypi-pyvips

docs

array-integration.md

enumerations.md

image-creation.md

image-operations.md

image-output.md

index.md

io-connections.md

properties-metadata.md

system-control.md

tile.json