Skip to content

Instantly share code, notes, and snippets.

@greg-randall
Created March 9, 2025 00:43
Show Gist options
  • Save greg-randall/304ef7ff6ac739bae6d777636821db16 to your computer and use it in GitHub Desktop.
Save greg-randall/304ef7ff6ac739bae6d777636821db16 to your computer and use it in GitHub Desktop.
A Python utility that combines vertical image strips into a seamlessish panorama with smooth blending between overlapping regions.
"""
# Panorama Strip Combiner
A Python utility that combines vertical image strips into a seamless panorama with smooth blending between overlapping regions.
## What It Does
This script takes a directory of vertical image strips and combines them into a single panoramic image. It features:
- Automatic ordering of strips based on filename
- Configurable overlap between strips
- Smooth feathered blending at the seams between strips
- Optional horizontal flipping of strips
- Ability to limit the number of strips used (for testing)
## Usage
Basic usage:
```bash
python combine.py
```
This will use the default settings and process images in the `frames-try1-stripes` directory.
### Command Line Options
```bash
python combine.py --strips_dir=YOUR_DIR --max_images=100 --flip --feather=15
```
- `--strips_dir`: Directory containing the strip images (default: 'frames-try1-stripes')
- `--max_images`: Maximum number of strips to process (default: 100, use 0 for all images)
- `--flip`: Flip all strips horizontally (useful for correcting mirrored scans)
- `--feather`: Width of the feathering effect in pixels (default: 15)
## Output
The script automatically generates an output filename with a timestamp:
`../[timestamp]_panorama_blended.tif`
## How It Works
1. The script loads all .tif files from the specified directory
2. It calculates the dimensions of the final panorama based on strip size and overlap
3. It creates a blank canvas for the panorama
4. For each strip, it:
- Applies horizontal flipping if requested
- Creates a gradient mask for the overlapping region
- Blends the new strip with the existing panorama using the mask
- Pastes the result onto the panorama
5. The final panorama is saved as a TIFF file
"""
#!/usr/bin/env python3
import os
import glob
import time
import numpy as np
from PIL import Image, ImageOps
import argparse
from tqdm import tqdm
def create_panorama(strips_dir, output_file, max_images=0, flip_horizontal=True, feather_width=20):
"""Create a panorama from vertical strips with improved blending."""
print(f"Creating panorama from strips in {strips_dir}")
print(f"Output will be saved as: {output_file}")
# Get the list of TIF files
strip_files = sorted(glob.glob(os.path.join(strips_dir, "*.tif")))
# Limit the number of images if specified
if max_images > 0 and max_images < len(strip_files):
strip_files = strip_files[:max_images]
print(f"SAMPLE MODE: Processing only the first {max_images} images")
total_strips = len(strip_files)
print(f"Processing {total_strips} strips")
if flip_horizontal:
print("FLIP MODE: Flipping all strips horizontally")
# Load the first strip to get dimensions
first_strip = Image.open(strip_files[0])
strip_width, strip_height = first_strip.size
print(f"Strip dimensions: {strip_width}x{strip_height}")
# Calculate overlap width between strips
overlap = feather_width * 2
# Calculate the final panorama width (accounting for overlap)
final_width = strip_width + (total_strips - 1) * (strip_width - overlap)
print(f"Creating panorama of size {final_width}x{strip_height}")
# Create a blank canvas for the panorama (use RGB mode with white background)
panorama = Image.new('RGB', (final_width, strip_height), (128, 128, 128))
# Process each strip
current_x = 0
for i, strip_file in enumerate(tqdm(strip_files, desc="Building panorama")):
# Load the strip
strip = Image.open(strip_file).convert('RGB')
# Flip if requested
if flip_horizontal:
strip = ImageOps.mirror(strip)
# If this is the first strip, just paste it directly
if i == 0:
panorama.paste(strip, (0, 0))
current_x = strip_width - overlap
continue
# For subsequent strips, we'll blend the overlap area
overlap_region = current_x
# Create masks for blending the overlap area
mask = np.zeros((strip_height, strip_width), dtype=np.float32)
# Create a linear gradient in the overlap area (left part of the strip)
for x in range(overlap):
# Weight increases from 0.0 to 1.0
weight = x / overlap
mask[:, x] = weight
# Fill the rest of the mask with 1.0
mask[:, overlap:] = 1.0
# Convert mask to PIL Image
alpha_mask = Image.fromarray((mask * 255).astype(np.uint8))
# Extract the region of the panorama that will overlap with the new strip
overlap_panorama = panorama.crop((overlap_region, 0, overlap_region + strip_width, strip_height))
# Create a composite image by blending the new strip with the overlapping region
composite = Image.composite(strip, overlap_panorama, alpha_mask)
# Paste the blended result back onto the panorama
panorama.paste(composite, (overlap_region, 0))
# Move to the next position
current_x += strip_width - overlap
# Save the final panorama
panorama.save(output_file, compression="tiff_deflate")
print(f"Panorama saved to {output_file}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Create a panorama from vertical strips.')
parser.add_argument('--strips_dir', default='frames-try1-stripes', help='Directory containing the strip images')
parser.add_argument('--max_images', type=int, default=100, help='Maximum number of images to process (0 for all)')
parser.add_argument('--flip', action='store_true', help='Flip strips horizontally')
parser.add_argument('--feather', type=int, default=15, help='Width of feathering effect in pixels')
args = parser.parse_args()
# Generate output filename with timestamp
timestamp = int(time.time())
output_file = f"../{timestamp}_panorama_blended.tif"
create_panorama(
args.strips_dir,
output_file,
max_images=args.max_images,
flip_horizontal=args.flip,
feather_width=args.feather
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment