HomeJournalThis post

Laplacian Pyramid Blending Without Seams

Implement reduce, expand, band blend, and reconstruct steps with an error receipt and a matched naive-alpha baseline.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

Laplacian pyramid blending lets fine detail cross a narrow seam while broad tone moves gradually. Decompose two generated worlds, verify reconstruction, and compare the result with honest baselines.

Laplacian pyramid blending separates spatial scales

Laplacian pyramid blending joins images by decomposing detail into frequency-like spatial bands, blending each band with a correspondingly smoothed mask, and reconstructing the result. Broad illumination can transition gradually while fine texture crosses a narrower seam, avoiding the single-width compromise of ordinary alpha feathering.

Burt and Adelson’s Laplacian pyramid paper describes Gaussian reduce and expand operations, difference levels, and reconstruction. A reproducible implementation must name filter taps, sampling alignment, dimensions, and edge extension.

The browser lab uses two procedural teaching images. It does not claim photographic realism, universal seam removal, or objective visual preference; it compares hard, alpha, and multiscale constructions while exporting numeric invariants.

A multiresolution blend earns trust by reconstructing each source before combining them. Build the Gaussian stack, subtract expanded neighbors into Laplacian bands, add the residual, then reverse the process; reconstruction error isolates pyramid defects from later mask or compositing choices before any final export, comparison, or aesthetic judgment.

Exploded multiresolution towerTwo generated-image pyramids and their Gaussian mask shrink through five dimensions emitted and hash-checked by the artifact.source Amasksource B6² · 12² · 24² · 48² · 96²
Exploded multiresolution tower
Two generated-image pyramids and their Gaussian mask shrink through five dimensions emitted and hash-checked by the artifact.
Aligned emitted pyramid levels
LevelSizeBlend-band RMS energyMask aligned?
096×960.0012560564yes
148×480.0042365641yes
224×240.0125944086yes
312×120.0248665922yes
46×60.4738370616yes
Figure 1: Source bands and mask levels share dimensions and expose current energy plus SHA-256 evidence.

Generate rights-clear source worlds

Create source A and B from pinned equations, dimensions, color channels, and seed if randomness exists. Procedural fixtures make exact reconstruction and seam behavior reviewable without importing uncertain image rights or hiding compression artifacts inside the algorithm test.

Laplacian pyramid blending still needs compatible subject geometry and palette. Optimal-transport color transfer can align distributions before blending, but it changes the source and deserves its own receipt and human inspection.

The sample botanical and mineral patterns are abstract labels for generated waves, not claims about captured materials. Their difference creates enough low- and high-frequency structure to expose mask and reconstruction mistakes.

Procedural sources keep licensing and provenance simple while creating a hard seam: one field carries fine warm texture, the other broad cool structure. Their generators, dimensions, seed, and color space belong in the receipt so the composite can be reproduced without a hidden asset.

For production photographs, retain original licenses, color profiles, orientation, and edit lineage. The generated browser worlds solve provenance for the tutorial only. A final composite should identify every source and transformation even when multiresolution blending makes the boundary visually disappear.

Build a Gaussian pyramid with explicit edges

At each level, low-pass filter before decimating so frequencies above the smaller grid’s limit do not alias. Decide how odd dimensions round and how samples outside the image are handled—clamp, reflect, wrap, or constant padding—and apply the same convention during expansion.

The teaching code uses a compact box reduction and nearest documented expansion for clarity. That is not the classic five-tap Gaussian spline, so the artifact labels its filters rather than borrowing quality claims from the primary literature.

Laplacian pyramid blending is sensitive to alignment. Store every level’s width and height, assert source and mask agreement, and test odd as well as even fixtures before moving to production images.

Multiresolution image blending assigns a different mask scale to each frequency band. Fine details cross a narrow transition while low-frequency tone moves gradually, avoiding the single compromise width imposed by ordinary alpha compositing.

Masks deserve adversarial fixtures: all zero, all one, a centered step, a diagonal edge, a single-pixel impulse, and odd dimensions. All-zero and all-one masks should reconstruct the corresponding source within tolerance, while complementary masks should exchange source ownership. These properties reveal channel broadcasting, inversion, and edge-alignment bugs more reliably than one elaborate collage.

Derive difference bands and preserve reconstruction

For each noncoarsest level, expand the next Gaussian level to the current size and subtract it from the current Gaussian image. Keep the coarsest residual directly; reconstruction reverses the process by expanding the current coarse image and adding the saved detail band.

An identity test should rebuild each source without blending and measure maximum absolute and aggregate error. A large error means reduce, expand, alignment, channel, or edge policy is inconsistent, regardless of whether the displayed image looks plausible.

The Laplacian pyramid blending lab requires finite values and a strict reconstruction tolerance for its Float64 fixture. It exports the runtime value instead of printing a decorative error constant in the article figure.

A Gaussian pyramid mask must share dimensions and edge policy with the source pyramids at every level. Record each width and height, reject a misaligned array immediately, and render the levels as a semantic table so a visually smooth final image cannot conceal a shifted mask.

Blur the mask at matching levels

Build a Gaussian pyramid from a mask in the same coordinate system as the sources. At level i, combine source bands as mask_i times A_i plus one minus mask_i times B_i; verify mask range, dimensions, channel broadcasting, and orientation at every level.

High-resolution masks retain narrow detail transitions, while coarse masks spread low-frequency changes over a wider area. This is the multiresolution spline idea developed in the image mosaics paper, not a generic blur applied once after compositing.

Visualize mask levels beside bands. An accidentally inverted mask or half-pixel shift is easier to identify in the exploded pyramid than in a busy final composition.

The hard cut is an honest baseline because it preserves both sources and exposes the raw seam. Single-scale alpha adds one feather width; comparing both with the pyramid result shows what multiscale processing changes without presenting the most flattering output alone. Keep all three previews under identical color management and crop geometry.

Runnable artifact — The sources are generated teaching images, the seam metrics are local proxies, and visual preference still requires human art direction.

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Unclipped Laplacian pyramid blender</title><style>:root{color-scheme:dark}*{box-sizing:border-box}body{font:16px/1.45 system-ui;background:#091318;color:#f4f7f6;max-width:980px;margin:auto;padding:24px}main{display:grid;gap:16px;min-width:0}fieldset,.panel{border:1px solid #8aa0aa;border-radius:12px;padding:14px}fieldset{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}label{display:grid;gap:5px}button,input,select,a,textarea{font:inherit;padding:9px}button,a{min-height:44px}textarea{width:100%;min-height:150px;background:#071014;color:#f4f7f6}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.status{padding:10px;border-left:5px solid #46e0c1;background:#10242b}svg,canvas{max-width:100%;height:auto}.sr{position:absolute;left:-9999px}table{border-collapse:collapse;width:100%;table-layout:fixed}th,td{padding:7px;border:1px solid #8aa0aa;text-align:left;overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important}}canvas{width:100%;border:1px solid #8aa0aa;background:#071014}</style><main><h1>Unclipped Laplacian pyramid blender</h1><p>The artifact rejects non-finite or out-of-range reconstruction before display. Hard cut, alpha feather, and pyramid output share the same generated source arrays.</p><p><button id="run">Rebuild all pyramid levels</button> <a id="pngExport" download="laplacian-comparison.png">Export current PNG</a> <a id="jsonExport" download="laplacian-diagnostics.json">Export current JSON</a></p><canvas id="canvas" width="288" height="96" aria-label="Hard cut, alpha feather, and unclipped Laplacian blend"></canvas><p id="status" class="status" aria-live="polite"></p><table><caption>Current seam-gradient and range evidence</caption><thead><tr><th>Output</th><th>Seam gradient</th><th>Minimum</th><th>Maximum</th></tr></thead><tbody id="rows"></tbody></table><textarea id="receipt" readonly aria-label="Execution receipt"></textarea></main><script>const W=96,H=96,C=3,LEVELS=5,canvas=document.getElementById('canvas'),context=canvas.getContext('2d'),receipt=document.getElementById('receipt'),statusNode=document.getElementById('status'),kernel=[1,4,6,4,1].map(value=>value/16);
function source(kind){const output=new Float64Array(W*H*C);for(let y=0;y<H;y++)for(let x=0;x<W;x++)for(let channel=0;channel<C;channel++){const nx=x/(W-1),ny=y/(H-1),index=(y*W+x)*C+channel;output[index]=kind==='botanical'?.43+.08*Math.sin(nx*9+channel*.7)+.06*Math.cos(ny*7-channel*.4)+.035*Math.sin((nx+ny)*17):.49+.07*Math.cos((nx+ny)*8+channel*.8)+.055*Math.sin(ny*11-channel*.5)+.025*Math.cos(nx*19)}return output}
function maskSource(){const output=new Float64Array(W*H);for(let y=0;y<H;y++)for(let x=0;x<W;x++)output[y*W+x]=1/(1+Math.exp((x-(W-1)/2)/7));return output}
function reduce(input,width,height,channels){const nextWidth=Math.ceil(width/2),nextHeight=Math.ceil(height/2),output=new Float64Array(nextWidth*nextHeight*channels);for(let y=0;y<nextHeight;y++)for(let x=0;x<nextWidth;x++)for(let channel=0;channel<channels;channel++){let sum=0;for(let ky=-2;ky<=2;ky++)for(let kx=-2;kx<=2;kx++){const sourceX=Math.max(0,Math.min(width-1,x*2+kx)),sourceY=Math.max(0,Math.min(height-1,y*2+ky));sum+=input[(sourceY*width+sourceX)*channels+channel]*kernel[kx+2]*kernel[ky+2]}output[(y*nextWidth+x)*channels+channel]=sum}return{data:output,width:nextWidth,height:nextHeight,channels}}
function expand(level,width,height){const output=new Float64Array(width*height*level.channels);for(let y=0;y<height;y++)for(let x=0;x<width;x++){const sourceX=x/2,sourceY=y/2,x0=Math.floor(sourceX),y0=Math.floor(sourceY),x1=Math.min(level.width-1,x0+1),y1=Math.min(level.height-1,y0+1),tx=sourceX-x0,ty=sourceY-y0;for(let channel=0;channel<level.channels;channel++){const a=level.data[(y0*level.width+x0)*level.channels+channel],b=level.data[(y0*level.width+x1)*level.channels+channel],c=level.data[(y1*level.width+x0)*level.channels+channel],d=level.data[(y1*level.width+x1)*level.channels+channel];output[(y*width+x)*level.channels+channel]=(a*(1-tx)+b*tx)*(1-ty)+(c*(1-tx)+d*tx)*ty}}return output}
function pyramids(input,channels){const gaussian=[{data:input,width:W,height:H,channels}];for(let index=1;index<LEVELS;index++)gaussian.push(reduce(gaussian.at(-1).data,gaussian.at(-1).width,gaussian.at(-1).height,channels));const bands=gaussian.slice(0,-1).map((level,index)=>{const expanded=expand(gaussian[index+1],level.width,level.height);return{...level,data:Float64Array.from(level.data,(value,offset)=>value-expanded[offset])}});bands.push(gaussian.at(-1));return{gaussian,bands}}
function reconstruct(bands){let current=bands.at(-1).data;for(let index=bands.length-2;index>=0;index--){const expanded=expand({data:current,width:bands[index+1].width,height:bands[index+1].height,channels:C},bands[index].width,bands[index].height);current=Float64Array.from(bands[index].data,(value,offset)=>value+expanded[offset])}return current}
function validate(label,values,rangeRequired){let nonFinite=0,outOfRange=0,min=Infinity,max=-Infinity;for(const value of values){if(!Number.isFinite(value))nonFinite++;else{min=Math.min(min,value);max=Math.max(max,value);if(value<0||value>1)outOfRange++}}if(nonFinite)throw Error(label+' contains '+nonFinite+' non-finite values');if(rangeRequired&&outOfRange)throw Error(label+' contains '+outOfRange+' out-of-range values; rendering rejected');return{min,max,nonFinite,outOfRange}}
function maxError(left,right){let maximum=0;for(let index=0;index<left.length;index++)maximum=Math.max(maximum,Math.abs(left[index]-right[index]));return maximum}
function energy(values){let sum=0;for(const value of values)sum+=value*value;return Math.sqrt(sum/values.length)}
function seamGradient(values){let sum=0,count=0;for(let y=0;y<H;y++)for(let x=Math.floor(W/2)-4;x<=Math.floor(W/2)+4;x++)for(let channel=0;channel<C;channel++){sum+=Math.abs(values[(y*W+x)*C+channel]-values[(y*W+x-1)*C+channel]);count++}return sum/count}
function shaBuffer(buffer){return crypto.subtle.digest('SHA-256',buffer).then(hash=>[...new Uint8Array(hash)].map(value=>value.toString(16).padStart(2,'0')).join(''))}
function toImage(values){validate('display output',values,true);const pixels=new Uint8ClampedArray(W*H*4);for(let index=0;index<W*H;index++){pixels[index*4]=Math.round(values[index*C]*255);pixels[index*4+1]=Math.round(values[index*C+1]*255);pixels[index*4+2]=Math.round(values[index*C+2]*255);pixels[index*4+3]=255}return new ImageData(pixels,W,H)}
async function execute(){try{const botanical=source('botanical'),mineral=source('mineral'),mask=maskSource(),a=pyramids(botanical,C),b=pyramids(mineral,C),m=pyramids(mask,1),reconstructedA=reconstruct(a.bands),reconstructedB=reconstruct(b.bands),blendBands=a.bands.map((level,index)=>({width:level.width,height:level.height,channels:C,data:Float64Array.from(level.data,(value,offset)=>{const pixel=Math.floor(offset/C),weight=m.gaussian[index].data[pixel];return value*weight+b.bands[index].data[offset]*(1-weight)})})),pyramid=reconstruct(blendBands),hard=Float64Array.from(botanical,(value,offset)=>Math.floor(offset/C)%W<W/2?value:mineral[offset]),alpha=Float64Array.from(botanical,(value,offset)=>{const weight=mask[Math.floor(offset/C)];return value*weight+mineral[offset]*(1-weight)}),ranges={hard:validate('hard cut',hard,true),alpha:validate('alpha feather',alpha,true),pyramid:validate('unclipped pyramid',pyramid,true)},reconstruction={botanicalMaxError:maxError(botanical,reconstructedA),mineralMaxError:maxError(mineral,reconstructedB)};if(reconstruction.botanicalMaxError>1e-12||reconstruction.mineralMaxError>1e-12)throw Error('source reconstruction error exceeds tolerance');const outputs={hard,alpha,pyramid};for(const [column,values] of Object.values(outputs).entries()){const temporary=document.createElement('canvas');temporary.width=W;temporary.height=H;temporary.getContext('2d').putImageData(toImage(values),0,0);context.drawImage(temporary,column*W,0)}const blob=await new Promise(resolve=>canvas.toBlob(resolve,'image/png')),hashLevels=async levels=>Promise.all(levels.map(async(level,index)=>({index,dimensions:[level.width,level.height],energy:energy(level.data),sha256:await shaBuffer(level.data.buffer)}))),seams={hard:seamGradient(hard),alpha:seamGradient(alpha),pyramid:seamGradient(pyramid)},failureFixtures={nonFiniteRejected:false,outOfRangeRejected:false};try{validate('non-finite fixture',new Float64Array([0,NaN]),true)}catch{failureFixtures.nonFiniteRejected=true}try{validate('out-of-range fixture',new Float64Array([0,1.001]),true)}catch{failureFixtures.outOfRangeRejected=true}const data={sources:'generated botanical and mineral fixtures',dimensions:[W,H],filter:'separable 5-tap binomial reduce; bilinear expand',edge:'clamp',levelCount:LEVELS,renderClampingApplied:false,reconstruction,ranges,seamGradient:seams,maskAlignment:m.gaussian.map((level,index)=>({index,mask:[level.width,level.height],band:[blendBands[index].width,blendBands[index].height],aligned:level.width===blendBands[index].width&&level.height===blendBands[index].height})),bandEvidence:{botanical:await hashLevels(a.bands),mineral:await hashLevels(b.bands),blend:await hashLevels(blendBands)},failureFixtures,checksums:{hard:await shaBuffer(hard.buffer),alpha:await shaBuffer(alpha.buffer),pyramid:await shaBuffer(pyramid.buffer),mask:await shaBuffer(mask.buffer),png:await shaBuffer(await blob.arrayBuffer())},baselines:['hard cut','single alpha feather','Laplacian pyramid']};const invariants={zeroClippedValues:ranges.pyramid.outOfRange===0,finite:ranges.pyramid.nonFinite===0,reconstructionExact:Object.values(reconstruction).every(value=>value<=1e-12),maskLevelsAligned:data.maskAlignment.every(level=>level.aligned),failurePathsExercised:Object.values(failureFixtures).every(Boolean),bandEvidenceComplete:Object.values(data.bandEvidence).every(levels=>levels.length===LEVELS)};if(!Object.values(invariants).every(Boolean))throw Error('pyramid evidence invariant failed');data.invariants=invariants;window.__laplacianLast={botanical,mineral,mask,a,b,m,blendBands,hard,alpha,pyramid,data};rows.innerHTML=Object.entries(outputs).map(([name,values])=>'<tr><th>'+name+'</th><td>'+seams[name]+'</td><td>'+ranges[name].min+'</td><td>'+ranges[name].max+'</td></tr>').join('');pngExport.href=URL.createObjectURL(blob);jsonExport.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));receipt.dataset.execution=JSON.stringify(data);receipt.value='PASS: '+JSON.stringify(data,null,2);statusNode.textContent='Unclipped output '+ranges.pyramid.min.toFixed(4)+'–'+ranges.pyramid.max.toFixed(4)+' · reconstruction error '+Math.max(...Object.values(reconstruction)).toExponential(2)}catch(error){receipt.dataset.execution=JSON.stringify({unexpectedError:error.name+': '+error.message});receipt.value='FAIL: unexpected '+error.message}}
run.onclick=()=>void execute();window.__laplacian={source,maskSource,reduce,expand,pyramids,reconstruct,validate,seamGradient,execute};void execute();</script></html>

Compare honest seam baselines

Produce a hard cut with the same center boundary and an alpha blend with the original full-resolution mask. Then compare all three outputs at identical size and color handling, preserving the baseline formulas so multiscale quality is not judged against an intentionally broken alternative.

Laplacian pyramid blending often reduces conspicuous low-frequency discontinuity while retaining texture, but it can create halos, ghosting, or doubled structures when sources are misregistered. Image quilting addresses patch boundaries through different candidate and seam logic, making it a useful conceptual contrast.

The lab displays its three generated outputs in one canvas. Human reviewers still decide which artifacts matter for the intended image.

A seamless image composite cannot be certified by one seam-gradient number. Publish local gradient energy, source reconstruction error, clipping counts, and per-level band energy, then keep human art direction responsible for halos, subject continuity, and whether the transition looks intentional.

Source alignment remains an artistic and technical prerequisite. Feature displacement produces ghosts across multiple bands; exposure mismatch creates broad transitions; different noise or sharpening can create halos. Register, crop, and color-adjust as explicit upstream transforms with reversible parameters. When the seam still fails, show the intermediate levels and choose a different mask or composition instead of increasing pyramid depth until the symptom becomes diffuse.

Measure invariants rather than beauty

Track per-level dimensions, energy or norm, finite values, mask range, clipping count, reconstruction error, and output hashes. A local seam-gradient proxy can compare one fixture when precisely defined, but it cannot measure composition, semantic alignment, color plausibility, or visual preference.

Avoid turning one low seam score into a universal claim. Report the crop, boundary region, derivative operator, channels, and aggregation, and keep the original images available for side-by-side review.

The article’s diagnostic figure names these evidence fields without assigning unsupported illustrative results. Executing Laplacian pyramid blending generates the actual errors, clipping count, and checksums used for its receipt.

Frequency band blending depends on reduce and expand filters plus boundary extension. Pin coefficients and reflect behavior in code, test constant and impulse fixtures, and avoid naming a generic resize call as a Laplacian pyramid when its kernel and edge semantics are unknown, implicit, or allowed to change between browser releases. Archive the impulse response beside every accepted filter revision for comparison.

Frequency-band blend sheetNarrow high-frequency seams and broad low-frequency transitions are blended separately before reconstruction.high band Anarrow maskhigh blendmid band Amedium maskmid blendlow field Abroad masklow blend
Frequency-band blend sheet
Narrow high-frequency seams and broad low-frequency transitions are blended separately before reconstruction.
  • High frequencies: narrow transition preserves small texture near the seam.
  • Middle frequencies: medium transition redistributes feature edges.
  • Low frequencies: broad transition prevents a large luminance step.
  • Reconstruction: sum expanded coarse field and all blended detail bands.
Figure 2: Transition width grows as spatial frequency falls.

Control clipping and color space

Difference bands contain signed values and should stay in a floating representation until final display. Count values outside the destination gamut, decide whether to clamp, tone-map, or convert, and preserve the unclipped computation for diagnostics so premature 8-bit conversion does not destroy reconstruction.

Blending encoded sRGB channel values differs from blending linear-light values. Choose and label the working space according to the artistic intent and image pipeline, then test browser Canvas conversions rather than assuming the pixel buffer and displayed output share one implicit interpretation.

The Canvas 2D standard supplies the raster and export primitives. Browser color-management changes are therefore a revisit trigger even when pyramid math is untouched.

Laplacian pyramid blending should retain signed band values until reconstruction. Clamping each level destroys negative detail and may still produce an attractive picture, so the artifact counts non-finite and final clipping events while keeping intermediate math in floating point through every level.

Compose multiscale collage deliberately

Registration, scale, perspective, edge content, mask shape, and palette determine whether the blend supports the composition. Use the pyramid to make transitions flexible, not to erase authorship; preserve where source worlds meet when that boundary carries narrative value.

Seam-carving before compositing can retarget a source, while reversible Haar detail offers another multiscale representation. Each transformation changes lineage and should produce a new digest instead of disappearing inside a final PNG.

Save intermediate band contact sheets for art review. They often reveal that one source dominates low frequencies or that mask structure introduces a halo before those effects become difficult to diagnose in the reconstruction.

The PNG and JSON receipt form one edition: the image shows the decision, while parameters, level shapes, errors, energies, and hashes explain how it was made. Same-code determinism is asserted in the artifact; cross-browser pixel identity remains bounded by Canvas color and encoding behavior.

Memory and latency grow with every stored level and channel. Estimate total pyramid storage, reuse buffers where correctness permits, and measure without changing numeric precision between baseline and optimized paths. A tiled implementation must include overlap sufficient for the largest filter footprint or it will reintroduce grid seams that the multiscale method was chosen to avoid. Record tile policy beside the filter.

Ship a multiresolution blend receipt

Archive source generators and hashes, dimensions, working color space, filter taps, reduce and expand alignment, edge policy, level sizes, mask generator, per-level checksums, reconstruction tests, clipping and nonfinite-value counts, baseline outputs, final PNG, and review notes. Names should distinguish generated fixtures from production imagery.

Run the artifact, inspect all levels in its JSON, and confirm source reconstruction before evaluating the blend. Then substitute rights-cleared images while preserving a tiny deterministic fixture in tests, since photographs can hide a broken invariant behind texture.

Revisit Laplacian pyramid blending when filters, edge policy, color management, seam metric, or source generator changes. The strongest result combines numerical reversibility with intentional human judgement, never one in place of the other.

Revisit after filter, edge policy, seam metric, color management, or source-generator changes. An exploded-pyramid poster can carry the artistic idea, provided it identifies procedural inputs and links back to the reconstruction and baseline evidence rather than promising an invisible seam everywhere.

Three seam baselinesHard cut, alpha feather, and pyramid reconstruction remain side by side while the live receipt rejects clipping and reports comparable seam gradients.hard · .016114alpha · .005968pyramid · .004877same generated sources · mean seam-gradient proxy
Three seam baselines
Hard cut, alpha feather, and pyramid reconstruction remain side by side while the live receipt rejects clipping and reports comparable seam gradients.
Executed generated-source comparison
MethodSeam gradientOutput range
Hard cut0.01611399360.258651–0.639500
Alpha feather0.00596822740.285253–0.624900
Laplacian pyramid0.00487739580.293140–0.620049
  • Both source reconstructions have zero maximum error; pyramid out-of-range and non-finite counts are zero.
Figure 3: The fixture is rendered unclipped only after range, finite-value, mask, band, and reconstruction checks pass.