Python binding for the libvips image processing library with high-performance streaming architecture
—
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.
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)")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')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)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)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)}")# 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)# 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