Hello,
I’m loading large images into my Bokeh plot using p.image_rgba
. I want to provide users with a way to increase or decrease the brightness of the image.
From my testing, I would subjectively conclude that adjusting the brightness using CustomJS
is faster than changing the brightness on the backend.
Is it possible to change the brightness only on the client side and refresh the canvas without involving the backend? Is it possible to update the plot client side without calling source.change.emit()
?
Additionally, I noticed the following message in the browser developer console:
Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
This message is triggered here:
_set_image_data_from_buffer(t, e) {
(0,
m.assert)(null != this.image_data);
const i = this._get_or_create_canvas(t)
, s = i.getContext("2d")
, a = s.getImageData(0, 0, this.image_width[t], this.image_height[t]);
a.data.set(e),
s.putImageData(a, 0, 0),
this.image_data[t] = i
}
Are there other options to optimize or boost the performance of changing brightness for large images?
P.S. I noticed that in the documentation here: BokehJS — Bokeh 3.7.2 Documentation the “Try on CodePen” button does not work.
Please find attached my sample code where I compare changing brightness on the backend vs the client (using CustomJS
). View the Demo here as well.
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider, CustomJS
from bokeh.plotting import figure
import numpy as np
def create_image_data(height=2048, width = 2048):
"""
Create a sample RGBA image data.
Returns:
np.ndarray: A 2D array of uint32 values representing the image.
"""
# Generate a dynamic image of size 1024x1024
image_data = np.zeros((height, width, 4), dtype=np.uint8)
# Fill the image with dynamic pixel values
for y in range(height):
for x in range(width):
image_data[y, x, 0] = x % 256 # Red channel
image_data[y, x, 1] = y % 256 # Green channel
image_data[y, x, 2] = (x + y) % 256 # Blue channel
image_data[y, x, 3] = 255 # Alpha channel (fully opaque)
# Convert the image to uint32 RGBA format
image_data_uint32 = (
(image_data[:, :, 0].astype(np.uint32) << 24) | # Red
(image_data[:, :, 1].astype(np.uint32) << 16) | # Green
(image_data[:, :, 2].astype(np.uint32) << 8) | # Blue
(image_data[:, :, 3].astype(np.uint32)) # Alpha
)
return image_data_uint32
def adjust_brightness(image, brightness_factor):
"""
Adjust the brightness of a uint32 RGBA image.
Parameters:
image (np.ndarray): The input image as a 2D array of uint32 values.
brightness_factor (float): The factor by which to adjust brightness (e.g., 1.0 = no change).
Returns:
np.ndarray: The modified image with adjusted brightness.
"""
# Decode RGBA components from uint32
r = (image >> 24) & 0xFF
g = (image >> 16) & 0xFF
b = (image >> 8) & 0xFF
a = image & 0xFF # Alpha channel remains unchanged
# print (f"Original R: {r[x,y]}, G: {g[x,y]}, B: {b[x,y]}, A: {a[x,y]}")
# Adjust brightness for RGB channels
r = np.clip(r * brightness_factor, 0, 255).astype(np.uint8)
g = np.clip(g * brightness_factor, 0, 255).astype(np.uint8)
b = np.clip(b * brightness_factor, 0, 255).astype(np.uint8)
# print (f"Adjusted R: {r[x,y]}, G: {g[x,y]}, B: {b[x,y]}, A: {a[x,y]}")
# Recombine RGBA components into uint32
updated_image = (
(r.astype(np.uint32) << 24) |
(g.astype(np.uint32) << 16) |
(b.astype(np.uint32) << 8) |
a.astype(np.uint32)
)
return updated_image
# Initialize the image
img = create_image_data()
backupImg= img.copy()
source = ColumnDataSource({'image': [img]})
# Create the Bokeh figure
p = figure(x_range=(0, 10), y_range=(0, 10))
p.image_rgba(image='image', x=0, y=0, dw=10, dh=10, source=source)
# Create a slider for brightness adjustment
brightness_slider = Slider(start=0.0, end=2.0, step=0.1, value=1.0, title="Brightness")
js_brightness_slider = Slider(start=0.1, end=2.0, step=0.1, value=1.0, title="JS Brightness")
# Define the CustomJS callback
js_brightness_callback = CustomJS(args=dict(source=source), code="""
const brightness = cb_obj.value; // Get the brightness factor from the slider
const data = source.data['image'][0]; // Access the image data (uint32 array)
// Use a static variable to store the original image data
if (typeof window.original_image_data === 'undefined') {
// If it doesn't exist, assign the current data to 'original_image_data'
window.original_image_data = [...data]; // Create a copy of the data
}
const original_data = window.original_image_data; // Use the original image data
// Loop through each pixel and adjust brightness
for (let i = 0; i < data.length; i++) {
const r = (original_data[i] >> 24) & 0xFF; // Extract Red
const g = (original_data[i] >> 16) & 0xFF; // Extract Green
const b = (original_data[i] >> 8) & 0xFF; // Extract Blue
const a = original_data[i] & 0xFF; // Extract Alpha
// Adjust brightness for RGB channels
const newR = Math.min(255, r * brightness);
const newG = Math.min(255, g * brightness);
const newB = Math.min(255, b * brightness);
// Recombine RGBA components into uint32
data[i] = (newR << 24) | (newG << 16) | (newB << 8) | a;
}
console.log("Brightness adjusted using JS callback");
source.change.emit(); // Trigger a re-render
""")
# Define the Python callback for the brightness slider
def update_brightness(attr, old, new):
brightness_factor = brightness_slider.value # Get the brightness factor
adjusted_img = adjust_brightness(backupImg.copy(), brightness_factor)
source.data = {'image': [adjusted_img]}
# Attach the callbacks to the sliders
brightness_slider.on_change('value', update_brightness)
js_brightness_slider.js_on_change('value', js_brightness_callback)
# Add the layout to the document
layout = column(p,
js_brightness_slider,
brightness_slider
)
curdoc().add_root(layout)
Thank you!