Note
Go to the end to download the full example code.
A Method to Subtract Background from an Image
This example shows how to subtract a background from an image.
The background is created via the mipmap levels of the image so that the background does not need to be computed per pixel during each render cycle. The mipmap level is sampled from to create the background.
One can adjust the background level via the dropdown menu. Each level subtracts a subsampled background from the image with the subsampling increasing by a power of 2 with each level. Level 0 is the original image with no background subtraction.

Imageio: 'hubble_deep_field.png' was not found on your computer; downloading it now.
Try 1. Download from https://github.com/imageio/imageio-binaries/raw/master/images/hubble_deep_field.png (1.6 MB)
Downloading: 8192/1668706 bytes (0.5%)1668706/1668706 bytes (100.0%)
Done
File saved as /home/docs/.imageio/images/hubble_deep_field.png.
import imageio.v3 as iio
import numpy as np
from rendercanvas.auto import RenderCanvas, loop
import pygfx as gfx
from pygfx.renderers.wgpu import register_wgpu_render_function
from pygfx.renderers.wgpu.shaders.imageshader import ImageShader
from wgpu.utils.imgui import ImguiRenderer
from imgui_bundle import imgui
# Load image
im = iio.imread("imageio:hubble_deep_field.png").astype(np.float32)
X, Y = np.meshgrid(
np.arange(im.shape[1]) - im.shape[1] / 2, np.arange(im.shape[0]) - im.shape[0] / 2
)
radius = np.sqrt(X**2 + Y**2)
im *= 1 - radius[..., np.newaxis] / radius.max()
im = im.astype(np.uint8)
canvas_size = im.shape[0], im.shape[1]
canvas = RenderCanvas(size=canvas_size, max_fps=999)
renderer = gfx.renderers.WgpuRenderer(canvas, show_fps=True)
scene = gfx.Scene()
camera = gfx.OrthographicCamera(canvas_size[0], canvas_size[1])
camera.local.y = canvas_size[1] / 2
camera.local.scale_y = -1
camera.local.x = canvas_size[0] / 2
controller = gfx.PanZoomController(camera, register_events=renderer)
image_texture = gfx.Texture(im, dim=2, generate_mipmaps=True)
class BackGroundRemovedImageMaterial(gfx.ImageBasicMaterial):
"""
An image that has the background removed.
"""
uniform_type = dict(
gfx.ImageBasicMaterial.uniform_type,
background_level="u4",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.background_level = 0
@property
def background_level(self):
return self.uniform_buffer.data["background_level"]
@background_level.setter
def background_level(self, value):
self.uniform_buffer.data["background_level"] = int(value)
self.uniform_buffer.update_range()
@register_wgpu_render_function(gfx.Image, BackGroundRemovedImageMaterial)
class BackGroundRemovedImageShader(ImageShader):
def __init__(self, wobject):
super().__init__(wobject)
def get_code(self):
code = super().get_code()
code = code.replace(
"""
let value = sample_im(varyings.texcoord.xy, sizef);
""",
"""
// Get the image value via bilinear interpolation
var value: vec4<f32> = textureSampleLevel(t_img, s_img, varyings.texcoord.xy, 0.);
let background_level = f32(u_material.background_level);
if background_level != 0.0 {
// Get the background value via bilinear interpolation
let background = textureSampleLevel(t_img, s_img, varyings.texcoord.xy, background_level);
value = vec4<f32>(
(value.rgb - background.rgb),
value.a
);
}
""",
)
return code
image = gfx.Image(
gfx.Geometry(grid=image_texture),
BackGroundRemovedImageMaterial(clim=(0, 255), interpolation="linear"),
)
scene.add(image)
current_background_index = 0
current_image_index = 0
def draw_imgui():
global current_background_index
global current_image_index
global im, image_texture
imgui.set_next_window_size((400, 0), imgui.Cond_.always)
imgui.set_next_window_pos((0, 0), imgui.Cond_.always)
is_expand, _ = imgui.begin("Controls", None)
if is_expand:
background_levels = ["0", "1", "2", "3", "4", "5", "6", "7", "8"]
# Background level selection dropdown
changed, current_background_index = imgui.combo(
"Background Level",
current_background_index,
background_levels,
len(background_levels),
)
if changed:
image.material.background_level = np.int32(
background_levels[current_background_index]
)
imgui.end()
# Create GUI renderer
gui_renderer = ImguiRenderer(renderer.device, canvas)
def animate():
renderer.render(scene, camera)
renderer.flush()
gui_renderer.render()
canvas.request_draw()
gui_renderer.set_gui(draw_imgui)
if __name__ == "__main__":
canvas.request_draw(animate)
loop.run()
Total running time of the script: (0 minutes 1.419 seconds)
Gallery generated by Sphinx-Gallery
Interactive example
Try this example in your browser using Pyodide. Might not work with all examples and all devices. Check the output and your browser’s console for details.