""" 09_opencv_cellshade/example_hybrid.py ────────────────────────────────────── Hybrid cell-shading pipeline: C++ nodes handle capture, grayscale conversion, and edge detection; a Python/numpy function replaces the C++ quantise node; Python drives the display loop using cv2. Pipeline: ┌─[py_quantise]──────────────┐ [CaptureNode] ─────┤ ├──[CompositeNode]──result──▶ cv2.imshow out0=colour └─[ToGrayNode]─[EdgesNode]───┘ edges───▶ cv2.imshow out1=grey For a pure-C++ version see main.cpp; for the C++ static-network version see 12_static_cellshade/main.cpp. Press 'q' or Esc to stop. """ import sys import os # Adjust path to wherever CMake placed the .so BUILD_DIR = os.environ.get("KPN_BUILD_DIR", os.path.join(os.path.dirname(__file__), "../../build/examples")) sys.path.insert(0, BUILD_DIR) import numpy as np import cv2 import kpn_opencv as kpn # ── Python node: replace the C++ quantise with numpy ───────────────────────── # Receives and returns a BGR numpy array (H×W×3 uint8). def py_quantise(bgr: np.ndarray) -> np.ndarray: levels = 4 step = 256 // levels q = (bgr.astype(np.int32) // step) * step + (step // 2) return q.clip(0, 255).astype(np.uint8) # ── Build network ───────────────────────────────────────────────────────────── net = kpn.Network() net.add("src", kpn.make_capture()) # out0=colour, out1=grey net.add_node("quant", py_quantise, # Python node — numpy in/out inputs=["mat"], outputs=["mat"]) net.add("gray", kpn.make_to_gray()) # in0=bgr → out0=gray net.add("edges", kpn.make_edges()) # in0=gray → out0=edge_mask net.add("comp", kpn.make_composite()) # in0=edge_mask, in1=colour # out0=result, out1=edge_mask # src.colour → py_quantise net.connect("src", 0, "quant", 0) # src.grey → to_gray net.connect("src", 1, "gray", 0) # gray → edges net.connect("gray", 0, "edges", 0) # quantised colour → composite.colour (input slot 1) net.connect("quant", 0, "comp", 1) # edge mask → composite.edges (input slot 0) net.connect("edges", 0, "comp", 0) net.build() net.start() # ── Display loop (drives GUI on this thread) ────────────────────────────────── cv2.namedWindow("Cell Shade (Python quant)", cv2.WINDOW_NORMAL) cv2.namedWindow("Edge Mask", cv2.WINDOW_NORMAL) cv2.resizeWindow("Cell Shade (Python quant)", 1280, 720) cv2.resizeWindow("Edge Mask", 640, 360) try: while True: # Blocking reads — GIL released while waiting so C++ threads can run result = net.read("comp", 0) # composite frame (BGR numpy array) edges = net.read("comp", 1) # edge mask (grayscale numpy array) cv2.imshow("Cell Shade (Python quant)", result) cv2.imshow("Edge Mask", cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)) key = cv2.waitKey(1) if key in (ord('q'), 27): break # Check windows still open try: if cv2.getWindowProperty("Cell Shade (Python quant)", cv2.WND_PROP_VISIBLE) < 1: break except cv2.error: break finally: net.stop() cv2.destroyAllWindows() del net # let C++ destructor run before nanobind tears down