Created
March 9, 2025 00:43
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
# 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