feat: incremental assembly for large panoramas
Replace parallel chunk strategy with sequential incremental assembly: 1. Start with first 20 images, full cpfind + optimize 2. Add 5 new images at a time with 5-image bridge to existing set 3. cpfind only on bridge+new group (small, fast) 4. Remap CPs to global indices and accumulate This ensures spatial coherence: each new batch is constrained by the already-stable panorama, preventing orientation flips that occurred with the parallel chunk approach. Images are sorted by azimuth, so consecutive indices = spatial neighbors (guaranteed contiguity). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -329,77 +329,116 @@ def run_pipeline(work_dir, clahe_images, original_images, azs, els,
|
|||||||
timings["cpclean"] = time.time() - t0
|
timings["cpclean"] = time.time() - t0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Large panorama: chunked cpfind strategy
|
# Large panorama: incremental assembly strategy
|
||||||
# Split into overlapping chunks, cpfind each, merge CPs
|
# Images are sorted by azimuth — consecutive indices = spatial neighbors.
|
||||||
step = chunk_size - chunk_overlap
|
# We build the panorama progressively:
|
||||||
chunks = []
|
# 1. Start with first initial_size images, full cpfind + optimize
|
||||||
i = 0
|
# 2. Add batch_size new images at a time
|
||||||
while i < n_images:
|
# 3. cpfind only between new images and the tail of existing set
|
||||||
end = min(i + chunk_size, n_images)
|
# 4. Optimize incrementally (existing images constrain new ones)
|
||||||
chunks.append((i, end))
|
initial_size = chunk_size # first group size
|
||||||
if end >= n_images:
|
batch_size = chunk_overlap # images to add per iteration
|
||||||
break
|
bridge_size = 5 # how many existing images to overlap with new ones
|
||||||
i += step
|
|
||||||
|
|
||||||
print(f" Chunked cpfind: {n_images} images -> {len(chunks)} chunks of ~{chunk_size} "
|
print(f" Incremental assembly: {n_images} images, "
|
||||||
f"(overlap={chunk_overlap})", flush=True)
|
f"initial={initial_size}, batch={batch_size}, bridge={bridge_size}",
|
||||||
|
flush=True)
|
||||||
|
|
||||||
all_cp_lines = []
|
all_cp_lines = []
|
||||||
t0_total = time.time()
|
t0_total = time.time()
|
||||||
|
|
||||||
for chunk_idx, (start, end) in enumerate(chunks):
|
# Phase 1: initial group — full cpfind
|
||||||
chunk_clahe = clahe_images[start:end]
|
init_end = min(initial_size, n_images)
|
||||||
chunk_azs = azs[start:end]
|
init_pto = os.path.join(work_dir, f"{output_name}_init.pto")
|
||||||
chunk_els = els[start:end]
|
build_pto(init_pto, clahe_images[:init_end],
|
||||||
|
azs[:init_end], els[:init_end], img_w, img_h, fov)
|
||||||
|
|
||||||
# Build chunk PTO
|
print(f" Phase 1: cpfind on images 0-{init_end-1} ({init_end} images)...",
|
||||||
chunk_pto = os.path.join(work_dir, f"{output_name}_chunk{chunk_idx}.pto")
|
flush=True)
|
||||||
build_pto(chunk_pto, chunk_clahe, chunk_azs, chunk_els, img_w, img_h, fov)
|
|
||||||
|
|
||||||
# cpfind on chunk
|
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
r = run_hugin("cpfind",
|
r = run_hugin("cpfind",
|
||||||
f"--multirow --celeste "
|
f"--multirow --celeste "
|
||||||
f"--sieve1width 20 --sieve1height 20 --sieve1size 1000 "
|
f"--sieve1width 20 --sieve1height 20 --sieve1size 1000 "
|
||||||
f"--sieve2width 10 --sieve2height 10 --sieve2size 5 "
|
f"--sieve2width 10 --sieve2height 10 --sieve2size 5 "
|
||||||
f"--minmatches 3 --ransaciter 2000 --ransacdist 50 "
|
f"--minmatches 3 --ransaciter 2000 --ransacdist 50 "
|
||||||
f'-o "{chunk_pto}" "{chunk_pto}"',
|
f'-o "{init_pto}" "{init_pto}"',
|
||||||
cwd=work_dir)
|
cwd=work_dir)
|
||||||
cp_time = time.time() - t0
|
run_hugin("cpclean", f'-o "{init_pto}" "{init_pto}"', cwd=work_dir)
|
||||||
|
|
||||||
# cpclean on chunk
|
# Read initial CPs (local indices = global indices for first group)
|
||||||
run_hugin("cpclean", f'-o "{chunk_pto}" "{chunk_pto}"', cwd=work_dir)
|
with open(init_pto) as f:
|
||||||
|
for line in f:
|
||||||
# Read CPs and remap indices to global
|
if line.startswith("c "):
|
||||||
with open(chunk_pto) as f:
|
all_cp_lines.append(line)
|
||||||
|
os.remove(init_pto)
|
||||||
|
|
||||||
|
init_time = time.time() - t0
|
||||||
|
print(f" Initial: {init_time:.0f}s, CPs: {len(all_cp_lines)}", flush=True)
|
||||||
|
|
||||||
|
# Phase 2: add batches incrementally
|
||||||
|
cursor = init_end
|
||||||
|
batch_num = 0
|
||||||
|
while cursor < n_images:
|
||||||
|
batch_num += 1
|
||||||
|
batch_end = min(cursor + batch_size, n_images)
|
||||||
|
new_count = batch_end - cursor
|
||||||
|
|
||||||
|
# Bridge: last bridge_size images from existing set + new images
|
||||||
|
bridge_start = max(0, cursor - bridge_size)
|
||||||
|
group_start = bridge_start
|
||||||
|
group_end = batch_end
|
||||||
|
group_size = group_end - group_start
|
||||||
|
|
||||||
|
# Build PTO for this bridge+new group
|
||||||
|
batch_pto = os.path.join(work_dir, f"{output_name}_batch{batch_num}.pto")
|
||||||
|
build_pto(batch_pto,
|
||||||
|
clahe_images[group_start:group_end],
|
||||||
|
azs[group_start:group_end],
|
||||||
|
els[group_start:group_end],
|
||||||
|
img_w, img_h, fov)
|
||||||
|
|
||||||
|
# cpfind on bridge+new group
|
||||||
|
t0 = time.time()
|
||||||
|
r = run_hugin("cpfind",
|
||||||
|
f"--multirow --celeste "
|
||||||
|
f"--sieve1width 20 --sieve1height 20 --sieve1size 1000 "
|
||||||
|
f"--sieve2width 10 --sieve2height 10 --sieve2size 5 "
|
||||||
|
f"--minmatches 3 --ransaciter 2000 --ransacdist 50 "
|
||||||
|
f'-o "{batch_pto}" "{batch_pto}"',
|
||||||
|
cwd=work_dir)
|
||||||
|
run_hugin("cpclean", f'-o "{batch_pto}" "{batch_pto}"', cwd=work_dir)
|
||||||
|
batch_time = time.time() - t0
|
||||||
|
|
||||||
|
# Read CPs and remap local indices to global
|
||||||
|
batch_cps = 0
|
||||||
|
with open(batch_pto) as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
if line.startswith("c "):
|
if line.startswith("c "):
|
||||||
# Remap local image indices to global
|
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
new_parts = []
|
new_parts = []
|
||||||
for p in parts:
|
for p in parts:
|
||||||
if p.startswith("n") and not p.startswith("N"):
|
if p.startswith("n") and not p.startswith("N"):
|
||||||
local_idx = int(p[1:])
|
local_idx = int(p[1:])
|
||||||
new_parts.append(f"n{start + local_idx}")
|
new_parts.append(f"n{group_start + local_idx}")
|
||||||
elif p.startswith("N"):
|
elif p.startswith("N"):
|
||||||
local_idx = int(p[1:])
|
local_idx = int(p[1:])
|
||||||
new_parts.append(f"N{start + local_idx}")
|
new_parts.append(f"N{group_start + local_idx}")
|
||||||
else:
|
else:
|
||||||
new_parts.append(p)
|
new_parts.append(p)
|
||||||
all_cp_lines.append(" ".join(new_parts) + "\n")
|
all_cp_lines.append(" ".join(new_parts) + "\n")
|
||||||
|
batch_cps += 1
|
||||||
|
|
||||||
# Cleanup chunk PTO
|
os.remove(batch_pto)
|
||||||
os.remove(chunk_pto)
|
print(f" Batch {batch_num}: imgs {cursor}-{batch_end-1} "
|
||||||
|
f"(bridge from {bridge_start}): {batch_time:.0f}s, "
|
||||||
|
f"+{batch_cps} CPs, total: {len(all_cp_lines)}", flush=True)
|
||||||
|
|
||||||
chunk_cps = len([l for l in all_cp_lines]) # running total
|
cursor = batch_end
|
||||||
print(f" Chunk {chunk_idx+1}/{len(chunks)} "
|
|
||||||
f"(imgs {start}-{end-1}): {cp_time:.0f}s, "
|
|
||||||
f"total CPs so far: {len(all_cp_lines)}", flush=True)
|
|
||||||
|
|
||||||
timings["cpfind"] = time.time() - t0_total
|
timings["cpfind"] = time.time() - t0_total
|
||||||
timings["cpclean"] = 0 # included in per-chunk processing
|
timings["cpclean"] = 0 # included in per-batch processing
|
||||||
|
|
||||||
# Write CPs back to the full CLAHE PTO
|
# Write all CPs to the full CLAHE PTO
|
||||||
with open(pto_clahe, "a") as f:
|
with open(pto_clahe, "a") as f:
|
||||||
f.writelines(all_cp_lines)
|
f.writelines(all_cp_lines)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user