1.增加http请求

2.修改配置文件解释方案
main
LAPTOP-S9HJSOEB\昊天 2 months ago
parent 930ab66199
commit 7b6b7aa0d9

@ -302,7 +302,9 @@ protected:
std::condition_variable m_cmd_op_cond;
std::map<uint16_t, std::shared_ptr<Pavo2CmdDataDef>> m_map_cmd;
SynchronizedQueue<pavo_point_cloud_scan_t> m_scans_que;
// 有界队列(上限1000帧):空闲期未采集时接收线程仍在持续 enqueue,
// 若放任无界会线性占用内存直至撑爆;超限时丢最旧、保留最新数据。
SynchronizedQueue<pavo_point_cloud_scan_t> m_scans_que{ 1000 };
SynchronizedQueue<pavo_ptp_deviation_t> m_ptp_que;
};

@ -1,44 +1,31 @@
#include "config.h"
#include "json_utils.h"
#include <cstdio>
#include <iostream>
#include <fstream>
#include <sstream>
// ==================== 配置变量定义 ====================
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<std::pair<double, double>> 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<std::pair<double, double>> 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<std::pair<double, double>> 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;
}

@ -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<std::pair<double, double>> 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<std::pair<double, double>> 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 不存在时打印错误并退出

@ -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": [""] }
}
}
]
}

@ -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"
}
}

@ -3,6 +3,7 @@
#include "config.h"
#include "filter.h"
#include "io_utils.h"
#include "lidar_utils.h"
#include <algorithm>
#include <cmath>
#include <fstream>
@ -12,6 +13,231 @@
#include <thread>
#include <unordered_map>
// ==================== 点云级稀疏拖影过滤 ====================
// 现象: 平台/物体左右边缘外伸的"胡须状拖影"= 每圈端部外侧 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<CountPoint>& 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<size_t> 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<Circle> 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<double> 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<uint8_t> sparse(n, 0);
for (const auto& c : circles)
{
std::vector<size_t> 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<uint8_t> 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<size_t> 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<size_t> 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<uint8_t>& image, int w, int h,
const std::vector<double>& cell_y_max)
{
if (w <= 0 || h <= 0) return;
const bool use_y = cell_y_max.size() == image.size();
std::vector<uint8_t> 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<double> 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<int>(s + cfg.filter.tail_cut.max_tail_length, e - cfg.filter.tail_cut.core_min_len + 1);
int right_cut = std::max<int>(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<uint8_t> points_to_image(const std::vector<CountPoint>& points,
double* area_out,
@ -24,16 +250,16 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
if (out_h) *out_h = 0;
if (points.empty()) return std::vector<uint8_t>((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<uint8_t> points_to_image(const std::vector<CountPoint>& points,
double xOffset = -polyXMin;
// 优化:Step1 先按1x1网格标记点位置(重复点只写一次),
// Step2 再对标记位置膨胀 Point_Dilate×Point_Dilate
// Step2 再对标记位置膨胀 cfg.count.point_dilate×cfg.count.point_dilate
std::vector<uint32_t> marks;
marks.reserve(points.size() / 2);
std::vector<double> 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<uint8_t> points_to_image(const std::vector<CountPoint>& 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<uint8_t> points_to_image(const std::vector<CountPoint>& points,
// ==================== z轴空隙插值 ====================
int z_gap_interpolate(std::vector<CountPoint>& 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<CountPoint>& 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<CountPoint>& 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<CountPoint>& 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<CountPoint>& points, const CountParams& bo
std::unordered_map<int, std::vector<CountPoint>> 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<CountPoint>& 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<CountPoint>& 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<double> 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<CountPoint>& 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<CountPoint>& 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)

@ -7,9 +7,14 @@
#include <cstdint>
#include "common.h"
// 点云级稀疏拖影过滤:删除每圈边缘外脱离主体的稀疏外伸点(胡须状拖影),
// 使 pcd/图片/面积统计都基于干净点云。判据见 cfg.filter.sparse_cut。
// 原地修改 points;返回删除点数。应在 z_gap_interpolate 之前调用。
int sparse_density_filter(std::vector<CountPoint>& 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<uint8_t> points_to_image(const std::vector<CountPoint>& points,
double* area_out = nullptr,
@ -19,12 +24,12 @@ std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& 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<CountPoint>& 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<CountPoint>& 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<CountPoint>& points,
const std::vector<uint8_t>& top_layer_img,
int top_img_w, int top_img_h,

@ -4,6 +4,7 @@
#include "detect.h"
#include "config.h"
#include "io_utils.h"
#include "lidar_utils.h"
#include <algorithm>
#include <cmath>
#include <cstring>
@ -18,9 +19,9 @@ void detect_analyze_slices(const std::vector<CountPoint>& 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<CountPoint>& 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<const CountPoint*> 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<CountPoint>& 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<CountPoint>& 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<CountPoint>& 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<CountPoint>& points,
log_msg("[文件] " + out_pcd + " (" + std::to_string(slice_xs.size()) + " 点)\n");
// X-Z平面映射:矩形渲染(横轴=Z 左近→右远, 纵轴=X 越大越靠上)
// 速度来源: configuration(变速)模式用剖面平均速度近似; 否则匀速 cfg.scan.z_speed; 时间模式默认10mm
std::vector<uint8_t> 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<CountPoint>& 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<CountPoint>& 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)

@ -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 返回)。

@ -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);
}

@ -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<std::pair<double, double>>& 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);

@ -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
}

@ -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)

@ -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<std::pair<double, double>>& 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;
}

@ -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<std::pair<double, double>>& 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<std::pair<double, double>>& out);
#endif // JSON_UTILS_H

@ -2,17 +2,21 @@
#include "lidar_utils.h"
#include "config.h"
#include "filter.h"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
// ==================== 雷达连接 ====================
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<std::pair<double, double>> 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<std::pair<double, double>> 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<std::mutex> 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<std::mutex> 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<std::mutex> lk(g_prof_mtx);
g_profile.clear();
}
}
bool speed_profile_loaded()
{
std::lock_guard<std::mutex> lk(g_prof_mtx);
return g_profile.size() >= 2;
}
double speed_profile_avg_velocity_mm_s()
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::pair<double, double>> 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::milliseconds>(
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<CountPoint>& out)
{

@ -3,6 +3,7 @@
#define LIDAR_UTILS_H
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <chrono>
@ -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))。
// 返回新增点数。

@ -1,8 +1,11 @@
// main.cpp - 程序入口(模块化重构版)
// 模块: config / json_utils / filter / io_utils / lidar_utils / count / detect / tcp_server
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <ctime>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
@ -17,34 +20,65 @@
#include "detect.h"
#include "tcp_server.h"
// ==================== 日志目录清理:启动时 + 每天 01:00 ====================
// ==================== 日志目录清理:启动时 + 每天 log.clean_time ====================
static std::atomic<bool> 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<std::mutex> 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<CountPoint> 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

Binary file not shown.

@ -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()

@ -25,7 +25,9 @@
#include <ws2tcpip.h>
#include <chrono>
#include <cctype>
#include <condition_variable>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <iomanip>
@ -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<CountPoint>& 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<CountPoint>& 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<CountPoint>& 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<std::mutex> 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();
}

@ -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

@ -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>:<Tcp_Port>/`(路径任意,服务端不校验)
- 请求头:`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` 和普通扫描冲突吗?** 不并发:标定开始前会强制中断进行中的扫描,标定期间其它命令排队等待。

@ -0,0 +1,32 @@
#include "config.h"
#include <iostream>
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;
}
Loading…
Cancel
Save