218 lines
8.6 KiB
C++
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;
|
|
}
|