// Example 13 — Debug Cell-Shading Pipeline with Tiled Debug Canvas // // An improved cell-shading pipeline where every processing node performs // exactly one OpenCV operation. A variadic DebugCanvas node tiles N // cv::Mat inputs into a single debug window, making each pipeline stage // visible side-by-side at runtime. // // Improvements over example 12: // - Bilateral filter before quantisation (edge-preserving smoothing): // flattens colour regions without softening object edges // - Dilated edge mask for bolder black outlines // - 6-level quantisation for richer tonal detail // // Topology (auto-fanouts inserted by make_network): // // [capture]──┬──> [bilateral]──> [quant]──┬──> [comp]──> [debug:0 result] // │ └──> [debug:1 quantised] // ├──> [to_gray]──> [blur]──> [canny]──┬──> [dilate]──> [comp] // │ └──> [debug:2 edges] // └──> [debug:3 original] // // make_network detects the 3-way fan from [capture], the 2-way fan from // [quant], and the 2-way fan from [canny], inserting FanoutNode instances // automatically. #include #include #include #include #include #include #include #include #include #include #include #include #include // ── Synthetic source for environments without a webcam ─────────────────────── 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; } // ── Pipeline nodes — one cv:: call per function ─────────────────────────────── static cv::Mat 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(); } // Edge-preserving smooth: flattens colour within regions while keeping sharp // boundaries — much better than Gaussian blur as a pre-quantisation step. static cv::Mat bilateral_filter(cv::Mat bgr) { cv::Mat out; cv::bilateralFilter(bgr, out, 5, 75, 75); return out; } // Snap each channel to N discrete tonal levels. static cv::Mat quantise(cv::Mat bgr) { constexpr int levels = 6; 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; } // BGR → greyscale; the edge path works on the original (not bilateral-filtered) // frame so that fine edge detail is preserved. static cv::Mat to_gray(cv::Mat bgr) { cv::Mat gray; cv::cvtColor(bgr, gray, cv::COLOR_BGR2GRAY); return gray; } // Suppress high-frequency noise before the Canny detector. static cv::Mat gaussian_blur(cv::Mat gray) { cv::Mat out; cv::GaussianBlur(gray, out, {5, 5}, 0); return out; } // Detect strong edges; returns a binary mask (CV_8UC1). static cv::Mat canny_edges(cv::Mat blurred) { cv::Mat out; cv::Canny(blurred, out, 50, 150); return out; } // Widen the edge mask for bolder cartoon outlines. static cv::Mat dilate_edges(cv::Mat edges) { static const cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, {3, 3}); cv::Mat out; cv::dilate(edges, out, kernel); return out; } // Burn black outlines into the quantised colour image. static cv::Mat composite(cv::Mat quantised, cv::Mat thick_edges) { cv::Mat result = quantised.clone(); result.setTo(cv::Scalar(0, 0, 0), thick_edges); return result; } // ── DebugCanvas ──────────────────────────────────────────────────────────── // // Variadic MainThreadNode that accepts N cv::Mat inputs (any mix of BGR and // greyscale) and tiles them into one debug window arranged as a // ceil(sqrt(N)) × ceil(N/cols) grid. // // Template trick: MatArg aliases cv::Mat for all I, so the pack expansion // MatArg<0>, MatArg<1>, ..., MatArg // produces exactly N cv::Mat arguments — enough to drive the MainThreadNode // base without manually spelling out the type N times. template using MatArg = cv::Mat; template class DebugCanvas; template> struct DebugCanvasBase; template struct DebugCanvasBase> { using type = kpn::MainThreadNode, kpn::in<>, MatArg...>; }; template class DebugCanvas : public DebugCanvasBase::type { using Base = typename DebugCanvasBase::type; public: static constexpr std::string_view label() { return "debug_canvas"; } static constexpr std::size_t unique_tag = 0; explicit DebugCanvas(std::vector slot_labels = {}, std::size_t fifo_cap = 4) : Base(fifo_cap), labels_(std::move(slot_labels)) { cv::namedWindow("Debug Canvas", cv::WINDOW_NORMAL); cv::resizeWindow("Debug Canvas", cols() * CW, rows() * CH); } ~DebugCanvas() { cv::destroyWindow("Debug Canvas"); } // Called by MainThreadNode::step() with exactly N cv::Mat arguments. template bool operator()(Ms&&... mats) { static_assert(sizeof...(Ms) == N, "DebugCanvas: wrong number of inputs"); std::vector imgs; imgs.reserve(N); (imgs.push_back(to_bgr(std::forward(mats))), ...); cv::imshow("Debug Canvas", tile(imgs)); int key = cv::waitKey(1); if (key == 'q' || key == 27) return false; try { return cv::getWindowProperty("Debug Canvas", cv::WND_PROP_VISIBLE) >= 1; } catch (const cv::Exception&) { return false; } } private: static constexpr int CW = 640, CH = 480; static int cols() { return std::max(1, (int)std::ceil(std::sqrt((double)N))); } static int rows() { return ((int)N + cols() - 1) / cols(); } std::vector labels_; static cv::Mat to_bgr(const cv::Mat& m) { if (m.channels() == 1) { cv::Mat bgr; cv::cvtColor(m, bgr, cv::COLOR_GRAY2BGR); return bgr; } return m; } cv::Mat tile(const std::vector& imgs) const { const int c = cols(), r = rows(); cv::Mat canvas(r * CH, c * CW, CV_8UC3, cv::Scalar(30, 30, 30)); for (int i = 0; i < (int)imgs.size(); ++i) { if (imgs[i].empty()) continue; cv::Mat cell = canvas(cv::Rect((i % c) * CW, (i / c) * CH, CW, CH)); cv::Mat resized; cv::resize(imgs[i], resized, {CW, CH}); resized.copyTo(cell); if (i < (int)labels_.size() && !labels_[i].empty()) cv::putText(cell, labels_[i], {8, 36}, cv::FONT_HERSHEY_SIMPLEX, 1.0, {0, 255, 255}, 2, cv::LINE_AA); } return canvas; } }; // ── main ────────────────────────────────────────────────────────────────────── int main() { using namespace kpn; auto src = make_node(4); auto bilateral= make_node(4); auto quant = make_node(4); auto gray = make_node(4); auto blur = make_node(4); auto canny = make_node(4); auto dilate = make_node(4); auto comp = make_node(4); DebugCanvas<4> debug({"result", "quantised", "edges", "original"}); // make_network auto-inserts FanoutNode instances wherever a source port // feeds more than one consumer: // capture → 3-way fanout (bilateral, to_gray, debug[3]) // quant → 2-way fanout (comp, debug[1]) // canny → 2-way fanout (dilate, debug[2]) auto net = make_network( edge(src.output<0>(), bilateral.input<0>()), // frame → bilateral edge(src.output<0>(), gray.input<0>()), // frame → to_gray edge(src.output<0>(), debug.input<3>()), // frame → debug[3] original edge(bilateral.output<0>(), quant.input<0>()), // smooth → quant edge(quant.output<0>(), comp.input<0>()), // quant → comp edge(quant.output<0>(), debug.input<1>()), // quant → debug[1] quantised edge(gray.output<0>(), blur.input<0>()), // gray → blur edge(blur.output<0>(), canny.input<0>()), // blurred→ canny edge(canny.output<0>(), dilate.input<0>()), // edges → dilate edge(canny.output<0>(), debug.input<2>()), // edges → debug[2] edges edge(dilate.output<0>(), comp.input<1>()), // thick → comp edge(comp.output<0>(), debug.input<0>()) // result → debug[0] result ); std::cout << "Debug cell-shading pipeline running — press 'q' to stop.\n"; std::cout << "Canvas: [0] result [1] quantised [2] edges [3] original\n"; net.start(); while (debug.step()) cv::waitKey(8); net.stop(); return 0; }