Fixed bug when generating identical fanouts

This commit is contained in:
2026-05-09 08:36:51 +02:00
parent 2bca2a7554
commit 9ce581b5ce
5 changed files with 366 additions and 11 deletions
+288
View File
@@ -0,0 +1,288 @@
// 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<N> 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 <kpn/kpn.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iostream>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>
// ── 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<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;
}
// 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<N> ────────────────────────────────────────────────────────────
//
// 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<I> aliases cv::Mat for all I, so the pack expansion
// MatArg<0>, MatArg<1>, ..., MatArg<N-1>
// produces exactly N cv::Mat arguments — enough to drive the MainThreadNode
// base without manually spelling out the type N times.
template<std::size_t>
using MatArg = cv::Mat;
template<std::size_t N>
class DebugCanvas;
template<std::size_t N, typename Seq = std::make_index_sequence<N>>
struct DebugCanvasBase;
template<std::size_t N, std::size_t... Is>
struct DebugCanvasBase<N, std::index_sequence<Is...>> {
using type = kpn::MainThreadNode<DebugCanvas<N>, kpn::in<>, MatArg<Is>...>;
};
template<std::size_t N>
class DebugCanvas : public DebugCanvasBase<N>::type {
using Base = typename DebugCanvasBase<N>::type;
public:
static constexpr std::string_view label() { return "debug_canvas"; }
static constexpr std::size_t unique_tag = 0;
explicit DebugCanvas(std::vector<std::string> 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<typename... Ms>
bool operator()(Ms&&... mats) {
static_assert(sizeof...(Ms) == N, "DebugCanvas: wrong number of inputs");
std::vector<cv::Mat> imgs;
imgs.reserve(N);
(imgs.push_back(to_bgr(std::forward<Ms>(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<std::string> 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<cv::Mat>& 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<capture, "capture" >(4);
auto bilateral= make_node<bilateral_filter, "bilateral" >(4);
auto quant = make_node<quantise, "quant" >(4);
auto gray = make_node<to_gray, "to_gray" >(4);
auto blur = make_node<gaussian_blur, "blur" >(4);
auto canny = make_node<canny_edges, "canny" >(4);
auto dilate = make_node<dilate_edges, "dilate" >(4);
auto comp = make_node<composite, "comp" >(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;
}