文档版本: v3.0 最后更新: 2025年10月24日
ImageProcess 是一个用于处理和分析二值图像的桌面应用程序,特别适合与STM32 UDP图传系统配合使用。支持智能CSV读取、可调整窗口布局、平铺帧选择器、实时数据示波器、动态日志系统等专业功能。
✨ 最新更新:
- 🔥 动态日志系统 - C接口直接添加日志,自动写入CSV
- ✅ 示波器功能完全支持中文显示
- ✅ 修复进度条回拖时波形错乱问题
- ✅ 智能CSV列检测,兼容任意列顺序
- ✅ Binary模式读取,容错特殊字符
- 支持PNG、JPEG格式的二值图像
- 自动缩放至120×188标准尺寸
- 自动二值化处理
- 二维数组存储供后续处理
- 智能列检测: 自动识别任意列顺序
- Binary模式读取: 容错NULL/EOF等特殊字符
- 完整记录保证: 从旧版84行→新版309行完整读取
- 灵活列扩展: 支持任意数量自定义变量
- 动态日志写入: 代码中直接添加日志,自动追加到CSV
- 可拖动布局: GtkPaned实现三区域可调整
- 自适应缩放: 图像自动适应窗口大小
- 平铺帧选择器: 1400×900大窗口网格显示
- 实时日志显示: 动态显示所有日志变量
- 实时示波器: 多通道波形显示
- 8种配色方案: 自动分配不同颜色
- 自动缩放: Y轴自动适应数据范围
- 时间窗口可调: 1-60秒可调节
本程序处理的是以PNG或JPEG格式存储的二值图像,而非必须是1位深度的图像。这意味着:
- ✅ 支持8位、24位等PNG格式,只要内容是二值化的
- ✅ 支持标准JPEG格式图像
- ✅ 自动转换彩色或灰度图像为二值数据
- ✅ 所有图像缩放至120×188尺寸(不裁切)
- PNG格式: 1位、8位、24位、32位深度
- JPEG格式: 灰度、RGB彩色
- 自动转换: 彩色→灰度→二值化
- 加载图像 → 检测格式并读取
- 缩放 → 双线性插值至120×188
- 二值化 → 阈值128转换为0/255
- 存储 → 二维uint8_t数组
自动识别以下列(不区分大小写):
| 列类型 | 关键词 | 示例 | 
|---|---|---|
| 时间戳 | time, iso, timestamp | host_recv_iso | 
| Hex数据 | hex | log_text_hex | 
| UTF-8文本 | utf, text | log_text_utf8 | 
| 自定义变量 | 任意 | 温度, 速度, 电压 | 
- 使用std::ios::binary标志
- 移除\r,\0,\x1A等特殊字符
- Try-catch保护,单行错误不影响整体
- Debug输出:totalLines, skippedLines, errorLines
- 📈 多通道显示: 最多支持8个通道
- 🎨 配色方案: 红、青、黄、紫、绿、橙、蓝、粉
- 🔄 实时更新: 随视频播放自动更新
- 📐 自动缩放: Y轴自动适应数据范围
- ⏱️ 时间窗口: 1-60秒可调节
- 👁️ 通道开关: 单独显示/隐藏通道
- ✅ 使用Pango库渲染中文
- ✅ Microsoft YaHei字体
- ✅ 完美显示中文变量名
- ✅ 图例、坐标轴标签全部支持
- ✅ 关闭后可重新打开
- ✅ 保持通道和设置
- ✅ 隐藏而不是销毁窗口
主窗口
├── 水平分隔条
│   ├── 左侧区域(垂直分隔)
│   │   ├── 原始图像显示
│   │   └── IMO数组显示
│   └── 右侧区域
│       └── 平铺帧选择器
- 窗口大小: 1400×900像素
- 缩略图: 每帧120×188像素
- 网格布局: 自动计算行列数
- 交互:
- 鼠标悬停高亮
- 点击切换帧
- 当前帧红色边框
- 滚动浏览支持
 
- 动态创建标签显示变量
- 100×100像素画布
- 黑色背景
- 白色文字
- 自动换行
否则会出现类似以下错误:
fatal error: gtk/gtk.h: 没有那个文件或目录
Package gtk+-3.0 was not found in the pkg-config search path
sudo apt-get update
sudo apt-get install libgtk-3-dev libpng-dev libjpeg-dev build-essential cmakesudo yum install gtk3-devel libpng-devel libjpeg-devel cmakesudo dnf install gtk3-devel libpng-devel libjpeg-devel cmakepacman -S mingw-w64-x86_64-gtk3 \
          mingw-w64-x86_64-libpng \
          mingw-w64-x86_64-libjpeg-turbo \
          mingw-w64-x86_64-opencv \
          mingw-w64-x86_64-cmake \
          mingw-w64-x86_64-gccImageProcess/
├── src/                    # 源代码
│   ├── main.cpp           # GUI入口
│   ├── oscilloscope.cpp   # 示波器实现 ⭐
│   ├── csv_reader.cpp     # CSV读取器 ⭐
│   ├── dynamic_log.cpp    # 动态日志系统 ⭐
│   ├── processor.c        # 图像处理核心
│   ├── image.c            # 图像加载
│   └── video_processor.cpp # 视频工具
├── build/                  # 构建临时文件
├── install/                # 输出目录
│   ├── bin/               # 可执行文件
│   └── lib/               # 库文件
├── docs/                   # 文档目录 ⭐
│   └── COMPLETE_GUIDE.md  # 完整指南
├── cmake/                  # CMake脚本
├── CMakeLists.txt          # CMake配置
├── build.bat               # 一键构建(Windows)
├── clean.bat               # 清理脚本
├── run.bat                 # 一键运行
└── README.md               # 简要说明
Windows系统:
.\build.bat该脚本会自动:
- ✅ 检查并配置CMake
- ✅ 使用MinGW Makefiles生成器
- ✅ 编译项目
- ✅ 输出到install/bin/目录
Linux/Mac系统:
./build.sh# 配置
cmake -S . -B build -G "MinGW Makefiles"
# 编译
cmake --build build
# Release模式(可选)
cmake --build build --config Release如果遇到pkg-config错误:
export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:/usr/lib/pkgconfigWindows系统:
.\run.bat该脚本会自动:
- ✅ 检查程序是否存在
- ✅ 配置MSYS2 MinGW64环境
- ✅ 启动程序
Linux/Mac:
./install/bin/imageprocessorWindows PowerShell:
# 设置环境变量
$env:PATH = "C:\msys64\mingw64\bin;$env:PATH"
$env:XDG_DATA_DIRS = "C:\msys64\mingw64\share;$env:XDG_DATA_DIRS"
$env:GSETTINGS_SCHEMA_DIR = "C:\msys64\mingw64\share\glib-2.0\schemas"
# 运行程序
./install/bin/imageprocessor.exe.\clean.bat该脚本会删除:
- build/目录
- install/目录
- log/目录
- 点击 "加载日志CSV" 按钮
- 选择UDP上位机生成的CSV文件
- aligned.csv(推荐,包含完整数据)
- logs.csv(日志数据)
- frames_index.csv(帧索引)
 
- 程序自动识别列顺序和自定义变量
- 右侧平铺帧选择器显示所有帧缩略图
- 方式一: 点击平铺帧选择器中的缩略图
- 方式二: 使用"上一帧"/"下一帧"按钮
- 方式三: 使用进度条拖动
显示区域:
- 左上区域:原始图像(保持原始尺寸)
- 左下区域:IMO数组(处理后结果)
- 右侧区域:平铺帧选择器
- 点击 "📊 示波器" 按钮
- 从下拉框选择要监控的变量(如"温度")
- 点击 "添加通道" 按钮
- 播放视频或切换帧,波形自动更新
- 点击 "处理图像 (original→imo)" 按钮
- 程序执行从原始数组到IMO数组的转换
- 左下区域自动更新显示处理结果
- 查看原始数组: 点击"查看原始数组",显示二值数据
- 查看IMO数组: 点击"查看IMO数组",彩色显示结果
如果您在处理代码中使用了动态日志接口:
- 加载CSV文件后,动态日志自动启用
- 代码中的 log_add_*()调用会自动写入CSV
- 日志窗口显示"🔧 动态日志"部分
- 示波器可以显示动态日志变量
创建 test_data.csv 测试:
host_recv_iso,log_text_hex,log_text_utf8,温度,速度,电压
2025-10-24 10:00:00.001,48656C6C6F,Hello,25.5,120,3.3
2025-10-24 10:00:00.034,576F726C64,World,26.1,125,3.2
2025-10-24 10:00:00.067,54657374,Test,25.8,118,3.4
2025-10-24 10:00:00.101,44617461,Data,27.2,130,3.5
2025-10-24 10:00:00.134,496E666F,Info,26.5,122,3.3
- 水平分隔条: 拖动调整左侧图像区和右侧帧选择器比例
- 垂直分隔条: 拖动调整左上原始图像和左下IMO数组比例
- 布局自动保存,下次打开恢复
- 主窗口: 1400×900像素(推荐)
- 左侧区域: 占60-70%宽度
- 原始图像: 占左侧50%高度
- IMO数组: 占左侧50%高度
- 右侧选择器: 占30-40%宽度
程序支持自动检测CSV列格式,无需固定列顺序!
| 列类型 | 关键词 | 示例 | 
|---|---|---|
| 时间戳 | time, iso, timestamp | host_recv_iso, timestamp | 
| Hex数据 | hex | log_text_hex, data_hex | 
| UTF-8文本 | utf, text | log_text_utf8, text | 
| 自定义变量 | (其他任意) | 温度, 速度, 电压, frame_id | 
host_recv_iso,log_text_hex,log_text_utf8,温度,速度,电压
2025-10-24 10:00:00.001,48656C6C6F,Hello,25.5,120,3.3
- ✅ 包含时间戳、hex、utf8、自定义变量
- ✅ 最完整的日志信息
frame_id,png_path,frame_host_iso,h,w,log_host_iso,log_text_utf8,温度
1,frames_png/frame_000001.png,2025-10-24 10:00:00.001,120,188,2025-10-24 10:00:00.001,Hello,25.5
- ✅ 图像和日志对齐
- ✅ 包含PNG路径
- ✅ 支持视频播放
timestamp,log_message,value1,value2
2025-10-24 10:00:00.001,Hello,100,200
- ✅ 最小化格式
- ✅ 仅时间戳和自定义变量
recv_time,hex,utf8,温度,湿度,速度,电压,状态
2025-10-24 10:00:00.001,48656C6C6F,Hello,25.5,60,120,3.3,OK
- ✅ 任意数量自定义变量
- ✅ 自动识别所有列
std::ifstream file(filename, std::ios::binary);- 防止EOF字符(0x1A)截断
- 防止NULL字符(0x00)中断
- 完整读取所有行
自动移除以下字符:
- \r- 回车符(CR)
- \0- NULL字符
- \x1A- EOF/SUB字符(Ctrl+Z)
- Try-catch保护每行解析
- 单行错误不影响整体
- Debug输出统计信息:
总行数: 309 跳过行数: 0 错误行数: 0
- 最佳: aligned.csv- 图像+日志完整对齐
- 次选: logs.csv- 完整日志信息
- 可用: frames_index.csv- 仅帧索引
- ✅ 时间戳列包含 "time"、"iso" 或 "timestamp"
- ✅ Hex列包含 "hex"
- ✅ 文本列包含 "utf" 或 "text"
- ✅ 自定义变量使用描述性名称(如"温度"、"速度")
- ⚠️ 确保至少有一个时间戳列
- ⚠️ 避免列名重复
- ⚠️ 数值列应包含可解析的数字
- ⚠️ 文本使用UTF-8编码
- 📈 多通道显示: 最多支持8个通道同时显示
- 🎨 8种配色方案: 自动为每个通道分配不同颜色
- 🔄 实时更新: 随视频播放自动更新波形
- 📐 自动缩放: Y轴可自动调整范围适应数据
- ⏱️ 可调时间窗口: 1-60秒可调节显示范围
- 👁️ 通道开关: 可单独显示/隐藏每个通道
- 📊 实时数值显示: 图例中显示当前值
- 🌐 完美中文支持: 使用Pango渲染中文
- 确保已加载CSV文件
- 点击主窗口的 "📊 示波器" 按钮
- 示波器窗口弹出
- 从 "选择变量添加通道" 下拉框选择变量
- 例如:温度、速度、电压
 
- 点击 "添加通道" 按钮
- 通道列表中出现新添加的通道
- 波形区域显示对应颜色的波形
- 在主窗口播放视频或切换帧
- 示波器自动更新显示对应时刻的数据
- 观察各通道波形的变化趋势
- 时间窗口: 调整滑块改变显示的时间范围
- 自动缩放: 勾选/取消勾选切换Y轴缩放模式
- 显示/隐藏: 勾选通道列表中的复选框
- 删除通道: 选中通道后点击"删除选中"
变量: 温度、湿度、气压
用途: 观察环境参数随时间变化趋势
颜色: 红色、青色、黄色
变量: 速度、加速度、角度
用途: 分析机器人或车辆的运动状态
颜色: 紫色、绿色、橙色
变量: 目标值、实际值、误差
用途: 观察PID控制效果
颜色: 蓝色、粉色、红色
变量: 电压、电流、功率
用途: 监控电源系统稳定性
颜色: 青色、黄色、紫色
- 网格线: 10×10网格方便读数
- 颜色编码: 每个通道不同颜色
- 图例: 右上角显示通道名和当前值
- 坐标轴:
- X轴:时间(秒)
- Y轴:数值(自动缩放)
 
- 变量选择: 下拉框列出所有可用变量
- 通道列表: 显示已添加的通道,支持开关控制
- 时间窗口: 滑块调节1-60秒
- 自动缩放: 复选框开关
- 按钮:
- 添加通道
- 删除选中
- 清空全部
 
系统预设8种颜色,循环使用:
| 编号 | 颜色 | RGB值 | 用途示例 | 
|---|---|---|---|
| 1 | 🔴 红色 | (1.0, 0.2, 0.2) | 温度、错误 | 
| 2 | 🔵 青色 | (0.2, 0.8, 1.0) | 湿度、状态 | 
| 3 | 🟡 黄色 | (1.0, 0.8, 0.2) | 速度、警告 | 
| 4 | 🟣 紫色 | (0.6, 0.4, 1.0) | 角度、模式 | 
| 5 | 🟢 绿色 | (0.4, 0.9, 0.5) | 电压、正常 | 
| 6 | 🟠 橙色 | (1.0, 0.6, 0.3) | 电流、中等 | 
| 7 | 🔷 蓝色 | (0.3, 0.6, 0.9) | 压力、深度 | 
| 8 | 🌸 粉色 | (1.0, 0.4, 0.6) | 其他变量 | 
- 支持整数、浮点数、科学计数法
- 自动提取字符串中的数字部分
- 容错处理:无法解析的值显示为0
// 假设视频帧率为30fps
double current_time = frame_index / 30.0;
// 时间窗口
double time_min = current_time - time_window;
double time_max = current_time;// 自动模式:找到所有可见通道的最小最大值
if (auto_scale) {
    y_min = min(all_visible_channels);
    y_max = max(all_visible_channels);
    
    // 添加10%边距
    double range = y_max - y_min;
    if (range < 0.001) {
        // 范围太小,使用固定范围
        y_min = center - 0.5;
        y_max = center + 0.5;
    } else {
        y_min -= range * 0.1;
        y_max += range * 0.1;
    }
}// 检测时间倒退(用户往回拖动进度条)
if (!channel.times.empty() && current_time < channel.times.back()) {
    // 清除所有当前时间之后的数据
    while (!channel.times.empty() && channel.times.back() > current_time) {
        channel.times.pop_back();
        channel.values.pop_back();
    }
}- 使用std::deque高效管理数据点
- 仅保留时间窗口内的数据
- 自动清理超出范围的旧数据
- 按需刷新绘图,避免过度渲染
- 使用Cairo硬件加速
- Pango文字缓存
原因: 未加载CSV或未添加通道 解决:
- 确认已加载CSV文件(主窗口显示日志信息)
- 确认CSV中有数值变量
- 尝试添加通道
原因: 视频未播放或通道被隐藏 解决:
- 确认视频正在播放或切换帧
- 检查通道复选框是否勾选
- 检查时间窗口设置是否合理
原因: CSV中对应列不包含数值 解决:
- 检查CSV中对应列是否包含数值
- 确认变量名称选择正确
- 使用文本编辑器查看CSV原始数据
原因: 系统字体缺失 解决:
- 确认系统已安装Microsoft YaHei或SimHei字体
- 修改oscilloscope.cpp中的字体设置
- 重新编译程序
状态: ✅ 已修复 原理: 窗口隐藏而不是销毁
动态日志系统允许您在图像处理代码中直接添加日志变量,这些日志会:
- ✅ 自动写入CSV文件(与UDP日志同一文件)
- ✅ 在GUI日志窗口实时显示
- ✅ 支持示波器波形显示
- ✅ 与CSV日志完全等地位
#include "dynamic_log.h"
// 添加不同类型的日志变量
log_add_int32("帧号", 100, frame_index);
log_add_float("白色占比%", 45.6f, frame_index);
log_add_string("状态", "处理完成", frame_index);| 函数 | 参数类型 | 说明 | 
|---|---|---|
| log_add_int8() | int8_t | 有符号8位整数 | 
| log_add_uint8() | uint8_t | 无符号8位整数 | 
| log_add_int16() | int16_t | 有符号16位整数 | 
| log_add_uint16() | uint16_t | 无符号16位整数 | 
| log_add_int32() | int32_t | 有符号32位整数 | 
| log_add_uint32() | uint32_t | 无符号32位整数 | 
| log_add_float() | float | 单精度浮点数 | 
| log_add_double() | double | 双精度浮点数 | 
| log_add_string() | const char* | 字符串 | 
void process_frame(int frame_index) {
    // 执行图像处理...
    uint8_t **image = get_original_bi_image();
    int width = get_image_width();
    int height = get_image_height();
    
    // 统计白色像素
    int white_count = 0;
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            if (image[y][x] > 127) white_count++;
        }
    }
    
    // 添加日志(自动写入CSV)
    log_add_int32("白色像素数", white_count, frame_index);
    log_add_float("白色占比%", (float)white_count / (width * height) * 100, frame_index);
    log_add_string("状态", "处理完成", frame_index);
}void morphological_process(int frame_index) {
    int kernel_size = 3;
    int iterations = 2;
    
    // 执行形态学处理...
    binary_opening_bitpacked(kernel_size);
    binary_closing_bitpacked(kernel_size);
    
    // 记录处理参数
    log_add_int32("卷积核大小", kernel_size, frame_index);
    log_add_int32("迭代次数", iterations, frame_index);
}void detect_features(int frame_index) {
    // 执行特征检测...
    int edge_count = detect_edges();
    int corner_count = detect_corners();
    float confidence = calculate_confidence();
    
    // 记录检测结果
    log_add_int32("边缘点数", edge_count, frame_index);
    log_add_int32("角点数", corner_count, frame_index);
    log_add_float("置信度", confidence, frame_index);
}动态日志会以标准CSV格式写入文件:
host_recv_iso,log_text_hex,log_text_utf8,白色像素数,白色占比%,状态
2025-10-24 10:30:00,,Hello,14400,100.0,正常
2025-10-24 10:30:15,,[dynamic],12500,86.8,处理完成
2025-10-24 10:30:30,,World,13200,91.7,正常
格式说明:
- 时间戳:自动生成当前时间
- log_text_hex:留空
- log_text_utf8:标记为 [dynamic]
- 自定义变量:按添加顺序排列
- 在GUI中点击"加载日志CSV"
- 选择CSV文件(如 logs.csv)
- 程序自动配置动态日志写入同一文件
// 在您的图像处理代码中
log_add_int32("处理时间ms", 15, frame_index);
log_add_float("精度", 0.95f, frame_index);- 日志窗口: 实时显示动态日志(标记为"🔧 动态日志")
- CSV文件: 日志自动追加到文件末尾
- 示波器: 重新加载CSV后可显示动态日志波形
| 特性 | UDP日志(CSV) | 动态日志 | 
|---|---|---|
| 来源 | UDP上位机接收 | 代码中添加 | 
| 存储 | 写入CSV | 写入同一CSV | 
| 格式 | 标准CSV格式 | 标准CSV格式 | 
| 显示 | GUI日志窗口 | GUI日志窗口 | 
| 示波器 | ✅ 支持 | ✅ 支持 | 
| 时间戳 | UDP接收时间 | 代码执行时间 | 
| 用途 | 原始数据记录 | 处理结果记录 | 
完全等地位:两种日志除了来源不同,在存储、显示、分析上完全相同!
// 使用 -1 表示当前帧(由GUI自动设置)
log_add_string("算法", "形态学处理", -1);
log_add_float("执行时间ms", 12.5f, -1);// 清空指定帧的日志
log_clear_frame(10);
// 清空所有日志
log_clear_all();// 通常自动完成,也可手动调用
log_flush_to_csv();- CSV文件: 必须先在GUI中加载CSV,才能启用动态日志
- 帧索引: 确保帧索引与视频帧对应
- 性能: 每次添加日志都会写文件,批量处理时注意性能
- 变量名: 使用描述性的变量名,方便后续分析
- 数据类型: 选择合适的数据类型,避免精度损失
- ✅ 记录关键中间结果(如特征点数、阈值等)
- ✅ 记录算法参数和配置
- ✅ 记录处理时间和性能指标
- ✅ 使用有意义的变量名
- ❌ 不要在高频循环中添加日志
- ❌ 不要记录过多冗余信息
- ❌ 不要使用过长的字符串
- ❌ 不要忘记对应的帧索引
日期: 2025-10-24
修复的问题:
- 
✅ 中文乱码 - 添加UTF-8编码设置
- 使用g_setenv("LANG", "zh_CN.UTF-8", TRUE)
 
- 
✅ 无法添加变量 - 添加CSV路径保存功能
- 改进变量加载逻辑
- 添加友好提示信息
 
日期: 2025-10-24
修复的问题:
- 
✅ 中文乱码彻底解决 - 原因: Cairo的cairo_show_text()不支持中文
- 方案: 使用Pango库替代Cairo绘制文字
- 实现:
// 旧方案(乱码) cairo_show_text(cr, "温度"); // 新方案(正常) PangoLayout *layout = pango_cairo_create_layout(cr); PangoFontDescription *desc = pango_font_description_from_string("Microsoft YaHei 10"); pango_layout_set_font_description(layout, desc); pango_layout_set_text(layout, "温度", -1); pango_cairo_show_layout(cr, layout); 
 
- 原因: Cairo的
- 
✅ 关闭后无法再打开 - 原因: 窗口被gtk_widget_destroy()销毁
- 方案: 隐藏窗口而不是销毁
- 实现:
// 窗口关闭事件:返回TRUE阻止默认销毁行为 gboolean onWindowDelete(GtkWidget *widget, GdkEvent *event, gpointer user_data) { gtk_widget_hide(widget); // 隐藏而不是销毁 return TRUE; // 阻止默认行为 } 
 
- 原因: 窗口被
修改的文件:
- src/oscilloscope.cpp- 所有文字渲染函数
- src/oscilloscope.h- 添加窗口事件处理声明
修改的函数:
- buildUI()- CSS字体设置
- drawLegend()- Pango渲染图例
- drawAxisLabels()- Pango渲染坐标轴
- drawChannels()- Pango渲染提示文字
- onWindowDelete()- 新增窗口关闭事件处理
日期: 2025-10-24
修复的问题:
- 
✅ 多通道Y轴范围错乱 - 原因: 不同通道数值范围差异大,Y轴计算不当
- 方案: 改进Y轴范围计算逻辑
- 实现:
double range = y_max - y_min; if (range < 0.001) { // 范围太小,使用固定范围 double center = (y_max + y_min) / 2.0; y_min = center - 0.5; y_max = center + 0.5; } else { // 添加10%边距 y_min -= range * 0.1; y_max += range * 0.1; } // 确保最终范围有效 double y_range = y_max - y_min; if (y_range < 0.001) { y_range = 1.0; y_max = y_min + y_range; } 
 
- 
✅ 进度条回拖波形错乱 - 原因: 时间倒退时新旧数据混合
- 方案: 检测时间倒退并清除未来数据
- 实现:
// 检测时间倒退(用户往回拖动进度条) if (!channel.times.empty() && current_time < channel.times.back()) { // 清除所有当前时间之后的数据 while (!channel.times.empty() && channel.times.back() > current_time) { channel.times.pop_back(); channel.values.pop_back(); } } 
 
修改的文件:
- src/oscilloscope.cpp- Y轴计算和数据管理逻辑
修改的函数:
- drawChannels()- Y轴范围计算
- drawAxisLabels()- 同步Y轴范围
- updateChannelData()- 时间倒退检测
日期: 2025-10-23
解决的问题:
- ✅ CSV只能读取部分记录(84/306行)
- ✅ 特殊字符导致读取中断
改进内容:
- 
Binary模式读取 // 旧方案(文本模式,遇到EOF截断) std::ifstream file(filename); // 新方案(二进制模式,完整读取) std::ifstream file(filename, std::ios::binary); 
- 
特殊字符清理 // 移除会导致解析问题的字符 line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); // CR line.erase(std::remove(line.begin(), line.end(), '\0'), line.end()); // NULL line.erase(std::remove(line.begin(), line.end(), '\x1A'), line.end()); // EOF 
- 
错误容忍 try { // 解析CSV行 parseCSVLine(line); } catch (const std::exception& e) { errorLines++; continue; // 单行错误不影响整体 } 
结果:
- 从84行 → 309行完整读取
- 容错能力增强
- 兼容性提升
日期: 2025-10-24
新增功能:
- ✅ 自动识别任意列顺序
- ✅ 中英文列名匹配
- ✅ 任意数量自定义变量
实现原理:
// 扫描表头,识别列类型
for (size_t i = 0; i < headers.size(); ++i) {
    std::string lower_header = toLowerCase(headers[i]);
    
    if (contains(lower_header, "time") || 
        contains(lower_header, "iso") ||
        contains(lower_header, "timestamp")) {
        time_col = i;  // 时间戳列
    }
    else if (contains(lower_header, "hex")) {
        hex_col = i;  // Hex数据列
    }
    else if (contains(lower_header, "utf") || 
             contains(lower_header, "text")) {
        utf8_col = i;  // UTF-8文本列
    }
    else {
        var_names.push_back(headers[i]);  // 自定义变量
        var_cols.push_back(i);
    }
}- 使用gtk_widget_queue_draw()替代强制刷新
- 减少不必要的重绘次数
- 优化图像缩放算法
- 正确释放GdkPixbuf对象
- 使用RAII管理Cairo资源
- 避免Pango对象泄漏
- 修复所有-Wdeprecated-declarations警告
- 替换过时的GTK API
- 更新到GTK3标准用法
ImageProcess
├── GUI模块 (main.cpp)
│   ├── 窗口管理
│   ├── 事件处理
│   └── 界面更新
├── 示波器模块 (oscilloscope.cpp)
│   ├── 波形绘制
│   ├── 通道管理
│   └── 数据更新
├── CSV读取模块 (csv_reader.cpp)
│   ├── 文件解析
│   ├── 列检测
│   └── 数据提取
├── 图像处理模块 (processor.c)
│   ├── 二值化
│   ├── 缩放
│   └── 数组转换
└── 视频工具 (video_processor.cpp)
    ├── 视频解码
    ├── 帧提取
    └── 批处理
CSV文件 → CSVReader → LogRecord
                          ↓
                    OscilloscopeWindow
                          ↓
                    Channel Data (deque)
                          ↓
                    Cairo Drawing → GTK显示
struct LogRecord {
    std::string timestamp;           // 时间戳
    std::string hex_data;            // Hex数据
    std::string utf8_text;           // UTF-8文本
    std::map<std::string, std::string> variables;  // 自定义变量
};struct ChannelData {
    std::string name;                // 通道名称
    std::string variable_name;       // 变量名
    std::deque<double> times;        // 时间点
    std::deque<double> values;       // 数值
    double min_value;                // 最小值
    double max_value;                // 最大值
    Color color;                     // 颜色
    bool visible;                    // 是否可见
};uint8_t** original_bi_image;  // 原始二值图像 [120][188]
uint8_t** imo;                // 处理后的IMO数组 [120][188]struct DynamicLogVariable {
    std::string name;        // 变量名
    LogVarType type;         // 变量类型(int8/uint8/float等)
    std::string value_str;   // 字符串形式的值
};std::vector<std::string> parseCSVLine(const std::string& line) {
    std::vector<std::string> fields;
    std::string field;
    bool in_quotes = false;
    
    for (char c : line) {
        if (c == '"') {
            in_quotes = !in_quotes;
        } else if (c == ',' && !in_quotes) {
            fields.push_back(field);
            field.clear();
        } else {
            field += c;
        }
    }
    fields.push_back(field);
    
    return fields;
}double parseValue(const std::string& str) {
    // 尝试直接转换
    try {
        return std::stod(str);
    } catch (...) {
        // 提取字符串中的数字
        std::regex number_regex("[+-]?\\d+\\.?\\d*([eE][+-]?\\d+)?");
        std::smatch match;
        if (std::regex_search(str, match, number_regex)) {
            return std::stod(match[0]);
        }
        return 0.0;
    }
}void resizeImage(const uint8_t* src, int src_w, int src_h,
                 uint8_t* dst, int dst_w, int dst_h) {
    for (int y = 0; y < dst_h; y++) {
        for (int x = 0; x < dst_w; x++) {
            // 映射到源图像坐标
            float src_x = x * (float)src_w / dst_w;
            float src_y = y * (float)src_h / dst_h;
            
            // 双线性插值
            int x0 = (int)src_x;
            int y0 = (int)src_y;
            int x1 = std::min(x0 + 1, src_w - 1);
            int y1 = std::min(y0 + 1, src_h - 1);
            
            float dx = src_x - x0;
            float dy = src_y - y0;
            
            uint8_t v00 = src[y0 * src_w + x0];
            uint8_t v01 = src[y0 * src_w + x1];
            uint8_t v10 = src[y1 * src_w + x0];
            uint8_t v11 = src[y1 * src_w + x1];
            
            float v0 = v00 * (1 - dx) + v01 * dx;
            float v1 = v10 * (1 - dx) + v11 * dx;
            float v = v0 * (1 - dy) + v1 * dy;
            
            dst[y * dst_w + x] = (uint8_t)v;
        }
    }
}// 使用智能指针管理资源
std::unique_ptr<uint8_t[]> buffer(new uint8_t[size]);
// RAII包装GTK对象
struct GObjectDeleter {
    void operator()(GObject* obj) {
        if (obj) g_object_unref(obj);
    }
};
using GObjectPtr = std::unique_ptr<GObject, GObjectDeleter>;// 只在数据变化时刷新
if (data_changed) {
    gtk_widget_queue_draw(drawing_area);
}
// 使用双缓冲
cairo_surface_t* surface = cairo_image_surface_create(
    CAIRO_FORMAT_ARGB32, width, height);
cairo_t* cr = cairo_create(surface);
// ... 绘制到surface
cairo_set_source_surface(window_cr, surface, 0, 0);
cairo_paint(window_cr);
cairo_destroy(cr);
cairo_surface_destroy(surface);// 使用deque高效管理时序数据
std::deque<double> times;    // O(1)头尾插入删除
std::deque<double> values;
// 只保留时间窗口内的数据
while (!times.empty() && 
       (current_time - times.front()) > time_window) {
    times.pop_front();
    values.pop_front();
}// RAII确保资源释放
class FileGuard {
    std::FILE* fp;
public:
    FileGuard(const char* path, const char* mode)
        : fp(std::fopen(path, mode)) {}
    ~FileGuard() { if (fp) std::fclose(fp); }
    std::FILE* get() { return fp; }
};
// 使用
FileGuard file("data.csv", "r");
if (!file.get()) {
    throw std::runtime_error("Failed to open file");
}// 使用返回码
bool loadCSV(const char* filename) {
    try {
        // ... 加载逻辑
        return true;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return false;
    }
}
// 调用处检查
if (!loadCSV(path)) {
    showErrorDialog("Failed to load CSV");
}#define DEBUG_MODE 1
#if DEBUG_MODE
    #define DEBUG_LOG(fmt, ...) \
        fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
    #define DEBUG_LOG(fmt, ...)
#endif
// 使用
DEBUG_LOG("Loading CSV: %s", filename);
DEBUG_LOG("Found %d records", count);# 编译时添加调试符号
cmake -DCMAKE_BUILD_TYPE=Debug -S . -B build
cmake --build build
# 使用GDB调试
gdb ./install/bin/imageprocessor
# GDB命令
(gdb) break oscilloscope.cpp:123
(gdb) run
(gdb) print variable_name
(gdb) backtrace# 检查内存泄漏
valgrind --leak-check=full --show-leak-kinds=all \
    ./install/bin/imageprocessor
# 检查内存错误
valgrind --tool=memcheck ./install/bin/imageprocessor原因: 未安装GTK开发库
解决方案:
# Ubuntu/Debian
sudo apt-get install libgtk-3-dev
# Windows (MSYS2)
pacman -S mingw-w64-x86_64-gtk3原因: 未找到OpenCV库
解决方案:
# Ubuntu/Debian
sudo apt-get install libopencv-dev
# Windows (MSYS2)
pacman -S mingw-w64-x86_64-opencv
# CMake配置
cmake -DOpenCV_DIR=/path/to/opencv -S . -B build原因: 并行编译导致资源不足
解决方案:
# 限制并行任务数
cmake --build build -j2
# 或使用单线程
cmake --build build -j1状态: ✅ 已修复(v2.0版本)
解决方案:
- 更新到最新版本
- 重新编译程序
- 确认使用Binary模式读取
原因: 列名不符合识别规则
解决方案:
- 时间戳列包含"time"、"iso"或"timestamp"
- Hex列包含"hex"
- UTF-8列包含"utf"或"text"
- 或手动修改列名
检查:
- CSV第一行是否为列名(标题行)
- 列名是否与数据对齐
- 控制台是否输出"成功加载CSV"
调试:
// 添加DEBUG输出查看识别的列
std::cout << "Time column: " << time_col << std::endl;
std::cout << "Hex column: " << hex_col << std::endl;
std::cout << "UTF8 column: " << utf8_col << std::endl;
std::cout << "Variable columns: ";
for (size_t i : var_cols) {
    std::cout << i << " ";
}
std::cout << std::endl;原因: 未加载CSV或未添加通道
解决方案:
- 确认主窗口已加载CSV
- 点击"添加通道"按钮
- 查看通道列表是否有内容
原因: 视频未播放或通道被隐藏
解决方案:
- 播放视频或切换帧
- 检查通道复选框是否勾选
- 检查时间窗口设置(建议5-10秒)
状态: ✅ 已修复(v2.0版本)
如仍有问题:
- 确认系统已安装Microsoft YaHei字体
- 检查编译版本是否为v2.0+
- 尝试修改字体设置:
// oscilloscope.cpp中修改 PangoFontDescription *desc = pango_font_description_from_string("SimHei 10"); 
状态: ✅ 已修复(v2.0版本)
确认修复:
- 检查是否使用最新版本
- 重新编译程序
- 测试:打开→关闭→再打开
状态: ✅ 已修复(v3.0版本)
原理: 检测时间倒退并清除未来数据
状态: ✅ 已修复(v3.0版本)
原理: 改进Y轴范围计算,处理边界情况
原因: 缩放算法导致
说明:
- 使用双线性插值保证图像质量
- 小图放大时自然会有模糊
- 建议使用原始尺寸接近120×188的图像
原因: 二值化阈值问题
解决方案:
修改image.c中的阈值:
// 当前阈值128
if (pixel > 128) {
    binary_value = 255;  // 白色
} else {
    binary_value = 0;    // 黑色
}原因: 相对路径问题
解决方案:
- 使用绝对路径
- 或将PNG文件放在程序同目录下
- 确认CSV中的路径正确
可能原因:
- 图像文件过大
- CSV记录过多
- 示波器通道过多
优化建议:
- 减小图像分辨率
- 减少示波器通道数量
- 减小时间窗口(5-10秒)
- 关闭不必要的窗口
原因: 大量图像数据缓存
解决方案:
- 不要同时打开过多帧选择器
- 定期关闭不使用的窗口
- 减少视频帧数
错误: "无法找到msys-2.0.dll"等
解决方案:
# 添加MSYS2路径到PATH
$env:PATH = "C:\msys64\mingw64\bin;$env:PATH"
# 或使用run.bat脚本
.\run.bat解决方案:
# 安装中文字体
sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei
# 刷新字体缓存
fc-cache -fv原因: Mac使用Clang编译器,部分选项不同
解决方案:
# 使用Homebrew安装依赖
brew install gtk+3 opencv pkg-config
# 设置PKG_CONFIG_PATH
export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH"
# 编译
cmake -S . -B build
cmake --build build原因: 未在GUI中加载CSV文件
解决方案:
- 先在GUI中点击"加载日志CSV"
- 选择CSV文件
- 确认提示"动态日志将写入同一文件"
- 然后代码中的日志才会写入
检查:
- 确认已调用 log_add_*()函数
- 确认帧索引正确
- 重新加载CSV文件
- 查看CSV文件末尾是否有新记录
说明: 动态日志使用代码执行时的系统时间,不是UDP接收时间
正常行为:
- UDP日志:UDP接收时间戳
- 动态日志:代码添加时的时间戳
- 两者可能不一致,属于正常现象
如遇到文档未覆盖的问题:
- 查看控制台输出: 程序会输出详细的调试信息
- 检查日志文件: log/目录(如有)
- 查看CSV文件: 确认动态日志是否写入
- GitHub Issues: 提交问题到项目仓库
- 邮件联系: 包含错误信息和环境描述
项目地址: https://github.com/Dengdxx/udp
功能特点:
- 实时UDP图像和日志传输
- 自动生成PNG序列和CSV数据
- 支持自定义协议和变量配置
- v2.2版本已修复CSV数据完整性问题
与ImageProcess的关系:
- ImageProcess是UDP上位机的配套分析工具
- 读取UDP生成的CSV和PNG文件
- 提供更强大的数据可视化功能
┌──────────────┐
│  STM32设备   │
│              │
│ • 图像采集   │
│ • 压缩传输   │
│ • 日志打包   │
└──────┬───────┘
       │ WiFi/UDP
       ↓
┌──────────────┐
│ UDP上位机    │
│              │
│ • UDP接收    │
│ • PNG保存    │
│ • CSV记录    │
│ • 实时显示   │
└──────┬───────┘
       │ aligned.csv
       │ frames_png/
       ↓
┌──────────────┐
│ ImageProcess │
│              │
│ • CSV智能读取│
│ • 平铺帧选择 │
│ • 数据分析   │
│ • 波形显示   │
└──────────────┘
// 图像配置
#define IMAGE_FORMAT  COMPRESSED_BINARY  // 压缩二值(1位)
#define IMAGE_HEIGHT  120
#define IMAGE_WIDTH   188
#define COMPRESSION   8_TO_1  // 8:1压缩比
// 通信配置
#define SPI_SPEED     4000000  // 4Mbps(推荐)
#define UDP_PORT      8080
#define PACKET_SIZE   1024# 配置
IMAGE_FORMAT = "压缩二值(1位)"
FIXED_H = 120
FIXED_W = 188
FRAME_HEADER = "A0FFFFA0"
FRAME_FOOTER = "B0B00A0D"
# 输出
PNG_DIR = "frames_png"
LOG_CSV = "logs.csv"
FRAME_INDEX_CSV = "frames_index.csv"
ALIGNED_CSV = "aligned.csv"  # 推荐用于ImageProcess加载文件: aligned.csv
窗口大小: 1400×900(推荐)
示波器:
  - 时间窗口: 5-10秒
  - 通道数: 3-5个
  - 刷新率: 10Hz
UDP数据包格式:
[帧头: A0FFFFA0]
[图像数据: 900字节(120×188÷8)]
[帧尾: B0B00A0D]
或
[帧头: BB66]
[日志数据: 变长]
[帧尾: 0D0A]
PNG文件:
frames_png/
├── frame_000001.png  (120×188)
├── frame_000002.png
└── ...
CSV文件:
aligned.csv
├── frame_id
├── png_path
├── frame_host_iso
├── log_text_utf8
├── 温度
├── 速度
└── ...(自定义变量)
1. CSV智能读取
   ↓
2. 自动识别列
   ↓
3. 加载PNG图像
   ↓
4. 显示+分析
   ↓
5. 示波器可视化
| ImageProcess | UDP上位机 | 说明 | 
|---|---|---|
| v3.1 | v2.2+ | ✅ 完全兼容 + 动态日志(推荐) | 
| v3.0 | v2.2+ | ✅ 完全兼容(推荐) | 
| v2.0 | v2.1+ | ✅ 兼容,建议升级 | 
| v1.0 | v2.0 | 
升级建议:
- 优先升级UDP上位机到v2.2
- 升级ImageProcess到v3.1(支持动态日志)
- 删除旧的CSV文件
- 重新采集数据
- 在代码中使用动态日志接口记录处理结果
Ctrl+O    - 打开CSV文件
Ctrl+S    - 保存设置
Ctrl+W    - 关闭窗口
Ctrl+Q    - 退出程序
Space     - 播放/暂停
←/→       - 上一帧/下一帧
Ctrl+Z    - 撤销
Ctrl+Y    - 重做
# 直接加载CSV
./imageprocessor --csv=aligned.csv
# 指定帧
./imageprocessor --csv=data.csv --frame=100
# 批处理模式
./imageprocessor --batch --input=input/ --output=output/frame_id,host_recv_iso,png_path,h,w,log_text_utf8,温度,速度,电压,湿度
1,2025-10-24 10:00:00.001,frames_png/frame_000001.png,120,188,Hello,25.5,120,3.3,60
2,2025-10-24 10:00:00.034,frames_png/frame_000002.png,120,188,World,26.1,125,3.2,62
3,2025-10-24 10:00:00.067,frames_png/frame_000003.png,120,188,Test,25.8,118,3.4,61
4,2025-10-24 10:00:00.101,frames_png/frame_000004.png,120,188,Data,27.2,130,3.5,65
5,2025-10-24 10:00:00.134,frames_png/frame_000005.png,120,188,Info,26.5,122,3.3,63
import csv
import random
from datetime import datetime, timedelta
# 生成测试CSV
def generate_test_csv(filename, num_records=100):
    with open(filename, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        
        # 表头
        writer.writerow([
            'frame_id', 'host_recv_iso', 'png_path', 'h', 'w',
            'log_text_utf8', '温度', '速度', '电压', '湿度'
        ])
        
        # 数据
        base_time = datetime.now()
        for i in range(1, num_records + 1):
            timestamp = base_time + timedelta(milliseconds=i*33)
            writer.writerow([
                i,
                timestamp.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3],
                f'frames_png/frame_{i:06d}.png',
                120, 188,
                f'Log{i}',
                round(25 + random.uniform(-2, 2), 1),  # 温度
                random.randint(100, 150),              # 速度
                round(3.3 + random.uniform(-0.2, 0.2), 2),  # 电压
                random.randint(50, 70)                 # 湿度
            ])
if __name__ == '__main__':
    generate_test_csv('test_data.csv', 100)
    print('Generated test_data.csv with 100 records')感谢以下开源项目:
- GTK+: 跨平台GUI工具包
- Cairo: 2D图形库
- Pango: 国际化文本渲染
- OpenCV: 计算机视觉库
- CMake: 跨平台构建系统
本项目采用 MIT License。
欢迎提交Issue和Pull Request!
提交Issue时请包含:
- 操作系统和版本
- 编译器版本
- 详细的错误信息
- 复现步骤
提交PR时请确保:
- 代码风格一致
- 添加必要的注释
- 通过编译测试
- 更新相关文档
- 🔥 新增动态日志系统
- C接口函数,支持9种数据类型
- 自动写入CSV文件(与UDP日志合并)
- GUI日志窗口实时显示
- 示波器完全支持动态日志
- 与CSV日志完全等地位
 
- ✅ 修复示波器进度条回拖波形错乱
- ✅ 修复多通道Y轴范围计算
- ✅ 改进时间倒退检测
- ✅ 优化坐标映射算法
- ✅ 完美支持中文显示(Pango)
- ✅ 修复示波器关闭后无法再打开
- ✅ Binary模式CSV读取
- ✅ 智能列检测
- 初始版本发布
- 基本图像处理功能
- CSV读取功能
- 示波器基础功能
- GitHub: https://github.com/Dengdxx/ImageProcess
- Issues: 提交问题
- Wiki: 查看Wiki
文档结束 | 最后更新: 2025年10月24日 | 版本: v3.0