Duncan Tourolle 2bca2a7554
All checks were successful
🧪 Test / test (push) Successful in 6m4s
Add static network
2026-05-08 20:00:15 +02:00

218 lines
8.6 KiB
C++

// 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<cv::Mat, 2> 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<cv::Mat, cv::Mat> (colour, grey).
// The two outputs are separate ports routed independently.
#include <kpn/kpn.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
#include <iostream>
#include <tuple>
#include <thread>
#include <chrono>
// ── 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<cv::Mat, 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(), 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<uchar>(i) = cv::saturate_cast<uchar>(
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<DisplayComposite,
kpn::in<"composite">,
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<DisplayEdges,
kpn::in<"edges">,
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<capture, "capture">(8);
auto gray_node = make_node<to_gray, "to_gray">(8);
auto edge_node = make_node<edges_fn, "edges" >(8);
auto quant = make_node<quantise, "quant" >(8);
auto comp = make_node<composite, "comp" >(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<cv::Mat, 2> 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;
}