From 2b351f6339bb121f4c65c5b83515d7d493513225 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:11 +0100 Subject: [PATCH 1/2] Add PyScript examples for opencv-python Generated by apply_llm_response.py from prompts/opencv-python/response.toml. Examples included: - image_basics: Image basics with OpenCV - filters_and_edges: Filtering and edge detection - thresholding_and_contours: Thresholding and contours Generated-By: apply_llm_response.py --- examples/opencv-python/README.md | 18 ++++ .../opencv-python/filters_and_edges/code.py | 58 +++++++++++++ .../filters_and_edges/config.toml | 1 + .../opencv-python/filters_and_edges/setup.py | 24 ++++++ examples/opencv-python/image_basics/code.py | 76 ++++++++++++++++ .../opencv-python/image_basics/config.toml | 1 + examples/opencv-python/image_basics/setup.py | 47 ++++++++++ examples/opencv-python/order.json | 5 ++ .../thresholding_and_contours/code.py | 86 +++++++++++++++++++ .../thresholding_and_contours/config.toml | 1 + .../thresholding_and_contours/setup.py | 24 ++++++ 11 files changed, 341 insertions(+) create mode 100644 examples/opencv-python/README.md create mode 100644 examples/opencv-python/filters_and_edges/code.py create mode 100644 examples/opencv-python/filters_and_edges/config.toml create mode 100644 examples/opencv-python/filters_and_edges/setup.py create mode 100644 examples/opencv-python/image_basics/code.py create mode 100644 examples/opencv-python/image_basics/config.toml create mode 100644 examples/opencv-python/image_basics/setup.py create mode 100644 examples/opencv-python/order.json create mode 100644 examples/opencv-python/thresholding_and_contours/code.py create mode 100644 examples/opencv-python/thresholding_and_contours/config.toml create mode 100644 examples/opencv-python/thresholding_and_contours/setup.py diff --git a/examples/opencv-python/README.md b/examples/opencv-python/README.md new file mode 100644 index 0000000..6a749a0 --- /dev/null +++ b/examples/opencv-python/README.md @@ -0,0 +1,18 @@ +# opencv-python Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/opencv-python/filters_and_edges/code.py b/examples/opencv-python/filters_and_edges/code.py new file mode 100644 index 0000000..0d0b7f5 --- /dev/null +++ b/examples/opencv-python/filters_and_edges/code.py @@ -0,0 +1,58 @@ +# --------------------------------------------------------------------- +# Smoothing noisy images and finding edges with Canny. +# --------------------------------------------------------------------- + +heading("Build a noisy test image") +note( + "We make a synthetic grayscale scene of overlapping shapes, " + "then add Gaussian noise. This gives us something to denoise " + "and find edges in." +) + +scene = np.full((200, 300), 40, dtype=np.uint8) +cv2.rectangle(scene, (30, 40), (140, 160), 200, thickness=-1) +cv2.circle(scene, (210, 100), 55, 140, thickness=-1) +cv2.rectangle(scene, (170, 30), (260, 80), 90, thickness=-1) + +# Add Gaussian noise and clip back to valid 8-bit range. +noise = rng.normal(0, 25, size=scene.shape) +noisy = np.clip(scene.astype(np.int16) + noise, 0, 255).astype(np.uint8) + +note( + f"Clean image dtype: {scene.dtype}, range " + f"[{scene.min()}, {scene.max()}]. Noisy range " + f"[{noisy.min()}, {noisy.max()}]." +) + +# --------------------------------------------------------------------- +# Smooth with a Gaussian blur, then run Canny edge detection. +# --------------------------------------------------------------------- + +# Gaussian blur: kernel size must be odd. Sigma 0 lets cv2 derive it. +smoothed = cv2.GaussianBlur(noisy, ksize=(5, 5), sigmaX=0) + +# Canny works best on a smoothed image. The two thresholds control +# which gradient magnitudes are kept (low) and which definitely start +# an edge (high). +edges_noisy = cv2.Canny(noisy, threshold1=80, threshold2=160) +edges_smooth = cv2.Canny(smoothed, threshold1=80, threshold2=160) + +fig, axes = plt.subplots(2, 2, figsize=(9, 6)) +axes[0, 0].imshow(noisy, cmap="gray") +axes[0, 0].set_title("Noisy input") +axes[0, 1].imshow(smoothed, cmap="gray") +axes[0, 1].set_title("Gaussian-blurred") +axes[1, 0].imshow(edges_noisy, cmap="gray") +axes[1, 0].set_title("Canny on noisy (lots of false edges)") +axes[1, 1].imshow(edges_smooth, cmap="gray") +axes[1, 1].set_title("Canny on blurred (cleaner edges)") +for ax in axes.flat: + ax.axis("off") +fig.tight_layout() +display(fig, append=True) + +note( + "Smoothing first removes high-frequency noise so the gradient-" + "based Canny detector finds the true object boundaries instead " + "of grain." +) diff --git a/examples/opencv-python/filters_and_edges/config.toml b/examples/opencv-python/filters_and_edges/config.toml new file mode 100644 index 0000000..25da125 --- /dev/null +++ b/examples/opencv-python/filters_and_edges/config.toml @@ -0,0 +1 @@ +packages = ["opencv-python", "numpy", "matplotlib"] diff --git a/examples/opencv-python/filters_and_edges/setup.py b/examples/opencv-python/filters_and_edges/setup.py new file mode 100644 index 0000000..64394f5 --- /dev/null +++ b/examples/opencv-python/filters_and_edges/setup.py @@ -0,0 +1,24 @@ +"""Setup for the filtering example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +import numpy as np +import cv2 +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) diff --git a/examples/opencv-python/image_basics/code.py b/examples/opencv-python/image_basics/code.py new file mode 100644 index 0000000..b5bcda9 --- /dev/null +++ b/examples/opencv-python/image_basics/code.py @@ -0,0 +1,76 @@ +""" +A first look at OpenCV (cv2): build a synthetic image, draw on it, +and convert between color spaces. + +OpenCV stores images as NumPy arrays. Color images are typically in +BGR order (blue, green, red), which differs from matplotlib's RGB. +We'll use cv2.cvtColor to translate between the two when displaying. + +Docs: https://docs.opencv.org/4.x/ +""" +from IPython.core.display import display, HTML + +heading("Drawing on a blank canvas") +note( + "We start with a black 200x300 BGR image (a NumPy array of " + "zeros) and draw a few primitives on it: a rectangle, a " + "filled circle, a line, and some text." +) + +# Create a blank BGR image: height=200, width=300, 3 channels. +canvas = np.zeros((200, 300, 3), dtype=np.uint8) + +# Colors are (B, G, R) tuples in OpenCV. +cv2.rectangle(canvas, (20, 20), (140, 120), (0, 200, 255), thickness=3) +cv2.circle(canvas, (220, 70), 40, (0, 255, 0), thickness=-1) # filled +cv2.line(canvas, (20, 160), (280, 160), (255, 100, 100), thickness=2) +cv2.putText( + canvas, "hello, cv2", + org=(40, 190), + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=0.7, + color=(255, 255, 255), + thickness=2, + lineType=cv2.LINE_AA, +) + +note(f"Canvas shape: {canvas.shape}, dtype: {canvas.dtype}") + +# Convert BGR -> RGB so matplotlib displays the colors correctly. +canvas_rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB) + +fig, ax = plt.subplots(figsize=(6, 4)) +ax.imshow(canvas_rgb) +ax.set_title("Primitives drawn with cv2") +ax.axis("off") +fig.tight_layout() +display(fig, append=True) + + +heading("Color spaces: BGR, RGB, and grayscale") +note( + "Real images come in many color spaces. Here we build a small " + "BGR test image with three colored stripes, then view it as " + "RGB (wrong) and after converting BGR->RGB (correct), and as " + "grayscale." +) + +stripes = np.zeros((120, 300, 3), dtype=np.uint8) +stripes[:, :100] = (255, 0, 0) # blue stripe in BGR +stripes[:, 100:200] = (0, 255, 0) # green stripe +stripes[:, 200:] = (0, 0, 255) # red stripe + +stripes_rgb = cv2.cvtColor(stripes, cv2.COLOR_BGR2RGB) +stripes_gray = cv2.cvtColor(stripes, cv2.COLOR_BGR2GRAY) + +fig, axes = plt.subplots(1, 3, figsize=(10, 3)) +axes[0].imshow(stripes) # matplotlib assumes RGB, so colors look swapped +axes[0].set_title("BGR shown as RGB (wrong)") +axes[1].imshow(stripes_rgb) +axes[1].set_title("After BGR->RGB (correct)") +axes[2].imshow(stripes_gray, cmap="gray") +axes[2].set_title("Grayscale") +for ax in axes: + ax.axis("off") +fig.tight_layout() +display(fig, append=True) diff --git a/examples/opencv-python/image_basics/config.toml b/examples/opencv-python/image_basics/config.toml new file mode 100644 index 0000000..25da125 --- /dev/null +++ b/examples/opencv-python/image_basics/config.toml @@ -0,0 +1 @@ +packages = ["opencv-python", "numpy", "matplotlib"] diff --git a/examples/opencv-python/image_basics/setup.py b/examples/opencv-python/image_basics/setup.py new file mode 100644 index 0000000..fe8c38d --- /dev/null +++ b/examples/opencv-python/image_basics/setup.py @@ -0,0 +1,47 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import cv2 +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) diff --git a/examples/opencv-python/order.json b/examples/opencv-python/order.json new file mode 100644 index 0000000..4cb8d5a --- /dev/null +++ b/examples/opencv-python/order.json @@ -0,0 +1,5 @@ +[ + "image_basics", + "filters_and_edges", + "thresholding_and_contours" +] diff --git a/examples/opencv-python/thresholding_and_contours/code.py b/examples/opencv-python/thresholding_and_contours/code.py new file mode 100644 index 0000000..2c78f9f --- /dev/null +++ b/examples/opencv-python/thresholding_and_contours/code.py @@ -0,0 +1,86 @@ +# --------------------------------------------------------------------- +# Segment shapes from the background and measure them with contours. +# --------------------------------------------------------------------- + +heading("A scattering of coins (well, disks)") +note( + "We sprinkle a handful of bright disks of varying size onto a " + "dark background, then use thresholding and findContours to " + "count them and measure each one's area and centroid." +) + +# Synthetic scene: dark background with several light circles. +image = np.full((260, 360, 3), 30, dtype=np.uint8) + +disks = [ + (60, 70, 25), + (140, 90, 35), + (240, 60, 20), + (310, 130, 30), + (90, 180, 40), + (200, 200, 28), + (290, 210, 22), +] +for cx, cy, r in disks: + color = tuple(int(v) for v in rng.integers(180, 255, size=3)) + cv2.circle(image, (cx, cy), r, color, thickness=-1) + +# Threshold on grayscale to separate foreground from background. +gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) +_, binary = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY) + +# Find external contours (one per disk). +contours, _ = cv2.findContours( + binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE, +) + +note(f"Found {len(contours)} contours.") + +# Annotate a copy of the original image with each contour's bounding +# box, centroid, and area. +annotated = image.copy() +rows = [] +for index, contour in enumerate(contours, start=1): + area = cv2.contourArea(contour) + x, y, w, h = cv2.boundingRect(contour) + + # Image moments give us the centroid (cx, cy) of the contour. + moments = cv2.moments(contour) + if moments["m00"] > 0: + cx = int(moments["m10"] / moments["m00"]) + cy = int(moments["m01"] / moments["m00"]) + else: + cx, cy = x + w // 2, y + h // 2 + + cv2.rectangle(annotated, (x, y), (x + w, y + h), (0, 255, 255), 2) + cv2.circle(annotated, (cx, cy), 3, (0, 0, 255), -1) + cv2.putText( + annotated, str(index), (cx + 6, cy - 6), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA, + ) + rows.append((index, area, (cx, cy), (w, h))) + +# Display the pipeline: original, binary mask, annotated result. +fig, axes = plt.subplots(1, 3, figsize=(12, 4)) +axes[0].imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) +axes[0].set_title("Original") +axes[1].imshow(binary, cmap="gray") +axes[1].set_title("Binary mask (threshold)") +axes[2].imshow(cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB)) +axes[2].set_title("Contours, bounding boxes, centroids") +for ax in axes: + ax.axis("off") +fig.tight_layout() +display(fig, append=True) + +# Print a small report of the per-disk measurements. +report_rows = "".join( + f"{i}{area:.0f}" + f"({cx}, {cy}){w}x{h}" + for i, area, (cx, cy), (w, h) in rows +) +display(HTML( + "" + "" + f"{report_rows}
#Area (px)CentroidBounding box
" +), append=True) diff --git a/examples/opencv-python/thresholding_and_contours/config.toml b/examples/opencv-python/thresholding_and_contours/config.toml new file mode 100644 index 0000000..25da125 --- /dev/null +++ b/examples/opencv-python/thresholding_and_contours/config.toml @@ -0,0 +1 @@ +packages = ["opencv-python", "numpy", "matplotlib"] diff --git a/examples/opencv-python/thresholding_and_contours/setup.py b/examples/opencv-python/thresholding_and_contours/setup.py new file mode 100644 index 0000000..d83cf9e --- /dev/null +++ b/examples/opencv-python/thresholding_and_contours/setup.py @@ -0,0 +1,24 @@ +"""Setup for the contours example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display(*args, **kwargs, target=__pyscript_display_target__) + + +import numpy as np +import cv2 +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) From a0cc2270085310de2cdec84a0049264c591ed22a Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Mon, 8 Jun 2026 16:26:12 +0100 Subject: [PATCH 2/2] Fix imports. --- examples/opencv-python/image_basics/code.py | 7 +++++++ examples/opencv-python/image_basics/setup.py | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/opencv-python/image_basics/code.py b/examples/opencv-python/image_basics/code.py index b5bcda9..9d0fd8b 100644 --- a/examples/opencv-python/image_basics/code.py +++ b/examples/opencv-python/image_basics/code.py @@ -10,6 +10,13 @@ """ from IPython.core.display import display, HTML +import numpy as np +import cv2 +import matplotlib.pyplot as plt + +rng = np.random.default_rng(7) + + heading("Drawing on a blank canvas") note( "We start with a black 200x300 BGR image (a NumPy array of " diff --git a/examples/opencv-python/image_basics/setup.py b/examples/opencv-python/image_basics/setup.py index fe8c38d..84faac4 100644 --- a/examples/opencv-python/image_basics/setup.py +++ b/examples/opencv-python/image_basics/setup.py @@ -38,10 +38,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import numpy as np -import cv2 -import matplotlib.pyplot as plt - -rng = np.random.default_rng(7)