"""Interactive rainbow visualizer"""
from collections import Counter
import sys
import tkinter as tk
try:
import numpy as np
except ImportError:
print("Error: numpy is missing. Install it with: pip install numpy")
sys.exit(1)
try:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
except ImportError:
print("Error: matplotlib is missing. Install it with: pip install matplotlib")
sys.exit(1)
try:
from colour import MSDS_CMFS, XYZ_to_sRGB
from colour.models import eotf_inverse_sRGB
except ImportError:
print("Error: colour-science is missing. Install it with: pip install colour-science")
sys.exit(1)
def wavelength_to_rgb(wavelength, r_boost=1/1.4, g_boost=1/1.4, b_boost=1/1.4,
return_clipping=False):
"""Convert wavelength to rgb tuple; optionally with clipping info."""
cmfs = MSDS_CMFS['CIE 2015 2 Degree Standard Observer']
if wavelength < 360 or wavelength > 830:
if return_clipping:
return (0.0, 0.0, 0.0), False
return (0.0, 0.0, 0.0)
wl_range = cmfs.wavelengths
x_bar = np.interp(wavelength, wl_range, cmfs.values[:, 0])
y_bar = np.interp(wavelength, wl_range, cmfs.values[:, 1])
z_bar = np.interp(wavelength, wl_range, cmfs.values[:, 2])
X = x_bar
Y = y_bar
Z = z_bar
XYZ = np.array([X, Y, Z])
rgb_linear = XYZ_to_sRGB(XYZ, apply_cctf_encoding=False)
r, g, b = rgb_linear
if r < 0 or g < 0 or b < 0:
min_component = min(r, g, b)
if Y > 0:
factor = Y / (Y - min_component)
r = Y + factor * (r - Y)
g = Y + factor * (g - Y)
b = Y + factor * (b - Y)
r *= r_boost
g *= g_boost
b *= b_boost
was_clipped = False
max_component = max(r, g, b)
if max_component > 1.0:
was_clipped = True
r /= max_component
g /= max_component
b /= max_component
r = max(0.0, min(1.0, r))
g = max(0.0, min(1.0, g))
b = max(0.0, min(1.0, b))
# gamma correct me, baby
r = eotf_inverse_sRGB(r)
g = eotf_inverse_sRGB(g)
b = eotf_inverse_sRGB(b)
if return_clipping:
return (r, g, b), was_clipped
return (r, g, b)
def visualize_rainbow():
"""Interactive Rainbow visualizer."""
root = tk.Tk()
root.title("Rainbow Visualizer")
root.geometry("1200x800")
root.rowconfigure(0, weight=1)
root.rowconfigure(1, weight=0)
root.columnconfigure(0, weight=1)
fig = Figure(figsize=(10, 10))
gs = fig.add_gridspec(3, 1, height_ratios=[1, 1, 0.5])
ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[1, 0])
ax3 = fig.add_subplot(gs[2, 0])
canvas = FigureCanvasTkAgg(fig, master=root)
canvas_widget = canvas.get_tk_widget()
canvas_widget.grid(row=0, column=0, sticky='nsew', padx=5, pady=5)
def remove_canvas_padding():
canvas_widget.grid_configure(padx=0, pady=0)
control_frame = tk.Frame(root)
control_frame.grid(row=1, column=0, sticky='ew', padx=10, pady=10)
r_var = tk.DoubleVar(value=1/1.4)
g_var = tk.DoubleVar(value=1/1.4)
b_var = tk.DoubleVar(value=1/1.4)
def on_mousewheel(event, var):
current = var.get()
increment = 0.1 if event.state & 0x0001 else 0.01
if event.delta > 0 or event.num == 4:
var.set(round(min(5.0, current + increment), 3))
elif event.delta < 0 or event.num == 5:
var.set(round(max(0.0, current - increment), 3))
update_plot()
def update_plot(*_args):
r_boost = r_var.get()
g_boost = g_var.get()
b_boost = b_var.get()
wavelengths = np.linspace(360, 800, 881)
results = [wavelength_to_rgb(wl, r_boost, g_boost, b_boost, return_clipping=True)
for wl in wavelengths]
rgb_values = np.array([rgb for rgb, _ in results])
clipping_flags = np.array([clipped for _, clipped in results])
ax1.clear()
ax2.clear()
ax3.clear()
rainbow_array = rgb_values.reshape(1, -1, 3)
ax1.imshow(rainbow_array, aspect='auto',
extent=[wavelengths[0], wavelengths[-1], 0, 1],
origin='lower', interpolation='nearest')
ax1.set_xlim(360, 800)
ax1.set_ylim(0, 1)
ax1.set_xlabel('Wavelength (nm)')
ax1.set_ylabel('Intensity')
ax1.set_title('Visible Spectrum')
ax2.plot(wavelengths, rgb_values[:, 0], 'r-', label='Red', linewidth=2)
ax2.plot(wavelengths, rgb_values[:, 1], 'g-', label='Green', linewidth=2)
ax2.plot(wavelengths, rgb_values[:, 2], 'b-', label='Blue', linewidth=2)
ax2.set_xlim(360, 800)
ax2.set_ylim(0, 1)
ax2.set_xlabel('Wavelength (nm)')
ax2.set_ylabel('RGB Component Value')
ax2.set_title('RGB Components across Spectrum')
ax2.legend()
ax2.grid(True, alpha=0.3)
rgb_tuples = [tuple(rgb) for rgb in rgb_values]
rgb_counts = Counter(rgb_tuples)
duplicate_wavelengths = [wavelengths[i] for i, rgb in enumerate(rgb_tuples)
if rgb_counts[rgb] > 1]
banding_mask = np.zeros(len(wavelengths))
clipping_mask = clipping_flags.astype(float)
for i, rgb in enumerate(rgb_tuples):
if rgb_counts[rgb] > 1:
banding_mask[i] = 1.0
ax3.fill_between(wavelengths, 0, clipping_mask, color='orange', alpha=0.6,
label=f'RGB Clipping ({int(np.sum(clipping_mask))} wavelengths)')
ax3.fill_between(wavelengths, 0, banding_mask, color='red', alpha=0.6,
label=f'Duplicate RGB ({len(duplicate_wavelengths)} wavelengths)')
ax3.set_xlim(360, 800)
ax3.set_ylim(0, 1.1)
ax3.set_xlabel('Wavelength (nm)')
ax3.set_ylabel('Banding')
ax3.set_title('Wavelengths with Duplicate RGB Values and Clipping')
ax3.legend()
ax3.grid(True, alpha=0.3)
fig.set_layout_engine('compressed')
canvas.draw()
tk.Label(control_frame, text="Red Boost:", fg='red').grid(
row=0, column=0, padx=5, sticky='e')
r_spinbox = tk.Spinbox(control_frame, from_=0.0, to=5.0, increment=0.01,
textvariable=r_var, command=update_plot, width=10)
r_spinbox.grid(row=0, column=1, padx=5)
r_spinbox.bind('<Return>', update_plot)
r_spinbox.bind('<FocusOut>', update_plot)
r_spinbox.bind('<MouseWheel>', lambda e: on_mousewheel(e, r_var))
r_spinbox.bind('<Button-4>', lambda e: on_mousewheel(e, r_var))
r_spinbox.bind('<Button-5>', lambda e: on_mousewheel(e, r_var))
tk.Label(control_frame, text="Green Boost:", fg='green').grid(
row=0, column=2, padx=5, sticky='e')
g_spinbox = tk.Spinbox(control_frame, from_=0.0, to=5.0, increment=0.01,
textvariable=g_var, command=update_plot, width=10)
g_spinbox.grid(row=0, column=3, padx=5)
g_spinbox.bind('<Return>', update_plot)
g_spinbox.bind('<FocusOut>', update_plot)
g_spinbox.bind('<MouseWheel>', lambda e: on_mousewheel(e, g_var))
g_spinbox.bind('<Button-4>', lambda e: on_mousewheel(e, g_var))
g_spinbox.bind('<Button-5>', lambda e: on_mousewheel(e, g_var))
tk.Label(control_frame, text="Blue Boost:", fg='blue').grid(
row=0, column=4, padx=5, sticky='e')
b_spinbox = tk.Spinbox(control_frame, from_=0.0, to=5.0, increment=0.01,
textvariable=b_var, command=update_plot, width=10)
b_spinbox.grid(row=0, column=5, padx=5)
b_spinbox.bind('<Return>', update_plot)
b_spinbox.bind('<FocusOut>', update_plot)
b_spinbox.bind('<MouseWheel>', lambda e: on_mousewheel(e, b_var))
b_spinbox.bind('<Button-4>', lambda e: on_mousewheel(e, b_var))
b_spinbox.bind('<Button-5>', lambda e: on_mousewheel(e, b_var))
for i in range(6):
control_frame.columnconfigure(i, weight=1)
update_plot()
root.after(100, remove_canvas_padding)
root.mainloop()
if __name__ == '__main__':
visualize_rainbow()
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.