Images of the Russian Empire

Colorizing the Prokudin-Gorskii photo collection

In this project, we develop an automated alignment algorithm using image pyramids to align the three color channels of images in the Library of Congress's Prokudin-Gorskii photo collection. Sergei Mikhailovich Prokudin-Gorskii was the first color photographer, and used an invention of his own genious design to snap three simultanous photographs, each of which used different color filters. When combined, these three apparently monochrome photographs produce stunning images of the world in color.

Image Pyramid

The challenge, however, lies in aligning the three photos, each of which would have been taken at a slightly different angle. Prokudin-Gorskii did so by hand, but today we can produce better results using automated image alignment algorithms. In this lab, we will demonstrate one such algorithm: the image pyramid.

1. Warming up

The Prokudin-Gorskii photo collection represents the first color photography

Import libraries

In [27]:
%pylab inline
%load_ext autoreload
%autoreload 2

import skimage as sk
import skimage.io as skio

from plotting import *
from align import *
from utils import *
Populating the interactive namespace from numpy and matplotlib
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

Read in the Bukhara Emir and plot the three channels

In [12]:
emir = sk.img_as_float(read_pg_img('data/emir.tif'))
print(emir.shape)
(3209, 3702, 3)
In [13]:
plot_3_channels(emir)

Combine the three channels without alignment

In [29]:
imshow(emir)
show()

2. Single-scale alignment

In [47]:
im = sk.img_as_float(read_pg_img('data/monastery.jpg'))
print(im.shape)
(341, 391, 3)
In [58]:
plot_3_channels(im)

2.1 Manually adjust the red channel and compare with the unaligned version:

In [62]:
r_translated = translate_img(im[:,:,0], translation=(0,-10))
im_align = np.dstack([r_translated, im[:,:,1:]])

ssd_realign = ssd_loss(r_translated, im[:,:,1])
ncc_realign = 1 - ncc_loss(r_translated, im[:,:,1])

ssd_orig = ssd_loss(im[:,:,0], im[:,:,1])
ncc_orig = 1 - ncc_loss(im[:,:,0], im[:,:,1])

plot_comparison(
    im, im_align,
    title1 = f'Before: SSD={ssd_orig:0.3f}, NCC={ncc_orig:0.3f}',
    title2 = f'After: SSD={ssd_realign:0.3f}, NCC={ncc_realign:0.3f}'
)

2.2 Use an automated alignment procedure to do the same

Using SSD as loss:

In [75]:
r_align = align_by_grid_search(im[:,:,0], im[:,:,1])
im_align = np.dstack([r_align, im[:,:,1:]])

ssd_realign = ssd_loss(r_align, im[:,:,1])
ncc_realign = 1 - 2*ncc_loss(r_align, im[:,:,1])

plot_comparison(
    im, im_align,
    title1 = f'Before: SSD={ssd_orig:0.3f}, NCC={ncc_orig:0.3f}',
    title2 = f'After: SSD={ssd_realign:0.3f}, NCC={ncc_realign:0.3f}'
)
ssd_loss: 3429.648

Using NCC in loss:

In [74]:
r_align_ncc = align_by_grid_search(im[:,:,0], im[:,:,1], loss_fn=ncc_loss)
im_align_ncc = np.dstack([r_align_ncc, im[:,:,1:]])

ssd_realign2 = ssd_loss(r_align_ncc, im[:,:,1])
ncc_realign2 = 1 - 2*ncc_loss(r_align_ncc, im[:,:,1])

plot_comparison(
    im, im_align_ncc,
    title1 = f'Before: SSD={ssd_orig:0.3f}, NCC={ncc_orig:0.3f}',
    title2 = f'After: SSD={ssd_realign:0.3f}, NCC={ncc_realign2:0.3f}'
)
ncc_loss: 0.096

2.3 Align all three channels

In [102]:
%%time
im_align = align_img(im, ref=1)
ssd_loss: 3429.648
ssd_loss: 4502.267
CPU times: user 6.71 s, sys: 0 ns, total: 6.71 s
Wall time: 6.71 s
In [103]:
plot_comparison(im, im_align)

Try a larger alignment grid

In [104]:
%%time
im_align = align_img(im, ref=1, window=(-30,30))
ssd_loss: 3429.648
ssd_loss: 4502.267
CPU times: user 6.64 s, sys: 0 ns, total: 6.64 s
Wall time: 6.64 s
In [105]:
plot_comparison(im, im_align)

2.4 Crop edges

Uniform cropping

In [108]:
im_align = align_img(crop_img_edges(im, pixels=(20,20,20,20)), ref=1)
plot_comparison(im, im_align)
ssd_loss: 3093.035
ssd_loss: 2125.403

3. Image Pyramid

3.1 Try the image pyramid on emir.tif

In [14]:
emir_cropped = crop_img_edges(sk.img_as_float(emir), pixels=(200,200,200,200))
In [48]:
%%time
# TODO: try ncc_loss
emir_aligned = align_pyramid(emir_cropped, debug=True)
G realignment: [0 2]
B realignment: [0 2]
G realignment: [ 4 14]
B realignment: [0 4]
G realignment: [ 8 28]
B realignment: [0 8]
Final G channel offset: [16 56]
Final B channel offset: [ 0 18]
CPU times: user 30.2 s, sys: 7.26 s, total: 37.5 s
Wall time: 50.3 s

3.2 Try NCC loss

In [49]:
%%time
emir_ncc = align_pyramid(emir_cropped, loss_fn=ncc_loss)
Final G channel offset: [16 56]
Final B channel offset: [ 0 32]
CPU times: user 25 s, sys: 3.61 s, total: 28.6 s
Wall time: 28.6 s
In [24]:
plot_comparison(emir_ncc, emir_aligned, title1="Using SSD loss", title2="Using NCC loss")

To my eyes, the SSD and NCC losses produce nearly identical results.

4. Other images

Castle

In [50]:
%%time
castle = read_and_crop_pg_img('data/castle.tif')
castle_aligned = align_pyramid(castle)
plot_comparison(castle, castle_aligned)
Final G channel offset: [ 0 64]
Final B channel offset: [ 4 98]
CPU times: user 24.9 s, sys: 4.39 s, total: 29.3 s
Wall time: 29.4 s

Harvesters

In [51]:
%%time
harvesters = read_and_crop_pg_img('data/harvesters.tif')
harvesters_aligned = align_pyramid(harvesters)
plot_comparison(harvesters, harvesters_aligned)
Final G channel offset: [-2 64]
Final B channel offset: [0 0]
CPU times: user 25.2 s, sys: 5.34 s, total: 30.5 s
Wall time: 34.3 s

Icon

In [52]:
%%time
icon = read_and_crop_pg_img('data/icon.tif')
icon_aligned = align_pyramid(icon)
plot_comparison(icon, icon_aligned)
Final G channel offset: [ 6 48]
Final B channel offset: [22 88]
CPU times: user 25.7 s, sys: 4.47 s, total: 30.2 s
Wall time: 30.7 s

Lady

In [53]:
%%time
lady = read_and_crop_pg_img('data/lady.tif')
lady_aligned = align_pyramid(lady)
plot_comparison(lady, lady_aligned)
Final G channel offset: [ 4 60]
Final B channel offset: [  8 112]
CPU times: user 21.5 s, sys: 9.97 s, total: 31.5 s
Wall time: 31.5 s

Melons

In [54]:
%%time
melons = read_and_crop_pg_img('data/melons.tif')
melons_aligned = align_pyramid(melons)
plot_comparison(melons, melons_aligned)
Final G channel offset: [ 4 96]
Final B channel offset: [ 12 176]
CPU times: user 23.5 s, sys: 29 s, total: 52.5 s
Wall time: 52.5 s

Onion Church

In [55]:
%%time
onion_church = read_and_crop_pg_img('data/onion_church.tif')
onion_church_aligned = align_pyramid(onion_church)
plot_comparison(onion_church, onion_church_aligned)
Final G channel offset: [10 56]
Final B channel offset: [ 36 108]
CPU times: user 23.6 s, sys: 12 s, total: 35.6 s
Wall time: 35.6 s

Self-portrait

In [56]:
%%time
self_portrait = read_and_crop_pg_img('data/self_portrait.tif')
self_portrait_aligned = align_pyramid(self_portrait)
plot_comparison(self_portrait, self_portrait_aligned)
Final G channel offset: [ 8 98]
Final B channel offset: [ 36 176]
CPU times: user 24.8 s, sys: 4.97 s, total: 29.8 s
Wall time: 29.8 s

Three generations

In [57]:
%%time
three_generations = read_and_crop_pg_img('data/three_generations.tif')
three_generations_aligned = align_pyramid(three_generations)
plot_comparison(three_generations, three_generations_aligned)
Final G channel offset: [-2 58]
Final B channel offset: [ 8 26]
CPU times: user 24.8 s, sys: 4.48 s, total: 29.3 s
Wall time: 29.4 s

Train

In [58]:
%%time
train = read_and_crop_pg_img('data/train.tif')
train_aligned = align_pyramid(train)
plot_comparison(train, train_aligned)
Final G channel offset: [26 44]
Final B channel offset: [ 0 12]
CPU times: user 25.9 s, sys: 5.35 s, total: 31.2 s
Wall time: 31.9 s

Workshop

In [59]:
%%time
workshop = read_and_crop_pg_img('data/workshop.tif')
workshop_aligned = align_pyramid(workshop)
plot_comparison(workshop, workshop_aligned)
Final G channel offset: [-12  52]
Final B channel offset: [-8  8]
CPU times: user 26.5 s, sys: 5.42 s, total: 31.9 s
Wall time: 37.7 s

Some images I chose

Stained glass

In [60]:
%%time
stained_glass = read_and_crop_pg_img('data/stained_glass.tif')
stained_glass_aligned = align_pyramid(stained_glass)
plot_comparison(stained_glass, stained_glass_aligned)
Final G channel offset: [ 4 38]
Final B channel offset: [16 60]
CPU times: user 23.7 s, sys: 4.26 s, total: 27.9 s
Wall time: 28 s

Cathedral roof

In [61]:
%%time
cathedral_roof = read_and_crop_pg_img('data/cathedral_roof.tif')
cathedral_roof_aligned = align_pyramid(cathedral_roof)
plot_comparison(cathedral_roof, cathedral_roof_aligned)
Final G channel offset: [10 68]
Final B channel offset: [24 84]
CPU times: user 26.7 s, sys: 5.11 s, total: 31.8 s
Wall time: 32.1 s

Cliff

In [62]:
%%time
cliff = read_and_crop_pg_img('data/cliff.tif')
cliff_aligned = align_pyramid(cliff)
plot_comparison(cliff, cliff_aligned)
Final G channel offset: [  0 -14]
Final B channel offset: [0 0]
CPU times: user 26 s, sys: 5.94 s, total: 31.9 s
Wall time: 34.6 s
In [ ]: