You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1844 lines
80 KiB
C++

2 months ago
// Pavo2 Lidar SDK - 雷达数据采集程序
// 功能1:获取单帧雷达数据 + 输出鸟瞰图(ppm)
// 功能2:获取多秒雷达数据,输出 x,y,时间戳(ms) 文件 + 深度图(ppm)
#include <iostream>
#include <fstream>
#include <vector>
#include <chrono>
#include <iomanip>
#include <thread>
#include <mutex>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <direct.h> // _mkdir
#include <sstream>
#include <unordered_map>
// pavo2_driver.h 内部会引入 winsock2.h,必须在 windows.h 之前
// NOMINMAX: MinGW GCC 在 os_defines.h 中已自动定义,无需手动定义
//#include "pavo2_driver.h"
#include "PAVO2_SDK/include/pavo2_driver.h"
#ifdef _WIN32
#include <windows.h>
#endif
// stb single-header JPEG 编码器
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "PAVO2_SDK/include/stb_image_write.h"
// ==================== 配置参数 ====================
//const std::string LIDAR_IP = "192.168.0.170"; // 雷达IP地址(必填)
//const uint16_t Leida_PORT = 2368; // 雷达数据端口
const uint32_t OPEN_TIMEOUT_MS = 3000; // 连接超时(ms)
const uint16_t FRAME_TIMEOUT_MS = 2000; // 获取帧数据超时(ms)
//driver.pavo2_open(Leida_IP, Leida_PORT, OPEN_TIMEOUT_MS)//0713改
// -------------------配置参数 0713增加------------------//
static std::string Leida_IP = "192.168.0.170";
static uint16_t Leida_PORT = 2368;
static int Collect_Duration_S = 6;
static double Speed_Z = 20.0;
static bool Log_Flag = true;
static double Z_RANGE_MM = 50.0; // Z截面区间长度(mm)
static double Y_RANGE_MM = 80.0; // Y截面区间长度(mm): 只取[Y最小, Y最小+此值]的点参与
static double Lidar_Hz = 50.0; // 雷达帧率(Hz),用于计算矩形长
static int Rect_Width_PX = 8; // 截面矩形宽度(px)
static double Rect_Length_Ratio = 1.5; // 矩形长倍率: 矩形长=速度/帧率×倍率
static std::string Run_Mode = "exe"; // exe=直接检测一次, tcp=服务端模式
static uint16_t Tcp_Port = 9601; // TCP模式监听端口
static double Floor_Height = 1600.0; // 地板高度(mm): 物品高度 = Floor_Height - Y轴(count计数用)
static double Count_Base_Tolerance = 50.0; // 分层初始冗余 50mm(count分层用)
static double Count_Additional_Tolerance_Per_Level = 20.0; // 每层增加 20mm 冗余(count分层用)
static int Point_Dilate = 8; // count画layer图: 每个点膨胀 Point_Dilate×Point_Dilate 像素
static std::string Z_Interpolate_Mode = "none"; // count z轴空隙插值模式: none=不插值 / CopyTranslation=同圈复制平移 / AngleBucketInterpolation=角度桶插值
static int Tuo_Wei = 2; // 拖尾滤波0=关, 1=弱, 2=中, 3=强
static int Qian_Chong = 2; //前冲滤波0=关, 1=弱, 2=中, 3=强
static int Yuan_Duan = 1; // 远端高反滤波0=关, 1=弱, 2=强
static bool Jin_Duan = true; // 近端高反识别true开,false关
//----------------------------------------------------//
// 多边形过滤开关(false 则禁用,全部保留)
const bool FILTER_ENABLE = true;
// 多边形过滤区域(顶点坐标,单位: mm)
static std::vector<std::pair<double, double>> Filter_Polygon = {
{-650, 0}, {-650, 670}, {650, 670}, {650, 0}
};
static int Filter_Poly_N = (int)Filter_Polygon.size(); // 自动获取顶点数 // <20>Զ<EFBFBD><D4B6><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// 当 true 时,所有Y值上移使最小Y≥0(消除负数)
const bool SHIFT_Y_POSITIVE = true;
// Z轴:速度(cm/s),0=使用时间(ms),>0=Z轴=速度×时间=移动距离(mm)
//const double Speed_Z = 20.0;
// 过滤映射到原点(0,0)的无效点(雷达噪点)
// x和y同时接近0时视为原点无效点,丢弃该点
//---------------加载配置文件---------------------------//
// 去除字符串首尾空白
static std::string trim_str(const std::string& s)
{
size_t b = s.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return "";
size_t e = s.find_last_not_of(" \t\r\n");
return s.substr(b, e - b + 1);
}
// GBK(ANSI) → UTF-8:日志文件按UTF-8写入,避免编辑器打开乱码
// (exe内字符串按 -fexec-charset=GBK 编译,控制台GBK显示正常,写文件需转UTF-8)
static std::string gbk_to_utf8(const std::string& s)
{
#ifdef _WIN32
if (s.empty()) return s;
int wlen = MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), nullptr, 0);
if (wlen <= 0) return s;
std::wstring wbuf(wlen, L'\0');
MultiByteToWideChar(CP_ACP, 0, s.c_str(), (int)s.size(), &wbuf[0], wlen);
int ulen = WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, nullptr, 0, nullptr, nullptr);
if (ulen <= 0) return s;
std::string ubuf(ulen, '\0');
WideCharToMultiByte(CP_UTF8, 0, wbuf.c_str(), wlen, &ubuf[0], ulen, nullptr, nullptr);
return ubuf;
#else
return s;
#endif
}
void load_config(const std::string& path = "./config/config")
{
std::ifstream fin(path);
if (!fin.is_open()) {
std::cerr << "[WARN] 未找到 config配置文件" << std::endl;
exit(-1);
}
std::string line;
while (std::getline(fin, line)) {
if (line.find("Leida_Ip:") == 0) {
Leida_IP = line.substr(line.find(':') + 1);
}
else if (line.find("Leida_Port:") == 0) {
Leida_PORT = static_cast<uint16_t>(std::stoi(line.substr(line.find(':') + 1)));
}
else if (line.find("Collect_Duration_S:") == 0) {
Collect_Duration_S = std::stoi(line.substr(line.find(':') + 1));
}
else if (line.find("Speed_Z:") == 0) {
Speed_Z = std::stod(line.substr(line.find(':') + 1));
}
else if (line.find("Log_Flag:") == 0) {
std::string flag = line.substr(line.find(':') + 1);
Log_Flag = (flag == "YES" || flag == "yes" || flag == "Yes");
}
else if (line.find("Z_Range_MM:") == 0) {
Z_RANGE_MM = std::stod(line.substr(line.find(':') + 1));
}
else if (line.find("Y_Range_MM:") == 0) {
Y_RANGE_MM = std::stod(line.substr(line.find(':') + 1));
}
else if (line.find("Lidar_Hz:") == 0) {
Lidar_Hz = std::stod(line.substr(line.find(':') + 1));
}
else if (line.find("Rect_Width_PX:") == 0) {
Rect_Width_PX = std::stoi(line.substr(line.find(':') + 1));
}
else if (line.find("Rect_Length_Ratio:") == 0) {
Rect_Length_Ratio = std::stod(line.substr(line.find(':') + 1));
}
else if (line.find("Run_Mode:") == 0) {
Run_Mode = trim_str(line.substr(line.find(':') + 1));
}
else if (line.find("Tcp_Port:") == 0) {
Tcp_Port = static_cast<uint16_t>(std::stoi(line.substr(line.find(':') + 1)));
}
else if (line.find("Filter_Polygon:") == 0) {
Filter_Polygon.clear();
std::string data = line.substr(line.find(':') + 1);
// 格式: {-650,0},{-650,670},{650,670},{650,0}
size_t pos = 0;
while (pos < data.size()) {
size_t lb = data.find('{', pos);
if (lb == std::string::npos) break;
size_t rb = data.find('}', lb);
if (rb == std::string::npos) break;
std::string pair = data.substr(lb + 1, rb - lb - 1);
size_t comma = pair.find(',');
if (comma != std::string::npos) {
double x = std::stod(pair.substr(0, comma));
double y = std::stod(pair.substr(comma + 1));
Filter_Polygon.push_back({x, y});
}
pos = rb + 1;
}
Filter_Poly_N = (int)Filter_Polygon.size();
}
else if (line.find("Tuo_Wei:") == 0) {
Tuo_Wei = std::stoi(line.substr(line.find(':') + 1));
if (Tuo_Wei < 0) Tuo_Wei = 0;
if (Tuo_Wei > 3) Tuo_Wei = 3;
}
else if (line.find("Qian_Chong:") == 0) {
Qian_Chong = std::stoi(line.substr(line.find(':') + 1));
if (Qian_Chong < 0) Qian_Chong = 0;
if (Qian_Chong > 3) Qian_Chong = 3;
}
else if (line.find("Yuan_Duan:") == 0) {
Yuan_Duan = std::stoi(line.substr(line.find(':') + 1));
if (Yuan_Duan < 0) Yuan_Duan = 0;
if (Yuan_Duan > 2) Yuan_Duan = 2;
}
else if (line.find("Jin_Duan:") == 0) {
std::string val = line.substr(line.find(':') + 1);
Jin_Duan = (val == "true" || val == "TRUE" || val == "True" ||
val == "yes" || val == "YES" || val == "Yes");
}
else if (line.find("Floor_Height:") == 0) {
Floor_Height = std::stod(line.substr(line.find(':') + 1));
}
else if (line.find("Count_Base_Tolerance:") == 0) {
Count_Base_Tolerance = std::stod(line.substr(line.find(':') + 1));
if (Count_Base_Tolerance < 0) Count_Base_Tolerance = 0;
}
else if (line.find("Count_Additional_Tolerance_Per_Level:") == 0) {
Count_Additional_Tolerance_Per_Level = std::stod(line.substr(line.find(':') + 1));
if (Count_Additional_Tolerance_Per_Level < 0) Count_Additional_Tolerance_Per_Level = 0;
}
else if (line.find("Point_Dilate:") == 0) {
Point_Dilate = std::stoi(line.substr(line.find(':') + 1));
if (Point_Dilate < 1) Point_Dilate = 1;
if (Point_Dilate > 64) Point_Dilate = 64; // 上限保护,防止内存爆炸
}
else if (line.find("Z_Interpolate_Mode:") == 0) {
std::string mode = line.substr(line.find(':') + 1);
// 去掉首尾空白
size_t b = mode.find_first_not_of(" \t\r");
if (b != std::string::npos) mode = mode.substr(b);
size_t e = mode.find_last_not_of(" \t\r");
if (e != std::string::npos) mode = mode.substr(0, e + 1);
if (mode == "CopyTranslation" || mode == "AngleBucketInterpolation")
Z_Interpolate_Mode = mode;
else
Z_Interpolate_Mode = "none";
}
}
fin.close();
}
const double ORIGIN_TOL_MM = 0.5; // mm,判定为原点的容差
inline bool is_origin_point(double x_mm, double y_mm) {
return std::abs(x_mm) < ORIGIN_TOL_MM && std::abs(y_mm) < ORIGIN_TOL_MM;
}
// ==================== 多边形过滤工具 ====================
// 射线法判断点是否在多边形内(支持凹/凸多边形)
// px, py: 点坐标, poly: 顶点数组(与点坐标相同单位), n: 顶点数
bool point_in_polygon(double px, double py,
const std::vector<std::pair<double, double>>& poly, int n)
{
bool inside = false;
for (int i = 0, j = n - 1; i < n; j = i++)
{
double xi = poly[i].first, yi = poly[i].second;
double xj = poly[j].first, yj = poly[j].second;
if (((yi > py) != (yj > py)) &&
(px < (xj - xi) * (py - yi) / (yj - yi) + xi))
{
inside = !inside;
}
}
return inside;
}
// ==================== 截面切片配置 ====================
// 2D映射图尺寸
const int SLICE_IMG_SIZE = 800; // 输出图片最大像素尺寸
// ==================== PCD 文件保存 ====================
void save_pcd_ascii(const std::string& filename,
const std::vector<double>& xs,
const std::vector<double>& ys,
const std::vector<double>& zs)
{
size_t n = xs.size();
// 一次性构建字符串再写入,避免逐行IO(大量点数时显著提速)
std::ostringstream oss;
oss << "# .PCD v0.7 - Point Cloud Data file format\n"
<< "VERSION 0.7\n"
<< "FIELDS x y z\n"
<< "SIZE 4 4 4\n"
<< "TYPE F F F\n"
<< "COUNT 1 1 1\n"
<< "WIDTH " << n << "\n"
<< "HEIGHT 1\n"
<< "VIEWPOINT 0 0 0 1 0 0 0\n"
<< "POINTS " << n << "\n"
<< "DATA ascii\n"
<< std::fixed << std::setprecision(1);
for (size_t i = 0; i < n; i++) {
oss << xs[i] << " " << ys[i] << " " << zs[i] << "\n";
}
std::string buf = oss.str();
std::ofstream f(filename, std::ios::binary);
f.write(buf.data(), (std::streamsize)buf.size());
f.close();
}
// 保存 JPG 灰度图(灰度转RGB后压缩为JPEG,quality=90)
bool save_jpg_gray(const std::string& filename, int width, int height,
const std::vector<uint8_t>& pixels, int quality = 90)
{
// stbi_write_jpg 需要 RGB 三通道数据
std::vector<uint8_t> rgb(width * height * 3);
for (int i = 0; i < width * height; i++) {
uint8_t v = pixels[i];
rgb[i * 3 + 0] = v;
rgb[i * 3 + 1] = v;
rgb[i * 3 + 2] = v;
}
int ret = stbi_write_jpg(filename.c_str(), width, height, 3, rgb.data(), quality);
return ret != 0;
}
// ==================== 连接雷达 ====================
bool connect_lidar(pavo2::pavo2_driver& driver)
{
driver.set_conn_protocol(pavo2::PAVO2_PROTO_TYPE::TCP);
std::cout << "[连接] 正在连接雷达 " << Leida_IP << ":" << Leida_PORT << " ..." << std::endl;
if (!driver.pavo2_open(Leida_IP, Leida_PORT, OPEN_TIMEOUT_MS))
{
std::cerr << "[错误] 连接雷达失败!请检查IP和网络连接。" << std::endl;
return false;
}
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);
std::string sn, pn, sdk_ver, fw_ver;
if (driver.get_device_sn(sn))
std::cout << "[信息] 设备SN: " << sn << std::endl;
if (driver.get_device_pn(pn))
std::cout << "[信息] 设备PN: " << pn << std::endl;
if (driver.get_sdk_ver(sdk_ver))
std::cout << "[信息] SDK版本: " << sdk_ver << std::endl;
if (driver.get_firmware_ver(fw_ver))
std::cout << "[信息] 固件版本: " << fw_ver << std::endl;
if (!driver.enable_data(true))
{
std::cerr << "[错误] 开启数据传输失败!" << std::endl;
return false;
}
std::cout << "[信息] 雷达数据传输已开启。" << std::endl;
return true;
}
// ==================== 功能1:单帧 + 鸟瞰图 ====================
void demo_single_frame(pavo2::pavo2_driver& driver)
{
std::cout << "\n========== 功能1:获取单帧雷达数据 ==========" << std::endl;
std::vector<pavo2::pavo_response_pcd_t> points;
uint32_t timestamp = 0;
if (!driver.get_scanned_data(points, timestamp, FRAME_TIMEOUT_MS))
{
std::cerr << "[错误] 获取单帧数据超时或失败!" << std::endl;
return;
}
std::cout << "[结果] 成功获取一帧数据:" << std::endl;
std::cout << " 原始点数量: " << points.size() << std::endl;
if (points.empty()) return;
// DEBUG: 检查原始坐标范围 + Y轴偏移计算
double y_offset_single = 0.0; // mm
{
double xmin = 1e9, xmax = -1e9, ymin = 1e9, ymax = -1e9;
int neg_y = 0, pos_y = 0;
for (const auto& p : points) {
double x_mm = p.x * 2.0, y_mm = p.y * 2.0;
if (x_mm < xmin) xmin = x_mm; if (x_mm > xmax) xmax = x_mm;
if (y_mm < ymin) ymin = y_mm; if (y_mm > ymax) ymax = y_mm;
if (y_mm < -0.5) neg_y++; if (y_mm > 0.5) pos_y++;
}
std::cout << "[DEBUG单帧] X范围: " << xmin << " ~ " << xmax << " mm" << std::endl;
std::cout << "[DEBUG单帧] Y范围: " << ymin << " ~ " << ymax << " mm" << std::endl;
std::cout << "[DEBUG单帧] Y>0点数: " << pos_y << " Y<0点数: " << neg_y << std::endl;
if (SHIFT_Y_POSITIVE && ymin < 0) {
y_offset_single = -ymin;
std::cout << "[DEBUG单帧] Y轴偏移: +" << y_offset_single << " mm (SHIFT_Y_POSITIVE=true)" << std::endl;
}
}
// --- 鸟瞰图参数 ---
const int IMG_SIZE = 800; // 图片像素尺寸
const double RANGE_MM = 10000.0; // ±10m 范围
const double SCALE = IMG_SIZE / (2.0 * RANGE_MM); // mm转像素
// 多边形过滤 + 原点过滤
int filtered_count = 0;
int origin_skip = 0;
std::vector<uint8_t> image(IMG_SIZE * IMG_SIZE, 0);
for (const auto& p : points)
{
double x_mm = p.x * 2.0; // 原始单位0.002m,转mm
double y_mm = p.y * 2.0 + y_offset_single; // Y轴上移
// 跳过原点无效点
if (is_origin_point(x_mm, y_mm)) {
origin_skip++;
continue;
}
if (FILTER_ENABLE && !point_in_polygon(x_mm, y_mm, Filter_Polygon, Filter_Poly_N))
continue;
filtered_count++;
// 世界坐标 -> 像素坐标(图片中心为原点,Y轴翻转)
int px = (int)((x_mm + RANGE_MM) * SCALE);
int py = (int)((RANGE_MM - y_mm) * SCALE); // 上方向为Y正
if (px >= 0 && px < IMG_SIZE && py >= 0 && py < IMG_SIZE)
{
// 亮度 = intensity,叠加时取最大值
int idx = py * IMG_SIZE + px;
uint8_t val = (uint8_t)(std::min(p.intensity, (uint16_t)255));
if (val > image[idx]) image[idx] = val;
}
}
// 标记雷达中心点 (小十字)
int cx = IMG_SIZE / 2, cy = IMG_SIZE / 2;
for (int dx = -3; dx <= 3; dx++) {
int idx = cy * IMG_SIZE + (cx + dx);
if (idx >= 0 && idx < IMG_SIZE * IMG_SIZE) image[idx] = 255;
}
for (int dy = -3; dy <= 3; dy++) {
int idx = (cy + dy) * IMG_SIZE + cx;
if (idx >= 0 && idx < IMG_SIZE * IMG_SIZE) image[idx] = 255;
}
std::string img_file = "single_frame.jpg";
if (save_jpg_gray(img_file, IMG_SIZE, IMG_SIZE, image))
std::cout << "[图像] 鸟瞰图已保存: " << img_file
<< " (" << IMG_SIZE << "x" << IMG_SIZE << ", 范围±" << RANGE_MM/1000 << "m, JPG)" << std::endl;
// 点云也保存为 txt(仅过滤区域内的点)
std::ofstream txt("single_frame.txt");
txt << "# 过滤区域: x∈[-10,10]m, y∈[10,20]m (FILTER_POLY_N=" << Filter_Poly_N << ")\n";
txt << "# x(mm) y(mm) intensity\n";
for (const auto& p : points)
{
double x_mm = p.x * 2.0;
double y_mm = p.y * 2.0 + y_offset_single; // Y轴上移
if (is_origin_point(x_mm, y_mm)) continue;
if (FILTER_ENABLE && !point_in_polygon(x_mm, y_mm, Filter_Polygon, Filter_Poly_N))
continue;
txt << std::fixed << std::setprecision(1)
<< x_mm << " " << y_mm << " " << p.intensity << "\n";
}
txt.close();
std::cout << "[过滤] 区域内点数: " << filtered_count << " / " << points.size() << std::endl;
if (origin_skip > 0)
std::cout << "[过滤] 原点无效点跳过: " << origin_skip << " 点" << std::endl;
std::cout << "[文件] 点云数据已保存: single_frame.txt" << std::endl;
}
// 一次检测的结果(供TCP返回)
struct DetectResult
{
double start_fill_pct = -1.0; // 起点截面白色占比(%)
double end_fill_pct = -1.0; // 终点截面白色占比(%)
double start_y_max = 0.0; // 起点截面Y最大值(mm)
double start_y_med = 0.0; // 起点截面Y中位数(mm)
double end_y_max = 0.0; // 终点截面Y最大值(mm)
double end_y_med = 0.0; // 终点截面Y中位数(mm)
bool valid = false; // 是否有有效结果
};
// ==================== 功能2:多秒采集 + 深度图 ====================
void demo_collect_multi(pavo2::pavo2_driver& driver, DetectResult* result = nullptr)
{
std::cout << "\n========== 功能2:采集 " << Collect_Duration_S
<< " 秒雷达数据 ==========" << std::endl;
// --- 创建输出目录(data/当前时间 年月日HH-MM-SS) + 日志文件 ---
time_t rawtime = std::time(nullptr);
struct tm* timeinfo = std::localtime(&rawtime);
char dir_buf[32];
//std::strftime(dir_buf, sizeof(dir_buf), "%H-%M-%S", timeinfo);
std::strftime(dir_buf, sizeof(dir_buf), "data/%Y%m%d/%H%M%S", timeinfo); //文件放到data下面好管理
std::string output_dir(dir_buf);
mkdir("data");
mkdir(output_dir.substr(0, output_dir.rfind('/')).c_str());
_mkdir(output_dir.c_str());
//增加日志使能
std::ofstream log_file;
if (Log_Flag) {
log_file.open(output_dir + "/log.txt");
}
//std::ofstream log_file(output_dir + "/log.txt");
auto log_msg = [&log_file](const std::string& s) {
std::cout << s;
if (log_file.is_open()) log_file << gbk_to_utf8(s);
};
log_msg("[目录] 输出目录: " + output_dir + "/\n");
struct PointRecord
{
double x_mm;
double y_mm;
double angle_deg; // 角度(度)
uint16_t distance; // 原始距离(unit=2mm)
double z_val; // Z轴值: 距离(mm) 或 时间(ms),取决于Speed_Z
};
// 帧数据(用于深度图)
struct FrameData
{
uint32_t elapsed_ms;
std::vector<uint16_t> dist_by_angle; // 每个角度bin的距离
std::vector<uint8_t> intensity_by_angle;
};
std::vector<PointRecord> all_points;
std::vector<FrameData> all_frames;
size_t origin_skip_total = 0;
// 清空SDK缓冲帧:enable_data后等待稳定期间,网络线程会把完整扫描帧缓存进队列,
// 若不清空,采集开始时会瞬间连续取出这些旧帧,它们的Z值几乎都为0,全部堆在Z起点。
int drained = 0;
{
pavo2::pavo_packet_info_scan_t tmp_scan;
while (driver.get_scanned_data(tmp_scan, 5)) // 5ms超时,空队列立即结束
drained++;
}
if (drained > 0)
std::cout << "[采集] 已清空缓冲旧帧 " << drained << " 帧" << std::endl;
auto start_time = std::chrono::steady_clock::now();
int frame_count = 0;
size_t total_points = 0;
// 深度图参数
const int DEPTH_ANGLE_BINS = 720; // 0.5度一个bin
std::cout << "[采集] 开始采集,将持续 " << Collect_Duration_S << " 秒..." << std::endl;
if (Speed_Z > 0)
std::cout << "[采集] z轴 = 距离(mm), 速度=" << Speed_Z << " cm/s" << std::endl;
else
std::cout << "[采集] z轴 = 从采集开始的系统毫秒时间" << std::endl;
while (true)
{
auto now = std::chrono::steady_clock::now();
auto elapsed_s = std::chrono::duration_cast<std::chrono::seconds>(now - start_time).count();
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_time).count();
if (elapsed_s >= Collect_Duration_S)
{
std::cout << "[采集] " << Collect_Duration_S << " 秒到,停止采集。" << std::endl;
break;
}
// 获取原始scan数据(带角度),便于做深度图
pavo2::pavo_packet_info_scan_t scan_info;
if (driver.get_scanned_data(scan_info, FRAME_TIMEOUT_MS))
{
frame_count++;
// Z轴:距离模式 或 时间模式
double z_val = (Speed_Z > 0)
? (Speed_Z * 10.0 * elapsed_ms / 1000.0) // cm/s → mm/s, × ms = mm
: (double)elapsed_ms;
// 构建本帧的角度-距离直方图
FrameData frame;
frame.elapsed_ms = (uint32_t)elapsed_ms;
frame.dist_by_angle.assign(DEPTH_ANGLE_BINS, 0);
frame.intensity_by_angle.assign(DEPTH_ANGLE_BINS, 0);
for (const auto& s : scan_info.vec_buff)
{
double angle_deg = s.angle / 100.0; // SDK角度单位是0.01度
// 雷达0°指向-Y方向(第3/4象限交界),旋转-90°映射:
// cos(A-90°)=sin(A), sin(A-90°)=-cos(A)
// 0°→(0,-dist)向下, 90°→(+dist,0)向右, 180°→(0,+dist)向上, 270°→(-dist,0)向左
double angle_rad = angle_deg * 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 (is_origin_point(x_mm, y_mm)) {
origin_skip_total++;
continue;
}
// 多边形过滤:只保留区域内的点(FILTER_POLY_N=0 则不过滤)
if (FILTER_ENABLE && !point_in_polygon(x_mm, y_mm, Filter_Polygon, Filter_Poly_N))
continue;
all_points.push_back({x_mm, y_mm, angle_deg, s.distance, z_val});
// 角度 -> bin
int bin = (int)(angle_deg / 360.0 * DEPTH_ANGLE_BINS + 0.5) % DEPTH_ANGLE_BINS;
if (s.distance > frame.dist_by_angle[bin])
frame.dist_by_angle[bin] = s.distance;
if (s.intensity > frame.intensity_by_angle[bin])
frame.intensity_by_angle[bin] = (uint8_t)(std::min(s.intensity, (uint16_t)255));
}
all_frames.push_back(frame);
total_points += scan_info.vec_buff.size();
if (frame_count % 50 == 0)
{
std::cout << " 已采集 " << elapsed_s << "s, 帧数=" << frame_count
<< ", 总点数=" << total_points << std::endl;
}
}
}
std::cout << "[采集] 采集完成!" << std::endl;
std::cout << " 总帧数: " << frame_count << std::endl;
std::cout << " 总点数: " << total_points << std::endl;
if (origin_skip_total > 0)
std::cout << " 原点无效点跳过: " << origin_skip_total << " 点" << std::endl;
// ==========> 保存多边形过滤后的原始PCD(Y偏移之前)<==========
if (!all_points.empty()) {
std::vector<double> raw_xs, raw_ys, raw_zs;
raw_xs.reserve(all_points.size());
raw_ys.reserve(all_points.size());
raw_zs.reserve(all_points.size());
for (const auto& pt : all_points) {
raw_xs.push_back(pt.x_mm);
raw_ys.push_back(pt.y_mm);
raw_zs.push_back(pt.z_val);
}
save_pcd_ascii(output_dir + "/lidar_data.pcd", raw_xs, raw_ys, raw_zs);
std::cout << "[文件] 多边形过滤后原始点云已保存: lidar_data.pcd ("
<< all_points.size() << " 点, Y未偏移)" << std::endl;
}
// Y轴偏移
if (!all_points.empty()) {
double ymin = 1e9;
for (const auto& pt : all_points) {
if (pt.y_mm < ymin) ymin = pt.y_mm;
}
if (SHIFT_Y_POSITIVE && ymin < 0) {
double y_offset = -ymin;
for (auto& pt : all_points) {
pt.y_mm += y_offset;
}
std::cout << "[信息] Y轴偏移: +" << y_offset << " mm" << std::endl;
}
}
// ==========> 保存Y偏移后的PCD <==========
if (!all_points.empty()) {
std::vector<double> shifted_xs, shifted_ys, shifted_zs;
shifted_xs.reserve(all_points.size());
shifted_ys.reserve(all_points.size());
shifted_zs.reserve(all_points.size());
for (const auto& pt : all_points) {
shifted_xs.push_back(pt.x_mm);
shifted_ys.push_back(pt.y_mm);
shifted_zs.push_back(pt.z_val);
}
save_pcd_ascii(output_dir + "/lidar_data_shifted.pcd", shifted_xs, shifted_ys, shifted_zs);
std::cout << "[文件] Y偏移后点云已保存: lidar_data_shifted.pcd ("
<< all_points.size() << " 点, Y已偏移)" << std::endl;
}
// ============ 截面切片:Y轴切片 → X-Z平面映射 (起点+终点各一张) ============
if (all_points.empty()) {
if (Log_Flag && log_file.is_open())//0713改
log_file << gbk_to_utf8("无点云数据,跳过截面切片。") << std::endl;
log_file.close();
return;
}
// Step 1: Y范围(全图)
double y_min = all_points[0].y_mm;
double y_max = all_points[0].y_mm;
for (const auto& pt : all_points) {
if (pt.y_mm < y_min) y_min = pt.y_mm;
if (pt.y_mm > y_max) y_max = pt.y_mm;
}
std::cout << "\n[截面] Y全图范围: " << y_min << " ~ " << y_max << " mm" << std::endl;
// Step 2: Y截面区间 = [Y最小, Y最小+Y_Range_MM],只取该区间内的点参与
double y_slice_max = y_min + Y_RANGE_MM;
std::vector<const PointRecord*> y_filtered;
y_filtered.reserve(all_points.size());
for (const auto& pt : all_points) {
if (pt.y_mm >= y_min && pt.y_mm <= y_slice_max)
y_filtered.push_back(&pt);
}
std::cout << "[截面] Y截面区间: " << y_min << " ~ " << y_slice_max
<< " mm (长度=" << Y_RANGE_MM << "mm, 配置Y_Range_MM)" << std::endl;
std::cout << "[截面] 参与点数: " << y_filtered.size() << " / " << all_points.size() << std::endl;
if (y_filtered.empty()) {
std::cout << "[截面] Y截面区间内无点,跳过截面生成。" << std::endl;
return;
}
// Step 3: 计算全局X范围和Z范围
// X范围: 用全图所有点的X最大~X最小(图像高度方向,必须全图范围)
// Z范围: 用Y过滤后点的Z最大~Z最小(定义起终点截面区间用)
double gx_min = all_points[0].x_mm, gx_max = all_points[0].x_mm;
for (const auto& pt : all_points) {
if (pt.x_mm < gx_min) gx_min = pt.x_mm;
if (pt.x_mm > gx_max) gx_max = pt.x_mm;
}
double gz_min = y_filtered[0]->z_val, gz_max = y_filtered[0]->z_val;
for (const auto* pt : y_filtered) {
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";
std::cout << "[截面] 全局X范围(全图点云): " << gx_min << " ~ " << gx_max << " mm (图像高度方向)" << std::endl;
std::cout << "[截面] 全局Z范围: " << gz_min << " ~ " << gz_max << " " << z_unit << std::endl;
std::cout << "[截面] 每张图高度由全图 X最大~X最小 范围决定(不再固定800px)" << std::endl;
// Step 4: 两个Z区间
double z_first_start = gz_min;
double z_first_end = gz_min + Z_RANGE_MM;
double z_last_end = gz_max;
double z_last_start = gz_max - 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;
// ====== 通用:Y+Z过滤 → X-Z平面二值映射 ======
auto make_xz_slice = [&](double z0, double z1, const std::string& label,
const std::string& out_pcd, const std::string& out_jpg,
double* out_fill_pct = nullptr,
int* out_w = nullptr, int* out_h = nullptr,
double* out_y_max = nullptr, double* out_y_med = nullptr,
std::vector<uint8_t>* out_img = nullptr,
bool right_align = false)
{
std::ostringstream oss;
oss << "\n[截面-" << label << "] Z区间: " << z0 << " ~ " << z1 << " " << z_unit << "\n";
log_msg(oss.str());
// 收集切片点(Y已过滤 + Z区间过滤)
std::vector<double> slice_xs, slice_ys, slice_zs;
for (const auto* pt : y_filtered) {
if (pt->z_val >= z0 && pt->z_val <= z1) {
slice_xs.push_back(pt->x_mm);
slice_ys.push_back(pt->y_mm);
slice_zs.push_back(pt->z_val);
}
}
oss.str(""); oss.clear();
oss << "[截面-" << label << "] 切片点数: " << slice_xs.size() << "\n";
log_msg(oss.str());
if (slice_xs.empty()) {
oss.str(""); oss.clear();
oss << "[截面-" << label << "] 无数据,跳过。\n";
log_msg(oss.str());
return;
}
// 图像高度方向采用全图 X 范围(gx_min~gx_max),高度=该范围/比例(不再固定800px)
double sx_min = gx_min, sx_max = gx_max;
double sx_range = sx_max - sx_min;
if (sx_range < 1.0) sx_range = 1.0;
// 图像尺寸:宽=Z_Range_MM(px),高=全图X范围(px),1px=1mm,不做比例缩放
const int img_w = std::max(1, (int)std::ceil(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)); // 高 = 全图X范围
if (out_w) *out_w = img_w;
if (out_h) *out_h = img_h;
oss.str(""); oss.clear();
oss << "[截面-" << label << "] 高度方向X范围(全图): " << gx_min << " ~ " << gx_max
<< " mm\n";
log_msg(oss.str());
oss.str(""); oss.clear();
oss << "[截面-" << label << "] 图像尺寸: " << img_w << "x" << img_h
<< " px (1px=1mm: 宽=Z_Range_MM=" << Z_RANGE_MM
<< ", 高=全图X范围=" << sx_range << ")\n";
log_msg(oss.str());
// Y统计: 最大值 + 中位数
double y_max_slice = *std::max_element(slice_ys.begin(), slice_ys.end());
std::vector<double> ys_sorted = slice_ys;
std::sort(ys_sorted.begin(), ys_sorted.end());
double y_med = (ys_sorted.size() % 2 == 1)
? ys_sorted[ys_sorted.size() / 2]
: (ys_sorted[ys_sorted.size() / 2 - 1] + ys_sorted[ys_sorted.size() / 2]) / 2.0;
oss.str(""); oss.clear();
oss << "[截面-" << label << "] Y统计: 最大值=" << std::fixed << std::setprecision(1)
<< y_max_slice << " mm, 中位数=" << y_med << " mm\n";
log_msg(oss.str());
if (out_y_max) *out_y_max = y_max_slice;
if (out_y_med) *out_y_med = y_med;
// 保存 PCD
save_pcd_ascii(out_pcd, slice_xs, slice_ys, slice_zs);
oss.str(""); oss.clear();
oss << "[文件] " << out_pcd << " (" << slice_xs.size() << " 点)\n";
log_msg(oss.str());
// X-Z平面映射:矩形渲染
// 横轴=Z(运动方向, 左近→右远), 纵轴=X(X越大越靠上)
// 每个点作为矩形的左边中心点,矩形向右延伸(长=速度/帧率=每帧移动距离),宽=Rect_Width_PX
std::vector<uint8_t> slice_img(img_w * img_h, 0);
// 矩形长(mm) = 运行速度(mm/s) / 雷达帧率(Hz) × 倍率(默认1.5)
double rect_len_mm = 10.0; // 时间模式(无速度)默认10mm
if (Speed_Z > 0)
rect_len_mm = Speed_Z * 10.0 / Lidar_Hz * Rect_Length_Ratio;
int len_px = std::max(1, (int)(rect_len_mm / img_scale));
int half_w = Rect_Width_PX / 2;
if (half_w < 1) half_w = 1;
// 水平方向定位:start左对齐(矩形紧贴左边缘),end右对齐(矩形紧贴右边缘)
// 切片Z区间长度==图像宽度(1px=1mm),矩形铺满整图,左右不留黑边
double z_ref = z0;
int x_offset = 0; // start:最左点的矩形左边缘=0
if (right_align && !slice_zs.empty()) {
z_ref = *std::max_element(slice_zs.begin(), slice_zs.end());
x_offset = img_w - len_px; // end:最右点的矩形右边缘=图宽-1
}
for (size_t i = 0; i < slice_xs.size(); i++) {
int cx = right_align
? (int)(x_offset - (z_ref - slice_zs[i]) / img_scale) // 右对齐:右侧中间
: (int)((slice_zs[i] - z0) / img_scale + x_offset); // 左对齐:左侧中间
int cy = (int)((sx_max - slice_xs[i]) / img_scale); // 矩形左边中心点y
// 以 (cx, cy) 为左边中心画矩形: 向右延伸 len_px,上下各 half_w
for (int dx = 0; dx < len_px; dx++) {
for (int dy = -half_w; dy <= half_w; dy++) {
int px = cx + dx;
int py = cy + dy;
if (px >= 0 && px < img_w && py >= 0 && py < img_h) {
slice_img[py * img_w + px] = 255;
}
}
}
}
// 统计矩形覆盖像素(白色)的占比
int filled = 0;
for (int i = 0; i < img_w * img_h; i++)
if (slice_img[i] > 0) filled++;
double fill_pct = (img_w * img_h > 0)
? filled * 100.0 / (img_w * img_h) : 0.0;
if (out_fill_pct) *out_fill_pct = fill_pct;
// 白色(所有矩形)占全图比例日志:无论图片保存成功与否都输出
oss.str(""); oss.clear();
oss << "[截面-" << label << "] 白色占比: " << filled << "/"
<< (img_w * img_h) << " 像素 ("
<< std::fixed << std::setprecision(2) << fill_pct << "%)\n";
log_msg(oss.str());
if (save_jpg_gray(out_jpg, img_w, img_h, slice_img)) {
oss.str(""); oss.clear();
oss << "[图像] " << out_jpg << " (" << img_w << "x" << img_h
<< ", Z=" << z0 << "~" << z1 << " " << z_unit
<< ", X=" << sx_min << "~" << sx_max << "mm"
<< ", 矩形长=" << rect_len_mm << "mm(" << len_px << "px)"
<< ", 矩形宽=" << Rect_Width_PX << "px"
<< ", 对齐=" << (right_align ? "右" : "左") << ")\n";
log_msg(oss.str());
}
if (out_img) *out_img = slice_img; // 回传渲染图,用于合并对比图
};
double start_fill_pct = -1.0, end_fill_pct = -1.0;
int start_w = 0, start_h = 0, end_w = 0, end_h = 0;
double start_y_max = 0.0, start_y_med = 0.0, end_y_max = 0.0, end_y_med = 0.0;
std::vector<uint8_t> start_img, end_img;
// 起点截面
make_xz_slice(z_first_start, z_first_end, "起点",
output_dir + "/lidar_slice_start.pcd",
output_dir + "/lidar_slice_start.jpg",
&start_fill_pct, &start_w, &start_h,
&start_y_max, &start_y_med, &start_img,
false); // 起点:左对齐,在左侧
// 终点截面(结束前 Z_RANGE_MM)
if (z_last_start < z_last_end) {
make_xz_slice(z_last_start, z_last_end, "终点",
output_dir + "/lidar_slice_end.pcd",
output_dir + "/lidar_slice_end.jpg",
&end_fill_pct, &end_w, &end_h,
&end_y_max, &end_y_med, &end_img,
true); // 终点:右对齐,在右侧
} else {
std::cout << "\n[截面-终点] Z总长不足" << Z_RANGE_MM << "mm,跳过终点截面。" << std::endl;
}
// ====== 合并对比图:start在左半区,end在右半区 ======
if (!start_img.empty() && !end_img.empty() && start_w == end_w && start_h == end_h) {
int comb_w = start_w * 2;
int comb_h = start_h;
std::vector<uint8_t> combined(comb_w * comb_h, 0);
for (int y = 0; y < comb_h; y++) {
std::memcpy(&combined[y * comb_w], &start_img[y * start_w], start_w);
std::memcpy(&combined[y * comb_w + start_w], &end_img[y * start_w], start_w);
combined[y * comb_w + start_w] = 128; // 中间分隔线(灰)
}
if (save_jpg_gray(output_dir + "/lidar_slice_combined.jpg", comb_w, comb_h, combined)) {
std::cout << "[图像] " << output_dir << "/lidar_slice_combined.jpg ("
<< comb_w << "x" << comb_h << ", 左=start 右=end)" << std::endl;
}
}
// 回传检测结果(供TCP返回)
if (result) {
result->valid = true;
result->start_fill_pct = start_fill_pct;
result->end_fill_pct = end_fill_pct;
result->start_y_max = start_y_max;
result->start_y_med = start_y_med;
result->end_y_max = end_y_max;
result->end_y_med = end_y_med;
}
// --- 写入日志摘要 ---
{
std::ostringstream sum;
sum << "\n========== 采集摘要 ==========" << std::endl;
sum << "采集时长: " << Collect_Duration_S << " s" << std::endl;
sum << "总帧数: " << frame_count << std::endl;
sum << "总点数: " << total_points << std::endl;
sum << "过滤后点数: " << all_points.size() << std::endl;
sum << "原点无效点跳过: " << origin_skip_total << std::endl;
sum << "Y范围(全部点参与): " << y_min << " ~ " << y_max << " mm" << std::endl;
sum << "参与点数: " << y_filtered.size() << std::endl;
sum << "Z截面区间长度: " << Z_RANGE_MM << " mm" << std::endl;
sum << "雷达帧率: " << Lidar_Hz << " Hz" << std::endl;
sum << "矩形宽度: " << Rect_Width_PX << " px" << std::endl;
sum << "全局X范围: " << gx_min << " ~ " << gx_max << " mm" << std::endl;
sum << "全局Z范围: " << gz_min << " ~ " << gz_max << " " << z_unit << std::endl;
if (start_w > 0)
sum << "起点截面图像尺寸: " << start_w << "x" << start_h << " px" << std::endl;
if (end_w > 0)
sum << "终点截面图像尺寸: " << end_w << "x" << end_h << " px" << std::endl;
if (start_fill_pct >= 0.0)
sum << "起点截面白色占比: " << std::fixed << std::setprecision(2)
<< start_fill_pct << "%" << std::endl;
if (end_fill_pct >= 0.0)
sum << "终点截面白色占比: " << std::fixed << std::setprecision(2)
<< end_fill_pct << "%" << std::endl;
sum << "输出文件:" << std::endl;
sum << " " << output_dir << "/lidar_data.pcd" << std::endl;
sum << " " << output_dir << "/lidar_data_shifted.pcd" << std::endl;
sum << " " << output_dir << "/lidar_slice_start.pcd" << std::endl;
sum << " " << output_dir << "/lidar_slice_start.jpg" << std::endl;
sum << " " << output_dir << "/lidar_slice_end.pcd" << std::endl;
sum << " " << output_dir << "/lidar_slice_end.jpg" << std::endl;
sum << "==============================" << std::endl;
if (log_file.is_open()) log_file << gbk_to_utf8(sum.str());
}
log_file.close();
std::cout << "\n[截面] 完成。" << std::endl;
}
// ==================== 计数模式(count) ====================
// TCP收到 {"count":"start","box":{"l":行mm,"w":宽mm,"h":层高mm,"maxPerLayer":每层最大个数},"task_id":"xxx"} 开始扫描
// TCP收到 {"count":"stop"} 停止扫描并返回计数结果
// 高度计算: 物品高度 = Floor_Height - Y轴(mm),参照Java: 地板值减去当前点的y轴值为高度
struct CountPoint
{
double x_mm;
double y_mm;
double z_val; // Z轴: 距离(mm)或时间(ms),用于面积计算的第二维度
};
struct CountParams
{
double l = 0.0; // 行(长) mm
double w = 0.0; // 宽 mm
double h = 0.0; // 层高 mm
int maxPerLayer = 0; // 每层最大个数
std::string task_id;
};
static std::mutex count_mtx;
static bool count_running = false;
static bool count_stop = false;
static std::thread count_thread;
static std::vector<CountPoint> count_points;
static CountParams count_params;
static std::chrono::steady_clock::time_point count_start_tp;
// 后台采集线程:count start 后持续扫描,count stop 后退出
// start 后先丢弃SDK队列中积压的旧扫描数据(雷达一直转, start之前已缓存多圈数据),
// 确保只统计 start 之后新扫描到的数据,否则积压的旧包会在图像开头z方向挤成一段密集点。
// z轴: 用本机时钟 elapsed_ms (相对清空积压后开始计时的时刻)。
static void count_collect_loop(pavo2::pavo2_driver* driver)
{
// 排空积压:把SDK缓存中 start 之前的数据包全部取出丢弃
pavo2::pavo_packet_info_scan_t flush_tmp;
int flushed = 0;
{
auto flush_begin = std::chrono::steady_clock::now();
while (true)
{
{
std::lock_guard<std::mutex> lk(count_mtx);
if (count_stop) return; // 排空期间被stop,直接退出
}
if (driver->get_scans_available() <= 0) break;
if (!driver->get_scanned_data(flush_tmp, 0)) break; // 0ms = 立即返回,不阻塞
flushed++;
// 防御:雷达持续产生数据,最多排空500ms,避免卡死
if (std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - flush_begin).count() > 500)
break;
}
}
if (flushed > 0)
std::cout << "[count] 已丢弃积压扫描包 " << flushed << " 个" << std::endl;
{
std::lock_guard<std::mutex> lk(count_mtx);
if (count_stop) return;
count_start_tp = std::chrono::steady_clock::now(); // 排空完成才开始计时
}
while (true)
{
pavo2::pavo_packet_info_scan_t scan_info;
if (driver->get_scanned_data(scan_info, 100)) // 100ms超时轮询,可及时响应stop
{
auto now = std::chrono::steady_clock::now();
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - count_start_tp).count();
double z_val = (Speed_Z > 0)
? (Speed_Z * 10.0 * elapsed_ms / 1000.0) // cm/s × 10 = mm/s, × s = mm
: (double)elapsed_ms;
std::lock_guard<std::mutex> lk(count_mtx);
if (count_stop) break;
for (const auto& s : scan_info.vec_buff)
{
double angle_deg = s.angle / 100.0;
double angle_rad = angle_deg * 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 (is_origin_point(x_mm, y_mm)) continue;
if (FILTER_ENABLE && !point_in_polygon(x_mm, y_mm, Filter_Polygon, Filter_Poly_N)) continue;
count_points.push_back({x_mm, y_mm, z_val});
}
}
else
{
std::lock_guard<std::mutex> lk(count_mtx);
if (count_stop) break;
}
}
}
// 简易JSON字段提取(仅用于内部命令解析)
static bool json_get_string(const std::string& json, const std::string& key, std::string& out)
{
std::string pat = "\"" + key + "\"";
size_t pos = json.find(pat);
if (pos == std::string::npos) return false;
size_t colon = json.find(':', pos + pat.size());
if (colon == std::string::npos) return false;
size_t p = colon + 1;
while (p < json.size() && (json[p] == ' ' || json[p] == '\t')) p++;
if (p < json.size() && json[p] == '"')
{
size_t q = json.find('"', p + 1);
if (q == std::string::npos) return false;
out = json.substr(p + 1, q - p - 1);
return true;
}
return false;
}
static bool json_get_number(const std::string& json, const std::string& key, double& out)
{
std::string pat = "\"" + key + "\"";
size_t pos = json.find(pat);
if (pos == std::string::npos) return false;
size_t colon = json.find(':', pos + pat.size());
if (colon == std::string::npos) return false;
size_t p = colon + 1;
while (p < json.size() && (json[p] == ' ' || json[p] == '\t')) p++;
size_t end = p;
while (end < json.size() && (std::isdigit((unsigned char)json[end]) || json[end] == '.' ||
json[end] == '-' || json[end] == '+' || json[end] == 'e' || json[end] == 'E')) end++;
if (end == p) return false;
try { out = std::stod(json.substr(p, end - p)); return true; }
catch (...) { return false; }
}
// 参照Java drawPointsAndCalculateArea:点集 → 图像(8x8点)
// → 统计非白像素 → 换算实际面积
// 宽: 由 Filter_Polygon 多边形 X 最大最小值构成;高: 由 z 最大值构成(Z为运动方向,从0开始)
// 绘制平面取 (x_mm, z_val):X为宽度方向,Z为运动方向
// 返回白底图像(0=白,255=黑点),便于保存为JPG;area_out返回换算面积;out_w/out_h返回实际图像尺寸
static std::vector<uint8_t> points_to_image(const std::vector<CountPoint>& points,
double* area_out = nullptr,
int* out_w = nullptr,
int* out_h = nullptr)
{
const int MIN_DIM = 8; // 最小尺寸保护
const int MAX_DIM = 20000; // 单边最大尺寸保护(防止z过大导致内存爆炸)
if (area_out) *area_out = 0.0;
if (out_w) *out_w = 0;
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)
double polyXMin = 0.0, polyXMax = 0.0;
if (Filter_Poly_N > 0)
{
polyXMin = Filter_Polygon[0].first;
polyXMax = Filter_Polygon[0].first;
for (int i = 1; i < Filter_Poly_N; i++)
{
polyXMin = std::min(polyXMin, Filter_Polygon[i].first);
polyXMax = std::max(polyXMax, Filter_Polygon[i].first);
}
}
double spanX = polyXMax - polyXMin;
if (spanX <= 0.0) spanX = 1.0;
// 高: z 最大值直接构成 (Z=运动方向, 从0开始递增, 1单位=1px)
double maxZ = 0.0;
for (const auto& p : points) maxZ = std::max(maxZ, p.z_val);
if (maxZ <= 0.0) maxZ = 1.0;
int imageWidth = (int)std::lround(spanX);
int imageHeight = (int)std::lround(maxZ);
if (imageWidth < MIN_DIM) imageWidth = MIN_DIM;
if (imageHeight < MIN_DIM) imageHeight = MIN_DIM;
if (imageWidth > MAX_DIM) imageWidth = MAX_DIM;
if (imageHeight > MAX_DIM) imageHeight = MAX_DIM;
if (out_w) *out_w = imageWidth;
if (out_h) *out_h = imageHeight;
std::vector<uint8_t> image((size_t)imageWidth * imageHeight, 0); // 0=白底
// X坐标相对多边形左边界偏移到0;Z从0开始
double scaleX = (double)imageWidth / spanX; // ≈1: 宽直接由多边形X跨度构成
double scaleY = (double)imageHeight / maxZ; // ≈1: 高直接由z最大值构成
double xOffset = -polyXMin;
// 优化:Step1 先按1x1网格标记点位置(重复点只写一次),
// Step2 再对标记位置膨胀 Point_Dilate×Point_Dilate(与逐点画 Point_Dilate×Point_Dilate 数学等价,写次数大幅减少)
std::vector<uint32_t> marks;
marks.reserve(points.size() / 2);
for (const auto& p : points)
{
int px = (int)((p.x_mm + xOffset) * scaleX);
int py = (int)(p.z_val * scaleY);
if (px < 0 || px >= imageWidth || py < 0 || py >= imageHeight) continue;
size_t idx = (size_t)py * imageWidth + px;
if (image[idx] == 0) // 未标记过才记录
{
image[idx] = 1;
marks.push_back((uint32_t)idx);
}
}
for (uint32_t idx : marks) // 膨胀 Point_Dilate×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++)
{
int ix = px + dx, iy = py + dy;
if (ix < imageWidth && iy < imageHeight)
image[(size_t)iy * imageWidth + ix] = 255; // 255=黑点
}
}
int totalPixels = 0;
for (size_t i = 0; i < image.size(); i++)
if (image[i] != 0) totalPixels++;
double pixelToAreaRatio = spanX * maxZ / (double)(imageWidth * imageHeight);
if (area_out) *area_out = totalPixels * pixelToAreaRatio;
return image;
}
static double draw_points_calc_area(const std::vector<CountPoint>& points)
{
double area = 0.0;
points_to_image(points, &area);
return area;
}
// ==================== z轴空隙插值 ====================
// 雷达某段时间无数据(断流/瞬间丢包)时, z方向会出现空白带。
// 原理: 同一scan包(一圈)的所有点 z 相同, 正常圈间隔近似恒定(取相邻圈z差的中位数);
// 若某两个相邻圈的 z 差远大于正常间隔 → 判定为空隙, 用前/后圈数据补出虚拟圈。
// Z_Interpolate_Mode:
// none = 不插值
// CopyTranslation = 空隙内按正常间隔分层, 复制"前一圈"的点平移填充
// AngleBucketInterpolation = 空隙内以前/后两圈做"角度桶"插值生成虚拟圈(平滑过渡)
// 返回新增点数
static int z_gap_interpolate(std::vector<CountPoint>& points)
{
if (points.empty() || 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/圈
if (normal_gap <= 0) return 0;
// 空隙判定: 本条与上一条 z 差 > 2×正常间隔 视为断流空隙
const double gap_threshold = 2.0 * normal_gap;
// 条件1: 前后两条 Y 平均值差超过30mm 不插(可能跨层/换物体)
const double max_y_mean_diff = 30.0;
// 一个空隙最多补的行数
const int MAX_FILL_LEVELS = 10;
// 1. 按 z 升序排序
std::sort(points.begin(), points.end(),
[](const CountPoint& a, const CountPoint& b) { return a.z_val < b.z_val; });
// 2. 聚类成"圈"(同一scan包的点 z 完全相同, 用区间 [begin,end) 索引 points)
struct Circle { double z; size_t begin; size_t end; };
std::vector<Circle> circles;
{
size_t i = 0;
while (i < points.size())
{
size_t j = i + 1;
while (j < points.size() && points[j].z_val - points[i].z_val < 1e-6) ++j;
circles.push_back(Circle());
circles.back().z = points[i].z_val;
circles.back().begin = i;
circles.back().end = j;
i = j;
}
}
if (circles.size() < 2)
return 0;
auto circle_mean_y = [&](const Circle& c) -> double
{
double s = 0;
for (size_t p = c.begin; p < c.end; ++p) s += points[p].y_mm;
return s / (double)(c.end - c.begin);
};
int added = 0;
// 3. 逐空隙补行
for (size_t i = 1; i < circles.size(); ++i)
{
double gap = circles[i].z - circles[i - 1].z;
if (gap <= gap_threshold) // 未超过2×正常间隔, 不算空隙
continue;
const Circle& cA = circles[i - 1]; // 上一条
const Circle& cB = circles[i]; // 本条
// 条件1: 前后两条的Y平均值几乎相当(差≤30mm)才插值
if (std::fabs(circle_mean_y(cA) - circle_mean_y(cB)) > max_y_mean_diff)
continue;
// 条件2: 生成行的 X 范围 = 这两条的 X 最大和最小
double x_lo = points[cA.begin].x_mm, x_hi = points[cA.begin].x_mm;
for (size_t p = cA.begin; p < cA.end; ++p)
{
if (points[p].x_mm < x_lo) x_lo = points[p].x_mm;
if (points[p].x_mm > x_hi) x_hi = points[p].x_mm;
}
for (size_t p = cB.begin; p < cB.end; ++p)
{
if (points[p].x_mm < x_lo) x_lo = points[p].x_mm;
if (points[p].x_mm > x_hi) x_hi = points[p].x_mm;
}
// 空隙内补行数: 按正常间隔容纳行数-1(空隙两端已有), 最多10行
int n = (int)(gap / normal_gap + 0.5) - 1;
if (n < 1) continue;
if (n > MAX_FILL_LEVELS) n = MAX_FILL_LEVELS;
if (Z_Interpolate_Mode == "CopyTranslation")
{
// 复制"上一条"的点, z 均匀平移到空隙内的各补行
for (int k = 1; k <= n; ++k)
{
double zk = cA.z + gap * (double)k / (n + 1); // 均匀分布
for (size_t p = cA.begin; p < cA.end; ++p)
{
const CountPoint& src = points[p];
if (src.x_mm < x_lo || src.x_mm > x_hi) continue; // X范围约束
CountPoint np = src;
np.z_val = zk;
points.push_back(np);
++added;
}
}
}
else if (Z_Interpolate_Mode == "AngleBucketInterpolation")
{
// 角度桶插值: 每桶用前A/后B平均点线性插值(平滑过渡), 同样受Y平均/X范围条件约束
struct Buck { double sx, sy; int cnt; Buck() : sx(0), sy(0), cnt(0) {} };
std::vector<Buck> bA(360), bB(360);
auto fill = [&](const Circle& c, std::vector<Buck>& out)
{
for (size_t p = c.begin; p < c.end; ++p)
{
double deg = std::atan2(points[p].x_mm, -points[p].y_mm) * 57.29577951308232;
if (deg < 0) deg += 360.0;
int b = (int)(deg + 0.5);
if (b >= 360) b = 0;
out[b].sx += points[p].x_mm;
out[b].sy += points[p].y_mm;
out[b].cnt++;
}
};
fill(cA, bA);
fill(cB, bB);
for (int k = 1; k <= n; ++k)
{
double zk = cA.z + gap * (double)k / (n + 1); // 均匀分布
double t = (zk - cA.z) / gap; // 0~1
for (int b = 0; b < 360; ++b)
{
if (bA[b].cnt <= 0 || bB[b].cnt <= 0) continue;
double ax = bA[b].sx / bA[b].cnt, ay = bA[b].sy / bA[b].cnt;
double bx = bB[b].sx / bB[b].cnt, by = bB[b].sy / bB[b].cnt;
double nx = ax + (bx - ax) * t;
double ny = ay + (by - ay) * t;
if (nx < x_lo || nx > x_hi) continue; // X范围约束
CountPoint np;
np.x_mm = nx;
np.y_mm = ny;
np.z_val = zk;
points.push_back(np);
++added;
}
}
}
}
if (added > 0)
std::cout << "[count] z空隙插值: 模式=" << Z_Interpolate_Mode
<< ", 正常间隔=" << normal_gap
<< ", 判定阈值=2×间隔=" << gap_threshold
<< ", 新增" << added << "点" << std::endl;
return added;
}
// 参照Java getLongitudinalType:按层统计总数
// 高度 = Floor_Height - Y轴; level = round(高度/层高);
// 冗余: 第1层50mm, 每高一层+20mm; 从最高层(5)往下找有效层, 该层面积/(行×宽) 得该层个数
// detail: 可选输出每层点数/面积详情(供日志使用)
// top_layer_img/level/area: 可选输出被计数层(最高有效层)的图像/层号/面积
static int calc_item_count(const std::vector<CountPoint>& points, const CountParams& box,
std::string* detail = nullptr,
std::vector<uint8_t>* top_layer_img = nullptr,
int* top_layer_img_w = nullptr,
int* top_layer_img_h = nullptr,
int* top_layer_level = nullptr,
double* top_layer_area = nullptr)
{
if (points.empty() || box.l <= 0 || box.w <= 0 || box.h <= 0 || box.maxPerLayer <= 0)
return 0;
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))
continue;
double currentHeight = 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;
if (std::abs(currentHeight - level * box.h) <= tolerance)
layer_map[level].push_back(p);
}
// 每层面积/图像只算一次,详情与结果共用,避免重复构建图像
std::ostringstream oss;
std::unordered_map<int, double> layer_area;
std::unordered_map<int, std::vector<uint8_t>> layer_img;
std::unordered_map<int, std::pair<int, int>> layer_img_size;
for (int h = 5; h > 0; h--)
{
auto it = layer_map.find(h);
if (it == layer_map.end()) continue;
oss << " 层" << h << ": 点数=" << it->second.size();
// 高度范围: 该层算法对应的理论高度区间(即生成该层count_layer截取的高度范围) - 每层都显示
double tol = Count_Base_Tolerance + h * Count_Additional_Tolerance_Per_Level;
double h_lo = h * box.h - tol;
double h_hi = h * box.h + tol;
oss << ", 高度范围(" << std::setprecision(0) << h_lo << "," << h_hi << ")";
if (it->second.size() > 1000)
{
double area = 0.0;
int img_w = 0, img_h = 0;
std::vector<uint8_t> img = points_to_image(it->second, &area, &img_w, &img_h);
layer_area[h] = area;
layer_img[h] = std::move(img);
layer_img_size[h] = std::make_pair(img_w, img_h);
double item_cnt = area / (box.l * box.w); // 未取整的个数(面积/单件面积)
oss << ", 面积=" << std::fixed << std::setprecision(1) << area << "mm^2"
<< ", 个数" << std::setprecision(2) << item_cnt << "个";
}
oss << "\n";
}
int layersCount = box.maxPerLayer; // 每层最大个数
int result = 0;
int top_lv = 0;
double top_area = 0.0;
for (int h = 5; h > 0; h--) // 最多5层,从顶层往下找
{
auto it = layer_map.find(h);
if (it != layer_map.end() && it->second.size() > 1000)
{
double area = layer_area[h];
if (area > 0)
{
int i = (int)std::min(std::round(area / (box.l * box.w)), (double)layersCount);
if (i > 0)
{
result = layersCount * (h - 1) + i;
top_lv = h;
top_area = area;
break;
}
}
}
}
if (detail) *detail = oss.str();
// 输出被计数层(最高有效层)的图像/尺寸/层号/面积
if (top_layer_img && top_lv > 0)
{
auto it = layer_img.find(top_lv);
if (it != layer_img.end()) *top_layer_img = it->second;
auto sit = layer_img_size.find(top_lv);
if (sit != layer_img_size.end())
{
if (top_layer_img_w) *top_layer_img_w = sit->second.first;
if (top_layer_img_h) *top_layer_img_h = sit->second.second;
}
}
if (top_layer_level) *top_layer_level = top_lv;
if (top_layer_area) *top_layer_area = top_area;
return result;
}
// ==================== TCP服务端模式 ====================
// 收到 {"check":1} 触发一次检测(demo_collect_multi)
void handle_tcp_client(SOCKET client, pavo2::pavo2_driver* driver)
{
static std::mutex check_mutex; // 串行化检测,避免并发
char buf[1024];
std::string recv_buf;
while (true)
{
int n = recv(client, buf, sizeof(buf) - 1, 0);
if (n <= 0) break; // 连接关闭或出错
buf[n] = '\0';
recv_buf.append(buf, n);
// 按行处理消息
size_t pos;
while ((pos = recv_buf.find('\n')) != std::string::npos)
{
std::string line = recv_buf.substr(0, pos);
recv_buf.erase(0, pos + 1);
// 去空白后匹配命令: {"check":1}=触发检测, {"config":1}=重读配置
std::string cleaned;
for (char c : line)
if (c != ' ' && c != '\t' && c != '\r' && c != '\n') cleaned += c;
if (cleaned == "{\"config\":1}")
{
std::cout << "[TCP] 收到配置重载命令,重新读取 config/config ..." << std::endl;
load_config();
std::cout << "[TCP] 配置已重载: 采集=" << Collect_Duration_S << "s, 速度=" << Speed_Z
<< "cm/s, Z区间=" << Z_RANGE_MM << "mm, 帧率=" << Lidar_Hz
<< "Hz, 矩形宽=" << Rect_Width_PX << "px, 模式=" << Run_Mode
<< ", 端口=" << Tcp_Port << std::endl;
const char* ack = "{\"code\":200,\"message\":\"config reloaded\"}\n";
send(client, ack, (int)std::strlen(ack), 0);
}
else if (cleaned == "{\"check\":1}")
{
std::lock_guard<std::mutex> lk(check_mutex);
std::cout << "[TCP] 收到检测命令: " << line << ",开始检测..." << std::endl;
const char* ack = "{\"code\":200,\"message\":\"check received, detecting...\"}\n";
send(client, ack, (int)std::strlen(ack), 0);
DetectResult res;
demo_collect_multi(*driver, &res);
// 返回检测完成 + start/end 白色占比 + Y最大/中位数
std::ostringstream resp;
resp << "{\"code\":200,\"message\":\"detect done\"";
if (res.valid) {
resp << ",\"start\":{\"fill_pct\":"
<< std::fixed << std::setprecision(2) << res.start_fill_pct
<< ",\"y_max\":" << std::setprecision(1) << res.start_y_max
<< ",\"y_median\":" << res.start_y_med << "}";
resp << ",\"end\":{\"fill_pct\":"
<< std::fixed << std::setprecision(2) << res.end_fill_pct
<< ",\"y_max\":" << std::setprecision(1) << res.end_y_max
<< ",\"y_median\":" << res.end_y_med << "}";
}
resp << "}\n";
std::string resp_str = resp.str();
send(client, resp_str.c_str(), (int)resp_str.size(), 0);
std::cout << "[TCP] 检测完成,已返回结果。" << std::endl;
}
else if (cleaned.find("\"count\"") != std::string::npos)
{
// count 计数命令: {"count":"start","box":{"l":行mm,"w":宽mm,"h":层高mm,"maxPerLayer":每层最大个数},"task_id":"xxx"}
// {"count":"stop"}
std::string count_cmd;
if (!json_get_string(cleaned, "count", count_cmd))
{
const char* ack = "{\"code\":400,\"message\":\"count parse error\"}\n";
send(client, ack, (int)std::strlen(ack), 0);
continue;
}
if (count_cmd == "start")
{
CountParams params;
json_get_string(cleaned, "task_id", params.task_id);
json_get_number(cleaned, "l", params.l);
json_get_number(cleaned, "w", params.w);
json_get_number(cleaned, "h", params.h);
double d = 0.0;
if (json_get_number(cleaned, "maxPerLayer", d) || json_get_number(cleaned, "max", d))
params.maxPerLayer = (int)d;
{
std::lock_guard<std::mutex> lk(count_mtx);
if (count_running)
{
const char* ack = "{\"code\":400,\"message\":\"count already running\"}\n";
send(client, ack, (int)std::strlen(ack), 0);
continue;
}
count_params = params;
count_points.clear();
count_stop = false;
count_running = true;
count_thread = std::thread(count_collect_loop, driver);
}
std::cout << "[TCP] 收到count:start, task_id=" << params.task_id
<< ", box(l=" << params.l << ",w=" << params.w
<< ",h=" << params.h << ",maxPerLayer=" << params.maxPerLayer
<< "),开始扫描..." << std::endl;
std::ostringstream resp;
resp << "{\"code\":200,\"message\":\"count start\"";
if (!params.task_id.empty())
resp << ",\"task_id\":\"" << params.task_id << "\"";
resp << "}\n";
std::string resp_str = resp.str();
send(client, resp_str.c_str(), (int)resp_str.size(), 0);
}
else if (count_cmd == "stop")
{
std::string task_id;
json_get_string(cleaned, "task_id", task_id);
// 只有真正 start 过的 stop 才有效,否则返回 0
bool was_running = false;
{
std::lock_guard<std::mutex> lk(count_mtx);
if (count_running)
{
count_stop = true;
count_running = false;
was_running = true;
}
}
if (count_thread.joinable()) count_thread.join();
int total = 0;
size_t pt_cnt = 0;
std::string layer_detail;
CountParams params_for_log; // 本次任务的box参数(日志用)
std::vector<CountPoint> saved_points;
std::vector<uint8_t> top_img; // 被计数层的图像
int top_lv = 0;
int top_img_w = 0, top_img_h = 0;
double top_area = 0.0;
{
std::lock_guard<std::mutex> lk(count_mtx);
saved_points = count_points;
z_gap_interpolate(saved_points); // z轴空隙插值(none时不处理)
pt_cnt = saved_points.size();
params_for_log = count_params;
if (was_running && pt_cnt > 0 && count_params.l > 0 &&
count_params.w > 0 && count_params.h > 0 && count_params.maxPerLayer > 0)
total = calc_item_count(saved_points, count_params, &layer_detail,
&top_img, &top_img_w, &top_img_h,
&top_lv, &top_area);
// 清空遗留数据/参数,避免未start的stop复用旧数据重复计数
count_points.clear();
count_params = CountParams();
}
std::cout << "[TCP] 收到count:stop, task_id=" << task_id
<< ", 采集点数=" << pt_cnt << ", 计数结果=" << total << std::endl;
// 先返回结果,PCD/图片/日志放到后台线程保存,不阻塞响应
std::ostringstream resp;
resp << "{\"count\":" << total;
if (!task_id.empty())
resp << ",\"task_id\":\"" << task_id << "\"";
resp << "}\n";
std::string resp_str = resp.str();
send(client, resp_str.c_str(), (int)resp_str.size(), 0);
if (was_running && pt_cnt > 0)
{
// C++11 兼容:先 move 到局部变量,再按值捕获
std::vector<CountPoint> bg_points = std::move(saved_points);
std::vector<uint8_t> bg_img = std::move(top_img);
std::thread([bg_points, bg_img, params_for_log, layer_detail, task_id,
total, pt_cnt, top_lv, top_area,
top_img_w, top_img_h]() {
// 目录: data/日期/时间
time_t rawtime = std::time(nullptr);
struct tm* timeinfo = std::localtime(&rawtime);
char dir_buf[32];
std::strftime(dir_buf, sizeof(dir_buf), "data/%Y%m%d/%H%M%S", timeinfo);
std::string out_dir(dir_buf);
mkdir("data");
mkdir(out_dir.substr(0, out_dir.rfind('/')).c_str());
_mkdir(out_dir.c_str());
// PCD:y 换成高度(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 : bg_points)
{
xs.push_back(p.x_mm);
ys.push_back(Floor_Height - p.y_mm);
zs.push_back(p.z_val);
}
save_pcd_ascii(out_dir + "/count_data.pcd", xs, ys, zs);
std::cout << "[count] 点云已保存: " << out_dir << "/count_data.pcd ("
<< pt_cnt << " 点, Y=高度)" << std::endl;
// 图片1:全点X-Z平面图(白底黑点, 尺寸按实际X/Z跨度比例)
double all_area = 0.0;
int all_w = 0, all_h = 0;
std::vector<uint8_t> all_img = points_to_image(bg_points, &all_area, &all_w, &all_h);
if (save_jpg_gray(out_dir + "/count_points.jpg", all_w, all_h, all_img))
std::cout << "[count] 点云图已保存: " << out_dir << "/count_points.jpg ("
<< all_w << "x" << all_h << ", X-Z平面, 面积≈"
<< std::fixed << std::setprecision(1)
<< all_area << "mm^2)" << std::endl;
// 图片2:被计数层(最高有效层)图像(Java drawPointsAndCalculateArea语义)
if (top_lv > 0 && !bg_img.empty() && top_img_w > 0 && top_img_h > 0)
{
if (save_jpg_gray(out_dir + "/count_layer.jpg", top_img_w, top_img_h, bg_img))
std::cout << "[count] 有效层图已保存: " << out_dir << "/count_layer.jpg ("
<< top_img_w << "x" << top_img_h << ", 层" << top_lv
<< ", 面积≈" << std::fixed
<< std::setprecision(1) << top_area << "mm^2)" << std::endl;
}
// 日志
if (Log_Flag)
{
std::ofstream lf(out_dir + "/log.txt");
if (lf.is_open())
{
std::ostringstream sum;
sum << "==============================\n";
sum << "[count] task_id: " << task_id << "\n";
sum << "[count] box: l=" << params_for_log.l
<< ", w=" << params_for_log.w
<< ", h=" << params_for_log.h
<< ", maxPerLayer=" << params_for_log.maxPerLayer << "\n";
sum << "[count] Floor_Height: " << Floor_Height << " mm\n";
sum << "[count] 采集点数: " << pt_cnt << "\n";
sum << "[count] 计数层: 层" << top_lv
<< " (面积=" << std::fixed << std::setprecision(1)
<< top_area << "mm^2)\n";
sum << "[count] 分层情况:\n" << layer_detail;
sum << "[count] 计数结果: " << total << "\n";
sum << "[count] 输出文件:\n";
sum << " " << out_dir << "/count_data.pcd\n";
sum << " " << out_dir << "/count_points.jpg (" << all_w << "x" << all_h << ")\n";
if (top_lv > 0 && !bg_img.empty() && top_img_w > 0 && top_img_h > 0)
sum << " " << out_dir << "/count_layer.jpg ("
<< top_img_w << "x" << top_img_h << ")\n";
sum << "==============================\n";
lf << gbk_to_utf8(sum.str());
lf.close();
}
}
std::cout << "[count] 输出目录: " << out_dir << std::endl;
}).detach();
}
}
else
{
const char* ack = "{\"code\":400,\"message\":\"unknown count command\"}\n";
send(client, ack, (int)std::strlen(ack), 0);
}
}
}
}
closesocket(client);
}
void run_tcp_server(pavo2::pavo2_driver& driver)
{
std::cout << "\n========== TCP服务端模式 ==========" << std::endl;
std::cout << "[TCP] 监听端口: " << Tcp_Port << std::endl;
SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == INVALID_SOCKET)
{
std::cerr << "[错误] 创建监听socket失败!" << std::endl;
return;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(Tcp_Port);
if (bind(listen_sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR)
{
std::cerr << "[错误] 绑定端口 " << Tcp_Port << " 失败!" << std::endl;
closesocket(listen_sock);
return;
}
if (listen(listen_sock, 5) == SOCKET_ERROR)
{
std::cerr << "[错误] 监听失败!" << std::endl;
closesocket(listen_sock);
return;
}
std::cout << "[TCP] 初始化完成,等待检测命令 {\"check\":1} ..." << std::endl;
while (true)
{
SOCKET client = accept(listen_sock, nullptr, nullptr);
if (client == INVALID_SOCKET)
{
std::cout << "[TCP] accept失败,重试..." << std::endl;
continue;
}
std::thread(handle_tcp_client, client, &driver).detach();
}
}
// ==================== 主函数 ====================
int main()
{
#ifdef _WIN32
//SetConsoleOutputCP(CP_UTF8); 20260714临时去掉,应对乱码
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
{
std::cerr << "[错误] WSAStartup 初始化失败!" << std::endl;
return -1;
}
#endif
load_config(); //加载配置文件0713增加
std::cout << "===============config配置内容===============" << std::endl;
std::cout << " 雷达IP: " << Leida_IP << std::endl;
std::cout << " 端口: " << Leida_PORT << std::endl;
std::cout << " 采集时间: " << Collect_Duration_S << " s" << std::endl;
std::cout << " 运行速度: " << Speed_Z << " cm/s" << std::endl;
std::cout << " Z截面区间: " << Z_RANGE_MM << " mm" << std::endl;
std::cout << " Y截面区间: " << Y_RANGE_MM << " mm (从Y最小开始)" << std::endl;
std::cout << " 雷达帧率: " << Lidar_Hz << " Hz" << std::endl;
std::cout << " 矩形宽度: " << Rect_Width_PX << " px" << std::endl;
std::cout << " 矩形长倍率: " << Rect_Length_Ratio << std::endl;
std::cout << " 运行模式: " << Run_Mode << " (tcp端口: " << Tcp_Port << ")" << std::endl;
std::cout << " 日志使能: " << (Log_Flag ? "YES" : "NO") << std::endl;
std::cout << " 地板高度(Floor_Height): " << Floor_Height << " mm (物品高度=Floor_Height-Y轴)" << std::endl;
std::cout << "========================================" << std::endl;
std::cout << "========================================" << std::endl;
std::cout << " Pavo2 Lidar 雷达数据采集程序" << std::endl;
std::cout << " SDK版本: " << SDK_VER << std::endl;
std::cout << "========================================" << std::endl;
pavo2::pavo2_driver driver;
if (!connect_lidar(driver))
{
std::cerr << "\n[退出] 连接雷达失败,程序退出。" << std::endl;
std::cout << "请检查:" << std::endl;
std::cout << " 1. 雷达IP地址是否正确(当前: " << Leida_IP << ")" << std::endl;
std::cout << " 2. 本机与雷达的网络是否连通" << std::endl;
std::cout << " 3. 防火墙是否放行端口 " << Leida_PORT << std::endl;
#ifdef _WIN32
WSACleanup();
#endif
return -1;
}
std::cout << "\n[等待] 等待雷达数据稳定(2秒)..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
// --- 功能1:单帧 + 鸟瞰图 (已关闭) ---
// demo_single_frame(driver);
// --- 运行模式: exe=直接检测一次; tcp=服务端模式,收到 {"check":1} 触发检测 ---
if (Run_Mode == "tcp")
{
run_tcp_server(driver);
}
else
{
// --- 功能2:多秒采集 + 深度图 ---
demo_collect_multi(driver);
}
std::cout << "\n[关闭] 正在断开雷达连接..." << std::endl;
driver.pavo2_close();
#ifdef _WIN32
WSACleanup();
#endif
std::cout << "[完成] 程序运行结束。" << std::endl;
return 0;
}