Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/opencv-python/README.md
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 58 additions & 0 deletions examples/opencv-python/filters_and_edges/code.py
Original file line number Diff line number Diff line change
@@ -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."
)
1 change: 1 addition & 0 deletions examples/opencv-python/filters_and_edges/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["opencv-python", "numpy", "matplotlib"]
24 changes: 24 additions & 0 deletions examples/opencv-python/filters_and_edges/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
83 changes: 83 additions & 0 deletions examples/opencv-python/image_basics/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
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

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 "
"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)
1 change: 1 addition & 0 deletions examples/opencv-python/image_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["opencv-python", "numpy", "matplotlib"]
40 changes: 40 additions & 0 deletions examples/opencv-python/image_basics/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
5 changes: 5 additions & 0 deletions examples/opencv-python/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"image_basics",
"filters_and_edges",
"thresholding_and_contours"
]
86 changes: 86 additions & 0 deletions examples/opencv-python/thresholding_and_contours/code.py
Original file line number Diff line number Diff line change
@@ -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 <strong>{len(contours)}</strong> 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"<tr><td>{i}</td><td>{area:.0f}</td>"
f"<td>({cx}, {cy})</td><td>{w}x{h}</td></tr>"
for i, area, (cx, cy), (w, h) in rows
)
display(HTML(
"<table><thead><tr><th>#</th><th>Area (px)</th>"
"<th>Centroid</th><th>Bounding box</th></tr></thead>"
f"<tbody>{report_rows}</tbody></table>"
), append=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["opencv-python", "numpy", "matplotlib"]
24 changes: 24 additions & 0 deletions examples/opencv-python/thresholding_and_contours/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)