Scikit Image Processing

Published June 3, 2022

Tags: Python Pyscript Scikit

A very simple demo of exploring the Scikit-image package in-browser using Pyscript. Inspired by Jan-Hendrik_Muller over on the Pyscript forum.

Please be patient, Pyscript and fetching each Emoji will take a moment.

- Pillow - matplotlib - scikit-image - numpy

Select an Emoji:

Select a Filter:

Original Image

Processed Image

Here is the source for the examples running above. Some details of formatting and CSS of on this page have bee omitted below for clarity. (Feel free to use View Source if you're like to look at the details)

index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<py-env>
- Pillow
- matplotlib
- scikit-image
- numpy
</py-env>

<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
<py-script src="./emoji_playground.py"></py-script>

<select id="emoji-selector">
    <option value="🤖">🤖</option>
    <option value="👨‍🎨">👨‍🎨</option>
    <option value="🧠">🧠</option>
    <option value="🦞">🦞</option>
    <option value="🌯">🌯</option>
    <option value="🎭">🎭</option>
  </select>

<p class="underline">Original Image</p>
<div id="original_image"></div>
<p class="underline">Processed Image</p>
<div id="new_image"></div>

emoji_playground.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from PIL import Image

from js import document, console, Uint8Array, window, File
from pyodide import create_proxy
from pyodide.http import pyfetch
import asyncio
import io
import numpy as np
from numpy import asarray
from functools import partial

from skimage.transform import swirl, PiecewiseAffineTransform, warp
from skimage.filters import butterworth

current_emoji = "🤖"
current_filter_name = "swirl"

emoji_data: dict[str, np.array] = {}

def swirl_filter(my_array: np.array) -> np.array:
    return swirl(my_array, rotation = 0, strength = 15, radius = 300)

def affine_filter(my_array: np.array) -> np.array:
    rows, cols = my_array.shape[0], my_array.shape[1]

    src_cols = np.linspace(0, cols, 20)
    src_rows = np.linspace(0, rows, 10)
    src_rows, src_cols = np.meshgrid(src_rows, src_cols)
    src = np.dstack([src_cols.flat, src_rows.flat])[0]

    # add sinusoidal oscillation to row coordinates
    dst_rows = src[:, 1] - np.sin(np.linspace(0, 3 * np.pi, src.shape[0])) * 50
    dst_cols = src[:, 0]
    dst_rows *= 1.5
    dst_rows -= 1.5 * 50
    dst = np.vstack([dst_cols, dst_rows]).T


    tform = PiecewiseAffineTransform()
    tform.estimate(src, dst)

    out_rows = my_array.shape[0] - 1.5 * 50
    out_cols = cols
    return warp(my_array, tform, output_shape=(out_rows, out_cols))

def butterworth_filter(my_array: np.array, frequency = 0.1, high_pass=False, order=8.0) -> np.array:
    return butterworth(my_array, frequency, high_pass=high_pass, order=order)

filter_names = {
    "swirl": swirl_filter,
    "affine": affine_filter,
    "butterworth_low": partial(butterworth_filter, high_pass=False, order=8.0),
    "butterworth_high": partial(butterworth_filter, frequency = 0.01, high_pass=True, order=8.0)
}

async def get_emoji_bytes(url: str):
    response = await pyfetch(url)
    if response.status == 200:
        return await response.bytes()

async def _select_emoji_and_display(e):
    global current_emoji
    current_emoji = e.target.value
    await _fetch_and_display()

async def _select_filter_and_display(e):
    global current_filter_name
    current_filter_name = e.target.value
    await _fetch_and_display()

async def _fetch_emoji_data(emoji_name: str) -> np.array:
    emoji_code = "-".join(f"{ord(c):x}" for c in emoji_name).upper()
    url = f"https://raw.githubusercontent.com/hfg-gmuend/openmoji/master/color/618x618/{emoji_code}.png"
    console.log(f"Getting emoji {emoji_name} with value {emoji_code} at url {url}")


    #BytesIO wants a bytes-like object, so convert to bytearray first
    bytes_list = bytearray(await get_emoji_bytes(url))
    my_bytes = io.BytesIO(bytes_list) 

    #Create PIL image from BytesIO 
    my_image = Image.open(my_bytes)

    #Convert to an np-array to allow for processing
    return np.array(my_image.convert()) # convert() is key, as these images use a pallete!!

async def _emoji_data(emoji_name: str) -> np.array:
    if emoji_name not in emoji_data:
        emoji_data[emoji_name] = await _fetch_emoji_data(emoji_name)
    
    return emoji_data[emoji_name]

def array_to_image(data:np.array) -> Image:
    if data[row:= 0][column:= 0][red:= 0] < .99:
        # Many transforms represent RGB as floats in the range 0-1, which pillow does not like
        # This converts their values back to 0-255
        return Image.fromarray((data*255).astype(np.uint8)) 
    else:
        return Image.fromarray(data.astype(np.uint8))

async def _fetch_and_display():
    # Get an emoji image from cache or fetch from web
    emoji_data = await _emoji_data(current_emoji)

    #Image Processing
    my_array = filter_names[current_filter_name](emoji_data)

    #convert back to Pillow image:
    my_image = array_to_image(my_array)

    #Export image from Pillow as bytes to get to Javascript
    my_processed_stream = io.BytesIO()
    my_image.save(my_processed_stream, format="PNG")
    processed_image_file = File.new([Uint8Array.new(my_processed_stream.getvalue())], "new_image_file.png", {type: "image/png"})

    #Export image from 
    my_original_stream = io.BytesIO()
    original_data = await _emoji_data(current_emoji)
    original_image = array_to_image(emoji_data)
    original_image.save(my_original_stream, format="PNG")
    original_image_file = File.new([Uint8Array.new(my_original_stream.getvalue())], "new_image_file.png", {type: "image/png"})

    #remove all children from divs:
    remove_all_children("new_image")
    remove_all_children("original_image")

    #Create new tags and insert into page
    new_image = document.createElement('img')
    new_image.src = window.URL.createObjectURL(processed_image_file)
    document.getElementById("new_image").appendChild(new_image)

    original_image = document.createElement('img')
    original_image.classList.add("w-auto")
    original_image.src = window.URL.createObjectURL(original_image_file)
    document.getElementById("original_image").appendChild(original_image)

select_emoji_and_display = create_proxy( _select_emoji_and_display)
document.getElementById("emoji-selector").addEventListener("change",  select_emoji_and_display)

select_filter_and_display = create_proxy( _select_filter_and_display)
document.getElementById("filter-selector").addEventListener("change",  select_filter_and_display)

def remove_all_children(parent_id: str):
    parent = document.getElementById(parent_id)

    while parent.firstChild is not None:
        parent.removeChild(parent.firstChild)

await _fetch_and_display()

x=1 #Prevents an apparent error of Pyscript trying to write its final value to the DOM

Scroll to see complete code