Spaces:
Running
Running
feat: add experimental fixed base image mechanism
Browse filesIntroduces a `fixed_base_image_index_input` option. When an index is specified, that image acts as the bottom-most layer (background), allowing other images to overlay it.
Note: This is an experimental feature and may not be fully effective in all stitching scenarios yet.
app.py
CHANGED
|
@@ -518,7 +518,8 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 518 |
max_blending_width=10000,
|
| 519 |
max_blending_height=10000,
|
| 520 |
blend_smooth_ksize=15,
|
| 521 |
-
num_blend_levels=4
|
|
|
|
| 522 |
):
|
| 523 |
"""
|
| 524 |
Stitches a new image (img_new) onto an existing composite image (img_composite)
|
|
@@ -879,14 +880,14 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 879 |
# --- Create Masks for Blending ---
|
| 880 |
# Create mask for the warped image 1 using Alpha Channel
|
| 881 |
if warped_img1_u8 is not None:
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
else:
|
| 889 |
-
|
| 890 |
|
| 891 |
# Find overlapping region mask (uint8 0 or 255)
|
| 892 |
overlap_mask = cv2.bitwise_and(mask_warped, mask_img2)
|
|
@@ -935,8 +936,14 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 935 |
mean2 = np.sum(img2_roi[overlap_mask_gain > 0]) / overlap_pixel_count if img2_roi is not None else 0
|
| 936 |
|
| 937 |
if mean1 > 1e-5 and mean2 > 1e-5:
|
| 938 |
-
|
| 939 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
gain = np.clip(gain, 0.5, 2.0) # Clamp gain
|
| 941 |
log_message = log_and_print(f"Clamped Gain: {gain:.2f}\n", log_message)
|
| 942 |
else:
|
|
@@ -951,16 +958,26 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 951 |
|
| 952 |
# Apply gain ONLY if calculated and different from 1.0
|
| 953 |
if abs(gain - 1.0) > 1e-5: # Check float difference
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
|
|
|
| 964 |
del gain_applied_float, temp_gain_applied
|
| 965 |
gc.collect()
|
| 966 |
log_message = log_and_print(f"Gain applied to warped image (RGB channels).\n", log_message)
|
|
@@ -1027,6 +1044,14 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 1027 |
weights_overlap = d1_overlap / (total_dist + 1e-7) # Epsilon for stability
|
| 1028 |
weight1_norm[overlap_indices] = np.clip(weights_overlap, 0.0, 1.0)
|
| 1029 |
log_message = log_and_print(f"Calculated distance transform weights for {num_overlap_pixels} overlap pixels.\n", log_message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
else:
|
| 1031 |
log_message = log_and_print("Warning: No overlap pixels found for distance transform weight calculation.\n", log_message)
|
| 1032 |
|
|
@@ -1191,11 +1216,10 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 1191 |
# Create weight maps (float32)
|
| 1192 |
weight1 = np.zeros(output_img.shape[:2], dtype=np.float32)
|
| 1193 |
weight2 = np.zeros(output_img.shape[:2], dtype=np.float32)
|
| 1194 |
-
blend_axis = 0 if w_overlap >= h_overlap else 1
|
| 1195 |
-
overlap_region_mask = overlap_mask[y_overlap : y_overlap + h_overlap, x_overlap : x_overlap + w_overlap]
|
| 1196 |
|
| 1197 |
# Generate gradient for the overlap box
|
| 1198 |
gradient = None
|
|
|
|
| 1199 |
if blend_axis == 0: # Horizontal blend
|
| 1200 |
gradient = np.tile(np.linspace(1.0, 0.0, w_overlap, dtype=np.float32), (h_overlap, 1))
|
| 1201 |
else: # Vertical blend
|
|
@@ -1204,6 +1228,8 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 1204 |
weight1_region = gradient
|
| 1205 |
weight2_region = 1.0 - gradient
|
| 1206 |
|
|
|
|
|
|
|
| 1207 |
# Apply weights only where the overlap mask is valid within the bounding box
|
| 1208 |
valid_overlap = overlap_region_mask > 0
|
| 1209 |
weight1[y_overlap : y_overlap + h_overlap, x_overlap : x_overlap + w_overlap][valid_overlap] = weight1_region[valid_overlap]
|
|
@@ -1353,7 +1379,17 @@ def stitch_pairwise_images(img_composite, img_new,
|
|
| 1353 |
log_message = log_and_print(f"Blending method '{effective_blend_method}' or overlap condition not met. Performing simple overlay.\n", log_message)
|
| 1354 |
|
| 1355 |
if gain_applied_warped_img1_u8 is not None: # Only copy if we have something to copy
|
| 1356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1357 |
|
| 1358 |
# --- Final Result Assignment ---
|
| 1359 |
final_output_img = output_img # Assign the final blended/overlaid image
|
|
@@ -1472,7 +1508,8 @@ def stitch_multiple_images(images, # List of NumPy images (BGR/BGRA, potentially
|
|
| 1472 |
max_blending_width=10000,
|
| 1473 |
max_blending_height=10000,
|
| 1474 |
blend_smooth_ksize=15,
|
| 1475 |
-
num_blend_levels=4
|
|
|
|
| 1476 |
):
|
| 1477 |
"""
|
| 1478 |
Stitches a list of images. Tries cv2.Stitcher first (unless 'DIRECT_PAIRWISE'),
|
|
@@ -1681,6 +1718,20 @@ def stitch_multiple_images(images, # List of NumPy images (BGR/BGRA, potentially
|
|
| 1681 |
else:
|
| 1682 |
next_image = images[i] # Can use directly if already uint8
|
| 1683 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1684 |
result, pairwise_log = stitch_pairwise_images(
|
| 1685 |
current_stitched_image, # BGR uint8
|
| 1686 |
next_image, # BGR uint8
|
|
@@ -1694,7 +1745,8 @@ def stitch_multiple_images(images, # List of NumPy images (BGR/BGRA, potentially
|
|
| 1694 |
max_blending_width=max_blending_width,
|
| 1695 |
max_blending_height=max_blending_height,
|
| 1696 |
blend_smooth_ksize=blend_smooth_ksize,
|
| 1697 |
-
num_blend_levels=num_blend_levels
|
|
|
|
| 1698 |
)
|
| 1699 |
log += pairwise_log
|
| 1700 |
|
|
@@ -1769,12 +1821,12 @@ def stitch_multiple_images(images, # List of NumPy images (BGR/BGRA, potentially
|
|
| 1769 |
return stitched_img_rgba, log
|
| 1770 |
except cv2.error as e_cvt:
|
| 1771 |
log = log_and_print(f"\nError converting final image: {e_cvt}. Returning None.\n", log)
|
| 1772 |
-
if '
|
| 1773 |
gc.collect()
|
| 1774 |
return None, log
|
| 1775 |
else:
|
| 1776 |
log = log_and_print("Error: Stitching failed. No final image generated.", log)
|
| 1777 |
-
if '
|
| 1778 |
del stitched_img_bgra
|
| 1779 |
gc.collect()
|
| 1780 |
return None, log
|
|
@@ -2051,7 +2103,7 @@ def stitch_video_frames(video_path,
|
|
| 2051 |
if last_saved_composite is not None:
|
| 2052 |
del last_saved_composite
|
| 2053 |
last_saved_composite = post_cropped_composite.copy() # Store BGR/BGRA
|
| 2054 |
-
log = log_and_print(f"Saved composite image {len(
|
| 2055 |
else:
|
| 2056 |
log = log_and_print("Skipping save: Result identical to previously saved image.\n", log)
|
| 2057 |
|
|
@@ -2113,9 +2165,9 @@ def stitch_video_frames(video_path,
|
|
| 2113 |
# Clean up potentially lingering frame data from the failed iteration
|
| 2114 |
if 'frame_bgr_raw' in locals() and frame_bgr_raw is not None:
|
| 2115 |
del frame_bgr_raw
|
| 2116 |
-
if '
|
| 2117 |
del frame_bgra
|
| 2118 |
-
if '
|
| 2119 |
del cropped_frame_bgra
|
| 2120 |
if 'current_frame_for_stitch' in locals() and current_frame_for_stitch is not None and current_frame_for_stitch is not anchor_frame:
|
| 2121 |
del current_frame_for_stitch
|
|
@@ -2145,7 +2197,11 @@ def stitch_video_frames(video_path,
|
|
| 2145 |
is_duplicate = True
|
| 2146 |
if not is_duplicate:
|
| 2147 |
append_result(post_cropped_final) # RGBA append
|
| 2148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2149 |
# No need to update last_saved_composite here, loop is finished
|
| 2150 |
else:
|
| 2151 |
log = log_and_print("Skipping save of final composite: Result identical to previously saved image.\n", log)
|
|
@@ -2174,15 +2230,15 @@ def stitch_video_frames(video_path,
|
|
| 2174 |
gc.collect()
|
| 2175 |
|
| 2176 |
total_end_time = time.time()
|
| 2177 |
-
log = log_and_print(f"\nVideo stitching process finished. Found {len(
|
| 2178 |
log = log_and_print(f"\nTotal processing time: {total_end_time - total_start_time:.2f} seconds.\n", log)
|
| 2179 |
|
| 2180 |
# Filter out potential None entries just before returning
|
| 2181 |
-
final_results = [img for img in
|
| 2182 |
-
if len(final_results) != len(
|
| 2183 |
-
log = log_and_print(f"Warning: Filtered out {len(
|
| 2184 |
# Clean up the original list with potential Nones
|
| 2185 |
-
del
|
| 2186 |
gc.collect()
|
| 2187 |
|
| 2188 |
return final_results, log
|
|
@@ -2200,6 +2256,7 @@ def run_stitching_interface(input_files,
|
|
| 2200 |
exposure_comp_type_str, # For cv2.Stitcher
|
| 2201 |
enable_cropping, # Post-stitch black border crop
|
| 2202 |
strict_no_black_edges_input,
|
|
|
|
| 2203 |
# Detailed Stitcher Settings
|
| 2204 |
transform_model_str,
|
| 2205 |
blend_method_str,
|
|
@@ -2230,10 +2287,11 @@ def run_stitching_interface(input_files,
|
|
| 2230 |
return [], "Please upload images or a video file."
|
| 2231 |
|
| 2232 |
# Convert Gradio inputs to correct types
|
| 2233 |
-
|
| 2234 |
-
num_blend_levels = int(num_blend_levels_input) if num_blend_levels_input is not None else 4
|
| 2235 |
ransac_reproj_thresh = float(ransac_reproj_thresh_input) if ransac_reproj_thresh_input is not None else 3.0
|
| 2236 |
max_distance_coeff = float(max_distance_coeff_input) if max_distance_coeff_input is not None else 0.5
|
|
|
|
|
|
|
| 2237 |
|
| 2238 |
log = f"Received {len(input_files)} file(s).\n"
|
| 2239 |
log = log_and_print(f"Pre-Crop Settings: Top={crop_top_percent}%, Bottom={crop_bottom_percent}%\n", log)
|
|
@@ -2457,7 +2515,7 @@ def run_stitching_interface(input_files,
|
|
| 2457 |
blend_method_lower = blend_method_str.lower() if blend_method_str else "multi-band"
|
| 2458 |
|
| 2459 |
# Call the modified stitch_multiple_images function
|
| 2460 |
-
|
| 2461 |
images_bgr_cropped, # Pass the list of cropped images (BGRA)
|
| 2462 |
stitcher_mode_str=stitcher_mode_str,
|
| 2463 |
registration_resol=registration_resol,
|
|
@@ -2477,11 +2535,12 @@ def run_stitching_interface(input_files,
|
|
| 2477 |
max_blending_width=max_blending_width,
|
| 2478 |
max_blending_height=max_blending_height,
|
| 2479 |
blend_smooth_ksize=blend_smooth_ksize,
|
| 2480 |
-
num_blend_levels=num_blend_levels
|
|
|
|
| 2481 |
)
|
| 2482 |
stitch_log += stitch_log_img # Append log from stitching function
|
| 2483 |
-
if
|
| 2484 |
-
final_stitched_images_rgba = [
|
| 2485 |
|
| 2486 |
# Clean up loaded images for list mode after stitching attempt
|
| 2487 |
if 'images_bgr_cropped' in locals():
|
|
@@ -2529,24 +2588,45 @@ def run_stitching_interface(input_files,
|
|
| 2529 |
temp_dir = tempfile.mkdtemp(prefix="stitch_run_", dir=target_temp_dir_base)
|
| 2530 |
final_log = log_and_print(f"\nInfo: Saving output images to temporary directory: {temp_dir}\n", final_log)
|
| 2531 |
|
| 2532 |
-
for i,
|
| 2533 |
-
if
|
| 2534 |
final_log = log_and_print(f"Warning: Skipping saving image index {i} because it is None or empty.\n", final_log)
|
| 2535 |
continue
|
| 2536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2537 |
# Use os.path.join for cross-platform compatibility
|
| 2538 |
full_path = os.path.join(temp_dir, filename)
|
| 2539 |
-
|
| 2540 |
try:
|
| 2541 |
# Handle RGBA to BGRA for saving
|
| 2542 |
# The result coming from stitcher is in RGB(A) format
|
| 2543 |
-
if
|
| 2544 |
-
|
| 2545 |
else:
|
| 2546 |
-
|
| 2547 |
|
| 2548 |
# Use imencode -> write pattern for better handling of paths/special chars
|
| 2549 |
-
is_success, buf = cv2.imencode('.png',
|
| 2550 |
if is_success:
|
| 2551 |
with open(full_path, 'wb') as f:
|
| 2552 |
f.write(buf)
|
|
@@ -2562,15 +2642,15 @@ def run_stitching_interface(input_files,
|
|
| 2562 |
except Exception as e_write:
|
| 2563 |
final_log = log_and_print(f"Unexpected error writing image {filename} to {full_path}: {e_write}\n", final_log)
|
| 2564 |
finally:
|
| 2565 |
-
if
|
| 2566 |
-
del
|
| 2567 |
gc.collect()
|
| 2568 |
except Exception as e_tempdir:
|
| 2569 |
final_log = log_and_print(f"Error creating temporary directory or saving output: {e_tempdir}\n", final_log)
|
| 2570 |
output_file_paths = [] # Fallback to empty list
|
| 2571 |
|
| 2572 |
# --- Final Cleanup of RGB images list ---
|
| 2573 |
-
if '
|
| 2574 |
for img_del in final_stitched_images_rgba:
|
| 2575 |
if img_del is not None:
|
| 2576 |
del img_del
|
|
@@ -2632,42 +2712,44 @@ with gr.Blocks() as demo:
|
|
| 2632 |
|
| 2633 |
# --- Detailed Stitcher Settings (Used for Video, DIRECT_PAIRWISE, and Fallback) ---
|
| 2634 |
with gr.Accordion("Pairwise Stitching Settings (Video / Direct / Fallback)", open=True):
|
|
|
|
|
|
|
| 2635 |
transform_model = gr.Radio(["Homography", "Affine_Partial", "Affine_Full"], label="Pairwise Transform Model", value="Homography", # Default to Homography
|
| 2636 |
-
|
| 2637 |
blend_method = gr.Radio(["Linear", "Multi-Band"], label="Blending Method", value="Multi-Band",
|
| 2638 |
-
|
| 2639 |
enable_gain_compensation = gr.Checkbox(value=True, label="Enable Gain Compensation",
|
| 2640 |
-
|
| 2641 |
orb_nfeatures = gr.Slider(500, 10000, step=100, value=2000, label="ORB Features",
|
| 2642 |
-
|
| 2643 |
match_ratio_thresh = gr.Slider(0.5, 0.95, step=0.01, value=0.75, label="Match Ratio Threshold",
|
| 2644 |
-
|
| 2645 |
ransac_reproj_thresh = gr.Slider(1.0, 10.0, step=0.1, value=5.0, label="RANSAC Reproj Threshold",
|
| 2646 |
-
|
| 2647 |
max_distance_coeff = gr.Slider(0.1, 2.0, step=0.05, value=0.5, label="Max Distance Coeff",
|
| 2648 |
-
|
| 2649 |
max_blending_width = gr.Number(value=10000, label="Max Blending Width", precision=0,
|
| 2650 |
-
|
| 2651 |
max_blending_height = gr.Number(value=10000, label="Max Blending Height", precision=0,
|
| 2652 |
-
|
| 2653 |
blend_smooth_ksize = gr.Number(value=15, label="Blend Smooth Kernel Size", precision=0,
|
| 2654 |
-
|
| 2655 |
num_blend_levels = gr.Slider(2, 7, step=1, value=4, label="Multi-Band Blend Levels",
|
| 2656 |
-
|
| 2657 |
|
| 2658 |
with gr.Accordion("Video Stitcher Settings", open=False):
|
| 2659 |
sample_interval_ms = gr.Number(value=3000, label="Sample Interval (ms)", precision=0,
|
| 2660 |
-
|
| 2661 |
max_composite_width_video = gr.Number(value=10000, label="Max Composite Width (Video)", precision=0,
|
| 2662 |
-
|
| 2663 |
max_composite_height_video = gr.Number(value=10000, label="Max Composite Height (Video)", precision=0,
|
| 2664 |
-
|
| 2665 |
|
| 2666 |
with gr.Accordion("Postprocessing Settings", open=False):
|
| 2667 |
enable_cropping = gr.Checkbox(value=True, label="Crop Black Borders (Post-Stitch)",
|
| 2668 |
-
|
| 2669 |
strict_no_black_edges_checkbox = gr.Checkbox(value=False, label="Strict No Black Edges (Post-Crop)",
|
| 2670 |
-
|
| 2671 |
|
| 2672 |
with gr.Column(scale=1):
|
| 2673 |
output_gallery = gr.Gallery(
|
|
@@ -2692,6 +2774,7 @@ with gr.Blocks() as demo:
|
|
| 2692 |
# Postprocessing
|
| 2693 |
enable_cropping,
|
| 2694 |
strict_no_black_edges_checkbox,
|
|
|
|
| 2695 |
# Detailed Stitcher Settings
|
| 2696 |
transform_model,
|
| 2697 |
blend_method,
|
|
@@ -2716,7 +2799,7 @@ with gr.Blocks() as demo:
|
|
| 2716 |
["examples/Wetter-Panorama/Wetter-Panorama1[NIuO6hrFTrg].mp4"],
|
| 2717 |
0, 20,
|
| 2718 |
"DIRECT_PAIRWISE", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2719 |
-
True, False,
|
| 2720 |
"Homography", "Multi-Band", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2721 |
2500, 10000, 10000,
|
| 2722 |
],
|
|
@@ -2724,7 +2807,7 @@ with gr.Blocks() as demo:
|
|
| 2724 |
["examples/Wetter-Panorama/Wetter-Panorama2[NIuO6hrFTrg].mp4"],
|
| 2725 |
0, 20,
|
| 2726 |
"DIRECT_PAIRWISE", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2727 |
-
True, False,
|
| 2728 |
"Homography", "Multi-Band", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2729 |
2500, 10000, 10000,
|
| 2730 |
],
|
|
@@ -2733,7 +2816,7 @@ with gr.Blocks() as demo:
|
|
| 2733 |
"examples/NieRAutomata/nier2B_06.jpg", "examples/NieRAutomata/nier2B_07.jpg", "examples/NieRAutomata/nier2B_08.jpg", "examples/NieRAutomata/nier2B_09.jpg", "examples/NieRAutomata/nier2B_10.jpg", ],
|
| 2734 |
0, 0,
|
| 2735 |
"PANORAMA", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2736 |
-
True, False,
|
| 2737 |
"Homography", "Multi-Band", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2738 |
5000, 10000, 10000,
|
| 2739 |
],
|
|
@@ -2741,7 +2824,7 @@ with gr.Blocks() as demo:
|
|
| 2741 |
["examples/cat/cat_left.jpg", "examples/cat/cat_right.jpg"],
|
| 2742 |
0, 0,
|
| 2743 |
"SCANS", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2744 |
-
True, False,
|
| 2745 |
"Affine_Partial", "Linear", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2746 |
5000, 10000, 10000,
|
| 2747 |
],
|
|
@@ -2749,7 +2832,7 @@ with gr.Blocks() as demo:
|
|
| 2749 |
["examples/ギルドの受付嬢ですが/Girumasu_1.jpg", "examples/ギルドの受付嬢ですが/Girumasu_2.jpg", "examples/ギルドの受付嬢ですが/Girumasu_3.jpg"],
|
| 2750 |
0, 0,
|
| 2751 |
"PANORAMA", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2752 |
-
True, False,
|
| 2753 |
"Affine_Partial", "Linear", True, 5000, 0.65, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2754 |
5000, 10000, 10000,
|
| 2755 |
],
|
|
@@ -2757,7 +2840,7 @@ with gr.Blocks() as demo:
|
|
| 2757 |
["examples/photographs1/img1.jpg", "examples/photographs1/img2.jpg", "examples/photographs1/img3.jpg", "examples/photographs1/img4.jpg"],
|
| 2758 |
0, 0,
|
| 2759 |
"PANORAMA", 0.6, 0.1, -1, True, "GAIN_BLOCKS",
|
| 2760 |
-
True, False,
|
| 2761 |
"Homography", "Linear", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2762 |
5000, 10000, 10000,
|
| 2763 |
]
|
|
|
|
| 518 |
max_blending_width=10000,
|
| 519 |
max_blending_height=10000,
|
| 520 |
blend_smooth_ksize=15,
|
| 521 |
+
num_blend_levels=4,
|
| 522 |
+
fixed_layer_type="none" # _type: "none", "img1", "img2"
|
| 523 |
):
|
| 524 |
"""
|
| 525 |
Stitches a new image (img_new) onto an existing composite image (img_composite)
|
|
|
|
| 880 |
# --- Create Masks for Blending ---
|
| 881 |
# Create mask for the warped image 1 using Alpha Channel
|
| 882 |
if warped_img1_u8 is not None:
|
| 883 |
+
# Check alpha channel (index 3)
|
| 884 |
+
mask_warped = (warped_img1_u8[:, :, 3] > 0).astype(np.uint8) * 255
|
| 885 |
+
# Erode the warped mask slightly to remove semi-transparent edge artifacts
|
| 886 |
+
# This removes the 1-pixel border caused by linear interpolation blending with transparency.
|
| 887 |
+
erosion_kernel = np.ones((3, 3), np.uint8)
|
| 888 |
+
mask_warped = cv2.erode(mask_warped, erosion_kernel, iterations=3)
|
| 889 |
else:
|
| 890 |
+
mask_warped = np.zeros(output_img.shape[:2], dtype=np.uint8) # Empty mask if warp failed
|
| 891 |
|
| 892 |
# Find overlapping region mask (uint8 0 or 255)
|
| 893 |
overlap_mask = cv2.bitwise_and(mask_warped, mask_img2)
|
|
|
|
| 936 |
mean2 = np.sum(img2_roi[overlap_mask_gain > 0]) / overlap_pixel_count if img2_roi is not None else 0
|
| 937 |
|
| 938 |
if mean1 > 1e-5 and mean2 > 1e-5:
|
| 939 |
+
if fixed_layer_type == "img1":
|
| 940 |
+
# If the composite (Img1) is the base image, do not touch Img1, adjust Img2 instead
|
| 941 |
+
# We need Img2's brightness to match Img1
|
| 942 |
+
gain = mean1 / mean2
|
| 943 |
+
log_message = log_and_print(f"Fixed Base (Img1): Adjusting Img2 brightness to match Base. Gain={gain:.2f}\n", log_message)
|
| 944 |
+
else:
|
| 945 |
+
gain = mean2 / mean1
|
| 946 |
+
log_message = log_and_print(f"Calculated Gain: {gain:.2f}\n", log_message)
|
| 947 |
gain = np.clip(gain, 0.5, 2.0) # Clamp gain
|
| 948 |
log_message = log_and_print(f"Clamped Gain: {gain:.2f}\n", log_message)
|
| 949 |
else:
|
|
|
|
| 958 |
|
| 959 |
# Apply gain ONLY if calculated and different from 1.0
|
| 960 |
if abs(gain - 1.0) > 1e-5: # Check float difference
|
| 961 |
+
if fixed_layer_type == "img1":
|
| 962 |
+
# Directly modify the Img2 region in output_img (RGB channels only)
|
| 963 |
+
# Note: output_img currently contains only Img2 pixels
|
| 964 |
+
gain_applied_float = output_img.astype(np.float32)
|
| 965 |
+
gain_applied_float[:, :, :3] *= gain
|
| 966 |
+
|
| 967 |
+
output_img = np.clip(gain_applied_float, 0, 255).astype(np.uint8)
|
| 968 |
+
gain_applied_warped_img1_u8 = warped_img1_u8
|
| 969 |
+
else:
|
| 970 |
+
gain_applied_float = warped_img1_u8.astype(np.float32)
|
| 971 |
+
# Apply gain to RGB channels (0,1,2), Leave Alpha (3) untouched
|
| 972 |
+
gain_applied_float[:, :, :3] *= gain
|
| 973 |
|
| 974 |
+
# *** Create new array for gain applied result ***
|
| 975 |
+
temp_gain_applied = gain_applied_float.clip(0, 255).astype(np.uint8)
|
| 976 |
+
# If gain_applied_warped_img1_u8 wasn't the original, delete it before reassigning
|
| 977 |
+
if gain_applied_warped_img1_u8 is not warped_img1_u8:
|
| 978 |
+
del gain_applied_warped_img1_u8
|
| 979 |
+
gain_applied_warped_img1_u8 = temp_gain_applied # Assign the new gain-applied image
|
| 980 |
+
|
| 981 |
del gain_applied_float, temp_gain_applied
|
| 982 |
gc.collect()
|
| 983 |
log_message = log_and_print(f"Gain applied to warped image (RGB channels).\n", log_message)
|
|
|
|
| 1044 |
weights_overlap = d1_overlap / (total_dist + 1e-7) # Epsilon for stability
|
| 1045 |
weight1_norm[overlap_indices] = np.clip(weights_overlap, 0.0, 1.0)
|
| 1046 |
log_message = log_and_print(f"Calculated distance transform weights for {num_overlap_pixels} overlap pixels.\n", log_message)
|
| 1047 |
+
# If a base image is specified, we force the overlap area to fully show the "top" layer (weight 0 or 1)
|
| 1048 |
+
# This solves the "semi-transparent/ghosting" issue
|
| 1049 |
+
if fixed_layer_type == "img2":
|
| 1050 |
+
# Img2 is Base, Img1 is Top -> Show Img1 (Weight 1.0)
|
| 1051 |
+
weight1_norm[overlap_indices] = 1.0
|
| 1052 |
+
elif fixed_layer_type == "img1":
|
| 1053 |
+
# Img1 is Base, Img2 is Top -> Show Img2 (Weight 0.0)
|
| 1054 |
+
weight1_norm[overlap_indices] = 0.0
|
| 1055 |
else:
|
| 1056 |
log_message = log_and_print("Warning: No overlap pixels found for distance transform weight calculation.\n", log_message)
|
| 1057 |
|
|
|
|
| 1216 |
# Create weight maps (float32)
|
| 1217 |
weight1 = np.zeros(output_img.shape[:2], dtype=np.float32)
|
| 1218 |
weight2 = np.zeros(output_img.shape[:2], dtype=np.float32)
|
|
|
|
|
|
|
| 1219 |
|
| 1220 |
# Generate gradient for the overlap box
|
| 1221 |
gradient = None
|
| 1222 |
+
blend_axis = 0 if w_overlap >= h_overlap else 1
|
| 1223 |
if blend_axis == 0: # Horizontal blend
|
| 1224 |
gradient = np.tile(np.linspace(1.0, 0.0, w_overlap, dtype=np.float32), (h_overlap, 1))
|
| 1225 |
else: # Vertical blend
|
|
|
|
| 1228 |
weight1_region = gradient
|
| 1229 |
weight2_region = 1.0 - gradient
|
| 1230 |
|
| 1231 |
+
overlap_region_mask = overlap_mask[y_overlap : y_overlap + h_overlap, x_overlap : x_overlap + w_overlap]
|
| 1232 |
+
|
| 1233 |
# Apply weights only where the overlap mask is valid within the bounding box
|
| 1234 |
valid_overlap = overlap_region_mask > 0
|
| 1235 |
weight1[y_overlap : y_overlap + h_overlap, x_overlap : x_overlap + w_overlap][valid_overlap] = weight1_region[valid_overlap]
|
|
|
|
| 1379 |
log_message = log_and_print(f"Blending method '{effective_blend_method}' or overlap condition not met. Performing simple overlay.\n", log_message)
|
| 1380 |
|
| 1381 |
if gain_applied_warped_img1_u8 is not None: # Only copy if we have something to copy
|
| 1382 |
+
# No blend or no overlap: Simple Overlay
|
| 1383 |
+
# Decide who covers whom based on Z-order
|
| 1384 |
+
# output_img already has Img2
|
| 1385 |
+
if fixed_layer_type == "img2":
|
| 1386 |
+
# Img2 is base, Img1 covers it
|
| 1387 |
+
output_img = cv2.copyTo(gain_applied_warped_img1_u8, mask_warped, output_img)
|
| 1388 |
+
else:
|
| 1389 |
+
# Img1 is base, Img2 covers it
|
| 1390 |
+
# output_img already has Img2, we just need to fill in where Img1 is not covered by Img2
|
| 1391 |
+
# Or place Img1 first, then Img2
|
| 1392 |
+
output_img = cv2.copyTo(output_img, mask_img2, gain_applied_warped_img1_u8)
|
| 1393 |
|
| 1394 |
# --- Final Result Assignment ---
|
| 1395 |
final_output_img = output_img # Assign the final blended/overlaid image
|
|
|
|
| 1508 |
max_blending_width=10000,
|
| 1509 |
max_blending_height=10000,
|
| 1510 |
blend_smooth_ksize=15,
|
| 1511 |
+
num_blend_levels=4,
|
| 1512 |
+
fixed_base_image_index=-1
|
| 1513 |
):
|
| 1514 |
"""
|
| 1515 |
Stitches a list of images. Tries cv2.Stitcher first (unless 'DIRECT_PAIRWISE'),
|
|
|
|
| 1718 |
else:
|
| 1719 |
next_image = images[i] # Can use directly if already uint8
|
| 1720 |
|
| 1721 |
+
# Determine Fixed Layer Type for this pair
|
| 1722 |
+
# fixed_layer_type logic:
|
| 1723 |
+
# - "img2": The new image currently being added (index i) is the specified base map.
|
| 1724 |
+
# - "img1": The current composite image (index < i) contains the specified base map.
|
| 1725 |
+
# - "none": No base map specified, perform normal blending.
|
| 1726 |
+
current_fixed_type = "none"
|
| 1727 |
+
if fixed_base_image_index != -1:
|
| 1728 |
+
if i == fixed_base_image_index:
|
| 1729 |
+
current_fixed_type = "img2" # The new image being added IS the fixed base
|
| 1730 |
+
elif fixed_base_image_index < i:
|
| 1731 |
+
current_fixed_type = "img1" # The composite (img1) contains the fixed base, so it should stay at bottom
|
| 1732 |
+
elif fixed_base_image_index == 0:
|
| 1733 |
+
current_fixed_type = "img1" # Special case: First image is base, so composite is always base
|
| 1734 |
+
|
| 1735 |
result, pairwise_log = stitch_pairwise_images(
|
| 1736 |
current_stitched_image, # BGR uint8
|
| 1737 |
next_image, # BGR uint8
|
|
|
|
| 1745 |
max_blending_width=max_blending_width,
|
| 1746 |
max_blending_height=max_blending_height,
|
| 1747 |
blend_smooth_ksize=blend_smooth_ksize,
|
| 1748 |
+
num_blend_levels=num_blend_levels,
|
| 1749 |
+
fixed_layer_type=current_fixed_type
|
| 1750 |
)
|
| 1751 |
log += pairwise_log
|
| 1752 |
|
|
|
|
| 1821 |
return stitched_img_rgba, log
|
| 1822 |
except cv2.error as e_cvt:
|
| 1823 |
log = log_and_print(f"\nError converting final image: {e_cvt}. Returning None.\n", log)
|
| 1824 |
+
if 'stitched_img_bgra' in locals(): del stitched_img_bgra
|
| 1825 |
gc.collect()
|
| 1826 |
return None, log
|
| 1827 |
else:
|
| 1828 |
log = log_and_print("Error: Stitching failed. No final image generated.", log)
|
| 1829 |
+
if 'stitched_img_bgra' in locals() and stitched_img_bgra is not None:
|
| 1830 |
del stitched_img_bgra
|
| 1831 |
gc.collect()
|
| 1832 |
return None, log
|
|
|
|
| 2103 |
if last_saved_composite is not None:
|
| 2104 |
del last_saved_composite
|
| 2105 |
last_saved_composite = post_cropped_composite.copy() # Store BGR/BGRA
|
| 2106 |
+
log = log_and_print(f"Saved composite image {len(stitched_results_rgba)} (Post-Cropped Shape: {post_cropped_composite.shape}).\n", log)
|
| 2107 |
else:
|
| 2108 |
log = log_and_print("Skipping save: Result identical to previously saved image.\n", log)
|
| 2109 |
|
|
|
|
| 2165 |
# Clean up potentially lingering frame data from the failed iteration
|
| 2166 |
if 'frame_bgr_raw' in locals() and frame_bgr_raw is not None:
|
| 2167 |
del frame_bgr_raw
|
| 2168 |
+
if 'frame_bgra' in locals() and frame_bgra is not None:
|
| 2169 |
del frame_bgra
|
| 2170 |
+
if 'cropped_frame_bgra' in locals() and cropped_frame_bgra is not None:
|
| 2171 |
del cropped_frame_bgra
|
| 2172 |
if 'current_frame_for_stitch' in locals() and current_frame_for_stitch is not None and current_frame_for_stitch is not anchor_frame:
|
| 2173 |
del current_frame_for_stitch
|
|
|
|
| 2197 |
is_duplicate = True
|
| 2198 |
if not is_duplicate:
|
| 2199 |
append_result(post_cropped_final) # RGBA append
|
| 2200 |
+
message = f"Saved final composite image {len(stitched_results_rgba)}"
|
| 2201 |
+
if 'post_cropped_composite' in locals():
|
| 2202 |
+
message += f" (Post-Cropped Shape: {post_cropped_composite.shape})"
|
| 2203 |
+
|
| 2204 |
+
log = log_and_print(f"{message}.\n", log)
|
| 2205 |
# No need to update last_saved_composite here, loop is finished
|
| 2206 |
else:
|
| 2207 |
log = log_and_print("Skipping save of final composite: Result identical to previously saved image.\n", log)
|
|
|
|
| 2230 |
gc.collect()
|
| 2231 |
|
| 2232 |
total_end_time = time.time()
|
| 2233 |
+
log = log_and_print(f"\nVideo stitching process finished. Found {len(stitched_results_rgba)} stitched image(s).", log)
|
| 2234 |
log = log_and_print(f"\nTotal processing time: {total_end_time - total_start_time:.2f} seconds.\n", log)
|
| 2235 |
|
| 2236 |
# Filter out potential None entries just before returning
|
| 2237 |
+
final_results = [img for img in stitched_results_rgba if img is not None and img.size > 0]
|
| 2238 |
+
if len(final_results) != len(stitched_results_rgba):
|
| 2239 |
+
log = log_and_print(f"Warning: Filtered out {len(stitched_results_rgba) - len(final_results)} None or empty results before final return.\n", log)
|
| 2240 |
# Clean up the original list with potential Nones
|
| 2241 |
+
del stitched_results_rgba
|
| 2242 |
gc.collect()
|
| 2243 |
|
| 2244 |
return final_results, log
|
|
|
|
| 2256 |
exposure_comp_type_str, # For cv2.Stitcher
|
| 2257 |
enable_cropping, # Post-stitch black border crop
|
| 2258 |
strict_no_black_edges_input,
|
| 2259 |
+
fixed_base_image_index_input,
|
| 2260 |
# Detailed Stitcher Settings
|
| 2261 |
transform_model_str,
|
| 2262 |
blend_method_str,
|
|
|
|
| 2287 |
return [], "Please upload images or a video file."
|
| 2288 |
|
| 2289 |
# Convert Gradio inputs to correct types
|
| 2290 |
+
fixed_base_image_index = int(fixed_base_image_index_input) if fixed_base_image_index_input is not None else -1
|
|
|
|
| 2291 |
ransac_reproj_thresh = float(ransac_reproj_thresh_input) if ransac_reproj_thresh_input is not None else 3.0
|
| 2292 |
max_distance_coeff = float(max_distance_coeff_input) if max_distance_coeff_input is not None else 0.5
|
| 2293 |
+
blend_smooth_ksize = int(blend_smooth_ksize_input) if blend_smooth_ksize_input is not None else -1
|
| 2294 |
+
num_blend_levels = int(num_blend_levels_input) if num_blend_levels_input is not None else 4
|
| 2295 |
|
| 2296 |
log = f"Received {len(input_files)} file(s).\n"
|
| 2297 |
log = log_and_print(f"Pre-Crop Settings: Top={crop_top_percent}%, Bottom={crop_bottom_percent}%\n", log)
|
|
|
|
| 2515 |
blend_method_lower = blend_method_str.lower() if blend_method_str else "multi-band"
|
| 2516 |
|
| 2517 |
# Call the modified stitch_multiple_images function
|
| 2518 |
+
stitched_single_rgba, stitch_log_img = stitch_multiple_images(
|
| 2519 |
images_bgr_cropped, # Pass the list of cropped images (BGRA)
|
| 2520 |
stitcher_mode_str=stitcher_mode_str,
|
| 2521 |
registration_resol=registration_resol,
|
|
|
|
| 2535 |
max_blending_width=max_blending_width,
|
| 2536 |
max_blending_height=max_blending_height,
|
| 2537 |
blend_smooth_ksize=blend_smooth_ksize,
|
| 2538 |
+
num_blend_levels=num_blend_levels,
|
| 2539 |
+
fixed_base_image_index=fixed_base_image_index
|
| 2540 |
)
|
| 2541 |
stitch_log += stitch_log_img # Append log from stitching function
|
| 2542 |
+
if stitched_single_rgba is not None:
|
| 2543 |
+
final_stitched_images_rgba = [stitched_single_rgba] # Result is a list containing the single image
|
| 2544 |
|
| 2545 |
# Clean up loaded images for list mode after stitching attempt
|
| 2546 |
if 'images_bgr_cropped' in locals():
|
|
|
|
| 2588 |
temp_dir = tempfile.mkdtemp(prefix="stitch_run_", dir=target_temp_dir_base)
|
| 2589 |
final_log = log_and_print(f"\nInfo: Saving output images to temporary directory: {temp_dir}\n", final_log)
|
| 2590 |
|
| 2591 |
+
for i, img_rgba in enumerate(final_stitched_images_rgba):
|
| 2592 |
+
if img_rgba is None or img_rgba.size == 0:
|
| 2593 |
final_log = log_and_print(f"Warning: Skipping saving image index {i} because it is None or empty.\n", final_log)
|
| 2594 |
continue
|
| 2595 |
+
|
| 2596 |
+
# Default filename
|
| 2597 |
+
filename = f"stitched_{i+1:03d}.png" # PNG is required for transparency
|
| 2598 |
+
|
| 2599 |
+
# If in multi-image mode (not video mode), and input paths exist
|
| 2600 |
+
if not is_video_input and image_paths:
|
| 2601 |
+
try:
|
| 2602 |
+
# Get the path of the last image
|
| 2603 |
+
last_image_path = image_paths[-1]
|
| 2604 |
+
# Get the filename and remove the extension (e.g., "filename.jpg" -> "filename")
|
| 2605 |
+
last_image_name = os.path.splitext(os.path.basename(last_image_path))[0]
|
| 2606 |
+
|
| 2607 |
+
if len(final_stitched_images_rgba) > 1:
|
| 2608 |
+
# In case multiple results are produced, add numbering to avoid overwriting
|
| 2609 |
+
filename = f"{last_image_name}_stitched_{i+1:03d}.png"
|
| 2610 |
+
else:
|
| 2611 |
+
# Normal single result, use requested format: [last_image_filename]_stitched.png
|
| 2612 |
+
filename = f"{last_image_name}_stitched.png"
|
| 2613 |
+
except Exception as e_name:
|
| 2614 |
+
# If an error occurs, keep the default filename
|
| 2615 |
+
pass
|
| 2616 |
+
|
| 2617 |
# Use os.path.join for cross-platform compatibility
|
| 2618 |
full_path = os.path.join(temp_dir, filename)
|
| 2619 |
+
img_bgra = None # Initialize for finally block
|
| 2620 |
try:
|
| 2621 |
# Handle RGBA to BGRA for saving
|
| 2622 |
# The result coming from stitcher is in RGB(A) format
|
| 2623 |
+
if img_rgba.shape[2] == 4:
|
| 2624 |
+
img_bgra = cv2.cvtColor(img_rgba, cv2.COLOR_RGBA2BGRA)
|
| 2625 |
else:
|
| 2626 |
+
img_bgra = cv2.cvtColor(img_rgba, cv2.COLOR_RGB2BGR)
|
| 2627 |
|
| 2628 |
# Use imencode -> write pattern for better handling of paths/special chars
|
| 2629 |
+
is_success, buf = cv2.imencode('.png', img_bgra)
|
| 2630 |
if is_success:
|
| 2631 |
with open(full_path, 'wb') as f:
|
| 2632 |
f.write(buf)
|
|
|
|
| 2642 |
except Exception as e_write:
|
| 2643 |
final_log = log_and_print(f"Unexpected error writing image {filename} to {full_path}: {e_write}\n", final_log)
|
| 2644 |
finally:
|
| 2645 |
+
if img_bgra is not None:
|
| 2646 |
+
del img_bgra
|
| 2647 |
gc.collect()
|
| 2648 |
except Exception as e_tempdir:
|
| 2649 |
final_log = log_and_print(f"Error creating temporary directory or saving output: {e_tempdir}\n", final_log)
|
| 2650 |
output_file_paths = [] # Fallback to empty list
|
| 2651 |
|
| 2652 |
# --- Final Cleanup of RGB images list ---
|
| 2653 |
+
if 'final_stitched_images_rgba' in locals():
|
| 2654 |
for img_del in final_stitched_images_rgba:
|
| 2655 |
if img_del is not None:
|
| 2656 |
del img_del
|
|
|
|
| 2712 |
|
| 2713 |
# --- Detailed Stitcher Settings (Used for Video, DIRECT_PAIRWISE, and Fallback) ---
|
| 2714 |
with gr.Accordion("Pairwise Stitching Settings (Video / Direct / Fallback)", open=True):
|
| 2715 |
+
fixed_base_image_index_input = gr.Number(value=-1, label="Fixed Base Image Index (0-based)", precision=0,
|
| 2716 |
+
info="Specify the index of the image (0, 1, 2...) that should serve as the 'Bottom/Base' layer without blending. Overlapping images will cover it, but brightness will still adjust. Set -1 to disable.")
|
| 2717 |
transform_model = gr.Radio(["Homography", "Affine_Partial", "Affine_Full"], label="Pairwise Transform Model", value="Homography", # Default to Homography
|
| 2718 |
+
info="Geometric model for pairwise alignment. 'Homography' handles perspective. 'Affine' (Partial/Full) handles translation, rotation, scale, shear (better for scans, less distortion risk). If stitching fails with one model, try another.")
|
| 2719 |
blend_method = gr.Radio(["Linear", "Multi-Band"], label="Blending Method", value="Multi-Band",
|
| 2720 |
+
info="Algorithm for smoothing seams in overlapping regions when using the detailed stitcher (for video or image list fallback). 'Multi-Band' is often better but slower.")
|
| 2721 |
enable_gain_compensation = gr.Checkbox(value=True, label="Enable Gain Compensation",
|
| 2722 |
+
info="Adjusts overall brightness difference *before* blending when using the detailed stitcher. Recommended.")
|
| 2723 |
orb_nfeatures = gr.Slider(500, 10000, step=100, value=2000, label="ORB Features",
|
| 2724 |
+
info="Maximum ORB keypoints detected per image/frame. Used by the detailed stitcher (for video or image list fallback).")
|
| 2725 |
match_ratio_thresh = gr.Slider(0.5, 0.95, step=0.01, value=0.75, label="Match Ratio Threshold",
|
| 2726 |
+
info="Lowe's ratio test threshold for filtering feature matches (lower = stricter). Used by the detailed stitcher (for video or image list fallback).")
|
| 2727 |
ransac_reproj_thresh = gr.Slider(1.0, 10.0, step=0.1, value=5.0, label="RANSAC Reproj Threshold",
|
| 2728 |
+
info="Maximum reprojection error (pixels) allowed for a match to be considered an inlier by RANSAC during transformation estimation. Lower values are stricter.")
|
| 2729 |
max_distance_coeff = gr.Slider(0.1, 2.0, step=0.05, value=0.5, label="Max Distance Coeff",
|
| 2730 |
+
info="Multiplier for image diagonal used to filter initial matches. Limits the pixel distance between matched keypoints (0.5 means half the diagonal).")
|
| 2731 |
max_blending_width = gr.Number(value=10000, label="Max Blending Width", precision=0,
|
| 2732 |
+
info="Limits the canvas width during the detailed pairwise blending step to prevent excessive memory usage. Relevant for the detailed stitcher.")
|
| 2733 |
max_blending_height = gr.Number(value=10000, label="Max Blending Height", precision=0,
|
| 2734 |
+
info="Limits the canvas height during the detailed pairwise blending step to prevent excessive memory usage. Relevant for the detailed stitcher.")
|
| 2735 |
blend_smooth_ksize = gr.Number(value=15, label="Blend Smooth Kernel Size", precision=0,
|
| 2736 |
+
info="Size of Gaussian kernel to smooth blend mask/weights. Must be POSITIVE ODD integer to enable smoothing (e.g., 5, 15, 21). Set to -1 or an even number to disable smoothing.")
|
| 2737 |
num_blend_levels = gr.Slider(2, 7, step=1, value=4, label="Multi-Band Blend Levels",
|
| 2738 |
+
info="Number of pyramid levels for Multi-Band blending. Fewer levels are faster but might have less smooth transitions.")
|
| 2739 |
|
| 2740 |
with gr.Accordion("Video Stitcher Settings", open=False):
|
| 2741 |
sample_interval_ms = gr.Number(value=3000, label="Sample Interval (ms)", precision=0,
|
| 2742 |
+
info="Time interval (in milliseconds) between sampled frames for video stitching. Smaller values sample more frames, increasing processing time but potentially improving tracking.")
|
| 2743 |
max_composite_width_video = gr.Number(value=10000, label="Max Composite Width (Video)", precision=0,
|
| 2744 |
+
info="Limits the width of the stitched output during video processing. If exceeded, the current result is saved and stitching restarts with the next frame. 0 = no limit.")
|
| 2745 |
max_composite_height_video = gr.Number(value=10000, label="Max Composite Height (Video)", precision=0,
|
| 2746 |
+
info="Limits the height of the stitched output during video processing. If exceeded, the current result is saved and stitching restarts with the next frame. 0 = no limit.")
|
| 2747 |
|
| 2748 |
with gr.Accordion("Postprocessing Settings", open=False):
|
| 2749 |
enable_cropping = gr.Checkbox(value=True, label="Crop Black Borders (Post-Stitch)",
|
| 2750 |
+
info="Automatically remove black border areas from the final stitched image(s) AFTER stitching.")
|
| 2751 |
strict_no_black_edges_checkbox = gr.Checkbox(value=False, label="Strict No Black Edges (Post-Crop)",
|
| 2752 |
+
info="If 'Crop Black Borders' is enabled, this forces removal of *any* remaining black pixels directly on the image edges after the main crop. Might slightly shrink the image further.")
|
| 2753 |
|
| 2754 |
with gr.Column(scale=1):
|
| 2755 |
output_gallery = gr.Gallery(
|
|
|
|
| 2774 |
# Postprocessing
|
| 2775 |
enable_cropping,
|
| 2776 |
strict_no_black_edges_checkbox,
|
| 2777 |
+
fixed_base_image_index_input,
|
| 2778 |
# Detailed Stitcher Settings
|
| 2779 |
transform_model,
|
| 2780 |
blend_method,
|
|
|
|
| 2799 |
["examples/Wetter-Panorama/Wetter-Panorama1[NIuO6hrFTrg].mp4"],
|
| 2800 |
0, 20,
|
| 2801 |
"DIRECT_PAIRWISE", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2802 |
+
True, False, -1,
|
| 2803 |
"Homography", "Multi-Band", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2804 |
2500, 10000, 10000,
|
| 2805 |
],
|
|
|
|
| 2807 |
["examples/Wetter-Panorama/Wetter-Panorama2[NIuO6hrFTrg].mp4"],
|
| 2808 |
0, 20,
|
| 2809 |
"DIRECT_PAIRWISE", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2810 |
+
True, False, -1,
|
| 2811 |
"Homography", "Multi-Band", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2812 |
2500, 10000, 10000,
|
| 2813 |
],
|
|
|
|
| 2816 |
"examples/NieRAutomata/nier2B_06.jpg", "examples/NieRAutomata/nier2B_07.jpg", "examples/NieRAutomata/nier2B_08.jpg", "examples/NieRAutomata/nier2B_09.jpg", "examples/NieRAutomata/nier2B_10.jpg", ],
|
| 2817 |
0, 0,
|
| 2818 |
"PANORAMA", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2819 |
+
True, False, -1,
|
| 2820 |
"Homography", "Multi-Band", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2821 |
5000, 10000, 10000,
|
| 2822 |
],
|
|
|
|
| 2824 |
["examples/cat/cat_left.jpg", "examples/cat/cat_right.jpg"],
|
| 2825 |
0, 0,
|
| 2826 |
"SCANS", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2827 |
+
True, False, -1,
|
| 2828 |
"Affine_Partial", "Linear", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2829 |
5000, 10000, 10000,
|
| 2830 |
],
|
|
|
|
| 2832 |
["examples/ギルドの受付嬢ですが/Girumasu_1.jpg", "examples/ギルドの受付嬢ですが/Girumasu_2.jpg", "examples/ギルドの受付嬢ですが/Girumasu_3.jpg"],
|
| 2833 |
0, 0,
|
| 2834 |
"PANORAMA", 0.6, 0.1, -1, False, "GAIN_BLOCKS",
|
| 2835 |
+
True, False, -1,
|
| 2836 |
"Affine_Partial", "Linear", True, 5000, 0.65, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2837 |
5000, 10000, 10000,
|
| 2838 |
],
|
|
|
|
| 2840 |
["examples/photographs1/img1.jpg", "examples/photographs1/img2.jpg", "examples/photographs1/img3.jpg", "examples/photographs1/img4.jpg"],
|
| 2841 |
0, 0,
|
| 2842 |
"PANORAMA", 0.6, 0.1, -1, True, "GAIN_BLOCKS",
|
| 2843 |
+
True, False, -1,
|
| 2844 |
"Homography", "Linear", True, 5000, 0.5, 5.0, 0.5, 10000, 10000, 15, 4,
|
| 2845 |
5000, 10000, 10000,
|
| 2846 |
]
|