RSSAmplifier

Wejn.org · Jan 14, 2026

Visualize those pesky rainbows (interactively)

0
Sign in to vote or save

Michal Jirků · Wejn.org

"""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()

Read the original on wejn.org

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.