#define KPN_BUILD_PYTHON #include #include #include #include #include #include #include #include #include #include namespace nb = nanobind; using namespace kpn; using namespace kpn::python; // ── PythonConverter ────────────────────────────────────────────────── // Converts cv::Mat ↔ numpy array (uint8, HxW or HxWxC shape). // // to_python: clones the mat onto the heap; the numpy array owns it via a // capsule deleter — no shared cv::Mat refcount dangling after the Variant dies. // from_python: calls numpy.ascontiguousarray, then clones into an owned cv::Mat. namespace kpn { template<> struct PythonConverter { static constexpr const char* type_name = "mat"; static nb::object to_python(const cv::Mat& m) { // Must be called with the GIL held (always true: called from read() or // from within the gil_scoped_acquire block in PyNode::run_loop). auto np = nb::module_::import_("numpy"); cv::Mat c = m.clone(); // ensure contiguous, independently owned nb::bytes raw(reinterpret_cast(c.data), c.total() * c.elemSize()); nb::object arr = np.attr("frombuffer")(raw, "uint8"); int H = c.rows, W = c.cols, C = c.channels(); arr = arr.attr("reshape")( C > 1 ? nb::make_tuple(H, W, C) : nb::make_tuple(H, W)); return arr.attr("copy")(); // writable, lifetime-independent copy } static cv::Mat from_python(nb::object o) { auto np = nb::module_::import_("numpy"); // Ensure contiguous uint8 layout (in-place if already compatible) nb::object arr = np.attr("ascontiguousarray")(o, "uint8"); auto shape = nb::cast>(arr.attr("shape")); if (shape.size() < 2 || shape.size() > 3) throw std::runtime_error( "cv::Mat from_python: expected 2D (H×W) or 3D (H×W×C) uint8 array"); int H = shape[0], W = shape[1]; int C = (shape.size() == 3) ? shape[2] : 1; int type = C > 1 ? CV_8UC(C) : CV_8UC1; // Cast to ndarray to get the raw data pointer auto binfo = nb::cast>(arr); cv::Mat wrap(H, W, type, binfo.data()); return wrap.clone(); // own the pixel data } }; } // namespace kpn // ── Pipeline functions ──────────────────────────────────────────────────────── static cv::Mat make_gradient(int W, int H) { cv::Mat xr(H, W, CV_8UC1), yg(H, W, CV_8UC1), b(H, W, CV_8UC1, cv::Scalar(128)); for (int x = 0; x < W; ++x) xr.col(x).setTo(x * 255 / W); for (int y = 0; y < H; ++y) yg.row(y).setTo(y * 255 / H); cv::Mat channels[3] = {b, yg, xr}; cv::Mat grad; cv::merge(channels, 3, grad); return grad; } static std::tuple capture() { constexpr int W = 640, H = 480; static cv::VideoCapture cap; static bool opened = false; if (!opened) { opened = true; cap.open(0, cv::CAP_V4L2); if (cap.isOpened()) { cap.set(cv::CAP_PROP_FRAME_WIDTH, W); cap.set(cv::CAP_PROP_FRAME_HEIGHT, H); } else { std::cerr << "[capture] no webcam — using synthetic animated pattern\n"; } } cv::Mat frame; if (cap.isOpened()) { auto t0 = std::chrono::steady_clock::now(); cap >> frame; auto elapsed = std::chrono::steady_clock::now() - t0; if (elapsed < std::chrono::milliseconds(20)) std::this_thread::sleep_for(std::chrono::milliseconds(33) - elapsed); if (frame.empty()) frame = cv::Mat::zeros(H, W, CV_8UC3); } else { static int tick = 0; static cv::Mat grad = make_gradient(W, H); ++tick; frame = grad.clone(); int r = 150 + (tick % 80) * 4; cv::circle(frame, {W/2, H/2}, r, {255, 200, 0}, -1); cv::circle(frame, {W/2, H/2}, r / 2, { 0, 128, 255}, -1); cv::circle(frame, {W*2/5, H*2/5}, r / 3, {200, 0, 200}, -1); std::this_thread::sleep_for(std::chrono::milliseconds(33)); } return {frame.clone(), frame.clone()}; } static cv::Mat to_gray(cv::Mat bgr) { cv::Mat gray; cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY); return gray; } static cv::Mat edges_fn(cv::Mat gray) { cv::Mat blurred, mask; cv::GaussianBlur(gray, blurred, {5, 5}, 0); cv::Canny(blurred, mask, 50, 150); return mask; } static cv::Mat quantise(cv::Mat bgr) { constexpr int levels = 4; constexpr double step = 256.0 / levels; static const cv::Mat lut = []() { cv::Mat l(1, 256, CV_8UC1); for (int i = 0; i < 256; ++i) l.at(i) = cv::saturate_cast( std::floor(i / step) * step + step / 2.0); return l; }(); cv::Mat out; cv::LUT(bgr, lut, out); return out; } // Returns composite frame AND edge mask so the display node can show both // without needing a fan-out on the edges channel. static std::tuple composite(cv::Mat edge_mask, cv::Mat colour) { cv::Mat result = colour.clone(); result.setTo(cv::Scalar(0, 0, 0), edge_mask); return {result, edge_mask}; } // ── Registry ────────────────────────────────────────────────────────────────── // Variant deduced as std::variant — every node uses only cv::Mat. using CvNodes = NodeRegistry< Entry, Entry, Entry, Entry, Entry >; // ── Module ──────────────────────────────────────────────────────────────────── NB_MODULE(kpn_opencv, m) { m.doc() = "KPN++ OpenCV bindings for the cell-shading pipeline"; // Registers: Network, INode, CaptureNode, ToGrayNode, EdgesNode, // QuantiseNode, CompositeNode, and make_() factories. // Network.add_node(name, callable, inputs=["mat"], outputs=["mat"]) // accepts Python callables that receive/return numpy uint8 arrays. bind_network(m); // Note: bind_debug is omitted here — cv::Mat functions cannot be called // directly from Python without the variant/network machinery. Use // net.write() + net.read() to inject/inspect individual nodes instead. }