/* * 自然写智能点阵笔嵌入式固件软件 V1.0 * coordinate_task.c - 坐标计算任务 * * 功能说明: * 1. 从图像帧中解码Anoto点阵图案 * 2. 计算笔尖在纸面的物理坐标 * 3. 坐标滤波与去抖(卡尔曼滤波) * 4. 坐标打包为BLE传输格式 */ #include #include #include #include #include "FreeRTOS.h" #include "task.h" #include "queue.h" #include "dot_decoder.h" /* ========== 坐标数据包结构 ========== */ typedef struct { uint32_t raw_x; /* 原始X坐标(20位精度) */ uint32_t raw_y; /* 原始Y坐标 */ uint16_t pressure; /* 压力值(12位) */ uint32_t timestamp_ms; /* 时间戳 */ uint32_t page_id; /* 页面ID */ uint8_t pen_state; /* 笔状态:0=书写中, 1=笔落下, 2=笔抬起 */ } CoordinatePacket; /* ========== 卡尔曼滤波器 ========== */ typedef struct { float x_estimate; /* X状态估计值 */ float y_estimate; /* Y状态估计值 */ float x_error; /* X估计误差协方差 */ float y_error; /* Y估计误差协方差 */ float process_noise; /* 过程噪声 Q */ float measurement_noise; /* 测量噪声 R */ bool initialized; /* 是否已初始化 */ } KalmanFilter2D; /* ========== 外部引用 ========== */ extern QueueHandle_t g_image_data_queue; extern QueueHandle_t g_coordinate_queue; /* ========== 图像帧元数据结构(与image_capture_task一致) ========== */ typedef struct { uint8_t *pixel_buffer; uint32_t frame_id; uint32_t timestamp_ms; uint8_t quality_score; uint8_t exposure_value; uint16_t pressure_raw; } ImageFrameMetadata; /* ========== 静态变量 ========== */ /* 卡尔曼滤波器实例 */ static KalmanFilter2D s_kalman; /* 上一次有效坐标(用于抖动检测) */ static float s_last_valid_x = 0; static float s_last_valid_y = 0; /* 点阵码解码工作缓冲区 */ static uint8_t s_decode_buffer[128]; /* 统计信息 */ static uint32_t s_total_decoded = 0; static uint32_t s_decode_failures = 0; /* ========== 卡尔曼滤波实现 ========== */ /** * 初始化卡尔曼滤波器 * @param kf 滤波器实例 * @param q 过程噪声(越大跟踪越快,噪声越多) * @param r 测量噪声(越大滤波越强,延迟越大) */ static void kalman_init(KalmanFilter2D *kf, float q, float r) { kf->x_estimate = 0; kf->y_estimate = 0; kf->x_error = 1.0f; kf->y_error = 1.0f; kf->process_noise = q; kf->measurement_noise = r; kf->initialized = false; } /** * 卡尔曼滤波更新 * @param kf 滤波器实例 * @param measured_x 测量X值 * @param measured_y 测量Y值 * @param out_x 滤波后X输出 * @param out_y 滤波后Y输出 */ static void kalman_update(KalmanFilter2D *kf, float measured_x, float measured_y, float *out_x, float *out_y) { if (!kf->initialized) { /* 第一次测量,直接使用测量值 */ kf->x_estimate = measured_x; kf->y_estimate = measured_y; kf->initialized = true; *out_x = measured_x; *out_y = measured_y; return; } /* 预测步骤:状态不变模型(笔的位置预测 = 上一次估计) */ float x_pred = kf->x_estimate; float y_pred = kf->y_estimate; float x_err_pred = kf->x_error + kf->process_noise; float y_err_pred = kf->y_error + kf->process_noise; /* 更新步骤:计算卡尔曼增益 */ float kx = x_err_pred / (x_err_pred + kf->measurement_noise); float ky = y_err_pred / (y_err_pred + kf->measurement_noise); /* 融合预测与测量 */ kf->x_estimate = x_pred + kx * (measured_x - x_pred); kf->y_estimate = y_pred + ky * (measured_y - y_pred); /* 更新误差协方差 */ kf->x_error = (1.0f - kx) * x_err_pred; kf->y_error = (1.0f - ky) * y_err_pred; *out_x = kf->x_estimate; *out_y = kf->y_estimate; } /** * 重置卡尔曼滤波器(新笔画开始时调用) */ static void kalman_reset(KalmanFilter2D *kf) { kf->initialized = false; kf->x_error = 1.0f; kf->y_error = 1.0f; } /* ========== 抖动检测 ========== */ /** * 检测坐标是否为抖动(笔静止时传感器的微小抖动) * 如果两次坐标之间的距离小于阈值,视为抖动并丢弃 * * @param x 当前X坐标 * @param y 当前Y坐标 * @param threshold 抖动阈值(坐标单位) * @return true表示是抖动,应丢弃 */ static bool is_jitter(float x, float y, float threshold) { float dx = x - s_last_valid_x; float dy = y - s_last_valid_y; float distance_sq = dx * dx + dy * dy; return (distance_sq < threshold * threshold); } /* ========== 点阵码图像解码 ========== */ /** * 从32x32灰度图像中解码Anoto点阵图案 * * 解码步骤: * 1. 二值化:将灰度图转为黑白图 * 2. 点检测:定位图案中的各个墨点位置 * 3. 网格对齐:将检测到的点对齐到规则网格 * 4. 编码读取:根据点相对于网格中心的偏移方向读取编码值 * 5. 坐标计算:将编码序列映射为全局坐标 * * @param pixels 32x32灰度图像数据 * @param quality 图像质量评分 * @param out_x 解码输出X坐标 * @param out_y 解码输出Y坐标 * @param out_page_id 解码输出页面ID * @return 0成功, -1解码失败 */ static int decode_dot_pattern(const uint8_t *pixels, uint8_t quality, uint32_t *out_x, uint32_t *out_y, uint32_t *out_page_id) { /* 步骤1:自适应二值化 */ uint8_t threshold = 128; /* 根据图像亮度动态调整阈值(Otsu方法简化版) */ uint32_t histogram[256] = {0}; uint16_t i; for (i = 0; i < SENSOR_PIXELS; i++) { histogram[pixels[i]]++; } /* 寻找双峰之间的谷值作为阈值 */ uint32_t total = SENSOR_PIXELS; uint32_t sum = 0; for (i = 0; i < 256; i++) { sum += i * histogram[i]; } uint32_t sum_bg = 0; uint32_t weight_bg = 0; float max_variance = 0; for (i = 0; i < 256; i++) { weight_bg += histogram[i]; if (weight_bg == 0) continue; uint32_t weight_fg = total - weight_bg; if (weight_fg == 0) break; sum_bg += i * histogram[i]; float mean_bg = (float)sum_bg / weight_bg; float mean_fg = (float)(sum - sum_bg) / weight_fg; float diff = mean_bg - mean_fg; float variance = (float)weight_bg * weight_fg * diff * diff; if (variance > max_variance) { max_variance = variance; threshold = (uint8_t)i; } } /* 步骤2:二值化并检测墨点中心 */ uint8_t dot_count = 0; int16_t dot_x[64], dot_y[64]; /* 最多检测64个点 */ for (int row = 1; row < SENSOR_HEIGHT - 1; row++) { for (int col = 1; col < SENSOR_WIDTH - 1; col++) { uint8_t center = pixels[row * SENSOR_WIDTH + col]; if (center < threshold) { /* 暗像素,检查是否为局部极小值(简单的点中心检测) */ uint8_t up = pixels[(row - 1) * SENSOR_WIDTH + col]; uint8_t down = pixels[(row + 1) * SENSOR_WIDTH + col]; uint8_t left = pixels[row * SENSOR_WIDTH + (col - 1)]; uint8_t right = pixels[row * SENSOR_WIDTH + (col + 1)]; if (center <= up && center <= down && center <= left && center <= right) { if (dot_count < 64) { dot_x[dot_count] = col; dot_y[dot_count] = row; dot_count++; } } } } } /* 至少需要检测到4个点才能解码 */ if (dot_count < 4) { return -1; } /* 步骤3-5:调用点阵码解码器(核心算法在dot_decoder模块中) */ DotDecodeResult result; int ret = dot_decoder_process(dot_x, dot_y, dot_count, &result); if (ret == 0) { *out_x = result.coordinate_x; *out_y = result.coordinate_y; *out_page_id = result.page_id; return 0; } return -1; } /* ========== 压力值处理 ========== */ /** * 处理原始ADC压力值 * 12位ADC → 归一化并应用非线性映射 * * @param raw_adc 原始ADC值(0-4095) * @return 处理后的压力值(0-4095,非线性映射后) */ static uint16_t process_pressure(uint16_t raw_adc) { /* 去除静态偏移(笔尖自重产生的基础压力) */ const uint16_t offset = 200; if (raw_adc < offset) { return 0; } uint16_t adjusted = raw_adc - offset; /* 非线性映射(平方根曲线,使轻触更灵敏) */ float normalized = (float)adjusted / (4095.0f - offset); float mapped = sqrtf(normalized); return (uint16_t)(mapped * 4095); } /* ========== 坐标计算主任务 ========== */ /** * 坐标计算任务(FreeRTOS任务函数) * * 运行流程: * 1. 从图像数据队列接收帧元数据 * 2. 解码点阵图案获得原始坐标 * 3. 卡尔曼滤波去噪 * 4. 抖动检测 * 5. 坐标打包并放入BLE发送队列 */ void coordinate_task(void *pvParameters) { (void)pvParameters; ImageFrameMetadata frame; CoordinatePacket packet; /* 初始化卡尔曼滤波器 */ /* Q=0.1 跟踪速度适中, R=0.5 中等滤波强度 */ kalman_init(&s_kalman, 0.1f, 0.5f); /* 抖动检测阈值(坐标单位,约0.1mm) */ const float jitter_threshold = 3.0f; while (1) { /* 阻塞等待图像帧数据 */ if (xQueueReceive(g_image_data_queue, &frame, portMAX_DELAY) != pdTRUE) { continue; } /* 解码点阵图案 */ uint32_t raw_x, raw_y, page_id; int decode_ret = decode_dot_pattern(frame.pixel_buffer, frame.quality_score, &raw_x, &raw_y, &page_id); if (decode_ret != 0) { s_decode_failures++; continue; } s_total_decoded++; /* 卡尔曼滤波 */ float filtered_x, filtered_y; kalman_update(&s_kalman, (float)raw_x, (float)raw_y, &filtered_x, &filtered_y); /* 抖动检测 */ if (is_jitter(filtered_x, filtered_y, jitter_threshold)) { continue; /* 丢弃抖动数据 */ } /* 更新最后有效坐标 */ s_last_valid_x = filtered_x; s_last_valid_y = filtered_y; /* 处理压力值 */ uint16_t pressure = process_pressure(frame.pressure_raw); /* 构建坐标数据包 */ packet.raw_x = (uint32_t)filtered_x; packet.raw_y = (uint32_t)filtered_y; packet.pressure = pressure; packet.timestamp_ms = frame.timestamp_ms; packet.page_id = page_id; packet.pen_state = 0; /* 书写中 */ /* 放入BLE发送队列(非阻塞,满则丢弃最老的) */ if (xQueueSend(g_coordinate_queue, &packet, 0) != pdTRUE) { /* 队列满,读出一个旧数据再写入 */ CoordinatePacket dummy; xQueueReceive(g_coordinate_queue, &dummy, 0); xQueueSend(g_coordinate_queue, &packet, 0); } } }