Rainbow effect

When I see a rainbow, I perceive the luminance inside the arc to be brighter than outside the arc. Is this effect percpetual (inside our head) or physical (inside each droplet in the sky). So, this is a simple notebook to show off how to synthesize the image of a rainbow on a realistic sky. TL;DR: there must be a physical reason for it.

Outline: The rainbow is a set of colors over a gradient of hues, masked for certain ones. The sky will be a gradient over blueish colors.

Let's first initialize the notebook:

In [1]:
%matplotlib inline
%config InlineBackend.figure_format='retina'

import numpy as np
import matplotlib.pyplot as plt
FIG_WIDTH = 15
FIGSIZE=(FIG_WIDTH, .618 * FIG_WIDTH)

Size of the images that we will generate, definition of the meshgrid:

In [2]:
N_pix = 1024
ruler = np.linspace(-1, 1, N_pix, endpoint=True)
X, Y = np.meshgrid(ruler, ruler)
print('X.shape =', X.shape)
X.shape = (1024, 1024)

Creation of a band:

In [3]:
width, X0, Y0 = .05, .0, 0.
blue2red = np.exp( -.5 * ((Y-Y0)**2)/width**2)
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow(blue2red, vmin=0, vmax=1);
No description has been provided for this image

Creation of a softmax band from zero to one:

In [4]:
blue2red = 1 / (1 + np.exp( (Y-Y0)/width) ) 
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow(blue2red, vmin=0, vmax=1);
No description has been provided for this image

Bending the band around a parabola:

In [5]:
width, X0, Y0, radius = .02, .0, -.25, 1.5
blue2red = 1 / (1 + np.exp( (- (X-X0)**2/radius**2 + Y-Y0)/width) ) 
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
cmap = ax.imshow(blue2red, vmin=0, vmax=1)
fig.colorbar(mappable=cmap);
No description has been provided for this image

Converting to a RGB image:

In [6]:
from matplotlib.colors import hsv_to_rgb
help(hsv_to_rgb)
Help on function hsv_to_rgb in module matplotlib.colors:

hsv_to_rgb(hsv)
    convert hsv values in a numpy array to rgb values
    all values assumed to be in range [0, 1]
    
    Parameters
    ----------
    hsv : (..., 3) array-like
       All values assumed to be in range [0, 1]
    
    Returns
    -------
    rgb : (..., 3) ndarray
       Colors converted to RGB values in range [0, 1]

Empirically defining the starting and ending hues in a rainbow:

In [7]:
h_start, h_stop = .8, .0
hue = h_start+(h_stop-h_start)*blue2red
print('hue.min()', hue.min(), ', hue.max()', hue.max())
hsv = np.dstack((hue, np.ones_like(hue), np.ones_like(hue)))
print('hsv.shape =', hsv.shape)
rainbow = hsv_to_rgb(hsv)
print('rainbow.min()', rainbow.min(), ', rainbow.max()', rainbow.max())
print('rainbow.shape =', rainbow.shape)
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow(rainbow, vmin=0, vmax=1)
hue.min() 0.0 , hue.max() 0.8
hsv.shape = (1024, 1024, 3)
rainbow.min() 0.0 , rainbow.max() 1.0
rainbow.shape = (1024, 1024, 3)
Out[7]:
<matplotlib.image.AxesImage at 0x1204d6908>
No description has been provided for this image

Creating a simple mask:

In [8]:
mask = blue2red * (1-blue2red)
mask /= mask.max()
hsv = np.dstack((hue, np.ones_like(hue), mask))
print('hsv.shape =', hsv.shape)
from matplotlib.colors import hsv_to_rgb
rainbow = hsv_to_rgb(hsv)
print('rainbow.shape =', rainbow.shape)
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow(rainbow, vmin=0, vmax=1);
hsv.shape = (1024, 1024, 3)
rainbow.shape = (1024, 1024, 3)
No description has been provided for this image

Now assembling this image to that of a background sky:

In [9]:
L_mean = .7
sky_contrast = .6
blue = np.array([0.1+L_mean, 0.05+L_mean, 1.])
image = (.5 + .5 * sky_contrast * Y)[:, :, None] * blue[None, None, :]
print('image.shape =', image.shape)
print('image.min()', image.min(), ', image.max()', image.max())

fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow(image, vmin=0, vmax=1);
image.shape = (1024, 1024, 3)
image.min() 0.15000000000000002 , image.max() 0.8
No description has been provided for this image
In [10]:
print(image.min(), image.max())
0.15000000000000002 0.8
In [11]:
rainbow_contrast = .5
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow((1.-rainbow_contrast)*image+rainbow_contrast*rainbow, vmin=0, vmax=1);
No description has been provided for this image

Quite realistic, no? Still, there is little effect at a high contrast for the rainbow. I do not perceive a "rainbow effect", so most likely I would predict there must be a physical reason for it.

Yet...

In [12]:
rainbow_contrast = .1
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow((1.-rainbow_contrast)*image+rainbow_contrast*rainbow, vmin=0, vmax=1);
No description has been provided for this image

There is a little effect at low contrast...

Take a picture found on the net (Hi, https://www.laniadvokat.com/blog/2018/7/24/lake-city-to-molas-pass !), with this nice double rainbow:

In [13]:
import imageio
image = imageio.imread('https://static1.squarespace.com/static/58efb414f7e0abff4dbfc0c4/t/5b5ff2f3aa4a99799747f7eb/1533014789574/IMG_3775.JPG')
print('image.shape =', image.shape)
print('image.min()', image.min(), ', image.max()', image.max())
fig, ax = plt.subplots(figsize=(FIG_WIDTH, FIG_WIDTH))
ax.imshow(image, vmin=0, vmax=255);
image.shape = (1333, 1000, 3)
image.min() 0 , image.max() 255