software copyright
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* 自然写互动课堂应用开发SDK软件 V1.0
|
||||
* BLE协议解析核心模块 - 蓝牙5.0点阵笔通信协议实现
|
||||
*
|
||||
* 跨平台C语言核心库,负责解析点阵笔BLE GATT数据
|
||||
* 提供笔迹坐标解包、协议帧校验、数据压缩解压等底层能力
|
||||
* 通过JNI/ObjC Bridge/FFI供各平台SDK调用
|
||||
*/
|
||||
|
||||
#ifndef BLE_PROTOCOL_H
|
||||
#define BLE_PROTOCOL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================== 协议常量定义 ==================== */
|
||||
|
||||
/* BLE GATT Service UUID(自定义服务) */
|
||||
#define WRITECH_SERVICE_UUID "0000FFE0-0000-1000-8000-00805F9B34FB"
|
||||
/* 笔迹数据Characteristic UUID */
|
||||
#define STROKE_DATA_CHAR_UUID "0000FFE1-0000-1000-8000-00805F9B34FB"
|
||||
/* 设备信息Characteristic UUID */
|
||||
#define DEVICE_INFO_CHAR_UUID "0000FFE2-0000-1000-8000-00805F9B34FB"
|
||||
/* 配置写入Characteristic UUID */
|
||||
#define CONFIG_WRITE_CHAR_UUID "0000FFE3-0000-1000-8000-00805F9B34FB"
|
||||
/* OTA DFU Characteristic UUID */
|
||||
#define OTA_DFU_CHAR_UUID "0000FFE4-0000-1000-8000-00805F9B34FB"
|
||||
|
||||
/* 协议帧标志 */
|
||||
#define FRAME_HEADER_MAGIC 0xAA55
|
||||
#define FRAME_MAX_PAYLOAD_SIZE 240 /* MTU=247, 减去帧头7字节 */
|
||||
#define MAX_POINTS_PER_FRAME 34 /* 每帧最多34个坐标点 */
|
||||
|
||||
/* 帧类型定义 */
|
||||
#define FRAME_TYPE_STROKE_DATA 0x01 /* 笔迹坐标数据 */
|
||||
#define FRAME_TYPE_PEN_UP 0x02 /* 抬笔事件 */
|
||||
#define FRAME_TYPE_PEN_DOWN 0x03 /* 落笔事件 */
|
||||
#define FRAME_TYPE_DEVICE_STATUS 0x04 /* 设备状态(电量等) */
|
||||
#define FRAME_TYPE_OFFLINE_SYNC 0x05 /* 离线数据同步 */
|
||||
#define FRAME_TYPE_OTA_DATA 0x06 /* OTA升级数据 */
|
||||
#define FRAME_TYPE_CONFIG_RSP 0x07 /* 配置响应 */
|
||||
|
||||
/* ==================== 数据结构定义 ==================== */
|
||||
|
||||
/**
|
||||
* 原始笔迹坐标点(7字节紧凑编码)
|
||||
* x: 16位无符号整数,点阵坐标X(分辨率约300DPI)
|
||||
* y: 16位无符号整数,点阵坐标Y
|
||||
* pressure: 8位无符号整数,压力值(0-255)
|
||||
* timestamp_delta: 16位无符号整数,距上一点的时间差(毫秒)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t x; /* X坐标(大端序) */
|
||||
uint16_t y; /* Y坐标(大端序) */
|
||||
uint8_t pressure; /* 压力值 0-255 */
|
||||
uint16_t timestamp_delta; /* 时间增量(毫秒) */
|
||||
} __attribute__((packed)) StrokePointRaw;
|
||||
|
||||
/**
|
||||
* 解码后的笔迹坐标点
|
||||
*/
|
||||
typedef struct {
|
||||
float x; /* X坐标(浮点) */
|
||||
float y; /* Y坐标(浮点) */
|
||||
float pressure; /* 压力值 0.0-1.0 */
|
||||
uint32_t timestamp; /* 绝对时间戳(毫秒) */
|
||||
uint8_t pen_state; /* 0=落笔, 1=抬笔 */
|
||||
} StrokePoint;
|
||||
|
||||
/**
|
||||
* BLE协议帧头(7字节)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t magic; /* 帧头魔数 0xAA55 */
|
||||
uint8_t frame_type; /* 帧类型 */
|
||||
uint8_t sequence; /* 帧序号(0-255循环) */
|
||||
uint16_t payload_length; /* 负载长度 */
|
||||
uint8_t checksum; /* 帧头校验和(XOR) */
|
||||
} __attribute__((packed)) FrameHeader;
|
||||
|
||||
/**
|
||||
* 笔迹数据帧
|
||||
*/
|
||||
typedef struct {
|
||||
FrameHeader header;
|
||||
uint8_t point_count; /* 本帧包含的坐标点数 */
|
||||
uint32_t page_id; /* 点阵码页面ID */
|
||||
StrokePointRaw points[MAX_POINTS_PER_FRAME]; /* 坐标点数组 */
|
||||
uint16_t crc16; /* CRC-16校验 */
|
||||
} __attribute__((packed)) StrokeDataFrame;
|
||||
|
||||
/**
|
||||
* 设备状态帧
|
||||
*/
|
||||
typedef struct {
|
||||
FrameHeader header;
|
||||
uint8_t battery_level; /* 电量百分比 0-100 */
|
||||
uint8_t charging_state; /* 充电状态: 0=未充电, 1=充电中, 2=已充满 */
|
||||
uint16_t firmware_version; /* 固件版本 (major*256+minor) */
|
||||
uint8_t connection_state; /* 连接状态 */
|
||||
uint32_t serial_number; /* 设备序列号 */
|
||||
uint16_t crc16;
|
||||
} __attribute__((packed)) DeviceStatusFrame;
|
||||
|
||||
/**
|
||||
* 解析回调函数类型定义
|
||||
*/
|
||||
typedef void (*on_stroke_point_cb)(const StrokePoint* point, void* user_data);
|
||||
typedef void (*on_pen_event_cb)(uint8_t event_type, uint32_t timestamp, void* user_data);
|
||||
typedef void (*on_device_status_cb)(uint8_t battery, uint8_t charging, uint16_t fw_ver, void* user_data);
|
||||
|
||||
/* ==================== 协议解析器 ==================== */
|
||||
|
||||
/**
|
||||
* BLE协议解析器上下文
|
||||
*/
|
||||
typedef struct {
|
||||
/* 接收缓冲区(处理分包/粘包) */
|
||||
uint8_t recv_buffer[512];
|
||||
size_t recv_length;
|
||||
|
||||
/* 序号跟踪(乱序检测) */
|
||||
uint8_t expected_sequence;
|
||||
|
||||
/* 时间戳基准 */
|
||||
uint32_t base_timestamp;
|
||||
uint32_t last_timestamp;
|
||||
|
||||
/* 统计信息 */
|
||||
uint32_t total_frames;
|
||||
uint32_t total_points;
|
||||
uint32_t error_frames;
|
||||
uint32_t lost_frames;
|
||||
|
||||
/* 回调函数 */
|
||||
on_stroke_point_cb stroke_cb;
|
||||
on_pen_event_cb pen_event_cb;
|
||||
on_device_status_cb status_cb;
|
||||
void* user_data;
|
||||
} BleProtocolParser;
|
||||
|
||||
/**
|
||||
* 初始化协议解析器
|
||||
*/
|
||||
static inline void ble_parser_init(BleProtocolParser* parser) {
|
||||
memset(parser, 0, sizeof(BleProtocolParser));
|
||||
parser->expected_sequence = 0;
|
||||
parser->base_timestamp = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置回调函数
|
||||
*/
|
||||
static inline void ble_parser_set_callbacks(
|
||||
BleProtocolParser* parser,
|
||||
on_stroke_point_cb stroke_cb,
|
||||
on_pen_event_cb pen_event_cb,
|
||||
on_device_status_cb status_cb,
|
||||
void* user_data
|
||||
) {
|
||||
parser->stroke_cb = stroke_cb;
|
||||
parser->pen_event_cb = pen_event_cb;
|
||||
parser->status_cb = status_cb;
|
||||
parser->user_data = user_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算CRC-16校验值(CCITT标准)
|
||||
*/
|
||||
static uint16_t calc_crc16(const uint8_t* data, size_t length) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
crc ^= (uint16_t)data[i] << 8;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (crc & 0x8000)
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验帧头
|
||||
*/
|
||||
static int validate_frame_header(const FrameHeader* header) {
|
||||
/* 校验魔数 */
|
||||
if (header->magic != FRAME_HEADER_MAGIC) return -1;
|
||||
/* 校验负载长度 */
|
||||
if (header->payload_length > FRAME_MAX_PAYLOAD_SIZE) return -2;
|
||||
/* 校验帧头XOR校验和 */
|
||||
uint8_t xor_sum = 0;
|
||||
const uint8_t* p = (const uint8_t*)header;
|
||||
for (int i = 0; i < 6; i++) xor_sum ^= p[i];
|
||||
if (xor_sum != header->checksum) return -3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大端序转小端序(16位)
|
||||
*/
|
||||
static inline uint16_t be16_to_le(uint16_t value) {
|
||||
return (value >> 8) | (value << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析笔迹数据帧
|
||||
* 从帧中提取坐标点并通过回调函数输出
|
||||
*/
|
||||
static int parse_stroke_frame(BleProtocolParser* parser, const uint8_t* data, size_t length) {
|
||||
if (length < sizeof(FrameHeader) + 5) return -1;
|
||||
|
||||
const FrameHeader* header = (const FrameHeader*)data;
|
||||
|
||||
/* 帧头校验 */
|
||||
if (validate_frame_header(header) != 0) {
|
||||
parser->error_frames++;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 序号连续性检查 */
|
||||
if (header->sequence != parser->expected_sequence) {
|
||||
uint8_t lost = header->sequence - parser->expected_sequence;
|
||||
parser->lost_frames += lost;
|
||||
}
|
||||
parser->expected_sequence = header->sequence + 1;
|
||||
|
||||
/* 解析负载 */
|
||||
const uint8_t* payload = data + sizeof(FrameHeader);
|
||||
uint8_t point_count = payload[0];
|
||||
uint32_t page_id = *(uint32_t*)(payload + 1);
|
||||
|
||||
if (point_count > MAX_POINTS_PER_FRAME) {
|
||||
parser->error_frames++;
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* CRC校验(校验帧头+负载) */
|
||||
size_t crc_data_len = length - 2;
|
||||
uint16_t expected_crc = *(uint16_t*)(data + crc_data_len);
|
||||
uint16_t actual_crc = calc_crc16(data, crc_data_len);
|
||||
if (expected_crc != actual_crc) {
|
||||
parser->error_frames++;
|
||||
return -3;
|
||||
}
|
||||
|
||||
/* 解析每个坐标点 */
|
||||
const StrokePointRaw* raw_points = (const StrokePointRaw*)(payload + 5);
|
||||
for (int i = 0; i < point_count; i++) {
|
||||
StrokePoint decoded;
|
||||
decoded.x = (float)be16_to_le(raw_points[i].x);
|
||||
decoded.y = (float)be16_to_le(raw_points[i].y);
|
||||
decoded.pressure = raw_points[i].pressure / 255.0f;
|
||||
|
||||
/* 累加时间增量得到绝对时间戳 */
|
||||
uint16_t delta = be16_to_le(raw_points[i].timestamp_delta);
|
||||
parser->last_timestamp += delta;
|
||||
decoded.timestamp = parser->base_timestamp + parser->last_timestamp;
|
||||
decoded.pen_state = 0; /* 落笔状态 */
|
||||
|
||||
/* 通过回调函数输出 */
|
||||
if (parser->stroke_cb) {
|
||||
parser->stroke_cb(&decoded, parser->user_data);
|
||||
}
|
||||
parser->total_points++;
|
||||
}
|
||||
|
||||
parser->total_frames++;
|
||||
return point_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入BLE Notify接收到的数据
|
||||
* 处理分包/粘包,自动检测帧边界并分发解析
|
||||
*/
|
||||
static int ble_parser_feed(BleProtocolParser* parser, const uint8_t* data, size_t length) {
|
||||
/* 追加到接收缓冲区 */
|
||||
if (parser->recv_length + length > sizeof(parser->recv_buffer)) {
|
||||
/* 缓冲区溢出,丢弃旧数据 */
|
||||
parser->recv_length = 0;
|
||||
}
|
||||
memcpy(parser->recv_buffer + parser->recv_length, data, length);
|
||||
parser->recv_length += length;
|
||||
|
||||
int parsed_count = 0;
|
||||
|
||||
/* 扫描缓冲区查找完整帧 */
|
||||
while (parser->recv_length >= sizeof(FrameHeader)) {
|
||||
/* 查找帧头魔数 */
|
||||
if (parser->recv_buffer[0] != 0xAA || parser->recv_buffer[1] != 0x55) {
|
||||
/* 跳过非法字节 */
|
||||
memmove(parser->recv_buffer, parser->recv_buffer + 1, parser->recv_length - 1);
|
||||
parser->recv_length--;
|
||||
continue;
|
||||
}
|
||||
|
||||
FrameHeader* header = (FrameHeader*)parser->recv_buffer;
|
||||
size_t frame_size = sizeof(FrameHeader) + header->payload_length + 2; /* +2 for CRC */
|
||||
|
||||
if (parser->recv_length < frame_size) {
|
||||
break; /* 帧数据不完整,等待更多数据 */
|
||||
}
|
||||
|
||||
/* 根据帧类型分发解析 */
|
||||
switch (header->frame_type) {
|
||||
case FRAME_TYPE_STROKE_DATA:
|
||||
parse_stroke_frame(parser, parser->recv_buffer, frame_size);
|
||||
parsed_count++;
|
||||
break;
|
||||
case FRAME_TYPE_PEN_UP:
|
||||
if (parser->pen_event_cb) {
|
||||
parser->pen_event_cb(1, parser->last_timestamp, parser->user_data);
|
||||
}
|
||||
break;
|
||||
case FRAME_TYPE_PEN_DOWN:
|
||||
if (parser->pen_event_cb) {
|
||||
parser->pen_event_cb(0, parser->last_timestamp, parser->user_data);
|
||||
}
|
||||
break;
|
||||
case FRAME_TYPE_DEVICE_STATUS: {
|
||||
DeviceStatusFrame* status = (DeviceStatusFrame*)parser->recv_buffer;
|
||||
if (parser->status_cb) {
|
||||
parser->status_cb(status->battery_level, status->charging_state,
|
||||
status->firmware_version, parser->user_data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/* 移除已处理的帧 */
|
||||
memmove(parser->recv_buffer, parser->recv_buffer + frame_size,
|
||||
parser->recv_length - frame_size);
|
||||
parser->recv_length -= frame_size;
|
||||
}
|
||||
|
||||
return parsed_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解析器统计信息
|
||||
*/
|
||||
static inline void ble_parser_get_stats(const BleProtocolParser* parser,
|
||||
uint32_t* total_frames, uint32_t* total_points,
|
||||
uint32_t* error_frames, uint32_t* lost_frames) {
|
||||
if (total_frames) *total_frames = parser->total_frames;
|
||||
if (total_points) *total_points = parser->total_points;
|
||||
if (error_frames) *error_frames = parser->error_frames;
|
||||
if (lost_frames) *lost_frames = parser->lost_frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置解析器状态
|
||||
*/
|
||||
static inline void ble_parser_reset(BleProtocolParser* parser) {
|
||||
parser->recv_length = 0;
|
||||
parser->expected_sequence = 0;
|
||||
parser->last_timestamp = 0;
|
||||
parser->total_frames = 0;
|
||||
parser->total_points = 0;
|
||||
parser->error_frames = 0;
|
||||
parser->lost_frames = 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* BLE_PROTOCOL_H */
|
||||
@@ -0,0 +1,614 @@
|
||||
/*
|
||||
* 自然写互动课堂应用开发SDK软件 V1.0
|
||||
* 坐标变换模块 - 点阵笔坐标到屏幕坐标的高精度映射
|
||||
*
|
||||
* 功能说明:
|
||||
* 1. 点阵码坐标解析与标准化(Anoto编码 → 物理坐标mm)
|
||||
* 2. 仿射变换矩阵计算(四角标定点 → 变换参数)
|
||||
* 3. 物理坐标到屏幕像素坐标的实时映射
|
||||
* 4. 多页面坐标空间管理(不同纸张/不同页面独立坐标系)
|
||||
* 5. 畸变校正(镜头畸变、纸张弯曲补偿)
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* ========== 数据结构定义 ========== */
|
||||
|
||||
/* 二维点(浮点精度) */
|
||||
typedef struct {
|
||||
double x; /* X坐标 */
|
||||
double y; /* Y坐标 */
|
||||
} Point2D;
|
||||
|
||||
/* 仿射变换矩阵 3x3(齐次坐标) */
|
||||
typedef struct {
|
||||
double m[3][3]; /* 变换矩阵元素 */
|
||||
} AffineMatrix;
|
||||
|
||||
/* 坐标空间描述 */
|
||||
typedef struct {
|
||||
unsigned int page_id; /* 页面唯一ID */
|
||||
unsigned int section_id; /* 区段ID(Anoto编码中的section) */
|
||||
unsigned int owner_id; /* 拥有者ID(Anoto编码) */
|
||||
double physical_width_mm; /* 纸张物理宽度(毫米) */
|
||||
double physical_height_mm; /* 纸张物理高度(毫米) */
|
||||
int screen_width_px; /* 对应屏幕区域宽度(像素) */
|
||||
int screen_height_px; /* 对应屏幕区域高度(像素) */
|
||||
AffineMatrix transform; /* 标定后的变换矩阵 */
|
||||
int is_calibrated; /* 是否已完成标定 */
|
||||
} CoordinateSpace;
|
||||
|
||||
/* 标定点对(物理坐标 ↔ 屏幕坐标) */
|
||||
typedef struct {
|
||||
Point2D physical; /* 物理坐标(mm) */
|
||||
Point2D screen; /* 屏幕坐标(px) */
|
||||
} CalibrationPair;
|
||||
|
||||
/* 畸变校正参数(Brown-Conrady模型简化版) */
|
||||
typedef struct {
|
||||
double k1; /* 径向畸变系数1 */
|
||||
double k2; /* 径向畸变系数2 */
|
||||
double p1; /* 切向畸变系数1 */
|
||||
double p2; /* 切向畸变系数2 */
|
||||
double cx; /* 畸变中心X */
|
||||
double cy; /* 畸变中心Y */
|
||||
} DistortionParams;
|
||||
|
||||
/* 坐标变换管理器 */
|
||||
typedef struct {
|
||||
CoordinateSpace spaces[64]; /* 最多支持64个坐标空间 */
|
||||
int space_count; /* 当前已注册的空间数 */
|
||||
DistortionParams distortion; /* 全局畸变校正参数 */
|
||||
int distortion_enabled; /* 是否启用畸变校正 */
|
||||
double dpi_resolution; /* 点阵笔DPI分辨率(通常为300或600) */
|
||||
} CoordinateManager;
|
||||
|
||||
/* 全局坐标管理器实例 */
|
||||
static CoordinateManager g_coord_manager;
|
||||
|
||||
/* ========== Anoto点阵码坐标解析 ========== */
|
||||
|
||||
/*
|
||||
* 将Anoto点阵码原始编码转换为物理坐标(毫米)
|
||||
* 点阵笔采集到的原始数据是基于Anoto编码系统的逻辑坐标
|
||||
* 需要根据DPI分辨率转换为实际的物理距离
|
||||
*
|
||||
* @param raw_x 点阵码原始X坐标值
|
||||
* @param raw_y 点阵码原始Y坐标值
|
||||
* @param section_id Anoto编码的section标识
|
||||
* @param out_physical 输出的物理坐标(mm)
|
||||
* @return 0成功, -1参数错误
|
||||
*/
|
||||
int anoto_to_physical(unsigned int raw_x, unsigned int raw_y,
|
||||
unsigned int section_id, Point2D *out_physical) {
|
||||
if (out_physical == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* DPI到毫米的转换因子:25.4mm / DPI */
|
||||
double dpi = g_coord_manager.dpi_resolution;
|
||||
if (dpi < 1.0) {
|
||||
dpi = 300.0; /* 默认300 DPI */
|
||||
}
|
||||
double dots_to_mm = 25.4 / dpi;
|
||||
|
||||
/* Anoto编码的原始坐标直接乘以转换因子得到物理坐标 */
|
||||
out_physical->x = (double)raw_x * dots_to_mm;
|
||||
out_physical->y = (double)raw_y * dots_to_mm;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 解析7字节紧凑坐标编码
|
||||
* 点阵笔通过BLE传输时使用7字节紧凑格式:
|
||||
* 字节0-1: X坐标高16位
|
||||
* 字节2-3: Y坐标高16位
|
||||
* 字节4: X低4位 | Y低4位
|
||||
* 字节5: 压力值高8位
|
||||
* 字节6: 压力值低8位 | 标志位
|
||||
*/
|
||||
int decode_compact_coordinate(const unsigned char *data, int data_len,
|
||||
unsigned int *out_x, unsigned int *out_y,
|
||||
unsigned int *out_pressure) {
|
||||
if (data == NULL || data_len < 7) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 解析X坐标(20位精度) */
|
||||
unsigned int x_high = ((unsigned int)data[0] << 8) | data[1];
|
||||
unsigned int x_low = (data[4] >> 4) & 0x0F;
|
||||
*out_x = (x_high << 4) | x_low;
|
||||
|
||||
/* 解析Y坐标(20位精度) */
|
||||
unsigned int y_high = ((unsigned int)data[2] << 8) | data[3];
|
||||
unsigned int y_low = data[4] & 0x0F;
|
||||
*out_y = (y_high << 4) | y_low;
|
||||
|
||||
/* 解析压力值(12位精度,0-4095) */
|
||||
unsigned int p_high = data[5];
|
||||
unsigned int p_low = (data[6] >> 4) & 0x0F;
|
||||
*out_pressure = (p_high << 4) | p_low;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ========== 仿射变换矩阵计算 ========== */
|
||||
|
||||
/*
|
||||
* 初始化为单位矩阵
|
||||
*/
|
||||
void matrix_identity(AffineMatrix *mat) {
|
||||
memset(mat->m, 0, sizeof(mat->m));
|
||||
mat->m[0][0] = 1.0;
|
||||
mat->m[1][1] = 1.0;
|
||||
mat->m[2][2] = 1.0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 矩阵乘法 result = a * b
|
||||
*/
|
||||
void matrix_multiply(const AffineMatrix *a, const AffineMatrix *b,
|
||||
AffineMatrix *result) {
|
||||
AffineMatrix tmp;
|
||||
int i, j, k;
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (j = 0; j < 3; j++) {
|
||||
tmp.m[i][j] = 0.0;
|
||||
for (k = 0; k < 3; k++) {
|
||||
tmp.m[i][j] += a->m[i][k] * b->m[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
memcpy(result->m, tmp.m, sizeof(tmp.m));
|
||||
}
|
||||
|
||||
/*
|
||||
* 使用最小二乘法从标定点对计算仿射变换矩阵
|
||||
* 至少需要3个不共线的标定点对
|
||||
* 使用正规方程法求解超定线性方程组
|
||||
*
|
||||
* @param pairs 标定点对数组
|
||||
* @param pair_count 标定点对数量(≥3)
|
||||
* @param out_matrix 输出的仿射变换矩阵
|
||||
* @return 0成功, -1参数不足, -2矩阵奇异
|
||||
*/
|
||||
int compute_affine_transform(const CalibrationPair *pairs, int pair_count,
|
||||
AffineMatrix *out_matrix) {
|
||||
if (pairs == NULL || pair_count < 3 || out_matrix == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 仿射变换方程:
|
||||
* screen_x = a11 * phys_x + a12 * phys_y + a13
|
||||
* screen_y = a21 * phys_x + a22 * phys_y + a23
|
||||
*
|
||||
* 构建 ATA * x = ATb 正规方程
|
||||
* A矩阵每行: [phys_x, phys_y, 1]
|
||||
*/
|
||||
double ATA[3][3] = {{0}};
|
||||
double ATb_x[3] = {0};
|
||||
double ATb_y[3] = {0};
|
||||
|
||||
int i;
|
||||
for (i = 0; i < pair_count; i++) {
|
||||
double px = pairs[i].physical.x;
|
||||
double py = pairs[i].physical.y;
|
||||
double sx = pairs[i].screen.x;
|
||||
double sy = pairs[i].screen.y;
|
||||
|
||||
/* 累加 ATA */
|
||||
ATA[0][0] += px * px;
|
||||
ATA[0][1] += px * py;
|
||||
ATA[0][2] += px;
|
||||
ATA[1][0] += py * px;
|
||||
ATA[1][1] += py * py;
|
||||
ATA[1][2] += py;
|
||||
ATA[2][0] += px;
|
||||
ATA[2][1] += py;
|
||||
ATA[2][2] += 1.0;
|
||||
|
||||
/* 累加 ATb */
|
||||
ATb_x[0] += px * sx;
|
||||
ATb_x[1] += py * sx;
|
||||
ATb_x[2] += sx;
|
||||
|
||||
ATb_y[0] += px * sy;
|
||||
ATb_y[1] += py * sy;
|
||||
ATb_y[2] += sy;
|
||||
}
|
||||
|
||||
/* 高斯消元法求解3x3线性方程组 */
|
||||
/* 先求解 screen_x 的系数 [a11, a12, a13] */
|
||||
double aug_x[3][4];
|
||||
double aug_y[3][4];
|
||||
int j, k;
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (j = 0; j < 3; j++) {
|
||||
aug_x[i][j] = ATA[i][j];
|
||||
aug_y[i][j] = ATA[i][j];
|
||||
}
|
||||
aug_x[i][3] = ATb_x[i];
|
||||
aug_y[i][3] = ATb_y[i];
|
||||
}
|
||||
|
||||
/* 高斯消元(部分主元选取) */
|
||||
for (k = 0; k < 3; k++) {
|
||||
/* 找主元 */
|
||||
int max_row = k;
|
||||
double max_val = fabs(aug_x[k][k]);
|
||||
for (i = k + 1; i < 3; i++) {
|
||||
if (fabs(aug_x[i][k]) > max_val) {
|
||||
max_val = fabs(aug_x[i][k]);
|
||||
max_row = i;
|
||||
}
|
||||
}
|
||||
if (max_val < 1e-12) {
|
||||
return -2; /* 矩阵奇异,标定点可能共线 */
|
||||
}
|
||||
/* 交换行 */
|
||||
if (max_row != k) {
|
||||
for (j = 0; j < 4; j++) {
|
||||
double tmp = aug_x[k][j];
|
||||
aug_x[k][j] = aug_x[max_row][j];
|
||||
aug_x[max_row][j] = tmp;
|
||||
tmp = aug_y[k][j];
|
||||
aug_y[k][j] = aug_y[max_row][j];
|
||||
aug_y[max_row][j] = tmp;
|
||||
}
|
||||
}
|
||||
/* 消元 */
|
||||
for (i = k + 1; i < 3; i++) {
|
||||
double factor_x = aug_x[i][k] / aug_x[k][k];
|
||||
double factor_y = aug_y[i][k] / aug_y[k][k];
|
||||
for (j = k; j < 4; j++) {
|
||||
aug_x[i][j] -= factor_x * aug_x[k][j];
|
||||
aug_y[i][j] -= factor_y * aug_y[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 回代求解 */
|
||||
double sol_x[3], sol_y[3];
|
||||
for (i = 2; i >= 0; i--) {
|
||||
sol_x[i] = aug_x[i][3];
|
||||
sol_y[i] = aug_y[i][3];
|
||||
for (j = i + 1; j < 3; j++) {
|
||||
sol_x[i] -= aug_x[i][j] * sol_x[j];
|
||||
sol_y[i] -= aug_y[i][j] * sol_y[j];
|
||||
}
|
||||
sol_x[i] /= aug_x[i][i];
|
||||
sol_y[i] /= aug_y[i][i];
|
||||
}
|
||||
|
||||
/* 填充仿射变换矩阵 */
|
||||
out_matrix->m[0][0] = sol_x[0]; /* a11 */
|
||||
out_matrix->m[0][1] = sol_x[1]; /* a12 */
|
||||
out_matrix->m[0][2] = sol_x[2]; /* a13(平移X) */
|
||||
out_matrix->m[1][0] = sol_y[0]; /* a21 */
|
||||
out_matrix->m[1][1] = sol_y[1]; /* a22 */
|
||||
out_matrix->m[1][2] = sol_y[2]; /* a23(平移Y) */
|
||||
out_matrix->m[2][0] = 0.0;
|
||||
out_matrix->m[2][1] = 0.0;
|
||||
out_matrix->m[2][2] = 1.0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ========== 坐标空间管理 ========== */
|
||||
|
||||
/*
|
||||
* 初始化坐标变换管理器
|
||||
* @param dpi 点阵笔的DPI分辨率(常见值:300, 600)
|
||||
*/
|
||||
void coordinate_manager_init(double dpi) {
|
||||
memset(&g_coord_manager, 0, sizeof(g_coord_manager));
|
||||
g_coord_manager.dpi_resolution = dpi;
|
||||
g_coord_manager.distortion_enabled = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 注册一个新的坐标空间(对应一个页面/纸张)
|
||||
* 在使用特定页面前需先注册其坐标空间参数
|
||||
*
|
||||
* @param page_id 页面唯一标识
|
||||
* @param section_id Anoto section编号
|
||||
* @param width_mm 纸张物理宽度
|
||||
* @param height_mm 纸张物理高度
|
||||
* @param screen_w 对应屏幕宽度像素
|
||||
* @param screen_h 对应屏幕高度像素
|
||||
* @return 空间索引, -1失败
|
||||
*/
|
||||
int register_coordinate_space(unsigned int page_id, unsigned int section_id,
|
||||
double width_mm, double height_mm,
|
||||
int screen_w, int screen_h) {
|
||||
if (g_coord_manager.space_count >= 64) {
|
||||
return -1; /* 空间已满 */
|
||||
}
|
||||
|
||||
int idx = g_coord_manager.space_count;
|
||||
CoordinateSpace *space = &g_coord_manager.spaces[idx];
|
||||
space->page_id = page_id;
|
||||
space->section_id = section_id;
|
||||
space->physical_width_mm = width_mm;
|
||||
space->physical_height_mm = height_mm;
|
||||
space->screen_width_px = screen_w;
|
||||
space->screen_height_px = screen_h;
|
||||
space->is_calibrated = 0;
|
||||
matrix_identity(&space->transform);
|
||||
|
||||
g_coord_manager.space_count++;
|
||||
return idx;
|
||||
}
|
||||
|
||||
/*
|
||||
* 对指定坐标空间执行标定
|
||||
* 使用用户提供的标定点对计算仿射变换矩阵
|
||||
*/
|
||||
int calibrate_space(int space_index, const CalibrationPair *pairs,
|
||||
int pair_count) {
|
||||
if (space_index < 0 || space_index >= g_coord_manager.space_count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CoordinateSpace *space = &g_coord_manager.spaces[space_index];
|
||||
int ret = compute_affine_transform(pairs, pair_count, &space->transform);
|
||||
if (ret == 0) {
|
||||
space->is_calibrated = 1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* 使用默认缩放(无旋转无畸变)进行快速标定
|
||||
* 适用于标准A4纸张等无需精确标定的场景
|
||||
*/
|
||||
int calibrate_space_default(int space_index) {
|
||||
if (space_index < 0 || space_index >= g_coord_manager.space_count) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CoordinateSpace *space = &g_coord_manager.spaces[space_index];
|
||||
matrix_identity(&space->transform);
|
||||
|
||||
/* 简单线性缩放:物理mm → 屏幕px */
|
||||
double scale_x = (double)space->screen_width_px / space->physical_width_mm;
|
||||
double scale_y = (double)space->screen_height_px / space->physical_height_mm;
|
||||
|
||||
space->transform.m[0][0] = scale_x;
|
||||
space->transform.m[1][1] = scale_y;
|
||||
space->is_calibrated = 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ========== 畸变校正 ========== */
|
||||
|
||||
/*
|
||||
* 设置畸变校正参数
|
||||
* 用于补偿摄像头镜头的径向和切向畸变
|
||||
*/
|
||||
void set_distortion_params(double k1, double k2, double p1, double p2,
|
||||
double cx, double cy) {
|
||||
g_coord_manager.distortion.k1 = k1;
|
||||
g_coord_manager.distortion.k2 = k2;
|
||||
g_coord_manager.distortion.p1 = p1;
|
||||
g_coord_manager.distortion.p2 = p2;
|
||||
g_coord_manager.distortion.cx = cx;
|
||||
g_coord_manager.distortion.cy = cy;
|
||||
g_coord_manager.distortion_enabled = 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 对物理坐标应用畸变校正(去畸变)
|
||||
* 使用Brown-Conrady模型的简化版本
|
||||
*
|
||||
* @param in 输入的物理坐标
|
||||
* @param out 校正后的物理坐标
|
||||
*/
|
||||
void apply_distortion_correction(const Point2D *in, Point2D *out) {
|
||||
if (!g_coord_manager.distortion_enabled) {
|
||||
out->x = in->x;
|
||||
out->y = in->y;
|
||||
return;
|
||||
}
|
||||
|
||||
DistortionParams *d = &g_coord_manager.distortion;
|
||||
|
||||
/* 以畸变中心为原点 */
|
||||
double dx = in->x - d->cx;
|
||||
double dy = in->y - d->cy;
|
||||
double r2 = dx * dx + dy * dy;
|
||||
double r4 = r2 * r2;
|
||||
|
||||
/* 径向畸变校正 */
|
||||
double radial = 1.0 + d->k1 * r2 + d->k2 * r4;
|
||||
|
||||
/* 切向畸变校正 */
|
||||
double tang_x = 2.0 * d->p1 * dx * dy + d->p2 * (r2 + 2.0 * dx * dx);
|
||||
double tang_y = d->p1 * (r2 + 2.0 * dy * dy) + 2.0 * d->p2 * dx * dy;
|
||||
|
||||
out->x = d->cx + dx * radial + tang_x;
|
||||
out->y = d->cy + dy * radial + tang_y;
|
||||
}
|
||||
|
||||
/* ========== 坐标变换核心接口 ========== */
|
||||
|
||||
/*
|
||||
* 根据page_id查找对应的坐标空间索引
|
||||
*/
|
||||
int find_space_by_page(unsigned int page_id) {
|
||||
int i;
|
||||
for (i = 0; i < g_coord_manager.space_count; i++) {
|
||||
if (g_coord_manager.spaces[i].page_id == page_id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 完整坐标变换流水线:原始点阵码坐标 → 屏幕像素坐标
|
||||
*
|
||||
* 处理步骤:
|
||||
* 1. Anoto编码 → 物理坐标(mm)
|
||||
* 2. 畸变校正(如果启用)
|
||||
* 3. 仿射变换 → 屏幕坐标(px)
|
||||
* 4. 边界裁剪(确保不超出屏幕范围)
|
||||
*
|
||||
* @param raw_x 原始X坐标
|
||||
* @param raw_y 原始Y坐标
|
||||
* @param page_id 页面ID
|
||||
* @param out_screen 输出屏幕坐标
|
||||
* @return 0成功, -1未找到坐标空间, -2未标定
|
||||
*/
|
||||
int transform_coordinate(unsigned int raw_x, unsigned int raw_y,
|
||||
unsigned int page_id, Point2D *out_screen) {
|
||||
if (out_screen == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 查找坐标空间 */
|
||||
int idx = find_space_by_page(page_id);
|
||||
if (idx < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CoordinateSpace *space = &g_coord_manager.spaces[idx];
|
||||
if (!space->is_calibrated) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* 步骤1:原始坐标 → 物理坐标 */
|
||||
Point2D physical;
|
||||
anoto_to_physical(raw_x, raw_y, space->section_id, &physical);
|
||||
|
||||
/* 步骤2:畸变校正 */
|
||||
Point2D corrected;
|
||||
apply_distortion_correction(&physical, &corrected);
|
||||
|
||||
/* 步骤3:仿射变换 → 屏幕坐标 */
|
||||
AffineMatrix *mat = &space->transform;
|
||||
out_screen->x = mat->m[0][0] * corrected.x
|
||||
+ mat->m[0][1] * corrected.y
|
||||
+ mat->m[0][2];
|
||||
out_screen->y = mat->m[1][0] * corrected.x
|
||||
+ mat->m[1][1] * corrected.y
|
||||
+ mat->m[1][2];
|
||||
|
||||
/* 步骤4:边界裁剪 */
|
||||
if (out_screen->x < 0.0) out_screen->x = 0.0;
|
||||
if (out_screen->y < 0.0) out_screen->y = 0.0;
|
||||
if (out_screen->x > (double)space->screen_width_px) {
|
||||
out_screen->x = (double)space->screen_width_px;
|
||||
}
|
||||
if (out_screen->y > (double)space->screen_height_px) {
|
||||
out_screen->y = (double)space->screen_height_px;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 批量坐标变换(优化版,避免重复查找坐标空间)
|
||||
* 适用于一次性转换整条笔画的所有采样点
|
||||
*
|
||||
* @param raw_points 原始坐标数组,每组2个unsigned int (x, y)
|
||||
* @param point_count 坐标点数量
|
||||
* @param page_id 页面ID
|
||||
* @param out_screen 输出屏幕坐标数组(调用者负责分配内存)
|
||||
* @return 成功转换的点数
|
||||
*/
|
||||
int transform_batch(const unsigned int *raw_points, int point_count,
|
||||
unsigned int page_id, Point2D *out_screen) {
|
||||
int idx = find_space_by_page(page_id);
|
||||
if (idx < 0 || out_screen == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
CoordinateSpace *space = &g_coord_manager.spaces[idx];
|
||||
if (!space->is_calibrated) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double dpi = g_coord_manager.dpi_resolution;
|
||||
if (dpi < 1.0) dpi = 300.0;
|
||||
double dots_to_mm = 25.4 / dpi;
|
||||
|
||||
AffineMatrix *mat = &space->transform;
|
||||
int converted = 0;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < point_count; i++) {
|
||||
/* 直接内联计算,减少函数调用开销 */
|
||||
double px = (double)raw_points[i * 2] * dots_to_mm;
|
||||
double py = (double)raw_points[i * 2 + 1] * dots_to_mm;
|
||||
|
||||
/* 畸变校正(内联) */
|
||||
if (g_coord_manager.distortion_enabled) {
|
||||
DistortionParams *d = &g_coord_manager.distortion;
|
||||
double dx = px - d->cx;
|
||||
double dy = py - d->cy;
|
||||
double r2 = dx * dx + dy * dy;
|
||||
double radial = 1.0 + d->k1 * r2 + d->k2 * r2 * r2;
|
||||
px = d->cx + dx * radial + 2.0 * d->p1 * dx * dy
|
||||
+ d->p2 * (r2 + 2.0 * dx * dx);
|
||||
py = d->cy + dy * radial + d->p1 * (r2 + 2.0 * dy * dy)
|
||||
+ 2.0 * d->p2 * dx * dy;
|
||||
}
|
||||
|
||||
/* 仿射变换 */
|
||||
double sx = mat->m[0][0] * px + mat->m[0][1] * py + mat->m[0][2];
|
||||
double sy = mat->m[1][0] * px + mat->m[1][1] * py + mat->m[1][2];
|
||||
|
||||
/* 边界裁剪 */
|
||||
if (sx < 0.0) sx = 0.0;
|
||||
if (sy < 0.0) sy = 0.0;
|
||||
if (sx > (double)space->screen_width_px) sx = (double)space->screen_width_px;
|
||||
if (sy > (double)space->screen_height_px) sy = (double)space->screen_height_px;
|
||||
|
||||
out_screen[i].x = sx;
|
||||
out_screen[i].y = sy;
|
||||
converted++;
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
/*
|
||||
* 反向变换:屏幕坐标 → 物理坐标
|
||||
* 用于在屏幕上点击后反推纸面物理位置
|
||||
* 需要计算仿射变换矩阵的逆矩阵
|
||||
*/
|
||||
int inverse_transform(double screen_x, double screen_y,
|
||||
unsigned int page_id, Point2D *out_physical) {
|
||||
int idx = find_space_by_page(page_id);
|
||||
if (idx < 0 || out_physical == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CoordinateSpace *space = &g_coord_manager.spaces[idx];
|
||||
AffineMatrix *mat = &space->transform;
|
||||
|
||||
/* 计算2x2子矩阵的行列式 */
|
||||
double det = mat->m[0][0] * mat->m[1][1] - mat->m[0][1] * mat->m[1][0];
|
||||
if (fabs(det) < 1e-12) {
|
||||
return -2; /* 矩阵不可逆 */
|
||||
}
|
||||
|
||||
double inv_det = 1.0 / det;
|
||||
|
||||
/* 减去平移分量 */
|
||||
double tx = screen_x - mat->m[0][2];
|
||||
double ty = screen_y - mat->m[1][2];
|
||||
|
||||
/* 应用逆矩阵 */
|
||||
out_physical->x = inv_det * (mat->m[1][1] * tx - mat->m[0][1] * ty);
|
||||
out_physical->y = inv_det * (mat->m[0][0] * ty - mat->m[1][0] * tx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* 自然写互动课堂应用开发SDK软件 V1.0
|
||||
* 笔迹平滑算法核心模块 - 笔迹坐标平滑与笔锋渲染
|
||||
*
|
||||
* 跨平台C语言核心库
|
||||
* 提供贝塞尔曲线平滑、笔锋宽度计算、坐标插值等算法
|
||||
* 确保各平台SDK输出一致的笔迹渲染效果
|
||||
*/
|
||||
|
||||
#ifndef STROKE_SMOOTHER_H
|
||||
#define STROKE_SMOOTHER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================== 常量定义 ==================== */
|
||||
|
||||
#define MAX_SMOOTH_POINTS 4096 /* 平滑输出点缓冲区大小 */
|
||||
#define MIN_POINT_DISTANCE 0.5f /* 最小点间距(低于此值合并) */
|
||||
#define BEZIER_SEGMENTS 8 /* 贝塞尔曲线分段数 */
|
||||
#define PRESSURE_SMOOTH_FACTOR 0.3f /* 压力平滑因子 */
|
||||
|
||||
/* ==================== 数据结构 ==================== */
|
||||
|
||||
/** 二维浮点坐标点 */
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
} Vec2f;
|
||||
|
||||
/** 带压力和时间戳的笔迹点 */
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
float pressure; /* 0.0-1.0 */
|
||||
float width; /* 计算后的笔画宽度 */
|
||||
uint32_t timestamp; /* 时间戳 */
|
||||
} SmoothPoint;
|
||||
|
||||
/** 笔迹平滑器上下文 */
|
||||
typedef struct {
|
||||
/* 输入点缓冲区(最近4个点,用于三次贝塞尔) */
|
||||
SmoothPoint input_buffer[4];
|
||||
int buffer_count;
|
||||
|
||||
/* 输出点缓冲区 */
|
||||
SmoothPoint output_buffer[MAX_SMOOTH_POINTS];
|
||||
int output_count;
|
||||
|
||||
/* 笔画宽度配置 */
|
||||
float min_width; /* 最小笔画宽度 */
|
||||
float max_width; /* 最大笔画宽度 */
|
||||
float velocity_scale; /* 速度对宽度的影响系数 */
|
||||
|
||||
/* 上一点的平滑压力值 */
|
||||
float last_smooth_pressure;
|
||||
|
||||
/* 统计信息 */
|
||||
uint32_t total_input_points;
|
||||
uint32_t total_output_points;
|
||||
} StrokeSmoother;
|
||||
|
||||
/* ==================== 数学工具函数 ==================== */
|
||||
|
||||
/** 两点间欧氏距离 */
|
||||
static inline float vec2f_distance(Vec2f a, Vec2f b) {
|
||||
float dx = b.x - a.x;
|
||||
float dy = b.y - a.y;
|
||||
return sqrtf(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/** 两点间线性插值 */
|
||||
static inline Vec2f vec2f_lerp(Vec2f a, Vec2f b, float t) {
|
||||
Vec2f result;
|
||||
result.x = a.x + (b.x - a.x) * t;
|
||||
result.y = a.y + (b.y - a.y) * t;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 浮点数线性插值 */
|
||||
static inline float float_lerp(float a, float b, float t) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
|
||||
/** 将值裁剪到范围 [min_val, max_val] */
|
||||
static inline float float_clamp(float value, float min_val, float max_val) {
|
||||
if (value < min_val) return min_val;
|
||||
if (value > max_val) return max_val;
|
||||
return value;
|
||||
}
|
||||
|
||||
/* ==================== 贝塞尔曲线算法 ==================== */
|
||||
|
||||
/**
|
||||
* 计算三次贝塞尔曲线上的点
|
||||
* B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
|
||||
*
|
||||
* 用于平滑连接相邻坐标点,消除折角使笔画圆润
|
||||
*/
|
||||
static Vec2f cubic_bezier(Vec2f p0, Vec2f p1, Vec2f p2, Vec2f p3, float t) {
|
||||
float u = 1.0f - t;
|
||||
float tt = t * t;
|
||||
float uu = u * u;
|
||||
float uuu = uu * u;
|
||||
float ttt = tt * t;
|
||||
|
||||
Vec2f point;
|
||||
point.x = uuu * p0.x + 3.0f * uu * t * p1.x + 3.0f * u * tt * p2.x + ttt * p3.x;
|
||||
point.y = uuu * p0.y + 3.0f * uu * t * p1.y + 3.0f * u * tt * p2.y + ttt * p3.y;
|
||||
return point;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Catmull-Rom样条生成贝塞尔控制点
|
||||
* 从4个数据点(p0,p1,p2,p3)计算p1到p2之间的贝塞尔控制点
|
||||
* 确保曲线经过原始数据点(C1连续)
|
||||
*/
|
||||
static void catmull_rom_to_bezier(
|
||||
Vec2f p0, Vec2f p1, Vec2f p2, Vec2f p3,
|
||||
Vec2f* cp1_out, Vec2f* cp2_out
|
||||
) {
|
||||
float tension = 0.5f; /* 张力系数,0.5为标准Catmull-Rom */
|
||||
cp1_out->x = p1.x + (p2.x - p0.x) * tension / 3.0f;
|
||||
cp1_out->y = p1.y + (p2.y - p0.y) * tension / 3.0f;
|
||||
cp2_out->x = p2.x - (p3.x - p1.x) * tension / 3.0f;
|
||||
cp2_out->y = p2.y - (p3.y - p1.y) * tension / 3.0f;
|
||||
}
|
||||
|
||||
/* ==================== 笔画宽度计算 ==================== */
|
||||
|
||||
/**
|
||||
* 根据压力和速度计算笔画宽度
|
||||
* 模拟真实毛笔/钢笔的笔锋效果:
|
||||
* - 压力越大,笔画越粗
|
||||
* - 速度越快,笔画越细(模拟快写时的飞白效果)
|
||||
* - 起笔/收笔处渐变细化
|
||||
*/
|
||||
static float calculate_stroke_width(
|
||||
float pressure, float velocity,
|
||||
float min_width, float max_width, float velocity_scale
|
||||
) {
|
||||
/* 压力影响:压力0→最细,压力1→最粗 */
|
||||
float pressure_width = min_width + (max_width - min_width) * pressure;
|
||||
|
||||
/* 速度衰减:速度快时笔画变细 */
|
||||
float velocity_factor = 1.0f / (1.0f + velocity * velocity_scale);
|
||||
|
||||
float width = pressure_width * velocity_factor;
|
||||
return float_clamp(width, min_width, max_width);
|
||||
}
|
||||
|
||||
/* ==================== 笔迹平滑器API ==================== */
|
||||
|
||||
/**
|
||||
* 初始化笔迹平滑器
|
||||
*/
|
||||
static void smoother_init(StrokeSmoother* ctx, float min_width, float max_width) {
|
||||
ctx->buffer_count = 0;
|
||||
ctx->output_count = 0;
|
||||
ctx->min_width = min_width;
|
||||
ctx->max_width = max_width;
|
||||
ctx->velocity_scale = 0.005f;
|
||||
ctx->last_smooth_pressure = 0.5f;
|
||||
ctx->total_input_points = 0;
|
||||
ctx->total_output_points = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入一个新的坐标点
|
||||
* 当缓冲区积累到4个点时,自动生成贝塞尔曲线平滑点
|
||||
* 返回新生成的平滑点数量
|
||||
*/
|
||||
static int smoother_add_point(StrokeSmoother* ctx, float x, float y,
|
||||
float pressure, uint32_t timestamp) {
|
||||
ctx->total_input_points++;
|
||||
|
||||
/* 压力平滑(低通滤波器,避免压力值跳变) */
|
||||
float smooth_pressure = ctx->last_smooth_pressure +
|
||||
PRESSURE_SMOOTH_FACTOR * (pressure - ctx->last_smooth_pressure);
|
||||
ctx->last_smooth_pressure = smooth_pressure;
|
||||
|
||||
/* 添加到输入缓冲区 */
|
||||
int idx = ctx->buffer_count;
|
||||
if (idx >= 4) {
|
||||
/* 缓冲区满,移位 */
|
||||
ctx->input_buffer[0] = ctx->input_buffer[1];
|
||||
ctx->input_buffer[1] = ctx->input_buffer[2];
|
||||
ctx->input_buffer[2] = ctx->input_buffer[3];
|
||||
idx = 3;
|
||||
}
|
||||
|
||||
ctx->input_buffer[idx].x = x;
|
||||
ctx->input_buffer[idx].y = y;
|
||||
ctx->input_buffer[idx].pressure = smooth_pressure;
|
||||
ctx->input_buffer[idx].timestamp = timestamp;
|
||||
ctx->buffer_count = idx + 1;
|
||||
|
||||
/* 不足4个点时直接输出原始点 */
|
||||
if (ctx->buffer_count < 4) {
|
||||
if (ctx->output_count < MAX_SMOOTH_POINTS) {
|
||||
/* 计算速度和宽度 */
|
||||
float velocity = 0;
|
||||
if (ctx->buffer_count >= 2) {
|
||||
Vec2f prev = {ctx->input_buffer[ctx->buffer_count-2].x, ctx->input_buffer[ctx->buffer_count-2].y};
|
||||
Vec2f curr = {x, y};
|
||||
float dt = (float)(timestamp - ctx->input_buffer[ctx->buffer_count-2].timestamp);
|
||||
if (dt > 0) velocity = vec2f_distance(prev, curr) / dt * 1000.0f;
|
||||
}
|
||||
|
||||
float width = calculate_stroke_width(smooth_pressure, velocity,
|
||||
ctx->min_width, ctx->max_width, ctx->velocity_scale);
|
||||
|
||||
SmoothPoint sp = {x, y, smooth_pressure, width, timestamp};
|
||||
ctx->output_buffer[ctx->output_count++] = sp;
|
||||
ctx->total_output_points++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 4个点准备好,生成贝塞尔曲线 */
|
||||
Vec2f p0 = {ctx->input_buffer[0].x, ctx->input_buffer[0].y};
|
||||
Vec2f p1 = {ctx->input_buffer[1].x, ctx->input_buffer[1].y};
|
||||
Vec2f p2 = {ctx->input_buffer[2].x, ctx->input_buffer[2].y};
|
||||
Vec2f p3 = {ctx->input_buffer[3].x, ctx->input_buffer[3].y};
|
||||
|
||||
/* 计算贝塞尔控制点 */
|
||||
Vec2f cp1, cp2;
|
||||
catmull_rom_to_bezier(p0, p1, p2, p3, &cp1, &cp2);
|
||||
|
||||
/* 在p1到p2之间生成平滑点 */
|
||||
int new_points = 0;
|
||||
for (int i = 0; i <= BEZIER_SEGMENTS; i++) {
|
||||
if (ctx->output_count >= MAX_SMOOTH_POINTS) break;
|
||||
|
||||
float t = (float)i / BEZIER_SEGMENTS;
|
||||
Vec2f pt = cubic_bezier(p1, cp1, cp2, p2, t);
|
||||
|
||||
/* 插值压力和时间戳 */
|
||||
float interp_pressure = float_lerp(ctx->input_buffer[1].pressure,
|
||||
ctx->input_buffer[2].pressure, t);
|
||||
uint32_t interp_time = (uint32_t)float_lerp(
|
||||
(float)ctx->input_buffer[1].timestamp,
|
||||
(float)ctx->input_buffer[2].timestamp, t);
|
||||
|
||||
/* 计算速度 */
|
||||
float velocity = 0;
|
||||
if (ctx->output_count > 0) {
|
||||
SmoothPoint* prev = &ctx->output_buffer[ctx->output_count - 1];
|
||||
Vec2f prev_v = {prev->x, prev->y};
|
||||
float dt = (float)(interp_time - prev->timestamp);
|
||||
if (dt > 0) velocity = vec2f_distance(prev_v, pt) / dt * 1000.0f;
|
||||
}
|
||||
|
||||
/* 计算笔画宽度 */
|
||||
float width = calculate_stroke_width(interp_pressure, velocity,
|
||||
ctx->min_width, ctx->max_width, ctx->velocity_scale);
|
||||
|
||||
/* 距离过滤:跳过距上一点太近的点 */
|
||||
if (ctx->output_count > 0) {
|
||||
SmoothPoint* prev = &ctx->output_buffer[ctx->output_count - 1];
|
||||
Vec2f prev_v = {prev->x, prev->y};
|
||||
if (vec2f_distance(prev_v, pt) < MIN_POINT_DISTANCE) continue;
|
||||
}
|
||||
|
||||
SmoothPoint sp = {pt.x, pt.y, interp_pressure, width, interp_time};
|
||||
ctx->output_buffer[ctx->output_count++] = sp;
|
||||
ctx->total_output_points++;
|
||||
new_points++;
|
||||
}
|
||||
|
||||
return new_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前笔画(抬笔时调用)
|
||||
* 输出最后一段贝塞尔曲线的收尾点
|
||||
*/
|
||||
static int smoother_end_stroke(StrokeSmoother* ctx) {
|
||||
int new_points = 0;
|
||||
|
||||
/* 输出缓冲区中剩余的点 */
|
||||
if (ctx->buffer_count >= 2 && ctx->output_count < MAX_SMOOTH_POINTS) {
|
||||
int last = ctx->buffer_count - 1;
|
||||
float width = calculate_stroke_width(
|
||||
ctx->input_buffer[last].pressure * 0.5f, 0, /* 收笔处宽度减半 */
|
||||
ctx->min_width, ctx->max_width, ctx->velocity_scale);
|
||||
|
||||
SmoothPoint sp = {
|
||||
ctx->input_buffer[last].x, ctx->input_buffer[last].y,
|
||||
ctx->input_buffer[last].pressure, width,
|
||||
ctx->input_buffer[last].timestamp
|
||||
};
|
||||
ctx->output_buffer[ctx->output_count++] = sp;
|
||||
new_points++;
|
||||
}
|
||||
|
||||
/* 重置输入缓冲区 */
|
||||
ctx->buffer_count = 0;
|
||||
ctx->last_smooth_pressure = 0.5f;
|
||||
|
||||
return new_points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平滑后的输出点
|
||||
*/
|
||||
static inline const SmoothPoint* smoother_get_output(const StrokeSmoother* ctx) {
|
||||
return ctx->output_buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输出点数量
|
||||
*/
|
||||
static inline int smoother_get_output_count(const StrokeSmoother* ctx) {
|
||||
return ctx->output_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除输出缓冲区
|
||||
*/
|
||||
static inline void smoother_clear_output(StrokeSmoother* ctx) {
|
||||
ctx->output_count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
static inline void smoother_get_stats(const StrokeSmoother* ctx,
|
||||
uint32_t* input_count, uint32_t* output_count) {
|
||||
if (input_count) *input_count = ctx->total_input_points;
|
||||
if (output_count) *output_count = ctx->total_output_points;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* STROKE_SMOOTHER_H */
|
||||
Reference in New Issue
Block a user