Technical report version
Developer
2026
Computer Vision, Algorithm Design, Testing
Python, NumPy, scikit-image
Individual project
Prokudin-Gorskii glass plate scans contain three vertically stacked monochrome exposures. I split each input into equal-height B, G, and R channel arrays, held B fixed, estimated separate G→B and R→B shifts, then formed an RGB image with np.dstack([R, G, B]).
The implementation uses Python 3, NumPy, and scikit-image. It saves the reconstructed image as a JPG and writes each channel displacement to out/alignment_results.txt.
For the small JPG inputs, I evaluated every integer displacement in a [-15, 15] window along both axes. Each trial applies np.roll to the moving channel and compares it with the fixed blue channel.
For each shift, calculate the square root of the sum of squared pixel differences. The shift with the lowest score wins.
Mean-center both images, normalize by their L2 norms, and choose the shift with the highest correlation score.
Both metrics produced equally good alignments on the three supplied small images. Because np.roll wraps pixels from one edge to the opposite edge, I scored only a cropped interior region. For JPGs, the crop removes 20 pixels on every edge before scoring; the chosen shift is still applied to the full-resolution channel.




Brute-force full-resolution search is too slow for the large TIF images. For these inputs, the pipeline first computes Sobel edge maps for the moving and reference channels. Edges remain more stable than raw brightness across differently exposed color channels, particularly for difficult images such as Emir.
At pyramid levels, NCC scores exclude a border of min(10, height // 20, width // 20) pixels. This leaves enough image structure for matching while eliminating misleading wraparound regions.









I also ran the same pyramid NCC method on three additional images selected for the project.



Large low-detail areas and repetitive patterns can yield a deceptively good score for the wrong displacement. Interior cropping, a bounded search window, and edge maps reduce this failure mode.
The first pyramid implementation failed because the border crop was too large—sometimes 70–80 pixels per side—and the search parameters were too narrow. Reducing the crop to 5–10 pixels, increasing the coarse search from ±30 to ±40, increasing local refinement from ±5 to ±8, and lowering the pyramid cutoff from 250 to 200 pixels improved convergence.
Implementation note: three_generations.tif could not be processed in the batch run and had to be run individually.
The report references cathedral, lugano, and three_generations outputs, but their image files are not present in the local web/assets folder. This page displays every available local result image.