// Example 12 — Static Cell-Shading Pipeline with Auto Fan-Out // // The same cell-shading effect as example 09, rebuilt with make_network(). // // Key differences from example 09: // // 1. Fan-out is automatic. The edge detector output feeds both the compositing // node and the debug display window. In example 09 this required composite() // to re-output the edge mask as a second return value (a workaround). Here // make_network() detects the duplicate source port and inserts // FanoutNode automatically — composite() is a clean single-output // node. // // 2. No add()/connect()/build() ceremony. The full topology is expressed once // in the make_network() call. Cycle detection and duplicate-tag checking are // compile-time static_asserts. // // 3. Every node has a Label NTTP so the web debug UI shows real names. // // Topology: // // [capture] --colour--> [quant] ──────────────────────────────> [comp] --> [display_composite] // [capture] --grey----> [to_gray] --> [edges] --edges--(fan)--> [comp] // --edges----------> [display_edges] ← auto-fanout // // Note: capture returns std::tuple (colour, grey). // The two outputs are separate ports routed independently. #include #include #include #include #include #include #include #include #include // ── Gradient base for synthetic pattern ─────────────────────────────────────── 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 functions ──────────────────────────────────────────────────────── 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; } // Clean single-output composite — no longer needs to pass edges through. static cv::Mat composite(cv::Mat edge_mask, cv::Mat colour) { cv::Mat result = colour.clone(); result.setTo(cv::Scalar(0, 0, 0), edge_mask); return result; } // ── Display nodes ───────────────────────────────────────────────────────────── // // Two separate MainThreadNode subclasses — one for the composited result, // one for the raw edge mask. Each runs on the main thread via step(). // The fan-out from [edges] to both consumers is inserted automatically by // make_network(). class DisplayComposite : public kpn::MainThreadNode, cv::Mat> { public: // Label and unique_tag for StaticNetwork identity static constexpr std::string_view label() { return "display_composite"; } static constexpr std::size_t unique_tag = 0; DisplayComposite() : MainThreadNode(8) { cv::namedWindow("Cell Shade", cv::WINDOW_NORMAL); cv::resizeWindow("Cell Shade", 1280, 720); } ~DisplayComposite() { cv::destroyWindow("Cell Shade"); } bool operator()(cv::Mat frame) { cv::imshow("Cell Shade", frame); int key = cv::waitKey(1); if (key == 'q' || key == 27) return false; try { return cv::getWindowProperty("Cell Shade", cv::WND_PROP_VISIBLE) >= 1; } catch (const cv::Exception&) { return false; } } }; class DisplayEdges : public kpn::MainThreadNode, cv::Mat> { public: static constexpr std::string_view label() { return "display_edges"; } static constexpr std::size_t unique_tag = 1; DisplayEdges() : MainThreadNode(8) { cv::namedWindow("Edge Mask", cv::WINDOW_NORMAL); cv::resizeWindow("Edge Mask", 640, 360); } ~DisplayEdges() { cv::destroyWindow("Edge Mask"); } bool operator()(cv::Mat mask) { cv::Mat bgr; cv::cvtColor(mask, bgr, cv::COLOR_GRAY2BGR); cv::imshow("Edge Mask", bgr); cv::waitKey(1); try { return cv::getWindowProperty("Edge Mask", cv::WND_PROP_VISIBLE) >= 1; } catch (const cv::Exception&) { return false; } } }; // ───────────────────────────────────────────────────────────────────────────── int main() { using namespace kpn; // Nodes — all labelled for web debug UI auto src = make_node(8); auto gray_node = make_node(8); auto edge_node = make_node(8); auto quant = make_node(8); auto comp = make_node(8); // DisplayNodes live on the main thread — registered as sinks DisplayComposite disp_comp; DisplayEdges disp_edges; // make_network() detects that edge_node.output<0>() feeds two consumers // (comp and disp_edges) and inserts FanoutNode automatically. auto net = make_network( edge(src.output<0>(), quant.input<0>()), // colour → quant edge(src.output<1>(), gray_node.input<0>()), // grey → to_gray edge(gray_node.output<0>(), edge_node.input<0>()), // gray → edges edge(edge_node.output<0>(), comp.input<0>()), // edges → comp (fan-out src) edge(edge_node.output<0>(), disp_edges.input<0>()), // edges → display_edges (auto fanout) edge(quant.output<0>(), comp.input<1>()), // quantised → comp edge(comp.output<0>(), disp_comp.input<0>()) // result → display_composite ); std::cout << "Cell-shading pipeline (static) running. Press 'q' to stop.\n"; net.start(); // Main thread drives both display nodes — step() returns false when // operator() returns false (q pressed or window closed). while (disp_comp.step() && disp_edges.step()) cv::waitKey(8); net.stop(); return 0; }