software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,405 @@
/**
* 自然写教室智能算力盒边缘计算软件 V1.0
* 笔迹预处理模块 - 笔迹坐标数据预处理管道
*
* 对网关转发的原始笔迹坐标进行预处理:
* 去噪滤波、坐标归一化、笔画分割、特征提取
* 预处理结果作为NPU/GPU推理的标准化输入
*/
#ifndef STROKE_PREPROCESSOR_H
#define STROKE_PREPROCESSOR_H
#include <vector>
#include <cmath>
#include <algorithm>
#include <numeric>
#include <cstring>
// ==================== 基础数据结构 ====================
/** 原始笔迹坐标点(来自网关gRPC数据流) */
struct RawPoint {
float x; // X坐标(点阵单位,约300DPI)
float y; // Y坐标
float pressure; // 压力值 (0.0-1.0)
uint32_t timestamp; // 采集时间戳(毫秒)
bool pen_up; // 抬笔标记
};
/** 归一化后的坐标点 */
struct NormalizedPoint {
float x; // 归一化X (0.0-1.0)
float y; // 归一化Y (0.0-1.0)
float pressure; // 压力值 (0.0-1.0)
};
/** 笔画数据 */
struct Stroke {
std::vector<NormalizedPoint> points; // 归一化坐标点序列
int stroke_index; // 笔画序号
float length; // 笔画路径长度
int duration_ms; // 书写耗时(毫秒)
};
/** 预处理输出(用于NPU推理输入) */
struct PreprocessedData {
std::vector<float> image; // 渲染后的灰度图像 (H*W)
int image_width; // 图像宽度
int image_height; // 图像高度
std::vector<Stroke> strokes; // 分割后的笔画列表
int total_points; // 总坐标点数
int stroke_count; // 笔画数量
};
// ==================== 去噪滤波器 ====================
/**
* 笔迹去噪滤波器
* 消除点阵笔采集过程中的抖动噪声和异常跳跃点
* 多级滤波策略:异常点剔除 → 中值滤波 → 移动平均平滑
*/
class StrokeNoiseFilter {
public:
/**
* 构造函数
* max_jump: 最大允许跳跃距离(超过则视为异常点)
* window_size: 滤波窗口大小(奇数)
*/
StrokeNoiseFilter(float max_jump = 50.0f, int window_size = 3)
: max_jump_(max_jump), window_size_(window_size) {}
/**
* 剔除异常跳跃点
* 点阵笔摄像头短暂遮挡会导致坐标突变,需要过滤
*/
std::vector<RawPoint> remove_outliers(const std::vector<RawPoint>& points) {
if (points.size() < 3) return points;
std::vector<RawPoint> result;
result.push_back(points[0]);
for (size_t i = 1; i < points.size(); i++) {
float dx = points[i].x - points[i-1].x;
float dy = points[i].y - points[i-1].y;
float dist = std::sqrt(dx * dx + dy * dy);
// 跳跃距离在合理范围内才保留该点
if (dist <= max_jump_) {
result.push_back(points[i]);
}
}
return result;
}
/**
* 中值滤波去噪
* 对X和Y坐标分别进行一维中值滤波
* 有效消除脉冲噪声同时保留笔画转折特征
*/
std::vector<RawPoint> median_filter(const std::vector<RawPoint>& points) {
int n = static_cast<int>(points.size());
if (n < window_size_) return points;
int half = window_size_ / 2;
std::vector<RawPoint> result(n);
for (int i = 0; i < n; i++) {
// 收集窗口内的X和Y值
std::vector<float> wx, wy;
for (int j = std::max(0, i - half); j <= std::min(n - 1, i + half); j++) {
wx.push_back(points[j].x);
wy.push_back(points[j].y);
}
// 排序取中值
std::sort(wx.begin(), wx.end());
std::sort(wy.begin(), wy.end());
result[i] = points[i];
result[i].x = wx[wx.size() / 2];
result[i].y = wy[wy.size() / 2];
}
return result;
}
/**
* 移动平均平滑
* 进一步减少微小抖动,使笔画更流畅
*/
std::vector<RawPoint> moving_average(const std::vector<RawPoint>& points) {
int n = static_cast<int>(points.size());
if (n < 3) return points;
std::vector<RawPoint> result(n);
int half = window_size_ / 2;
for (int i = 0; i < n; i++) {
float sum_x = 0, sum_y = 0;
int count = 0;
for (int j = std::max(0, i - half); j <= std::min(n - 1, i + half); j++) {
sum_x += points[j].x;
sum_y += points[j].y;
count++;
}
result[i] = points[i];
result[i].x = sum_x / count;
result[i].y = sum_y / count;
}
return result;
}
/** 执行完整去噪流程 */
std::vector<RawPoint> apply(const std::vector<RawPoint>& points) {
auto step1 = remove_outliers(points);
auto step2 = median_filter(step1);
auto step3 = moving_average(step2);
return step3;
}
private:
float max_jump_;
int window_size_;
};
// ==================== 坐标归一化器 ====================
/**
* 坐标归一化器
* 将不同纸张尺寸和分辨率的原始坐标统一归一化到[0,1]范围
* 保持宽高比以避免笔迹变形
*/
class CoordinateNormalizer {
public:
CoordinateNormalizer(bool preserve_aspect = true) : preserve_aspect_(preserve_aspect) {}
/**
* Min-Max归一化,映射到[0,1]范围
*/
std::vector<NormalizedPoint> normalize(const std::vector<RawPoint>& points) {
if (points.empty()) return {};
// 计算坐标范围
float min_x = points[0].x, max_x = points[0].x;
float min_y = points[0].y, max_y = points[0].y;
for (const auto& p : points) {
min_x = std::min(min_x, p.x);
max_x = std::max(max_x, p.x);
min_y = std::min(min_y, p.y);
max_y = std::max(max_y, p.y);
}
float range_x = max_x - min_x;
float range_y = max_y - min_y;
// 保持宽高比时使用统一的缩放因子
float scale = 1.0f;
if (preserve_aspect_) {
scale = std::max(range_x, range_y);
if (scale < 1e-6f) scale = 1.0f;
}
std::vector<NormalizedPoint> result;
result.reserve(points.size());
for (const auto& p : points) {
NormalizedPoint np;
if (preserve_aspect_) {
np.x = (p.x - min_x) / scale;
np.y = (p.y - min_y) / scale;
} else {
np.x = (range_x > 1e-6f) ? (p.x - min_x) / range_x : 0.5f;
np.y = (range_y > 1e-6f) ? (p.y - min_y) / range_y : 0.5f;
}
np.pressure = p.pressure;
result.push_back(np);
}
return result;
}
private:
bool preserve_aspect_;
};
// ==================== 笔画分割器 ====================
/**
* 笔画分割器
* 根据抬笔事件和时间间隔将连续坐标流分割为独立笔画
*/
class StrokeSegmenter {
public:
StrokeSegmenter(int time_threshold_ms = 200, int min_points = 3)
: time_threshold_(time_threshold_ms), min_points_(min_points) {}
/**
* 将原始点序列分割为笔画列表
*/
std::vector<std::vector<RawPoint>> segment(const std::vector<RawPoint>& points) {
if (points.empty()) return {};
std::vector<std::vector<RawPoint>> strokes;
std::vector<RawPoint> current;
current.push_back(points[0]);
for (size_t i = 1; i < points.size(); i++) {
bool is_break = points[i].pen_up;
int time_gap = static_cast<int>(points[i].timestamp - points[i-1].timestamp);
if ((is_break || time_gap > time_threshold_) &&
static_cast<int>(current.size()) >= min_points_) {
strokes.push_back(current);
current.clear();
}
if (!points[i].pen_up) {
current.push_back(points[i]);
}
}
if (static_cast<int>(current.size()) >= min_points_) {
strokes.push_back(current);
}
return strokes;
}
private:
int time_threshold_;
int min_points_;
};
// ==================== 图像渲染器 ====================
/**
* 笔迹图像渲染器
* 将归一化坐标渲染为灰度图像作为CNN模型输入
* 使用Bresenham直线算法连接相邻坐标点
*/
class StrokeImageRenderer {
public:
StrokeImageRenderer(int width = 64, int height = 64)
: width_(width), height_(height) {}
/**
* 将坐标序列渲染为灰度图像
* 输出一维浮点数组,值域[0,1],1表示笔迹
*/
std::vector<float> render(const std::vector<NormalizedPoint>& points) {
std::vector<float> image(width_ * height_, 0.0f);
for (size_t i = 1; i < points.size(); i++) {
int x0 = static_cast<int>(points[i-1].x * (width_ - 1));
int y0 = static_cast<int>(points[i-1].y * (height_ - 1));
int x1 = static_cast<int>(points[i].x * (width_ - 1));
int y1 = static_cast<int>(points[i].y * (height_ - 1));
// 裁剪到图像范围
x0 = std::clamp(x0, 0, width_ - 1);
y0 = std::clamp(y0, 0, height_ - 1);
x1 = std::clamp(x1, 0, width_ - 1);
y1 = std::clamp(y1, 0, height_ - 1);
float pressure = (points[i-1].pressure + points[i].pressure) * 0.5f;
// Bresenham直线算法
draw_line(image, x0, y0, x1, y1, pressure);
}
return image;
}
private:
void draw_line(std::vector<float>& image, int x0, int y0, int x1, int y1, float value) {
int dx = std::abs(x1 - x0);
int dy = std::abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
while (true) {
int idx = y0 * width_ + x0;
if (idx >= 0 && idx < width_ * height_) {
image[idx] = std::max(image[idx], value);
}
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
int width_;
int height_;
};
// ==================== 预处理管道(整合) ====================
/**
* 笔迹预处理管道
* 整合去噪、归一化、分割、渲染的完整处理流程
* 输入原始坐标点序列,输出标准化的推理输入数据
*/
class StrokePreprocessor {
public:
StrokePreprocessor(int image_size = 64)
: noise_filter_(50.0f, 3),
normalizer_(true),
segmenter_(200, 3),
renderer_(image_size, image_size),
image_size_(image_size) {}
/**
* 执行完整预处理管道
* 流程:原始坐标 → 去噪 → 归一化 → 笔画分割 → 图像渲染
*/
PreprocessedData process(const std::vector<RawPoint>& raw_points) {
PreprocessedData result;
// 步骤1:去噪滤波
auto denoised = noise_filter_.apply(raw_points);
// 步骤2:坐标归一化
auto normalized = normalizer_.normalize(denoised);
// 步骤3:笔画分割
auto stroke_groups = segmenter_.segment(denoised);
// 构建笔画数据
for (int i = 0; i < static_cast<int>(stroke_groups.size()); i++) {
Stroke stroke;
stroke.stroke_index = i;
auto norm_group = normalizer_.normalize(stroke_groups[i]);
stroke.points = norm_group;
stroke.length = calc_path_length(norm_group);
if (stroke_groups[i].size() >= 2) {
stroke.duration_ms = static_cast<int>(
stroke_groups[i].back().timestamp - stroke_groups[i].front().timestamp);
}
result.strokes.push_back(stroke);
}
// 步骤4:渲染为灰度图像
result.image = renderer_.render(normalized);
result.image_width = image_size_;
result.image_height = image_size_;
result.total_points = static_cast<int>(denoised.size());
result.stroke_count = static_cast<int>(result.strokes.size());
return result;
}
private:
float calc_path_length(const std::vector<NormalizedPoint>& points) {
float total = 0.0f;
for (size_t i = 1; i < points.size(); i++) {
float dx = points[i].x - points[i-1].x;
float dy = points[i].y - points[i-1].y;
total += std::sqrt(dx * dx + dy * dy);
}
return total;
}
StrokeNoiseFilter noise_filter_;
CoordinateNormalizer normalizer_;
StrokeSegmenter segmenter_;
StrokeImageRenderer renderer_;
int image_size_;
};
#endif // STROKE_PREPROCESSOR_H