一套"特征类更精简 + 更内藏 "的重定义方案,把它定位成:当新增/更新特征值节点时,负责把"值节点"管理成可用的"特征摘要(区间/原型)",并提供特征比较结论。对外只暴露少量入口,其它都藏在内部流水线里。
1) 新的职责边界
特征类只干两件事(其它都不干)
- 写入时管理特征值节点
- 维护
当前值 - 维护
命中次数/最后观测时间 - 维护
近帧候选缓冲(用于稳态判定) - 把多次观测的值压缩成"区间特征摘要" (写入到
特征节点主信息)
- 比较两个特征节点
- 基于特征值关系:相等 / 相似 / 包含 / 部分包含 / 不相交
- 输出更上层结论:相同 / 相似 / 不同 / 无法比较
关键点:特征类不"拥有"值数据,不做"特征值仓库",不负责"查找存在"等宏观逻辑。它是一个"特征节点内部管理员 + 比较裁判"。
2) 特征节点主信息需要新增的"内藏字段"
建议在 特征节点主信息类 中增加一个"区间摘要槽",以及一个候选缓冲(ring/deque 都行):
cpp
// 建议放进 特征节点主信息类
struct 区间摘要I64 {
bool 有效 = false;
std::uint16_t 维度 = 0; // 1=标量, 3=xyz, 6=... 看你 VecI64 语义
VecI64 最小; // size = 维度
VecI64 最大; // size = 维度
std::uint32_t 稳态命中 = 0; // 连续(或近帧)命中次数
特征值节点类* 区间值节点 = nullptr; // 可选:若你希望区间也进入"特征值链"
};
struct 近帧候选项 {
时间戳 ts = 0;
特征值节点类* v = nullptr;
};
struct 特征节点主信息类 : 基础信息基类 {
词性节点类* 类型 = nullptr;
特征值节点类* 当前值 = nullptr;
std::uint64_t 命中次数 = 0;
时间戳 最后观测时间 = 0;
// 内藏:稳态与压缩
std::deque<近帧候选项> 近帧候选; // 建议上限 8~32
区间摘要I64 区间; // 先做 I64/VecI64 这类可区间化的
};
你之前提到"区间存储在特征节点主信息中",这里就是落点。
区间值节点是否创建,作为可选优化。
3) 对外 API 设计(尽量少)
写入入口(最常用的那条)
cpp
struct 特征写入参数 {
时间戳 now = 0;
std::uint32_t 候选窗口 = 12; // 近帧缓冲最大长度
std::uint32_t 稳态阈值 = 5; // 达到后认为区间有效
std::int64_t 容忍误差 = 5; // mm 或你定义的标量单位
bool 允许区间化 = true;
};
struct 特征写入结果 {
特征节点类* 特征 = nullptr;
特征值节点类* 当前值 = nullptr;
bool 区间更新 = false;
};
export class 特征类 {
public:
static 特征写入结果 写入观测特征(
基础信息节点类* 宿主,
词性节点类* 特征类型,
特征值节点类* 新值,
const 特征写入参数& p = {});
static bool 获取当前特征值(
基础信息节点类* 宿主,
词性节点类* 特征类型,
特征值载体& 输出);
// 比较入口(特征节点 vs 特征节点)
static 特征比较结果 比较(特征节点类* A, 特征节点类* B);
private:
// 全部内藏:候选缓冲、区间融合、关系判定、距离计算...
};
4) "区间融合"规则(内藏功能)
4.1 哪些值允许区间化?
-
I64 / U64 / VecI64(可解释为多维标量):强烈建议区间化
- 位置(3维)、尺寸(3维)、速度(3维)、时间戳(1维) 都能做
-
字符串:通常不区间化(除非你搞词典/正则桶)
-
VecIU64(轮廓金字塔/bit块):一般做"原型池/聚类",不做区间;区间会变成噪声海
4.2 区间的统一表示
把区间当成 AABB:
- 维度为
D 最小[D]、最大[D]- 新值
x[D]进入时:
最小[i] = min(最小[i], x[i])
最大[i] = max(最大[i], x[i])
"相似"的判定用容忍误差:
- 标量:
abs(a-b) <= 容忍误差 - 多维:
max_i abs(ai-bi) <= 容忍误差(L∞ 很适合做区间稳态)
4.3 稳态阈值
- 连续命中(或近帧窗口里高占比命中)达到
稳态阈值,将区间.有效=true - 一旦大幅跳变:重置区间为新值(或开一个"第二原型",这属于下一阶段)
5) 特征比较:关系 + 结论
5.1 枚举定义(建议)
cpp
enum class 枚举_特征关系 : std::int8_t {
未定义 = 0,
相等,
相似,
包含, // A 包含 B
被包含, // A 被 B 包含
部分包含, // 有交集但互不完全包含
不相交,
无法比较
};
enum class 枚举_特征结论 : std::int8_t {
未定义 = 0,
相同,
相似,
不同,
无法比较
};
struct 特征比较结果 {
枚举_特征结论 结论 = 枚举_特征结论::未定义;
枚举_特征关系 关系 = 枚举_特征关系::未定义;
std::int64_t 距离 = 0; // 你可以定义:标量差 / 区间间隙 / 海明距离...
};
5.2 关系判定优先级(非常实用)
比较时优先使用"摘要"(区间)而不是"瞬时当前值":
- 两边都
区间.有效:走区间关系
- 完全相同 → 相等 → 结论相同
- AABB 包含 → 包含/被包含 → 通常结论相似或相同(看你的语义)
- 有交集 → 部分包含 → 结论相似
- 无交集 → 不相交 → 结论不同(距离=区间间隙)
- 否则退化到
当前值:
-
同类型载体:
- I64/U64/VecI64:差值是否在阈值内 → 相似,否则不同
- VecIU64:用你现有"海明/轮廓策略距离" → 阈值化
- string:通常严格相等才算相同
-
类型都不一致:无法比较
6) 核心实现骨架(写入 + 区间融合 + 比较)
下面这段是可直接落到你工程里的实现方向(内部调用你已有的"找到或创建特征节点"即可),我把"读取特征值载体"的地方做成小工具,避免 vptr 写入但取值失败那类坑。
cpp
// ====== 内部工具:从 特征值节点类* 取出载体 ======
// 你需要按你工程里"特征值节点主信息类/特征值主信息类"的真实名字改一下 dynamic_cast 的目标类型
static bool 取载体(const 特征值节点类* v, 特征值载体& out) {
if (!v) return false;
auto* mi = dynamic_cast<const 特征值主信息类*>(v->主信息);
if (!mi) return false;
out = mi->值; // 你的主信息里如果字段名不是"值",这里替换
return true;
}
// 把载体转成"可区间化向量"(VecI64),标量也当 1 维
static bool 载体转VecI64(const 特征值载体& v, VecI64& out) {
out.clear();
if (std::holds_alternative<I64>(v)) { out.push_back(std::get<I64>(v)); return true; }
if (std::holds_alternative<U64>(v)) { out.push_back((I64)std::get<U64>(v)); return true; }
if (std::holds_alternative<VecI64>(v)) { out = std::get<VecI64>(v); return true; }
return false;
}
// ====== 区间更新:返回是否真的更新了区间 ======
static bool 更新区间(特征节点主信息类& fmi, const VecI64& x, const 特征写入参数& p) {
if (!p.允许区间化) return false;
if (x.empty()) return false;
auto& itv = fmi.区间;
const std::uint16_t D = (std::uint16_t)x.size();
auto within_tol = [&](const VecI64& a, const VecI64& b)->bool {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
if (std::llabs(a[i] - b[i]) > p.容忍误差) return false;
}
return true;
};
// 初次建立
if (!itv.有效 && itv.稳态命中 == 0) {
itv.维度 = D;
itv.最小 = x;
itv.最大 = x;
itv.稳态命中 = 1;
return true;
}
// 维度不一致:重置(避免把 xyz 和 标量 混在一起)
if (itv.维度 != D) {
itv.维度 = D;
itv.最小 = x;
itv.最大 = x;
itv.稳态命中 = 1;
itv.有效 = false;
return true;
}
// "稳态命中"判定:新值若接近区间中心/边界,就累积;否则重置
VecI64 center = itv.最小;
for (size_t i = 0; i < center.size(); ++i) center[i] = (itv.最小[i] + itv.最大[i]) / 2;
if (!within_tol(x, center)) {
itv.最小 = x;
itv.最大 = x;
itv.稳态命中 = 1;
itv.有效 = false;
return true;
}
// 扩张区间
for (size_t i = 0; i < x.size(); ++i) {
itv.最小[i] = std::min(itv.最小[i], x[i]);
itv.最大[i] = std::max(itv.最大[i], x[i]);
}
itv.稳态命中++;
if (!itv.有效 && itv.稳态命中 >= p.稳态阈值) {
itv.有效 = true;
return true;
}
return true;
}
// ====== 候选缓冲更新(内藏) ======
static void 更新候选缓冲(特征节点主信息类& fmi, 特征值节点类* v, const 特征写入参数& p) {
if (!v) return;
fmi.近帧候选.push_back({ p.now, v });
while (fmi.近帧候选.size() > p.候选窗口) fmi.近帧候选.pop_front();
}
// ====== 区间关系比较(AABB) ======
static 枚举_特征关系 比较区间(const 区间摘要I64& A, const 区间摘要I64& B, std::int64_t& outDist) {
outDist = 0;
if (!A.有效 || !B.有效) return 枚举_特征关系::无法比较;
if (A.维度 != B.维度) return 枚举_特征关系::无法比较;
bool eq = (A.最小 == B.最小) && (A.最大 == B.最大);
if (eq) return 枚举_特征关系::相等;
auto contains = [](const 区间摘要I64& X, const 区间摘要I64& Y)->bool {
for (size_t i = 0; i < X.最小.size(); ++i) {
if (X.最小[i] > Y.最小[i]) return false;
if (X.最大[i] < Y.最大[i]) return false;
}
return true;
};
bool AcontainsB = contains(A, B);
bool BcontainsA = contains(B, A);
if (AcontainsB) return 枚举_特征关系::包含;
if (BcontainsA) return 枚举_特征关系::被包含;
// 交集判断 + 间隙距离(L∞ 间隙)
bool overlap = true;
std::int64_t gap = 0;
for (size_t i = 0; i < A.最小.size(); ++i) {
if (A.最大[i] < B.最小[i]) { overlap = false; gap = std::max(gap, B.最小[i] - A.最大[i]); }
else if (B.最大[i] < A.最小[i]) { overlap = false; gap = std::max(gap, A.最小[i] - B.最大[i]); }
}
outDist = gap;
return overlap ? 枚举_特征关系::部分包含 : 枚举_特征关系::不相交;
}
结论映射建议(很直觉):
相等→相同包含/被包含/部分包含→相似(或按你的语义决定"相同")不相交→不同无法比较→无法比较
7) 得到的实际收益(工程层面)
-
特征写入变得"可控":不再是"每帧都堆一个值节点",而是逐步压成区间摘要
-
特征比较不再靠拍脑袋:关系明确(相等/包含/交集/间隙),结论明确(相同/相似/不同)
-
后续扩展很干净:
- 对 VecIU64(轮廓金字塔)你可以加"原型池 + 聚类阈值",仍不破坏这套接口
- 对 string 你可以加"同义词/归一化"也仍不破坏