diff --git a/PAVO2_SDK/include/pavo2_parser.h b/PAVO2_SDK/include/pavo2_parser.h index 9ba43e6..ff44875 100644 --- a/PAVO2_SDK/include/pavo2_parser.h +++ b/PAVO2_SDK/include/pavo2_parser.h @@ -302,7 +302,9 @@ protected: std::condition_variable m_cmd_op_cond; std::map> m_map_cmd; - SynchronizedQueue m_scans_que; + // 有界队列(上限1000帧):空闲期未采集时接收线程仍在持续 enqueue, + // 若放任无界会线性占用内存直至撑爆;超限时丢最旧、保留最新数据。 + SynchronizedQueue m_scans_que{ 1000 }; SynchronizedQueue m_ptp_que; }; diff --git a/config.cpp b/config.cpp index 635521b..d0f0850 100644 --- a/config.cpp +++ b/config.cpp @@ -1,44 +1,31 @@ #include "config.h" #include "json_utils.h" +#include #include #include #include -// ==================== 配置变量定义 ==================== -std::string Leida_IP = "192.168.0.170"; -uint16_t Leida_PORT = 2368; -int Collect_Duration_S = 6; -double Speed_Z = 20.0; -bool Log_Flag = true; -double Z_RANGE_MM = 50.0; -double Y_RANGE_MM = 80.0; -double Lidar_Hz = 50.0; -int Rect_Width_PX = 8; -double Rect_Length_Ratio = 1.5; -std::string Run_Mode = "exe"; -uint16_t Tcp_Port = 9601; -double Floor_Height = 1600.0; -double Count_Base_Tolerance = 50.0; -double Count_Additional_Tolerance_Per_Level = 20.0; -int Point_Dilate = 8; -std::string Z_Interpolate_Mode = "none"; -int Log_Retain_Days = 15; -int Tuo_Wei = 2; -int Qian_Chong = 2; -int Yuan_Duan = 1; -bool Jin_Duan = true; - -std::vector> Filter_Polygon = { - {-650, 0}, {-650, 670}, {650, 670}, {650, 0} -}; -int Filter_Poly_N = (int)Filter_Polygon.size(); - -// ==================== 配置加载(JSON + // 注释行) ==================== +// ==================== 全局配置实例 ==================== +AppConfig cfg; + +// 解析 "HH:MM[:SS]" -> 时/分/秒;非法返回 false +static bool parse_clean_time(const std::string& s, int& h, int& m, int& sec) +{ + int a = -1, b = -1, c = -1; + int n = std::sscanf(s.c_str(), "%d:%d:%d", &a, &b, &c); + if (n == 2) { h = a; m = b; sec = 0; } + else if (n == 3) { h = a; m = b; sec = c; } + else return false; + return h >= 0 && h <= 23 && m >= 0 && m <= 59 && sec >= 0 && sec <= 59; +} + +// ==================== 配置加载(JSON + // 注释行,按分组嵌套解析) ==================== void load_config(const std::string& path) { std::ifstream fin(path); - if (!fin.is_open()) { + if (!fin.is_open()) + { std::cerr << "[WARN] 未找到配置文件: " << path << std::endl; exit(-1); } @@ -54,56 +41,149 @@ void load_config(const std::string& path) json_oss << line << "\n"; } fin.close(); - std::string text = json_oss.str(); - - std::string s; double d; bool bv; std::vector> poly; - - if (json_get_string(text, "Leida_IP", s)) Leida_IP = s; - if (json_get_number(text, "Leida_Port", d)) Leida_PORT = (uint16_t)d; - if (json_get_number(text, "Collect_Duration_S", d)) Collect_Duration_S = (int)d; - if (json_get_number(text, "Speed_Z", d)) Speed_Z = d; - if (json_get_bool(text, "Log_Flag", bv)) Log_Flag = bv; - if (json_get_number(text, "Z_Range_MM", d)) Z_RANGE_MM = d; - if (json_get_number(text, "Y_Range_MM", d)) Y_RANGE_MM = d; - if (json_get_number(text, "Lidar_Hz", d)) Lidar_Hz = d; - if (json_get_number(text, "Rect_Width_PX", d)) Rect_Width_PX = (int)d; - if (json_get_number(text, "Rect_Length_Ratio", d)) Rect_Length_Ratio = d; - if (json_get_string(text, "Run_Mode", s)) Run_Mode = s; - if (json_get_number(text, "Tcp_Port", d)) Tcp_Port = (uint16_t)d; - if (json_get_number(text, "Floor_Height", d)) Floor_Height = d; - if (json_get_number(text, "Count_Base_Tolerance", d)) { Count_Base_Tolerance = d; if (Count_Base_Tolerance < 0) Count_Base_Tolerance = 0; } - if (json_get_number(text, "Count_Additional_Tolerance_Per_Level", d)) { Count_Additional_Tolerance_Per_Level = d; if (Count_Additional_Tolerance_Per_Level < 0) Count_Additional_Tolerance_Per_Level = 0; } - if (json_get_number(text, "Point_Dilate", d)) { Point_Dilate = (int)d; if (Point_Dilate < 1) Point_Dilate = 1; if (Point_Dilate > 64) Point_Dilate = 64; } - if (json_get_string(text, "Z_Interpolate_Mode", s)) + const std::string text = json_oss.str(); + + std::string s; double d; bool bv; + std::vector> poly; + + // ---------- radar: 雷达连接与雷达内置滤波 ---------- + if (json_get_string(text, "radar", "ip", s)) cfg.radar.ip = s; + if (json_get_number(text, "radar", "port", d)) cfg.radar.port = (uint16_t)d; + if (json_get_number(text, "radar", "hz", d)) cfg.radar.hz = d; + if (json_get_number(text, "radar", "tail_filter", d)) cfg.radar.tail_filter = (int)d; + if (json_get_number(text, "radar", "front_filter", d)) cfg.radar.front_filter = (int)d; + if (json_get_number(text, "radar", "far_hr_filter", d)) cfg.radar.far_hr_filter = (int)d; + if (json_get_bool(text, "radar", "near_hr_filter", bv)) cfg.radar.near_hr_filter = bv; + + // ---------- scan: 扫描采集与走行 ---------- + if (json_get_number(text, "scan", "collect_duration_s", d)) cfg.scan.collect_duration_s = (int)d; + if (json_get_number(text, "scan", "z_speed", d)) cfg.scan.z_speed = d; + if (json_get_string(text, "scan", "z_speed_type", s)) + { + if (s == "configuration" || s == "uniformSpeed") cfg.scan.z_speed_type = s; + else cfg.scan.z_speed_type = "uniformSpeed"; + } + + // ---------- detect: 截面分析 ---------- + if (json_get_number(text, "detect", "z_range_mm", d)) cfg.detect.z_range_mm = d; + if (json_get_number(text, "detect", "y_range_mm", d)) cfg.detect.y_range_mm = d; + if (json_get_number(text, "detect", "rect_width_px", d)) cfg.detect.rect_width_px = (int)d; + if (json_get_number(text, "detect", "rect_length_ratio", d)) cfg.detect.rect_length_ratio = d; + + // ---------- count: 分层计数 ---------- + if (json_get_number(text, "count", "floor_height", d)) cfg.count.floor_height = d; + if (json_get_number(text, "count", "base_tolerance", d)) + { + cfg.count.base_tolerance = d; + if (cfg.count.base_tolerance < 0) cfg.count.base_tolerance = 0; + } + if (json_get_number(text, "count", "per_level_tolerance", d)) + { + cfg.count.per_level_tolerance = d; + if (cfg.count.per_level_tolerance < 0) cfg.count.per_level_tolerance = 0; + } + if (json_get_number(text, "count", "point_dilate", d)) + { + cfg.count.point_dilate = (int)d; + if (cfg.count.point_dilate < 1) cfg.count.point_dilate = 1; + if (cfg.count.point_dilate > 64) cfg.count.point_dilate = 64; + } + if (json_get_string(text, "count", "z_interpolate_mode", s)) + { + if (s == "CopyTranslation" || s == "AngleBucketInterpolation") cfg.count.z_interpolate_mode = s; + else cfg.count.z_interpolate_mode = "none"; + } + + // ---------- filter: 多边形过滤 ---------- + poly.clear(); + if (json_get_point_array(text, "filter", "polygon", poly) >= 3) + { + cfg.filter.polygon = poly; + cfg.filter.poly_n = (int)poly.size(); + } + + // ---------- filter.tail_cut: 图片级尾部减除(旧方案) ---------- + // 三级取值:filter 组 -> tail_cut 子对象 -> 子对象内字段 + { + std::string o = json_get_raw(text, "filter", "tail_cut"); + if (!o.empty()) + { + if (json_get_bool(o, "enable", bv)) cfg.filter.tail_cut.enable = bv; + if (json_get_number(o, "max_tail_length", d)) { cfg.filter.tail_cut.max_tail_length = (int)d; if (cfg.filter.tail_cut.max_tail_length < 0) cfg.filter.tail_cut.max_tail_length = 0; } + if (json_get_number(o, "y_delta", d)) { cfg.filter.tail_cut.y_delta = d; if (cfg.filter.tail_cut.y_delta < 0) cfg.filter.tail_cut.y_delta = 0; } + if (json_get_number(o, "min_run_length", d)) { cfg.filter.tail_cut.min_run_length = (int)d; if (cfg.filter.tail_cut.min_run_length < 1) cfg.filter.tail_cut.min_run_length = 1; } + if (json_get_number(o, "core_min_len", d)) { cfg.filter.tail_cut.core_min_len = (int)d; if (cfg.filter.tail_cut.core_min_len < 0) cfg.filter.tail_cut.core_min_len = 0; } + if (json_get_number(o, "support_rows", d)) { cfg.filter.tail_cut.support_rows = (int)d; if (cfg.filter.tail_cut.support_rows < 0) cfg.filter.tail_cut.support_rows = 0; if (cfg.filter.tail_cut.support_rows > 4) cfg.filter.tail_cut.support_rows = 4; } + } + } + + // ---------- filter.sparse_cut: 点云级稀疏拖影过滤(新方案) ---------- + { + std::string o = json_get_raw(text, "filter", "sparse_cut"); + if (!o.empty()) + { + if (json_get_bool(o, "enable", bv)) cfg.filter.sparse_cut.enable = bv; + if (json_get_number(o, "x_radius", d)) { cfg.filter.sparse_cut.x_radius = d; if (cfg.filter.sparse_cut.x_radius < 0) cfg.filter.sparse_cut.x_radius = 0; } + if (json_get_number(o, "circle_min", d)) { cfg.filter.sparse_cut.circle_min = (int)d; if (cfg.filter.sparse_cut.circle_min < 1) cfg.filter.sparse_cut.circle_min = 1; } + if (json_get_number(o, "run_min", d)) { cfg.filter.sparse_cut.run_min = (int)d; if (cfg.filter.sparse_cut.run_min < 1) cfg.filter.sparse_cut.run_min = 1; } + } + } + + // ---------- server: 运行模式 ---------- + if (json_get_string(text, "server", "mode", s)) cfg.server.mode = s; + if (json_get_number(text, "server", "port", d)) cfg.server.port = (uint16_t)d; + + // ---------- log: 日志 ---------- + if (json_get_bool(text, "log", "enable", bv)) cfg.log.enable = bv; + if (json_get_number(text, "log", "retain_days", d)) { - if (s == "CopyTranslation" || s == "AngleBucketInterpolation") Z_Interpolate_Mode = s; - else Z_Interpolate_Mode = "none"; + cfg.log.retain_days = (int)d; + if (cfg.log.retain_days < 1) cfg.log.retain_days = 1; } - if (json_get_number(text, "Log_Retain_Days", d)) { Log_Retain_Days = (int)d; if (Log_Retain_Days < 1) Log_Retain_Days = 1; } - if (json_get_number(text, "Tuo_Wei", d)) Tuo_Wei = (int)d; - if (json_get_number(text, "Qian_Chong", d)) Qian_Chong = (int)d; - if (json_get_number(text, "Yuan_Duan", d)) Yuan_Duan = (int)d; - if (json_get_bool(text, "Jin_Duan", bv)) Jin_Duan = bv; - if (json_get_point_array(text, "Filter_Polygon", poly) >= 3) + if (json_get_string(text, "log", "clean_time", s)) { - Filter_Polygon = poly; - Filter_Poly_N = (int)poly.size(); + int h = -1, m = -1, sec = -1; + if (parse_clean_time(s, h, m, sec)) + { + cfg.log.clean_hour = h; + cfg.log.clean_minute = m; + cfg.log.clean_sec = sec; + } + else + { + std::cout << "[WARN] log.clean_time 格式应为 \"HH:MM:SS\",已忽略: " << s + << " (仍用 " << cfg.log.clean_hour << ":" + << (cfg.log.clean_minute < 10 ? "0" : "") << cfg.log.clean_minute << ")" + << std::endl; + } } + // ==================== 打印配置摘要 ==================== std::cout << "========== 配置已加载 ==========" << std::endl; - std::cout << "雷达: " << Leida_IP << ":" << Leida_PORT - << " 帧率: " << Lidar_Hz << "Hz" << std::endl; - std::cout << "扫描默认时长: " << Collect_Duration_S << "s" - << " Z速度: " << Speed_Z << "cm/s(0=时间)" - << " 日志保留: " << Log_Retain_Days << "天" << std::endl; - std::cout << "运行模式: " << Run_Mode << (Run_Mode == "tcp" ? " (端口 " + std::to_string(Tcp_Port) + ")" : "") << std::endl; - std::cout << "分层冗余: 基准" << Count_Base_Tolerance - << " + 每层" << Count_Additional_Tolerance_Per_Level - << " 膨胀: " << Point_Dilate - << " 插值模式: " << Z_Interpolate_Mode << std::endl; - std::cout << "过滤多边形(" << Filter_Poly_N << "点):"; - for (const auto& p : Filter_Polygon) + std::cout << "雷达: " << cfg.radar.ip << ":" << cfg.radar.port + << " 帧率: " << cfg.radar.hz << "Hz" + << " 拖尾滤波: " << cfg.radar.tail_filter + << " 前冲滤波: " << cfg.radar.front_filter << std::endl; + std::cout << "扫描默认时长: " << cfg.scan.collect_duration_s << "s" + << " Z速度模式: " << cfg.scan.z_speed_type + << " Z速度: " << cfg.scan.z_speed << "cm/s(0=时间)" + << " 日志保留: " << cfg.log.retain_days << "天" + << " 每日清理: " << (cfg.log.clean_hour < 10 ? "0" : "") << cfg.log.clean_hour << ":" + << (cfg.log.clean_minute < 10 ? "0" : "") << cfg.log.clean_minute << ":" + << (cfg.log.clean_sec < 10 ? "0" : "") << cfg.log.clean_sec << std::endl; + std::cout << "运行模式: " << cfg.server.mode + << (cfg.server.mode == "tcp" || cfg.server.mode == "http" ? " (端口 " + std::to_string(cfg.server.port) + ")" : "") << std::endl; + std::cout << "分层冗余: 基准" << cfg.count.base_tolerance + << " + 每层" << cfg.count.per_level_tolerance + << " 膨胀: " << cfg.count.point_dilate + << " 插值模式: " << cfg.count.z_interpolate_mode << std::endl; + std::cout << "过滤多边形(" << cfg.filter.poly_n << "点):"; + for (const auto& p : cfg.filter.polygon) std::cout << " (" << p.first << "," << p.second << ")"; std::cout << std::endl; + std::cout << "拖影过滤: SparseCut=" << (cfg.filter.sparse_cut.enable ? "开" : "关") + << "(X±" << cfg.filter.sparse_cut.x_radius << "mm,同圈>=" << cfg.filter.sparse_cut.circle_min + << ",跨圈>=" << cfg.filter.sparse_cut.run_min << ")" + << " TailCut=" << (cfg.filter.tail_cut.enable ? "开" : "关") << std::endl; std::cout << "================================" << std::endl; } diff --git a/config.h b/config.h index e81cbf5..111898c 100644 --- a/config.h +++ b/config.h @@ -1,4 +1,6 @@ // 全局配置:变量声明 + 配置加载接口 +// 运行时配置按功能分组为 AppConfig 结构体,全局实例 cfg 由 config.cpp 定义。 +// 业务代码通过 cfg.radar.ip / cfg.log.enable / cfg.count.base_tolerance 等访问。 #ifndef CONFIG_H #define CONFIG_H @@ -15,31 +17,99 @@ const bool SHIFT_Y_POSITIVE = true; // Y值上移使最小Y≥0 const int SLICE_IMG_SIZE = 800; // 截面图片最大像素尺寸 const double ORIGIN_TOL_MM = 0.5; // 原点无效点判定容差(mm) -// ==================== 运行时配置变量(load_config 加载) ==================== -extern std::string Leida_IP; // 雷达IP -extern uint16_t Leida_PORT; // 雷达数据端口 -extern int Collect_Duration_S; // exe模式/默认扫描时长(秒) -extern double Speed_Z; // Z轴速度(cm/s),0=时间(ms),>0=移动距离(mm) -extern bool Log_Flag; // 是否写日志文件 -extern double Z_RANGE_MM; // Z截面区间长度(mm) -extern double Y_RANGE_MM; // Y截面区间长度(mm) -extern double Lidar_Hz; // 雷达帧率(Hz) -extern int Rect_Width_PX; // 截面矩形宽度(px) -extern double Rect_Length_Ratio; // 矩形长倍率 -extern std::string Run_Mode; // exe=直接检测一次, tcp=服务端模式 -extern uint16_t Tcp_Port; // TCP监听端口 -extern double Floor_Height; // 地板高度(mm): 物品高度=Floor_Height-Y -extern double Count_Base_Tolerance; // 分层初始冗余(mm) -extern double Count_Additional_Tolerance_Per_Level; // 每层递增冗余(mm) -extern int Point_Dilate; // 画layer图每个点膨胀像素 -extern std::string Z_Interpolate_Mode; // none/CopyTranslation/AngleBucketInterpolation -extern int Log_Retain_Days; // 日志(任务目录)保留天数, 超出自动删除 -extern int Tuo_Wei; // 拖尾滤波 0关1弱2中3强 -extern int Qian_Chong; // 前冲滤波 0关1弱2中3强 -extern int Yuan_Duan; // 远端高反 0关1弱2强 -extern bool Jin_Duan; // 近端高反识别 -extern std::vector> Filter_Polygon; // 过滤多边形(mm) -extern int Filter_Poly_N; // 多边形顶点数 +// ==================== 运行时配置(load_config 从 config/config 加载) ==================== +struct AppConfig +{ + // ---------- radar: 雷达连接 + 雷达内置滤波 ---------- + struct Radar + { + std::string ip = "192.168.0.170"; // 雷达IP + uint16_t port = 2368; // 雷达数据端口 + double hz = 50.0; // 雷达帧率(Hz) + int tail_filter = 2; // 拖尾滤波 0关1弱2中3强 + int front_filter = 2; // 前冲滤波 0关1弱2中3强 + int far_hr_filter = 1; // 远端高反 0关1弱2强 + bool near_hr_filter = true; // 近端高反识别 + } radar; + + // ---------- scan: 扫描采集与走行 ---------- + struct Scan + { + int collect_duration_s = 6; // exe模式/默认扫描时长(秒) + double z_speed = 20.0; // Z轴速度(cm/s),0=时间(ms),>0=移动距离(mm) + std::string z_speed_type = "uniformSpeed"; // uniformSpeed=匀速用z_speed / configuration=用config/speed.txt变速 + } scan; + + // ---------- detect: 截面分析 ---------- + struct Detect + { + double z_range_mm = 50.0; // Z截面区间长度(mm) + double y_range_mm = 80.0; // Y截面区间长度(mm) + int rect_width_px = 8; // 截面矩形宽度(px) + double rect_length_ratio = 1.5; // 矩形长倍率 + } detect; + + // ---------- count: 分层计数 ---------- + struct Count + { + double floor_height = 1600.0; // 地板高度(mm): 物品高度=floor_height-Y + double base_tolerance = 50.0; // 分层初始冗余(mm) + double per_level_tolerance = 20.0; // 每层递增冗余(mm) + int point_dilate = 8; // 画layer图每个点膨胀像素 + std::string z_interpolate_mode = "none"; // none/CopyTranslation/AngleBucketInterpolation + } count; + + // ---------- filter: 软件滤波(多边形 + 拖影) ---------- + struct Filter + { + // 连续段尾部减除(图片级, 旧方案, 默认关闭):剪除 count_points 图像左右两侧的胡须状拖影 + struct TailCut + { + bool enable = false; // 是否启用尾部减除 + int max_tail_length = 15; // 每端最多保留尾部长度(px≈mm) + double y_delta = 30.0; // 尾部Y比核心Y高出多少(mm)更易被剪 + int min_run_length = 5; // 连续段长度低于此直接删除 + int core_min_len = 5; // 主体核心最小长度,保护小物体 + int support_rows = 1; // 上下几行同列有支撑则不剪 + } tail_cut; + + // 点云级稀疏拖影过滤(计数模式, 默认开启):扫描结束后删除"胡须状拖影"点。 + // 稀疏点定义: 同一扫描圈(z相同)内 X±XRadius(mm) 无其它点(不同圈同X不算邻居)。 + // 满足任一判据即删该稀疏点: + // 1) 同圈连续: 同一圈内稀疏点数 >= circle_min(如某圈边缘外伸了N个点) + // 2) 跨圈连续: 相近X位置(±x_radius)的稀疏点在z上连续出现圈数 >= run_min + struct SparseCut + { + bool enable = true; // 总开关 + double x_radius = 10.0; // 同圈X邻域半径(mm): X±此值内无点=稀疏 + int circle_min = 2; // 同圈稀疏点数≥此 即删 + int run_min = 2; // 同一X位置跨圈连续出现≥此圈 即删 + } sparse_cut; + + std::vector> polygon = { {-650, 0}, {-650, 670}, {650, 670}, {650, 0} }; // 过滤多边形(mm) + int poly_n = (int)polygon.size(); // 多边形顶点数 + } filter; + + // ---------- server: 运行模式 ---------- + struct Server + { + std::string mode = "exe"; // exe=直接检测一次 / tcp=TCP服务端 / http=HTTP服务端 + uint16_t port = 9601; // TCP/HTTP监听端口 + } server; + + // ---------- log: 日志 ---------- + struct Log + { + bool enable = true; // 是否写日志文件 + int retain_days = 15; // 日志(任务目录)保留天数, 超出自动删除 + int clean_hour = 1; // 每日日志定时清理时刻 时(0-23),来源: 配置 log.clean_time "HH:MM:SS" + int clean_minute = 0; // 每日日志定时清理时刻 分(0-59) + int clean_sec = 0; // 每日日志定时清理时刻 秒(0-59) + } log; +}; + +// 全局配置实例(config.cpp 中定义) +extern AppConfig cfg; // 加载 JSON 格式配置文件(行首(忽略空白)以 // 开头的注释行会被剔除) // path 不存在时打印错误并退出 diff --git a/config/PAVO2_HTTP_API.postman_collection.json b/config/PAVO2_HTTP_API.postman_collection.json new file mode 100644 index 0000000..d914410 --- /dev/null +++ b/config/PAVO2_HTTP_API.postman_collection.json @@ -0,0 +1,143 @@ +{ + "info": { + "_postman_id": "3f0d9a12-7c5e-4b21-9d8a-6e1f2a3b4c5d", + "name": "PAVO2 雷达 HTTP API (POST json)", + "description": "HTTP 运行模式接口。运行模式 Run_Mode 需改为 \"http\"。\n用法: 程序启动后向 http://{{host}}:{{port}}/ 发送 POST 请求, body 为与 TCP 协议完全相同的 JSON(不带结尾 '>')。响应 body 与 TCP 相同(不带 '>')。\n提示: 修改 collection 变量 host 为服务程序所在电脑的 IP(默认 127.0.0.1); port 与配置 Tcp_Port 一致(默认 9601)。\n注意: HTTP 无长连接, 建议 count/check 使用 start(不带 scanTime) → 完成后 stop 拿结果; 若使用 scanTime 到时自动停止, 结果不会推送(仅打印在服务端日志)。", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { "key": "host", "value": "127.0.0.1", "type": "string" }, + { "key": "port", "value": "9601", "type": "string" }, + { "key": "baseUrl", "value": "http://{{host}}:{{port}}", "type": "string" } + ], + "item": [ + { + "name": "count start (固定时长, 到时自动停止)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"count\",\n \"scan\": \"start\",\n \"scanTime\": 6,\n \"box\": {\n \"l\": 154,\n \"w\": 153,\n \"h\": 310,\n \"maxPerLayer\": 30\n },\n \"task_id\": \"t1\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "count start (手动, 等 stop 拿结果)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"count\",\n \"scan\": \"start\",\n \"box\": {\n \"l\": 154,\n \"w\": 153,\n \"h\": 310,\n \"maxPerLayer\": 30\n },\n \"task_id\": \"t2\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "count stop (拿结果)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"count\",\n \"scan\": \"stop\",\n \"task_id\": \"t2\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "count stop (未扫描时查询, 返回 scan not running + count:0)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"count\",\n \"scan\": \"stop\",\n \"task_id\": \"none\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "check start (手动)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"check\",\n \"scan\": \"start\",\n \"task_id\": \"c1\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "check stop", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"check\",\n \"scan\": \"stop\",\n \"task_id\": \"c1\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "config (重载 config/config)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"config\"\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "ConfigureSpeed (标定速度剖面)", + "request": { + "method": "POST", + "header": [ + { "key": "Content-Type", "value": "application/json" } + ], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] }, + "body": { + "mode": "raw", + "raw": "{\n \"function\": \"ConfigureSpeed\",\n \"scanTime\": 6\n}", + "options": { "raw": { "language": "json" } } + } + } + }, + { + "name": "非 POST 测试 (应返回 405)", + "request": { + "method": "GET", + "header": [], + "url": { "raw": "{{baseUrl}}/", "host": ["{{baseUrl}}"], "path": [""] } + } + } + ] +} diff --git a/config/config b/config/config index 2ab9df4..3d06e6b 100644 --- a/config/config +++ b/config/config @@ -1,47 +1,110 @@ -//雷达IP +// 全局配置文件(分组嵌套结构) +// 注:注释须整行且以 // 开头;键值保持全局可读 { - //雷达IP - "Leida_IP": "192.168.1.170", - //雷达数据端口 - "Leida_Port": 2368, - //z轴速度 厘米/秒 (0=z轴为时间ms, >0=z轴为移动距离mm) - "Speed_Z": 20, - //日志打印使能 true/false - "Log_Flag": true, - //Z截面区间长度 毫米 - "Z_Range_MM": 50, - //Y截面区间长度 毫米 (只取 Y最小 ~ Y最小+此值 的点参与) - "Y_Range_MM": 80, - //雷达帧率 Hz (矩形长 = 速度/帧率) - "Lidar_Hz": 50, - //截面矩形宽度 像素 - "Rect_Width_PX": 12, - //截面矩形长度倍率 (矩形长=速度/帧率×倍率) - "Rect_Length_Ratio": 1.5, - //运行模式 exe=直接检测 / tcp=服务端模式 - "Run_Mode": "tcp", - //TCP服务端监听端口 (仅tcp模式有效) - "Tcp_Port": 9601, - //过滤多边形(mm), 至少3个顶点 - "Filter_Polygon": [[-550,0],[-550,1600],[500,1600],[500,0]], - //拖尾滤波 0=关, 1=弱, 2=中, 3=强 - "Tuo_Wei": 3, - //前冲滤波 0=关, 1=弱, 2=中, 3=强 - "Qian_Chong": 3, - //远端高反滤波 0=关, 1=弱, 2=强 - "Yuan_Duan": 2, - //近端高反识别 true开,false关 - "Jin_Duan": true, - //地板高度 毫米 (物品高度 = Floor_Height - Y轴, count计数用) - "Floor_Height": 1600, - //count分层冗余 初始冗余 毫米 (第1层容差) - "Count_Base_Tolerance": 50, - //count分层冗余 每层增加 毫米 - "Count_Additional_Tolerance_Per_Level": 20, - //count画layer图: 每个点膨胀像素 (Point_Dilate×Point_Dilate) - "Point_Dilate": 8, - //count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation - "Z_Interpolate_Mode": "CopyTranslation", - //日志(任务目录)保留天数, 启动与每日01:00自动清理更早的目录 - "Log_Retain_Days": 15 + // ==================== radar: 雷达连接与雷达内置滤波 ==================== + "radar": { + //雷达IP + "ip": "192.168.1.170", + //雷达数据端口 + "port": 2368, + //雷达帧率 Hz (矩形长 = 速度/帧率) + "hz": 50, + //拖尾滤波 0=关, 1=弱, 2=中, 3=强 + "tail_filter": 3, + //前冲滤波 0=关, 1=弱, 2=中, 3=强 + "front_filter": 3, + //远端高反滤波 0=关, 1=弱, 2=强 + "far_hr_filter": 2, + //近端高反识别 true开,false关 + "near_hr_filter": true + }, + + // ==================== scan: 扫描采集与走行 ==================== + "scan": { + //z轴速度模式: uniformSpeed=匀速(用下方z_speed), configuration=变速(用config/speed.txt, 每行"时间ms 距离mm", 无此文件时回退z_speed) + "z_speed_type": "uniformSpeed", + //z轴速度 厘米/秒 (0=z轴为时间ms, >0=z轴为移动距离mm; 仅在z_speed_type=uniformSpeed时直接生效) + "z_speed": 20, + //exe模式/默认扫描时长 秒 (tcp/http 模式可被请求中的 scanTime 覆盖) + "collect_duration_s": 6 + }, + + // ==================== detect: 截面分析 ==================== + "detect": { + //Z截面区间长度 毫米 + "z_range_mm": 50, + //Y截面区间长度 毫米 (只取 Y最小 ~ Y最小+此值 的点参与) + "y_range_mm": 80, + //截面矩形宽度 像素 + "rect_width_px": 12, + //截面矩形长度倍率 (矩形长=速度/帧率×倍率) + "rect_length_ratio": 1.5 + }, + + // ==================== count: 分层计数 ==================== + "count": { + //地板高度 毫米 (物品高度 = floor_height - Y轴, count计数用) + "floor_height": 1600, + //count分层冗余 初始冗余 毫米 (第1层容差)上下各base_tolerance + "base_tolerance": 15, + //count分层冗余 每层增加 毫米 + "per_level_tolerance": 1, + //count画layer图: 每个点膨胀像素 (point_dilate×point_dilate) + "point_dilate": 8, + //count z轴空隙插值模式: none / CopyTranslation / AngleBucketInterpolation + "z_interpolate_mode": "CopyTranslation" + }, + + // ==================== filter: 软件滤波(多边形 + 拖影) ==================== + "filter": { + //过滤多边形(mm), 至少3个顶点 + "polygon": [[-550,0],[-550,1600],[500,1600],[500,0]], + + //连续段尾部减除(图片级旧方案, 已由点云级 sparse_cut 取代, 建议保持 false) + "tail_cut": { + //是否启用尾部减除 + "enable": false, + //每端最多保留的尾部长度(px≈mm): 越小剪得越狠, 太大会误剪真实物体边缘 + "max_tail_length": 30, + //尾部像素Y比段核心Y高出的阈值(mm): 越小越激进剪拖影, 太大剪不干净 + "y_delta": 30, + //连续段长度低于此值(px)整段删除(孤立噪点) + "min_run_length": 5, + //主体核心最小长度(px), 保护小物体不被整体剪掉 + "core_min_len": 5, + //上下几行同列有点视为竖直主体边缘则豁免不剪(0=不使用) + "support_rows": 0 + }, + + //点云级稀疏拖影过滤(推荐, 默认开): 删除每圈边缘外脱离主体的稀疏外伸点(胡须状拖影) + //稀疏点 = 同一扫描圈(z相同)内 X±XRadius mm 无其它点(不同圈同X不算邻居) + "sparse_cut": { + //总开关 + "enable": true, + //同圈X邻域半径mm: 调大→删得狠; 调小→保守 + "x_radius": 30, + //同圈连续: 同一圈内稀疏点数≥此值即删 (对应某圈边缘外伸了N个点的情况) + "circle_min": 2, + //跨圈连续: 同一X位置在z方向连续出现≥此圈即删 (对应每圈外伸1~2个点、跨几十圈稳定) + "run_min": 2 + } + }, + + // ==================== server: 运行模式 ==================== + "server": { + //运行模式 exe=直接检测 / tcp=服务端模式 / http=HTTP服务端 + "mode": "tcp", + //TCP/HTTP服务端监听端口 (仅tcp/http模式有效) + "port": 9601 + }, + + // ==================== log: 日志 ==================== + "log": { + //日志打印使能 true/false + "enable": true, + //日志(任务目录)保留天数, 超出自动删除更早的目录 + "retain_days": 1, + //每日日志定时清理时刻, 格式 "HH:MM:SS" (24小时制, 秒可省略, 如 "06:01" 或 "01:00:00") + "clean_time": "06:01:30" + } } diff --git a/count.cpp b/count.cpp index e0d72ba..49e0666 100644 --- a/count.cpp +++ b/count.cpp @@ -3,6 +3,7 @@ #include "config.h" #include "filter.h" #include "io_utils.h" +#include "lidar_utils.h" #include #include #include @@ -12,6 +13,231 @@ #include #include +// ==================== 点云级稀疏拖影过滤 ==================== +// 现象: 平台/物体左右边缘外伸的"胡须状拖影"= 每圈端部外侧 1~N 个点, +// 圈内 X±cfg.filter.sparse_cut.x_radius 内无同圈邻居(稀疏),且沿 z 方向稳定跨很多圈; +// 若放任不管,这些点膨胀后会撑大 count 的面积/个数统计。 +// 稀疏点判定: 邻居只在同一圈(z相同)内比较(不同圈同X的点不互为邻居)。 +// 删除判据(满足任一即删该稀疏点): +// 1) 同圈连续: 同一圈内稀疏点数 >= cfg.filter.sparse_cut.circle_min; +// 2) 跨圈连续: 相近 X 位置(±2×XRadius 归并, 容忍圈间抖动)的稀疏点, +// z 上连续出现(相邻圈间隔容差=3×中位圈间隔,允许个别缺圈) 圈数 >= cfg.filter.sparse_cut.run_min。 +// 原地修改 points;返回删除点数。应在 z_gap_interpolate 之前调用。 +int sparse_density_filter(std::vector& points) +{ + if (!cfg.filter.sparse_cut.enable || points.empty()) return 0; + const double R = std::max(cfg.filter.sparse_cut.x_radius, 0.0); + if (R <= 0.0) return 0; + const int Cm = std::max(cfg.filter.sparse_cut.circle_min, 1); + const int Rm = std::max(cfg.filter.sparse_cut.run_min, 1); + const size_t n = points.size(); + + // ---- 1) 按 z 聚类成圈(同一 scan 包的点 z 相同) ---- + std::vector order(n); + for (size_t i = 0; i < n; i++) order[i] = i; + std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { + return points[a].z_val < points[b].z_val; }); + struct Circle { size_t beg, end; }; // [beg,end) 在 order 中 + std::vector circles; + { + size_t i = 0; + while (i < n) + { + size_t j = i + 1; + while (j < n && points[order[j]].z_val - points[order[i]].z_val < 1e-6) ++j; + circles.push_back({ i, j }); + i = j; + } + } + + // 相邻圈 z 间隔中位数 ×3 = 跨圈"连续"容差(允许个别圈缺失/抖动) + double z_merge = 1.0; + if (circles.size() >= 2) + { + std::vector dz; + dz.reserve(circles.size() - 1); + for (size_t i = 1; i < circles.size(); i++) + dz.push_back(points[order[circles[i].beg]].z_val - + points[order[circles[i - 1].beg]].z_val); + std::sort(dz.begin(), dz.end()); + z_merge = 3.0 * dz[dz.size() / 2]; + if (z_merge < 1.0) z_merge = 1.0; + } + + // ---- 2) 稀疏点判定: 同圈内 X±R 无其它点 ---- + std::vector sparse(n, 0); + for (const auto& c : circles) + { + std::vector ci; + ci.reserve(c.end - c.beg); + for (size_t k = c.beg; k < c.end; k++) ci.push_back(order[k]); + std::sort(ci.begin(), ci.end(), [&](size_t a, size_t b) { + return points[a].x_mm < points[b].x_mm; }); + + const size_t m = ci.size(); + if (m == 0) continue; + if (m == 1) { sparse[ci[0]] = 1; continue; } // 单点圈: 无同圈邻居 + + size_t lo = 0, hi = 0; + for (size_t k = 0; k < m; k++) + { + double xk = points[ci[k]].x_mm; + while (lo < m && points[ci[lo]].x_mm < xk - R) ++lo; + if (hi < lo) hi = lo; + while (hi + 1 < m && points[ci[hi + 1]].x_mm <= xk + R) ++hi; + if (hi - lo + 1 <= 1) sparse[ci[k]] = 1; // 窗口内只有自己 + } + } + + // ---- 3) 删除标记 ---- + std::vector del(n, 0); + // (a) 同圈连续: 该圈稀疏点数 >= Cm 则整圈的稀疏点删除 + for (const auto& c : circles) + { + int cnt = 0; + for (size_t k = c.beg; k < c.end; k++) + if (sparse[order[k]]) cnt++; + if (cnt >= Cm) + for (size_t k = c.beg; k < c.end; k++) + if (sparse[order[k]]) del[order[k]] = 1; + } + // (b) 跨圈连续: 相近X(±R)的稀疏点按x归簇, 簇内z连续段圈数 >= Rm 则删除 + { + std::vector sp; + for (size_t i = 0; i < n; i++) + if (sparse[i]) sp.push_back(i); + if (!sp.empty()) + { + std::sort(sp.begin(), sp.end(), [&](size_t a, size_t b) { + return points[a].x_mm < points[b].x_mm; }); + size_t i = 0; + while (i < sp.size()) + { + size_t j = i + 1; + std::vector clu; + clu.reserve(64); + clu.push_back(sp[i]); + // 同X位置归并容差 2×R:拖影点X在圈间有轻微抖动,只用R会漏掉抖动>R的点 + while (j < sp.size() && points[sp[j]].x_mm - points[sp[i]].x_mm <= 2.0 * R) + { + clu.push_back(sp[j]); + ++j; + } + std::sort(clu.begin(), clu.end(), [&](size_t a, size_t b) { + return points[a].z_val < points[b].z_val; }); + size_t s2 = 0; + while (s2 < clu.size()) + { + size_t e2 = s2 + 1; + while (e2 < clu.size() && + points[clu[e2]].z_val - points[clu[e2 - 1]].z_val <= z_merge) + ++e2; + if (e2 - s2 >= (size_t)Rm) + for (size_t k = s2; k < e2; k++) del[clu[k]] = 1; + s2 = e2; + } + i = j; + } + } + } + + // ---- 4) 原地删除并统计 ---- + size_t w = 0; + for (size_t i = 0; i < n; i++) + if (!del[i]) points[w++] = points[i]; + size_t rm = n - w; + points.resize(w); + if (rm > 0) + std::cout << "[count] 稀疏拖影过滤: 删除 " << rm << " 点 (剩 " << w << ")" + << " [X±" << R << "mm 无邻, 同圈>=" << Cm << "点, 跨圈连续>=" + << Rm << "圈]" << std::endl; + return (int)rm; +} + +// ==================== 连续段尾部减除 ==================== +// 对膨胀前的 0/1 二值图逐行(X方向)处理连续点段: +// 1) 长度 < cfg.filter.tail_cut.min_run_length 的段视为孤立噪点,整段删除; +// 2) 正常段保留中间核心 + 左右各 cfg.filter.tail_cut.max_tail_length,核心区外为"尾部候选"; +// 3) 尾部候选若满足 (该格最大Y - 段核心中位Y) > cfg.filter.tail_cut.y_delta 则删除 +// (对应左右边缘 Y 逐渐变大的胡须状拖影);若尾部Y与核心接近则保留,避免误伤真实边缘; +// 4) cfg.filter.tail_cut.support_rows>0 时,上下行同列有点视为竖直主体,豁免不删。 +// cell_y_max 为空数组表示不使用 Y 判据(此时只按长度裁剪)。 +static void trim_tail_runs(std::vector& image, int w, int h, + const std::vector& cell_y_max) +{ + if (w <= 0 || h <= 0) return; + const bool use_y = cell_y_max.size() == image.size(); + std::vector deleted(image.size(), 0); + auto at = [&](int x, int y) -> int { + if (x < 0 || x >= w || y < 0 || y >= h) return 0; + return image[(size_t)y * w + x]; + }; + + for (int y = 0; y < h; y++) + { + int x = 0; + while (x < w) + { + if (!at(x, y)) { x++; continue; } + int s = x; + while (x + 1 < w && at(x + 1, y)) x++; + int e = x; // 连续段 [s, e] + x = e + 1; + int len = e - s + 1; + + if (len < cfg.filter.tail_cut.min_run_length) // 孤立短段:整段删除 + { + for (int c = s; c <= e; c++) deleted[(size_t)y * w + c] = 1; + continue; + } + + // 段内核心 Y 中位数(该行该段的主体典型高度) + double core_y = 0.0; + if (use_y) + { + std::vector ys; + ys.reserve(len); + for (int c = s; c <= e; c++) + { + double ym = cell_y_max[(size_t)y * w + c]; + if (ym > -1e8) ys.push_back(ym); + } + if (ys.empty()) continue; + std::sort(ys.begin(), ys.end()); + core_y = ys[ys.size() / 2]; + } + + // 核心区:两端各留 MaxTailLength,中间至少要留 CoreMinLen + int left_cut = std::min(s + cfg.filter.tail_cut.max_tail_length, e - cfg.filter.tail_cut.core_min_len + 1); + int right_cut = std::max(e - cfg.filter.tail_cut.max_tail_length, s + cfg.filter.tail_cut.core_min_len - 1); + if (left_cut > right_cut) continue; // 段过短:整体视为核心,不裁剪 + + for (int c = s; c <= e; c++) + { + if (c >= left_cut && c <= right_cut) continue; // 核心保留 + + // Y 判据:尾部 Y 未明显高于核心 → 保留(真实边缘) + if (use_y) + { + double ym = cell_y_max[(size_t)y * w + c]; + if (ym > -1e8 && ym <= core_y + cfg.filter.tail_cut.y_delta) continue; + } + // 支撑判据:上下行同列有点 → 竖直主体边缘,豁免 + if (cfg.filter.tail_cut.support_rows > 0) + { + bool supported = false; + for (int r = 1; r <= cfg.filter.tail_cut.support_rows; r++) + if (at(c, y - r) || at(c, y + r)) { supported = true; break; } + if (supported) continue; + } + deleted[(size_t)y * w + c] = 1; + } + } + } + for (size_t i = 0; i < image.size(); i++) + if (deleted[i]) image[i] = 0; +} + // ==================== 点集 → 图像 ==================== std::vector points_to_image(const std::vector& points, double* area_out, @@ -24,16 +250,16 @@ std::vector points_to_image(const std::vector& points, if (out_h) *out_h = 0; if (points.empty()) return std::vector((size_t)MIN_DIM * MIN_DIM, 0); - // 宽: Filter_Polygon 多边形 X 最大最小值直接构成 (1mm=1px) + // 宽: cfg.filter.polygon 多边形 X 最大最小值直接构成 (1mm=1px) double polyXMin = 0.0, polyXMax = 0.0; - if (Filter_Poly_N > 0) + if (cfg.filter.poly_n > 0) { - polyXMin = Filter_Polygon[0].first; - polyXMax = Filter_Polygon[0].first; - for (int i = 1; i < Filter_Poly_N; i++) + polyXMin = cfg.filter.polygon[0].first; + polyXMax = cfg.filter.polygon[0].first; + for (int i = 1; i < cfg.filter.poly_n; i++) { - polyXMin = std::min(polyXMin, Filter_Polygon[i].first); - polyXMax = std::max(polyXMax, Filter_Polygon[i].first); + polyXMin = std::min(polyXMin, cfg.filter.polygon[i].first); + polyXMax = std::max(polyXMax, cfg.filter.polygon[i].first); } } double spanX = polyXMax - polyXMin; @@ -60,9 +286,11 @@ std::vector points_to_image(const std::vector& points, double xOffset = -polyXMin; // 优化:Step1 先按1x1网格标记点位置(重复点只写一次), - // Step2 再对标记位置膨胀 Point_Dilate×Point_Dilate + // Step2 再对标记位置膨胀 cfg.count.point_dilate×cfg.count.point_dilate std::vector marks; marks.reserve(points.size() / 2); + std::vector cell_y_max; // 每格最大Y(仅尾部减除启用时分配) + if (cfg.filter.tail_cut.enable) cell_y_max.assign((size_t)imageWidth * imageHeight, -1e9); for (const auto& p : points) { int px = (int)((p.x_mm + xOffset) * scaleX); @@ -74,13 +302,26 @@ std::vector points_to_image(const std::vector& points, image[idx] = 1; marks.push_back((uint32_t)idx); } + if (cfg.filter.tail_cut.enable && p.y_mm > cell_y_max[idx]) cell_y_max[idx] = p.y_mm; + } + + // 连续段尾部减除(膨胀前):剪掉左右两侧胡须状拖影, + // 避免其经膨胀放大后混入层面积统计 + if (cfg.filter.tail_cut.enable) + { + trim_tail_runs(image, imageWidth, imageHeight, cell_y_max); + // 剪除会改动 image,需重建 marks,防止被删格经膨胀“复活” + marks.clear(); + marks.reserve(image.size() / 8); + for (size_t i = 0; i < image.size(); i++) + if (image[i] == 1) marks.push_back((uint32_t)i); } - for (uint32_t idx : marks) // 膨胀 Point_Dilate×Point_Dilate + for (uint32_t idx : marks) // 膨胀 cfg.count.point_dilate×cfg.count.point_dilate { int px = (int)(idx % imageWidth); int py = (int)(idx / imageWidth); - for (int dy = 0; dy < Point_Dilate; dy++) - for (int dx = 0; dx < Point_Dilate; dx++) + for (int dy = 0; dy < cfg.count.point_dilate; dy++) + for (int dx = 0; dx < cfg.count.point_dilate; dx++) { int ix = px + dx, iy = py + dy; if (ix < imageWidth && iy < imageHeight) @@ -99,13 +340,19 @@ std::vector points_to_image(const std::vector& points, // ==================== z轴空隙插值 ==================== int z_gap_interpolate(std::vector& points) { - if (points.empty() || Z_Interpolate_Mode == "none") + if (points.empty() || cfg.count.z_interpolate_mode == "none") return 0; // 正常相邻两条(圈)的 z 间隔: 速度模式 = 每圈行进距离(mm); 时间模式 = 每圈耗时(ms) - double normal_gap = (Speed_Z > 0) - ? (Speed_Z * 10.0 / Lidar_Hz) // cm/s ×10 = mm/s, ÷Hz = mm/圈 - : (1000.0 / Lidar_Hz); // ms/圈 + // configuration(变速)模式: 用 speed.txt 剖面平均速度(mm/s)/帧率 作近似; 表无效时回退匀速。 + double normal_gap = 0.0; + double prof_v = speed_profile_avg_velocity_mm_s(); + if (cfg.scan.z_speed_type == "configuration" && prof_v > 0) + normal_gap = prof_v / cfg.radar.hz; // 平均 mm/圈 + else if (cfg.scan.z_speed > 0) + normal_gap = cfg.scan.z_speed * 10.0 / cfg.radar.hz; // cm/s ×10 = mm/s, ÷Hz = mm/圈 + else + normal_gap = 1000.0 / cfg.radar.hz; // ms/圈 if (normal_gap <= 0) return 0; // 空隙判定: 本条与上一条 z 差 > 2×正常间隔 视为断流空隙 @@ -178,7 +425,7 @@ int z_gap_interpolate(std::vector& points) if (n < 1) continue; if (n > MAX_FILL_LEVELS) n = MAX_FILL_LEVELS; - if (Z_Interpolate_Mode == "CopyTranslation") + if (cfg.count.z_interpolate_mode == "CopyTranslation") { // 复制"上一条"的点, z 均匀平移到空隙内的各补行 for (int k = 1; k <= n; ++k) @@ -195,7 +442,7 @@ int z_gap_interpolate(std::vector& points) } } } - else if (Z_Interpolate_Mode == "AngleBucketInterpolation") + else if (cfg.count.z_interpolate_mode == "AngleBucketInterpolation") { // 角度桶插值: 每桶用前A/后B平均点线性插值(平滑过渡) struct Buck { double sx, sy; int cnt; Buck() : sx(0), sy(0), cnt(0) {} }; @@ -239,7 +486,7 @@ int z_gap_interpolate(std::vector& points) } if (added > 0) - std::cout << "[count] z空隙插值: 模式=" << Z_Interpolate_Mode + std::cout << "[count] z空隙插值: 模式=" << cfg.count.z_interpolate_mode << ", 正常间隔=" << normal_gap << ", 判定阈值=2×间隔=" << gap_threshold << ", 新增" << added << "点" << std::endl; @@ -261,12 +508,12 @@ int calc_item_count(const std::vector& points, const CountParams& bo std::unordered_map> layer_map; for (const auto& p : points) { - if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, Filter_Polygon, Filter_Poly_N)) + if (FILTER_ENABLE && !point_in_polygon(p.x_mm, p.y_mm, cfg.filter.polygon, cfg.filter.poly_n)) continue; - double currentHeight = Floor_Height - p.y_mm; // 高度 = 地板值 - Y轴 + double currentHeight = cfg.count.floor_height - p.y_mm; // 高度 = 地板值 - Y轴 if (currentHeight < 0) continue; int level = (int)std::round(currentHeight / box.h); - double tolerance = Count_Base_Tolerance + level * Count_Additional_Tolerance_Per_Level; + double tolerance = cfg.count.base_tolerance + level * cfg.count.per_level_tolerance; if (std::abs(currentHeight - level * box.h) <= tolerance) layer_map[level].push_back(p); } @@ -282,10 +529,11 @@ int calc_item_count(const std::vector& points, const CountParams& bo if (it == layer_map.end()) continue; oss << " 层" << h << ": 点数=" << it->second.size(); // 高度范围: 该层算法对应的理论高度区间(每层都显示) - double tol = Count_Base_Tolerance + h * Count_Additional_Tolerance_Per_Level; + double tol = cfg.count.base_tolerance + h * cfg.count.per_level_tolerance; double h_lo = h * box.h - tol; double h_hi = h * box.h + tol; - oss << ", 高度范围(" << std::setprecision(0) << h_lo << "," << h_hi << ")"; + // setprecision(0) 在数值较大时会被切为科学计数法(1e+03),直接四舍五入输出整数 + oss << ", 高度范围(" << (long long)std::llround(h_lo) << "," << (long long)std::llround(h_hi) << ")"; if (it->second.size() > 1000) { double area = 0.0; @@ -357,13 +605,13 @@ void save_count_files_async(const std::vector& points, std::string out_dir = make_task_dir(params.task_id); size_t pt_cnt = points.size(); - // PCD:y 换成高度(Floor_Height - y_mm) + // PCD:y 换成高度(cfg.count.floor_height - y_mm) std::vector xs, ys, zs; xs.reserve(pt_cnt); ys.reserve(pt_cnt); zs.reserve(pt_cnt); for (const auto& p : points) { xs.push_back(p.x_mm); - ys.push_back(Floor_Height - p.y_mm); + ys.push_back(cfg.count.floor_height - p.y_mm); zs.push_back(p.z_val); } save_pcd_ascii(out_dir + "/count_data.pcd", xs, ys, zs); @@ -391,7 +639,7 @@ void save_count_files_async(const std::vector& points, } // 日志 - if (Log_Flag) + if (cfg.log.enable) { std::ofstream lf(out_dir + "/log.txt"); if (lf.is_open()) @@ -403,7 +651,7 @@ void save_count_files_async(const std::vector& points, << ", w=" << params.w << ", h=" << params.h << ", maxPerLayer=" << params.maxPerLayer << "\n"; - sum << "[count] Floor_Height: " << Floor_Height << " mm\n"; + sum << "[count] floor_height: " << cfg.count.floor_height << " mm\n"; sum << "[count] 采集点数: " << pt_cnt << "\n"; sum << "[count] 计数层: 层" << top_lv << " (面积=" << std::fixed << std::setprecision(1) diff --git a/count.h b/count.h index f8bf572..f1e6037 100644 --- a/count.h +++ b/count.h @@ -7,9 +7,14 @@ #include #include "common.h" +// 点云级稀疏拖影过滤:删除每圈边缘外脱离主体的稀疏外伸点(胡须状拖影), +// 使 pcd/图片/面积统计都基于干净点云。判据见 cfg.filter.sparse_cut。 +// 原地修改 points;返回删除点数。应在 z_gap_interpolate 之前调用。 +int sparse_density_filter(std::vector& points); + // 点集 → X-Z平面白底图像(0=白,255=黑点) + 换算面积。 -// 宽: Filter_Polygon 多边形X跨度(1mm=1px); 高: z最大值(1单位=1px); -// 点先按1x1网格标记再膨胀 Point_Dilate×Point_Dilate。 +// 宽: cfg.filter.polygon 多边形X跨度(1mm=1px); 高: z最大值(1单位=1px); +// 点先按1x1网格标记再膨胀 cfg.count.point_dilate×cfg.count.point_dilate。 // area_out 返回换算实际面积(mm^2); out_w/out_h 返回实际图像尺寸。 std::vector points_to_image(const std::vector& points, double* area_out = nullptr, @@ -19,12 +24,12 @@ std::vector points_to_image(const std::vector& points, // z轴空隙插值:雷达断流/瞬间丢包导致的 z 空白带,仅在停止采集后调用一次。 // 原理:同一圈(scan包)点 z 相同,正常圈间隔≈normal_gap; // gap>2×normal_gap 且相邻两圈 Y均值差≤30mm 且 X 在相邻圈范围内时,空隙内补行。 -// Z_Interpolate_Mode: none/CopyTranslation/AngleBucketInterpolation(最多补10行)。 +// cfg.count.z_interpolate_mode: none/CopyTranslation/AngleBucketInterpolation(最多补10行)。 // 会原地修改 points(新增点追加到末尾);返回新增点数。 int z_gap_interpolate(std::vector& points); -// 按层统计物品总数(参照Java):高度=Floor_Height-Y;level=round(高度/层高); -// 每层容差 = Count_Base_Tolerance + level*Count_Additional_Tolerance_Per_Level; +// 按层统计物品总数(参照Java):高度=cfg.count.floor_height-Y;level=round(高度/层高); +// 每层容差 = cfg.count.base_tolerance + level*cfg.count.per_level_tolerance; // 从最高层(5)往下找有效层,个数=min(round(面积/(行*宽)), maxPerLayer), // 结果 = maxPerLayer*(层号-1)+该层个数。 // 返回总个数;不足一个完整箱时返回0。 @@ -37,10 +42,10 @@ int calc_item_count(const std::vector& points, const CountParams& bo double* top_layer_area = nullptr); // 将一次 count 结果保存到任务目录 make_task_dir(task_id)(后台线程异步执行,不阻塞响应): -// count_data.pcd (Y已换成高度 = Floor_Height - y_mm) +// count_data.pcd (Y已换成高度 = cfg.count.floor_height - y_mm) // count_points.jpg (全部点 X-Z 平面图) // count_layer.jpg (被计数层图像, 有有效层时) -// log.txt (Log_Flag 开启时) +// log.txt (cfg.log.enable 开启时) void save_count_files_async(const std::vector& points, const std::vector& top_layer_img, int top_img_w, int top_img_h, diff --git a/detect.cpp b/detect.cpp index 435a597..fa85443 100644 --- a/detect.cpp +++ b/detect.cpp @@ -4,6 +4,7 @@ #include "detect.h" #include "config.h" #include "io_utils.h" +#include "lidar_utils.h" #include #include #include @@ -18,9 +19,9 @@ void detect_analyze_slices(const std::vector& points, { make_dirs(out_dir); - // 日志:Log_Flag 开启时同时写 log.txt(UTF-8) + // 日志:cfg.log.enable 开启时同时写 log.txt(UTF-8) std::ofstream log_file; - if (Log_Flag) + if (cfg.log.enable) log_file.open(out_dir + "/log.txt"); auto log_msg = [&log_file](const std::string& s) { std::cout << s; @@ -92,14 +93,14 @@ void detect_analyze_slices(const std::vector& points, std::to_string(y_max) + " mm\n"); // Step 2: Y截面区间 = [Y最小, Y最小+Y_Range_MM] - double y_slice_max = y_min + Y_RANGE_MM; + double y_slice_max = y_min + cfg.detect.y_range_mm; std::vector y_filtered; y_filtered.reserve(pts.size()); for (const auto& pt : pts) if (pt.y_mm >= y_min && pt.y_mm <= y_slice_max) y_filtered.push_back(&pt); log_msg("[截面] Y截面区间: " + std::to_string(y_min) + " ~ " + - std::to_string(y_slice_max) + " mm (长度=" + std::to_string(Y_RANGE_MM) + + std::to_string(y_slice_max) + " mm (长度=" + std::to_string(cfg.detect.y_range_mm) + "mm, 配置Y_Range_MM)\n"); log_msg("[截面] 参与点数: " + std::to_string(y_filtered.size()) + " / " + std::to_string(pts.size()) + "\n"); @@ -123,7 +124,7 @@ void detect_analyze_slices(const std::vector& points, if (pt->z_val < gz_min) gz_min = pt->z_val; if (pt->z_val > gz_max) gz_max = pt->z_val; } - const char* z_unit = (Speed_Z > 0) ? "mm" : "ms"; + const char* z_unit = z_is_distance_mode() ? "mm" : "ms"; log_msg("[截面] 全局X范围(全图点云): " + std::to_string(gx_min) + " ~ " + std::to_string(gx_max) + " mm (图像高度方向)\n"); log_msg("[截面] 全局Z范围: " + std::to_string(gz_min) + " ~ " + @@ -132,9 +133,9 @@ void detect_analyze_slices(const std::vector& points, // Step 4: 两个Z区间 double z_first_start = gz_min; - double z_first_end = gz_min + Z_RANGE_MM; + double z_first_end = gz_min + cfg.detect.z_range_mm; double z_last_end = gz_max; - double z_last_start = gz_max - Z_RANGE_MM; + double z_last_start = gz_max - cfg.detect.z_range_mm; if (z_first_end > gz_max) z_first_end = gz_max; if (z_last_start < gz_min) z_last_start = gz_min; @@ -168,7 +169,7 @@ void detect_analyze_slices(const std::vector& points, double sx_range = gx_max - gx_min; if (sx_range < 1.0) sx_range = 1.0; - const int img_w = std::max(1, (int)std::ceil(Z_RANGE_MM)); // 宽 = Z_Range_MM + const int img_w = std::max(1, (int)std::ceil(cfg.detect.z_range_mm)); // 宽 = Z_Range_MM const double img_scale = 1.0; // 1px = 1mm int img_h = std::max(1, (int)std::ceil(sx_range)); if (out_w) *out_w = img_w; @@ -193,12 +194,16 @@ void detect_analyze_slices(const std::vector& points, log_msg("[文件] " + out_pcd + " (" + std::to_string(slice_xs.size()) + " 点)\n"); // X-Z平面映射:矩形渲染(横轴=Z 左近→右远, 纵轴=X 越大越靠上) + // 速度来源: configuration(变速)模式用剖面平均速度近似; 否则匀速 cfg.scan.z_speed; 时间模式默认10mm std::vector slice_img(img_w * img_h, 0); double rect_len_mm = 10.0; // 时间模式(无速度)默认10mm - if (Speed_Z > 0) - rect_len_mm = Speed_Z * 10.0 / Lidar_Hz * Rect_Length_Ratio; + double prof_v = speed_profile_avg_velocity_mm_s(); + if (cfg.scan.z_speed_type == "configuration" && prof_v > 0) + rect_len_mm = prof_v / cfg.radar.hz * cfg.detect.rect_length_ratio; + else if (cfg.scan.z_speed > 0) + rect_len_mm = cfg.scan.z_speed * 10.0 / cfg.radar.hz * cfg.detect.rect_length_ratio; int len_px = std::max(1, (int)(rect_len_mm / img_scale)); - int half_w = Rect_Width_PX / 2; + int half_w = cfg.detect.rect_width_px / 2; if (half_w < 1) half_w = 1; double z_ref = z0; @@ -274,7 +279,7 @@ void detect_analyze_slices(const std::vector& points, } else { - log_msg("[截面-终点] Z总长不足" + std::to_string(Z_RANGE_MM) + "mm,跳过终点截面。\n"); + log_msg("[截面-终点] Z总长不足" + std::to_string(cfg.detect.z_range_mm) + "mm,跳过终点截面。\n"); } // ====== 合并对比图:start左半区, end右半区 ====== @@ -314,7 +319,7 @@ void detect_analyze_slices(const std::vector& points, sum << "输入点数: " << pts.size() << "\n"; sum << "Y范围(全部点): " << y_min << " ~ " << y_max << " mm\n"; sum << "Y截面参与点数: " << y_filtered.size() << "\n"; - sum << "Z截面区间长度: " << Z_RANGE_MM << " mm\n"; + sum << "Z截面区间长度: " << cfg.detect.z_range_mm << " mm\n"; sum << "全局X范围: " << gx_min << " ~ " << gx_max << " mm\n"; sum << "全局Z范围: " << gz_min << " ~ " << gz_max << " " << z_unit << "\n"; if (start_w > 0) diff --git a/detect.h b/detect.h index bf3cdec..e951303 100644 --- a/detect.h +++ b/detect.h @@ -11,7 +11,7 @@ // 2) Y轴偏移(SHIFT_Y_POSITIVE)后保存 lidar_data_shifted.pcd // 3) 起点(Z开头Z_Range_MM区间)与终点(结束前Z_Range_MM区间)两个 X-Z 截面 // → 保存切片PCD + 二值图JPG + 合并对比图 lidar_slice_combined.jpg -// 4) 写 log.txt(Log_Flag 开启时) +// 4) 写 log.txt(cfg.log.enable 开启时) // points 会被复制处理(内部可能做 Y 偏移),不修改调用方数据。 // out_dir:已创建或待创建的任务输出目录(如 data/20260902/143000_task1)。 // result:可选,回传各截面白色占比/Y统计(供 TCP 返回)。 diff --git a/filter.cpp b/filter.cpp index dc63863..1cdb52d 100644 --- a/filter.cpp +++ b/filter.cpp @@ -29,5 +29,5 @@ bool point_in_polygon(double px, double py, bool xy_filter_allowed(double x_mm, double y_mm) { if (is_origin_point(x_mm, y_mm)) return false; // 原点噪点丢弃 if (!FILTER_ENABLE) return true; // 未启用多边形过滤则全保留 - return point_in_polygon(x_mm, y_mm, Filter_Polygon, Filter_Poly_N); + return point_in_polygon(x_mm, y_mm, cfg.filter.polygon, cfg.filter.poly_n); } diff --git a/filter.h b/filter.h index 7509dfe..24a8424 100644 --- a/filter.h +++ b/filter.h @@ -13,7 +13,7 @@ bool is_origin_point(double x_mm, double y_mm); bool point_in_polygon(double px, double py, const std::vector>& poly, int n); -// 综合过滤: 通过(FILTER_ENABLE && Filter_Polygon)判定该 XY 点是否保留 +// 综合过滤: 通过(FILTER_ENABLE && cfg.filter.poly_n)判定该 XY 点是否保留 // 返回 true=保留, false=丢弃(原点噪点或超出多边形) bool xy_filter_allowed(double x_mm, double y_mm); diff --git a/io_utils.cpp b/io_utils.cpp index 8b69f2c..3b45af3 100644 --- a/io_utils.cpp +++ b/io_utils.cpp @@ -94,39 +94,84 @@ std::string make_task_dir(const std::string& task_id) return task_dir; } -// 仅 Windows 用简化目录遍历;其他平台留空实现(本程序运行于 Windows) +// Windows 递归删除目录(含只读文件),直接用 API,不弹 cmd 黑窗 +#ifdef _WIN32 +static bool remove_dir_recursive(const std::wstring& dir) +{ + std::wstring pattern = dir + L"\\*"; + WIN32_FIND_DATAW fd; + HANDLE h = FindFirstFileW(pattern.c_str(), &fd); + if (h == INVALID_HANDLE_VALUE) + return RemoveDirectoryW(dir.c_str()) != 0; + + do { + std::wstring name = fd.cFileName; + if (name == L"." || name == L"..") continue; + std::wstring full = dir + L"\\" + name; + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + remove_dir_recursive(full); // 子目录删不掉不致命,继续删其它 + } else { + DWORD attrs = GetFileAttributesW(full.c_str()); + if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY)) + SetFileAttributesW(full.c_str(), attrs & ~FILE_ATTRIBUTE_READONLY); + DeleteFileW(full.c_str()); + } + } while (FindNextFileW(h, &fd)); + FindClose(h); + return RemoveDirectoryW(dir.c_str()) != 0; +} +#endif + +// 清理 data/日期 下早于 cfg.log.retain_days 天的过期日期目录(保留最近 N 天) +// 在程序启动与每日 01:00 调用。目录名 YYYYMMDD 为纯数字,字典序即日期序。 void clean_expired_log_dirs() { - if (Log_Retain_Days <= 0) return; + std::cout << "[清理] 定时检查过期日志目录 (保留最近 " << cfg.log.retain_days << " 天)" << std::endl; + if (cfg.log.retain_days <= 0) { std::cout << "[清理] 已禁用(cfg.log.retain_days<=0), 跳过" << std::endl; return; } #ifdef _WIN32 - std::string base = "data"; - WIN32_FIND_DATAA fd; - HANDLE h = FindFirstFileA((base + "/*").c_str(), &fd); - if (h == INVALID_HANDLE_VALUE) return; + // 截止日期 = 今天 - cfg.log.retain_days 天(用文件时间做减法,避免 localtime/mktime + // 修改共享 tm 缓冲区,与 make_task_dir 等其它线程并发调用时产生数据竞争) + SYSTEMTIME st; + GetLocalTime(&st); + FILETIME ft; + SystemTimeToFileTime(&st, &ft); + ULONGLONG t = ((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime; + t -= (ULONGLONG)cfg.log.retain_days * 86400ULL * 10000000ULL; // 100ns 单位 + ft.dwHighDateTime = (DWORD)(t >> 32); + ft.dwLowDateTime = (DWORD)t; + FileTimeToSystemTime(&ft, &st); + + char cutoff[16]; + std::snprintf(cutoff, sizeof(cutoff), "%04u%02u%02u", st.wYear, st.wMonth, st.wDay); + + const std::string base = "data"; + const std::wstring wbase(base.begin(), base.end()); + WIN32_FIND_DATAW fd; + HANDLE h = FindFirstFileW((wbase + L"\\*").c_str(), &fd); + if (h == INVALID_HANDLE_VALUE) { std::cout << "[清理] data 目录不存在, 跳过" << std::endl; return; } + int removed = 0; do { if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue; - std::string name = fd.cFileName; - if (name.size() != 8) continue; // 仅处理 YYYYMMDD + std::wstring wname = fd.cFileName; + if (wname.size() != 8) continue; // 仅处理 YYYYMMDD bool is_num = true; - for (char c : name) if (c < '0' || c > '9') { is_num = false; break; } + for (wchar_t c : wname) + if (c < L'0' || c > L'9') { is_num = false; break; } if (!is_num) continue; - time_t now = std::time(nullptr); - struct tm* t = std::localtime(&now); - t->tm_year = std::atoi(name.substr(0, 4).c_str()) - 1900; - t->tm_mon = std::atoi(name.substr(4, 2).c_str()) - 1; - t->tm_mday = std::atoi(name.substr(6, 2).c_str()); - time_t dir_time = std::mktime(t); - double days = std::difftime(now, dir_time) / 86400.0; - if (days > Log_Retain_Days) { - std::string full = base + "/" + name; - // 删除目录(含子文件,Windows 下逐级删除) - std::string del_cmd = "rmdir /s /q \"" + full + "\""; - system(del_cmd.c_str()); - std::cout << "[清理] 已删除过期日志目录: " << full << std::endl; + std::string name(wname.begin(), wname.end()); // 纯数字 ASCII,无损转换 + if (name < cutoff) { // 早于截止日期才删 + std::wstring full = wbase + L"\\" + wname; + if (remove_dir_recursive(full)) { + std::cout << "[清理] 已删除过期日志目录: data/" << name << std::endl; + ++removed; + } + else + std::cout << "[清理] 删除失败(可能正被占用): data/" << name << std::endl; } - } while (FindNextFileA(h, &fd)); + } while (FindNextFileW(h, &fd)); FindClose(h); + std::cout << "[清理] 完成: 本次共删除 " << removed << " 个过期目录" << std::endl; #endif } diff --git a/io_utils.h b/io_utils.h index d6c137c..c020ecd 100644 --- a/io_utils.h +++ b/io_utils.h @@ -19,8 +19,8 @@ bool make_dirs(const std::string& path); // taskId 为空时返回 data/日期/时间_ (调用方需自行创建 data 根目录) std::string make_task_dir(const std::string& task_id); -// 按配置 Log_Retain_Days 清理 data/日期 下的过期日期目录(保留最近 N 天) -// 在程序启动与每日 01:00 调用 +// 按配置 cfg.log.retain_days 清理 data/日期 下的过期日期目录(保留最近 N 天) +// 在程序启动与每日 log.clean_time 时刻调用 void clean_expired_log_dirs(); // 保存 ASCII PCD 点云(一次性构建字符串写入,避免逐行IO) diff --git a/json_utils.cpp b/json_utils.cpp index e8b9ee6..e57f7b7 100644 --- a/json_utils.cpp +++ b/json_utils.cpp @@ -146,3 +146,75 @@ int json_get_point_array(const std::string& text, const std::string& key, } return cnt; } + +// ---- 分组两级查找实现 ---- +std::string json_get_raw(const std::string& text, const std::string& group, const std::string& key) +{ + std::string obj = group.empty() ? text : json_get_raw(text, group); + if (obj.empty()) return ""; + return json_get_raw(obj, key); +} + +bool json_get_string(const std::string& text, const std::string& group, const std::string& key, std::string& out) +{ + std::string raw = json_get_raw(text, group, key); + if (raw.empty()) return false; + out = unquote(raw); + return true; +} + +bool json_get_number(const std::string& text, const std::string& group, const std::string& key, double& out) +{ + std::string raw = json_get_raw(text, group, key); + if (raw.empty()) return false; + std::string t = json_trim(raw); + if (t.empty()) return false; + try { out = std::stod(t); return true; } + catch (...) { return false; } +} + +bool json_get_bool(const std::string& text, const std::string& group, const std::string& key, bool& out) +{ + std::string raw = json_get_raw(text, group, key); + if (raw.empty()) return false; + std::string t = json_trim(raw); + if (t == "true") { out = true; return true; } + if (t == "false") { out = false; return true; } + double d; + if (json_get_number(text, group, key, d)) { out = (d != 0); return true; } + return false; +} + +int json_get_point_array(const std::string& text, const std::string& group, const std::string& key, + std::vector>& out) +{ + std::string raw = json_get_raw(text, group, key); + if (raw.empty()) return 0; + std::string t = json_trim(raw); + if (t.empty() || t[0] != '[') return 0; + // 逐个匹配 "[数,数]" 子元素 + size_t i = 1; + int cnt = 0; + while (i < t.size()) + { + size_t l = t.find('[', i); + if (l == std::string::npos) break; + size_t r = t.find(']', l); + if (r == std::string::npos) break; + std::string ele = t.substr(l + 1, r - l - 1); + size_t comma = ele.find(','); + if (comma != std::string::npos) + { + try + { + double a = std::stod(json_trim(ele.substr(0, comma))); + double b = std::stod(json_trim(ele.substr(comma + 1))); + out.push_back(std::make_pair(a, b)); + ++cnt; + } + catch (...) {} + } + i = r + 1; + } + return cnt; +} diff --git a/json_utils.h b/json_utils.h index cab873e..d447406 100644 --- a/json_utils.h +++ b/json_utils.h @@ -19,9 +19,19 @@ bool json_get_string(const std::string& text, const std::string& key, std::strin bool json_get_number(const std::string& text, const std::string& key, double& out); bool json_get_bool(const std::string& text, const std::string& key, bool& out); -// 解析多边形/二维点数组: "Filter_Polygon":[[-650,0],[-650,670],[650,670],[650,0]] +// 解析多边形/二维点数组: "polygon":[[-650,0],[-650,670],[650,670],[650,0]] // 追加到 out,成功返回解析到的点数 int json_get_point_array(const std::string& text, const std::string& key, std::vector>& out); +// ---- 分组两级查找:先取 "group": {...} 对象,再在该对象内按 key 取 value ---- +// 用于嵌套分组配置(如 {"radar": {"ip": ...}} 中取 radar 组内的 ip)。 +// group 传空串时等价于不带 group 的版本。 +std::string json_get_raw(const std::string& text, const std::string& group, const std::string& key); +bool json_get_string(const std::string& text, const std::string& group, const std::string& key, std::string& out); +bool json_get_number(const std::string& text, const std::string& group, const std::string& key, double& out); +bool json_get_bool(const std::string& text, const std::string& group, const std::string& key, bool& out); +int json_get_point_array(const std::string& text, const std::string& group, const std::string& key, + std::vector>& out); + #endif // JSON_UTILS_H diff --git a/lidar_utils.cpp b/lidar_utils.cpp index 3225fce..2a6dc65 100644 --- a/lidar_utils.cpp +++ b/lidar_utils.cpp @@ -2,17 +2,21 @@ #include "lidar_utils.h" #include "config.h" #include "filter.h" +#include #include +#include +#include #include +#include // ==================== 雷达连接 ==================== bool connect_lidar(pavo2::pavo2_driver& driver) { driver.set_conn_protocol(pavo2::PAVO2_PROTO_TYPE::TCP); - std::cout << "[连接] 正在连接雷达 " << Leida_IP << ":" << Leida_PORT << " ..." << std::endl; + std::cout << "[连接] 正在连接雷达 " << cfg.radar.ip << ":" << cfg.radar.port << " ..." << std::endl; - if (!driver.pavo2_open(Leida_IP, Leida_PORT, OPEN_TIMEOUT_MS)) + if (!driver.pavo2_open(cfg.radar.ip, cfg.radar.port, OPEN_TIMEOUT_MS)) { std::cerr << "[错误] 连接雷达失败!请检查IP和网络连接。" << std::endl; return false; @@ -20,10 +24,10 @@ bool connect_lidar(pavo2::pavo2_driver& driver) std::cout << "[连接] 雷达连接成功!" << std::endl; - driver.enable_tail_filter(Tuo_Wei); - driver.enable_front_filter(Qian_Chong); - driver.enable_far_hr_filter(Yuan_Duan); - driver.enable_near_hr(Jin_Duan); + driver.enable_tail_filter(cfg.radar.tail_filter); + driver.enable_front_filter(cfg.radar.front_filter); + driver.enable_far_hr_filter(cfg.radar.far_hr_filter); + driver.enable_near_hr(cfg.radar.near_hr_filter); std::string sn, pn, sdk_ver, fw_ver; if (driver.get_device_sn(sn)) @@ -63,13 +67,233 @@ int drain_stale_scans(pavo2::pavo2_driver& driver, int max_ms) return flushed; } +// ==================== 变速速度剖面 (speed.txt) ==================== +// 全局剖面: (时间ms, 距离mm),时间严格递增。 +namespace +{ + std::mutex g_prof_mtx; + std::vector> g_profile; + + // 时间→z 插值(调用方需已持有锁)。 + // 命中返回 true;表不足两行返回 false(由调用方回退匀速)。 + bool profile_lookup_locked(double t_ms, double* out) + { + size_t n = g_profile.size(); + if (n < 2) return false; + // 表首之前:按前两点斜率回推(通常 t0=0,不会进入) + if (t_ms <= g_profile[0].first) + { + double dt = g_profile[1].first - g_profile[0].first; + double dz = g_profile[1].second - g_profile[0].second; + double slope = (dt > 0) ? (dz / dt) : 0.0; + *out = g_profile[0].second + slope * (t_ms - g_profile[0].first); + return true; + } + // 表末之后(需求5):按最后两条的平均速度继续延伸 + if (t_ms >= g_profile[n - 1].first) + { + double dt = g_profile[n - 1].first - g_profile[n - 2].first; + double dz = g_profile[n - 1].second - g_profile[n - 2].second; + double slope = (dt > 0) ? (dz / dt) : 0.0; + *out = g_profile[n - 1].second + slope * (t_ms - g_profile[n - 1].first); + return true; + } + // 线性扫区间插值(表一般几百行,足够快) + for (size_t i = 0; i + 1 < n; i++) + { + if (t_ms <= g_profile[i + 1].first) + { + double a = g_profile[i].first, b = g_profile[i + 1].first; + double f = (b > a) ? ((t_ms - a) / (b - a)) : 0.0; + *out = g_profile[i].second + + (g_profile[i + 1].second - g_profile[i].second) * f; + return true; + } + } + return false; + } +} + +bool load_speed_profile(const std::string& path) +{ + std::vector> rows; + std::ifstream fin(path); + if (fin.is_open()) + { + std::string line; + while (std::getline(fin, line)) + { + size_t b = line.find_first_not_of(" \t\r\n"); + if (b == std::string::npos) continue; // 空行 + if (line[b] == '#' || line[b] == ';') continue; // 注释 + std::istringstream iss(line.substr(b)); + double t = 0.0, v = 0.0; + if (!(iss >> t >> v)) continue; + if (!rows.empty() && t <= rows.back().first) + { + std::cout << "[speed] 警告: speed.txt 第" << (rows.size() + 1) + << "行时间未递增(" << t << "),已跳过" << std::endl; + continue; + } + rows.push_back(std::make_pair(t, v)); + } + fin.close(); + } + else + { + std::cout << "[speed] 未找到 " << path << ",使用匀速 z_speed=" + << cfg.scan.z_speed << "cm/s" << std::endl; + } + + std::lock_guard lk(g_prof_mtx); + if (rows.size() < 2) + { + g_profile.clear(); + return false; + } + g_profile.swap(rows); + return true; +} + +void reload_speed_profile() +{ + if (cfg.scan.z_speed_type == "configuration") + { + if (load_speed_profile()) + { + std::lock_guard lk(g_prof_mtx); + std::cout << "[speed] 变速剖面已加载(" << g_profile.size() << "行): " + << "首点(t=" << g_profile.front().first << "ms, z=" + << g_profile.front().second << "mm), 末点(t=" + << g_profile.back().first << "ms, z=" + << g_profile.back().second << "mm); 超出末点时间按最后两点平均速度延伸" + << std::endl; + } + else + { + std::cout << "[speed] 剖面无效,回退匀速 z_speed=" << cfg.scan.z_speed + << "cm/s(0=时间)" << std::endl; + } + } + else + { + std::lock_guard lk(g_prof_mtx); + g_profile.clear(); + } +} + +bool speed_profile_loaded() +{ + std::lock_guard lk(g_prof_mtx); + return g_profile.size() >= 2; +} + +double speed_profile_avg_velocity_mm_s() +{ + std::lock_guard lk(g_prof_mtx); + if (g_profile.size() < 2) return 0.0; + double dt_ms = g_profile.back().first - g_profile.front().first; + double dz = g_profile.back().second - g_profile.front().second; + if (dt_ms <= 0.0) return 0.0; + double v = dz / (dt_ms / 1000.0); + return (v >= 0) ? v : -v; +} + +bool z_is_distance_mode() +{ + if (cfg.scan.z_speed_type == "configuration") + return speed_profile_loaded() || cfg.scan.z_speed > 0.0; + return cfg.scan.z_speed > 0.0; +} + double z_for_elapsed(double elapsed_ms) { - return (Speed_Z > 0) - ? (Speed_Z * 10.0 * elapsed_ms / 1000.0) // cm/s ×10 = mm/s, ×秒 = mm + if (cfg.scan.z_speed_type == "configuration") + { + double z = 0.0; + { + std::lock_guard lk(g_prof_mtx); + if (profile_lookup_locked(elapsed_ms, &z)) return z; + } + // 表无效/不足两行 → 回退匀速 cfg.scan.z_speed(无 speed.txt 时的兜底) + } + return (cfg.scan.z_speed > 0) + ? (cfg.scan.z_speed * 10.0 * elapsed_ms / 1000.0) // cm/s ×10 = mm/s, ×秒 = mm : (double)elapsed_ms; } +// ConfigureSpeed 标定采集:雷达横放时物体沿 Y 移动,逐帧取(过滤后)Y最大值 +// 即该时刻被测物在行进方向上的距离,生成 speed.txt 供变速识别使用。 +int collect_speed_profile(pavo2::pavo2_driver& driver, double seconds, + std::string& err) +{ + const std::string path = "./config/speed.txt"; + int flushed = drain_stale_scans(driver, 500); + if (flushed > 0) + std::cout << "[ConfigureSpeed] 已丢弃积压扫描包 " << flushed << " 个" << std::endl; + + auto t0 = std::chrono::steady_clock::now(); + const double target_ms = seconds * 1000.0; + std::vector> rows; // (elapsed_ms, y_max_mm) + rows.reserve((size_t)(seconds * 200.0) + 64); + + while (true) + { + pavo2::pavo_packet_info_scan_t scan_info; + bool got = driver.get_scanned_data(scan_info, 100); // 100ms轮询 + double elapsed_ms = (double)std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + if (got) + { + double max_y = -1e18; + for (const auto& s : scan_info.vec_buff) + { + double angle_rad = (s.angle / 100.0) * 3.14159265 / 180.0; + double x_mm = s.distance * sin(angle_rad) * 2.0; + double y_mm = -s.distance * cos(angle_rad) * 2.0; + if (!xy_filter_allowed(x_mm, y_mm)) continue; + if (y_mm > max_y) max_y = y_mm; + } + if (max_y > -1e17) + rows.push_back(std::make_pair(elapsed_ms, max_y)); + } + if (elapsed_ms >= target_ms) break; + } + + if (rows.size() < 2) + { + err = "采集样本过少(未收到有效雷达帧)"; + return -1; + } + + // 平滑:抑制与总体趋势反向且超过5mm的抖动,保持时间→z 单调可用 + double trend = (rows.back().second >= rows.front().second) ? 1.0 : -1.0; + const double tol = 5.0; + for (size_t i = 1; i < rows.size(); i++) + if ((rows[i].second - rows[i - 1].second) * trend < -tol) + rows[i].second = rows[i - 1].second; + + // 写文件 + std::ofstream f(path); + if (!f.is_open()) + { + err = "无法写入 " + path; + return -1; + } + f << "# speed.txt - 变速运动z轴速度剖面(每行: 时间ms 距离mm; 可由ConfigureSpeed生成或手工编辑)\n"; + f << "# 首行后请勿保留其他说明; 时间须递增。cfg.scan.z_speed_type=configuration 时生效,\n"; + f << "# 识别时间超出表末按最后两行平均速度延伸。\n"; + f << std::fixed << std::setprecision(3); + for (const auto& r : rows) + f << (long long)std::lround(r.first) << " " << r.second << "\n"; + f.close(); + + // 载入为新剖面(无论当前类型是否 configuration,先保存供切换后立即使用) + load_speed_profile(path); + err.clear(); + return (int)rows.size(); +} + size_t scan_convert_points(const pavo2::pavo_packet_info_scan_t& scan_info, double z_val, std::vector& out) { diff --git a/lidar_utils.h b/lidar_utils.h index 66ca63a..261cd92 100644 --- a/lidar_utils.h +++ b/lidar_utils.h @@ -3,6 +3,7 @@ #define LIDAR_UTILS_H #include +#include #include #include #include @@ -20,10 +21,42 @@ bool connect_lidar(pavo2::pavo2_driver& driver); // 返回丢弃包数。 int drain_stale_scans(pavo2::pavo2_driver& driver, int max_ms = 500); -// z轴值:Speed_Z>0 时为移动距离(mm)=Speed_Z(cm/s)*10*秒; -// Speed_Z=0 时 z 直接取 elapsed_ms(时间模式)。 +// z轴值换算(时间→z): +// - cfg.scan.z_speed_type == "configuration" 且 speed.txt 有效:按速度剖面(时间→距离)变速插值, +// 超过表末尾时间按最后两条的平均速度外推延伸; +// - 否则按原逻辑:cfg.scan.z_speed>0 时为移动距离(mm)=cfg.scan.z_speed(cm/s)*10*秒; +// cfg.scan.z_speed=0 时 z 直接取 elapsed_ms(时间模式)。 double z_for_elapsed(double elapsed_ms); +// ==================== 变速速度剖面 (speed.txt) ==================== +// speed.txt 每行两个数字: "时间(ms) 距离(mm)"(# 开头为注释,空行忽略),时间须递增。 +// 当 cfg.scan.z_speed_type == "configuration" 时程序使用该表把采集时间换算为 z(行进距离, mm), +// 从而实现变速运动识别;未找到有效表时自动回退使用 cfg.scan.z_speed 匀速。 +// 默认路径 ./config/speed.txt(与 config/config 同目录)。 +// 加载剖面(成功后内部替换当前剖面)。返回是否有效(≥2行)。 +bool load_speed_profile(const std::string& path = "./config/speed.txt"); + +// 按当前 cfg.scan.z_speed_type 重载:configuration → 重新读 speed.txt; +// uniformSpeed → 清空剖面(不使用表)。配置重载(load_config)后应调用一次。 +void reload_speed_profile(); + +// 当前剖面是否有效(≥2行,配置为 configuration 时 z 按表换算)。 +bool speed_profile_loaded(); + +// 剖面平均速度(mm/s);无效或非 distance 数据时返回 0。 +double speed_profile_avg_velocity_mm_s(); + +// 当前 z 轴是否为距离(mm):configuration 且剖面有效=true; +// uniformSpeed 时为 cfg.scan.z_speed>0。 +bool z_is_distance_mode(); + +// ConfigureSpeed 标定采集:采集 seconds 秒,逐帧记录(时间, 过滤后Y最大值), +// 平滑(抑制与总体趋势反向>5mm的抖动)后写入 ./config/speed.txt。 +// 返回写入行数;失败返回 <0 并在 err 中给出原因。 +// 注意:调用前应确保没有其他线程同时 get_scanned_data(与 ScanSession 互斥使用)。 +int collect_speed_profile(pavo2::pavo2_driver& driver, double seconds, + std::string& err); + // 将一帧扫描包 vec_buff 中的点做 XY 过滤(原点噪点 + 过滤多边形)后追加到 out。 // z 由调用方给出(通常为 z_for_elapsed(elapsed_ms))。 // 返回新增点数。 diff --git a/main.cpp b/main.cpp index 80d4d81..ebdeb64 100644 --- a/main.cpp +++ b/main.cpp @@ -1,8 +1,11 @@ // main.cpp - 程序入口(模块化重构版) // 模块: config / json_utils / filter / io_utils / lidar_utils / count / detect / tcp_server +#include #include +#include #include #include +#include #include #include @@ -17,34 +20,65 @@ #include "detect.h" #include "tcp_server.h" -// ==================== 日志目录清理:启动时 + 每天 01:00 ==================== +// ==================== 日志目录清理:启动时 + 每天 log.clean_time ==================== +static std::atomic g_cleanup_stop{ false }; +static std::mutex g_cleanup_mtx; +static std::condition_variable g_cleanup_cv; +static std::thread g_cleanup_thread; + +// 启动清理线程:等下一个 01:00 执行一次清理后循环;等待可被 stop_daily_cleanup() 中断 static void start_daily_cleanup() { clean_expired_log_dirs(); // 启动清理一次 - std::thread([]() { + g_cleanup_stop = false; + g_cleanup_thread = std::thread([]() { for (;;) { + // 清理时刻来自配置(cfg.log.clean_hour/minute/sec),每轮读取:运行期发 config + // 命令重载后,下一轮即按新时刻执行 + int hour = cfg.log.clean_hour, minute = cfg.log.clean_minute, sec = cfg.log.clean_sec; time_t raw = std::time(nullptr); struct tm* t = std::localtime(&raw); std::tm next = *t; - next.tm_hour = 1; - next.tm_min = 0; - next.tm_sec = 0; + next.tm_hour = hour; + next.tm_min = minute; + next.tm_sec = sec; time_t next_t = std::mktime(&next); - if (next_t <= raw) next_t += 86400; // 已过今日 01:00 → 明日 - std::this_thread::sleep_for(std::chrono::seconds(next_t - raw)); + if (next_t <= raw) next_t += 86400; + + char buf[32]; + std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour, minute, sec); + std::cout << "[清理] 下次定时清理: 每天 " << buf << " (等待 " + << (next_t - raw) / 60 << " 分钟)" << std::endl; + + // 可中断等待:wait_for 返回 true 表示 stop 标志已置位(程序退出) + std::unique_lock lk(g_cleanup_mtx); + if (g_cleanup_cv.wait_for(lk, std::chrono::seconds(next_t - raw), + [] { return g_cleanup_stop.load(); })) + return; // 程序正在退出 + lk.unlock(); + clean_expired_log_dirs(); } - }).detach(); + }); +} + +// 主程序退出前调用:唤醒并等待清理线程结束, +// 避免进程析构时后台线程仍在访问全局对象/标准输出导致崩溃(原 detach 版的崩退根因) +static void stop_daily_cleanup() +{ + g_cleanup_stop = true; + g_cleanup_cv.notify_all(); + if (g_cleanup_thread.joinable()) g_cleanup_thread.join(); } // ==================== exe 模式:自动做一次截面检测 ==================== -// 启动统一后台扫描,采 Collect_Duration_S 秒后自动停止,做截面检测并保存到 +// 启动统一后台扫描,采 cfg.scan.collect_duration_s 秒后自动停止,做截面检测并保存到 // data/日期/时间_ 目录(无 task_id)。 static int run_exe_check(pavo2::pavo2_driver& driver) { - std::cout << "\n[exe] 自动检测开始: 采集 " << Collect_Duration_S + std::cout << "\n[exe] 自动检测开始: 采集 " << cfg.scan.collect_duration_s << " 秒 ..." << std::endl; ScanSession scan; @@ -54,7 +88,7 @@ static int run_exe_check(pavo2::pavo2_driver& driver) std::cerr << "[错误] 启动扫描会话失败!" << std::endl; return -1; } - std::this_thread::sleep_for(std::chrono::seconds(Collect_Duration_S)); + std::this_thread::sleep_for(std::chrono::seconds(cfg.scan.collect_duration_s)); scan.stop(); std::vector pts = scan.snapshot(); scan.reset(); @@ -91,19 +125,23 @@ int main() #endif load_config(); // 默认读取 ./config/config (JSON) + reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时加载 config/speed.txt start_daily_cleanup(); std::cout << "==================== 配置文件内容 ====================" << std::endl; - std::cout << "雷达IP: " << Leida_IP << " : " << Leida_PORT << std::endl; - std::cout << "运行模式: " << Run_Mode << " (tcp=服务端, 其他=自动检测)" << std::endl; - std::cout << "TCP端口: " << Tcp_Port << std::endl; - std::cout << "采集时长: " << Collect_Duration_S << " 秒" << std::endl; - std::cout << "移动速度: " << Speed_Z << " cm/s (0=时间模式, >0=距离模式)" + std::cout << "雷达IP: " << cfg.radar.ip << " : " << cfg.radar.port << std::endl; + std::cout << "运行模式: " << cfg.server.mode + << " (tcp=TCP服务端, http=HTTP服务端, 其他=自动检测)" << std::endl; + std::cout << "TCP/HTTP端口: " << cfg.server.port << std::endl; + std::cout << "采集时长: " << cfg.scan.collect_duration_s << " 秒" << std::endl; + std::cout << "Z轴模式: " << cfg.scan.z_speed_type + << " (uniformSpeed=匀速, configuration=config/speed.txt变速)" << std::endl; + std::cout << "移动速度: " << cfg.scan.z_speed << " cm/s (0=时间模式, >0=距离模式)" << std::endl; - std::cout << "Z范围: " << Z_RANGE_MM << " mm | Y范围: " << Y_RANGE_MM - << " mm | 雷达频率: " << Lidar_Hz << " Hz" << std::endl; - std::cout << "z空隙插值模式: " << Z_Interpolate_Mode << std::endl; - std::cout << "日志保留天数: " << Log_Retain_Days << std::endl; + std::cout << "Z范围: " << cfg.detect.z_range_mm << " mm | Y范围: " << cfg.detect.y_range_mm + << " mm | 雷达频率: " << cfg.radar.hz << " Hz" << std::endl; + std::cout << "z空隙插值模式: " << cfg.count.z_interpolate_mode << std::endl; + std::cout << "日志保留天数: " << cfg.log.retain_days << std::endl; std::cout << "======================================================" << std::endl; std::cout << "======================================================" << std::endl; std::cout << " Pavo2 Lidar 雷达数据采集程序 (模块化)" << std::endl; @@ -112,6 +150,7 @@ int main() pavo2::pavo2_driver driver; if (!connect_lidar(driver)) { + stop_daily_cleanup(); // 提前退出也要先停掉后台清理线程 #ifdef _WIN32 WSACleanup(); #endif @@ -122,11 +161,15 @@ int main() std::this_thread::sleep_for(std::chrono::seconds(2)); int rc = 0; - if (Run_Mode == "tcp") + if (cfg.server.mode == "tcp") run_tcp_server(driver); + else if (cfg.server.mode == "http") + run_http_server(driver); else rc = run_exe_check(driver); + stop_daily_cleanup(); // 通知后台清理线程退出并等待结束,防止崩退 + std::cout << "\n[关闭] 正在断开雷达连接..." << std::endl; driver.pavo2_close(); #ifdef _WIN32 diff --git a/main.exe b/main.exe index 5ff5157..e13ae5a 100644 Binary files a/main.exe and b/main.exe differ diff --git a/process_folders.py b/process_folders.py new file mode 100644 index 0000000..f007c2b --- /dev/null +++ b/process_folders.py @@ -0,0 +1,183 @@ +import os +import shutil +import random + + +def get_all_dirs_with_files(root_dir): + """递归获取 root_dir 下所有包含文件的目录""" + result = [] + for dirpath, dirnames, filenames in os.walk(root_dir): + if filenames: + result.append(dirpath) + return result + + +def process_directory(dir_path, seq_num, all_pairs): + """处理单个目录:修改文本文件第一列为序列号,BMP 复制对应的 txt""" + all_files = os.listdir(dir_path) + + bmp_files = [f for f in all_files if f.lower().endswith('.bmp')] + txt_files = [f for f in all_files if f.lower().endswith('.txt')] + other_files = [f for f in all_files + if not f.lower().endswith('.bmp') and not f.lower().endswith('.txt')] + + text_candidates = txt_files + other_files + + if not text_candidates: + print(f" [{os.path.basename(dir_path)}] 未找到文本文件,跳过") + return + + text_filename = text_candidates[0] + text_path = os.path.join(dir_path, text_filename) + + if not os.path.isfile(text_path): + print(f" [{os.path.basename(dir_path)}] 跳过非文件: {text_filename}") + return + + short_name = os.path.basename(dir_path) + print(f" 处理: {short_name} (seq={seq_num}), 文本: {text_filename}") + + # 读取文件 + try: + with open(text_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except (UnicodeDecodeError, PermissionError, OSError): + try: + with open(text_path, 'r', encoding='gbk') as f: + lines = f.readlines() + except Exception as e: + print(f" 读取失败: {e}") + return + + modified_lines = [] + for line in lines: + line = line.rstrip('\n').rstrip('\r') + if not line.strip(): + modified_lines.append(line) + continue + parts = line.split() + if parts: + parts[0] = str(seq_num) + modified_lines.append(' '.join(parts)) + + # 写回 + with open(text_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(modified_lines)) + print(f" 已修改 {len([l for l in modified_lines if l.strip()])} 行") + + # 为每个 BMP 复制/关联文档 + if bmp_files: + for bmp_filename in bmp_files: + bmp_basename = os.path.splitext(bmp_filename)[0] + copy_filename = f"{bmp_basename}.txt" + copy_path = os.path.join(dir_path, copy_filename) + + if os.path.normpath(text_path) == os.path.normpath(copy_path): + all_pairs.append((os.path.join(dir_path, bmp_filename), copy_path)) + else: + shutil.copy2(text_path, copy_path) + all_pairs.append((os.path.join(dir_path, bmp_filename), copy_path)) + print(f" BMP 数量: {len(bmp_files)}") + else: + print(f" 无 BMP 图片") + + +def main(): + base_dir = r"D:\xwechat_files\wxid_ttkf0xgmyihv22_2388\msg\file\2026-07\抓图(1)\抓图\新建文件夹\1\1\新建文件夹" + + output_dir = os.path.join(os.path.dirname(base_dir.rstrip('\\/')), "yolo_output") + train_img_dir = os.path.join(output_dir, "train", "images") + train_lbl_dir = os.path.join(output_dir, "train", "labels") + val_img_dir = os.path.join(output_dir, "val", "images") + val_lbl_dir = os.path.join(output_dir, "val", "labels") + + for d in [train_img_dir, train_lbl_dir, val_img_dir, val_lbl_dir]: + os.makedirs(d, exist_ok=True) + + if not os.path.isdir(base_dir): + print(f"错误: 路径不存在: {base_dir}") + return + + top_dirs = [d for d in os.listdir(base_dir) if os.path.isdir(os.path.join(base_dir, d))] + top_dirs.sort() + + if not top_dirs: + print("没有找到子文件夹") + return + + seq_start = 20 + all_pairs = [] # (bmp_path, txt_path) + mapping = {} # 顶层文件夹名 -> 序列号 + + for idx, top_name in enumerate(top_dirs): + top_path = os.path.join(base_dir, top_name) + seq_num = seq_start + idx + mapping[top_name] = seq_num + + print(f"\n{'='*50}") + print(f"顶层文件夹: {top_name} -> 序列号: {seq_num}") + + # 递归获取该顶层文件夹下所有含文件的目录 + leaf_dirs = get_all_dirs_with_files(top_path) + + if not leaf_dirs: + print(f" 无文件,跳过") + continue + + for leaf_dir in leaf_dirs: + process_directory(leaf_dir, seq_num, all_pairs) + + # ---- 输出对应关系 ---- + print("\n" + "=" * 50) + print("文件夹与序列号对应关系:") + print("-" * 40) + for name, num in mapping.items(): + print(f" {num} : {name}") + print("=" * 50) + + # ---- YOLO 格式 train/val 划分 ---- + if not all_pairs: + print("\n没有 BMP 图片,跳过 YOLO 输出") + return + + random.seed(42) + random.shuffle(all_pairs) + + split_idx = int(len(all_pairs) * 0.8) + train_pairs = all_pairs[:split_idx] + val_pairs = all_pairs[split_idx:] + + print(f"\n总图片数: {len(all_pairs)}") + print(f"train: {len(train_pairs)} | val: {len(val_pairs)}") + + def copy_pairs(pairs, img_dir, lbl_dir): + for bmp_path, txt_path in pairs: + bmp_name = os.path.basename(bmp_path) + txt_name = os.path.basename(txt_path) + + dst_bmp = bmp_name + dst_txt = txt_name + + counter = 0 + while os.path.exists(os.path.join(img_dir, dst_bmp)): + counter += 1 + base, ext = os.path.splitext(bmp_name) + dst_bmp = f"{base}_{counter}{ext}" + dst_txt = f"{base}_{counter}.txt" + + shutil.copy2(bmp_path, os.path.join(img_dir, dst_bmp)) + shutil.copy2(txt_path, os.path.join(lbl_dir, dst_txt)) + + copy_pairs(train_pairs, train_img_dir, train_lbl_dir) + copy_pairs(val_pairs, val_img_dir, val_lbl_dir) + + print(f"\nYOLO 格式输出: {output_dir}") + print(f" train/images/ ({len(train_pairs)} 张)") + print(f" train/labels/ ({len(train_pairs)} 个)") + print(f" val/images/ ({len(val_pairs)} 张)") + print(f" val/labels/ ({len(val_pairs)} 个)") + print("\n全部完成!") + + +if __name__ == "__main__": + main() diff --git a/tcp_server.cpp b/tcp_server.cpp index e127096..121528b 100644 --- a/tcp_server.cpp +++ b/tcp_server.cpp @@ -25,7 +25,9 @@ #include #include +#include #include +#include #include #include #include @@ -65,6 +67,13 @@ namespace send(s, out.c_str(), (int)out.size(), 0); } + // 响应出口:resp==nullptr 直接发到 client(TCP 模式);否则收集到 resp(HTTP 模式)。 + static void send_reply(SOCKET client, std::string* resp, const std::string& msg) + { + if (resp) *resp = msg; + else send_text(client, msg); + } + // ==================== 停止并取回结果 ==================== // need_auto=true 表示"自动到时"触发,只允许在 auto_stop 仍武装时抢到; // 否则(手动 stop)只要 active 即可。 @@ -108,7 +117,7 @@ namespace // ==================== 结果计算与回包 ==================== static void finish_count(const SessionState& snap, const std::vector& pts, - SOCKET target) + SOCKET target, std::string* resp = nullptr) { bool valid_box = snap.box.l > 0 && snap.box.w > 0 && snap.box.h > 0 && snap.box.maxPerLayer > 0; @@ -119,6 +128,7 @@ namespace double tarea = 0.0; if (!pts.empty() && valid_box) { + sparse_density_filter(work); // 点云级稀疏拖影过滤(默认开, 配置可调) z_gap_interpolate(work); // z空隙插值:只在停止后执行一次 total = calc_item_count(work, snap.box, &detail, &top_img, &tw, &th, &tlv, &tarea); @@ -131,7 +141,7 @@ namespace if (!snap.task_id.empty()) r << ",\"task_id\":\"" << snap.task_id << "\""; r << "}"; - send_text(target, r.str()); + send_reply(target, resp, r.str()); // 后台保存 PCD/图片/日志(不阻塞响应) if (!pts.empty() && valid_box) @@ -141,7 +151,7 @@ namespace static void finish_check(const SessionState& snap, const std::vector& pts, - SOCKET target) + SOCKET target, std::string* resp = nullptr) { // 输出目录 data/日期/时间_taskId(由 io_utils 统一创建) std::string out_dir = make_task_dir(snap.task_id); @@ -166,7 +176,7 @@ namespace if (!snap.task_id.empty()) r << ",\"task_id\":\"" << snap.task_id << "\""; r << "}"; - send_text(target, r.str()); + send_reply(target, resp, r.str()); std::cout << "[TCP] check结束: task_id=" << snap.task_id << ", 点数=" << pts.size() << (res.valid ? ", 检测有效" : ", 检测无效") << std::endl; @@ -174,12 +184,12 @@ namespace static void finish_and_reply(const SessionState& snap, const std::vector& pts, - SOCKET target) + SOCKET target, std::string* resp = nullptr) { if (snap.function == "count") - finish_count(snap, pts, target); + finish_count(snap, pts, target, resp); else if (snap.function == "check") - finish_check(snap, pts, target); + finish_check(snap, pts, target, resp); } // ==================== 自动到时停止线程 ==================== @@ -205,13 +215,23 @@ namespace { std::cout << "[TCP] 扫描到时自动停止: function=" << snap.function << ", task_id=" << snap.task_id << std::endl; - finish_and_reply(snap, pts, snap.owner); + if (snap.owner != INVALID_SOCKET) + finish_and_reply(snap, pts, snap.owner); + else + { + // HTTP 模式无长连接可推送,结果只打印并留痕 + std::string out; + finish_and_reply(snap, pts, INVALID_SOCKET, &out); + std::cout << "[HTTP] scanTime到时自动停止, 无连接可推送, 结果: " + << out << std::endl; + } } } } // ==================== 命令处理 ==================== - static void handle_scan_start(const std::string& msg, SOCKET client) + static void handle_scan_start(const std::string& msg, SOCKET client, + std::string* resp = nullptr) { std::string fn; json_get_string(msg, "function", fn); @@ -259,7 +279,8 @@ namespace ns.function = fn; ns.box = box; ns.task_id = task_id; - ns.owner = client; + // TCP 用连接作 owner(自动到时结果回发);HTTP 无长连接,置无效不推送 + ns.owner = resp ? INVALID_SOCKET : client; if (scan_time > 0) { ns.auto_stop = true; @@ -281,16 +302,17 @@ namespace if (!started) { std::cerr << "[TCP] " << fail << std::endl; - send_text(client, "{\"code\":400,\"message\":\"" + fail + "\"}"); + send_reply(client, resp, "{\"code\":400,\"message\":\"" + fail + "\"}"); return; } std::cout << "[TCP] 扫描开始: function=" << fn << ", task_id=" << task_id << (scan_time > 0 ? ", 到时自动停止" : ", 等待stop") << std::endl; - send_text(client, ack.str()); + send_reply(client, resp, ack.str()); } - static void handle_scan_stop(const std::string& msg, SOCKET client) + static void handle_scan_stop(const std::string& msg, SOCKET client, + std::string* resp = nullptr) { std::string fn; json_get_string(msg, "function", fn); @@ -305,47 +327,108 @@ namespace if (fn == "count") r << ",\"count\":0"; r << "}"; - send_text(client, r.str()); + send_reply(client, resp, r.str()); return; } std::cout << "[TCP] 收到stop: function=" << snap.function << ", task_id=" << snap.task_id << std::endl; - finish_and_reply(snap, pts, client); + finish_and_reply(snap, pts, client, resp); } - static void handle_config(const std::string& msg, SOCKET client) + static void handle_config(const std::string& msg, SOCKET client, + std::string* resp = nullptr) { (void)msg; std::cout << "[TCP] 收到config命令,重新读取 config/config ..." << std::endl; load_config(); - std::cout << "[TCP] 配置已重载: 采集=" << Collect_Duration_S - << "s, 速度=" << Speed_Z << "cm/s, 插值=" << Z_Interpolate_Mode - << ", 保留=" << Log_Retain_Days << "天, 端口=" << Tcp_Port << std::endl; - send_text(client, "{\"code\":200,\"message\":\"config reloaded\"}"); + reload_speed_profile(); // cfg.scan.z_speed_type=configuration 时重新读取 speed.txt + std::cout << "[TCP] 配置已重载: 采集=" << cfg.scan.collect_duration_s + << "s, 速度模式=" << cfg.scan.z_speed_type + << ", 速度=" << cfg.scan.z_speed << "cm/s, 插值=" << cfg.count.z_interpolate_mode + << ", 保留=" << cfg.log.retain_days << "天, 端口=" << cfg.server.port << std::endl; + std::cout << "[TCP] count容差: 基准=" << cfg.count.base_tolerance + << "mm + 每层" << cfg.count.per_level_tolerance << "mm" + << ", 地板高=" << cfg.count.floor_height << "mm" + << ", SparseCut=" << (cfg.filter.sparse_cut.enable ? "开" : "关") + << "(X±" << cfg.filter.sparse_cut.x_radius << "mm,同圈>=" << cfg.filter.sparse_cut.circle_min + << ",跨圈>=" << cfg.filter.sparse_cut.run_min << ")" + << ", TailCut=" << (cfg.filter.tail_cut.enable ? "开" : "关") << std::endl; + send_reply(client, resp, "{\"code\":200,\"message\":\"config reloaded\"}"); + } + + // ConfigureSpeed: 标定生成 config/speed.txt(雷达横放时,每帧Y最大值=行进距离)。 + // 请求: {"function":"ConfigureSpeed","scanTime":N},N缺省用 cfg.scan.collect_duration_s。 + // 标定期间与扫描会话互斥(全程持 g_life,其他命令排队)。 + static void handle_configure_speed(const std::string& msg, SOCKET client, + std::string* resp = nullptr) + { + double scan_time = 0.0; + json_get_number(msg, "scanTime", scan_time); + double seconds = (scan_time > 0) ? scan_time : (double)cfg.scan.collect_duration_s; + if (seconds <= 0.0) seconds = 5.0; + std::cout << "[TCP] 收到ConfigureSpeed: 采集" << seconds + << "s 标定变速剖面(每帧Y最大值) ..." << std::endl; + + int rows = -1; + std::string err; + { + std::lock_guard lk(g_life); + if (discard_current_locked()) // 扫描运行中先强制停止(本函数持锁) + std::cout << "[TCP] 扫描运行中被ConfigureSpeed中断" << std::endl; + rows = collect_speed_profile(*g_driver, seconds, err); + } + if (rows < 0) + { + std::cerr << "[TCP] ConfigureSpeed失败: " << err << std::endl; + send_reply(client, resp, "{\"code\":400,\"message\":\"configure speed failed\"}"); + return; + } + reload_speed_profile(); // 重新载入新生成的剖面(若已是configuration则立即生效) + std::cout << "[TCP] speed.txt 已更新: " << rows + << " 帧样本 (采集" << seconds << "s)" << std::endl; + std::ostringstream r; + r << "{\"code\":200,\"message\":\"configure speed done\",\"rows\":" << rows + << ",\"duration\":" << (int)seconds + << ",\"file\":\"config/speed.txt\"}"; + send_reply(client, resp, r.str()); } - static void dispatch(const std::string& raw, SOCKET client) + // 处理一条命令。resp==nullptr: 直接发送(TCP);resp!=nullptr: 响应写入该串(HTTP)。 + // 返回值: 是否产生响应。 + static bool dispatch(const std::string& raw, SOCKET client, + std::string* resp = nullptr) { std::string msg = json_trim(raw); - if (msg.empty()) return; + if (msg.empty()) return false; std::string fn; - if (!json_get_string(msg, "function", fn)) return; // 未知消息忽略 + if (!json_get_string(msg, "function", fn)) return false; // 未知消息忽略 if (fn == "config") { - handle_config(msg, client); - return; + handle_config(msg, client, resp); + return true; } - if (fn != "count" && fn != "check") return; // 未知功能忽略 + if (fn == "ConfigureSpeed" || fn == "configurespeed") + { + handle_configure_speed(msg, client, resp); + return true; + } + if (fn != "count" && fn != "check") return false; // 未知功能忽略 std::string scan_cmd; json_get_string(msg, "scan", scan_cmd); if (scan_cmd == "stop") - handle_scan_stop(msg, client); + { + handle_scan_stop(msg, client, resp); + return true; + } else if (scan_cmd == "start") - handle_scan_start(msg, client); - // 其余(无scan/未知scan值)忽略 + { + handle_scan_start(msg, client, resp); + return true; + } + return false; // 无scan/未知scan值忽略 } static void handle_client(SOCKET client) @@ -378,7 +461,7 @@ void run_tcp_server(pavo2::pavo2_driver& driver) { g_driver = &driver; std::cout << "\n========== TCP服务端模式 ==========" << std::endl; - std::cout << "[TCP] 监听端口: " << Tcp_Port + std::cout << "[TCP] 监听端口: " << cfg.server.port << " | 消息以 '>' 结尾" << std::endl; SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0); @@ -391,10 +474,10 @@ void run_tcp_server(pavo2::pavo2_driver& driver) sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(Tcp_Port); + addr.sin_port = htons(cfg.server.port); if (bind(listen_sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { - std::cerr << "[错误] 绑定端口 " << Tcp_Port << " 失败!" << std::endl; + std::cerr << "[错误] 绑定端口 " << cfg.server.port << " 失败!" << std::endl; closesocket(listen_sock); return; } @@ -421,3 +504,177 @@ void run_tcp_server(pavo2::pavo2_driver& driver) std::thread(handle_client, client).detach(); } } + +// ==================== HTTP 服务端模式 ==================== +// 请求: POST /<任意路径>, body = 与 TCP 协议相同的 JSON(不带结尾 '>') +// 响应: HTTP 200 + application/json, body = 与 TCP 协议相同的响应文本(不带 '>') +// 注: HTTP 请求是"无连接"的。scanTime>0 的扫描到点自动停止后无法主动推送 +// 结果(结果打印到日志);客户端应在需要结果时再发一次 stop(会返回 +// "scan not running" + count:0) 或通过文件目录查看。建议流程: +// start(不带scanTime) → 采集完成 → stop 拿结果。 +namespace +{ + static bool http_read_head(SOCKET c, std::string& head, std::string& rest) + { + char buf[8192]; + head.clear(); + rest.clear(); + while (head.size() < 65536) // 头上限 64KB,防呆 + { + int n = recv(c, buf, (int)sizeof(buf), 0); + if (n <= 0) return false; + head.append(buf, n); + size_t p = head.find("\r\n\r\n"); + if (p != std::string::npos) + { + rest = head.substr(p + 4); // 头部之后可能已带部分 body + head = head.substr(0, p); + return true; + } + } + return false; + } + + static long http_find_content_length(const std::string& head) + { + std::istringstream iss(head); + std::string line; + while (std::getline(iss, line)) + { + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t c = line.find(':'); + if (c == std::string::npos) continue; + std::string key = line.substr(0, c); + for (auto& ch : key) ch = (char)tolower((unsigned char)ch); + if (key == "content-length") + return strtol(line.c_str() + c + 1, nullptr, 10); + } + return -1; + } + + static void http_send_response(SOCKET c, int code, const char* reason, + const std::string& body) + { + std::ostringstream h; + h << "HTTP/1.1 " << code << " " << reason << "\r\n" + << "Content-Type: application/json; charset=utf-8\r\n" + << "Content-Length: " << body.size() << "\r\n" + << "Connection: close\r\n\r\n"; + std::string out = h.str() + body; + int off = 0; + while (off < (int)out.size()) + { + int n = send(c, out.c_str() + off, (int)out.size() - off, 0); + if (n <= 0) break; + off += n; + } + } + + // 每个连接处理一个请求后即关闭(Connection: close) + static void handle_http_client(SOCKET c) + { + std::string head, rest; + if (!http_read_head(c, head, rest)) + { + closesocket(c); + return; + } + + std::string line0 = head.substr(0, head.find("\r\n")); + std::istringstream iss(line0); + std::string method, path; + iss >> method >> path; + + if (method != "POST") + { + http_send_response(c, 405, "Method Not Allowed", + "{\"code\":405,\"message\":\"use POST, body same as TCP protocol json\"}"); + closesocket(c); + return; + } + + long cl = http_find_content_length(head); + if (cl < 0 || cl > 4 * 1024 * 1024) // body 上限 4MB + { + http_send_response(c, 411, "Length Required", + "{\"code\":411,\"message\":\"missing/invalid Content-Length\"}"); + closesocket(c); + return; + } + + std::string body = rest; + while ((long)body.size() < cl) + { + char buf[8192]; + int n = recv(c, buf, (int)sizeof(buf), 0); + if (n <= 0) break; + body.append(buf, n); + } + if ((long)body.size() > cl) body.resize(cl); + + std::string brief = json_trim(body); + if (brief.size() > 120) brief = brief.substr(0, 120) + "..."; + std::cout << "[HTTP] POST " << path << " (" << cl << "B): " + << brief << std::endl; + + std::string out; + bool handled = dispatch(brief, INVALID_SOCKET, &out); + if (handled) + http_send_response(c, 200, "OK", out); + else + http_send_response(c, 400, "Bad Request", + "{\"code\":400,\"message\":\"unknown command, see TCP protocol\"}"); + closesocket(c); + } + + static void run_http_server_internal() + { + SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0); + if (listen_sock == INVALID_SOCKET) + { + std::cerr << "[错误] 创建HTTP监听socket失败!" << std::endl; + return; + } + sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(cfg.server.port); + if (bind(listen_sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) + { + std::cerr << "[错误] 绑定HTTP端口 " << cfg.server.port << " 失败!" << std::endl; + closesocket(listen_sock); + return; + } + if (listen(listen_sock, 5) == SOCKET_ERROR) + { + std::cerr << "[错误] HTTP监听失败!" << std::endl; + closesocket(listen_sock); + return; + } + + // 自动到时停止线程(scanTime 模式; HTTP 下结果打印日志不推送) + std::thread(auto_stop_loop).detach(); + + std::cout << "[HTTP] 初始化完成,等待 POST 命令 " + "{\"function\":\"count\",\"scan\":\"start\",...} ..." << std::endl; + while (true) + { + SOCKET client = accept(listen_sock, nullptr, nullptr); + if (client == INVALID_SOCKET) + { + std::cout << "[HTTP] accept失败,重试..." << std::endl; + continue; + } + std::thread(handle_http_client, client).detach(); + } + } +} + +void run_http_server(pavo2::pavo2_driver& driver) +{ + g_driver = &driver; + std::cout << "\n========== HTTP服务端模式 ==========" << std::endl; + std::cout << "[HTTP] 监听端口: " << cfg.server.port + << " | POST json body(与TCP协议相同, 不带结尾'>')" << std::endl; + run_http_server_internal(); +} diff --git a/tcp_server.h b/tcp_server.h index 4e3fca6..12d1344 100644 --- a/tcp_server.h +++ b/tcp_server.h @@ -8,4 +8,9 @@ // 需在 WSAStartup、load_config、connect_lidar 之后调用。 void run_tcp_server(pavo2::pavo2_driver& driver); +// 启动 HTTP 服务端(阻塞运行直到进程退出/失败)。 +// POST json:body 与 TCP 协议消息相同(不带结尾 '>'),响应同 TCP(不带 '>')。 +// 需在 WSAStartup、load_config、connect_lidar 之后调用。 +void run_http_server(pavo2::pavo2_driver& driver); + #endif // TCP_SERVER_H diff --git a/tcp通信协议文档.md b/tcp通信协议文档.md index 1fa61ea..630e5ca 100644 --- a/tcp通信协议文档.md +++ b/tcp通信协议文档.md @@ -37,7 +37,7 @@ - 所有响应同样以 `>` 结尾(服务端自动补 `>`)。 - 常规响应:`{"code":200,"message":"..."}` - 错误响应:`{"code":400,"message":"..."}` -- 扫描结果响应:含 `function` 对应的结果字段(见 3.3 / 3.4)。 +- 扫描结果响应:含 `function` 对应的结果字段(见 3.4 / 3.5)。 ### 2.3 字段解析宽松性 @@ -48,9 +48,51 @@ ## 3. 命令说明 -### 3.1 配置重载 `{"function":"config"}` +### 3.1 速度剖面标定 `{"function":"ConfigureSpeed"}` -重新读取 `config\config` 配置文件(雷达 IP/端口、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。 +雷达**横放**(Y 轴为行进方向)时,用于生成变速运动的 `config/speed.txt`(时间 → z 距离 速度剖面):程序采集 `scanTime` 秒,**逐帧记录(时间, 该帧过滤后 Y 最大值)**,Y 最大值即该时刻被测物在行进方向上的距离;对与总体趋势反向的抖动做平滑后写入文件。 + +**请求:** +```json +{"function":"ConfigureSpeed","scanTime":6} +``` + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `scanTime` | number | 否 | 标定采集时长(秒)。缺省或 ≤0 时用配置 `Collect_Duration_S` | + +**行为:** + +- 标定采集与扫描会话互斥:若当前有扫描在运行,会被强制中断后开始标定(其它命令在标定期间排队)。 +- 结束后自动重载新剖面:若当前 `Speed_Z_Type` 已是 `configuration`,下一次扫描立即按新表变速换算;否则需在配置中把 `Speed_Z_Type` 改为 `configuration` 并发送 `config` 命令生效。 + +**响应:** +```json +{"code":200,"message":"configure speed done","rows":302,"duration":6,"file":"config/speed.txt"} +``` + +**失败响应:** +```json +{"code":400,"message":"configure speed failed"} +``` + +**`config/speed.txt` 格式(每行两列,时间须递增):** +``` +# speed.txt - 变速运动z轴速度剖面 (每行: 时间ms 距离mm) +0 100.000 +20 104.500 +40 109.300 +... +6000 1650.200 +``` +- 以 `#`(或 `;`)开头的行与空行被忽略; +- 时间单位 **ms**,距离单位 **mm**; +- 无此文件或行数不足 2 时,程序自动回退使用 `Speed_Z` 匀速; +- 识别时间**超出表末**时,按表中**最后两行的平均速度**继续向外延伸。 + +### 3.2 配置重载 `{"function":"config"}` + +重新读取 `config\config` 配置文件(雷达 IP/端口、采集时长、多边形过滤区域、分层容差、插值模式、日志保留天数等全部重新加载),无需重启程序。若 `Speed_Z_Type` 为 `configuration`,同时**重新读取 `config/speed.txt`**(配合 3.1 标定的新表直接生效)。 **请求:** ```json @@ -62,7 +104,7 @@ {"code":200,"message":"config reloaded"} ``` -### 3.2 扫描命令总体结构 `function=count / check` +### 3.3 扫描命令总体结构 `function=count / check` ```json {"function":"count|check","scan":"start|stop","scanTime":6,"box":{...},"task_id":"t1"} @@ -81,7 +123,7 @@ `check` 不需要 `box`;`box` 字段在 count 模式下缺失或为 0 时,本次只采集不回计数(结果为 `count:0`)。 -### 3.3 开始扫描 `{"function":"count|check","scan":"start",...}` +### 3.4 开始扫描 `{"function":"count|check","scan":"start",...}` **请求(count 示例):** ```json @@ -109,7 +151,7 @@ {"code":400,"message":"scan start failed"} ``` -### 3.4 停止扫描 `{"function":"count|check","scan":"stop",...}` +### 3.5 停止扫描 `{"function":"count|check","scan":"stop",...}` 手动立即停止当前扫描并返回结果。`function` 应与发起 start 时一致。 @@ -151,14 +193,14 @@ | `count` | int | 物品总个数(0 表示无有效计数) | | `task_id` | string | 仅当请求中携带时才回传 | -### 3.5 check 检测原理与数据产物 +### 3.6 check 检测原理与数据产物 截面检测在 **stop 时**对本次采集的全部点执行一次: 1. Y 整体偏移(`SHIFT_Y_POSITIVE`,使最小 Y ≥ 0); 2. 起点截面 = 行进方向起始端 `Z_Range_MM` 区间内的点;终点截面 = 结束端前 `Z_Range_MM` 区间内的点; 3. 每个截面内取 Y 在 `[Y最小, Y最小 + Y_Range_MM]` 的点,生成 X-Z 二值图像,统计白色占比,并求截面点集的 Y 最大值 / 中位数; -4. 结果回发(见 3.4),文件保存到任务目录(不阻塞响应)。 +4. 结果回发(见 3.5),文件保存到任务目录(不阻塞响应)。 **任务目录:** `data/YYYYMMDD/HHMMSS_taskId/`(`task_id` 为空时名为 `HHMMSS_`;`task_id` 建议避免含 `/ \ : * ? " < > |` 等路径非法字符)。 @@ -173,7 +215,7 @@ | `lidar_slice_end.pcd` / `.jpg` | 终点截面点云 / X-Z 二值图 | | `lidar_slice_combined.jpg` | 起点与终点截面合并对比图(中缝为分隔线) | -### 3.6 count 计数原理与数据产物 +### 3.7 count 计数原理与数据产物 **停止后处理链:** z 空隙插值(仅执行一次)→ 按层统计 → 回发结果 → 后台线程异步保存文件(不阻塞响应)。 @@ -189,7 +231,7 @@ **z 空隙插值(count 模式):** 雷达断流 / 瞬间丢包会在 z 方向产生空白带。停止采集后按 `Z_Interpolate_Mode` 修复: - `none`:不插值; - `CopyTranslation` / `AngleBucketInterpolation`:对超过 2×正常圈间隔且满足同层约束(相邻圈 Y 均值差 ≤ 30 mm、X 落在相邻圈范围内)的空隙补行,最多补 10 行。 -- 正常圈间隔:`Speed_Z>0` 时为 `Speed_Z×10/Lidar_Hz`(mm),否则为 `1000/Lidar_Hz`(ms)。 +- 正常圈间隔:`configuration`(变速)模式用 `speed.txt` 剖面平均速度(mm/s)÷`Lidar_Hz`(mm/圈)近似;否则 `Speed_Z>0` 时为 `Speed_Z×10/Lidar_Hz`(mm),`Speed_Z=0`(时间模式)时为 `1000/Lidar_Hz`(ms)。 **输出文件(count):** @@ -200,7 +242,7 @@ | `count_layer.jpg` | 被计数层(最高有效层)图像(有有效层时) | | `log.txt` | 日志:task_id、box 参数、分层情况(各层点数/高度范围/面积)、计数结果(`Log_Flag` 开启时) | -### 3.7 未知 / 非法命令 +### 3.8 未知 / 非法命令 | 情况 | 行为 | |---|---| @@ -266,19 +308,21 @@ |---|---|---| | `Leida_IP` / `Leida_Port` | string / number | 雷达 IP 与数据端口 | | `Collect_Duration_S` | number | 默认扫描时长(秒)。**exe 模式**下自动采集该时长后做一次 check;TCP 模式下仅作参考(不带 `scanTime` 的 start 仍等手动 stop) | -| `Speed_Z` | number | Z 轴速度(cm/s)。0 = z 轴为时间(ms);>0 = z 轴为移动距离(mm) | +| `Speed_Z_Type` | string | z 轴速度来源模式。`uniformSpeed` = 匀速(用 `Speed_Z`);`configuration` = 变速(用 `config/speed.txt`,缺表时回退 `Speed_Z`) | +| `Speed_Z` | number | Z 轴速度(cm/s)。0 = z 轴为时间(ms);>0 = z 轴为移动距离(mm)。仅 `uniformSpeed` 模式直接生效 | | `Lidar_Hz` | number | 雷达帧率(Hz),用于正常圈间隔与截面矩形长度计算 | | `Z_Range_MM` / `Y_Range_MM` | number | 截面检测区间长度(mm) | | `Rect_Width_PX` / `Rect_Length_Ratio` | number | 截面矩形宽度(px)/ 长度倍率 | -| `Run_Mode` | string | `"tcp"` = 服务端模式;其他 = exe 模式自动检测一次 | -| `Tcp_Port` | number | TCP 监听端口 | +| `Run_Mode` | string | `"tcp"` = TCP 服务端;`"http"` = HTTP 服务端(POST json);其他 = exe 模式自动检测一次 | +| `Tcp_Port` | number | TCP / HTTP 监听端口 | | `Filter_Polygon` | array | 过滤多边形顶点(mm,至少 3 个顶点) | | `Floor_Height` | number | 地板高度(mm),物品高度 = `Floor_Height - Y` | | `Count_Base_Tolerance` | number | count 分层初始冗余(第 1 层容差,mm) | | `Count_Additional_Tolerance_Per_Level` | number | count 每层递增冗余(mm) | | `Point_Dilate` | number | 画 layer 图每个点膨胀的像素(Point_Dilate × Point_Dilate) | | `Z_Interpolate_Mode` | string | `none` / `CopyTranslation` / `AngleBucketInterpolation` | -| `Log_Retain_Days` | number | 任务目录保留天数;程序启动时与每日 01:00 自动清理更早的目录 | +| `Log_Retain_Days` | number | 任务目录保留天数;程序启动时与每日定时(`Log_Clean_Hour`/`Log_Clean_Minute`)自动清理更早的目录 | +| `Log_Clean_Hour` / `Log_Clean_Minute` | number | 每日日志定时清理时刻(时 0-23 / 分 0-59,默认 1:00)。运行期发 `config` 命令重载后下一轮生效 | | `Tuo_Wei` / `Qian_Chong` / `Yuan_Duan` / `Jin_Duan` | number / bool | 雷达滤波参数(拖尾 / 前冲 / 远端高反 / 近端高反) | ## 7. 常见问题 @@ -287,8 +331,46 @@ - **Q:不带 scanTime 的 start 之后忘了发 stop 会怎样?** 扫描会一直持续,直到该连接发 `scan:"stop"` 或其它连接再发 `scan:"start"`(强制重启旧扫描)。建议每次都带 `scanTime` 或确保成对发送。 - **Q:扫描进行中再发一次 start?** 会强制重启:旧扫描被丢弃,按新参数重新开始,旧任务不返回结果。 - **Q:check 和 count 能并发吗?** 不能。二者共用同一个采集会话,同一时刻只能有一个扫描;新的 start 会强制重启当前扫描。 + +## 8. HTTP 运行模式(可选,替代 TCP 客户端调试) + +`Run_Mode` 设为 `"http"` 后,程序改用 HTTP 协议监听 `Tcp_Port`,方便浏览器 / Postman / 网页上位机直连,无需自定义 TCP 客户端。 + +### 8.1 请求 + +- 方法:`POST`(其他方法返回 `405`) +- URL:`http://<服务器IP>:/`(路径任意,服务端不校验) +- 请求头:`Content-Type: application/json` +- Body:与 TCP 协议完全相同的 JSON 字符串,**不带结尾的 `>`**(TCP 的 `>` 只是消息边界符,HTTP 用 Content-Length 定界) + +示例: + +```json +{"function":"count","scan":"start","scanTime":6,"box":{"l":154,"w":153,"h":310,"maxPerLayer":30},"task_id":"t1"} +``` + +### 8.2 响应 + +- 成功:`HTTP/1.1 200 OK`,`Content-Type: application/json` +- Body:与 TCP 协议相同的响应 JSON,**不带结尾的 `>`** +- 非法命令:`400 Bad Request`,body `{"code":400,...}` + +### 8.3 与 TCP 的差异(无连接语义) + +| 能力 | TCP | HTTP | +|---|---|---| +| `start`/`stop`/`config` 等请求-响应 | 支持 | 支持(每请求独立连接) | +| `scanTime` 到时自动停止并推送结果 | 支持(推送给该连接) | 不支持推送:结果打印到服务端日志,任务文件照常保存 | +| 推荐流程 | `start(scanTime)` 即可 | `start`(不带 scanTime)→ 采集完成后发 `stop` 拿结果 | + +### 8.4 Postman 调试 + +`config/PAVO2_HTTP_API.postman_collection.json` 为现成 Postman Collection(v2.1),导入后把集合变量 `host` 改为服务程序所在电脑 IP 即可逐条调试。 - **Q:收到 stop 但之前没 start 过?** 返回 `{"code":200,"message":"scan not running","count":0}`(count),不会误用历史数据。 - **Q:带 scanTime 的扫描,其连接中途断开会怎样?** 扫描仍会到时自动停止,但结果回发给已断开的 socket 会失败;不带 scanTime 的扫描则继续,直到被 start 强制重启或 stop。 - **Q:同一秒内连续两次停止,会不会写到同一目录?** `task_id` 相同且在同一秒内完成时会写入同一目录(文件覆盖);建议任务间隔 >1 秒或使用不同 `task_id`。 - **Q:输出目录在哪?** `data/YYYYMMDD/HHMMSS_taskId/`;超过 `Log_Retain_Days` 天的旧目录会在启动与每日 01:00 被自动清理。 - **Q:端口是多少?** 配置文件 `Tcp_Port`,默认 `9601`。 +- **Q:如何实现变速(非匀速)运动下的测量?** 三步:① 把 `Speed_Z_Type` 设为 `configuration`;② 生成 `config/speed.txt`(用 `ConfigureSpeed` 命令让被测物跑一遍自动标定,或手工按“时间ms 距离mm”填写);③ 发送 `config` 命令重载(此后 z 按表变速插值)。无 `speed.txt` 时自动回退 `Speed_Z` 匀速。 +- **Q:`speed.txt` 时间/距离的单位?** 时间毫秒(ms)、距离毫米(mm),时间列须递增;识别时间超过表末时按表内最后两行平均速度延伸。 +- **Q:`ConfigureSpeed` 和普通扫描冲突吗?** 不并发:标定开始前会强制中断进行中的扫描,标定期间其它命令排队等待。 diff --git a/test_cfg.cpp b/test_cfg.cpp new file mode 100644 index 0000000..1c295af --- /dev/null +++ b/test_cfg.cpp @@ -0,0 +1,32 @@ +#include "config.h" +#include + +int main() +{ + load_config("./config/config"); + std::cout << "radar: " << cfg.radar.ip << ":" << cfg.radar.port + << " hz=" << cfg.radar.hz + << " tail=" << cfg.radar.tail_filter << std::endl; + std::cout << "clean_time => " << cfg.log.clean_hour << ":" << cfg.log.clean_minute + << ":" << cfg.log.clean_sec << std::endl; + std::cout << "log.enable=" << cfg.log.enable << " retain=" << cfg.log.retain_days << std::endl; + std::cout << "poly_n=" << cfg.filter.poly_n << std::endl; + std::cout << "tail_cut.enable=" << cfg.filter.tail_cut.enable + << " max_tail=" << cfg.filter.tail_cut.max_tail_length << std::endl; + std::cout << "sparse_cut.enable=" << cfg.filter.sparse_cut.enable + << " x_radius=" << cfg.filter.sparse_cut.x_radius + << " circle=" << cfg.filter.sparse_cut.circle_min + << " run=" << cfg.filter.sparse_cut.run_min << std::endl; + std::cout << "server: " << cfg.server.mode << ":" << cfg.server.port << std::endl; + std::cout << "detect: z=" << cfg.detect.z_range_mm << " y=" << cfg.detect.y_range_mm + << " w=" << cfg.detect.rect_width_px << " ratio=" << cfg.detect.rect_length_ratio << std::endl; + std::cout << "count: floor=" << cfg.count.floor_height + << " base_tol=" << cfg.count.base_tolerance + << " per_tol=" << cfg.count.per_level_tolerance + << " dilate=" << cfg.count.point_dilate + << " interp=" << cfg.count.z_interpolate_mode << std::endl; + std::cout << "scan: dur=" << cfg.scan.collect_duration_s + << " type=" << cfg.scan.z_speed_type + << " speed=" << cfg.scan.z_speed << std::endl; + return 0; +}