Hi,
I updated to the last Valis repo yesterday, was code was working well before BUT now it doesn't :/
Here is my code and the error : (Also for whatever reason at the error measurement step the dimensions are swapped ! so I added some code to verify and swap it back (no idea why this occurs , BUT if you can have a look that will be great !)
Could you please have a look and fix it , I really need Valis to align images :D
THanks
antho
Code :
VALIS last update :
RUNNING 1:44 8/18/2025
import os
import pathlib
import numpy as np
from pathlib import Path
from tqdm import tqdm
import pyvips
import cv2
from skimage import exposure
from valis import valtils, slide_io, registration
from valis import feature_detectors, feature_matcher # Import the new feature modules
from valis.micro_rigid_registrar import MicroRigidRegistrar
import shutil
import re
Add missing imports
try:
from tqdm.notebook import tqdm
except ImportError:
from tqdm import tqdm
print("tqdm.notebook not found. Using tqdm instead.")
def organize_channels_automatically(base_dir, target_dapi_dir=None, target_all_channels_dir=None):
"""
Automatically organize channel files from a base directory
Args:
base_dir: Path to the folder containing all channel files (e.g., ".../lb06/R2/")
target_dapi_dir: Where to move ch00 files (if None, creates dapi subfolder)
target_all_channels_dir: Where to move ch01/ch02 files (if None, creates all_channels subfolder)
"""
base_path = Path(base_dir)
# Create target directories if not specified
if target_dapi_dir is None:
target_dapi_dir = base_path / "dapi"
else:
target_dapi_dir = Path(target_dapi_dir)
if target_all_channels_dir is None:
target_all_channels_dir = base_path / "all_channels"
else:
target_all_channels_dir = Path(target_all_channels_dir)
# Create directories if they don't exist
target_dapi_dir.mkdir(parents=True, exist_ok=True)
target_all_channels_dir.mkdir(parents=True, exist_ok=True)
print(f"Organizing files from: {base_path}")
print(f"DAPI (ch00) files will go to: {target_dapi_dir}")
print(f"Other channels will go to: {target_all_channels_dir}")
print("-" * 60)
# Get all files in the base directory
all_files = [f for f in base_path.iterdir() if f.is_file()]
moved_files = {"dapi": [], "other_channels": [], "unrecognized": []}
for file_path in all_files:
filename = file_path.name.lower()
# Check for channel patterns
if "_ch00" in filename or "_merged_ch00" in filename:
# This is a DAPI file
target_path = target_dapi_dir / file_path.name
shutil.move(str(file_path), str(target_path))
moved_files["dapi"].append(file_path.name)
print(f"📁 DAPI: {file_path.name}")
elif "_ch01" in filename or "_ch02" in filename or "_ch03" in filename or "_merged_ch01" in filename or "_merged_ch02" in filename or "_merged_ch03" in filename:
# This is another channel
target_path = target_all_channels_dir / file_path.name
shutil.move(str(file_path), str(target_path))
moved_files["other_channels"].append(file_path.name)
print(f"🔬 Channel: {file_path.name}")
else:
# Unrecognized file pattern
moved_files["unrecognized"].append(file_path.name)
print(f"❓ Unrecognized: {file_path.name}")
# Summary
print("\n" + "="*60)
print("ORGANIZATION COMPLETE!")
print("="*60)
print(f"✅ DAPI files moved: {len(moved_files['dapi'])}")
print(f"✅ Other channel files moved: {len(moved_files['other_channels'])}")
print(f"⚠️ Unrecognized files: {len(moved_files['unrecognized'])}")
if moved_files["unrecognized"]:
print("\nUnrecognized files (not moved):")
for filename in moved_files["unrecognized"]:
print(f" - {filename}")
return moved_files
def get_round_name(src_f):
"""Extract the round name from the filename"""
img_name = valtils.get_name(src_f)
round_name = img_name.lower().split("_merged")[0]
return round_name
def get_channel_number(src_f):
"""Extract the channel number from the filename"""
img_name = os.path.basename(src_f).lower()
if "_ch00" in img_name:
return 0
elif "_ch01" in img_name:
return 1
elif "_ch02" in img_name:
return 2
else:
return None
def pad_image(img, target_width, target_height):
"""Pad image to target dimensions, centering the original content"""
pad_width = max(0, target_width - img.width)
pad_height = max(0, target_height - img.height)
if pad_width > 0 or pad_height > 0:
pad_left = pad_width // 2
pad_top = pad_height // 2
padded = img.embed(pad_left, pad_top, target_width, target_height, extend="black")
return padded
return img
def apply_clahe_normalization(img_array, clip_limit=2.0, tile_grid_size=(16,16)):
"""Apply CLAHE normalization to improve contrast for registration"""
if img_array.dtype != np.uint8:
# Convert to uint8 for CLAHE
img_normalized = exposure.rescale_intensity(img_array, out_range=(0, 255)).astype(np.uint8)
else:
img_normalized = img_array.copy()
# Apply CLAHE
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
clahe_img = clahe.apply(img_normalized)
return clahe_img
def create_clahe_padded_image(img_path, target_width, target_height, output_path, clip_limit=2.0, tile_grid_size=(16, 16)):
"""Pad image first, then apply CLAHE to the padded result"""
try:
# Load original image
img = pyvips.Image.new_from_file(img_path)
# Print image format for debugging
print(f"Image format for {os.path.basename(img_path)}: {img.format}, bands: {img.bands}")
# Step 1: Pad the image
padded_img = pad_image(img, target_width, target_height)
# Step 2: Convert padded image to numpy for CLAHE processing
img_array = np.ndarray(buffer=padded_img.write_to_memory(),
dtype=np.uint8 if padded_img.format == 'uchar' else np.uint16,
shape=[padded_img.height, padded_img.width, padded_img.bands])
# Step 3: Apply CLAHE to first channel (DAPI)
if padded_img.bands == 1:
clahe_array = apply_clahe_normalization(img_array[:, :, 0], clip_limit, tile_grid_size)
clahe_array = clahe_array[:, :, np.newaxis]
else:
# For multi-channel, apply CLAHE to first channel only
clahe_array = img_array.copy()
clahe_array[:, :, 0] = apply_clahe_normalization(img_array[:, :, 0], clip_limit, tile_grid_size)
# Step 4: Convert back to pyvips - always use 'uchar' for CLAHE output
# CLAHE always outputs uint8 data
clahe_padded_img = pyvips.Image.new_from_memory(clahe_array.data,
clahe_array.shape[1],
clahe_array.shape[0],
clahe_array.shape[2],
'uchar')
# Step 5: Save CLAHE-processed padded image
clahe_padded_img.write_to_file(output_path)
return output_path
except Exception as e:
print(f"Error creating CLAHE-padded image for {img_path}: {str(e)}")
return None
def resize_warped_images(warped_dir, padded_reference_path):
"""
Resize warped images to match padded reference dimensions.
Args:
warped_dir: Directory containing warped images
padded_reference_path: Path to padded reference image to get correct dimensions
"""
print(f"Resizing warped images in {warped_dir}...")
# Load padded reference image to get correct dimensions
ref_img = pyvips.Image.new_from_file(padded_reference_path)
ref_width = ref_img.width
ref_height = ref_img.height
print(f"Padded reference dimensions: {ref_width} x {ref_height}")
# Create backup directory
backup_dir = os.path.join(os.path.dirname(warped_dir), "resize_backup")
os.makedirs(backup_dir, exist_ok=True)
# Process all warped images
resized_count = 0
for root, dirs, files in os.walk(warped_dir):
for file in files:
if file.startswith("warped_") and file.endswith(".tif"):
img_path = os.path.join(root, file)
try:
# Load warped image
warped_img = pyvips.Image.new_from_file(img_path)
# Check if dimensions match reference
if warped_img.width != ref_width or warped_img.height != ref_height:
print(f"Resizing image: {file}")
print(f" Current dimensions: {warped_img.width} x {warped_img.height}")
# Create backup
backup_path = os.path.join(backup_dir, file)
shutil.copy2(img_path, backup_path)
# Resize to match reference dimensions
# Use bilinear interpolation for better quality
resized_img = warped_img.resize(ref_width / warped_img.width,
vscale=ref_height / warped_img.height,
kernel="bilinear")
# Save with proper format preservation
if warped_img.format == 'ushort':
resized_img.write_to_file(img_path, compression="none", tile=False)
else:
resized_img.write_to_file(img_path)
print(f" Resized dimensions: {resized_img.width} x {resized_img.height}")
resized_count += 1
else:
print(f"Image dimensions already match reference: {file}")
except Exception as e:
print(f"Error processing {file}: {str(e)}")
print(f"\nResized {resized_count} images")
return resized_count
def fix_warped_images(individual_channel_dir, reference_slide_path):
"""
Fix warped images that appear squished by ensuring they maintain the correct aspect ratio
relative to the reference image.
Args:
individual_channel_dir: Directory containing warped images
reference_slide_path: Path to reference image to get correct dimensions
"""
print("\nFixing warped images to maintain correct aspect ratio...")
# Load reference image to get correct dimensions
ref_img = pyvips.Image.new_from_file(reference_slide_path)
ref_width = ref_img.width
ref_height = ref_img.height
ref_format = ref_img.format
ref_bands = ref_img.bands
print(f"Reference image: {ref_width}x{ref_height}, format={ref_format}, bands={ref_bands}")
# Create backup directory
backup_dir = os.path.join(os.path.dirname(individual_channel_dir), "warped_backup")
os.makedirs(backup_dir, exist_ok=True)
# Process all warped images in the directory and subdirectories
for root, dirs, files in os.walk(individual_channel_dir):
for file in files:
if file.startswith("warped_") and file.endswith(".tif"):
img_path = os.path.join(root, file)
try:
# Load warped image
warped_img = pyvips.Image.new_from_file(img_path)
# Print image details for debugging
print(f"Processing {file}: {warped_img.width}x{warped_img.height}, format={warped_img.format}, bands={warped_img.bands}")
# Check if dimensions match reference
if warped_img.width != ref_width or warped_img.height != ref_height:
print(f"Fixing dimensions for {file}")
# Create backup
backup_path = os.path.join(backup_dir, file)
shutil.copy2(img_path, backup_path)
# For 16-bit images, ensure we preserve the format
# Use affine transformation instead of resize to maintain aspect ratio
# Calculate the scale factors
scale_x = ref_width / warped_img.width
scale_y = ref_height / warped_img.height
# Use the minimum scale to avoid stretching
scale = min(scale_x, scale_y)
# Calculate dimensions after scaling
new_width = int(warped_img.width * scale)
new_height = int(warped_img.height * scale)
# Calculate padding to center the image
left_pad = (ref_width - new_width) // 2
top_pad = (ref_height - new_height) // 2
# Create transformation matrix for affine transform
# [scale, 0, left_pad, 0, scale, top_pad]
transform = [scale, 0, left_pad, 0, scale, top_pad]
# Apply affine transformation with proper interpolation
interp = pyvips.Interpolate.new('bicubic')
fixed_img = warped_img.affine(transform, interpolate=interp)
# If the image is smaller than reference after transform, pad it
if fixed_img.width < ref_width or fixed_img.height < ref_height:
fixed_img = fixed_img.embed(
0, 0,
ref_width, ref_height,
extend="black"
)
# Ensure we crop to exact dimensions if somehow larger
if fixed_img.width > ref_width or fixed_img.height > ref_height:
fixed_img = fixed_img.crop(0, 0, ref_width, ref_height)
# Save with proper format preservation
# For 16-bit TIFFs, we need to ensure we save with the right options
if warped_img.format == 'ushort':
fixed_img.write_to_file(img_path, compression="none", tile=False)
else:
fixed_img.write_to_file(img_path)
print(f"Fixed image saved: {img_path}")
except Exception as e:
print(f"Error fixing {file}: {str(e)}")
print("✅ Fixed warped images to match reference dimensions")
return backup_dir
def main():
# Set up base directory
base_dir = "####your directory /##"
# Organize files automatically
print("Organizing channel files...")
organize_channels_automatically(base_dir)
# Set up directories using the organized structure
slide_src_dir = Path(base_dir) / "dapi" # Now points to organized DAPI folder
path_to_all_channels = Path(base_dir) / "all_channels" # Now points to organized channels folder
# Results directory
results_dst_dir = Path(base_dir) / "registration"
results_dst_dir.mkdir(parents=True, exist_ok=True)
registered_slide_dst_dir = results_dst_dir / "registered_slides"
registered_slide_dst_dir.mkdir(parents=True, exist_ok=True)
# Reference slide - now from the organized DAPI folder
reference_slide_name = "###.tif" # Update this to your actual reference
reference_slide = str(slide_src_dir / reference_slide_name)
print(f"✅ DAPI images directory: {slide_src_dir}")
print(f"✅ All channels directory: {path_to_all_channels}")
print(f"✅ Results directory: {results_dst_dir}")
print(f"✅ Reference slide: {reference_slide}")
# Process images: PAD FIRST, then CLAHE on DAPI only
print("Starting image processing: PADDING FIRST, then CLAHE on DAPI only...")
# Create output directories
src_dir = str(slide_src_dir)
dst_dir = str(results_dst_dir)
all_channels_dir = str(path_to_all_channels)
# Directory for padded images (original intensities)
padded_dir = os.path.join(dst_dir, "padded_images")
os.makedirs(padded_dir, exist_ok=True)
padded_dapi_dir = os.path.join(padded_dir, "dapi")
os.makedirs(padded_dapi_dir, exist_ok=True)
padded_all_channels_dir = os.path.join(padded_dir, "all_channels")
os.makedirs(padded_all_channels_dir, exist_ok=True)
padded_ref_dir = os.path.join(padded_dir, "reference")
os.makedirs(padded_ref_dir, exist_ok=True)
# Directory for CLAHE-padded DAPI images (for registration only)
clahe_padded_dir = os.path.join(dst_dir, "clahe_padded_dapi")
os.makedirs(clahe_padded_dir, exist_ok=True)
# Directory for individual warped channels
individual_channel_dir = os.path.join(dst_dir, "warped_channels")
os.makedirs(individual_channel_dir, exist_ok=True)
# Get all images
dapi_imgs = [os.path.join(src_dir, f) for f in os.listdir(src_dir)]
all_channel_imgs = [os.path.join(all_channels_dir, f) for f in os.listdir(all_channels_dir)]
print(f"Found {len(dapi_imgs)} DAPI images for registration")
print(f"Found {len(all_channel_imgs)} other channel images")
# STEP 1: Find maximum dimensions across all images
max_width = 0
max_height = 0
print("Finding maximum dimensions across all images...")
for img_path in dapi_imgs + all_channel_imgs:
try:
img = pyvips.Image.new_from_file(img_path)
max_width = max(max_width, img.width)
max_height = max(max_height, img.height)
# Print format for the first few images to debug
if max_width == img.width or max_height == img.height:
print(f"New max from {os.path.basename(img_path)}: {img.width}x{img.height}, format={img.format}")
except Exception as e:
print(f"Error reading {img_path}: {str(e)}")
print(f"Maximum dimensions: {max_width} x {max_height}")
# STEP 2: Create padded images (original intensities) AND CLAHE-padded DAPI images
print("Creating padded images and CLAHE-padded DAPI images...")
# Process reference image
ref_img = pyvips.Image.new_from_file(reference_slide)
print(f"Reference image format: {ref_img.format}, bands: {ref_img.bands}")
padded_ref_img = pad_image(ref_img, max_width, max_height)
ref_basename = os.path.basename(reference_slide)
padded_reference_slide = os.path.join(padded_ref_dir, f"padded_{ref_basename}")
# Save with proper format preservation for 16-bit images
if ref_img.format == 'ushort':
padded_ref_img.write_to_file(padded_reference_slide, compression="none", tile=False)
else:
padded_ref_img.write_to_file(padded_reference_slide)
print(f"Padded reference: {padded_reference_slide}")
# Create CLAHE-padded version of reference for registration
clahe_padded_reference = os.path.join(clahe_padded_dir, f"clahe_padded_{ref_basename}")
create_clahe_padded_image(reference_slide, max_width, max_height, clahe_padded_reference)
print(f"CLAHE-padded reference: {clahe_padded_reference}")
# Process DAPI images with existence check
padded_dapi_paths = []
clahe_padded_dapi_paths = []
for img_path in dapi_imgs:
try:
basename = os.path.basename(img_path)
padded_path = os.path.join(padded_dapi_dir, f"padded_{basename}")
# Check if padded image already exists
if os.path.exists(padded_path):
print(f"✅ Reusing existing padded DAPI: {basename}")
padded_dapi_paths.append(padded_path)
continue # Skip processing if exists
# Process new image
img = pyvips.Image.new_from_file(img_path)
padded_img = pad_image(img, max_width, max_height)
if img.format == 'ushort':
padded_img.write_to_file(padded_path, compression="none", tile=False)
else:
padded_img.write_to_file(padded_path)
padded_dapi_paths.append(padded_path)
# Create CLAHE-padded version (for registration)
clahe_padded_path = os.path.join(clahe_padded_dir, f"clahe_padded_{basename}")
if create_clahe_padded_image(img_path, max_width, max_height, clahe_padded_path):
clahe_padded_dapi_paths.append(clahe_padded_path)
print(f"Processed DAPI: {basename}")
except Exception as e:
print(f"Error processing {img_path}: {str(e)}")
# Process all channel images (only padded, no CLAHE) with existence check
padded_all_channel_paths = []
for img_path in all_channel_imgs:
try:
basename = os.path.basename(img_path)
padded_path = os.path.join(padded_all_channels_dir, f"padded_{basename}")
# Check if padded image already exists
if os.path.exists(padded_path):
print(f"✅ Reusing existing padded channel: {basename}")
padded_all_channel_paths.append(padded_path)
continue # Skip processing if exists
# Process new image
img = pyvips.Image.new_from_file(img_path)
padded_img = pad_image(img, max_width, max_height)
if img.format == 'ushort':
padded_img.write_to_file(padded_path, compression="none", tile=False)
else:
padded_img.write_to_file(padded_path)
padded_all_channel_paths.append(padded_path)
print(f"Padded channel: {basename}")
except Exception as e:
print(f"Error padding {img_path}: {str(e)}")
# STEP 3: Perform registration using CLAHE-PADDED DAPI images
print("Performing registration with CLAHE-padded DAPI images...")
# NEW: Configure feature detector and matcher for the latest Valis version
# Using DISK feature detector with grayscale images (more stable for DAPI)
fd = feature_detectors.DiskFD(rgb=False, num_features=2000, device='cpu')
matcher = feature_matcher.LightGlueMatcher(fd)
registrar = registration.Valis(
clahe_padded_dir, # Directory with CLAHE-padded DAPI images
dst_dir,
reference_img_f=clahe_padded_reference,
align_to_reference=True,
micro_rigid_registrar_cls=MicroRigidRegistrar,
max_processed_image_dim_px=2000,
max_non_rigid_registration_dim_px=1500,
norm_method='img_stats',
matcher=matcher # Device is already configured in the matcher
)
rigid_registrar, non_rigid_registrar, error_df = registrar.register()
# Perform micro-registration
print("Performing micro-registration...")
micro_reg, micro_error = registrar.register_micro(
max_non_rigid_registration_dim_px=2000,
align_to_reference=True
)
# STEP 4: Apply registration transforms to original padded images
print("Applying registration transforms to original intensity padded images...")
# Create mapping between CLAHE-padded slides and original padded slides
clahe_to_original_mapping = {}
for slide_name, slide_obj in registrar.slide_dict.items():
# Find corresponding original padded image
clahe_basename = os.path.basename(slide_obj.src_f).replace("clahe_padded_", "padded_")
original_path = os.path.join(padded_dapi_dir, clahe_basename)
clahe_to_original_mapping[slide_obj] = original_path
# Warp original padded DAPI images
dapi_warped_dir = os.path.join(individual_channel_dir, "dapi_warped")
os.makedirs(dapi_warped_dir, exist_ok=True)
for slide_obj, original_dapi_path in clahe_to_original_mapping.items():
try:
# Load original padded DAPI (original intensities)
original_dapi_img = pyvips.Image.new_from_file(original_dapi_path)
orig_width, orig_height = original_dapi_img.width, original_dapi_img.height
print(f"Warping {os.path.basename(original_dapi_path)}, format={original_dapi_img.format}, dimensions={orig_width}x{orig_height}")
# Apply transformation found using CLAHE-padded DAPI
warped_dapi = slide_obj.warp_img(img=original_dapi_img, non_rigid=True, crop=False)
# CRITICAL CHECK: Verify dimensions have correct orientation
current_width, current_height = warped_dapi.width, warped_dapi.height
print(f" Raw warped dimensions: {current_width}x{current_height}")
# Check if width/height are transposed (width should be larger than height for landscape images)
orig_orientation = "landscape" if orig_width > orig_height else "portrait"
warped_orientation = "landscape" if current_width > current_height else "portrait"
# Calculate aspect ratio match
orig_aspect = orig_width / orig_height
warped_aspect = current_width / current_height
aspect_ratio_diff = abs(warped_aspect - orig_aspect)
aspect_ratio_diff_transposed = abs(warped_aspect - (1/orig_aspect))
# Determine if transposition is needed
needs_transposition = False
if orig_orientation == warped_orientation:
# Same orientation, but check if aspect ratios match better when transposed
if aspect_ratio_diff_transposed < aspect_ratio_diff * 0.5:
needs_transposition = True
print(f" ⚠️ Aspect ratio mismatch suggests transposition needed")
else:
# Different orientation - likely needs transposition
needs_transposition = True
print(f" ⚠️ Orientation mismatch: original={orig_orientation}, warped={warped_orientation}")
# Apply transposition if needed
if needs_transposition:
print(f" 💡 Transposing image to fix orientation...")
warped_dapi = warped_dapi.transpose(pyvips.enums.Orientation.ROT90)
current_width, current_height = warped_dapi.width, warped_dapi.height
print(f" ✅ Fixed dimensions: {current_width}x{current_height}")
basename = os.path.basename(original_dapi_path)
warped_path = os.path.join(dapi_warped_dir, f"warped_{basename}")
# Save with proper format preservation for 16-bit images
if original_dapi_img.format == 'ushort':
warped_dapi.write_to_file(warped_path, compression="none", tile=False)
else:
warped_dapi.write_to_file(warped_path)
print(f"Warped original DAPI: {warped_path}, dimensions={current_width}x{current_height}")
except Exception as e:
print(f"Error warping original DAPI {slide_obj.name}: {str(e)}")
# Warp all other channels (ch01, ch02) with original intensities
print("Warping all other channels with original intensities...")
for slide_obj, round_slides in round_dict.items():
print(f"\nWarping channels for round: {slide_obj.name}")
valtils.sort_nicely(round_slides)
for src_f in round_slides:
img_name = os.path.basename(src_f)
print(f"Warping channel: {img_name}")
try:
# Load original intensity padded channel
channel_img = pyvips.Image.new_from_file(src_f)
orig_width, orig_height = channel_img.width, channel_img.height
print(f" Original channel dimensions: {orig_width}x{orig_height}, format={channel_img.format}, bands={channel_img.bands}")
# Apply transformation found using CLAHE-padded DAPI
warped_channel = slide_obj.warp_img(img=channel_img, non_rigid=True, crop=False)
# CRITICAL CHECK: Verify dimensions have correct orientation
current_width, current_height = warped_channel.width, warped_channel.height
print(f" Raw warped dimensions: {current_width}x{current_height}")
# Check if width/height are transposed
orig_orientation = "landscape" if orig_width > orig_height else "portrait"
warped_orientation = "landscape" if current_width > current_height else "portrait"
# Calculate aspect ratio match
orig_aspect = orig_width / orig_height
warped_aspect = current_width / current_height
aspect_ratio_diff = abs(warped_aspect - orig_aspect)
aspect_ratio_diff_transposed = abs(warped_aspect - (1/orig_aspect))
# Determine if transposition is needed
needs_transposition = False
if orig_orientation == warped_orientation:
if aspect_ratio_diff_transposed < aspect_ratio_diff * 0.5:
needs_transposition = True
print(f" ⚠️ Aspect ratio mismatch suggests transposition needed")
else:
needs_transposition = True
print(f" ⚠️ Orientation mismatch: original={orig_orientation}, warped={warped_orientation}")
# Apply transposition if needed
if needs_transposition:
print(f" 💡 Transposing image to fix orientation...")
warped_channel = warped_channel.transpose(pyvips.enums.Orientation.ROT90)
current_width, current_height = warped_channel.width, warped_channel.height
print(f" ✅ Fixed dimensions: {current_width}x{current_height}")
channel_basename = os.path.basename(src_f).replace("padded_", "")
individual_channel_path = os.path.join(individual_channel_dir, f"warped_{channel_basename}")
# Save with proper format preservation for 16-bit images
if channel_img.format == 'ushort':
warped_channel.write_to_file(individual_channel_path, compression="none", tile=False)
else:
warped_channel.write_to_file(individual_channel_path)
print(f"Saved warped channel: {individual_channel_path}, dimensions={current_width}x{current_height}")
except Exception as e:
print(f"Error processing {src_f}: {str(e)}")
# # STEP 5: Resize warped images to match padded reference dimensions
# print("Resizing warped images to match padded reference dimensions...")
# resize_warped_images(individual_channel_dir, padded_reference_slide)
# # STEP 6: Fix any remaining aspect ratio issues
# fix_warped_images(individual_channel_dir, padded_reference_slide)
print("\n" + "="*60)
print("REGISTRATION COMPLETE!")
print("="*60)
print(f"✅ CLAHE applied ONLY to padded DAPI (ch00) for registration")
print(f"✅ All channels warped with ORIGINAL intensities preserved")
print(f"✅ 16-bit image format preserved throughout processing")
print(f"✅ Warped images resized to match padded reference dimensions ({max_width} x {max_height})")
print(f"✅ Warped images fixed to maintain correct aspect ratio")
print(f"📁 Original padded images: {padded_dir}")
print(f"📁 Final warped images: {individual_channel_dir}")
print(f"📁 Backup of original warped images: {os.path.join(dst_dir, 'resize_backup')}")
# Clean up CLAHE-padded directory
print("\nCleaning up temporary CLAHE-padded files...")
shutil.rmtree(clahe_padded_dir)
print("✅ Cleaned up CLAHE-padded temporary files")
# Kill JVM
registration.kill_jvm()
if name == "main":
main()
ERROR TRACE :
Performing registration with CLAHE-padded DAPI images...
Loaded LightGlue model
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: Can't find slide file associated with masks
warnings.warn(warning_msg, warning_type)
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: Can't find slide file associated with processed
warnings.warn(warning_msg, warning_type)
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: max_image_dim_px is 1024 but needs to be less or equal to 2000. Setting max_image_dim_px to 2000
warnings.warn(warning_msg, warning_type)
==== Converting images
Converting images: 100%|██████████| 11/11 [04:22<00:00, 23.87s/image]
==== Processing images
Processing images : 100%|██████████| 11/11 [04:26<00:00, 24.26s/image]
Normalizing images: 100%|██████████| 11/11 [00:04<00:00, 2.24image/s]
==== Rigid registration
Detecting features : 0%| | 0/11 [00:00<?, ?image/s]
detecting features in level 0 with image shape (1019, 2000)
Detecting features : 9%|▉ | 1/11 [00:03<00:38, 3.82s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 18%|█▊ | 2/11 [00:08<00:38, 4.31s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 27%|██▋ | 3/11 [00:11<00:30, 3.84s/image]
detecting features in level 0 with image shape (1019, 2000)
Detecting features : 36%|███▋ | 4/11 [00:15<00:27, 3.90s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 45%|████▌ | 5/11 [00:19<00:22, 3.75s/image]
detecting features in level 0 with image shape (1018, 2000)
Detecting features : 55%|█████▍ | 6/11 [00:23<00:19, 3.88s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 64%|██████▎ | 7/11 [00:27<00:15, 3.82s/image]
detecting features in level 0 with image shape (878, 2000)
Detecting features : 73%|███████▎ | 8/11 [00:30<00:10, 3.64s/image]
detecting features in level 0 with image shape (1153, 2000)
Detecting features : 82%|████████▏ | 9/11 [00:34<00:07, 3.94s/image]
detecting features in level 0 with image shape (877, 1999)
Detecting features : 91%|█████████ | 10/11 [00:39<00:04, 4.00s/image]
detecting features in level 0 with image shape (1018, 1999)
Detecting features : 100%|██████████| 11/11 [00:41<00:00, 3.80s/image]
QUEUEING TASKS | Matching images : 100%|██████████| 11/11 [00:00<00:00, 211.23image/s]
PROCESSING TASKS | Matching images : 100%|██████████| 11/11 [00:51<00:00, 4.64s/image]
COLLECTING RESULTS | Matching images : 100%|██████████| 11/11 [00:00<00:00, 37755.60image/s]
Images sorted using VggFD features. Will now use LightGlueMatcher to match images using DiskFD features
detecting features in level 0 with image shape (1153, 2000)
Re-matching images: 0%| | 0/10 [00:00<?, ?image/s]
detecting features in level 0 with image shape (1256, 2146)
Re-matching images: 0%| | 0/10 [00:03<?, ?image/s]
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
warnings.warn(warning_msg, warning_type)
Traceback (most recent call last):
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py", line 4736, in register
rigid_registrar = self.rigid_register()
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py", line 3401, in rigid_register
rigid_registrar = serial_rigid.register_images(img_dir=self.processed_dir,
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/serial_rigid.py", line 1825, in register_images
registrar.rematch(matcher_obj=matcher, valis_obj=valis_obj, keep_unfiltered=False)
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/serial_rigid.py", line 901, in rematch
matcher_obj.match_images(img1=prev_img, img2=rotated_img,
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/feature_matcher.py", line 1458, in match_images
match_distances = match_distances.detach().numpy()
TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
Performing micro-registration...
TypeError Traceback (most recent call last)
Cell In[7], line 698
695 registration.kill_jvm()
697 if name == "main":
--> 698 main()
Cell In[7], line 529
527 # Perform micro-registration
528 print("Performing micro-registration...")
--> 529 micro_reg, micro_error = registrar.register_micro(
530 max_non_rigid_registration_dim_px=2000,
531 align_to_reference=True
532 )
534 # STEP 4: Apply registration transforms to original padded images
535 print("Applying registration transforms to original intensity padded images...")
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:40, in deprecated_args..deco..wrapper(*args, **kwargs)
37 @functools.wraps(f)
38 def wrapper(*args, **kwargs):
39 rename_kwargs(f.name, kwargs, aliases)
---> 40 return f(*args, **kwargs)
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py:4901, in Valis.register_micro(self, brightfield_processing_cls, brightfield_processing_kwargs, if_processing_cls, if_processing_kwargs, processor_dict, max_non_rigid_registration_dim_px, non_rigid_registrar_cls, non_rigid_reg_params, reference_img_f, align_to_reference, mask, tile_wh)
4897 non_rigid_registrar_obj = non_rigid_registrar_cls
4899 reg_in_rgb = non_rigid_registrar_obj.rgb
4900 nr_reg_src, max_img_dim, non_rigid_reg_mask, full_out_shape_rc, mask_bbox_xywh, using_tiler =
-> 4901 self.prep_images_for_large_non_rigid_registration(max_img_dim=max_non_rigid_registration_dim_px,
4902 processor_dict=slide_processors,
4903 updating_non_rigid=True,
4904 mask=mask,
4905 rgb=reg_in_rgb)
4907 img_specific_args = None
4908 write_dxdy = isinstance(ref_slide.bk_dxdy, pyvips.Image)
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py:3892, in Valis.prep_images_for_large_non_rigid_registration(self, max_img_dim, processor_dict, updating_non_rigid, mask, rgb)
3889 s = np.min(max_img_dim/np.array(to_reg_mask_wh))
3891 if s < max_s:
-> 3892 full_out_shape = self.get_aligned_slide_shape(s)
3893 else:
3894 full_out_shape = self.get_aligned_slide_shape(0)
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py:5095, in Valis.get_aligned_slide_shape(self, level)
5092 else:
5093 s_rc = level
-> 5095 aligned_out_shape_rc = np.ceil(np.array(ref_slide.reg_img_shape_rc)*s_rc).astype(int)
5097 return aligned_out_shape_rc
TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'
Hi,
I updated to the last Valis repo yesterday, was code was working well before BUT now it doesn't :/
Here is my code and the error : (Also for whatever reason at the error measurement step the dimensions are swapped ! so I added some code to verify and swap it back (no idea why this occurs , BUT if you can have a look that will be great !)
Could you please have a look and fix it , I really need Valis to align images :D
THanks
antho
Code :
VALIS last update :
RUNNING 1:44 8/18/2025
import os
import pathlib
import numpy as np
from pathlib import Path
from tqdm import tqdm
import pyvips
import cv2
from skimage import exposure
from valis import valtils, slide_io, registration
from valis import feature_detectors, feature_matcher # Import the new feature modules
from valis.micro_rigid_registrar import MicroRigidRegistrar
import shutil
import re
Add missing imports
try:
from tqdm.notebook import tqdm
except ImportError:
from tqdm import tqdm
print("tqdm.notebook not found. Using tqdm instead.")
def organize_channels_automatically(base_dir, target_dapi_dir=None, target_all_channels_dir=None):
"""
Automatically organize channel files from a base directory
def get_round_name(src_f):
"""Extract the round name from the filename"""
img_name = valtils.get_name(src_f)
round_name = img_name.lower().split("_merged")[0]
return round_name
def get_channel_number(src_f):
"""Extract the channel number from the filename"""
img_name = os.path.basename(src_f).lower()
if "_ch00" in img_name:
return 0
elif "_ch01" in img_name:
return 1
elif "_ch02" in img_name:
return 2
else:
return None
def pad_image(img, target_width, target_height):
"""Pad image to target dimensions, centering the original content"""
pad_width = max(0, target_width - img.width)
pad_height = max(0, target_height - img.height)
def apply_clahe_normalization(img_array, clip_limit=2.0, tile_grid_size=(16,16)):
"""Apply CLAHE normalization to improve contrast for registration"""
if img_array.dtype != np.uint8:
# Convert to uint8 for CLAHE
img_normalized = exposure.rescale_intensity(img_array, out_range=(0, 255)).astype(np.uint8)
else:
img_normalized = img_array.copy()
def create_clahe_padded_image(img_path, target_width, target_height, output_path, clip_limit=2.0, tile_grid_size=(16, 16)):
"""Pad image first, then apply CLAHE to the padded result"""
try:
# Load original image
img = pyvips.Image.new_from_file(img_path)
def resize_warped_images(warped_dir, padded_reference_path):
"""
Resize warped images to match padded reference dimensions.
def fix_warped_images(individual_channel_dir, reference_slide_path):
"""
Fix warped images that appear squished by ensuring they maintain the correct aspect ratio
relative to the reference image.
def main():
# Set up base directory
base_dir = "####your directory /##"
if name == "main":
main()
ERROR TRACE :
Performing registration with CLAHE-padded DAPI images...
Loaded LightGlue model
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: Can't find slide file associated with masks
warnings.warn(warning_msg, warning_type)
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: Can't find slide file associated with processed
warnings.warn(warning_msg, warning_type)
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: max_image_dim_px is 1024 but needs to be less or equal to 2000. Setting max_image_dim_px to 2000
warnings.warn(warning_msg, warning_type)
==== Converting images
Converting images: 100%|██████████| 11/11 [04:22<00:00, 23.87s/image]
==== Processing images
Processing images : 100%|██████████| 11/11 [04:26<00:00, 24.26s/image]
Normalizing images: 100%|██████████| 11/11 [00:04<00:00, 2.24image/s]
==== Rigid registration
Detecting features : 0%| | 0/11 [00:00<?, ?image/s]
detecting features in level 0 with image shape (1019, 2000)
Detecting features : 9%|▉ | 1/11 [00:03<00:38, 3.82s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 18%|█▊ | 2/11 [00:08<00:38, 4.31s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 27%|██▋ | 3/11 [00:11<00:30, 3.84s/image]
detecting features in level 0 with image shape (1019, 2000)
Detecting features : 36%|███▋ | 4/11 [00:15<00:27, 3.90s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 45%|████▌ | 5/11 [00:19<00:22, 3.75s/image]
detecting features in level 0 with image shape (1018, 2000)
Detecting features : 55%|█████▍ | 6/11 [00:23<00:19, 3.88s/image]
detecting features in level 0 with image shape (1081, 2000)
Detecting features : 64%|██████▎ | 7/11 [00:27<00:15, 3.82s/image]
detecting features in level 0 with image shape (878, 2000)
Detecting features : 73%|███████▎ | 8/11 [00:30<00:10, 3.64s/image]
detecting features in level 0 with image shape (1153, 2000)
Detecting features : 82%|████████▏ | 9/11 [00:34<00:07, 3.94s/image]
detecting features in level 0 with image shape (877, 1999)
Detecting features : 91%|█████████ | 10/11 [00:39<00:04, 4.00s/image]
detecting features in level 0 with image shape (1018, 1999)
Detecting features : 100%|██████████| 11/11 [00:41<00:00, 3.80s/image]
QUEUEING TASKS | Matching images : 100%|██████████| 11/11 [00:00<00:00, 211.23image/s]
PROCESSING TASKS | Matching images : 100%|██████████| 11/11 [00:51<00:00, 4.64s/image]
COLLECTING RESULTS | Matching images : 100%|██████████| 11/11 [00:00<00:00, 37755.60image/s]
Images sorted using VggFD features. Will now use LightGlueMatcher to match images using DiskFD features
detecting features in level 0 with image shape (1153, 2000)
Re-matching images: 0%| | 0/10 [00:00<?, ?image/s]
detecting features in level 0 with image shape (1256, 2146)
Re-matching images: 0%| | 0/10 [00:03<?, ?image/s]
/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:29: UserWarning: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
warnings.warn(warning_msg, warning_type)
Traceback (most recent call last):
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py", line 4736, in register
rigid_registrar = self.rigid_register()
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py", line 3401, in rigid_register
rigid_registrar = serial_rigid.register_images(img_dir=self.processed_dir,
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/serial_rigid.py", line 1825, in register_images
registrar.rematch(matcher_obj=matcher, valis_obj=valis_obj, keep_unfiltered=False)
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/serial_rigid.py", line 901, in rematch
matcher_obj.match_images(img1=prev_img, img2=rotated_img,
File "/home/m197816/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/feature_matcher.py", line 1458, in match_images
match_distances = match_distances.detach().numpy()
TypeError: can't convert cuda:0 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
Performing micro-registration...
TypeError Traceback (most recent call last)
Cell In[7], line 698
695 registration.kill_jvm()
697 if name == "main":
--> 698 main()
Cell In[7], line 529
527 # Perform micro-registration
528 print("Performing micro-registration...")
--> 529 micro_reg, micro_error = registrar.register_micro(
530 max_non_rigid_registration_dim_px=2000,
531 align_to_reference=True
532 )
534 # STEP 4: Apply registration transforms to original padded images
535 print("Applying registration transforms to original intensity padded images...")
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/valtils.py:40, in deprecated_args..deco..wrapper(*args, **kwargs)
37 @functools.wraps(f)
38 def wrapper(*args, **kwargs):
39 rename_kwargs(f.name, kwargs, aliases)
---> 40 return f(*args, **kwargs)
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py:4901, in Valis.register_micro(self, brightfield_processing_cls, brightfield_processing_kwargs, if_processing_cls, if_processing_kwargs, processor_dict, max_non_rigid_registration_dim_px, non_rigid_registrar_cls, non_rigid_reg_params, reference_img_f, align_to_reference, mask, tile_wh)
4897 non_rigid_registrar_obj = non_rigid_registrar_cls
4899 reg_in_rgb = non_rigid_registrar_obj.rgb
4900 nr_reg_src, max_img_dim, non_rigid_reg_mask, full_out_shape_rc, mask_bbox_xywh, using_tiler =
-> 4901 self.prep_images_for_large_non_rigid_registration(max_img_dim=max_non_rigid_registration_dim_px,
4902 processor_dict=slide_processors,
4903 updating_non_rigid=True,
4904 mask=mask,
4905 rgb=reg_in_rgb)
4907 img_specific_args = None
4908 write_dxdy = isinstance(ref_slide.bk_dxdy, pyvips.Image)
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py:3892, in Valis.prep_images_for_large_non_rigid_registration(self, max_img_dim, processor_dict, updating_non_rigid, mask, rgb)
3889 s = np.min(max_img_dim/np.array(to_reg_mask_wh))
3891 if s < max_s:
-> 3892 full_out_shape = self.get_aligned_slide_shape(s)
3893 else:
3894 full_out_shape = self.get_aligned_slide_shape(0)
File ~/anaconda3/envs/VALIS2/lib/python3.10/site-packages/valis/registration.py:5095, in Valis.get_aligned_slide_shape(self, level)
5092 else:
5093 s_rc = level
-> 5095 aligned_out_shape_rc = np.ceil(np.array(ref_slide.reg_img_shape_rc)*s_rc).astype(int)
5097 return aligned_out_shape_rc
TypeError: unsupported operand type(s) for *: 'NoneType' and 'float'