How To Create Your Own Widgets

This notebook provides a simple tutorial on how to create your own widgets using MenpoWidgets, similar to the ones in Menpo Widgets.ipynb and MenpoFit Widgets.ipynb. New widgets can be synthesized using the components presented in Options Widgets.ipynb. So before continuing with this notebook, please go through that one first.

The widget we create here is very simple since the purpose of this tutorial is to provide a basic introduction. The aim of the widget will be to go through a list of images and crop them with respect to a proportion of their landmarks. The crop proportion will be controlled by the user using a slider. There will also be some information printing regarding each image's size.

We split the presentation in the following steps:

  1. Import Images
  2. Required Widgets
  3. Widgets Grouping and Styling
  4. Define Render Function
  5. Final Widget Function

1. Import Images

Firstly, we need to import a list of images. Let's import the first 20 images of LFPW's testset and convert them to grayscale.


In [1]:
%matplotlib inline
import menpo.io as mio

path_to_lfpw = '/vol/atlas/databases/lfpw/'

images = []
for i in mio.import_images(path_to_lfpw + 'testset/*', max_images=20, verbose=True):
    # convert it to greyscale if needed
    if i.n_channels == 3:
        i = i.as_greyscale(mode='luminosity')
    # append it to the list
    images.append(i)


Found 20 assets, index the returned LazyList to import.

2. Required Widgets

We need the AnimationOptionsWidget in order to be able to browse through the list of images. This takes a dict with the initial indexing options. Of course the maximum index value should be equal to the number of loaded images.


In [2]:
from menpowidgets.options import AnimationOptionsWidget

index = {'min': 0, 'max': len(images) - 1, 'step': 1, 'index': 0}

anim_wid = AnimationOptionsWidget(index, description='Image')
anim_wid

We also need a way to define the crop percentage. This can be done with a simple slider that selects float numbers and is bounded between 0.0 and 1.0. Thus


In [3]:
from ipywidgets import FloatSlider

prop_wid = FloatSlider(value=1., min=0., max=1., step=0.1, 
                       description='Proportion:', width='4.5cm')
prop_wid

Moreover, we can also create a TextPrintWidget for printing some info regarding the cropped image. For example


In [4]:
from menpowidgets.options import TextPrintWidget

text_per_line = ['> Original size: ', '> Final size: ']

txt_wid = TextPrintWidget(text_per_line)
txt_wid

3. Widgets Grouping and Styling

We now need to group the widgets in a single box. This can be done using ipywidgets's FlexBox and HBox.


In [5]:
from ipywidgets import FlexBox, HBox

# Create an initial box for the proportion slider and the print info widget
opts_box = HBox(children=[prop_wid, txt_wid], align='center')

# Create the final widget's box
wid = FlexBox(children=[anim_wid, opts_box])

wid

Now we can apply some styling on the final widget


In [6]:
from menpowidgets.style import map_styles_to_hex_colours

# Select some styles
final_widget_style = 'info'
print_widget_style = 'warning'
animation_widget_style = 'danger'

# Apply the selected final widget style
wid.box_style = final_widget_style
wid.border_width = 1
wid.border_color = map_styles_to_hex_colours(final_widget_style)
wid.border_radius = 10
prop_wid.slider_color = map_styles_to_hex_colours(final_widget_style)
prop_wid.background_color = map_styles_to_hex_colours(final_widget_style)

# Apply the selected print text widget style
txt_wid.predefined_style(style=print_widget_style)

# Apply the selected animation options widget style
anim_wid.predefined_style(style=animation_widget_style)
anim_wid.border_radius = 10
anim_wid.border_width = 1
anim_wid.border_color = map_styles_to_hex_colours(animation_widget_style)

4. Define Render Function

The final step is to define the rendering callback function that will get called everytime a widget value is changed. The function needs to get the image index and proportion value, copy the original image, crop it, visualize it and update the printed info. Remember that all funtions that are used as callbacks in IPython widgets need to get two arguments: name and value.


In [7]:
from IPython.display import clear_output
from matplotlib.pyplot import show as pltshow

def render_function(change):
    # Clear current figure, but wait until the generation of the new data
    # that will be rendered
    clear_output(wait=True)
    
    # Get selected image index
    i = anim_wid.selected_values
    
    # Get selected crop proportion
    prop = prop_wid.value
    
    # Get a copy of the original image
    im_copy = images[i].copy()
    
    # Crop the copied image
    im_copy = im_copy.crop_to_landmarks_proportion(prop)
    
    # Visualize it
    im_copy.view_landmarks();
    
    # Update the printed info
    text_per_line = ['> Original size: {}x{}'.format(images[i].height, images[i].width), 
                     '> Final size: {}x{}'.format(im_copy.height, im_copy.width)]
    txt_wid.set_widget_state(text_per_line=text_per_line)
    
    # Make sure that image gets rendered
    pltshow()

Let's check that it works properly


In [8]:
render_function({})


5. Final Widget Function

Let's now define a function that summarizes the above steps. It will get as input the images list and a style argument that can be either 'coloured' or 'minimal'.


In [9]:
from IPython.display import display

def crop_images_widget(images, style='coloured'):
    # Create widgets
    index = {'min': 0, 'max': len(images) - 1, 'step': 1, 'index': 0}
    anim_wid = AnimationOptionsWidget(index, description='Image')
    prop_wid = FloatSlider(value=1., min=0., max=1., step=0.1, continuous_update=False,
                           description='Proportion:', width='4.5cm')
    text_per_line = ['> Original size: ', '> Final size: ']
    txt_wid = TextPrintWidget(text_per_line=text_per_line)
    
    # Group widgets
    opts_box = HBox(children=[prop_wid, txt_wid], align='center')
    wid = FlexBox(children=[anim_wid, opts_box])
    
    # Styling
    final_widget_style = ''
    border_width = 0
    print_widget_style = 'minimal'
    animation_widget_style = 'minimal'
    if style == 'coloured':
        final_widget_style = 'info'
        print_widget_style = 'warning'
        animation_widget_style = 'danger'
        border_width = 1
    wid.box_style = final_widget_style
    wid.border_width = border_width
    wid.border_color = map_styles_to_hex_colours(final_widget_style)
    wid.border_radius = 10
    prop_wid.slider_color = map_styles_to_hex_colours(final_widget_style)
    prop_wid.background_color = map_styles_to_hex_colours(final_widget_style)
    txt_wid.predefined_style(style=print_widget_style)
    anim_wid.predefined_style(style=animation_widget_style)
    anim_wid.border_radius = 10
    anim_wid.border_width = border_width
    anim_wid.border_color = map_styles_to_hex_colours(animation_widget_style)
    
    # Define render function
    def render_function(change):
        clear_output(wait=True)
        i = anim_wid.selected_values
        prop = prop_wid.value
        im_copy = images[i].copy()
        im_copy = im_copy.crop_to_landmarks_proportion(prop)
        im_copy.view_landmarks();
        pltshow()
        text_per_line = ['> Original size: {}x{}'.format(images[i].height, images[i].width), 
                         '> Final size: {}x{}'.format(im_copy.height, im_copy.width)]
        txt_wid.set_widget_state(text_per_line=text_per_line)
        
    # Add render_function to the animation widget
    anim_wid.add_render_function(render_function)
    
    # Assign render_function to the proportion slider's value trait
    prop_wid.observe(render_function, names='value', type='change')
    
    # Display the widget
    display(wid)
    
    # Trigger its initial rendering
    render_function({})

That's it! Let's see what we designed!


In [10]:
crop_images_widget(images, style='coloured')