balthazarneveu / interactive_pipe

create interactive image processing pipeline with friendly sliders
The Unlicense
5 stars 1 forks source link

clean declaration of filters with controls #35

Open balthazarneveu opened 1 year ago

balthazarneveu commented 1 year ago

Using the interactive decorator

WARNING

In notebooks, people tend to execute the same cell several times, you can't declare the same interactive_filters several times :-1: The trick is to reset the registered_controls_names list. from interactive_pipe.helper import _private _private.registered_controls_names = []

Preliminary checks

kb = KeyboardControl(value_default=0, value_range=[0, 2], keydown="pagedown", keyup="pageup", modulo=True)

@interactive(image_index=kb)
def switch_image(img1, img2, img3, image_index=0):
    return [img1, img2, img3][image_index]
@interactive()
def switch_image(img1, img2, img3, image_index=kb):
    return [img1, img2, img3][image_index]

@interactive will

You probably can't have twice the same slider name.


MAJOR BUG RELATED #18 There was no update_parameters_from_controls method called in __run in the HeadlessPipeline class

def update_parameters_from_controls(self):
        for ctrl in self.controls:
            logging.info(f"{ctrl.filter_to_connect.name}, {ctrl.parameter_name_to_connect}, {ctrl.value}")
            self.parameters = {ctrl.filter_to_connect.name: {ctrl.parameter_name_to_connect:  ctrl.value}}

:exclamation: This whole thing worked thanks to the decorator which keeps all sliders & applies .values before running the function. This is a nice trick to be able to use the decorated function "as-is" but it hides some bugs.

When moving on to using filters, you actually get "non responsive" sliders.

TODOS

balthazarneveu commented 1 year ago

c0ef1d4f9fa564331ac636d58bd1e405d419352c forbid the declaration of multiple sliders using the same name. :warning: Note that using a unique id defined internally instead of a user defined (or autodefined name) would allow general handling (we could warn the user but still get two sliders with the same "pretty name")

:warning: If you remove name="image_index_2" , you will get an assert

from interactive_pipe import interactive, interactive_pipeline, pipeline, Control, KeyboardControl
from interactive_pipe.helper.decorator import filter_from_function, get_interactive_pipeline_class
import numpy as np

COLOR_DICT = {"red": [1., 0., 0.],  "green": [0., 1.,0.], "blue": [0., 0., 1.], "gray": [0.5, 0.5, 0.5]}
@interactive()
def generate_flat_colored_image_list():
    '''Generate a constant colorful image
    '''
    flat_list = []
    for color_choice, color_val in COLOR_DICT.items():
        flat_list.append (np.array(color_val) * np.ones((64, 64, 3)))
    return flat_list

kb1 = KeyboardControl(value_default=0, value_range=[0, 2], keydown="pagedown", keyup="pageup", modulo=True)
@interactive(image_index=kb1)
def switch_image_1(img1, img2, img3, image_index=0):
    return [img1, img2, img3][image_index]

kb2 = KeyboardControl(value_default=0, value_range=[0, 2], name="image_index_2", keydown="down", keyup="up", modulo=True)
@interactive(image_index=kb2)
def switch_image_2(img1, img2, img3, image_index=0):
    return [img1, img2, img3][image_index]

@interactive_pipeline(gui="qt")
def sample_pipeline_generated_image():
    red, green, blue, gray = generate_flat_colored_image_list()
    chosen = switch_image_1(red, green, blue)
    chosen2 = switch_image_2(red, green, blue)
    return chosen, chosen2

sample_pipeline_generated_image()
balthazarneveu commented 1 year ago

A simple filter should be "testable" with a graphical interface....

basically like interact in iPywidget Solution Use the @interact decorator

Write a simple "library".

Example

Library like definition

def generate_sine_wave(
    frequency=1,
    phase=0.,
):
# THIS MIMICKS A PURE LIBRARY. generate_sine_wave will stay untouched
    x = np.linspace(0., 1., 100)
    crv = Curve(
        [
            [
                x,
                np.cos(2.*np.pi*frequency*x+np.deg2rad(phase)),
                "k-", 
                f"sinewave {frequency:.1f}Hz\nphase={int(phase):d}°"
            ], 
            [x, np.cos(2.*np.pi*frequency*x), "g--", f"sinewave {frequency:.1f}Hz"],
        ],
        xlabel="time [s]", 
        ylabel="value",
        grid=True,
        title="Oscillator"
    )
    return crv

Then in another cell

frequency_slider = (1., [0.1, 10.], "freq [Hz]")
phase_slider = (90, [-180, 180], "phase [°]")
interact(frequency=frequency_slider, phase=phase_slider, gui="nb")(generate_sine_wave)

If you check generate_sine_wave(frequency=5, phase=10).show() , you'll see a regular static plot, meaning that your original function is still there, left untouched!

This is very similar to Interact in ipywidget

Sliders in keyword args

This is sort of the dirty way to define things, but it works. But you can't use the oscillator function as is...

@interact
def oscillator(
    frequency=(1., [0.1, 10.], "freq [Hz]"),
    phase=(90, [-180, 180], "phase [°]")
):
    x = np.linspace(0., 1., 100)
    crv = Curve(
        [
            [
                x,
                np.cos(2.*np.pi*frequency*x+np.deg2rad(phase)),
                "k-", 
                f"sinewave {frequency:.1f}Hz\nphase={int(phase):d}°"
            ], 
            [x, np.cos(2.*np.pi*frequency*x), "g--", f"sinewave {frequency:.1f}Hz"],
        ],
        xlabel="time [s]", 
        ylabel="value",
        grid=True,
        title="Oscillator"
    )
    return crv

# you should see a GUI appear now!

Now, you can still run oscillator afterward like oscillator(frequency=5., phase=80.).show() .

But keep in mind that you can't run oscillator() just like that..

because the default arguments are tuples or Controls ... which are not supported by the pure function. So you need to pass the keyword args you want.

Specifying the sliders in the interact decorator

@interact(
    frequency = (1., [0.1, 10.], "freq [Hz]"),
    phase = (17, [-180, 180], "phase [°]")
)
def oscillator_func( frequency=4.2, phase=42,):

Now you can even run the function again oscillator_func().show() (will use frequency=4.2, phase=42)

Conclusion

This is a nice little decorator which allows you performing an almost "unitary" check on each filter definition, without spoiling the code. You could simply leave the default values in your code for the sliders & comment/uncomment the decorator or use disable=True

You can also use a dedicated file for interactive "testing".

import generate_sine_wave
from interactive_pipe import interact

frequency_slider = (8., [0.1, 10.], "freq [Hz]")
phase_slider = (90, [-180, 180], "phase [°]")
interact(frequency=frequency_slider, phase=phase_slider, gui="nb")(generate_sine_wave)

in another file...