// ============================================================ // vqt6/_bridge_canvas.cpp — 自绘控件桥接实现 // // * Vqt6Canvas 手绘板 / 28x28 灰度显示 // * Vqt6Graph3D 软件 3D 神经网络结构图(透视投影 + 画家算法排序) // * ChartView Qt Charts 折线图 // // 三个控件都不使用 Q_OBJECT / 信号, 回调统一经 vqt6_signal_register // 拿到 slot_id, 事件发生时 vqt6_trigger_slot(slot_id) 回调 Viper。 // ============================================================ #include "_bridge.h" #include "_bridge_canvas.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ============================================================ // 公共小工具 // ============================================================ namespace { inline double ClampD(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi : v); } // 自绘控件统一的方形绘制区(居中): 返回边长与左上角偏移 inline double SquareSide(int w, int h, double* ox, double* oy) { double side = (w < h) ? static_cast(w) : static_cast(h); *ox = (static_cast(w) - side) * 0.5; *oy = (static_cast(h) - side) * 0.5; return side; } } // namespace // ============================================================ // Vqt6Canvas — 手绘板 / 28x28 灰度显示 // ============================================================ namespace { class Vqt6Canvas : public QWidget { public: explicit Vqt6Canvas(QWidget* parent = nullptr) : QWidget(parent) { setMinimumSize(200, 200); setAttribute(Qt::WA_OpaquePaintEvent, true); } void Clear() { strokes_.clear(); mode_ = kModeDraw; update(); } bool HasInk() const { return !strokes_.empty(); } void SetMode(int m) { mode_ = (m != 0) ? kModeShow : kModeDraw; update(); } int Mode() const { return mode_; } void SetPen(const QColor& c, double width_norm) { penColor_ = c; penWidth_ = ClampD(width_norm, 0.005, 0.4); update(); } void SetGrid(bool on) { grid_ = on; update(); } void SetScanLine(int y_norm) { scanY_ = y_norm; update(); } void SetBg(const QColor& c) { bg_ = c; update(); } // 手绘 → 28x28 灰度(MNIST 预处理); 返回是否有墨迹 int ToMnist28(unsigned char* out) { if (out == nullptr || strokes_.empty()) return 0; const int kSuper = 4; // 超采样倍数 const int kSide = kMnist * kSuper; // 112 QImage hi(kSide, kSide, QImage::Format_Grayscale8); hi.fill(0); { QPainter p(&hi); p.setRenderHint(QPainter::Antialiasing, true); QPen pen(QColor(255, 255, 255)); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); double pw = penWidth_ * kSide; if (pw < kSuper) pw = kSuper; pen.setWidthF(pw); p.setPen(pen); p.setBrush(Qt::NoBrush); for (size_t s = 0; s < strokes_.size(); ++s) { const std::vector& st = strokes_[s]; if (st.empty()) continue; if (st.size() == 1) { p.drawPoint(QPointF(st[0].x() * kSide, st[0].y() * kSide)); continue; } QPainterPath path(QPointF(st[0].x() * kSide, st[0].y() * kSide)); for (size_t i = 1; i < st.size(); ++i) { path.lineTo(QPointF(st[i].x() * kSide, st[i].y() * kSide)); } p.drawPath(path); } } // 4x4 盒式降采样 double raw[kMnist][kMnist]; double maxv = 0.0; for (int y = 0; y < kMnist; ++y) { for (int x = 0; x < kMnist; ++x) { double acc = 0.0; for (int dy = 0; dy < kSuper; ++dy) { for (int dx = 0; dx < kSuper; ++dx) { QRgb px = hi.pixel(x * kSuper + dx, y * kSuper + dy); acc += static_cast(qGray(px)); } } double v = acc / (kSuper * kSuper); raw[y][x] = v; if (v > maxv) maxv = v; } } if (maxv <= 0.0) return 0; for (int y = 0; y < kMnist; ++y) { for (int x = 0; x < kMnist; ++x) raw[y][x] /= maxv; } // 包围盒 int minX = kMnist, maxX = -1, minY = kMnist, maxY = -1; for (int y = 0; y < kMnist; ++y) { for (int x = 0; x < kMnist; ++x) { if (raw[y][x] > 0.02) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } } if (maxX < minX || maxY < minY) return 0; // 包围盒长边缩放到 20x20(对齐 MNIST 官方预处理) int bw = maxX - minX + 1; int bh = maxY - minY + 1; double scale = 20.0 / static_cast(bw > bh ? bw : bh); double cx0 = (minX + maxX) * 0.5; double cy0 = (minY + maxY) * 0.5; double tmp[kMnist][kMnist]; std::memset(tmp, 0, sizeof(tmp)); for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { double v = raw[y][x]; if (v <= 0.0) continue; int tx = static_cast(std::lround((x - cx0) * scale + 14.0)); int ty = static_cast(std::lround((y - cy0) * scale + 14.0)); if (tx < 0 || tx >= kMnist || ty < 0 || ty >= kMnist) continue; if (v > tmp[ty][tx]) tmp[ty][tx] = v; } } // 质心居中 double sum = 0.0, mx = 0.0, my = 0.0; for (int y = 0; y < kMnist; ++y) { for (int x = 0; x < kMnist; ++x) { sum += tmp[y][x]; mx += tmp[y][x] * x; my += tmp[y][x] * y; } } if (sum <= 0.0) return 0; mx /= sum; my /= sum; int shiftX = static_cast(std::lround(14.0 - mx)); int shiftY = static_cast(std::lround(14.0 - my)); unsigned char dst[kMnist][kMnist]; std::memset(dst, 0, sizeof(dst)); for (int y = 0; y < kMnist; ++y) { for (int x = 0; x < kMnist; ++x) { double v = tmp[y][x]; if (v <= 0.0) continue; int tx = x + shiftX; int ty = y + shiftY; if (tx < 0 || tx >= kMnist || ty < 0 || ty >= kMnist) continue; int g = static_cast(std::lround(v * 255.0)); dst[ty][tx] = static_cast(g < 0 ? 0 : (g > 255 ? 255 : g)); } } std::memcpy(out, dst, kMnist * kMnist); return 1; } void SetImage28(const unsigned char* in) { if (in == nullptr) return; QImage img(kMnist, kMnist, QImage::Format_Grayscale8); std::memcpy(img.bits(), in, kMnist * kMnist); image_ = img; mode_ = kModeShow; update(); } int slotId = -1; vqt6_slot_callback_t changedCb = nullptr; void* userData = nullptr; protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.fillRect(rect(), bg_); double ox = 0.0, oy = 0.0; double side = SquareSide(width(), height(), &ox, &oy); if (side <= 1.0) return; p.setRenderHint(QPainter::Antialiasing, true); if (mode_ == kModeShow && !image_.isNull()) { // 图像显示: 最近邻放大, 保持像素感 p.setRenderHint(QPainter::SmoothPixmapTransform, false); QRectF dstRect(ox, oy, side, side); p.drawImage(dstRect, image_); } else { // 手绘: 归一化坐标 → 方形绘制区 QPen pen(penColor_); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); pen.setWidthF(penWidth_ * side); p.setPen(pen); p.setBrush(Qt::NoBrush); for (size_t s = 0; s < strokes_.size(); ++s) { const std::vector& st = strokes_[s]; if (st.empty()) continue; if (st.size() == 1) { p.drawPoint(QPointF(ox + st[0].x() * side, oy + st[0].y() * side)); continue; } QPainterPath path(QPointF(ox + st[0].x() * side, oy + st[0].y() * side)); for (size_t i = 1; i < st.size(); ++i) { path.lineTo(QPointF(ox + st[i].x() * side, oy + st[i].y() * side)); } p.drawPath(path); } } // 28x28 辅助网格 if (grid_) { QPen gp(QColor(90, 100, 120, 90)); gp.setWidthF(1.0); p.setPen(gp); for (int i = 1; i < kMnist; ++i) { double t = static_cast(i) / kMnist; p.drawLine(QPointF(ox + t * side, oy), QPointF(ox + t * side, oy + side)); p.drawLine(QPointF(ox, oy + t * side), QPointF(ox + side, oy + t * side)); } } // 扫描线(推理动画) if (scanY_ >= 0) { double t = ClampD(static_cast(scanY_) / 1000.0, 0.0, 1.0); QPen sp(QColor(255, 214, 102, 220)); sp.setWidthF(2.0); p.setPen(sp); p.drawLine(QPointF(ox, oy + t * side), QPointF(ox + side, oy + t * side)); } // 边框 QPen bp(QColor(70, 78, 92)); bp.setWidthF(1.0); p.setPen(bp); p.setBrush(Qt::NoBrush); p.drawRect(QRectF(ox, oy, side, side)); } void mousePressEvent(QMouseEvent* e) override { if (mode_ != kModeDraw || e->button() != Qt::LeftButton) return; strokes_.push_back(std::vector()); AppendPoint(e->position()); } void mouseMoveEvent(QMouseEvent* e) override { if (mode_ != kModeDraw) return; if (!(e->buttons() & Qt::LeftButton)) return; AppendPoint(e->position()); } void mouseReleaseEvent(QMouseEvent* e) override { if (mode_ != kModeDraw || e->button() != Qt::LeftButton) return; if (slotId >= 0) vqt6_trigger_slot(slotId); } private: void AppendPoint(const QPointF& pos) { if (strokes_.empty()) return; double ox = 0.0, oy = 0.0; double side = SquareSide(width(), height(), &ox, &oy); if (side <= 1.0) return; double nx = ClampD((pos.x() - ox) / side, 0.0, 1.0); double ny = ClampD((pos.y() - oy) / side, 0.0, 1.0); strokes_.back().push_back(QPointF(nx, ny)); update(); } static const int kModeDraw = 0; static const int kModeShow = 1; static const int kMnist = 28; std::vector > strokes_; QImage image_; QColor penColor_ = QColor(255, 255, 255); QColor bg_ = QColor(12, 14, 18); double penWidth_ = 0.07; bool grid_ = false; int scanY_ = -1; int mode_ = kModeDraw; }; } // namespace extern "C" VQT6_API void* vqt6_canvas_create(void) { return new Vqt6Canvas(); } extern "C" VQT6_API void* vqt6_canvas_create_with_parent(void* parent) { if (parent == nullptr) return nullptr; return new Vqt6Canvas(static_cast(parent)); } extern "C" VQT6_API void vqt6_canvas_clear(void* canvas) { if (canvas == nullptr) return; static_cast(canvas)->Clear(); } extern "C" VQT6_API int vqt6_canvas_has_ink(void* canvas) { if (canvas == nullptr) return 0; return static_cast(canvas)->HasInk() ? 1 : 0; } extern "C" VQT6_API void vqt6_canvas_set_mode(void* canvas, int mode) { if (canvas == nullptr) return; static_cast(canvas)->SetMode(mode); } extern "C" VQT6_API int vqt6_canvas_mode(void* canvas) { if (canvas == nullptr) return 0; return static_cast(canvas)->Mode(); } extern "C" VQT6_API void vqt6_canvas_set_pen(void* canvas, int r, int g, int b, double width_norm) { if (canvas == nullptr) return; static_cast(canvas)->SetPen(QColor(r, g, b), width_norm); } extern "C" VQT6_API void vqt6_canvas_set_grid(void* canvas, int on) { if (canvas == nullptr) return; static_cast(canvas)->SetGrid(on != 0); } extern "C" VQT6_API void vqt6_canvas_set_scan_line(void* canvas, int y_norm) { if (canvas == nullptr) return; static_cast(canvas)->SetScanLine(y_norm); } extern "C" VQT6_API void vqt6_canvas_set_bg(void* canvas, int r, int g, int b) { if (canvas == nullptr) return; static_cast(canvas)->SetBg(QColor(r, g, b)); } extern "C" VQT6_API int vqt6_canvas_to_mnist28(void* canvas, unsigned char* out) { if (canvas == nullptr) return 0; return static_cast(canvas)->ToMnist28(out); } extern "C" VQT6_API void vqt6_canvas_set_image28(void* canvas, const unsigned char* in) { if (canvas == nullptr) return; static_cast(canvas)->SetImage28(in); } extern "C" VQT6_API void vqt6_canvas_connect_changed(void* canvas, vqt6_slot_callback_t cb, void* user_data) { if (canvas == nullptr || cb == nullptr) return; Vqt6Canvas* c = static_cast(canvas); c->changedCb = cb; c->userData = user_data; c->slotId = vqt6_signal_register(cb, user_data); } // ============================================================ // Vqt6Graph3D — 软件 3D 神经网络结构图 // ============================================================ namespace { struct GraphNode { double x, y, z; int layer; double value; }; struct GraphEdge { int a, b; double w; double norm; // |w| / max|w| }; struct GraphProj { double sx, sy; double depth; // 相机距离, 越大越远 double scale; bool ok; }; class Vqt6Graph3D : public QWidget { public: explicit Vqt6Graph3D(QWidget* parent = nullptr) : QWidget(parent) { setMinimumSize(320, 240); setAttribute(Qt::WA_OpaquePaintEvent, true); setMouseTracking(true); } void Setup(const int* sizes, int n_layers) { nodes_.clear(); edges_.clear(); layerStart_.clear(); layerCount_.clear(); layerAlpha_.clear(); edgeDraw_.clear(); if (sizes == nullptr || n_layers <= 0) { update(); return; } // 各层沿 Z 轴等距排布(输入层在最远端) for (int li = 0; li < n_layers; ++li) { int cnt = sizes[li]; if (cnt < 0) cnt = 0; layerStart_.push_back(static_cast(nodes_.size())); layerCount_.push_back(cnt); layerAlpha_.push_back(1.0); double z = (n_layers > 1) ? (-1.1 + 2.2 * static_cast(li) / static_cast(n_layers - 1)) : 0.0; // 近正方形网格布局 int cols = static_cast(std::ceil(std::sqrt(static_cast(cnt > 0 ? cnt : 1)))); if (cols < 1) cols = 1; int rows = (cnt + cols - 1) / cols; if (rows < 1) rows = 1; double spacing = 1.6 / static_cast(cols > rows ? cols : rows); for (int i = 0; i < cnt; ++i) { int cx = i % cols; int cy = i / cols; GraphNode n; n.x = (static_cast(cx) - (cols - 1) * 0.5) * spacing; n.y = (static_cast(cy) - (rows - 1) * 0.5) * spacing; n.z = z; n.layer = li; n.value = 0.0; nodes_.push_back(n); } } // 相邻层前向全连接 for (int li = 0; li + 1 < n_layers; ++li) { int aStart = layerStart_[li], aCnt = layerCount_[li]; int bStart = layerStart_[li + 1], bCnt = layerCount_[li + 1]; for (int i = 0; i < aCnt; ++i) { for (int j = 0; j < bCnt; ++j) { GraphEdge e; e.a = aStart + i; e.b = bStart + j; e.w = 0.0; e.norm = 0.0; edges_.push_back(e); } } } RebuildEdgeDraw(); update(); } void Clear() { nodes_.clear(); edges_.clear(); layerStart_.clear(); layerCount_.clear(); layerAlpha_.clear(); edgeDraw_.clear(); update(); } int NodeCount() const { return static_cast(nodes_.size()); } int EdgeCount() const { return static_cast(edges_.size()); } void SetLayerAlpha(int layer, double a) { if (layer < 0 || layer >= static_cast(layerAlpha_.size())) return; layerAlpha_[layer] = ClampD(a, 0.0, 1.0); update(); } void SetAllLayerAlpha(double a) { for (size_t i = 0; i < layerAlpha_.size(); ++i) layerAlpha_[i] = ClampD(a, 0.0, 1.0); update(); } void SetValues(const double* v, int n) { if (v == nullptr) return; int cnt = (n < static_cast(nodes_.size())) ? n : static_cast(nodes_.size()); for (int i = 0; i < cnt; ++i) nodes_[i].value = ClampD(v[i], 0.0, 1.0); update(); } void SetEdgeWeights(const double* w, int n) { if (w == nullptr) return; int cnt = (n < static_cast(edges_.size())) ? n : static_cast(edges_.size()); double maxAbs = 0.0; for (int i = 0; i < cnt; ++i) { edges_[i].w = w[i]; double a = std::fabs(w[i]); if (a > maxAbs) maxAbs = a; } for (int i = 0; i < cnt; ++i) { edges_[i].norm = (maxAbs > 0.0) ? (std::fabs(edges_[i].w) / maxAbs) : 0.0; } RebuildEdgeDraw(); update(); } void SetEdgeThreshold(double t) { edgeThreshold_ = ClampD(t, 0.0, 1.0); RebuildEdgeDraw(); update(); } void SetEdgeBudget(int k) { edgeBudget_ = (k > 0) ? k : 0; RebuildEdgeDraw(); update(); } void SetView(double yaw, double pitch, double dist) { yaw_ = yaw; pitch_ = ClampD(pitch, -85.0, 85.0); dist_ = ClampD(dist, 0.6, 20.0); update(); } void GetView(double* yaw, double* pitch, double* dist) const { if (yaw != nullptr) *yaw = yaw_; if (pitch != nullptr) *pitch = pitch_; if (dist != nullptr) *dist = dist_; } void ResetView() { yaw_ = -28.0; pitch_ = 16.0; dist_ = 3.4; update(); } void SetBg(const QColor& c) { bg_ = c; update(); } void SetNodeScale(double s) { nodeScale_ = ClampD(s, 0.1, 5.0); update(); } int slotId = -1; vqt6_slot_callback_t viewCb = nullptr; void* userData = nullptr; protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.fillRect(rect(), bg_); const int w = width(); const int h = height(); if (w < 8 || h < 8) return; p.setRenderHint(QPainter::Antialiasing, true); if (nodes_.empty()) { p.setPen(QColor(120, 130, 148)); p.drawText(rect(), Qt::AlignCenter, QStringLiteral("尚无网络结构")); return; } const double px = (w < h ? w : h) * 0.34; // 单位长度 → 像素 const double cx = w * 0.5; const double cy = h * 0.5; const double focal = 1.6; const double ry = yaw_ * kPi / 180.0; const double rx = pitch_ * kPi / 180.0; const double sy = std::sin(ry), cyw = std::cos(ry); const double sx = std::sin(rx), cxw = std::cos(rx); // 投影全部节点 std::vector proj(nodes_.size()); for (size_t i = 0; i < nodes_.size(); ++i) { const GraphNode& n = nodes_[i]; double x1 = n.x * cyw + n.z * sy; double z1 = -n.x * sy + n.z * cyw; double y2 = n.y * cxw - z1 * sx; double z2 = n.y * sx + z1 * cxw; double depth = dist_ - z2; GraphProj gp; if (depth < 0.15) { gp.ok = false; gp.sx = gp.sy = 0.0; gp.depth = 1e9; gp.scale = 0.0; } else { double sc = focal / depth; gp.ok = true; gp.sx = cx + x1 * sc * px; gp.sy = cy - y2 * sc * px; gp.depth = depth; gp.scale = sc; } proj[i] = gp; } // ---- 边: 先画远的 ---- std::vector eorder(edgeDraw_.begin(), edgeDraw_.end()); std::sort(eorder.begin(), eorder.end(), [&](int a, int b) { const GraphEdge& ea = edges_[a]; const GraphEdge& eb = edges_[b]; double da = proj[ea.a].depth + proj[ea.b].depth; double db = proj[eb.a].depth + proj[eb.b].depth; return da > db; }); p.setBrush(Qt::NoBrush); for (size_t k = 0; k < eorder.size(); ++k) { const GraphEdge& e = edges_[eorder[k]]; const GraphProj& pa = proj[e.a]; const GraphProj& pb = proj[e.b]; if (!pa.ok || !pb.ok) continue; double la = LayerAlphaOf(e.a); double lb = LayerAlphaOf(e.b); double la2 = (la < lb) ? la : lb; if (la2 <= 0.01) continue; double depthFall = ClampD(1.35 - (pa.depth + pb.depth) * 0.5 / dist_, 0.15, 1.0); int alpha = static_cast(std::lround((0.08 + 0.42 * e.norm) * la2 * depthFall * 255.0)); if (alpha < 4) continue; QColor col = (e.w >= 0.0) ? QColor(255, 107, 107, alpha) : QColor(77, 171, 247, alpha); QPen pen(col); pen.setWidthF(1.0); p.setPen(pen); p.drawLine(QPointF(pa.sx, pa.sy), QPointF(pb.sx, pb.sy)); } // ---- 节点: 由远及近 ---- std::vector norder(nodes_.size()); for (size_t i = 0; i < norder.size(); ++i) norder[i] = static_cast(i); std::sort(norder.begin(), norder.end(), [&](int a, int b) { return proj[a].depth > proj[b].depth; }); for (size_t k = 0; k < norder.size(); ++k) { int idx = norder[k]; const GraphNode& n = nodes_[idx]; const GraphProj& gp = proj[idx]; if (!gp.ok) continue; double la = LayerAlphaOf(idx); if (la <= 0.01) continue; double r = 0.020 * px * nodeScale_ * (0.55 + 0.95 * n.value) * gp.scale; if (r < 1.2) r = 1.2; QColor base = LayerColor(n.layer); double depthFall = ClampD(1.3 - gp.depth / dist_, 0.25, 1.0); int a = static_cast(std::lround((0.28 + 0.72 * n.value) * la * depthFall * 255.0)); QColor col(base.red(), base.green(), base.blue(), a < 0 ? 0 : (a > 255 ? 255 : a)); p.setPen(Qt::NoPen); p.setBrush(col); p.drawEllipse(QPointF(gp.sx, gp.sy), r, r); } // ---- 输出层数字标签 ---- int last = static_cast(layerCount_.size()) - 1; if (last >= 0 && LayerAlphaOf(layerStart_[last]) > 0.25) { int a = static_cast(std::lround(LayerAlphaOf(layerStart_[last]) * 255.0)); QFont f = p.font(); f.setPointSizeF(9.0); f.setBold(true); p.setFont(f); int base = layerStart_[last]; int cnt = layerCount_[last]; for (int i = 0; i < cnt; ++i) { const GraphProj& gp = proj[base + i]; if (!gp.ok) continue; p.setPen(QColor(255, 214, 102, a)); p.drawText(QPointF(gp.sx + 7.0, gp.sy + 4.0), QString::number(i)); } } // ---- 层标题 ---- const char* kNames[] = { "输入", "隐藏", "输出" }; QFont sf = p.font(); sf.setPointSizeF(8.5); p.setFont(sf); for (size_t li = 0; li < layerCount_.size(); ++li) { double z = LayerZ(static_cast(li)); double x1 = 0.0 * cyw + z * sy; double z1 = -0.0 * sy + z * cyw; double y2 = (-0.95) * cxw - z1 * sx; double z2 = (-0.95) * sx + z1 * cxw; double depth = dist_ - z2; if (depth < 0.15) continue; double sc = focal / depth; double txx = cx + x1 * sc * px; double tyy = cy - y2 * sc * px; int a = static_cast(std::lround(LayerAlphaOf(layerStart_[li]) * 210.0)); if (a < 10) a = 10; QString cap; if (li < 3) cap = QString::fromUtf8(kNames[li]); else cap = QStringLiteral("层"); cap += QStringLiteral(" %1").arg(layerCount_[li]); p.setPen(QColor(150, 160, 180, a)); p.drawText(QPointF(txx - 22.0, tyy), cap); } } void mousePressEvent(QMouseEvent* e) override { if (e->button() != Qt::LeftButton) return; dragging_ = true; lastPos_ = e->position(); } void mouseMoveEvent(QMouseEvent* e) override { if (!dragging_) return; QPointF d = e->position() - lastPos_; lastPos_ = e->position(); yaw_ += d.x() * 0.45; pitch_ = ClampD(pitch_ + d.y() * 0.35, -85.0, 85.0); update(); if (slotId >= 0) vqt6_trigger_slot(slotId); } void mouseReleaseEvent(QMouseEvent* e) override { if (e->button() != Qt::LeftButton) return; dragging_ = false; if (slotId >= 0) vqt6_trigger_slot(slotId); } void wheelEvent(QWheelEvent* e) override { double steps = e->angleDelta().y() / 120.0; dist_ = ClampD(dist_ * std::pow(1.12, -steps), 0.6, 20.0); update(); if (slotId >= 0) vqt6_trigger_slot(slotId); } private: static constexpr double kPi = 3.14159265358979323846; double LayerZ(int li) const { int n = static_cast(layerCount_.size()); if (n <= 1) return 0.0; return -1.1 + 2.2 * static_cast(li) / static_cast(n - 1); } double LayerAlphaOf(int nodeIndex) const { if (nodeIndex < 0 || nodeIndex >= static_cast(nodes_.size())) return 1.0; int li = nodes_[nodeIndex].layer; if (li < 0 || li >= static_cast(layerAlpha_.size())) return 1.0; return layerAlpha_[li]; } QColor LayerColor(int layer) const { if (layer == 0) return QColor(200, 210, 225); // 输入层: 冷白 int last = static_cast(layerCount_.size()) - 1; if (layer == last) return QColor(255, 190, 70); // 输出层: 琥珀 return QColor(90, 200, 190); // 隐藏层: 青 } // 按阈值 + 预算重建待画边索引(|w| 大的优先) void RebuildEdgeDraw() { edgeDraw_.clear(); std::vector cand; for (size_t i = 0; i < edges_.size(); ++i) { if (edges_[i].norm >= edgeThreshold_ && edges_[i].norm > 0.0) { cand.push_back(static_cast(i)); } } if (edgeBudget_ > 0 && static_cast(cand.size()) > edgeBudget_) { std::partial_sort(cand.begin(), cand.begin() + edgeBudget_, cand.end(), [&](int a, int b) { return edges_[a].norm > edges_[b].norm; }); cand.resize(static_cast(edgeBudget_)); } edgeDraw_ = cand; } std::vector nodes_; std::vector edges_; std::vector layerStart_; std::vector layerCount_; std::vector layerAlpha_; std::vector edgeDraw_; double edgeThreshold_ = 0.0; int edgeBudget_ = 3000; double yaw_ = -28.0; double pitch_ = 16.0; double dist_ = 3.4; double nodeScale_ = 1.0; QColor bg_ = QColor(14, 16, 22); bool dragging_ = false; QPointF lastPos_; }; } // namespace extern "C" VQT6_API void* vqt6_graph3d_create(void) { return new Vqt6Graph3D(); } extern "C" VQT6_API void* vqt6_graph3d_create_with_parent(void* parent) { if (parent == nullptr) return nullptr; return new Vqt6Graph3D(static_cast(parent)); } extern "C" VQT6_API void vqt6_graph3d_setup(void* g, const int* layer_sizes, int n_layers) { if (g == nullptr) return; static_cast(g)->Setup(layer_sizes, n_layers); } extern "C" VQT6_API void vqt6_graph3d_clear(void* g) { if (g == nullptr) return; static_cast(g)->Clear(); } extern "C" VQT6_API int vqt6_graph3d_node_count(void* g) { if (g == nullptr) return 0; return static_cast(g)->NodeCount(); } extern "C" VQT6_API int vqt6_graph3d_edge_count(void* g) { if (g == nullptr) return 0; return static_cast(g)->EdgeCount(); } extern "C" VQT6_API void vqt6_graph3d_set_layer_alpha(void* g, int layer, double alpha) { if (g == nullptr) return; static_cast(g)->SetLayerAlpha(layer, alpha); } extern "C" VQT6_API void vqt6_graph3d_set_all_layer_alpha(void* g, double alpha) { if (g == nullptr) return; static_cast(g)->SetAllLayerAlpha(alpha); } extern "C" VQT6_API void vqt6_graph3d_set_values(void* g, const double* values, int n) { if (g == nullptr) return; static_cast(g)->SetValues(values, n); } extern "C" VQT6_API void vqt6_graph3d_set_edge_weights(void* g, const double* w, int n) { if (g == nullptr) return; static_cast(g)->SetEdgeWeights(w, n); } extern "C" VQT6_API void vqt6_graph3d_set_edge_threshold(void* g, double t) { if (g == nullptr) return; static_cast(g)->SetEdgeThreshold(t); } extern "C" VQT6_API void vqt6_graph3d_set_edge_budget(void* g, int max_edges) { if (g == nullptr) return; static_cast(g)->SetEdgeBudget(max_edges); } extern "C" VQT6_API void vqt6_graph3d_set_view(void* g, double yaw_deg, double pitch_deg, double dist) { if (g == nullptr) return; static_cast(g)->SetView(yaw_deg, pitch_deg, dist); } extern "C" VQT6_API void vqt6_graph3d_get_view(void* g, double* yaw_deg, double* pitch_deg, double* dist) { if (g == nullptr) return; static_cast(g)->GetView(yaw_deg, pitch_deg, dist); } extern "C" VQT6_API void vqt6_graph3d_set_bg(void* g, int r, int gcol, int b) { if (g == nullptr) return; static_cast(g)->SetBg(QColor(r, gcol, b)); } extern "C" VQT6_API void vqt6_graph3d_set_node_scale(void* g, double s) { if (g == nullptr) return; static_cast(g)->SetNodeScale(s); } extern "C" VQT6_API void vqt6_graph3d_connect_view_changed(void* g, vqt6_slot_callback_t cb, void* user_data) { if (g == nullptr || cb == nullptr) return; Vqt6Graph3D* gp = static_cast(g); gp->viewCb = cb; gp->userData = user_data; gp->slotId = vqt6_signal_register(cb, user_data); } extern "C" VQT6_API void vqt6_graph3d_reset_view(void* g) { if (g == nullptr) return; static_cast(g)->ResetView(); } // ============================================================ // ChartView — Qt Charts 折线图 // ============================================================ namespace { struct ChartAxes { QValueAxis* x; QValueAxis* yLeft; QValueAxis* yRight; }; QHash& ChartAxesMap() { static QHash m; return m; } QHash& SeriesSideMap() { static QHash m; return m; } // 每条曲线的滑动窗口上限(0 = 不限制), 由 vqt6_chartview_set_max_points 设置 QHash& MaxPointsMap() { static QHash m; return m; } // 放大窗: 每个 ChartView 至多一个, 建一次后复用 // (不设 WA_DeleteOnClose —— QChartView 被 delete 时 Qt Charts 的销毁路径很脆, // 关闭大窗只要隐藏即可, 下次点"查看大图"直接复用) struct ZoomView { QMainWindow* win; QChartView* view; QChart* chart; QValueAxis* x; QValueAxis* yLeft; QValueAxis* yRight; QList series; // 与源图 series 顺序一一对应 ZoomView() : win(nullptr), view(nullptr), chart(nullptr), x(nullptr), yLeft(nullptr), yRight(nullptr) {} }; QHash& ZoomMap() { static QHash m; return m; } // 把源图当前所有曲线与轴范围整体复制到放大窗(替换式, 不是追加) void SyncZoomView(QChartView* src, ZoomView& z) { if (z.chart == nullptr || z.win == nullptr) return; if (!z.win->isVisible()) return; // 关着就不做无谓的拷贝 const QList all = src->chart()->series(); if (z.series.size() != all.size()) return; for (int i = 0; i < all.size(); ++i) { QLineSeries* ls = qobject_cast(all.at(i)); if (ls == nullptr) continue; z.series.at(i)->replace(ls->points()); } ChartAxes sa = ChartAxesMap().value(src); if (sa.x != nullptr) z.x->setRange(sa.x->min(), sa.x->max()); if (sa.yLeft != nullptr) z.yLeft->setRange(sa.yLeft->min(), sa.yLeft->max()); if (sa.yRight != nullptr && sa.yRight->isVisible()) { z.yRight->setRange(sa.yRight->min(), sa.yRight->max()); z.yRight->setVisible(true); } } } // namespace extern "C" VQT6_API void* vqt6_chartview_create(void) { QChart* chart = new QChart(); QChartView* view = new QChartView(chart); view->setRenderHint(QPainter::Antialiasing, true); chart->legend()->setVisible(true); chart->legend()->setAlignment(Qt::AlignBottom); chart->setMargins(QMargins(4, 4, 4, 4)); chart->setBackgroundRoundness(0); ChartAxes ax; ax.x = new QValueAxis(); ax.x->setTitleText(QStringLiteral("step")); ax.yLeft = new QValueAxis(); ax.yRight = new QValueAxis(); ax.yRight->setVisible(false); chart->addAxis(ax.x, Qt::AlignBottom); chart->addAxis(ax.yLeft, Qt::AlignLeft); chart->addAxis(ax.yRight, Qt::AlignRight); ChartAxesMap().insert(view, ax); return view; } extern "C" VQT6_API void* vqt6_chartview_create_with_parent(void* parent) { if (parent == nullptr) return nullptr; void* view = vqt6_chartview_create(); static_cast(view)->setParent(static_cast(parent)); return view; } extern "C" VQT6_API void* vqt6_chartview_add_line(void* view, const char* name, int r, int g, int b, int right_axis) { if (view == nullptr) return nullptr; QChartView* cv = static_cast(view); ChartAxes axes = ChartAxesMap().value(cv); if (axes.x == nullptr) return nullptr; QLineSeries* series = new QLineSeries(); if (name != nullptr) series->setName(QString::fromUtf8(name)); QPen pen(QColor(r, g, b)); pen.setWidthF(2.0); series->setPen(pen); cv->chart()->addSeries(series); series->attachAxis(axes.x); if (right_axis != 0) { series->attachAxis(axes.yRight); axes.yRight->setVisible(true); } else { series->attachAxis(axes.yLeft); } SeriesSideMap().insert(series, right_axis != 0 ? 1 : 0); return series; } extern "C" VQT6_API void vqt6_chartview_append(void* view, void* series, double x, double y) { if (view == nullptr || series == nullptr) return; QLineSeries* ls = static_cast(series); ls->append(x, y); // 滑动窗口: 超出上限就丢掉最旧的点, 避免长时间训练后曲线挤成一团 const int maxp = MaxPointsMap().value(static_cast(view), 0); if (maxp > 0) { const int extra = ls->count() - maxp; if (extra > 0) ls->removePoints(0, extra); } } extern "C" VQT6_API void vqt6_chartview_clear_series(void* view, void* series) { if (series == nullptr) return; static_cast(series)->clear(); } extern "C" VQT6_API void vqt6_chartview_clear_all(void* view) { if (view == nullptr) return; QChartView* cv = static_cast(view); const QList all = cv->chart()->series(); for (int i = 0; i < all.size(); ++i) { QLineSeries* ls = qobject_cast(all.at(i)); if (ls != nullptr) ls->clear(); } } extern "C" VQT6_API void vqt6_chartview_autoscale(void* view) { if (view == nullptr) return; QChartView* cv = static_cast(view); ChartAxes axes = ChartAxesMap().value(cv); if (axes.x == nullptr) return; const QList all = cv->chart()->series(); double xmin = 1e300, xmax = -1e300; double lmin = 1e300, lmax = -1e300; double rmin = 1e300, rmax = -1e300; bool hasRight = false; for (int i = 0; i < all.size(); ++i) { QLineSeries* ls = qobject_cast(all.at(i)); if (ls == nullptr) continue; int side = SeriesSideMap().value(ls, 0); QVector pts = ls->points(); for (int k = 0; k < pts.size(); ++k) { double x = pts.at(k).x(); double y = pts.at(k).y(); if (x < xmin) xmin = x; if (x > xmax) xmax = x; if (side != 0) { hasRight = true; if (y < rmin) rmin = y; if (y > rmax) rmax = y; } else { if (y < lmin) lmin = y; if (y > lmax) lmax = y; } } } if (xmax < xmin) { xmin = 0.0; xmax = 1.0; } if (xmax - xmin < 1e-9) xmax = xmin + 1.0; axes.x->setRange(xmin, xmax); if (lmax >= lmin) { if (lmax - lmin < 1e-9) { lmin -= 0.5; lmax += 0.5; } axes.yLeft->setRange(lmin, lmax); } if (hasRight && rmax >= rmin) { if (rmax - rmin < 1e-9) { rmin -= 0.5; rmax += 0.5; } axes.yRight->setRange(rmin, rmax); axes.yRight->setVisible(true); } } extern "C" VQT6_API void vqt6_chartview_set_title(void* view, const char* utf8) { if (view == nullptr) return; QChartView* cv = static_cast(view); cv->chart()->setTitle(utf8 != nullptr ? QString::fromUtf8(utf8) : QString()); } extern "C" VQT6_API void vqt6_chartview_set_legend_visible(void* view, int visible) { if (view == nullptr) return; QChartView* cv = static_cast(view); cv->chart()->legend()->setVisible(visible != 0); } extern "C" VQT6_API void vqt6_chartview_set_background(void* view, int r, int g, int b) { if (view == nullptr) return; QChartView* cv = static_cast(view); QColor c(r, g, b); cv->chart()->setBackgroundVisible(true); cv->chart()->setBackgroundBrush(c); cv->chart()->setPlotAreaBackgroundVisible(true); cv->chart()->setPlotAreaBackgroundBrush(c.darker(115)); } extern "C" VQT6_API void vqt6_chartview_set_max_points(void* view, int max_points) { if (view == nullptr) return; MaxPointsMap().insert(static_cast(view), max_points); } extern "C" VQT6_API void* vqt6_chartview_open_zoom(void* view, const char* title) { if (view == nullptr) return nullptr; QChartView* src = static_cast(view); ZoomView& z = ZoomMap()[src]; if (z.win == nullptr) { QChart* chart = new QChart(); chart->legend()->setVisible(true); chart->legend()->setAlignment(Qt::AlignBottom); chart->setMargins(QMargins(6, 6, 6, 6)); chart->setBackgroundRoundness(0); chart->setBackgroundVisible(true); chart->setBackgroundBrush(src->chart()->backgroundBrush()); chart->setPlotAreaBackgroundVisible(true); chart->setPlotAreaBackgroundBrush(src->chart()->plotAreaBackgroundBrush()); QValueAxis* ax = new QValueAxis(); ax->setTitleText(QStringLiteral("step")); QValueAxis* yLeft = new QValueAxis(); QValueAxis* yRight = new QValueAxis(); yRight->setVisible(false); chart->addAxis(ax, Qt::AlignBottom); chart->addAxis(yLeft, Qt::AlignLeft); chart->addAxis(yRight, Qt::AlignRight); // 按源图的曲线条数建空曲线(同名/同色/同左右轴), 数据靠 SyncZoomView 拷贝 const QList all = src->chart()->series(); for (int i = 0; i < all.size(); ++i) { QLineSeries* ls = qobject_cast(all.at(i)); if (ls == nullptr) continue; QLineSeries* cp = new QLineSeries(); cp->setName(ls->name()); cp->setPen(ls->pen()); chart->addSeries(cp); cp->attachAxis(ax); if (SeriesSideMap().value(ls, 0) != 0) { cp->attachAxis(yRight); yRight->setVisible(true); } else { cp->attachAxis(yLeft); } z.series.append(cp); } QChartView* zv = new QChartView(chart); zv->setRenderHint(QPainter::Antialiasing, true); QMainWindow* win = new QMainWindow(); win->setCentralWidget(zv); win->resize(1100, 700); z.win = win; z.view = zv; z.chart = chart; z.x = ax; z.yLeft = yLeft; z.yRight = yRight; } z.win->setWindowTitle(title != nullptr ? QString::fromUtf8(title) : QStringLiteral("训练曲线")); // 先显示再同步: SyncZoomView 只在窗口可见时才拷贝, 首次打开也要有数据 z.win->show(); z.win->raise(); z.win->activateWindow(); SyncZoomView(src, z); return z.win; } extern "C" VQT6_API void vqt6_chartview_sync_zoom(void* view) { if (view == nullptr) return; QChartView* src = static_cast(view); if (!ZoomMap().contains(src)) return; SyncZoomView(src, ZoomMap()[src]); }