[{"content":"程序设计实习报告 桂林理工大学 GUILIN UNIVERSITY OF TECHNOLOGY\n程序设计实践课程 实习报告\n学 院： 计算机科学与工程学院 # 班 级： ## 组 长： 组 员： 组 员： 组 员： 指导教师： 评 分/价：\n1.基础实践部分\n1.1. 编写程序，求n！ 1.1.1题目内容 编写程序，求n！(n的值要能大于13)，其结果用一个不超过64位的十进制数输出。\n1.1.2题目要求 【输入格式】 输入一个非负整数，其值介于2到49之间的数。 【输出格式】 对每一个输入的整数，在一行中输出相应的阶乘值，输出结果的高位用0填充。 【输入样例】 在这里给出一组输入。例如： 7 结尾无空行 【输出样例】 5040\n1.1.3设计思想 deepseek_mermaid_20260108_2d339a.png\n1.1.4算法分析 时间复杂度：O(n² log n) 空间复杂度：O(n log n) 1.1.5核心代码 // 编写程序,求n! #include #include #include #include using namespace std;\nstring calculate(int n) { if (n == 0 || n == 1) { return \u0026ldquo;1\u0026rdquo;; }\nvector\u0026lt;int\u0026gt; result; result.push_back(1); for (int i = 2; i \u0026lt;= n; i++) { int carry = 0; // 将每个结果乘以i for (int j = 0; j \u0026lt; result.size(); j++) { int product = result[j] * i + carry; result[j] = product % 10; carry = product /= 10; } // 处理剩余进位 while (carry \u0026gt; 0) { result.push_back(carry % 10); carry /= 10; } } // 高位填充0 for (int i = 0 ; i \u0026lt; 64 - result.size(); i++) { result.push_back(0); } // 转换为0 string res; reverse(result.begin(),result.end()); for (int i = 0; i \u0026lt; result.size(); i++) { res += to_string(result[i]); } return res; }\nint main() { int n; cin \u0026raquo; n;\nstring result = calculate(n); cout \u0026lt;\u0026lt; result \u0026lt;\u0026lt; endl; }\n1.1.6测试数据或截图 image.png\n1.1.7心得体会 在本次程序设计实习中，我通过实现大数阶乘计算，深入理解了数组模拟高精度运算的原理和实现细节，锻炼了问题分解和逻辑思维能力。\n1.2 找最大数和最小数 1.2.1题目内容 本题目要求读入n个整数，找到最大数和最小数，并输出结果。\n1.2.2题目要求 【输入格式】 例如当n=4时，输入给出4个绝对值不超过1000的整数A、B、C、D，以空格分隔。 【输出格式】 最大数和最小数，“max=?,min=?”。 【输入样例】 在这里给出一组输入。例如： 18 98 59 25 【输出样例】 在这里给出相应的输出。例如： max=98, min=18\n1.2.3设计思想\ndeepseek_mermaid_20260108_33f2cf.png\n1.2.4算法分析 时间复杂度：O(n) 空间复杂度：O(n)\n1.2.5核心代码 // 找最大数和最小数 #include #include #include #include using namespace std;\nint main() { int n; cin \u0026raquo; n; vector nums(n); cin \u0026raquo; nums[0]; int max_num = nums[0]; int min_num = nums[0]; for (int i = 1; i \u0026lt; n; i++) { cin \u0026raquo; nums[i]; max_num = max(max_num,nums[i]); min_num = min(min_num,nums[i]); }\ncout \u0026lt;\u0026lt; \u0026quot;max_num=\u0026quot; \u0026lt;\u0026lt; max_num \u0026lt;\u0026lt; \u0026quot;min_num=\u0026quot; \u0026lt;\u0026lt; min_num; }\n1.2.6测试数据或截图 image.png\n1.2.7心得体会 初次编写查找最值程序时，我意识到在循环中同时更新最大最小值能有效减少遍历次数，对时间复杂度有了更直观的理解，也学会了注意数组边界等细节\n1.3 统计字母出现频率 1.3.1题目内容 输入一段文字（以回车结束），统计其中每个字母出现的频率。\n1.3.2题目要求 【输入格式】 一段文字（以回车结束） 【输出格式】 统计结果（包括次数和百分比，并显示条状图，参见输出样例） 【输入样例】 This is a pen. That is a pencil. 【输出样例】 A: 3 13.0% ************* C: 1 4.3% **** E: 2 8.7% ********* H: 2 8.7% ********* I: 4 17.4% ***************** L: 1 4.3% **** N: 2 8.7% ********* P: 2 8.7% ********* S: 3 13.0% ************* T: 3 13.0% *************\n1.3.3设计思想 deepseek_mermaid_20260108_bb824e.png\n1.3.4算法分析 时间复杂度：O(n) 空间复杂度：O(1)\n1.3.5核心代码 // 统计字母出现频率 #include #include #include #include #include using namespace std;\nint main() { string str_input; cin \u0026raquo; str_input; vector cnt(26); for (int i = 0; i \u0026lt; str_input.size(); i++) { cnt[str_input[i] - \u0026lsquo;a\u0026rsquo;] += 1; } int total = 0; for (int i = 0; i \u0026lt; cnt.size(); i++) { total += cnt[i]; } for (int i = 0; i \u0026lt; cnt.size(); i++) { // 输出统计对象 cout \u0026laquo; char(\u0026lsquo;A\u0026rsquo; + i) \u0026laquo; \u0026quot; \u0026ldquo;; // 输出统计个数 cout \u0026laquo; cnt[i] \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; endl;\n// 输出百分比 double temp = (cnt[i] * 100) / (double)total; // 限制小数点两位 cout \u0026lt;\u0026lt; fixed \u0026lt;\u0026lt; setprecision(2) \u0026lt;\u0026lt; temp \u0026lt;\u0026lt; \u0026quot;%\u0026quot;; // 输出条状图 int each = (cnt[i] * 100) / total ; if ((cnt[i] * 100) / total \u0026gt; 0) { each += 1; } for (int i = 0; i \u0026lt; each; i++) { cout \u0026lt;\u0026lt; \u0026quot;*\u0026quot;; } cout \u0026lt;\u0026lt; endl; } }\n1.3.6测试数据或截图 image.png\n1.3.7心得体会 通过这次字母频率统计程序的编写，我学会了如何将抽象的数据统计需求转化为具体代码逻辑，掌握了数组映射和格式化输出的技巧，意识到程序不仅要关注正确性，输出格式的可读性也同样重要。\n1.4最小公倍数 1.4.1题目内容 请编写程序，输入两个整数，计算并输出它们的输出最小公倍数。\n1.4.2题目要求 【输入格式】 两个整数 【输出格式】 最小公倍数(正整数) 说明：两个整数可以是正数、零和负数。最小公倍数必须是自然数。题目保证两个整数及其最小公倍数的绝对值都小于2(63) 。 【输入样例】 935761 -5128800173759 【输出样例】 4799331179396895599\n1.4.3设计思想 deepseek_mermaid_20260108_05f056.png\n1.4.4算法分析 时间复杂度：O(log(min(a, b))) 空间复杂度：O(1)\n1.4.5核心代码 // 最小公倍数 // 辗转相除法: // lcm: 最小公倍数 gcd: 最大公约数 // LCM(a, b) = |a × b| ÷ GCD(a, b) #include #include #include #include #include using namespace std;\n// 求解最大公约数 long long gcd(long long a, long long b) { // 反复用余数替换原数相除，直至整除，最后的除数即为最大公约数。 while(b != 0) { long long temp = b; b = a % b; a = temp; } return a; }\n// 求解最小公约数 long long lcm(long long a, long long b) { // 防止溢出: 先除最大公约数再乘 return a / gcd(a, b) * b; }\nint main() { long long num1,num2; cin \u0026raquo; num1 \u0026raquo; num2; long long gcd_num = gcd(num1,num2); long long lcm_num = lcm(num1,num2); cout \u0026laquo; lcm_num \u0026laquo; endl; }\n1.4.6测试数据或截图 image.png\n1.4.7心得体会 通过这个程序，我明白了数学算法在编程中的重要性。辗转相除法求最大公因数的方法简洁高效，而最小公倍数与最大公因数的数学关系让我领略到算法设计的巧妙。同时，先除后乘的顺序处理让我注意到整数溢出的预防，这些都是很实用的编程经验。\n1.5学车费用 1.5.1题目内容 小华学开车后，才发现他的教练对不同的学员收取不同的费用。小华想分别对他所了解到的学车同学的各项费用进行累加求出总费用，然后按下面的排序规则排序并输出，以便了解教练的收费情况。排序规则： 先按总费用从多到少排序，若总费用相同则按姓名的ASCII码序从小到大排序，若总费用相同而且姓名也相同则按编号（即输入时的顺序号，从1开始编）从小到大排序。\n1.5.2题目要求 【输入格式】 测试数据有多组，处理到文件尾。每组测试数据先输入一个正整数n（n≤20），然后是n行输入，第i行先输入第i个人的姓名（长度不超过10个字符，且只包含大小写英文字母），然后再输入若干个整数（不超过10个），表示第i个人的各项费用，数据之间都以一个空格分隔，第i行输入的编号为i。输入数据和结果均在32位int型范围之内。 【输出格式】 对于每组测试，在按描述中要求的排序规则进行排序后，按顺序逐行输出每个人费用情况，包括：费用排名（从1开始，费用相同则排名也相同）、编号、姓名、总费用。每行输出的数据之间留1个空格。 【输入样例】 3 Tim 2800 900 2000 500 600 Lucy 3800 400 1500 300 Tim 6700 100\n【输出样例】 1 1 Tim 6800 1 3 Tim 6800 3 2 Lucy 6000\n1.5.3设计思想 deepseek_mermaid_20260108_9eac00.png\n1.5.4算法分析 时间复杂度接近O(n × m) 空间复杂度高：O(n × m)\n1.5.5核心代码\n// 学车费用 // 排序规则： 先按总费用从多到少排序，若总费用相同则按姓名的ASCII码序从小到大排序， // 若总费用相同而且姓名也相同则按编号（即输入时的顺序号，从1开始编）从小到大排序。\n/* 知识点补充: stringstream 是C++中一个强大的流类,用于字符串的输入输出操作 #include // 必须包含这个头文件 stringstream：既可读又可写 istringstream：只读（从字符串读取） ostringstream：只写（写入字符串）\nstringstream 是一个字符串流，它把字符串当作一个连续的字符序列， 并维护一个内部指针来跟踪当前读取位置。 提取运算符 \u0026gt;\u0026gt;：根据目标类型读取数据 */\n#include #include #include #include #include #include using namespace std;\nclass Student { public: string m_name; vector m_prices; int m_total; int m_id; Student() : m_total(0) , m_id(0) {} };\nstatic bool compare(Student stu1, Student stu2) { // 如果费用相等,按照姓名排序 if (stu1.m_total == stu2.m_total) { // 如果姓名也相同,按照输入顺序排序 if (stu1.m_name == stu2.m_name) { return stu1.m_id \u0026lt; stu2.m_id; } return stu1.m_name \u0026lt; stu2.m_name; } else { // 费用由多到少排序 return stu1.m_total \u0026gt; stu2.m_total; } }\nint main() { int n; cin \u0026raquo; n; cin.ignore(); // 忽略第一行后面的换行符\n// 读取并且创建学生类 vector\u0026lt;Student\u0026gt; students(n); for (int i = 0; i \u0026lt; n; i++) { string line; getline(cin, line); // 读取整行 stringstream ss(line); // 读取id students[i].m_id = i + 1; // 读取姓名 string name; ss \u0026gt;\u0026gt; name; students[i].m_name = name; // 读取各个费用 // 这里读取到回车就终止 int price; while(ss \u0026gt;\u0026gt; price) { students[i].m_prices.push_back(price); } // 计算费用总和 int total = 0; for (int j = 0; j \u0026lt; students[i].m_prices.size(); j++) { total += students[i].m_prices[j]; } students[i].m_total = total; } sort(students.begin(),students.end(),compare); for (int j = 0; j \u0026lt; students.size(); j++) { // 输出排名 cout \u0026lt;\u0026lt; j+1 \u0026lt;\u0026lt; \u0026quot; \u0026quot;; // 输出姓名 cout \u0026lt;\u0026lt; students[j].m_name \u0026lt;\u0026lt; \u0026quot; \u0026quot;; // 输出总费用 cout \u0026lt;\u0026lt; students[j].m_total \u0026lt;\u0026lt; endl; } }\n1.5.6测试数据或截图 image.png\n1.5.7心得体会 通过编写学车费用统计排序程序，我掌握了多条件自定义排序的设计方法。学会了使用stringstream处理混合数据类型的输入，理解了类设计在组织复杂数据时的优势。最重要的是，意识到程序不仅要实现功能，还要考虑数据的完整性和排序的精确性。\n1.6求n个整数的平均值与中位数 1.6.1题目内容 从键盘接收一个整数n，假定用户输入的n一定是满足3 \u0026lt;= n \u0026lt;= 100。接下来，从键盘接收n个整数存入数组。用户输入的整数，大小是杂乱无序的。 计算这n个数的平均值和中位数。中位数就是数组元素升序排列后，最中间的一个数(奇数个元素)，或中间两个元素平均值(偶数个元素)。\n1.6.2题目要求 【输入格式】 第一个整数4告诉计算机要输入4个数字。第二行输入这四个数字，数字之间用空格分开。 4 3 1 2 9 【输出格式】 平均值保留2位小数，中位数保留1位小数。两项信息之间用纯英文逗号隔开，整个输出信息中不含空格。 mean=3.75, median=2.5\n1.6.3设计思想 deepseek_mermaid_20260108_a9e34f.png\ndeepseek_mermaid_20260108_43a706.png\n1.6.4算法分析 需要存储n个整数的数组：O(n) 其他变量占用常数空间：O(1)\n1.6.5核心代码\n// 求n个整数的平均值与中位数 #include #include #include #include #include using namespace std;\nint main() { int n; cin \u0026raquo; n; vector nums(n); int total = 0; for (int i = 0; i \u0026lt; n; i++) { cin \u0026raquo; nums[i]; total += nums[i]; } sort(nums.begin(),nums.end()); int len = nums.size(); // 求中位数 int mid_num = 0; if (len % 2 == 1) { mid_num = nums[len/2]; } else { mid_num = nums[len/2] + nums[(len/2)-1]; }\ndouble avg = (double)total / (double)n; cout \u0026lt;\u0026lt; \u0026quot;mean=\u0026quot; \u0026lt;\u0026lt; avg \u0026lt;\u0026lt; \u0026quot;median=\u0026quot; \u0026lt;\u0026lt; mid_num \u0026lt;\u0026lt; endl; }\n1.6.6测试数据或截图 image.png\n1.6.7心得体会 通过编写这个求平均值和中位数的程序，我掌握了数据统计的基本方法。排序函数的使用让我明白了预处理数据的重要性，同时也注意到整数除法和浮点数除法的区别。程序虽然简单，但让我对数组操作和条件判断有了更深的理解。\n1.7天才婴儿 1.7.1题目内容 育婴室里有从1到n编号的n个婴儿。有一天，有一个科研团队来到育婴室，科研团队认为某个婴儿的编号的值减去这个编号每一位数字的和代表一个婴儿的智商，他们同时有个智商衡量标准k，若某个婴儿的智商不小于k，那么科研团队就认为这个婴儿是天才。例如，编号为12的婴儿，他的智商应为12−(1+2)，即为9。科研团队想请你求出育婴室里有多少天才婴儿。\n1.7.2题目要求 【输入格式】 输入包含两个整数n,k，分别表示婴儿的个数和科研团队的智商衡量标准。 【输出格式】 输出一个整数，表示育婴室里天才婴儿的数量。 【输入样例】 23 8 【输出格式】 14 【评测数据规模】 对于所有评测数据，1≤n，k≤10(9)。\n1.7.3设计思想 deepseek_mermaid_20260108_39a1da.png\n1.7.4算法分析 空间复杂度：O(1)（常数空间） 时间复杂度：O(n log n) 或更精确地说 O(n × d)，其中 d 是 n 的位数\n1.7.5核心代码\n// 天才婴儿 #include #include #include #include #include using namespace std;\nint main() { int n,k; cin \u0026raquo; n \u0026raquo; k; int cnt = 0; for (int i = 1; i \u0026lt;= n; i++) { int num = i; int temp = 0; while(num \u0026gt; 0) { temp += num%10; num /= 10; } int k0 = i - temp; if (k0 \u0026gt; k) { cnt++; } } cout \u0026laquo; cnt; }\n1.7.6测试数据或截图 image.png\n1.7.7心得体会 通过这个数字特性统计程序，我掌握了数字分离求和的技巧。在遍历1到n的过程中，不仅学会了用取余和整除逐位提取数字，也加深了对条件判断的理解。虽然题目简单，但让我意识到细心处理边界情况的重要性。\n1.8金币支付 1.8.1题目内容 小凯手中有两种面值的金币，两种面值均为正整数且彼此互素。每种金币小凯都有无数个。在不找零的情况下，仅凭这两种金币，有些物品他是无法准确支付的。现在小凯想知道在无法准确支付的物品中，最贵的价值是多少金币？注意：输入数据保证存在小凯无法准确支付的商品。输入数据仅一行，包含两个正整数a和b，它们之间用一个空格隔开，表示小凯手中金币的面值。其中，1≤a，b≤109\n1.8.2题目要求 【输入格式】 输出仅一行，一个正整数N，表示不找零的情况下，小凯用手中的金币不能准确支付的最贵的物品的价值。 【输入样例】 3 7 【输出样例】 11\n1.8.3设计思想 deepseek_mermaid_20260108_e5415c.png\n1.8.5核心代码\n// 金币支付 -\u0026gt; 用a和b的线性组合不能被表示的最大整数 // 由于a和b互质, // 任何大于等于(a-1)(b-1)的整数都可以表示为a和b的非负整数线性组合 // 而ab - a - b正好等于(a-1)*(b-1) - 1,无法被表示,且是最大的这样的数 // 两个互质数，不能凑出的最大数 = 两数乘积减去两数之和 #include #include #include #include #include using namespace std;\nint main() { int a,b; cin \u0026raquo; a \u0026raquo; b;\ncout \u0026lt;\u0026lt; a*b - a - b; }\n1.8.6测试数据或截图 image.png\n1.8.7心得体会 通过这道题，我深刻体会到数学思维在编程中的重要性。看似复杂的线性组合问题，运用互质数的性质只需一个简洁公式就能解决，这让我认识到算法优化往往源于对问题本质的深入理解。\n1.9鱼 1.9.1题目内容 在平面坐标系上给定 n 个不同的整点（也即横坐标与纵坐标皆为整数的点）。我们称从这 n 个点中选择 6 个不同的点所组成的有序六元组 \u0026lt;A,B,C,D,E,F\u0026gt; 是一条「鱼」，当且仅当：AB=AC,BD=CD,DE=DF（身形要对称），并且 ∠BAD,∠BDA 与 ∠CAD,∠CDA 都是锐角（脑袋和屁股显然不能是凹的），∠ADE,∠ADF 大于 90°（也即为钝角或平角，为了使尾巴不至于翘那么别扭）。 image.png\n其中点的组成相同，但顺序不同的鱼视为不同的鱼，即 \u0026lt;A,B,C,D,E,F\u0026gt; 和 \u0026lt;A,C,B,D,E,F\u0026gt; 视为不同的两条鱼（毕竟鱼也有背和肚子的两面），同理 \u0026lt;A,B,C,D,E,F\u0026gt; 和 \u0026lt;A,B,C,D,F,E\u0026gt; 也可以视为不同的两条鱼（假设鱼尾巴可以打结）。 问给定的 n 个点可以构成多少条鱼。注意：数据保证 n 个点互不重复。\n1.9.2题目要求 【输入格式】 第一行一个正整数 n ，代表平面上点的个数。 接下来 n 行每行两个整数 x,y ，代表点的横纵坐标。 【输出格式】 输出一行一个非负整数，代表鱼的个数。 【输入样例】 8 -2 0 -1 0 0 1 0 -1 1 0 2 0 3 1 3 -1 【输出样例】 16\n1.9.3设计思想 deepseek_mermaid_20260108_2376e4.png\n1.9.4算法分析 最坏时间复杂度：O(n⁶) 最坏空间复杂度：O(n⁶)（结果存储）\n1.9.5核心代码 // 鱼 #include #include #include #include #include #include \u0026lt;unordered_map\u0026gt; using namespace std;\nclass Point { public: Point() {} Point(int x,int y) : m_x(x),m_y(y){} int m_x,m_y; };\n// 计算两个向量的平方距离 int get_distance_sqrt(Point a, Point b) { int abx = b.m_x - a.m_x; int aby = b.m_y - a.m_y; return abxabx + abyaby; }\nint dotProduct(Point A, Point B, Point C) { // 计算向量BA与向量BC的点积 int BAx = A.m_x - B.m_x; int BAy = A.m_x - B.m_x; int BCx = C.m_x - B.m_x; int BCy = C.m_x - B.m_x; return BAx * BCx + BAy * BCy; }\nint main() { int n; cin \u0026raquo; n; vector Points(n); for (int i = 0; i \u0026lt; n; i++) { int x,y; cin \u0026raquo; x \u0026raquo; y; Points[i] = Point(x,y); }\nvector\u0026lt;unordered_map\u0026lt;char,Point\u0026gt;\u0026gt; result; // 暴力查找每一个可以构成鱼的六元组 for (int a = 0; a \u0026lt; n; a++) { for (int b = 0; b \u0026lt; n; b++) { if (b == a) { continue; } for (int c = 0; c \u0026lt; n; c++) { if (c == a || c == b) { continue; } if (get_distance_sqrt(Points[a],Points[b]) != get_distance_sqrt(Points[a],Points[c])) { continue; } for (int d = 0; d \u0026lt; n; d++) { if (d == a || d == b || d == c){ continue; } if (get_distance_sqrt(Points[b],Points[d]) != get_distance_sqrt(Points[c],Points[d])) { continue; } for (int e = 0; e \u0026lt; n; e++) { if (e == a || e == b || e == c || e == d) { continue; } for (int f = 0; f \u0026lt; n; f++) { if (f == a || f == b || f == c || f == d || f == e) { continue; } if (get_distance_sqrt(Points[d],Points[e]) != get_distance_sqrt(Points[d],Points[f])) { continue; } // 角BAD,角BDA,角CAD,角CDA都是锐角 // 角ADE,角ADF大于90度 if (dotProduct(Points[b],Points[a],Points[d]) \u0026gt; 0 \u0026amp;\u0026amp; dotProduct(Points[b],Points[d],Points[a]) \u0026gt; 0 \u0026amp;\u0026amp; dotProduct(Points[c],Points[a],Points[d]) \u0026gt; 0 \u0026amp;\u0026amp; dotProduct(Points[c],Points[d],Points[a]) \u0026gt; 0 \u0026amp;\u0026amp; dotProduct(Points[a],Points[d],Points[e]) \u0026lt; 0 \u0026amp;\u0026amp; dotProduct(Points[a],Points[d],Points[f]) \u0026lt; 0 ) { // 维护可以构成鱼的六元组 unordered_map\u0026lt;char,Point\u0026gt; umap; umap.insert(make_pair('A',Points[a])); umap.insert(make_pair('B',Points[b])); umap.insert(make_pair('C',Points[c])); umap.insert(make_pair('D',Points[d])); umap.insert(make_pair('E',Points[e])); umap.insert(make_pair('F',Points[f])); result.push_back(umap); } } } } } } } cout \u0026lt;\u0026lt; result.size(); }\n1.9.6测试数据或截图 image.png\n1.9.7心得体会 通过这个复杂的几何形状检测程序，我深刻体会到暴力枚举算法的局限性。六重循环虽然直观但效率低下，这让我意识到必须寻找更优化的算法设计。同时，向量点积计算几何角度的方法让我对数学在图形处理中的应用有了新认识，也学会了如何通过条件剪枝减少不必要的计算。\n1.10迷你搜索引擎 1.10.1题目内容 实现一种简单的搜索引擎功能，快速满足多达10 (5)\n1.10.2题目要求 【输入格式】 输入首先给出正整数 N（≤ 100），为文件总数。随后按以下格式给出每个文件的内容：第一行给出文件的标题，随后给出不超过 100 行的文件正文，最后在一行中只给出一个字符 #，表示文件结束。每行不超过 50 个字符。在 N 个文件内容结束之后，给出查询总数 M（≤10(5)），随后 M 行，每行给出不超过 10 个英文单词，其间以空格分隔，每个单词不超过 10 个英文字母，不区分大小写。 【输出格式】 针对每一条查询，首先在一行中输出包含全部该查询单词的文件总数；如果总数为 0，则输出 Not Found。如果有找到符合条件的文件，则按输入的先后顺序输出这些文件，格式为：第1行输出文件标题；随后顺序输出包含查询单词的那些行内容。注意不能把相同的一行重复输出。 【输入样例】 4 A00 Gold silver truck\nA01 Shipment of gold damaged in a fire\nA02 Delivery of silver arrived in a silver truck\nA03 Shipment of gold arrived in a truck\n2 what ever silver truck 【输出样例】 0 Not Found 2 A00 silver truck A02 of silver a silver truck\n1.10.3设计思想 deepseek_mermaid_20260108_e3ba21.png\n1.10.4算法分析 时间复杂度：\nN: 文件数量 L: 每个文件的平均行数 W: 每行的平均单词数（去重后） C: 每个单词的平均长度 M: 查询数量 Q: 每个查询的平均词数 R: 每个查询的平均结果文件数 P: 每个文件中的平均相关行数\n索引构建： O(N×L×W)\n查询处理： 平均: O(Q×R + P log P) 最坏: O(Q×N + L log L)\n空间复杂度：\n索引构建： O(T)\n单个查询： O(N+Q+P)\n内存使用： O(N×L×C + T)\n1.10.5核心代码\n// 迷你搜索引擎 #include #include #include #include #include \u0026lt;unordered_map\u0026gt; #include \u0026lt;unordered_set\u0026gt; #include #include using namespace std;\nclass File{ public: string title; vector text;\nFile() {} // 逐行添加文件内容 void addLine(const string\u0026amp; line) { text.push_back(line); } // 返回文件总行数 int getLineCount() const { return text.size(); } // 获取指定行的内容 string getLine(int lineNum) const { if (lineNum \u0026gt;= 0 \u0026amp;\u0026amp; lineNum \u0026lt; text.size()) { return text[lineNum]; } return \u0026quot;\u0026quot;; } };\n// 将大写转换为小写 string toLower(const string\u0026amp; s) { string result = s; transform(result.begin(),result.end(),result.begin(),::tolower); return result; }\n// 分割一行文本为单词 vector splitLine(const string\u0026amp; line) { vector words; stringstream ss(toLower(line)); string word; while(ss \u0026raquo; word) { words.push_back(word); } return words; }\nint main() { // 读取N个文件文件 int N; cin \u0026raquo; N; cin.ignore(); // 忽略换行符\nvector\u0026lt;File\u0026gt; Files(N); // 建立倒排索引 // 索引结果: 单词 -\u0026gt; 文件索引集合 unordered_map\u0026lt;string, unordered_map\u0026lt;int, unordered_set\u0026lt;int\u0026gt;\u0026gt;\u0026gt; invertedIndex; // 读取每个文件 for (int fileIdx = 0; fileIdx \u0026lt; N; fileIdx++) { // 读取文件标题 string name; getline(cin,name); Files[fileIdx].title = name; // 读取文件内容 int lineNum = 0; // 当前行号 while(true) { string line; getline(cin,line); if (line == \u0026quot;#\u0026quot;) { break; } // 逐行存储文件内容 Files[fileIdx].addLine(line); // 处理当前行的单词,建立索引 vector\u0026lt;string\u0026gt; words = splitLine(line); // 去重: 同一行的同一个单词只记录一次 unordered_set\u0026lt;string\u0026gt; uniqueWords(words.begin(), words.end()); // 更新倒排索引 for (const string\u0026amp; word : uniqueWords) { invertedIndex[word][fileIdx].insert(lineNum); } lineNum++; } } // 读取M个查询 int M; cin \u0026gt;\u0026gt; M; cin.ignore(); // 忽略换行符 // 处理每个查询 for (int i = 0; i \u0026lt; M; i++) { string query; getline(cin, query); // 分割查询词 vector\u0026lt;string\u0026gt; queryWords = splitLine(query); if (queryWords.empty()) { cout \u0026lt;\u0026lt; \u0026quot;0\\nNot Found\\n\u0026quot;; continue; } // 1.找到包含所有查询词的文件 unordered_set\u0026lt;int\u0026gt; commonFiles; // 包含所有查询词的文件索引 // 初始化: 用第一个查询词的文件集合 if (invertedIndex.count(queryWords[0])) { for (const auto\u0026amp; entry : invertedIndex[queryWords[0]]) { commonFiles.insert(entry.first); // entry.first是文件索引 } } // 取交集: 确保文件包含所有查询词 for (size_t i = 1; i \u0026lt; queryWords.size(); i++) { const string\u0026amp; word = queryWords[i]; if (!invertedIndex.count(word)) { // 如果某个词在任何文件中都不存在，交集为空 commonFiles.clear(); break; } unordered_set\u0026lt;int\u0026gt; currentFiles; for (const auto\u0026amp; entry : invertedIndex[word]) { currentFiles.insert(entry.first); } // 取交集 unordered_set\u0026lt;int\u0026gt; newCommonFiles; for (int fileIdx : commonFiles) { if (currentFiles.count(fileIdx)) { newCommonFiles.insert(fileIdx); } } commonFiles = newCommonFiles; } // 2. 输出符合条件的文件数 cout \u0026lt;\u0026lt; commonFiles.size() \u0026lt;\u0026lt; endl; // 如果没有符合条件的文件则输出未找到 if (commonFiles.empty()) { cout \u0026lt;\u0026lt; \u0026quot;Not Found\\n\u0026quot;; } else { // 如果有符合条件的文件: 按文件输入顺序输出（0到N-1） for (int fileIdx = 0; fileIdx \u0026lt; N; fileIdx++) { if (commonFiles.count(fileIdx)) { // 输出文件标题 cout \u0026lt;\u0026lt; Files[fileIdx].title \u0026lt;\u0026lt; endl; //收集相关行号 unordered_set\u0026lt;int\u0026gt; relevantLines; for (const string\u0026amp; word : queryWords) { if (invertedIndex.count(word) \u0026amp;\u0026amp; invertedIndex[word].count(fileIdx)) { const unordered_set\u0026lt;int\u0026gt;\u0026amp; lines = invertedIndex[word][fileIdx]; relevantLines.insert(lines.begin(), lines.end()); } } // 将行号转换为向量并排序（按行号顺序输出） vector\u0026lt;int\u0026gt; sortedLines(relevantLines.begin(), relevantLines.end()); sort(sortedLines.begin(), sortedLines.end()); // 输出包含查询词的行 for (int lineNum : sortedLines) { cout \u0026lt;\u0026lt; Files[fileIdx].getLine(lineNum) \u0026lt;\u0026lt; endl; } } } } } }\n1.10.6测试数据或截图 image.png\n1.10.7心得体会 通过这次迷你搜索引擎的实现，我掌握了倒排索引这一核心数据结构的设计与应用。从文本预处理、大小写转换到多关键词交集查询，每个环节都锻炼了我的工程能力。更重要的是，我体会到设计一个高效检索系统需要综合考虑数据结构、算法效率和实际应用场景的平衡。\n2.进阶实践部分 2.1整数因子分解问题 2.1.1题目内容 大于1的正整数n可以分解为： 转word后抄 例如若n=12，共用8种不同的分解式： 转word后抄 对于给定的正整数n，编程计算n有多少种不同的分解式。\n2.1.2题目要求 【输入】：数据有多行，给出正整数n 1 \u0026lt;= n \u0026lt;= 2000000000; 【输出】：每个数据输出一行，是正整数n的不同的分解式数量。 【输入样例】： 12 35 【输入样例】 8 3\n2.1.3设计思想 deepseek_mermaid_20260108_5fc30e.png\n2.1.4算法分析 时间复杂度：O(n * sqrt(n)) 空间复杂度：O(sqrt(n))\n2.1.5核心代码\n// #include // #include // #include // using namespace std;\n// map\u0026lt;int, int\u0026gt; memo; // 记忆化，避免重复计算 // int solve(int num) { // if (num == 1) return 1;\n// // 如果已经计算过，直接返回 // if (memo.find(num) != memo.end()) return memo[num];\n// int count = 1; // n本身是一种分解\n// for (int i = 2; i \u0026lt;= num / 2; i++) { // if (num % i == 0) { // count += solve(i); // } // }\n// // 保存结果到记忆化表 // memo[num] = count; // return count; // }\n// int main() { // int n; // while(cin \u0026raquo; n) { // cout \u0026laquo; solve(n) \u0026laquo; endl; // } // return 0; // }\n// 记忆化搜索 // 终止条件: 当num递归到1时,当num是质数时 // 递推表达式: f(n) = 1 sum(f(n / i)) // 整数因子分解问题 #include #include using namespace std;\nmap\u0026lt;int,int\u0026gt; memo; int solve(int num) { if (num == 1) return 1;\nif (memo.find(num) != memo.end()) return memo[num]; int count = 1; for (int i = 2; i \u0026lt;= num / 2; i++) { if (num % i == 0) { count += solve(num / i); } } memo[num] = count; return count; }\nint main() { int n; while(cin \u0026raquo; n) { cout \u0026laquo; solve(n) \u0026laquo; endl; } }\n2.1.6测试数据或截图 image.png\n2.1.7心得体会 通过这次整数因子分解问题的实现，我深刻理解了记忆化搜索在递归算法中的重要作用。将大问题分解为子问题并存储中间结果，不仅大幅提升了效率，还让我对动态规划和递归的关系有了更直观的认识。\n2.2选择问题 2.2.1题目内容 给定的n个元素数组a[0: n-1]，要求找出第k小的元素。\n2.2.2题目要求 【输入】：数据有多行，给出正整数n\n转word后抄 【输出】：每个数据输出一行，是正整数n的不同的分解式数量。 【输入样例】： 12 35 【输入样例】 8 3\n2.2.3设计思想 deepseek_mermaid_20260108_2d5195 (1).png\n2.2.4算法分析 时间复杂度：O(n log n) 空间复杂度：O(n)\n2.2.5核心代码 #include #include #include #include #include #include #include #include using namespace std;\n// (2) 选择问题 给定的n个元素数组a[0: n-1]，要求找出第k小的元素。 int main() { // 只能按住ctrk + c等退出 int n = 0,k = 0; while(cin \u0026raquo; n \u0026raquo; k) { set s; for (int i = 0; i \u0026lt; n; i++) { int num; cin \u0026raquo; num; s.insert(num); }\nvector\u0026lt;int\u0026gt; nums; for (set\u0026lt;int\u0026gt;::iterator it = s.begin(); it != s.end(); it++) { nums.push_back(*it); } // sort(nums.begin(),nums.end()); set已经是有序了不需要排序 cout \u0026lt;\u0026lt; nums[k -1] \u0026lt;\u0026lt; endl; } }\n2.2.6测试数据或截图 image.png\n2.2.7心得体会 使用set自动去重排序，注意原数组可能有重复元素，需根据题意判断是否允许去重。\n2.3凑零钱 2.3.1题目内容 凑零钱 韩梅梅喜欢满宇宙到处逛街。现在她逛到了一家火星店里，发现这家店有个特别的规矩：你可以用任何星球的硬币付钱，但是绝不找零，当然也不能欠债。韩梅梅手边有 10 (4) 枚来自各个星球的硬币，需要请你帮她盘算一下，是否可能精确凑出要付的款额。\n2.3.2题目要求 【输入格式】 输入第一行给出两个正整数：N（≤10(4) ）是硬币的总个数，M（≤10(2) ）是韩梅梅要付的款额。第二行给出 N 枚硬币的正整数面值。数字间以空格分隔。 【输出格式】 在一行中输出硬币的面值 V(1)≤V(2) ≤⋯≤V(k) ，满足条件 V(1) +V(2) +\u0026hellip;+V(k) =M。数字间以 1 个空格分隔，行首尾不得有多余空格。若解不唯一，则输出最小序列。若无解，则输出 No Solution。 注：我们说序列{ A[1],A[2],⋯ }比{ B[1],B[2],⋯ }“小”，是指存在 k≥1 使得 A[i]=B[i] 对所有 i\u0026lt;k 成立，并且 A[k]\u0026lt;B[k]。\n2.3.3设计思想 deepseek_mermaid_20260108_453da0.png\n2.3.4算法分析 时间复杂度：O(N log N + NM) 空间复杂度：O(NM)\n2.3.5核心代码 // 动态规划 #include #include #include #include using namespace std;\nint main() { int N,M; // N枚硬币总数,M付款金额 cin \u0026raquo; N \u0026raquo; M; vector coins(N); // 读取N之后再创建硬币 for (int i = 0; i \u0026lt; N; i++) { cin \u0026raquo; coins[i]; }\nsort(coins.begin(),coins.end()); // 从小到大排序 // dp[i][j] 表示第i+1个物品在背包为j的空格下的最大价值 vector\u0026lt;vector\u0026lt;int\u0026gt; \u0026gt; dp(N,vector\u0026lt;int\u0026gt;(M + 1)); // 老编译器版本 int bagweight = M; // 路径记录 vector\u0026lt;vector\u0026lt;bool\u0026gt; \u0026gt; choice(N,vector\u0026lt;bool\u0026gt;(M + 1,false)); // 初始化 for (int i = 0; i \u0026lt;= bagweight; i++) { if (i \u0026gt;= coins[0]) { dp[0][i] = coins[0]; choice[0][i] = true; // 选择第一个硬币 } } // 动态规划方程 // 先遍历物品,再遍历背包重量 for (int i = 1; i \u0026lt; N; i++) { for (int j = 0; j \u0026lt;= bagweight; j++) { // cout \u0026lt;\u0026lt; \u0026quot;debug: \u0026quot; \u0026lt;\u0026lt; dp[i][j] \u0026lt;\u0026lt; endl; if (j \u0026gt;= coins[i] \u0026amp;\u0026amp; coins[i] + dp[i-1][j - coins[i]] \u0026gt; dp[i-1][j]) { dp[i][j] = coins[i] + dp[i-1][j - coins[i]]; choice[i][j] = true; // 选择了第i个硬币 } else { // cout \u0026lt;\u0026lt; \u0026quot;debug: 大小不够\u0026quot; \u0026lt;\u0026lt; endl; dp[i][j] = dp[i-1][j]; } // cout \u0026lt;\u0026lt; dp[i][j] \u0026lt;\u0026lt; endl; } } // 判断是否能够凑出 // 当背包的最大价值刚好等于自己的最大容量的时候 // 说明此时刚好能够凑出 // cout \u0026lt;\u0026lt; dp[N-1][M] \u0026lt;\u0026lt; endl; if (dp[N-1][M] == M) { cout \u0026lt;\u0026lt; \u0026quot;Yes\u0026quot; \u0026lt;\u0026lt; endl; } else { cout \u0026lt;\u0026lt; \u0026quot;No Solution\u0026quot; \u0026lt;\u0026lt; endl; } // // debug // for (int i = 0; i \u0026lt; N; i++) { // for (int j = 0; j \u0026lt;= bagweight; j++) { // cout \u0026lt;\u0026lt; choice[i][j] \u0026lt;\u0026lt; \u0026quot; \u0026quot;; // } // cout \u0026lt;\u0026lt; endl; // } // 回溯构造序列 // 题目指的的最小序列指的是字典序 vector\u0026lt;int\u0026gt; result; int i = N - 1,j = M; while(i \u0026gt;= 0 \u0026amp;\u0026amp; j \u0026gt; 0) { if (choice[i][j]) { result.push_back(coins[i]); j -= coins[i]; } i--; } sort(result.begin(), result.end()); for (int k = 0; k \u0026lt; result.size(); k++) { if (k != 0) cout \u0026lt;\u0026lt; \u0026quot; \u0026quot;; cout \u0026lt;\u0026lt; result[k]; } cout \u0026lt;\u0026lt; endl; }\n2.3.6测试数据或截图 image.png\n2.3.7心得体会 通过这个动态规划硬币找零问题，我深入理解了0-1背包问题在实际场景中的应用。从状态定义、递推公式到路径回溯，整个过程让我体会到动态规划的精妙之处。特别是字典序最小序列的构造，让我认识到算法不仅要解决问题，还要考虑输出结果的优化和规范化。\n2.4多处最优服务次序问题 2.4.1题目内容 假设n个顾客同时等待一项服务，顾客i需要的服务时间为 （转word后添加） ，共有s处可以提供此服务。应如何安排n个顾客的服务次序才能使得平均等待时间达到最小？平均等待时间是n个顾客等待服务时间的总和除以n。 对于给定的n个顾客需要的服务时间和s的值，编程计算最优的服务次序。\n2.4.2题目要求 【输入】 第一行有两个正整数n和s，表示n个顾客和s处可以为顾客提供需要的服务。 接下来的一行中，有n个正整数，表示n个顾客需要的服务时间。 【输出】 最小平均等待时间，保留三位小数 【输入样例】 10 2 56 12 1 99 1000 234 33 55 99 812 【输出样例】 336.00\n2.4.3设计思想 deepseek_mermaid_20260108_9cc952.png\n2.4.4算法分析 时间复杂度：O(n log n + n*s) 空间复杂度：O(n + s)\n2.4.5核心代码\n// 5.3 // 贪心算法 // 多处最优服务次序问题 #include #include #include #include #include using namespace std;\nint main() { int n,s; // n个顾客 s处服务地点 cin \u0026raquo; n \u0026raquo; s; vector clinets(n); vector services(s); vector result(n); for (int i = 0; i \u0026lt; n; i++) { cin \u0026raquo; clinets[i]; }\n// 先服务耗时短的(贪心 -\u0026gt; 局部最优堆叠全局最优) sort(clinets.begin(),clinets.end()); // // debug // for (int i = 0; i \u0026lt; clinets.size(); i++) { // cout \u0026lt;\u0026lt; clinets[i] \u0026lt;\u0026lt; endl; // } // 每个服务点维护一个当前的工作时间,表示该服务点已经工作了多长时间 // 每当一个顾客被分配了一个服务点,该服务点的工作时间就会增加该顾客的服务时间 // 为什么服务时间要把自己也算上 -\u0026gt; 如果这样子的话题目表述应该为题目逗留时间 for (int i = 0; i \u0026lt; clinets.size(); i++) { // if (s1 \u0026lt;= s2) { // s1 += clinets[i]; // result[i] += s1; // } // else { // s2 += clinets[i]; // result[i] += s2; // } // 将只能适配两个服务点的代码升级成适配多喝服务点的代码 // 选择已经服务时间最小的等待点 int min_services = 0; int min_services_index = 0; for (int j = 1; j \u0026lt; services.size(); j++) { // 更新最小值 min_services = min(services[min_services_index],services[j]); // 更新最小下标 if (services[min_services_index] \u0026gt;= services[j]) { min_services_index = j; } } services[min_services_index] += clinets[i]; result[i] += services[min_services_index]; } // 计算等待时间 double avg; double total = 0; for (int i = 0; i \u0026lt; result.size(); i++) { total += result[i]; } avg = total / result.size(); cout \u0026lt;\u0026lt; fixed \u0026lt;\u0026lt; setprecision(3) \u0026lt;\u0026lt; avg \u0026lt;\u0026lt; endl;; }\n2.4.6测试数据或截图 image.png\n2.4.7心得体会 排序后优先分配给当前耗时最少的服务点，贪心策略有效减少了总体等待时间。\n2.5最长公共子串问题 2.5.1题目内容 假设有两个字符串（可能包含空格），找出其中最长的公共连续子串，并输出其长度。 2.5.2题目要求 输入描述: 输入为两行字符串（可能包含空格），长度均小于等于50 输出描述: 输出为一个整数，表示最长公共连续子串的长度 输入例子: abcde abgde 输出例子: 2 ab de\n2.5.3设计思想 deepseek_mermaid_20260108_8d0fa8.png\n2.5.4算法分析 时间复杂度：O(m × n × min(m, n)) 空间复杂度：O(m × n × min(m, n))\n2.5.5核心代码\n#include #include #include #include #include #include #include using namespace std;\n// 回溯法与分支定界法 // 最长公共子串问题\nint main() {\n// // // 维护区间的暴力解法 -\u0026gt; 失败(只能处理相同长度字串的操作) // string str1; // string str2; // cin \u0026gt;\u0026gt; str1; // cin \u0026gt;\u0026gt; str2; // set\u0026lt;string\u0026gt; result; // 存储找到的最长子串 // int max_sub_len = 0; // for (int i = 0; i \u0026lt; str1.size(); i++) { // for (int j = i; j \u0026lt; str1.size(); j++) { // int each = i; // int sub_len = 0; // string str_sub = \u0026quot;\u0026quot;; // while(each \u0026lt;= j) { // if (str1[each] == str2[each]) { // sub_len++; // str_sub += str1[each]; // } // // max_sub_len = max(max_sub_len,sub_len); // if (sub_len \u0026gt; max_sub_len) { // // 如果有更长的最长字串就清除掉旧的,放入新的 // result.clear(); // result.insert(str_sub); // max_sub_len = sub_len; // } // else if (max_sub_len == sub_len) { // // 如果有相同的最长子串就放入 // result.insert(str_sub); // } // else { // // 没有的话,不做更新 // } // // 类似于减枝 // if (str1[each] != str2[each]) { // break; // } // each++; // } // } // } // // 输出结果 // cout \u0026lt;\u0026lt; max_sub_len \u0026lt;\u0026lt; endl; // for (set\u0026lt;string\u0026gt;::iterator it = result.begin(); it != result.end(); ++it) { // cout \u0026lt;\u0026lt; *it \u0026lt;\u0026lt; endl; // } // // 遍历开头的暴力解法 -\u0026gt; 正确 // string str1; // string str2; // cin \u0026gt;\u0026gt; str1; // cin \u0026gt;\u0026gt; str2; // int max_sub_len = 0; // set\u0026lt;string\u0026gt; result; // for (int i = 0; i \u0026lt; str1.size(); i++) { // for (int j = 0; j \u0026lt; str2.size(); j++) { // int x = i, y = j; // string str_sub = \u0026quot;\u0026quot;; // while (x \u0026lt; str1.size() \u0026amp;\u0026amp; y \u0026lt; str2.size() \u0026amp;\u0026amp; str1[x] == str2[y]) { // str_sub += str1[x]; // x++; // y++; // } // int sub_len = (x+1) - i - 1; // // max_sub_len = max(max_sub_len,sub_len); // if (sub_len \u0026gt; max_sub_len) { // // 如果有更长的最长字串就清除掉旧的,放入新的 // result.clear(); // result.insert(str_sub); // max_sub_len = sub_len; // } // else if (max_sub_len == sub_len) { // // 如果有相同的最长子串就放入 // result.insert(str_sub); // } // else { // // 没有的话,不做更新 // } // } // } // // 输出结果 // // cout \u0026lt;\u0026lt; max_sub_len \u0026lt;\u0026lt; endl; // // for (string sub : result) { // c98 // // cout \u0026lt;\u0026lt; sub \u0026lt;\u0026lt; endl; // // } // cout \u0026lt;\u0026lt; max_sub_len \u0026lt;\u0026lt; endl; // for (set\u0026lt;string\u0026gt;::iterator it = result.begin(); it != result.end(); ++it) { // cout \u0026lt;\u0026lt; *it \u0026lt;\u0026lt; endl; // } // 动态规划解法 \u0026amp;\u0026amp; 分支定界法 /* 二维数组可以比较好的记录所有比较情况 dp[i-1][j-1] // 以下标i-1为结尾的A,以下表j-1为结尾的B -\u0026gt; 方便初始化 // dp[i][0] 和 dp[0][j] 没有意义 */ string str1; string str2; cin \u0026gt;\u0026gt; str1; cin \u0026gt;\u0026gt; str2; int max_sub_len = 0; vector\u0026lt;vector\u0026lt;int\u0026gt; \u0026gt; dp(str1.size() + 1, vector\u0026lt;int\u0026gt;(str2.size() + 1,0)); set\u0026lt;string\u0026gt; result; for (int i = 1; i \u0026lt;= str1.size(); i++) { for (int j = 1; j \u0026lt;= str2.size(); j++) { if (str1[i-1] == str2[j-1]) { dp[i][j] = dp[i-1][j-1] + 1; } if (dp[i][j] \u0026gt; max_sub_len) { max_sub_len = dp[i][j]; result.clear(); result.insert(str1.substr((i + 1) - max_sub_len - 1, max_sub_len)); } else if (dp[i][j] == max_sub_len) { result.insert(str1.substr((i + 1) - max_sub_len - 1, max_sub_len)); } } } cout \u0026lt;\u0026lt; max_sub_len \u0026lt;\u0026lt; endl; for (set\u0026lt;string\u0026gt;::iterator it = result.begin(); it != result.end(); ++it) { cout \u0026lt;\u0026lt; *it \u0026lt;\u0026lt; endl; } }\n2.5.6测试数据或截图 image.png\n2.5.7心得体会 动态规划记录子串匹配状态，高效求解最长公共子串，避免重复比较。\n2.6哈夫曼编码译码 2.6.1题目内容 编写一个哈夫曼编码译码程序。 按词频从小到大的顺序给出各个字符（不超过30个）的词频，根据词频构造哈夫曼树，给出每个字符的哈夫曼编码，并对给出的语句进行译码。 为确保构建的哈夫曼树唯一，本题做如下限定： （1）选择根结点权值最小的两棵二叉树时，选取权值较小者作为左子树。 （2）若多棵二叉树根结点权值相等，按先后次序分左右，先出现的作为左子树，后出现的作为右子树。 生成哈夫曼编码时，哈夫曼树左分支标记为0，右分支标记为1。\n2.6.2题目要求 【输入格式】 第一行输入字符个数n； 第二行到第n行输入相应的字符及其词频(可以是整数，与可以是小数）； 最后一行输入需进行译码的串。 【输出格式】 首先按树的先序顺序输出所有字符的编码，每个编码占一行； 最后一行输出需译码的原文，加上original:字样。 输出中均无空格 【样例输入】 3 m1 n1 c2 10110 【样例输出】 c:0 m:10 n:11 original:mnc\n2.6.3设计思想 deepseek_mermaid_20260108_5c150b.png\n2.6.4算法分析 时间复杂度：O(n² + n log n + L) 空间复杂度：O(n log n)\n2.6.5核心代码 #include #include #include #include #include #include using namespace std;\n// 回溯法与分支定界法 // 哈夫曼编码译码\n// 哈夫曼树节点 class HFNode { public: int weight; // 权重域 char ch; // 数据域,保存字符信息 vector code; // 哈夫曼编码 int lchild,rchild,parent; // 左孩子索引,右孩子索引,父节点索引 };\n// 哈夫曼树类 class HFTree { public:\n// 构造函数 HFTree(int n) { num = n; // 设置叶子节点数量 int m = 2 * n - 1; // 设置哈夫曼节点数量 // 总节点数 = 叶子节点数 + 内部节点数 = n + (n-1) = 2n - 1 nodes.resize(m); // 为节点数组分配空间 root = -1; // 初始化根节点索引为-1 } // 构建哈夫曼树 void createHFTree() { /* 节点索引分配 索引0到n-1: 分配给原始叶子节点 索引0到2n-2: 分配给新创建的内部节点 */ int m = 2 * num - 1; // 逐个创建内部节点 for (int i = num; i \u0026lt; m; i++) { // 选择两个权值最小的节点 int min1 = -1 , min2 = -1; // 我们已经创建了i个节点(包括原始节点和之间创建的内部节点) // 我们需要从这i个节点中找出两个还没有被合并的（parent == -1）且权重最小的节点 // 第一次遍历: 找到第一个最小的 for (int j = 0; j \u0026lt; i; j++) { if (nodes[j].parent == -1) { if (min1 == -1 || nodes[j].weight \u0026lt; nodes[min1].weight) { min1 = j; } else if (nodes[j].weight == nodes[min1].weight) { // 权值相等时,按照出现先后顺序,下标小的优先 if (j \u0026lt; min1) { min1 = j; } } } } // 第二次遍历: 找到第二个最小的 for (int j = 0; j \u0026lt; i; j++) { if (nodes[j].parent == -1 \u0026amp;\u0026amp; j != min1) { // 不等于最小的最小就是第二个最小的 且 还没有被合并 if (min2 == -1 || nodes[j].weight \u0026lt; nodes[min2].weight) { min2 = j; } else if (nodes[j].weight == nodes[min2].weight) { // 权值相等时,按照出现先后顺序,下标小的优先 if (j \u0026lt; min2) { min2 = j; } } } } // 确保左子树的权值不大于右子树 if (nodes[min1].weight \u0026gt; nodes[min2].weight) { swap(min1, min2); } // 创建新节点 nodes[i].weight = nodes[min1].weight + nodes[min2].weight; nodes[i].lchild = min1; nodes[i].rchild = min2; nodes[i].parent = -1; // 更新子节点的父节点 nodes[min1].parent = i; nodes[min2].parent = i; // 最后一个创建的节点是根节点 if (i == m - 1) { root = i; } } } // 先序遍历输出编码 // 输入: 根节点序号 void preOrderHelper(int node, vector\u0026lt;pair\u0026lt;char,vector\u0026lt;char\u0026gt;\u0026gt;\u0026gt;\u0026amp; result) { if (node == -1) return; // 如果是叶子节点,保存字符和编码 if (nodes[node].lchild == -1 \u0026amp;\u0026amp; nodes[node].rchild == -1) { result.push_back({nodes[node].ch, nodes[node].code}); } preOrderHelper(nodes[node].lchild,result); preOrderHelper(nodes[node].rchild,result); } // 先序遍历输出编码 void preOrder() { vector\u0026lt;pair\u0026lt;char, vector\u0026lt;char\u0026gt;\u0026gt;\u0026gt; result; preOrderHelper(root, result); for (auto\u0026amp; p : result) { cout \u0026lt;\u0026lt; p.first \u0026lt;\u0026lt; \u0026quot;:\u0026quot;; for (char c : p.second) { cout \u0026lt;\u0026lt; c; } cout \u0026lt;\u0026lt; endl; } } // 初始化叶子节点 void Initialization(const vector\u0026lt;pair\u0026lt;char, double\u0026gt;\u0026gt;\u0026amp; chars) { for (int i = 0; i \u0026lt; num; i++) { nodes[i].ch = chars[i].first; // 当前节点字符 nodes[i].weight = chars[i].second * 100; // 频率 nodes[i].lchild = -1; nodes[i].rchild = -1; nodes[i].parent = -1; } } // 生成哈夫曼编码 void generateCode() { // 对每个叶子节点生成编码 for (int i = 0; i \u0026lt; num; i++) { int child = i; int parent = nodes[child].parent; vector\u0026lt;char\u0026gt; code; // 从叶子节点回溯到根节点 while (parent != -1) { if (nodes[parent].lchild == child) { code.push_back('0'); // 左分支标记问0 } else { code.push_back('1'); // 右分支标记为1 } child = parent; parent = nodes[child].parent; } // 反转编码 reverse(code.begin(),code.end()); nodes[i].code = code; } } // 译码 string Decoding(string encodedStr) { string result = \u0026quot;\u0026quot;; int current = root; for (char bit : encodedStr) { // 如果当前读取位是'0'表示哈夫曼树中应该走左分支 if (bit == '0') { current = nodes[current].lchild; } // 如果当前不是'0'就走右分支 else { current = nodes[current].rchild; } // 如果到达叶子节点 if (nodes[current].lchild == -1 \u0026amp;\u0026amp; nodes[current].rchild == -1) { result += nodes[current].ch; current = root; } } return result; } private: vector nodes; // 哈夫曼节点数组 int num; // 叶子结点个数 int root; // 根节点索引 };\nint main() { // 读取输入字符串个数 int n; cin \u0026raquo; n;\n// 读取输入的相应的字符及其词频 vector\u0026lt;pair\u0026lt;char,double\u0026gt;\u0026gt; chars(n); for (int i = 0; i \u0026lt; n; i++) { string line; cin \u0026gt;\u0026gt; line; char ch = line[0]; double freq = stod(line.substr(1)); // 提取字串(提取到末尾) // 返回从索引1到末尾的子串 chars[i] = {ch, freq}; } // 输入译码 string encodedStr; cin \u0026gt;\u0026gt; encodedStr; // 构建哈夫曼树 HFTree hfTree(n); // 构造函数 hfTree.Initialization(chars); // 初始化 hfTree.createHFTree(); // 创建哈夫曼树 hfTree.generateCode(); // 生成哈夫曼编码 // 先序遍历输出编码 hfTree.preOrder(); // 译码并输出原文 string decodedStr = hfTree.Decoding(encodedStr); cout \u0026lt;\u0026lt; \u0026quot;original:\u0026quot; \u0026lt;\u0026lt; decodedStr \u0026lt;\u0026lt; endl; }\n2.6.6测试数据或截图 image.png\n2.6.7心得体会 贪心构建哈夫曼树，从叶子到根反向生成编码，实现高效压缩与准确译码。\n2.7奇怪的Andy，奇怪的旅行！ 2.7.1题目内容 在地球上有一个奇怪的国家，这个国家有 n 个城市，但却只有 n−1 条道路，但是每个城市之间都可以互相到达。 某天 Andy 来到了这个国家，但是他以前没有出去旅游，真是个奇怪的人呢。他来到了这个国家进行一次旅行，想花尽量少的钱走过更多的城市， 他不想走回头路，因为这样会多花钱，真是个抠门的人呢。已知的是 dh 可以任意选一个城市作为他旅行的起点。现在他找到你，他想知道他最多能走过多少个城市。\n2.7.2题目要求 【输入格式】 第一行一个整数 n 表示这个国家的城市数量。 接下来 n−1行，每一行有两个整数 (u,v) 表示u,v之间有一条边。 tips: 1\u0026lt;=n\u0026lt;=100000 【输出格式】 输出一个数字，表示 Andy最多能走过多少个城市 【样例输入】 3 1 2 1 3 【样例输出】 3 tips: Andy可以按照城市2 −\u0026gt; 城市1 −\u0026gt; 城市3的路线进行旅行。\n2.7.3设计思想 deepseek_mermaid_20260108_4a70ab.png\n2.7.4算法分析 空间复杂度：O(n) 时间复杂度：O(n)\n2.7.5核心代码 #include #include #include #include #include #include #include using namespace std; const int MAXN = 100005;\n// 回溯法与分支定界法 // 奇怪的Andy,奇怪的旅行 // 最多能走过多个城市 -\u0026gt; 任意点最远点是直径端点 /* u: 当前正在访问的节点 parent: 当前节点u的父节点 depth: 从起点到当前节点的距离 */\nvoid dfs(int u, int parent ,int depth , vector\u0026amp; dist, vector\u0026lt;vector \u0026gt; graph) { dist[u] = depth; // 记录当前节点的距离原点的距离 // for (int v : graph[u]) { // if (v != parent) { // 不允许访问同一座城市 // dfs(v,u,depth + 1,dist,graph); // 搜索下一个节点 // } // }\nfor (int i = 0; i \u0026lt; graph[u].size(); i++) { if (graph[u][i] != parent) { // 不允许访问同一个城市 dfs(graph[u][i], u, depth + 1, dist, graph); } } }\nint main() { // 邻接表构造图 int n; cin \u0026raquo; n; vector dist(n+1,0); // 记录各个节点距离原点的距离(dist[u],表示距离节点u的最大距离) vector\u0026lt;vector \u0026gt; graph(n+1); // 邻接图 for (int i = 0; i \u0026lt; n-1; i++) { // n-1条边 int u,v; cin \u0026raquo; u \u0026raquo; v; graph[u].push_back(v); graph[v].push_back(u); }\n// 第一次dfs： 从任意节点开始,找到距离最远的节点 // (在树中,从任意节点出发,距离它最远的点一定是树直径的一个端点) dfs(1, -1, 0,dist,graph); // 以这个节点为起点 那么其父节点为-1(不存在) // 寻找距离原点距离最远的点 int farthest_first_Node = 0; for (int i = 2; i \u0026lt; dist.size(); i++) { // 节点1是起点就不遍历了 if (dist[i] \u0026gt; farthest_first_Node) { farthest_first_Node = i; } } // 第二次dfs: 从第一次找到的最远节点开始(树直径的一个端点),找到直径的另一端 dist.assign(n+1, 0); // 将 vector 的所有元素设置为 0 dfs(farthest_first_Node, -1, 0,dist,graph); // 查找两次端点的距离就是最大距离 int maxDist = 0; for (int i = 1; i \u0026lt; dist.size(); i++) { if (dist[i] \u0026gt; maxDist) { maxDist = dist[i]; } } // 输出最多能走过多少个城市 (最大距离 + 1) - \u0026gt; 直径的节点数 = 边数 + 1 cout \u0026lt;\u0026lt; maxDist + 1 \u0026lt;\u0026lt; endl; }\n2.7.6测试数据或截图 image.png\n2.7.7心得体会 两次DFS求解树直径，巧妙利用最远端点性质。代码简洁高效，个人觉得这种思路非常优雅。\n2.8n皇后问题 2.8.1题目内容 给定一个nn格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋规则棋盘中有一些位置不能放皇后。问总共有多少种放法？使任意的两个皇后都不在同一行、同一列或同一条对角线上。 编程要求：当面向nn格的棋盘上放置彼此不受攻击的n个皇后，找出所有放置方案。\n2.8.2题目要求 【输入】 【输入包含多组测试例】 对每个测试例，每行只有一个数字n，(4\u0026lt;=n\u0026lt;=12) 【输出】 对每组测试数据，输出所有可能的放置情况，最后一行是方案总数。 【输入样例】 5 【输出样例】 1 3 5 2 4 1 4 2 5 3 2 4 1 3 5 2 5 3 1 4 3 1 4 2 5 3 5 2 4 1 4 1 3 5 2 4 2 5 3 1 5 2 4 1 3 5 3 1 4 2 Total = 10\n2.8.3设计思想 deepseek_mermaid_20260108_71f137.png\n2.8.4算法分析 空间复杂度：O(S × n²) 时间复杂度：O(n!)（最坏情况，但实际有大量剪枝）\n2.8.5核心代码\n#include #include #include #include #include #include using namespace std;\n// 回溯法与分支定界法 // n皇后问题\n// 存储每个方案 vector\u0026lt;vector\u0026lt;vector \u0026gt; \u0026gt; result;\n// 补充: 这里不需要遍历行,因为在单层搜索的过程中,每一层递归,只会选 // for循环里面的一个元素 // 满足n皇后就返回真,不满足n皇后就返回假 bool isValid(vector\u0026lt;vector \u0026gt; chessboard,int col, int row, int n) { // 遍历列 for (int i = 0; i \u0026lt; row; i++) { if (chessboard[i][col] == \u0026lsquo;\u0026rsquo;) { return false; } } // 遍历45度 for (int i = row - 1,j = col - 1; i \u0026gt;= 0 \u0026amp;\u0026amp; j \u0026gt;= 0; i\u0026ndash;,j\u0026ndash;) { if (chessboard[i][j] == \u0026lsquo;\u0026rsquo;) { return false; } }\n// 遍历135度 for (int i = row - 1, j = col + 1; i \u0026gt;= 0 \u0026amp;\u0026amp; j \u0026lt; n; i--,j++) { if (chessboard[i][j] == '*') { return false; } } return true; }\n// 回溯法暴搜索n皇后问题 void backtracking(int n,int row,vector\u0026lt;vector \u0026gt;\u0026amp; chessboard) { if (row == n) { result.push_back(chessboard); return; }\nfor (int col = 0; col \u0026lt; n; col++) { if (isValid(chessboard,col,row,n)) { // 如果同行同列同斜线存在就不继续搜索了 chessboard[row][col] = '*'; // 放入皇后 backtracking(n,row + 1,chessboard); // 继续递归 chessboard[row][col] = '.'; // 回溯 } } }\nint main() { // \u0026lsquo;\u0026lsquo;表示放置皇后 \u0026lsquo;.\u0026lsquo;表示不防止皇后 int n; // nn的棋盘 cin \u0026raquo; n; // 构造棋盘 vector\u0026lt;vector \u0026gt; chessboard(n,vector(n)); for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n; j++) { chessboard[i][j] = \u0026lsquo;.\u0026rsquo;; } }\n// 存储方案总数 vector\u0026lt;vector\u0026lt;int\u0026gt; \u0026gt; count; // 暴搜计算结果 backtracking(n,0,chessboard); // 先从第0行开始搜索 for (int i = 0; i \u0026lt; result.size(); i++) { vector\u0026lt;int\u0026gt; temp; for (int j = 0; j \u0026lt; n; j++) { for (int k = 0; k \u0026lt; n; k++) { if (result[i][j][k] == '*') { // 如果第i个结果的第j行第k列示皇后则统计数据 temp.push_back(k); } } } count.push_back(temp); } // 打印数据 for (int i = 0; i \u0026lt; count.size(); i++) { for (int j = 0; j \u0026lt; count[0].size() - 1; j++) { cout \u0026lt;\u0026lt; count[i][j] \u0026lt;\u0026lt; \u0026quot; \u0026quot;; } cout \u0026lt;\u0026lt; count[i][count[0].size()-1] \u0026lt;\u0026lt; endl; } // 题目理解错误的代码 // // 统计结果 // for (int i = 0; i \u0026lt; result.size(); i++) { // for (int j = 0; j \u0026lt; n; j++) { // for (int k = 0; k \u0026lt; n; k++) { // if (result[i][j][k] == '*') { // 如果第i个结果的第j行第k列示皇后则统计数据 // cnt[j][k]++; // } // } // } // } // // 输出结果: 输出的是所有可能的放置情况,每一行代表一种方案 // // 该行的数字表示每一行的皇后所在的列号 // for (int i = 0; i \u0026lt; cnt.size(); i++) { // for (int j = 0; j \u0026lt; cnt[0].size() - 1; j++) { // cout \u0026lt;\u0026lt; cnt[i][j] \u0026lt;\u0026lt; \u0026quot; \u0026quot;; // } // cout \u0026lt;\u0026lt; cnt[i][cnt[0].size() - 1] \u0026lt;\u0026lt; endl; // } cout \u0026lt;\u0026lt; \u0026quot;Total=\u0026quot; \u0026lt;\u0026lt; result.size() \u0026lt;\u0026lt; endl; }\n2.8.6测试数据或截图 image.png\n2.8.7心得体会 回溯法逐行放置皇后，利用约束条件剪枝，输出时注意题目要求列编号从1开始。\n3.蓝桥杯题目 3.1星球骑士 3.1.1题目内容 小明冒充 XX 星球的骑士，进入了一个奇怪的城堡。 城堡里边什么都没有，只有方形石头铺成的地面。 假设城堡地面是 n×nn×n 个方格。如下图所示。 按习俗，骑士要从西北角走到东南角。可以横向或纵向移动，但不能斜着走，也不能跳跃。每走到一个新方格，就要向正北方和正西方各射一箭。（城堡的西墙和北墙内各有 nn 个靶子）同一个方格只允许经过一次。但不必走完所有的方格。如果只给出靶子上箭的数目，你能推断出骑士的行走路线吗？有时是可以的，比如上图中的例子。 本题的要求就是已知箭靶数字，求骑士的行走路径（测试数据保证路径唯一）\n3.1.2题目要求 输入描述 第一行一个整数 NN (0≤N≤200≤N≤20)，表示地面有 N×NN×N 个方格。 第二行 NN 个整数，空格分开，表示北边的箭靶上的数字（自西向东） 第三行 NN 个整数，空格分开，表示西边的箭靶上的数字（自北向南） 输出描述 输出一行若干个整数，表示骑士路径。 为了方便表示，我们约定每个小格子用一个数字代表，从西北角开始编号: 0,1,2,3 ⋯⋯ 比如，上图中的方块编号为： 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 输入输出样例 示例\n输入 4 2 4 3 4 4 3 3 3 3.1.3设计思想 deepseek_mermaid_20260108_55fb64.png\n3.1.4算法分析 时间复杂度: O(4^(N²)) - 每个位置最多尝试4个方向 空间复杂度: O(N²) - 存储地图、路径和递归栈\n3.1.5核心代码 第一题#include #include using namespace std;\nstruct Node { bool flag; int x, y; };\nNode map[20][20]; vector road; int N, X[20], Y[20], sum; int dir[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};\nbool dfs(int x, int y) { if (x == N - 1 \u0026amp;\u0026amp; y == N - 1) { for (int i = 0; i \u0026lt; N; i++) { if (X[i] || Y[i] || sum) return false; road.push_back(x * N + y); return true; } }\nroad.push_back(x * N + y); map[x][y].flag = true; for (int i = 0; i \u0026lt; 4; i++) { int tx = x + dir[i][0]; int ty = y + dir[i][1]; if (tx \u0026lt; 0 || tx \u0026gt; (N - 1) || ty \u0026lt; 0 || ty \u0026gt; (N - 1)) continue; if (!map[tx][ty].flag \u0026amp;\u0026amp; (X[tx] \u0026gt; 0 \u0026amp;\u0026amp; Y[ty] \u0026gt; 0)) { X[tx]--;Y[ty]--;sum -= 2; if (dfs(tx, ty)) return true; else { X[tx]++; Y[ty]++; sum += 2; } } } map[x][y].flag = false; road.erase(road.begin() + road.size() - 1); return false; }\nint main() { cin \u0026raquo; N; for (int i = 0; i \u0026lt; N; i++){ cin \u0026raquo; Y[i]; sum += Y[i]; } for (int i = 0; i \u0026lt; N; i++) { cin \u0026raquo; X[i]; sum += X[i]; } for (int i = 0; i \u0026lt; N; i++) { for (int j = 0; j \u0026lt; N; j++) { map[i][j].flag = false; map[i][j].x = i; map[i][j].y = j; } } X[0]\u0026ndash;;Y[0]\u0026ndash;;sum -= 2; dfs(0, 0); for (int i = 0; i \u0026lt; road.size(); i++) cout \u0026laquo; road[i] \u0026laquo; \u0026rsquo; \u0026lsquo;; return 0; } 3.1.6测试数据或截图 image.png\n3.1.7心得体会 回溯剪枝巧妙，但指数复杂度不适合大规模问题。\n3.2星球青蛙 3.2.1题目内容 X 星球的流行宠物是青蛙，一般有两种颜色：白色和黑色。 XX 星球的居民喜欢把它们放在一排茶杯里，这样可以观察它们跳来跳去。 如下图，有一排杯子，左边的一个是空着的，右边的杯子，每个里边有一只青蛙。 ∗WWWBBB∗WWWBBB 其中，WW 字母表示白色青蛙，BB 表示黑色青蛙，∗∗ 表示空杯子。 XX 星的青蛙很有些癖好，它们只做 3 个动作之一： 跳到相邻的空杯子里。 隔着 1 只其它的青蛙（随便什么颜色）跳到空杯子里。 隔着 2 只其它的青蛙（随便什么颜色）跳到空杯子里。 对于上图的局面，只要 1 步，就可跳成下图局面： WWW∗BBBWWW∗BBB 本题的任务就是已知初始局面，询问至少需要几步，才能跳成另一个目标局面。\n3.2.2题目要求 输入描述 输入为 2 行，2 个串，表示初始局面和目标局面。我们约定，输入的串的长度不超过 15。 输出描述 输出要求为一个整数，表示至少需要多少步的青蛙跳。 输入输出样例 示例\n输入 WWBB WWBB\n3.2.3设计思想 deepseek_mermaid_20260108_858ef9.png\n3.2.4算法分析 时间复杂度: O(6^L) - 最坏情况下每个位置有6种移动选择，L为字符串长度 空间复杂度: O(L×N) - 存储队列中的状态和距离映射，N为状态\n3.2.5核心代码 #include #include #include #include \u0026lt;unordered_map\u0026gt; #include using namespace std;\nstring start, endd; queue q;//存储状态 int len;//状态的长度\nint bfs() { unordered_map\u0026lt;string, int\u0026gt; d;//存储该状态下的坐标 q.push(start);//将初始状态入队列 d[start] = 0;//初始状态距离为0 int dx[6] = {-3, -2, -1, 1, 2, 3};//6个向量左三、左二、左一、右一、右二、右三\nwhile (q.size())//当队列为空时结束bfs算法 { string t = q.front();//出队列,命名为状态t q.pop(); int distance = d[t];//取出t状态的距离distance if(t == endd) return distance;//如果t状态和终止状态相等,退出bfs,返回distance int k = t.find('*');//找出空杯所在的位置 for (int i = 0; i \u0026lt; 6; i ++ )//枚举6个向量 { int a = k + dx[i];//加上偏移量后点的坐标 if(a \u0026gt;= 0 \u0026amp;\u0026amp; a \u0026lt; len)//如果没有越界 { swap(t[a], t[k]);//交换位置,获取新的状态 if(!d.count(t))//如果该状态未被遍历则更新该状态的距离,入队列 { d[t] = distance + 1; q.push(t); } swap(t[a], t[k]);//还原现场,因为还有剩余的向量并没有被枚举过 } } } return -1;//如果没有找到的话,返回-1 }\nint main() { cin.tie(0);//cin加速器\ncin \u0026gt;\u0026gt; start;//读入开始状态 cin \u0026gt;\u0026gt; endd;//读入终止状态 len = start.length();//获取状态的长度 cout \u0026lt;\u0026lt; bfs();//输出答案 return 0; } 3.2.6测试数据或截图 image.png\n3.2.7心得体会 BFS求最短路径，状态转移巧妙，利用哈希表避免重复访问，效率较高。\n3.3星球坦克 3.3.1题目内容 X 星的坦克战车很奇怪，它必须交替地穿越正能量辐射区和负能量辐射区才能保持正常运转，否则将报废。 某坦克需要从 A 区到 B 区去（ A，B 区本身是安全区，没有正能量或负能量特征），怎样走才能路径最短？ 已知的地图是一个方阵，上面用字母标出了 A，B 区，其它区都标了正号或负号分别表示正负能量辐射区。 例如： A + - + - + B + - + - 坦克车只能水平或垂直方向上移动到相邻的区。\n3.3.2题目要求 输入描述 第一行是一个整数 nn，表示方阵的大小， 4≤n\u0026lt;1004≤n\u0026lt;100。 接下来是 nn 行，每行有 nn 个数据，可能是 A，B，+，- 中的某一个，中间用空格分开。A，B 都只出现一次。 输出描述 输出一个整数，表示坦克从 A 区到 B 区的最少移动步数。 如果没有方案，则输出 -1。 输入输出样例 示例\n输入 5 A + - + - + B + - + - 3.3.3设计思想 deepseek_mermaid_20260108_046e8f.png\n3.3.4算法分析 时间复杂度: O(N²) - 每个网格位置最多被访问两次（以\u0026rsquo;+\u0026lsquo;结束和以\u0026rsquo;-\u0026lsquo;结束各一次） 空间复杂度: O(N²) - visited数组和队列占用的空间\n3.3.5核心代码 #include #include #include #include using namespace std;\nconst int MAX_N = 100;\nstruct Node { int x, y, steps; char last_sign; // 上一步经过的辐射区类型：\u0026rsquo;+\u0026rsquo; 或 \u0026lsquo;-\u0026rsquo; };\nint n; vector\u0026lt;vector\u0026gt; grid; bool visited[MAX_N][MAX_N][2]; // visited[x][y][0] 表示以\u0026rsquo;+\u0026lsquo;结束访问, visited[x][y][1] 表示以\u0026rsquo;-\u0026lsquo;结束访问 int startX, startY, endX, endY;\n// 四个移动方向：下，上，右，左 int dir[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };\nint bfs() { queue q;\n// 从起点A开始，可以走任意相邻的辐射区 for (int d = 0; d \u0026lt; 4; d++) { int nx = startX + dir[d][0]; int ny = startY + dir[d][1]; if (nx \u0026lt; 0 || nx \u0026gt;= n || ny \u0026lt; 0 || ny \u0026gt;= n) continue; char cell = grid[nx][ny]; if (cell == 'B') { // A直接相邻B return 1; } if (cell == '+' || cell == '-') { int state = (cell == '+') ? 0 : 1; if (!visited[nx][ny][state]) { visited[nx][ny][state] = true; q.push({ nx, ny, 1, cell }); } } } while (!q.empty()) { Node cur = q.front(); q.pop(); // 如果当前就是终点 if (cur.x == endX \u0026amp;\u0026amp; cur.y == endY) { return cur.steps; } // 尝试四个方向 for (int d = 0; d \u0026lt; 4; d++) { int nx = cur.x + dir[d][0]; int ny = cur.y + dir[d][1]; if (nx \u0026lt; 0 || nx \u0026gt;= n || ny \u0026lt; 0 || ny \u0026gt;= n) continue; char next_cell = grid[nx][ny]; // 不能走回A if (next_cell == 'A') continue; // 规则：当前是'+'，下一步必须是'-'或B // 当前是'-'，下一步必须是'+'或B bool can_move = false; if (cur.last_sign == '+') { can_move = (next_cell == '-' || next_cell == 'B'); } else if (cur.last_sign == '-') { can_move = (next_cell == '+' || next_cell == 'B'); } if (can_move) { int state = 0; char next_sign = cur.last_sign; // 占位 if (next_cell == '+') { state = 0; next_sign = '+'; } else if (next_cell == '-') { state = 1; next_sign = '-'; } else if (next_cell == 'B') { // B可以接在任何符号后面，状态可以任意（这里用0） state = 0; next_sign = cur.last_sign; } if (!visited[nx][ny][state]) { visited[nx][ny][state] = true; q.push({ nx, ny, cur.steps + 1, next_sign }); } } } } return -1; }\nint main() { cin \u0026raquo; n; grid.resize(n, vector(n));\n// 读取网格 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n; j++) { cin \u0026gt;\u0026gt; grid[i][j]; if (grid[i][j] == 'A') { startX = i; startY = j; } else if (grid[i][j] == 'B') { endX = i; endY = j; } } } // 初始化visited数组 memset(visited, 0, sizeof(visited)); // 执行BFS int result = bfs(); // 输出结果 cout \u0026lt;\u0026lt; result \u0026lt;\u0026lt; endl; return 0; } 3.3.6测试数据或截图 image.png\n3.3.7心得体会 带状态的BFS解决交替路径问题，状态设计巧妙，避免重复访问相同位置的同种状态。\n3.4星球电报 3.4.1题目内容 从 X 星截获一份电码，是一些数字，如下： 13 1113 3113 132113 1113122113 ⋯⋯ YY 博士经彻夜研究，发现了规律： 第一行的数字随便是什么，以后每一行都是对上一行\u0026quot;读出来\u0026rdquo; 比如第 2 行，是对第 1 行的描述，意思是：1 个 1，1 个 3，所以是：1113 第 3 行，意思是：3 个 1,1 个 3，所以是：3113 请你编写一个程序，可以从初始数字开始，连续进行这样的变换。 3.4.2题目要求 输入描述 第一行输入一个数字组成的串，不超过 100 位。 第二行，一个数字 nn，表示需要你连续变换多少次，nn 不超过 20。 输出描述 输出一个串，表示最后一次变换完的结果。 输入输出样例 示例\n输入 5 7\n3.4.3设计思想 deepseek_mermaid_20260108_456153.png\n3.4.4算法分析 时间复杂度: O(n×k)，其中n为字符串长度，k为b次循环次数，字符串长度可能指数增长 空间复杂度: O(m)，m为最长字符串长度，需要存储中间结果\n3.4.5核心代码 #include\u0026lt;bits/stdc++.h\u0026gt; using namespace std; string change(string str) { int i = 0; string ans; while(i \u0026lt; str.size()) { int cont = 0; while(i + cont \u0026lt; str.size() \u0026amp;\u0026amp; str[i + cont] == str[i]) cont++; ans += to_string(cont) + str[i]; i = i + cont; } return ans; }\nint main() { string a; int b; cin \u0026raquo; a \u0026raquo; b; while(b\u0026ndash;) a = change(a); cout \u0026laquo; a; return 0; }//by wqs 3.4.6测试数据或截图 image.png\n3.4.7心得体会 外观数列的简洁实现，递归生成模式有趣，字符计数拼接巧妙。\n3.5移动距离 3.5.1题目内容 题目描述 X 星球居民小区的楼房全是一样的，并且按矩阵样式排列。其楼房的编号为 1,2,3,⋯⋯ 当排满一行时，从下一行相邻的楼往反方向排号。 比如：当小区排号宽度为 6 时，开始情形如下： 1 2 3 4 5 6 12 11 10 9 8 7 13 14 15 ⋯⋯ 我们的问题是：已知了两个楼号 m,nm,n，需要求出它们之间的最短移动距离（不能斜线方向移动） 3.5.2题目要求 输入描述 输入为 3 个整数 w,m,nw,m,n，空格分开，都在 1 到 10000 范围内，ww 为排号宽度，m,nm,n 为待计算的楼号。 输出描述 要求输出一个整数，表示 m,nm,n 两楼间最短移动距离。 输入输出样例 示例 1\n输入 6 2 8\n3.5.3设计思想 deepseek_mermaid_20260108_9c9b21.png\n3.5.4算法分析 时间复杂度: O(1) - 只进行固定次数的算术运算和条件判断 空间复杂度: O(1) - 只使用固定大小的数组和变量\n3.5.5核心代码 #include #include using namespace std; int i=0; int main() { int Location(int,int,int abs[]);//abs数组记录两个点的行号和行内位置！！ int width,start,end,group;//group设定为两个点的行之间的距离！！ int slocation,location;//Location设定为两个点之间的距离，slocation设定为两个点的行内距离！！ int abs[4] = {0,0,0,0}; cin\u0026raquo;width; cin\u0026raquo;start; cin\u0026raquo;end; Location(start,width,abs); Location(end,width,abs); group = fabs(abs[2]-abs[0]); slocation = fabs(abs[1]-abs[3]); location = slocation+group; cout\u0026laquo;location\u0026laquo;endl; return 0; } int Location(int input,int width,int abs[]) { int location,group;//这里的location设定为每一个点的行内位置，group设置为行号 if(input%width == 0)//若输入的楼号恰好能被宽度整除，则该楼的行号为两数整除的结果，否则+1！ group = input/width; else group = input/width+1; if(group%2!=0)//观察楼号的排列顺序，我们会发现奇数号楼正序排列，偶数号楼逆序排列，所以先判断楼号的奇偶性 { if(input%width == 0 )//这里注意楼号能被宽度整除的行内具体位置的确定，需单独计算！！ location = width; else location = input%width; } else { if(input%width == 0 ) location = 1; else location = width-input%width+1;//这里将正序排列逆序一下，尤为注意的细节是+1（在涉及减法和距离以及位置的确定时尤为需要注意+1的问题）！！ } abs[i] = group; i++; abs[i] = location; i++; return 0; } 3.5.6测试数据或截图 image.png\n3.5.7心得体会 曼哈顿距离的变体计算，楼号与坐标映射巧妙，注意奇偶行排列方向的差异处理。\n3.6交换瓶子 3.6.1题目内容 题目描述 有 NN 个瓶子，编号 1 ~ NN，放在架子上。 比如有 5 个瓶子： 2 1 3 5 4 要求每次拿起 2 个瓶子，交换它们的位置。 经过若干次后，使得瓶子的序号为： 1 2 3 4 5 对于这么简单的情况，显然，至少需要交换 2 次就可以复位。 如果瓶子更多呢？你可以通过编程来解决。\n3.6.2题目要求 输入描述 输入格式为两行： 第一行: 一个正整数 N (N\u0026lt;104)N (N\u0026lt;104), 表示瓶子的数目 第二行： NN 个正整数，用空格分开，表示瓶子目前的排列情况。 输出描述 输出数据为一行一个正整数，表示至少交换多少次，才能完成排序。 输入输出样例 示例\n输入 5 3 1 2 5 4\n3.6.3设计思想 deepseek_mermaid_20260108_ff4632.png\n3.6.4算法分析 时间复杂度：O(n) - 每个元素仅被访问一次 空间复杂度：O(n) - 使用两个大小为n的数组\n3.6.5核心代码 #include #include #include using namespace std; const int N = 1e5 + 5; int a[N]; //储存初始顺序的数组 bool st[N]; //标记数组 int main() { int n; cin \u0026raquo; n; //输入数组 for (int i = 1; i \u0026lt;= n; i++) { cin \u0026raquo; a[i]; }\nint cnt = 0; //记录初始环数 for (int i = 1; i \u0026lt;= n; i++) { if (!st[i]) { //没有被标记，找到一个环的起点 cnt++; for (int j = i; !st[j]; j = a[j]) { //访问这个环，将这个环中所有结点都进行标记 st[j] = true; } } } cout \u0026lt;\u0026lt; n - cnt; } 3.6.6测试数据或截图 image.png\n3.6.7心得体会 通过计算排列中的环数求最小交换次数，思路巧妙，效率极高。\n3.7会议描述 3.7.1题目内容 小蓝组织了一场算法交流会议，总共有 50 50 人参加了本次会议。在会议上，大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手 (且仅有一次)。但有 7 7 个人，这 7 7 人彼此之间没有进行握手 (但这 7 7 人与除这 7 7 人以外的所有人进行了握手)。请问这些人之间一共进行了多少次握手? 注意 A A 和 B B 握手的同时也意味着 B B 和 A A 握手了，所以算作是一次握手。\n3.7.2题目要求 这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。\n3.7.3设计思想 image.png\n3.7.4算法分析 deepseek_mermaid_20260108_347dc0.png\n3.7.5核心代码 #include #include using namespace std;\nint main() { const int TOTAL_PEOPLE = 50; const int SPECIAL_GROUP_SIZE = 7;\n// 假设编号0-6是那7个人 vector\u0026lt;int\u0026gt; is_special(TOTAL_PEOPLE, 0); for (int i = 0; i \u0026lt; SPECIAL_GROUP_SIZE; i++) { is_special[i] = 1; } int handshakes = 0; // 遍历所有可能的握手对 for (int i = 0; i \u0026lt; TOTAL_PEOPLE; i++) { for (int j = i + 1; j \u0026lt; TOTAL_PEOPLE; j++) { // 如果两个人都属于特殊组，则不握手 if (is_special[i] \u0026amp;\u0026amp; is_special[j]) { continue; } handshakes++; } } cout \u0026lt;\u0026lt; handshakes \u0026lt;\u0026lt; endl; return 0; } 3.7.6测试数据或截图 时间复杂度: O(n²)，其中n=50（固定规模，实际为常数操作） 空间复杂度: O(n)，使用了一个大小为n的标记数组\n3.7.7心得体会 组合数学问题，双重循环模拟握手，排除特殊组，简单直接。\n3.8庆祝生日 3.8.1题目内容 小橙子为了庆祝生日，买了 nn 种不同尺寸的的矩形蛋糕（可以认为每种蛋糕有无限个，因为小橙子很 rich），第 ii 种蛋糕的长为 aiai，宽为 bibi，一块蛋糕在旋转之后其长和宽将会变为 bi,aibi,ai。小橙子热衷于将一些蛋糕摆放在一条线上（她可以选择旋转蛋糕），并得到以下特殊的“橙线”。 橙线上任意相邻两块蛋糕不能是同一种形状。从第二块蛋糕开始，每一块蛋糕的长度必须等于前一块蛋糕的宽度（第一块蛋糕没有限制）。 请你计算对于长度为 lenlen 的“橙线”，小橙子有多少种不同的摆放方案。因为方案数可能很大，请对 109+7109+7 取模。 两个方案被认为是相同的,当且仅当蛋糕的类型顺序和旋转情况完全一致（正方形的蛋糕无论如何旋转都认为其是同一种情况）。\n3.8.2题目要求 第一行两个整数 n,lenn,len，表示蛋糕种类的数量，橙线的长度。 接下来 nn 行，第 ii 行两个正整数 a,ba,b 表示第 i−1i−1 种蛋糕的长和宽。 数据范围保证：1≤n≤1001≤n≤100，1≤len≤20001≤len≤2000, 1≤ai,bi≤1051≤ai,bi≤105。 输出格式 输出一行整数，表示有几种摆放方法可以获得长度为 lenlen 的橙线，对 109+7109+7 取模。 样例输入 2 5 1 4 4 5 样例输出 2 样例说明 第一种：将第一种蛋糕竖着放，即长度贡献为 11，然后第二块蛋糕也竖着放。 第二种：将第二块蛋糕横着放，直接满足了条件。\n3.8.3设计思想 deepseek_mermaid_20260108_5fced9.png\n3.8.4算法分析 时间复杂度: O(n²)，其中n=50（固定规模，实际为常数操作） 空间复杂度: O(n)，使用了一个大小为n的标记数组\n3.8.5核心代码 #include #include #include using namespace std;\nconst int MOD = 1e9 + 7; const int MAXL = 2005; const int MAXS = 205;\nint n, L; vector\u0026lt;pair\u0026lt;int, int\u0026raquo; shapes; // (length, width) vector cake_id; // 每个形状属于哪个蛋糕类型 int dp[MAXL][MAXS];\nint main() { cin \u0026raquo; n \u0026raquo; L; for (int i = 0; i \u0026lt; n; i++) { int a, b; cin \u0026raquo; a \u0026raquo; b; shapes.push_back({ a, b }); cake_id.push_back(i); if (a != b) { shapes.push_back({ b, a }); cake_id.push_back(i); } } int S = shapes.size();\n// 初始化：第一块蛋糕 for (int s = 0; s \u0026lt; S; s++) { int len = shapes[s].first; if (len \u0026lt;= L) { dp[len][s] = 1; } } // 转移 for (int l = 1; l \u0026lt;= L; l++) { for (int s = 0; s \u0026lt; S; s++) { if (dp[l][s] == 0) continue; int w = shapes[s].second; for (int t = 0; t \u0026lt; S; t++) { if (cake_id[t] == cake_id[s]) continue; // 相邻不能是同一蛋糕类型 if (shapes[t].first != w) continue; int nl = l + shapes[t].first; if (nl \u0026gt; L) continue; dp[nl][t] = (dp[nl][t] + dp[l][s]) % MOD; } } } int ans = 0; for (int s = 0; s \u0026lt; S; s++) { ans = (ans + dp[L][s]) % MOD; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; endl; return 0; } 3.8.6测试数据或截图 image.png\n3.8.7心得体会 组合数学问题，双重循环模拟握手，排除特殊组，简单直接。\n3.9农场黄牛 3.9.1题目内容 小怂有一个超级大的农场，他在里面养了 nn 头黄牛，编号为 1,2,3,…,n1,2,3,…,n，而每头黄牛都有一个家。小怂创立了黄牛派对节，每年都会选择在某头牛的家中举行派对。 今年的黄牛派对节到了，小怂的 nn 头黄牛都要去参加一场在编号为 xx 的黄牛的家中举行的派对，共有 mm 条有向路，每条路都有一定的长度。 每头黄牛参加完派对后都必须回到各自的家中。小怂虽笨，但他养的黄牛很聪明，无论是去参加派对还是回家，每头黄牛都会选择最短路径，求这 nn 头黄牛走一个来回的最短路径中最长的一条路径长度。\n3.9.2题目要求 输入格式 第一行有三个正整数 n,m,xn,m,x，分别表示牛的数量 nn，道路数 mm 和在编号为 xx 的黄牛家中举行派对。 接下来 mm 行，每行三个整数 u,v,wu,v,w，表示存在一条由 uu 到 vv 的长度为 ww 的道路。 数据保证：1≤x≤n≤10001≤x≤n≤1000，1≤m≤1051≤m≤105，1≤u,v≤n1≤u,v≤n，1≤w≤1001≤w≤100，保证从任何一个结点出发都能到达 xx 号结点，且从 xx 出发可以到达其他所有节点。 输出格式 输出共 11 行，一个整数，表示这 nn 头黄牛走一个来回的最短路径中最长的一条路径长度。 样例输入 4 8 2 1 2 4 1 3 2 1 4 7 2 1 1 2 3 5 3 1 2 3 4 4 4 2 3 样例输出 10 样例解释 对于第 11 头黄牛，1→2→11→2→1，所以它走一个来回的最短路径是 55。 对于第 22 头黄牛，在它这开派对，所以它的一个来回的最短路径是 00。 对于第 33 头黄牛，3→1→2→1→33→1→2→1→3，所以它的一个来回的最短路径是 99。 对于第 44 头黄牛，4→2→1→2→44→2→1→2→4，所以它的一个来回的最短路径是 1010。 所以，一个来回的最短路径中最长的一条路径长度是 1010\n3.9.3设计思想 deepseek_mermaid_20260108_4859c6.png\n3.9.4算法分析 时间复杂度: O(m log n)，其中n为顶点数，m为边数 空间复杂度: O(n + m)，存储图和距离数组\n3.9.5核心代码 #include #include #include #include using namespace std;\ntypedef pair\u0026lt;int, int\u0026gt; pii; // (距离, 顶点)\nvoid dijkstra(int start, const vector\u0026lt;vector\u0026gt;\u0026amp; graph, vector\u0026amp; dist) { int n = graph.size() - 1; dist.assign(n + 1, INT_MAX); dist[start] = 0;\npriority_queue\u0026lt;pii, vector\u0026lt;pii\u0026gt;, greater\u0026lt;pii\u0026gt;\u0026gt; pq; pq.push({0, start}); while (!pq.empty()) { int d = pq.top().first; int u = pq.top().second; pq.pop(); if (d \u0026gt; dist[u]) continue; for (const pii\u0026amp; edge : graph[u]) { int v = edge.first; int w = edge.second; int nd = d + w; if (nd \u0026lt; dist[v]) { dist[v] = nd; pq.push({nd, v}); } } } }\nint main() { ios::sync_with_stdio(false); cin.tie(nullptr);\nint n, m, x; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m \u0026gt;\u0026gt; x; vector\u0026lt;vector\u0026lt;pii\u0026gt;\u0026gt; graph(n + 1); // 原图 vector\u0026lt;vector\u0026lt;pii\u0026gt;\u0026gt; rev_graph(n + 1); // 反向图 for (int i = 0; i \u0026lt; m; i++) { int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; graph[u].push_back({v, w}); rev_graph[v].push_back({u, w}); // 反向边 } vector\u0026lt;int\u0026gt; dist_to(n + 1); // 从x到各点的距离（回家） vector\u0026lt;int\u0026gt; dist_from(n + 1); // 从各点到x的距离（去派对） dijkstra(x, graph, dist_to); // 计算回家的最短路径 dijkstra(x, rev_graph, dist_from); // 计算去派对的最短路径（使用反向图） int ans = 0; for (int i = 1; i \u0026lt;= n; i++) { int total = dist_to[i] + dist_from[i]; ans = max(ans, total); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; endl; return 0; } 3.9.6测试数据或截图 image.png\n3.9.7心得体会 两次Dijkstra + 反向图，巧妙计算往返最大时间，图论思想精妙。\n3.10雨林探险 3.10.1题目内容 小怂和小乐是好朋友，他们一起去到雨林中探险，突然雷风大作，紧接着豆大的雨点从天空中打落下来，然后一只大怪物出现了，小怂与小乐惊呆了，吓得抱在了一起。 瞬间，地上出现了一个 nn 行 mm 列的超大矩阵，矩阵的每个格子要么是空地 . 或者是障碍 #。 他们的起点在 (1,1)(1,1)，要逃到 (n,m)(n,m) 的出口。他们可以上下左右移动一格，这样算作一步。但是幸运的是，他们手上有一个一次性传送门，使用传送门可以瞬移到相对自己位置的 (D,R)(D,R) 向量，也就是说假设他们原来在 (x,y)(x,y)，使用传送门可以到 (x+D,y+R)(x+D,y+R)，这个也算作一步。当然，他们也可以不使用传送门；D,RD,R 可以为负数或零。 他们都很害怕，想要赶紧逃离，所以他们想要知道最小需要几步操作可以离开这个地方，当然他们也可能逃不出来，那就只能等死。\n3.10.2题目要求 输入格式 第一行有 44 个正整数， n,m,D,Rn,m,D,R，具体意义已经在问题描述说明。 接下来 nn 行，每行长度是 mm，仅有 . 或者 # 的字符串。 数据保证：1≤n,m≤10001≤n,m≤1000，∣D∣\u0026lt;n∣D∣\u0026lt;n，∣R∣\u0026lt;m∣R∣\u0026lt;m。 输出格式 一行，一个整数，表示逃出这个地方的最小步数。如果他们逃不出来，则输出 −1−1。 样例输入 11 3 6 2 1 \u0026hellip;#.. ..##.. ..#\u0026hellip; 样例输出 11 5 样例输入 22 3 7 2 1 ..#..#. .##.##. .#..#.. 样例输出 22 -1 样例解释 样例解释 11 (1,1)→(1,2)→(1,3)→(1,1)→(1,2)→(1,3)→ 使用传送门 →(3,4)→(3,5)→(3,6)→(3,4)→(3,5)→(3,6)。 样例解释 22 只有一个一次性传送门的话，他们没办法逃出来。\n3.10.3设计思想 deepseek_mermaid_20260108_81be25.png\n3.10.4算法分析 时间复杂度: O(n×m) - BFS遍历整个网格两次，再加一次遍历所有点检查传送门 空间复杂度: O(n×m) - 存储两个距离矩阵和网格\n3.10.5核心代码 #include #include #include #include using namespace std;\n// BFS函数，计算从起点(sx,sy)到所有点的最短距离 vector\u0026lt;vector\u0026gt; bfs(int sx, int sy, const vector\u0026amp; grid, int n, int m) { vector\u0026lt;vector\u0026gt; dist(n, vector(m, -1)); if (grid[sx][sy] == \u0026lsquo;#\u0026rsquo;) return dist; queue\u0026lt;pair\u0026lt;int, int\u0026raquo; q; dist[sx][sy] = 0; q.push({sx, sy}); int dx[4] = {0, 0, 1, -1}; int dy[4] = {1, -1, 0, 0}; while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int d = 0; d \u0026lt; 4; d++) { int nx = x + dx[d]; int ny = y + dy[d]; if (nx \u0026gt;= 0 \u0026amp;\u0026amp; nx \u0026lt; n \u0026amp;\u0026amp; ny \u0026gt;= 0 \u0026amp;\u0026amp; ny \u0026lt; m \u0026amp;\u0026amp; grid[nx][ny] == \u0026lsquo;.\u0026rsquo; \u0026amp;\u0026amp; dist[nx][ny] == -1) { dist[nx][ny] = dist[x][y] + 1; q.push({nx, ny}); } } } return dist; }\nint main() { ios::sync_with_stdio(false); int n, m, D, R; cin \u0026raquo; n \u0026raquo; m \u0026raquo; D \u0026raquo; R; vector grid(n); for (int i = 0; i \u0026lt; n; i++) { cin \u0026raquo; grid[i]; }\n// 起点或终点是障碍，直接无法到达 if (grid[0][0] == '#' || grid[n - 1][m - 1] == '#') { cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; endl; return 0; } // 计算从起点和终点出发的最短距离 auto dist_from_start = bfs(0, 0, grid, n, m); auto dist_from_end = bfs(n - 1, m - 1, grid, n, m); // 初始答案为不使用传送门的步数 int ans = dist_from_start[n - 1][m - 1]; // 枚举使用传送门的点 for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; m; j++) { if (grid[i][j] == '.' \u0026amp;\u0026amp; dist_from_start[i][j] != -1) { int ni = i + D; int nj = j + R; // 检查传送目标是否合法 if (ni \u0026gt;= 0 \u0026amp;\u0026amp; ni \u0026lt; n \u0026amp;\u0026amp; nj \u0026gt;= 0 \u0026amp;\u0026amp; nj \u0026lt; m \u0026amp;\u0026amp; grid[ni][nj] == '.' \u0026amp;\u0026amp; dist_from_end[ni][nj] != -1) { int steps = dist_from_start[i][j] + 1 + dist_from_end[ni][nj]; if (ans == -1 || steps \u0026lt; ans) { ans = steps; } } } } } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; endl; return 0; } 3.10.6测试数据或截图 image.png\n3.10.7心得体会 BFS+传送门枚举，分情况讨论清晰，双起点BFS求最短路径巧妙。\n![图片 25](/images/feishu/程序设计实习报告/deepseek_mermaid_20260108_2d5195 (1)_9108a3.png) ","permalink":"https://hydarealman.github.io/wander/posts/2026/06/%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E5%AE%9E%E4%B9%A0%E6%8A%A5%E5%91%8A/","summary":"\u003cp\u003e程序设计实习报告\n桂林理工大学\nGUILIN UNIVERSITY OF TECHNOLOGY\u003c/p\u003e\n\u003cp\u003e程序设计实践课程  \u003cbr\u003e\n实习报告\u003c/p\u003e\n\u003cp\u003e学      院： 计算机科学与工程学院   #\n班      级：                        ##\n组      长：                     \u003cbr\u003e\n组      员：                      \u003cbr\u003e\n组      员：                     \u003cbr\u003e\n组      员：                     \u003cbr\u003e\n指导教师：                     \u003cbr\u003e\n评   分/价：\u003c/p\u003e\n\u003cp\u003e1.基础实践部分\u003c/p\u003e","title":"程序设计实习报告"},{"content":"视觉完整形态文档二\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/%E8%A7%86%E8%A7%89%E5%AE%8C%E6%95%B4%E5%BD%A2%E6%80%81%E6%96%87%E6%A1%A3%E4%BA%8C/","summary":"\u003cp\u003e视觉完整形态文档二\u003c/p\u003e","title":"视觉完整形态文档二"},{"content":"视觉组招新 自瞄: 1 (懂自瞄原理)\n能量机关: 1(要求会调自瞄) 导航: 2(新导航 + 仿真) 两个人都要求会调导航 雷达+神经网络: 1(要求会调自瞄)\n覃焕懿:× 翟清宇:× 代龙涵: 雷桐富: 何嘉琦:× 张瑄晴:× 覃富滔:× 范宴榕:\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/%E8%A7%86%E8%A7%89%E7%BB%84%E6%8B%9B%E6%96%B0/","summary":"\u003cp\u003e视觉组招新\n自瞄: 1 (懂自瞄原理)\u003c/p\u003e\n\u003ch3 id=\"能量机关-1要求会调自瞄\"\u003e能量机关: 1(要求会调自瞄)\u003c/h3\u003e\n\u003ch3 id=\"导航-2新导航--仿真-两个人都要求会调导航\"\u003e导航: 2(新导航 + 仿真) 两个人都要求会调导航\u003c/h3\u003e\n\u003cp\u003e雷达+神经网络: 1(要求会调自瞄)\u003c/p\u003e\n\u003cp\u003e覃焕懿:×\n翟清宇:×\n代龙涵:\n雷桐富:\n何嘉琦:×\n张瑄晴:×\n覃富滔:×\n范宴榕:\u003c/p\u003e","title":"视觉组招新"},{"content":"tmux简明速查手册(WSL/Linux通用) 1.安装\n1 sudo apt update \u0026amp;\u0026amp; sudo apt install tmux 2.启动与退出\n启动tmux\n1 tmux 退出当前窗格\n1 exit 或 Ctrl+d 3.会话管理\n1 2 3 4 5 分离会话（后台运行） Ctrl+b 然后按 d 重新连接（回到上次现场） tmux attach 列出所有会话 tmux ls 连接到指定会话 tmux attach -t 会话名 新建命名会话 tmux new -s 会话名 4.窗格管理 按键(先按Ctrl+b,再按下下一个键)\n垂直分割 %\n水平分割 \u0026quot;\n切换窗格 方向键（↑ ↓ ← →）\n调整窗格大小 Ctrl+b 按住，然后按方向键\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/tmux%E7%AE%80%E6%98%8E%E9%80%9F%E6%9F%A5%E6%89%8B%E5%86%8C-wsl_linux%E9%80%9A%E7%94%A8/","summary":"\u003ch1 id=\"tmux简明速查手册wsllinux通用\"\u003etmux简明速查手册(WSL/Linux通用)\u003c/h1\u003e\n\u003cp\u003e1.安装\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esudo\u003c/span\u003e \u003cspan class=\"n\"\u003eapt\u003c/span\u003e \u003cspan class=\"n\"\u003eupdate\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e \u003cspan class=\"n\"\u003esudo\u003c/span\u003e \u003cspan class=\"n\"\u003eapt\u003c/span\u003e \u003cspan class=\"n\"\u003einstall\u003c/span\u003e \u003cspan class=\"n\"\u003etmux\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e2.启动与退出\u003c/p\u003e\n\u003cp\u003e启动tmux\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etmux\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e退出当前窗格\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eexit\u003c/span\u003e \u003cspan class=\"err\"\u003e或\u003c/span\u003e \u003cspan class=\"n\"\u003eCtrl\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"n\"\u003ed\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e3.会话管理\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e分离会话（后台运行）\u003c/span\u003e     \u003cspan class=\"n\"\u003eCtrl\u003c/span\u003e\u003cspan class=\"o\"\u003e+\u003c/span\u003e\u003cspan class=\"n\"\u003eb\u003c/span\u003e \u003cspan class=\"err\"\u003e然后按\u003c/span\u003e \u003cspan class=\"n\"\u003ed\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e重新连接（回到上次现场）\u003c/span\u003e \u003cspan class=\"n\"\u003etmux\u003c/span\u003e \u003cspan class=\"n\"\u003eattach\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e列出所有会话\u003c/span\u003e            \u003cspan class=\"n\"\u003etmux\u003c/span\u003e \u003cspan class=\"n\"\u003els\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e连接到指定会话\u003c/span\u003e          \u003cspan class=\"n\"\u003etmux\u003c/span\u003e \u003cspan class=\"n\"\u003eattach\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003et\u003c/span\u003e \u003cspan class=\"err\"\u003e会话名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e新建命名会话\u003c/span\u003e            \u003cspan class=\"n\"\u003etmux\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003es\u003c/span\u003e \u003cspan class=\"err\"\u003e会话名\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e4.窗格管理 按键(先按Ctrl+b,再按下下一个键)\u003c/p\u003e","title":"tmux简明速查手册(WSL_Linux通用)"},{"content":"rust仿真环境配置: wsl + ros2 + rust 1.以管理员身份打开PowerShell或CMD\n2.执行安装命令\n1 wsl --install -d Ubuntu-24.04 3.设置用户名和密码\n列出所有的ubantu版本\n1 wsl -l -v 进入想要的版本\n1 wsl -d Ubuntu-24.04 复制文件到根目录\n1 cp -r /mnt/c/Users/dong/Desktop/at_vision_simulator-master ~/ ROS2使用小鱼自动安装\n更新系统并安装基础依赖\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y sudo apt install -y \\ build-essential \\ pkg-config \\ libx11-dev \\ libasound2-dev \\ libudev-dev \\ libxkbcommon-x11-0 \\ libwayland-dev \\ libxkbcommon-dev \\ mesa-utils \\ libgl1-mesa-glx \\ libgl1-mesa-dri \\ curl \\ git \\ cmake 安装Rust\n1 curl --proto \u0026#39;=https\u0026#39; --tlsv1.2 -sSf https://sh.rustup.rs | sh 选择默认安装1\n安装完成后,重新加载环境变量\n1 source ~/.cargo/env 验证\n1 cargo --version 更新系统并安装Vulkan驱动与工具\n1 2 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y sudo apt install -y mesa-vulkan-drivers vulkan-tools ","permalink":"https://hydarealman.github.io/wander/posts/2026/06/rust%E4%BB%BF%E7%9C%9F%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE_-wsl--ros2--rust/","summary":"\u003ch1 id=\"rust仿真环境配置-wsl--ros2--rust\"\u003erust仿真环境配置: wsl + ros2 + rust\u003c/h1\u003e\n\u003cp\u003e1.以管理员身份打开PowerShell或CMD\u003c/p\u003e\n\u003cp\u003e2.执行安装命令\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ewsl\u003c/span\u003e \u003cspan class=\"o\"\u003e--\u003c/span\u003e\u003cspan class=\"n\"\u003einstall\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003ed\u003c/span\u003e \u003cspan class=\"n\"\u003eUbuntu\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mf\"\u003e24.04\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e3.设置用户名和密码\u003c/p\u003e\n\u003cp\u003e列出所有的ubantu版本\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ewsl\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003el\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003ev\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e进入想要的版本\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C++\" data-lang=\"C++\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ewsl\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"n\"\u003ed\u003c/span\u003e \u003cspan class=\"n\"\u003eUbuntu\u003c/span\u003e\u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mf\"\u003e24.04\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e复制文件到根目录\u003c/p\u003e","title":"rust仿真环境配置_ wsl + ros2 + rust"},{"content":"自瞄赛季汇报 自瞄技术历经人员流失与两年断续迭代，今年虽终得稳定落地，但7v7实战效果未达预期。当前流水火控仅适配爆发模式与高弹频哨兵，面对前哨站及冷却模式步兵命中效果大幅下滑，底层PID控制滞后严重，视觉组已开发的MPC先进火控受限于目前pid控制算法的落后难以落地，亟需为前哨站设计专用火控逻辑。\n硬件层面，pitch轴6020电机小角度控制超调，轴承磨损与电机老化引入回差；微机平台老化严重，内存接口脱落导致帧率骤降，串口与相机掉线重连长达4和14秒，关键对局直接断供，013相机清晰度与主流016系列存在代差。传统底盘地形适应性差，复杂地形下车身剧烈晃动，自瞄难以持续锁定，算法能力无从发挥。\n必须同步推进轮腿机器人研发以提升瞄准平台稳定性，并将p轴更换为4310电机、更新微机与相机，为MPC火控及专用击打逻辑扫清部署障碍，方能将已有识别跟踪能力转化为赛场实打实的命中效果。\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/%E8%87%AA%E7%9E%84%E8%B5%9B%E5%AD%A3%E6%B1%87%E6%8A%A5/","summary":"\u003ch1 id=\"自瞄赛季汇报\"\u003e自瞄赛季汇报\u003c/h1\u003e\n\u003cp\u003e自瞄技术历经人员流失与两年断续迭代，今年虽终得稳定落地，但7v7实战效果未达预期。当前流水火控仅适配爆发模式与高弹频哨兵，面对前哨站及冷却模式步兵命中效果大幅下滑，底层PID控制滞后严重，视觉组已开发的MPC先进火控受限于目前pid控制算法的落后难以落地，亟需为前哨站设计专用火控逻辑。\u003c/p\u003e","title":"自瞄赛季汇报"},{"content":"三维点云测试方法 概述 安装PCL\n安装Open3D\n安装CloudCompare\n激光三角测距: 线激光器向物体表面投射一条激光线,当物体表面有高低起伏时,激光线会发生弯曲和位移,相机(与激光器成固定角度)捕捉到这条变形激光线的二维图像,通过三角测量法计算出每个点亮度的中心点,最终拼接成完整的三维点云\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/%E4%B8%89%E7%BB%B4%E7%82%B9%E4%BA%91%E6%B5%8B%E8%AF%95%E6%96%B9%E6%B3%95/","summary":"\u003ch1 id=\"三维点云测试方法\"\u003e三维点云测试方法\u003c/h1\u003e\n\u003ch3 id=\"概述\"\u003e概述\u003c/h3\u003e\n\u003cp\u003e安装PCL\u003c/p\u003e\n\u003cp\u003e安装Open3D\u003c/p\u003e\n\u003cp\u003e安装CloudCompare\u003c/p\u003e\n\u003ch3 id=\"激光三角测距\"\u003e激光三角测距:\u003c/h3\u003e\n\u003cp\u003e线激光器向物体表面投射一条激光线,当物体表面有高低起伏时,激光线会发生弯曲和位移,相机(与激光器成固定角度)捕捉到这条变形激光线的二维图像,通过三角测量法计算出每个点亮度的中心点,最终拼接成完整的三维点云\u003c/p\u003e","title":"三维点云测试方法"},{"content":"完整形态文档: 自瞄开源引用 西工大自瞄:\nhttps://github.com/SnocrashWang/WMJAimer/wiki/WMJAimer-Project-Report 结合卡尔曼滤波器和熵权法匹配多运动模型的整车建模\n上交自瞄:\nhttps://github.com/julyfun/rm.cv.fans?tab=readme-ov-file 三分法降自由度的yaw角优化 弹道重现的可视化调参 坐标变换器\n同济自瞄:\nhttps://github.com/TongjiSuperPower/sp_vision_25/ 暴力搜索法降自由度的yaw角优化 MPC轨迹规划器\n华南师范自瞄:\nhttps://github.com/FaterYU/rm_auto_aim fitLine角点优化\n中南大学自瞄\nhttps://github.com/CSU-FYT-Vision/FYT2024_vision pca角点优化\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/%E5%AE%8C%E6%95%B4%E5%BD%A2%E6%80%81%E6%96%87%E6%A1%A3_-%E8%87%AA%E7%9E%84%E5%BC%80%E6%BA%90%E5%BC%95%E7%94%A8/","summary":"\u003ch1 id=\"完整形态文档-自瞄开源引用\"\u003e完整形态文档: 自瞄开源引用\u003c/h1\u003e\n\u003cp\u003e西工大自瞄:\u003c/p\u003e\n\u003cp\u003ehttps://github.com/SnocrashWang/WMJAimer/wiki/WMJAimer-Project-Report\n结合卡尔曼滤波器和熵权法匹配多运动模型的整车建模\u003c/p\u003e\n\u003cp\u003e上交自瞄:\u003c/p\u003e\n\u003cp\u003ehttps://github.com/julyfun/rm.cv.fans?tab=readme-ov-file\n三分法降自由度的yaw角优化 弹道重现的可视化调参 坐标变换器\u003c/p\u003e\n\u003cp\u003e同济自瞄:\u003c/p\u003e","title":"完整形态文档_ 自瞄开源引用"},{"content":"Kalman filter ","permalink":"https://hydarealman.github.io/wander/posts/2026/06/kalman-filter/","summary":"\u003ch1 id=\"kalman-filter\"\u003eKalman filter\u003c/h1\u003e","title":"Kalman filter"},{"content":"opencv知识库---整理 基本概念 预处理\n按我的话来说，所谓预处理，就是在图像还未真正进行识别处理前，对图像进行简易、全局的处理。这一块通常在产生图片时进行操作，不属于我们视觉识别的重点（但是预处理真的很重要）。这里仅简单介绍我们使用的两个预处理过程。\n降低曝光度\n通过相机，我们能够源源不断地获取到当前的画面，也就是一帧帧的图像。自瞄算法处理的对象，就是这每一张图像。\n在实战中，由于环境光的干扰，如果直接对图片进行算法处理，会错误的提取多余的特征，导致算法的准确度和速度大大降低。由于灯条自己会发光，可以有效和将灯条与环境光很好的区分开来。为了便于分离灯条与其他光线，一般将曝光设置得很低，如下图为相机获取到的画面：（RoboMaster视觉教程（1）摄像头）\n常见关键字 数据类型---类对象 1. cv::Point2f\ncv::Point2f 是一个二维点，包含两个浮点数（x 和 y），用于表示二维空间中的点。\n加法和减法：可以直接对 Point2f 进行加法和减法操作。\n计算距离：使用 cv::norm 函数计算两点之间的欧几里得距离。\n例如：\nfloat distance = norm(point1 - point2);\n2. cv::Point3f\ncv::Point3f 是一个三维点，包含三个浮点数（x、y 和 z），用于表示三维空间中的点。\n加法和减法：可以直接对 Point3f 进行加法和减法操作。\n计算距离：使用 cv::norm 函数计算两点之间的欧几里得距离。\n例如：\nfloat distance = norm(point1 - point2);\n3. cv::Point2i 和 cv::Point3i\n除了浮点数版本的 Point2f 和 Point3f，OpenCV 还提供了整数版本的点类型：\ncv::Point2i：二维整数点，包含两个整数（x 和 y）。\ncv::Point3i：三维整数点，包含三个整数（x、y 和 z）。\n它们的用法与浮点数版本类似，只是数据类型为整数。\nMat赋初值（经常忘记） cv::Mat cameraMatrix = (cv::Mat_\u0026lt;double\u0026gt;(3, 3) \u0026lt;\u0026lt;\n1200.0, 0.0, 640.0,\n0.0, 1200.0, 360.0,\n0.0, 0.0, 1.0;\n函数 常见程序设计流程 PNP测距 robomaster装甲板识别代码讲解 需求:用方框框住装甲板\n分析:\n观察视频，我发现装甲板有两条竖直平行的灯条在装甲板的左右两端。所以大致分析，解决该问题的关键就是通过筛选出视频中的灯条，通过计算像素坐标，来绘制矩形框住装甲板。\n视频中的灯条可以看成一个旋转矩形RotatedRect，为了方便后续对灯条几何特征（角度/中心点等）进行成对匹配，我将矩形的各个属性：宽，长，中心，角度，面积，封装成一个灯条类。\n将VideoCaputure初始化，用于读取视频中的每一帧\n为了方便对图像中灯条的提取\n我首先对图像进行了预处理\n1.由于视频中的灯条是红色的，选择红色通道进行二值化\n2.将图片进行阈值处理，灯条是图片中亮度较高的区域，阈值220可以有效提取明亮区域\n3.利用高斯模糊消除微小噪声点\n4.对图像进行膨胀操作，连接断裂的灯条区域，将灯条扩大，方便提取，使用5x5矩形结构元素element增强膨胀元素\n接着检测灯条的轮廓\n利用findContours函数读取预处理好的图像，获得存储灯条轮廓的点集\nhierarchy表示输出各个轮廓的继承关系\nRETR_TREE表示检测所有轮廓，并且建立所有的继承关系，CHAIN_APPROX_NONE表示把轮廓的所有点存储\n对轮廓进行处理与筛选\n将面积太小，点数太少，长宽比太大也就是太过细长的灯条筛选掉\n使用椭圆拟合函数fitEllipse:返回旋转矩形\nopencv常用100个API 图像操作 cv2.imread(filename, flags) - 读取图像。\ncv2.imwrite(filename, img) - 保存图像。\ncv2.imshow(window_name, img) - 显示图像。\ncv2.cvtColor(src, code) - 转换图像颜色空间。\ncv2.resize(src, dsize, fx, fy, interpolation) - 缩放图像。\ncv2.rotate(src, rotateCode) - 旋转图像。\ncv2.flip(src, flipCode) - 翻转图像。\ncv2.split(src) - 拆分通道。\ncv2.merge(mv) - 合并通道。\ncv2.copyMakeBorder(src, top, bottom, left, right, borderType, value) - 添加边框。\n图像变换 cv2.warpAffine(src, M, dsize) - 仿射变换。\ncv2.getAffineTransform(srcPoints, dstPoints) - 获取仿射变换矩阵。\ncv2.warpPerspective(src, M, dsize) - 透视变换。\ncv2.getPerspectiveTransform(srcPoints, dstPoints) - 获取透视变换矩阵。\ncv2.remap(src, map1, map2, interpolation) - 重映射。\ncv2.resize(src, dsize) - 调整大小。\ncv2.getRotationMatrix2D(center, angle, scale) - 获取旋转矩阵。\ncv2.invertAffineTransform(M) - 仿射矩阵求逆。\ncv2.convertScaleAbs(src, alpha, beta) - 调整对比度和亮度。\ncv2.normalize(src, dst, alpha, beta, norm_type) - 归一化。\n绘图功能 cv2.line(img, pt1, pt2, color, thickness) - 画线。\ncv2.rectangle(img, pt1, pt2, color, thickness) - 画矩形。\ncv2.circle(img, center, radius, color, thickness) - 画圆。\ncv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness) - 画椭圆。\ncv2.polylines(img, pts, isClosed, color, thickness) - 画多边形。\ncv2.fillPoly(img, pts, color) - 填充多边形。\ncv2.putText(img, text, org, fontFace, fontScale, color, thickness) - 添加文本。\n图像阈值 cv2.threshold(src, thresh, maxval, type) - 图像二值化。\ncv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C) - 自适应阈值。\ncv2.inRange(src, lowerb, upperb) - 范围筛选。\n图像平滑与滤波 cv2.blur(src, ksize) - 均值滤波。\ncv2.GaussianBlur(src, ksize, sigmaX) - 高斯滤波。\ncv2.medianBlur(src, ksize) - 中值滤波。\ncv2.bilateralFilter(src, d, sigmaColor, sigmaSpace) - 双边滤波。\ncv2.filter2D(src, ddepth, kernel) - 任意核卷积。\n边缘检测与轮廓 cv2.Canny(image, threshold1, threshold2) - 边缘检测。\ncv2.findContours(image, mode, method) - 查找轮廓。\ncv2.drawContours(image, contours, contourIdx, color, thickness) - 绘制轮廓。\ncv2.arcLength(contour, closed) - 计算轮廓周长。\ncv2.contourArea(contour) - 计算轮廓面积。\ncv2.approxPolyDP(curve, epsilon, closed) - 多边形逼近。\ncv2.boundingRect(points) - 计算矩形边界。\ncv2.minEnclosingCircle(points) - 最小包围圆。\ncv2.convexHull(points) - 凸包。\ncv2.isContourConvex(contour) - 判断是否为凸形。\n形态学操作 cv2.erode(src, kernel, iterations) - 腐蚀。\ncv2.dilate(src, kernel, iterations) - 膨胀。\ncv2.morphologyEx(src, op, kernel) - 形态学操作（开闭运算等）。\ncv2.getStructuringElement(shape, ksize) - 获取结构元素。\n图像直方图 cv2.calcHist(images, channels, mask, histSize, ranges) - 计算直方图。\ncv2.equalizeHist(src) - 直方图均衡化。\ncv2.createCLAHE(clipLimit, tileGridSize) - 自适应直方图均衡化。\n特征检测与描述 cv2.SIFT_create() - SIFT特征检测。\ncv2.ORB_create() - ORB特征检测。\ncv2.FastFeatureDetector_create() - FAST特征检测。\ncv2.MSER_create() - MSER特征检测。\ncv2.BRISK_create() - BRISK特征检测。\ncv2.SimpleBlobDetector_create() - 简单Blob检测。\ncv2.goodFeaturesToTrack(src, maxCorners, qualityLevel, minDistance) - 检测角点。\n特征匹配 cv2.BFMatcher(normType) - 暴力匹配器。\ncv2.FlannBasedMatcher() - FLANN匹配器。\ncv2.drawMatches(img1, kp1, img2, kp2, matches, outImg) - 绘制匹配结果。\n视频操作 cv2.VideoCapture(source) - 打开视频文件或摄像头。\ncv2.VideoWriter(filename, fourcc, fps, frameSize) - 保存视频。\ncap.read() - 读取视频帧。\ncap.isOpened() - 检查视频是否打开。\ncap.release() - 释放视频资源。\n几何变换与数学操作 cv2.addWeighted(src1, alpha, src2, beta, gamma) - 图像加权。\ncv2.bitwise_and(src1, src2) - 按位与。\ncv2.bitwise_or(src1, src2) - 按位或。\ncv2.bitwise_not(src) - 按位取反。\ncv2.bitwise_xor(src1, src2) - 按位异或。\ncv2.minMaxLoc(src) - 最值定位。\ncv2.reduce(src, dim, rtype) - 归约操作。\n模板匹配 cv2.matchTemplate(image, templ, method) - 模板匹配。\ncv2.minMaxLoc(result) - 获取匹配位置。\n深度学习相关 cv2.dnn.readNetFromCaffe(protoTxt, model) - 读取Caffe模型。\ncv2.dnn.readNetFromTensorflow(model, config) - 读取TensorFlow模型。\ncv2.dnn.readNetFromONNX(model) - 读取ONNX模型。\ncv2.dnn.blobFromImage(image, scalefactor, size, mean, swapRB, crop) - 图像转换为深度学习输入。\n基本工具 cv2.waitKey(delay) - 等待键盘输入。\ncv2.destroyAllWindows() - 销毁所有窗口。\ncv2.getTickCount() - 获取时间戳。\ncv2.getTickFrequency() - 获取时间频率。\ncv2.setMouseCallback(window_name, callback) - 设置鼠标回调。\n深入功能 cv2.calcOpticalFlowFarneback(prev, next, flow, pyrScale, levels, winsize, iterations, polyN, polySigma, flags) - 光流计算。\ncv2.cornerHarris(src, blockSize, ksize, k) - Harris角点检测。\ncv2.cornerSubPix(image, corners, winSize, zeroZone, criteria) - 亚像素角点优化。\n自定义与扩展 cv2.getTrackbarPos(trackbarname, winname) - 获取滑块值。\ncv2.createTrackbar(trackbarname, winname, value, count, onChange) - 创建滑块。\ncv2.fillConvexPoly(img, points, color) - 填充凸多边形。\ncv2.fillPoly(img, pts, color) - 填充多边形。\n图像与视频编码解码 cv2.imencode(ext, img) - 编码图像。\ncv2.imdecode(buf, flags) - 解码图像。\ncv2.VideoWriter_fourcc(c1, c2, c3, c4) - 获取视频编码器。\n其他实用功能 cv2.phase(x, y) - 计算幅角。\ncv2.cartToPolar(x, y) - 笛卡尔坐标到极坐标转换。\ncv2.polarToCart(magnitude, angle) - 极坐标到笛卡尔坐标转换。\ncv2.kmeans(data, K, bestLabels, criteria, attempts, flags) - KMeans 聚类。\ncv2.connectedComponents(image) - 连通域分析。\n补充API 1.glob void cv::glob(cv::String pattern, std::vector\u0026lt;cv::String\u0026gt;\u0026amp; result, bool recursive = false);\npattern：文件路径模式，支持通配符（如 * 和 ?）。例如，\u0026quot;./data/*.jpg\u0026quot; 表示获取 data 文件夹下所有扩展名为 .jpg 的文件。\nresult：用于存储匹配路径的容器，类型为 std::vector\u0026lt;cv::String\u0026gt;。\nrecursive：是否递归搜索子文件夹。默认为 false，表示仅搜索当前目录。\n2.Size cv::Size 指定图像尺寸\ncv::Size 是一个简单的结构体，包含两个成员变量：width 和 height。它通常用于指定图像的尺寸，例如在 cv::resize 函数中\ncv::Size(-1, -1) 的含义\n在 OpenCV 的 cv::resize 函数中，cv::Size 的参数用于指定目标图像的大小。如果将 cv::Size 的宽度和高度都设置为 -1，这通常意味着目标图像的大小是通过缩放比例（fx 和 fy）来计算的，而不是直接指定目标尺寸。\n例如:\ncv::resize(src, dst, cv::Size(-1, -1), fx, fy, interpolation);\n3.findChessboardCorners 在 OpenCV 中，cv::findChessboardCorners 是一个用于检测棋盘格角点的函数，广泛应用于相机标定和三维重建等任务中\nbool cv::findChessboardCorners(\nInputArray image, // 输入图像，必须是8位灰度或彩色图像\nSize patternSize, // 棋盘格的尺寸，表示内部角点的数量（例如8x6的棋盘格，patternSize为(7,5)）\nOutputArray corners, // 检测到的角点坐标\nint flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE // 操作标志\n);\nimage：输入图像，必须是8位灰度或彩色图像。\npatternSize：棋盘格的尺寸，表示内部角点的数量（例如8x6的棋盘格，patternSize为(7,5)）。\ncorners：检测到的角点坐标，存储为 std::vector\u0026lt;cv::Point2f\u0026gt;。\nflags：操作标志，可以组合以下值：\nCALIB_CB_ADAPTIVE_THRESH：使用自适应阈值。\nCALIB_CB_NORMALIZE_IMAGE：对图像进行归一化。\nCALIB_CB_FAST_CHECK：快速检查图像是否包含棋盘格，如果未找到则提前退出\n4.cornerSubPix//用于优化角点坐标//亚像素级精确定位 void cv::cornerSubPix(\nInputArray image, // 输入图像，通常是单通道灰度图像。\nInputOutputArray corners, // 输入角点的初始坐标（例如由 findChessboardCorners 或 goodFeaturesToTrack 检测到的角点），优化后的角点坐标将直接输出到此参数[^23^][^24^]。\nSize winSize, // 搜索窗口的一半尺寸。例如，Size(5, 5) 表示搜索窗口大小为 (5*2+1)×(5*2+1)=11×11[^21^][^23^]。\nSize zeroZone, // 死区的一半尺寸，用于避免搜索区域的中心部分。值为 (-1, -1) 表示没有死区[^21^][^23^]。\nTermCriteria criteria // 迭代过程的终止条件，可以是最大迭代次数或精度阈值[^23^]。\n);\n参数说明\nimage：输入图像，必须是单通道灰度图像。\ncorners：角点的初始坐标（输入）和优化后的坐标（输出）。初始坐标通常由 findChessboardCorners 或 goodFeaturesToTrack 提供。\nwinSize：搜索窗口的一半尺寸，决定了角点优化时考虑的区域范围。\nzeroZone：死区的一半尺寸，用于避免搜索区域的中心部分。值为 (-1, -1) 表示没有死区。\ncriteria：迭代终止条件，通常设置为 TermCriteria::EPS + TermCriteria::MAX_ITER，表示达到指定精度或最大迭代次数时停止。\n5.calibrateCamera 在 OpenCV 中，cv::calibrateCamera 是一个用于相机标定的函数，通过一系列棋盘格图像来计算相机的内参和外参，以及畸变系数。以下是关于 cv::calibrateCamera 的使用方法和一个完整的示例代码。\ndouble cv::calibrateCamera(\nInputArrayOfArrays objectPoints, // 三维空间中的点坐标（通常是棋盘格的角点）\nInputArrayOfArrays imagePoints, // 图像中的对应点坐标（棋盘格角点的图像坐标）\nSize imageSize, // 图像的尺寸\nInputOutputArray cameraMatrix, // 输出的相机内参矩阵\nInputOutputArray distCoeffs, // 输出的畸变系数\nOutputArrayOfArrays rvecs, // 每幅图像的旋转向量\nOutputArrayOfArrays tvecs, // 每幅图像的平移向量\nint flags = 0, // 标定选项\nTermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON) // 迭代终止条件\n);\n参数说明\nobjectPoints：三维空间中的点坐标，通常是棋盘格的角点。对于每幅图像，这些点的坐标是相同的。\nimagePoints：检测到的棋盘格角点的图像坐标。\nimageSize：图像的尺寸（宽和高）。\ncameraMatrix：相机内参矩阵，输出结果。\ndistCoeffs：畸变系数，输出结果。\nrvecs：每幅图像的旋转向量，表示相机的旋转。\ntvecs：每幅图像的平移向量，表示相机的平移。\nflags：标定选项，例如 CALIB_FIX_PRINCIPAL_POINT、CALIB_FIX_ASPECT_RATIO 等。\ncriteria：迭代优化的终止条件。\n6.find4QuardCornerSubpix//用于优化角点坐标 cv::find4QuadCornerSubpix 是一个用于精确定位四边形四个角点亚像素位置的函数。它通常在已经通过其他方法（如 cv::goodFeaturesToTrack 或 cv::cornerHarris）粗略定位角点之后使用，以提高角点检测的准确性\nbool cv::find4QuadCornerSubpix(\nInputArray img,\nInputOutputArray corners,\nSize region_size\n);\nimg：输入图像，应为灰度图，类型为 8-bit 或浮点型的单通道图像。\ncorners：输入/输出参数。初始的角点坐标作为输入，优化后的角点坐标作为输出。这是一个包含 (x, y) 坐标的浮点数向量。\nregion_size：搜索窗口大小。对于每个角点，将在这个区域内的子窗口中寻找更准确的位置。\n7.drawChessboardCorners cv::drawChessboardCorners 是 OpenCV 中用于在图像上绘制检测到的棋盘格角点的函数。它通常用于相机标定过程中，帮助可视化检测到的角点，以验证角点检测的准确性\nvoid cv::drawChessboardCorners(\nInputOutputArray image, // 目标图像，必须是8位彩色图像\nSize patternSize, // 棋盘格的内角点数，格式为 cv::Size(columns, rows)\nInputArray corners, // 检测到的角点数组，由 findChessboardCorners 函数输出\nbool patternWasFound // 指示是否成功检测到完整的棋盘格，应传入 findChessboardCorners 的返回值\n);\nimage：目标图像，必须是8位彩色图像。\npatternSize：棋盘格的内角点数，格式为 cv::Size(columns, rows)，其中 columns 和 rows 分别是棋盘格的列数和行数（注意是内角点数，而非方格数）。\ncorners：检测到的角点数组，由 findChessboardCorners 函数输出。\npatternWasFound：指示是否成功检测到完整的棋盘格，应传入 findChessboardCorners 的返回值。\n8.getAffineTransform 计算仿射变换矩阵\n仿射变换是一种二维坐标到二维坐标的线性变换，它保持了直线和平行性，但可以改变形状和大小。\n需要三个点\ncv::Mat cv::getAffineTransform(const Point2f src[], const Point2f dst[]);\ncv::Mat cv::getAffineTransform(InputArray src, InputArray dst);\nsrc：源图像中三角形顶点的坐标，需要提供三个点。\ndst：目标图像中相应三角形顶点的坐标，与 src 中的点一一对应。\n返回值：一个 2×3 的浮点型矩阵，表示从 src 到 dst 的仿射变换矩阵\n使用步骤\n计算仿射变换矩阵：通过 getAffineTransform 函数计算出源图像和目标图像之间的仿射变换矩阵。\n应用仿射变换：使用 warpAffine 函数将计算出的仿射变换矩阵应用到图像上，实现图像的仿射变换。\n9.getPerspectiveTransform 计算透视变换矩阵\n透视变换是一种更复杂的变换，它将一个平面映射到另一个平面，可以改变直线的平行性，从而实现更复杂的几何变换。\n透视变换矩阵是一个 3×3 的矩阵，形式如下：\n需要四个点\nOpenCV 中用于计算透视变换矩阵的函数。它通过给定的四个点对（源点和目标点）来计算从源图像到目标图像的透视变换矩阵。这个矩阵可以用于将图像从一个平面映射到另一个平面，实现更复杂的几何变换，例如将矩形图像映射为平行四边形或梯形。\ncv::Mat cv::getPerspectiveTransform(const Point2f src[], const Point2f dst[]);\ncv::Mat cv::getPerspectiveTransform(InputArray src, InputArray dst);\nsrc：源图像中的四个点的坐标，这些点必须是不共线的。\ndst：目标图像中对应的四个点的坐标，与 src 中的点一一对应。\n返回值：一个 3×3 的浮点型矩阵，表示从 src 到 dst 的透视变换矩阵。\n使用步骤\n定义源点和目标点：选择源图像和目标图像中的四个点。\n计算透视变换矩阵：使用 getPerspectiveTransform 函数计算透视变换矩阵。\n应用透视变换：使用 warpPerspective 函数将计算出的透视变换矩阵应用到图像上，实现图像的透视变换。\n10.warpAffine 是 OpenCV 中用于应用仿射变换的函数。它通过一个 2×3 的仿射变换矩阵，将输入图像映射到输出图像。这种变换可以实现平移、旋转、缩放和剪切等操作。\n仿射变换（Affine Transformation）：\n仿射变换是一种二维坐标到二维坐标的线性变换，保持直线和平行性，但可以改变形状和大小。\n可以实现平移、旋转、缩放和剪切等操作。\n矩阵维度：2×3 的矩阵。\nwarpAffine：\n适用于简单的几何变换，如平移、旋转、缩放和剪切。\n常用于局部变换，例如将图像的一部分旋转或缩放后嵌入到另一幅图像中。\n示例：将图像的一部分旋转 45 度并缩放 0.5 倍。\n需要三个点对 三个点不能共线\nvoid cv::warpAffine(\nInputArray src, // 输入图像\nOutputArray dst, // 输出图像\nInputArray M, // 2×3 的仿射变换矩阵\nSize dsize, // 输出图像的大小\nint flags = INTER_LINEAR,// 插值方法\nint borderMode = BORDER_CONSTANT, // 边界填充模式\nconst Scalar\u0026amp; borderValue = Scalar() // 边界填充值\n);\n参数说明\nsrc：输入图像，可以是任意通道数的单通道或多通道图像。\ndst：输出图像，其大小由 dsize 参数决定，类型与输入图像相同。\nM：2×3 的仿射变换矩阵，通常由 getAffineTransform 或其他方式计算得到。\ndsize：输出图像的大小，格式为 cv::Size(width, height)。\nflags：插值方法，常用的有：\nINTER_LINEAR：双线性插值（默认值）。\nINTER_NEAREST：最近邻插值。\nINTER_CUBIC：双三次插值。\nborderMode：边界填充模式，常用的有：\nBORDER_CONSTANT：用指定的 borderValue 填充边界。\nBORDER_REPLICATE：复制边缘像素。\nBORDER_REFLECT：反射边缘像素。\nborderValue：当 borderMode 为 BORDER_CONSTANT 时，用于填充边界的值，默认为黑色（0）。\n11.warpPerspective 适用于更复杂的几何变换，如透视校正、文档扫描、3D 效果等。\n常用于将图像从一个平面映射到另一个平面，例如将倾斜的文档图像校正为正面视图。\n示例：将矩形图像变换为梯形或平行四边形。\n需要四个点对 这四个点不能共线,且不能共面\n透视变换（Perspective Transformation）：\n透视变换是一种更复杂的变换，可以改变直线的平行性，从而实现更复杂的几何变换，例如将矩形变换为梯形或平行四边形。\n适用于模拟三维空间中的视角变化，例如文档扫描、透视校正等。\n矩阵维度：3×3 的矩阵。\nwarpPerspective 是 OpenCV 中用于应用透视变换的函数。它通过一个 3×3 的透视变换矩阵，将输入图像映射到输出图像。这种变换可以实现图像的倾斜、扭曲或视角变化，通常用于模拟三维空间中的视角变化\nvoid cv::warpPerspective(\nInputArray src, // 输入图像\nOutputArray dst, // 输出图像\nInputArray M, // 3×3 的透视变换矩阵\nSize dsize, // 输出图像的大小\nint flags = INTER_LINEAR,// 插值方法\nint borderMode = BORDER_CONSTANT, // 边界填充模式\nconst Scalar\u0026amp; borderValue = Scalar() // 边界填充值\n);\n参数说明\nsrc：输入图像，可以是任意通道数的单通道或多通道图像。\ndst：输出图像，其大小由 dsize 参数决定，类型与输入图像相同。\nM：3×3 的透视变换矩阵，通常通过 getPerspectiveTransform 函数计算得到。\ndsize：输出图像的大小，格式为 cv::Size(width, height)。\nflags：插值方法，常用的有：\nINTER_LINEAR：双线性插值（默认值）。\nINTER_NEAREST：最近邻插值。\nINTER_CUBIC：双三次插值。\nborderMode：边界填充模式，常用的有：\nBORDER_CONSTANT：用指定的 borderValue 填充边界。\nBORDER_REPLICATE：复制边缘像素。\nborderValue：当 borderMode 为 BORDER_CONSTANT 时，用于填充边界的值，默认为黑色（0）。\n计算透视变换矩阵：使用 getPerspectiveTransform 函数计算 3×3 的透视变换矩阵。\n**调用 **warpPerspective：将计算得到的矩阵应用到输入图像上，生成输出图像。\n12.norm\ncv::norm 函数用于计算矩阵或向量的范数。它是一个非常有用的工具，可以用于测量向量的长度、矩阵的大小，或者计算两个矩阵之间的差异\ndouble cv::norm(InputArray src1, int normType = NORM_L2, InputArray mask = noArray());\nsrc1：输入矩阵或向量。\nnormType：范数类型，默认为 NORM_L2，即欧几里得范数。其他常见范数类型包括：\nNORM_L1：L1 范数，即绝对值之和。\nNORM_L2：L2 范数，即欧几里得范数。\nNORM_INF：无穷范数，即最大绝对值。\nNORM_HAMMING：汉明距离。\nmask：可选参数，用于指定计算范数时的掩码。\n12.solvePnP bool solvePnP(InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess = false, int flags = SOLVEPNP_ITERATIVE);\nobjectPoints：目标物体的 3D 点坐标，类型为 vector\u0026lt;Point3f\u0026gt; 或 Mat。\nimagePoints：目标物体在图像中的 2D 投影点坐标，类型为 vector\u0026lt;Point2f\u0026gt; 或 Mat。\ncameraMatrix：相机的内参矩阵，类型为 Mat。\ndistCoeffs：相机的畸变系数，类型为 Mat。\nrvec：输出的旋转向量（Rodrigues 表示法），类型为 Mat。\ntvec：输出的平移向量，类型为 Mat。\nuseExtrinsicGuess：是否使用初始的外参估计值。如果为 true，则 rvec 和 tvec 会被用作初始猜测值。\nflags：指定 PnP 算法的类型，常见的选项包括：\nSOLVEPNP_ITERATIVE：使用非线性优化方法（默认）。\nSOLVEPNP_P3P：使用 P3P 算法（至少需要 3 个点）。\nSOLVEPNP_UPNP：使用 UPnP 算法。\nSOLVEPNP_DLS：使用 DLS 算法（至少需要 2 个点）。\nSOLVEPNP_AP3P：使用 AP3P 算法。\n13.norm 在 OpenCV 的 C/C++ 接口中，norm 函数用于计算数组的范数，它在图像处理和计算机视觉中非常有用，例如用于计算图像之间的差异或特征向量的长度。以下是关于 norm 函数的详细说明：\ndouble cv::norm(InputArray src1, InputArray src2 = noArray(), int normType = NORM_L2, InputArray mask = noArray());\n参数说明：\nsrc1：输入数组（图像或矩阵）。\nsrc2：可选的第二个输入数组，如果提供，则计算两个数组之间的范数。\nnormType：范数类型，默认为 NORM_L2，可选值包括：\nNORM_INF：无穷范数，即最大绝对值。\nNORM_L1：L1 范数，即绝对值之和。\nNORM_L2：L2 范数，即欧几里得范数。\nNORM_L2SQR：L2 范数的平方。\nNORM_HAMMING：汉明范数，适用于二进制数据。\nNORM_HAMMING2：汉明范数的变体。\nNORM_MINMAX：归一化范数。\nmask：可选的掩码，用于指定哪些元素参与计算。\nAPI区别和异同 minAreaRect 和 fitEllipse 是 OpenCV 中用于轮廓拟合的两种不同方法，它们的主要区别如下： 1. 功能定义\nminAreaRect：用于计算能够完全包围输入点集（通常是轮廓）的最小面积矩形。这个矩形可以是旋转的，因此能够更好地适应不规则形状。\nfitEllipse：用于拟合一个椭圆，使其最优地匹配输入的点集（通常是轮廓）。这个椭圆能够更好地描述轮廓的形状特征。\n2. 返回值\nminAreaRect：返回一个 RotatedRect 对象，包含以下信息：\n矩形的中心点坐标。\n矩形的宽度和高度。\n矩形的旋转角度。\nfitEllipse：返回一个椭圆的参数，包括：\n椭圆的中心点坐标。\n椭圆的主轴和次轴长度。\n椭圆的旋转角度。\n3. 使用场景\nminAreaRect：适用于需要最小面积矩形来包围轮廓的场景，例如目标检测、物体定位等。它能够提供更紧凑的包围形状。\nfitEllipse：适用于需要描述轮廓的形状特征或进行椭圆拟合的场景，例如在医学图像分析中拟合细胞形状。\n4. 输入要求\nminAreaRect：输入为一组二维点集（通常是轮廓的点集）。\nfitEllipse：输入同样为一组二维点集，但要求点集的数量至少为 5 个。\n5. 输出形状\nminAreaRect：输出是一个旋转矩形，可以通过 cv2.boxPoints 获取其四个顶点。\nfitEllipse：输出是一个椭圆，可以通过 cv2.ellipse 绘制。\n总结\n如果目标是找到最小面积的矩形包围框，选择 minAreaRect。\n如果目标是拟合一个椭圆来描述轮廓的形状，选择 fitEllipse。\n希望这些信息能帮助你理解两者的区别。\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/opencv%E7%9F%A5%E8%AF%86%E5%BA%93-%E6%95%B4%E7%90%86/","summary":"\u003ch1 id=\"opencv知识库---整理\"\u003eopencv知识库---整理\u003c/h1\u003e\n\u003ch2 id=\"基本概念\"\u003e基本概念\u003c/h2\u003e\n\u003cp\u003e预处理\u003c/p\u003e\n\u003cp\u003e按我的话来说，所谓预处理，就是在图像还未真正进行识别处理前，对图像进行简易、全局的处理。这一块通常在产生图片时进行操作，不属于我们\u003ca href=\"https://so.csdn.net/so/search?q=%E8%A7%86%E8%A7%89%E8%AF%86%E5%88%AB\u0026amp;spm=1001.2101.3001.7020\"\u003e视觉识别\u003c/a\u003e的重点（但是预处理真的很重要）。这里仅简单介绍我们使用的两个预处理过程。\u003c/p\u003e\n\u003cp\u003e降低曝光度\u003c/p\u003e","title":"opencv知识库---整理"},{"content":"leetcode //to_string()//数字转字符串\n//stoi()字符串转数字\n//string(1,char)字符转数字//string构造函数\n1LL会在运算时把后面的临时数据扩容成long long类型，再在赋值给左边时转回int类型。\narray\u0026lt;int, 26\u0026gt; array：这是 C++ 标准库中的一个模板类，位于 \u0026lt;array\u0026gt; 头文件中。它是一个固定大小的容器，类似于传统的 C++ 数组，但提供了更多功能和安全性。\nint：表示数组中存储的元素类型是整数。\n26：表示数组的大小，即数组中有 26 个元素\nunordered_map 在C++中，std::unordered_map 是一个关联容器，用于存储键值对（key-value pairs）。如果你想检查 std::unordered_map 是否包含某个键（key），可以使用 find() 方法或 count() 方法。虽然 C++20 引入了 contains() 方法，但如果你使用的是 C++20 之前的版本，就需要用其他方式来实现。\n使用 find() 方法 find() 方法会在容器中查找指定的键。如果找到了，返回一个指向该键值对的迭代器；如果没有找到，返回 end() 迭代器。\n2.使用 count() 方法\ncount() 方法会返回键在容器中出现的次数（对于 unordered_map，只能是 0 或 1）\n3.使用 C++20 的 contains() 方法\n如果你使用的是 C++20 或更高版本，可以直接使用 contains() 方法。它会返回一个布尔值，表示容器是否包含指定的键。\n// 注意不要直接 += cnt[sj-k]，如果 sj-k 不存在，会插入 sj-k\nstatic constexpr int directions[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; 这种定义通常用于二维平面的搜索问题，如迷宫搜索、棋盘游戏、网格路径搜索等。\n通过遍历这个数组，可以方便地实现从当前位置向四个方向的移动。\nconstexpr** 的作用**：\nconstexpr 表示这个数组是一个编译时常量，它的值在编译时就已经确定，不能在运行时修改。\n这样可以提高代码的效率和安全性，同时避免在运行时动态分配内存。\nconstexpr 是 C++11 引入的一个非常强大的关键字，它的作用是声明一个“编译时常量表达式”，即在编译阶段就能确定其值的变量或函数。使用 constexpr 可以显著提高代码的效率和安全性，同时还能让代码更加清晰和易于维护。\n具体用法略\nlambada表达式 这段代码是一个使用C++17标准的lambda表达式，并且使用了递归和参数化捕获的特性\n[\u0026amp;]表示捕获当前作用域中的所有变量\nthis auto\u0026amp;\u0026amp; dfs：这是C++17中引入的参数化捕获特性。this auto\u0026amp;\u0026amp;表示捕获当前lambda对象的引用，允许lambda在递归调用时引用自身。\n-\u0026gt; void：表示这个lambda表达式没有返回值\nauto dfs = [\u0026amp;](this auto\u0026amp;\u0026amp; dfs, int i) -\u0026gt; void {\nif (i == nums.size()) {\nans++;\nreturn;\n}\n};\nLambda 表达式是一种强大的工具，它结合了简洁性、匿名性、闭包特性以及与 STL 算法的无缝集成。它在现代 C++ 编程中被广泛应用，尤其是在需要定义简单函数、捕获上下文或实现递归时。\n1. 简洁性\n2. 捕获上下文\nLambda 表达式可以捕获外部变量（通过 [\u0026amp;] 或 [=]），这使得它能够直接访问和修改外部作用域的变量，而无需通过参数传递。\n3. 匿名性\n4. 支持闭包\nLambda 表达式本质上是一种闭包，它可以捕获外部变量并将其封装起来。这使得 Lambda 表达式可以作为函数对象（functor）使用，而无需显式定义类。\n5.支持递归\n从 C++17 开始，Lambda 表达式可以通过 this auto\u0026amp;\u0026amp; 捕获自身，从而实现递归调用。这使得 Lambda 表达式可以用于复杂算法（如深度优先搜索、动态规划等）\n6.与 STL 算法结合\nLambda 表达式与 C++ 标准库中的算法（如 std::sort、std::for_each、std::transform 等）结合得非常好，可以实现非常简洁的代码。\n7. 减少代码冗余\n在某些场景下，使用 Lambda 表达式可以避免定义多个小函数，从而减少代码冗余。例如，在多线程编程中，Lambda 表达式可以直接捕获线程需要的上下文。\nemplace_back contains 在C++中，std::unordered_set 是一个关联容器，用于存储唯一的元素。从C++20开始，std::unordered_set 提供了一个成员函数 contains，用于检查容器中是否包含某个元素。这是一个非常方便的函数，可以替代之前的 find 或 count 方法。\nstd::unordered_set::contains 的用法 函数原型\ncpp复制\n1 bool contains(const key_type\u0026amp; key) const; 参数：key 是要检查的元素。\n返回值：如果容器中包含该元素，则返回 true；否则返回 false。\nlower_bound(nums.begin(),nums.end(),target); lower_bound() 是 C++ 标准库中的一个函数，它在有序容器（如 std::vector、std::array、std::deque 等）中查找不小于给定值的第一个元素。这个函数使用二分查找算法，因此它的查找效率是 O(log n)。\nmemset 它定义在 \u0026lt;cstring\u0026gt;（C++）或 \u0026lt;string.h\u0026gt;（C）头文件中\nmemset 用于在内存中填充指定的字节值.\nvoid* memset(void* dest, int value, size_t count);\ndest:\n指向目标内存区域的指针。\n该内存区域将被填充。\nvalue:\n要填充的字节值。\n注意：value 是一个 int 类型，但它会被解释为一个 单字节值（即只使用其最低的 8 位）。因此，value 的有效范围是 0 到 255。\ncount:\n要填充的字节数\n指定从dest开始的内存区域中有多少字节需要被填充\n返回值:\n返回目标内存区域的指针,方便链式调用\n是一个底层的内存操作函数\n常见用途: 初始化内存区域\n清空内存\nsizeof sizeof 是 C++ 中的一个运算符，用于获取变量、类型或表达式的大小（以字节为单位）。它是一个编译时运算符，因此它的结果在编译时就已经确定，不会在运行时计算。\n函数的声明如下： Vector 1.assign\nassign 用于重新分配容器的内容，会清空当前容器，并用新的内容填充。\n2.resize\nresize 用于调整容器的大小，同时可以指定新元素的默认值。\n3.reverse\nreverse 并不是 std::vector 的成员函数，而是 C++ 标准库中的一个算法，用于反转容器中的元素顺序。\n4.reserve\nreserve 用于预留容器的内存空间，但不会改变容器的大小\nmax_element 在 C++ 中，max_element 是标准模板库（STL）中的一个算法函数，定义在头文件 \u0026lt;algorithm\u0026gt; 中。它用于查找指定范围内的最大元素。\nmax_element 的第一个和第二个参数分别是迭代器，表示要查找的范围的开始和结束。\n它返回一个迭代器，指向范围内的最大元素。\nreduce std::reduce 是C++17中引入的一个算法，它位于 \u0026lt;numeric\u0026gt; 头文件中。它类似于 std::accumulate，但提供了更好的并行化支持。\n：将一个范围内的元素通过指定的二元操作符进行归并（reduce）\n//默认使用加法\n在代码中，dfs 函数的参数 const string\u0026amp; s 使用了引用传参，这是出于**性能优化**和**语义清晰**的考虑。以下是详细解释： 1. 性能优化：避免不必要的拷贝\n在C++中，传递大型对象（如字符串、向量等）时，直接传递会触发**拷贝构造函数**，导致对象被复制一份。对于字符串 s，如果直接传递，每次递归调用都会复制整个字符串，这会带来不必要的开销，尤其是在字符串较长时。\n例如：\n1 void dfs(string s, int i); // 直接传递 每次调用 dfs 时，都会复制整个字符串 s，这会导致时间复杂度和空间复杂度显著增加。\n而使用引用传参：\n1 void dfs(const string\u0026amp; s, int i); // 引用传参 这种方式不会复制字符串，而是直接传递原始字符串的引用。这样可以显著减少内存占用和拷贝时间，提高程序的运行效率。\n2. 语义清晰：明确字符串不会被修改\n在 dfs 函数中，字符串 s 是输入参数，且在递归过程中不需要修改它。使用 const string\u0026amp; 表示：\n只读访问：const 修饰符表明 s 在函数内部不会被修改，这有助于代码的可读性和安全性。\n明确意图：引用传参表明 s 是一个共享的输入数据，而不是每次递归调用时的独立副本。\n这种写法清晰地表达了函数的语义：dfs 函数只是对输入字符串 s 进行读取操作，而不会修改它。\n3. 对比：直接传递 vs 引用传递\n假设字符串 s 的长度为 n，递归深度为 n：\n直接传递：每次递归调用都会复制整个字符串，总的时间复杂度为 O(n^2)，空间复杂度也为 O(n^2)。\n引用传递：每次递归调用只是传递一个引用，时间复杂度为 O(n)，空间复杂度为 O(n)（主要来自递归栈）。\n因此，使用引用传参可以显著优化性能，尤其是在处理大字符串时。\n4. 总结\n在 dfs 函数中，使用 const string\u0026amp; s 的原因如下：\n性能优化：避免不必要的字符串拷贝，减少时间和空间开销。\n语义清晰：明确字符串是只读的输入参数，不会被修改。\n最佳实践：在C++中，对于大型对象（如字符串、向量等），通常推荐使用引用传参，以提高效率。\n这种写法是C++编程中的常见优化技巧，尤其适用于递归函数和深度优先搜索场景。\nperror函数 perror 是一个在 C 语言中常用的函数，用于打印错误信息。它属于标准库 \u0026lt;stdio.h\u0026gt;，主要用于将错误信息输出到标准错误输出（通常是屏幕）。\nvoid perror(const char *s);\nperror 常用于处理系统调用或库函数失败时的错误。当这些函数失败时，它们通常会将错误码存储在 errno 中，而 perror 可以帮助开发者快速定位问题。\ntypdef函数 C语言允许用户使用 typedef 关键字来定义自己习惯的数据类型名称，来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等\n为基本数据类型定义新的类型名 为自定义数据类型（结构体、共用体和枚举类型）定义简洁的类型名称 typedef struct tagNode { char *pItem; pNode pNext; } *pNode;\n其实问题并非在于 struct 定义的本身，大家应该都知道，C 语言是允许在结构中包含指向它自己的指针的，我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢？其实，根本问题还是在于 typedef 的应用。\n在上面的代码中，新结构建立的过程中遇到了 pNext 声明，其类型是 pNode。这里要特别注意的是，pNode 表示的是该结构体的新别名。于是问题出现了，在结构体类型本身还没有建立完成的时候，编译器根本就不认识 pNode，因为这个结构体类型的新别名还不存在，所以自然就会报错。因此，我们要做一些适当的调整，比如将结构体中的 pNext 声明修改成如下方式：\n解决办法\n1.在struct前加typdef\n2.将struct与typdef分开定义\n为数组定义简洁的类型名称 为指针定义简洁的名称 typedef 是用来定义一种类型的新别名的，它不同于宏，不是简单的字符串替换\nassert函数 assert 是 C 语言中一个非常有用的调试工具，用于在程序运行时检查条件是否为真。如果条件为假（即表达式的结果为 0），程序会终止运行，并打印一条错误信息，指出断言失败的位置。\nC语言和C++的最大数据结构和最小数据结构 头文件都问 \u0026lt;limits.h\u0026gt;\nC语言最大数据结构\nINT_MAX\nC语言最小数据结构\nINT_MIN\nC++语言最大数据结构\nINT32_MAX\nC++语言最小数据结构\nINT32_MIN\nuint64_t 是一种数据类型，通常用于表示无符号的64位整数\n定义 它是C语言和C++语言中定义的一种标准整数类型。\n在C语言中，uint64_t 是通过头文件 \u0026lt;stdint.h\u0026gt; 定义的。\n在C++语言中，uint64_t 是通过头文件 \u0026lt;cstdint\u0026gt; 定义的。\n特点 无符号：uint64_t 是无符号整数类型，这意味着它不能表示负数，只能表示非负整数。\n64位：uint64_t 占用64位（8字节）的存储空间，因此它可以表示的数值范围是从0到 264−1，即从0到18446744073709551615。\n平台无关性：uint64_t 是一种固定宽度的整数类型，它的大小在所有支持它的平台上都是固定的，不会因平台的不同而改变。\n使用场景 大整数计算：当需要处理较大的整数时，uint64_t 是一个合适的选择。例如，在处理大文件的偏移量、大数组的索引或者大范围的计数器时，uint64_t 可以提供足够的存储空间。\n跨平台开发：在跨平台的程序中，使用 uint64_t 可以确保整数的大小在不同的平台上保持一致，避免因平台差异导致的错误。\n性能优化：在某些情况下，使用 uint64_t 可以提高程序的性能。例如，在进行位运算或整数运算时，64位整数的运算速度可能会比32位整数更快。\n","permalink":"https://hydarealman.github.io/wander/posts/2026/06/leetcode_/","summary":"\u003ch1 id=\"leetcode\"\u003eleetcode\u003c/h1\u003e\n\u003cp\u003e//to_string()//数字转字符串\u003c/p\u003e\n\u003cp\u003e//stoi()字符串转数字\u003c/p\u003e\n\u003cp\u003e//string(1,char)字符转数字//string构造函数\u003c/p\u003e\n\u003cp\u003e1LL会在运算时把后面的临时数据扩容成long \u003ca href=\"https://so.csdn.net/so/search?q=long%E7%B1%BB%E5%9E%8B\u0026amp;spm=1001.2101.3001.7020\"\u003elong类型\u003c/a\u003e，再在赋值给左边时转回int类型。\u003c/p\u003e","title":"leetcode_"},{"content":"测试笔记 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%B5%8B%E8%AF%95%E7%AC%94%E8%AE%B0/","summary":"\u003ch1 id=\"测试笔记\"\u003e测试笔记\u003c/h1\u003e","title":"测试笔记"},{"content":"Claude Code CLI: cli是一种通过输入文本指令来与电脑操作系统或软件进行交互的方式及\n斜杠命令: 核心命令 /help 显示所有可用命令、快捷键和帮助信息，是入门和遗忘时的好帮手。\n/init 在项目根目录生成 CLAUDE.md 文件。Claude 会在每次会话中读取此文件，用于存储项目规范、技术栈等持久化信息\n/clear 硬重置,清空所有对话历史,开始一个全新的会话\n/model 在会话中切换模型\n开发辅助: /plan 进入计划模式,Claude会先给出执行方案,经你确认后再动手,适合复杂任务\n/btw 在主任务进行时,并行提出一个不相关的问题,不打断当前流程\n/rewind 回退到之前的某个节点,可以同时回退代码和对话状态\n/simplify 对代码进行三重审查,寻找可以简化的地方\n监控与配置 /cost 查看当前会话已经消耗的Token费用\n/context 查看当前已加载的上下文信息\n/memory 直接编译CLAUDE.md文件\n/doctor 诊断Claude Code的安装和配置问题\n/config 打开全局配置\n/login / logout 进行会话的身份验证或断开连接\n键盘快捷键 Agent skills ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/claude-code/","summary":"\u003ch1 id=\"claude-code\"\u003eClaude Code\u003c/h1\u003e\n\u003ch2 id=\"cli\"\u003eCLI:\u003c/h2\u003e\n\u003cp\u003ecli是一种通过输入文本指令来与电脑操作系统或软件进行交互的方式及\u003c/p\u003e\n\u003ch2 id=\"斜杠命令\"\u003e斜杠命令:\u003c/h2\u003e\n\u003ch3 id=\"核心命令\"\u003e核心命令\u003c/h3\u003e\n\u003ch4 id=\"help\"\u003e/help\u003c/h4\u003e\n\u003cp\u003e显示所有可用命令、快捷键和帮助信息，是入门和遗忘时的好帮手。\u003c/p\u003e\n\u003ch4 id=\"init\"\u003e/init\u003c/h4\u003e\n\u003cp\u003e在项目根目录生成 CLAUDE.md 文件。Claude 会在每次会话中读取此文件，用于存储项目规范、技术栈等持久化信息\u003c/p\u003e","title":"Claude Code"},{"content":"jakac5机械臂日志 开发日志: 6月9日 配置moveit_resources-ros2的官方例程 跑通单臂的rviz\n6月10日 跑通双臂的rviz\n6月11日 \u0026amp; 6月12日 准备考试 暂时搁置\n6月13日 熟悉整个代码框架\n6月14日 \u0026amp; 6月15日 复习数电\n6月16日 实现jaka c5 双臂防碰撞轨迹规划执行\n遇到的bug:\n双机械臂防碰撞 Demo 调试总结 本文档记录了搭建 JAKA C5 双机械臂防碰撞演示项目过程中遇到的关键 Bug 及其解决方案。\nBug 1：轨迹时间戳全为零，Plan 成功但 Execute 失败 现象：\nRViz 中 Plan 能找到无碰撞路径（OMPL 规划成功） Plan \u0026amp; Execute 时机械臂不动 move_group 日志：Time between points 0 and 1 is not strictly increasing, it is 0.000000 手动发送带时间戳的轨迹到 controller 可以正常执行 根因： AddTimeOptimalParameterization（时间最优轨迹时间参数化）响应适配器没有被正确加载。\n在 ROS2 Humble 的 MoveIt2 中，该类注册在：\n1 default_planner_request_adapters/AddTimeOptimalParameterization 但我们的 ompl_planning.yaml 中把它放在了 response_adapters，且用了错误的命名空间：\n1 2 # ❌ 错误配置（双机械臂套件常见错误） response_adapters: \u0026#34;default_planner_response_adapters/AddTimeOptimalParameterization ...\u0026#34; default_planner_response_adapters/ 这个命名空间下没有注册任何插件，导致 AddTimeOptimalParameterization 加载失败，轨迹的时间戳全为零。\n对比单机械臂 panda_moveit_config（可正常工作）：\n1 2 # ✅ 正确配置 request_adapters: \u0026#34;... default_planner_request_adapters/AddTimeOptimalParameterization\u0026#34; 解决： 将 AddTimeOptimalParameterization 移到 request_adapters 链中，使用正确的命名空间 default_planner_request_adapters/。\n教训：\n不要混用 default_planner_request_adapters/ 和 default_planner_response_adapters/ 所有 MoveIt2 Humble 的 motion planning adapter 都注册在 default_planner_request_adapters/ 下 用 cat /opt/ros/humble/share/moveit_ros_planning/planning_request_adapters_plugin_description.xml 可查看所有已注册的适配器 Bug 2：RViz 拖动交互标记只能旋转不能平移 现象：\nRViz 中拖动机械臂末端的交互标记时，只能改变末端姿态（旋转），不能改变位置（平移） 之前可以正常拖动，切换为 OMPL 后出现问题 根因： 在 SRDF 中定义了末端执行器（end effector）组，但配置不完整——定义了 parent 和 group 但不匹配，导致 MoveIt 的逆运动学（IK）求解器无法正确计算末端位置对应的关节角。\n解决： 删除了不完整的末端执行器定义，回归之前的简洁配置。在没有末端执行器的情况下，RViz 使用规划组（left_arm / right_arm）的 tip link 作为交互标记的参考点，IK 求解正常工作。\nBug 3：CHOMP 规划器被误选为默认规划器 现象：\nmove_group 日志显示使用了 chomp_interface/CHOMPPlanner CHOMP 规划失败或行为不符合预期 明确配置了 planning_plugin: \u0026quot;ompl_interface/OMPLPlanner\u0026quot; 但无效 根因： MoveIt2 的规划器选择逻辑：当系统中安装了多个规划器插件时（CHOMP、OMPL、STOMP 等），MoveIt 可能选择非预期的默认规划器。planning_plugin 参数在某些配置路径下被忽略。\n解决： 卸载 CHOMP 规划器包，确保 OMPL 是唯一可用的规划器：\n1 sudo apt remove ros-humble-moveit-planners-chomp Bug 4：move_group 启动崩溃 — YAML 参数格式错误 现象：\n1 [FATAL] Cannot have a value before ros__parameters move_group 进程启动后立即崩溃。\n根因： move_group_params.yaml（或类似命名的 ROS 参数文件）格式不正确。ROS2 Humble 的 rclcpp 要求参数文件必须以 /** 节点名开头，然后是 ros__parameters 键：\n1 2 3 4 5 6 7 # ❌ 错误格式 planning_plugin: \u0026#34;ompl_interface/OMPLPlanner\u0026#34; # ✅ 正确格式 /**: ros__parameters: planning_plugin: \u0026#34;ompl_interface/OMPLPlanner\u0026#34; 解决： 最终放弃了独立的参数 YAML 文件，改为在 launch 文件中通过 Python 字典传递参数（moveit_config.to_dict()），这由 MoveItConfigsBuilder 自动处理。\nBug 5：Ros2ControlManager 崩溃 — 插件未找到 现象：\n1 [FATAL] [moveit_ros_control_interface]: The \u0026#39;moveit_ros_control_interface/Ros2ControlManager\u0026#39; plugin failed to load move_group 启动后崩溃。\n根因： Ros2ControlManager 需要特定的插件库，在当前 ROS2 Humble apt 安装的版本中不可用或未正确注册。\n解决： 切换为 MoveItSimpleControllerManager，配置 FollowJointTrajectory 动作接口：\n1 2 3 4 5 6 7 8 9 10 moveit_controller_manager: moveit_simple_controller_manager/MoveItSimpleControllerManager moveit_simple_controller_manager: controller_names: - left_arm_controller - right_arm_controller left_arm_controller: type: FollowJointTrajectory action_ns: follow_joint_trajectory joints: [left_joint_1, ..., left_joint_6] Bug 6：OMPL 配置 \u0026ldquo;Could not find the planner configuration \u0026lsquo;RRTConnect\u0026rsquo;\u0026rdquo; 现象： move_group 日志报错找不到 RRTConnect 规划器配置。\n根因： ompl_planning.yaml 中设置了一个指向不存在配置名称的 default_planner_config：\n1 2 # ❌ 错误 default_planner_config: RRTConnect 实际的配置名是 RRTConnectkConfigDefault（带 kConfigDefault 后缀）。\n解决： 删除 default_planner_config 和 longest_valid_segment_fraction 键（这些是 MoveIt1 的配置项，MoveIt2 中不再支持或移动到其他位置）。\n总结 Bug 类别 严重程度 修复方式 AddTimeOptimalParameterization 命名空间错误 配置 🔴 阻塞 移到 request_adapters + 正确命名空间 交互标记无法平移 配置 🟡 中等 删除不完整的 end effector 定义 CHOMP 被误选 依赖 🟡 中等 卸载 CHOMP 包 YAML 参数格式错误 配置 🔴 崩溃 改用 Python 字典传参 Ros2ControlManager 加载失败 插件 🔴 崩溃 改用 MoveItSimpleControllerManager OMPL 配置名无效 配置 🟡 中等 删除无效的 default_planner_config 核心教训： 参考官方单机械臂配置（panda_moveit_config）是验证双机械臂配置正确性的最可靠方法。当双机械臂配置出现问题时，逐项对比与单机械臂配置的差异是最有效的调试策略。\n知识点: 碰撞检测 MoveIt 在做运动规划时，要保证机械臂在运动中不撞到任何东西。为此它需要检查： 机械臂自己的连杆之间是否会自碰撞（self-collision） 机械臂是否碰到了环境中的物体（障碍物）\nMoveIt 假设任何两个连杆都可能碰撞，会对所有连杆对做检测。但这样做非常消耗计算资源。实际上很多连杆对是永远不可能碰撞的——disable_collisions 就是用来告诉 MoveIt：\u0026ldquo;这对不用查了\u0026rdquo;，从而提高规划效率。\nreason的三种取值\nURDF 只定义机器人的几何、运动学、惯性、碰撞模型（连杆、关节、形状等）。 SRDF 补充高层语义：哪些关节可以一起运动（规划组）、预设姿态、哪些连杆之间永远不检查碰撞、末端执行器定义、虚拟关节等\n– 规划组 定义一组关节/连杆，用于运动规划。\n\u0026lt;group_state\u0026gt; – 预设关节状态 某个组定义一组关节的命名目标值，便于快速调用（如回家、伸展、闭合手爪）\n\u0026lt;virtual_joint\u0026gt; – 虚拟关节 将机器人连接到外部坐标系（如世界坐标系、基座固定点）。 type=\u0026ldquo;floating\u0026rdquo; 表示机器人可以在世界坐标系中自由移动（一般用于移动基座或仿真中的浮动基座）。对于固定基座机械臂，这里只是建模方便，实际运动中不会产生位移。\n\u0026lt;disable_collisions\u0026gt; – 禁用碰撞检测 指定两个连杆之间永远不需要检查碰撞（提高规划效率）\n\u0026lt;end_effector\u0026gt; – 末端执行器定义 将某个组标记为末端执行器，并关联到父组（手臂）。\npassive_joint\u0026gt; – 被动关节 在 内部标记一个非驱动的从动关节（如耦合手指的第二关节）。该关节随主动关节运动，不需要额外控制。\nOMPL（Open Motion Planning Library） OMPL中的算法都属于基于采样的运动规划,核心思路是在机器人的关键空间 (Configuration Space)中随机采样,然后尝试把这些随机点连接成一条从起点到终点的无碰撞路径\n参数调优的本质，是在探索速度(快速找到一条可行路径)和路径质量 (路径短不短,平滑不平滑)之间做权衡\nrange: 它代表每次扩展时,从当前节点向外探索的最大步长 较大 : 树生长快,探索范围广，规划速度快 适用 空旷环境,快速 找路 较小 : 探索更精细,路径更平滑,但规划变慢 适用 狭窄通道,高精度需求 0.0 : OMPL会根据状态空间自动计算合适的值 适用 大多数情况下的安全选择\n单树采样: 代表算法: RRT, RRTstar, TRRT, STRIDE, KPIECE, EST, SBL, PDST, ProjEST, LBTRRT 特点: 从起点开始生长一棵树，适合低维或中等维度空间\n双树采样: 代表算法: RRTConnect, BiTRRT, BiEST 特点: 同时从起点和终点生长两棵树，收敛快，常用于高维机械臂\n多查询/概率图 代表算法: PRM, PRMstar, LazyPRM, LazyPRMstar, SPARS, SPARStwo 特点: 先随机采样构建路图，再查询路径，适合多次规划\n最优路径: 代表算法: RRTstar, PRMstar, BFMT, FMT, TRRT 特点: 渐进地优化路径长度（或其它代价）\n任意时间优化 代表算法: APS (Anytime Path Shortening)\n特点: 先快速找到可行路径，然后在剩余时间内不断缩短\n轨迹规划: 代表算法: TrajOpt（基于梯度的局部优化）\n特点: 从初始猜测出发，通过非线性优化得到光滑无碰撞轨迹\n世界坐标系(根链接) 有且仅有一个根节点: URDF（统一机器人描述格式）在解析时强制要求整个模型呈树状结构 (Tree Structure),不能有环,也不能有多个根,如果没有word这个链接,左臂和右臂各自是一个独立的树,无法合并成一个合法的机器人模型文件\nword充当了左右两个独立机械臂的公共父级,通过它,两个原本独立的运动学 子树被焊接到了同一个坐标系下,满足了ROS对机器人模型的底层数据结构要求\n定义安装基准: 在word坐标系下,你通过你通过 和 xyz=\u0026ldquo;0 0.25 0\u0026rdquo; 定义了双臂基座的安装位置。这意味着： 左臂基座中心点位于 world 的 Y 轴负方向 0.25m 处。 右臂基座中心点位于 world 的 Y 轴正方向 0.25m 处。\n在TF(坐标变换)树中的特殊地位 所有变换的源头 静态变换发布: 在此URDF被加载到robot_state_publisher后,它会自动发布从 wordl到左右臂基座连杆的静态变换,这些变换时永恒不变的,为Moveit运动规划提供了绝对参考系\n物理仿真: 默认无质量刚体: 在物理引擎中,如果没有定义（惯性矩阵）和（碰撞体）,它会被引擎自动视为固定不动的\u0026quot;大地\u0026quot;,质量视为无穷大,位置绝对锁定\n千万不能加惯性参数: 这个\u0026quot;世界链接\u0026quot;会受重力影响直接向下坠落,导致整个双臂模型瞬间他先散架,因此根链接通过保持空链接,只作为纯数学参考点\nURDF(统一机器人描述格式): 全称：Unified Robot Description Format 职责：描述机器人的物理与几何属性，是 ROS 中最底层的模型文件\n内容: 连杆: Visual Collision Inertial 关节: origin parent/child axis limit\n谁在使用: Rviz Gazebo/lgnition/Moveit\nSRDF(语义机器人描述格式) : Moveit的\u0026quot;策略配置文件\u0026quot; 全称：Semantic Robot Description Format 职责: 基于URDF提供额外的\u0026quot;规划层信息\u0026quot;，专门服务于运动规划框架Moveit\n规划组(Group) 意义: MoveIt 不用关心底座（world）和基座（Link_00）的固定关节，只需要知道控制这 6 个旋转轴就能移动末端。\n预设姿态(Group State) 意义: 一键让手臂跳到指定角度，省去手动拖动滑块的麻烦，也是程序启动时的安全初始化姿势。\n碰撞禁用矩阵(Disable Collisions) 意义: 极大降低碰撞检测的 CPU 计算量，让规划速度翻倍。\n虚拟关节(Virtual Joints) 用于把机器人固定在浮动基座上（比如移动底盘）\n笛卡尔空间: xyz三维直角坐标系 rpy角: roll横滚角 pitch俯仰角 yaw偏航角\n描述对象: 末端执行器(手抓 吸盘 焊枪) 在物理位置的具体位置和朝向\n关节空间: 机械臂所有关节变量的集合,对于旋转关节,它是角度值(θ1, θ2 \u0026hellip; θn) 对于平移关节,它是位移值(d1, d2 \u0026hellip; dn)\n描述对象: 机械臂本身的整体构型\n正逆运动学 正运动学（Forward Kinematics, FK） 知道关节角度,求解末端位置 计算方向: 关节空间 -\u0026gt; 笛卡尔空间 齐次变换矩阵的顺序连乘 唯一解\n逆运动学（Inverse Kinematics, IK） 知道末端位置,求关节角度 计算方向: 笛卡尔空间 -\u0026gt; 关节空间 求解非线性超越方程组 零解 单解 多解 无穷解\nIK求解器 KDL MoveIt默认的IK求解器。它是一个基于牛顿-拉夫森迭代法的数值求解器\narm_group: kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin kinematics_solver_timeout: 0.005 kinematics_solver_attempts: 3\nTRAC-IK (更可靠的升级版)：一个为替代KDL而生的数值求解器。它通过并发运行两种算法（改进的KDL算法和SQP非线性优化）来提高求解成功率\narm_group: kinematics_solver: trac_ik_kinematics_plugin/TRAC_IKKinematicsPlugin kinematics_solver_timeout: 0.005 # 超时时间（秒） solve_type: Speed # 可选: Speed, Distance, Manip1, Manip2\nkinematics_solver_attempts: 3 # TRAC-IK 不需要此参数 IKFast (追求极致性能)：一个解析求解器，通过编译器生成针对特定机器人的C++代码，速度极快。 优点：速度极快（可达微秒级），能找到所有数学解。 缺点：配置极其复杂，依赖OpenRAVE环境。 配置方法：需要安装OpenRAVE，将URDF转换为DAE格式，再用IKFast生成代码，最后编译成MoveIt插件。 pick_ik (新一代选择)：一个较新的IK求解器，结合了全局优化（进化算法）和局部优化（梯度下降）。 优点：鲁棒且可定制，能更好地找到全局最优解。 配置示例:\narm_group: kinematics_solver: pick_ik/PickIkPlugin kinematics_solver_timeout: 0.05 mode: global position_threshold: 0.001\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/jakac5%E6%9C%BA%E6%A2%B0%E8%87%82%E6%97%A5%E5%BF%97/","summary":"\u003ch1 id=\"jakac5机械臂日志\"\u003ejakac5机械臂日志\u003c/h1\u003e\n\u003ch2 id=\"开发日志\"\u003e开发日志:\u003c/h2\u003e\n\u003ch3 id=\"6月9日\"\u003e6月9日\u003c/h3\u003e\n\u003cp\u003e配置moveit_resources-ros2的官方例程\n跑通单臂的rviz\u003c/p\u003e\n\u003ch3 id=\"6月10日\"\u003e6月10日\u003c/h3\u003e\n\u003cp\u003e跑通双臂的rviz\u003c/p\u003e\n\u003ch3 id=\"6月11日--6月12日\"\u003e6月11日 \u0026amp; 6月12日\u003c/h3\u003e\n\u003cp\u003e准备考试 暂时搁置\u003c/p\u003e\n\u003ch3 id=\"6月13日\"\u003e6月13日\u003c/h3\u003e\n\u003cp\u003e熟悉整个代码框架\u003c/p\u003e","title":"jakac5机械臂日志"},{"content":"lab_plant ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/lab_plant/","summary":"\u003ch1 id=\"lab_plant\"\u003elab_plant\u003c/h1\u003e","title":"labplant"},{"content":"lab技术栈 三维重建: MVS（多视图立体） SfM（运动恢复结构） LIDAR SLAM\n机器学习: 传统机器学习：随机森林、支持向量机(SVM)、岭回归(BLUP) 深度学习：基于多层神经网络的端到端学习方法\n深度学习: 卷积神经网络 (CNN)：ResNet、U-Net、YOLO系列、Mask R-CNN 循环神经网络 (RNN)：LSTM、GRU Transformer：ViT (Vision Transformer)、Cropformer 点云神经网络：PointNet++ 时序卷积网络：TCN\n计算机视觉 目标检测与实例分割: YOLO系列,Mask R-CNN 语义分割: U-Net,DeepLabV3+ 实例分割：Mask R-CNN 图像分类与特征提取: ResNet,ViT (Vision Transformer) 密度估计: 密度回归网络 时序建模: LSTM GRU TCN (时间卷积网络)\n多模态融合: 多任务学习 跨模态注意力机制 数据融合 / 特征融合 / 结果融合\npca主成分分析法: 找到数据变化最大的几个方向,扔掉变化小的方向,实现降维去冗余 PCA（Principal Component Analysis，主成分分析）是一种降维技术,它通过数学变换,把原始数据中相互关联的多个变量,转换成少数几个互不相关的\u0026quot;主成分\u0026quot;,并且尽可能保留原始数据 降维：把100维数据压缩到2-3维，方便可视化和后续处理 去冗余：消除变量之间的相关性，提取独立信息 特征提取：找出数据中最重要的变化方向\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/lab%E6%8A%80%E6%9C%AF%E6%A0%88/","summary":"\u003ch1 id=\"lab技术栈\"\u003elab技术栈\u003c/h1\u003e\n\u003ch2 id=\"三维重建\"\u003e三维重建:\u003c/h2\u003e\n\u003cp\u003eMVS（多视图立体）\nSfM（运动恢复结构）\nLIDAR SLAM\u003c/p\u003e\n\u003ch4 id=\"机器学习\"\u003e机器学习:\u003c/h4\u003e\n\u003cp\u003e传统机器学习：随机森林、支持向量机(SVM)、岭回归(BLUP)\n深度学习：基于多层神经网络的端到端学习方法\u003c/p\u003e","title":"lab技术栈"},{"content":"QT QT控件: QPushBotton 作用: 可点击的按钮\nQLabel 作用: 显示文字或图片\nQSlider 作用: 滑块\nQTextEdit 作用: 带边框的分组框\nQProgress 作用: 进度条\nQWeight QWeight是Qt中所有用户界面对象的基类: 它提供最基本的窗口/控件功能: 尺寸,位置,鼠标键盘事件, 绘图,样式表，父子关系\nQMainWindow 补充: QMainWindow 继承自 QWidget，但增加了菜单栏、工具栏、状态栏、停靠窗口等布局区域。\nQMainWindow 提供了一个预定义布局的主窗口框架\n┌─────────────────────────────────┐ │ 菜单栏 (Menu Bar) │ ├─────────────────────────────────┤ │ 工具栏 (Tool Bars) │ ├──────────────────┬──────────────┤ │ │ │ │ 停靠窗口区域 │ 中心部件 │ │ (Dock Widgets) │ (Central │ │ │ Widget) │ │ │ │ ├──────────────────┴──────────────┤ │ 状态栏 (Status Bar) │ └─────────────────────────────────┘\n1. 中心部件相关 void setCentralWidget(QWidget *widget) 作用：设置窗口中央区域显示的部件（必须调用）。 参数：任何 QWidget 子类（如 QTextEdit、QTableWidget、自定义部件）。 示例： cpp QTextEdit *edit = new QTextEdit;setCentralWidget(edit); QWidget *centralWidget() const 作用：返回当前的中心部件，没有则返回 nullptr。\n2. 菜单栏相关 void setMenuBar(QMenuBar *menuBar) 作用：将指定的菜单栏设置为窗口的菜单栏。 注意：通常直接用 menuBar() 获取默认菜单栏，无需手动创建。 示例： cpp QMenuBar *myMenuBar = new QMenuBar(this);myMenuBar-\u0026gt;addMenu(\u0026ldquo;文件\u0026rdquo;);setMenuBar(myMenuBar); QMenuBar *menuBar() const 作用：返回窗口的菜单栏（如果没有则自动创建一个空的菜单栏）。\n3. 状态栏相关 void setStatusBar(QStatusBar *statusBar) 作用：设置状态栏。 示例： cpp QStatusBar *sb = new QStatusBar(this);sb-\u0026gt;showMessage(\u0026ldquo;就绪\u0026rdquo;);setStatusBar(sb); QStatusBar *statusBar() const 作用：返回状态栏（如果没有则自动创建）。\n4. 工具栏相关 void addToolBar(QToolBar *toolbar) 作用：添加一个工具栏，默认放在顶部区域。 示例： cpp QToolBar *toolBar = new QToolBar(\u0026ldquo;主要工具\u0026rdquo;);toolBar-\u0026gt;addAction(\u0026ldquo;打开\u0026rdquo;);addToolBar(toolBar); void addToolBar(Qt::ToolBarArea area, QToolBar *toolbar) 作用：将工具栏添加到指定区域： Qt::TopToolBarArea（顶部） Qt::BottomToolBarArea（底部） Qt::LeftToolBarArea（左侧） Qt::RightToolBarArea（右侧） void insertToolBar(QToolBar *before, QToolBar *toolbar) 作用：在某个已有工具栏之前插入新工具栏。 Qt::ToolBarArea toolBarArea(const QToolBar *toolbar) const 作用：返回指定工具栏当前所在的区域。 void setToolButtonStyle(Qt::ToolButtonStyle style) 作用：控制所有工具栏上的按钮样式（仅图标、仅文字、文字在图标下等）。\n5. 停靠窗口（QDockWidget）相关 void addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget) 作用：将停靠窗口添加到指定区域（左侧、右侧、顶部、底部）。 示例： cpp QDockWidget *dock = new QDockWidget(\u0026ldquo;文件列表\u0026rdquo;);dock-\u0026gt;setWidget(new QListWidget);addDockWidget(Qt::LeftDockWidgetArea, dock); void addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget, Qt::Orientation orientation) 作用：在分割停靠区域时指定布局方向。 void splitDockWidget(QDockWidget *first, QDockWidget *second, Qt::Orientation orientation) 作用：将两个停靠窗口以分割方式排列（水平或垂直）。 void tabifyDockWidget(QDockWidget *first, QDockWidget *second) 作用：将第二个停靠窗口以标签页形式与第一个合并。 void setDockOptions(DockOptions options) 作用：设置停靠窗口的行为，例如： QMainWindow::AnimatedDocks（动画效果） QMainWindow::AllowTabbedDocks（允许标签页） QMainWindow::VerticalTabs（垂直标签）\n6. 布局与外观 void setCorner(Qt::Corner corner, Qt::DockWidgetArea area) 作用：指定哪个停靠区域可以占据窗口的角落。 示例：让右上角属于左侧停靠区： cpp setCorner(Qt::TopRightCorner, Qt::LeftDockWidgetArea); void setDocumentMode(bool enabled) 作用：启用“文档模式”（没有单独的工具栏/菜单栏边框，适合嵌入文档）。\n7. 状态保存与恢复（重要！） QByteArray saveState(int version = 0) const 作用：保存当前窗口所有工具栏、停靠窗口的位置、大小、可见性状态。 返回值：可以保存到文件或 QSettings。 bool restoreState(const QByteArray \u0026amp;state, int version = 0) 作用：恢复之前保存的状态。 示例： cpp // 保存QSettings settings(\u0026ldquo;MyCompany\u0026rdquo;, \u0026ldquo;MyApp\u0026rdquo;);settings.setValue(\u0026ldquo;mainWindow/state\u0026rdquo;, saveState());// 恢复restoreState(settings.value(\u0026ldquo;mainWindow/state\u0026rdquo;).toByteArray());\n8. 其他常用函数 void setIconSize(const QSize \u0026amp;iconSize) 作用：设置工具栏图标的大小。 void setToolTipDuration(int msec) 作用：设置工具提示显示的毫秒数。 QMenu *createPopupMenu() 作用：创建右击工具栏/停靠窗口时弹出的菜单（可重写以自定义）。\nQApplication 补充: QApplication 把自己存到全局变量 → QWidget::show() 通过全局变量找到 QApplication → 把自己注册进 QApplication 的窗口列表 → app.exec() 遍历这个列表分发事件。\n每个使用Qt的GUI程序有且只有一个QApplication对象 (或者它的兄弟QGuiApplication/QCoreApplication) 管理事件循环: 处理窗口刷新,鼠标点击,键盘输入等事件 初始化应用程序: 处理命令行参数,设置系统字体/样式 提供全局信息: 屏幕尺寸,剪贴板,样式主题,调色板 管理资源: 处理翻译文件(.qm),样式表(QSS),图标搜索路径\n事件循环 核心概念: exec()开启了一个无限循环,不断从系统消息队列中取出事件 (鼠标点击,键盘,重绘等) 并发送到对应的窗口部件\nQApplication::exec() 作用: 启动应用程序的主事件循环,没有它,窗口会一闪而过然后退出 阻塞行为: 调用后程序会一直运行,直到你调用quit()或关闭所有窗口\n典型用法:\nint main(int argc, char *argv[]) { QApplication app(argc, argv); MyMainWindow w; w.show(); return app.exec(); // 进入主事件循环 }\nQDialog::exec() 作用: 以模态方式显示一个对话框\nQT数据类型 QString – 字符串 作用：Unicode 字符串，Qt 的专用字符串类型，支持隐式共享（写时复制），效率高。\nQColor – 颜色 作用：存储 ARGB、HSV、CMYK 等颜色值，常用于绘图、样式表\nQByteArray – 字节数组 作用：存储原始字节数据（二进制或 8 位文本），常用于文件读写、网络传输。\nQList – 列表 最常用的容器，支持通过索引快速访问，尾部插入快。存储类型 T 必须是可复制的（QObject 子类不能直接存储，需用指针）。\nQVector – 动态数组 作用：与 QList 类似，但连续内存访问效率更高。在 Qt 6 中，QList 与 QVector 几乎统一，但习惯上仍可区分使用。\nQMap\u0026lt;K, V\u0026gt; – 映射（有序） 作用：键值对存储，按键排序。\nQHash\u0026lt;K, V\u0026gt; – 哈希表（无序） 作用：更快的查找，键不排序。\nQVariant – 通用值容器 作用：可存储任意 Qt 支持的类型（int, QString, QColor，甚至自定义类型需注册），用于模型/视图、设置项、信号槽跨类型传参。\nQRect / QRectF – 矩形 作用：存储整型坐标的矩形（x, y, width, height），常用于窗口几何、绘图区域。\nQPoint / QPointF – 点 作用：存储 (x, y) 坐标。\nQSize / QSizeF – 尺寸 作用：存储宽度和高度。\nQDateTime / QDate / QTime – 日期时间 处理日期、时间及时区\nQUrl – 统一资源定位符 解析和构建 URL，用于网络请求、本地文件路径（QUrl::fromLocalFile()）。\nQJsonDocument / QJsonObject / QJsonArray – JSON 解析和生成 JSON 数据，常用于 Web API 通信。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/qt/","summary":"\u003ch1 id=\"qt\"\u003eQT\u003c/h1\u003e\n\u003ch2 id=\"qt控件\"\u003eQT控件:\u003c/h2\u003e\n\u003ch3 id=\"qpushbotton\"\u003eQPushBotton\u003c/h3\u003e\n\u003cp\u003e作用: 可点击的按钮\u003c/p\u003e\n\u003ch3 id=\"qlabel\"\u003eQLabel\u003c/h3\u003e\n\u003cp\u003e作用: 显示文字或图片\u003c/p\u003e\n\u003ch3 id=\"qslider\"\u003eQSlider\u003c/h3\u003e\n\u003cp\u003e作用: 滑块\u003c/p\u003e\n\u003ch3 id=\"qtextedit\"\u003eQTextEdit\u003c/h3\u003e\n\u003cp\u003e作用: 带边框的分组框\u003c/p\u003e\n\u003ch3 id=\"qprogress\"\u003eQProgress\u003c/h3\u003e\n\u003cp\u003e作用: 进度条\u003c/p\u003e\n\u003ch2 id=\"qweight\"\u003eQWeight\u003c/h2\u003e\n\u003cp\u003eQWeight是Qt中所有用户界面对象的基类:\n它提供最基本的窗口/控件功能: 尺寸,位置,鼠标键盘事件,\n绘图,样式表，父子关系\u003c/p\u003e","title":"QT"},{"content":"rust仿真环境配置: wsl + ros2 + rust 1.以管理员身份打开PowerShell或CMD\n2.执行安装命令\nwsl \u0026ndash;install -d Ubuntu-24.04 3.设置用户名和密码\n列出所有的ubantu版本\nwsl -l -v\n进入想要的版本\nwsl -d Ubuntu-24.04\n复制文件到根目录\ncp -r /mnt/c/Users/dong/Desktop/at_vision_simulator-master ~/\nROS2使用小鱼自动安装\n更新系统并安装基础依赖\nsudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y sudo apt install -y build-essential pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev libxkbcommon-dev mesa-utils libgl1-mesa-glx libgl1-mesa-dri curl git cmake\n安装Rust\ncurl \u0026ndash;proto \u0026lsquo;=https\u0026rsquo; \u0026ndash;tlsv1.2 -sSf https://sh.rustup.rs | sh 选择默认安装1 安装完成后,重新加载环境变量\nsource ~/.cargo/env 验证\ncargo \u0026ndash;version\n更新系统并安装Vulkan驱动与工具\nsudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y sudo apt install -y mesa-vulkan-drivers vulkan-tools\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/rust%E4%BB%BF%E7%9C%9F%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE-wsl--ros2--rust/","summary":"\u003ch1 id=\"rust仿真环境配置-wsl--ros2--rust\"\u003erust仿真环境配置: wsl + ros2 + rust\u003c/h1\u003e\n\u003cp\u003e1.以管理员身份打开PowerShell或CMD\u003c/p\u003e\n\u003cp\u003e2.执行安装命令\u003c/p\u003e\n\u003cp\u003ewsl \u0026ndash;install -d Ubuntu-24.04\n3.设置用户名和密码\u003c/p\u003e\n\u003cp\u003e列出所有的ubantu版本\u003c/p\u003e\n\u003cp\u003ewsl -l -v\u003c/p\u003e\n\u003cp\u003e进入想要的版本\u003c/p\u003e","title":"rust仿真环境配置: wsl + ros2 + rust"},{"content":"SSH ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/ssh/","summary":"\u003ch1 id=\"ssh\"\u003eSSH\u003c/h1\u003e","title":"SSH"},{"content":"tmux简明速查手册(WSL/Linux通用) 1.安装\nsudo apt update \u0026amp;\u0026amp; sudo apt install tmux\n2.启动与退出 启动tmux\ntmux 退出当前窗格\nexit 或 Ctrl+d\n3.会话管理\n分离会话（后台运行） Ctrl+b 然后按 d 重新连接（回到上次现场） tmux attach 列出所有会话 tmux ls 连接到指定会话 tmux attach -t 会话名 新建命名会话 tmux new -s 会话名\n4.窗格管理 按键(先按Ctrl+b,再按下下一个键) 垂直分割 % 水平分割 \u0026quot; 切换窗格 方向键（↑ ↓ ← →） 调整窗格大小 Ctrl+b 按住，然后按方向键\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/tmux%E7%AE%80%E6%98%8E%E9%80%9F%E6%9F%A5%E6%89%8B%E5%86%8C-wsl-linux%E9%80%9A%E7%94%A8/","summary":"\u003ch1 id=\"tmux简明速查手册wsllinux通用\"\u003etmux简明速查手册(WSL/Linux通用)\u003c/h1\u003e\n\u003cp\u003e1.安装\u003c/p\u003e\n\u003cp\u003esudo apt update \u0026amp;\u0026amp; sudo apt install tmux\u003c/p\u003e\n\u003cp\u003e2.启动与退出\n启动tmux\u003c/p\u003e\n\u003cp\u003etmux\n退出当前窗格\u003c/p\u003e\n\u003cp\u003eexit 或 Ctrl+d\u003c/p\u003e\n\u003cp\u003e3.会话管理\u003c/p\u003e\n\u003cp\u003e分离会话（后台运行）     Ctrl+b 然后按 d\n重新连接（回到上次现场） tmux attach\n列出所有会话            tmux ls\n连接到指定会话          tmux attach -t 会话名\n新建命名会话            tmux new -s 会话名\u003c/p\u003e","title":"tmux简明速查手册(WSL/Linux通用)"},{"content":"vision-lab开发日志 开发日志: lab_huofu: 5月28日 \u0026amp; 5月29日 % 5月30日 完成霍夫圆识别水质圆圈的程序 使用pca 分析试剂的色值 并做线性回归\nlab_water: 6月7日: 拷贝程序 6月8日: 测试水质分析仪的摄像头\n6月13日: 浏览项目框架 阅读项目qt程序\n好像没什么难度\nlab_plant: 5月28日 ~ 6月6日： 购买材料:\n在自己电脑上测试代码\n6月7日： 环境部署： lubancat 烧录镜像 环境部署\n6月8日 ~ 6月16日： 准备考试\n6月17日: 继续部署环境 尝试运行代码\nlab_water项目 lab_huofu项目 lab_plant项目 环境配置: 更新系统底层软件源：\nsudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y\n将python升级到3.9及以上 安装rust编译工具\n1. 升级 pip, setuptools, wheel pip3 install \u0026ndash;upgrade pip setuptools wheel\n2. 安装 Rust 编译工具链 sudo apt update sudo apt install cargo rustc -y\n部署轻量化推理模型:\nsudo apt install libgl1-mesa-glx libglib2.0-0 -y\nnumpy\npip3 install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple\npandas\npip3 install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple\nopencv-python-headless\npip3 install opencv-python-headless -i https://pypi.tuna.tsinghua.edu.cn/simple\nultralytics\npip3 install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple 但是polars会报错: 绕过polars: 不一定使用\npip install ultralytics \u0026ndash;no-deps \u0026amp;\u0026amp; pip install numpy opencv-python torch torchvision matplotlib requests scipy pyyaml psutil ultralytics-thop\n拷贝程序 将程序从u盘拷贝到主目录\nsudo cp -r /media/usb1/lab_plant.zip /home/cat/\n解压缩\nunzip lab_plant.zip\n删除压缩包\nrm -rf lab_plant.zip\nbug: 6月17日 千万不要运行\nsudo ifconfig eth0 down\n现在只能通过网线ssh远程连接树莓派,没办法使用显示屏和串口 由于ipv6走的也是这张网卡 所以eth0关掉后ssh也会断掉\n知识点 三维点云测量 概述 安装PCL 安装Open3D 安装CloudCompare\n激光三角测距: 线激光器向物体表面投射一条激光线,当物体表面有高低起伏时,激光线会发生弯曲和位移,相机(与激光器成固定角度)捕捉到这条变形激光线的二维图像,通过三角测量法计算出每个点亮度的中心点,最终拼接成完整的三维点云\nRANSAC (Random Sample Consensus)：随机采样一致性算法 随机采样一致性算法，从包含大量离群点的数据中拟合数学模型\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/vision-lab%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97/","summary":"\u003ch1 id=\"vision-lab开发日志\"\u003evision-lab开发日志\u003c/h1\u003e\n\u003ch2 id=\"开发日志\"\u003e开发日志:\u003c/h2\u003e\n\u003ch3 id=\"lab_huofu\"\u003elab_huofu:\u003c/h3\u003e\n\u003cp\u003e5月28日 \u0026amp; 5月29日 % 5月30日\n完成霍夫圆识别水质圆圈的程序\n使用pca 分析试剂的色值\n并做线性回归\u003c/p\u003e\n\u003ch3 id=\"lab_water\"\u003elab_water:\u003c/h3\u003e\n\u003ch4 id=\"6月7日\"\u003e6月7日:\u003c/h4\u003e\n\u003ch3 id=\"拷贝程序\"\u003e拷贝程序\u003c/h3\u003e\n\u003ch4 id=\"6月8日\"\u003e6月8日:\u003c/h4\u003e\n\u003cp\u003e测试水质分析仪的摄像头\u003c/p\u003e\n\u003ch4 id=\"6月13日\"\u003e6月13日:\u003c/h4\u003e\n\u003cp\u003e浏览项目框架\n阅读项目qt程序\u003c/p\u003e","title":"vision-lab开发日志"},{"content":"吊车防碰撞开发日志 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%90%8A%E8%BD%A6%E9%98%B2%E7%A2%B0%E6%92%9E%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97/","summary":"\u003ch1 id=\"吊车防碰撞开发日志\"\u003e吊车防碰撞开发日志\u003c/h1\u003e","title":"吊车防碰撞开发日志"},{"content":"吊车物料单 激光雷达 livox - mid360 价格：4400\nhttps://e.tb.cn/h.RrpiTHWhtCLfNLI?tk=yAT9ghmzX5k\n深度相机 oak D - Lite 价格：1490\n【京东】https://3.cn/2T3-w43E 「SmartFLY【OAK中国】[OAK-D-Lite] 人工智能双目深度相机 OpenCV AI Kit FF版本【定焦款】」\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%90%8A%E8%BD%A6%E7%89%A9%E6%96%99%E5%8D%95/","summary":"\u003ch1 id=\"吊车物料单\"\u003e吊车物料单\u003c/h1\u003e\n\u003ch2 id=\"激光雷达-livox---mid360\"\u003e激光雷达 livox - mid360\u003c/h2\u003e\n\u003cp\u003e价格：4400\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://e.tb.cn/h.RrpiTHWhtCLfNLI?tk=yAT9ghmzX5k\"\u003ehttps://e.tb.cn/h.RrpiTHWhtCLfNLI?tk=yAT9ghmzX5k\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"深度相机-oak-d---lite\"\u003e深度相机 oak D - Lite\u003c/h2\u003e\n\u003cp\u003e价格：1490\u003c/p\u003e\n\u003cp\u003e【京东】https://3.cn/2T3-w43E 「SmartFLY【OAK中国】[OAK-D-Lite] 人工智能双目深度相机 OpenCV AI Kit FF版本【定焦款】」\u003c/p\u003e","title":"吊车物料单"},{"content":"滤波器 卡尔曼滤波 互补滤波 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%BB%A4%E6%B3%A2%E5%99%A8/","summary":"\u003ch1 id=\"滤波器\"\u003e滤波器\u003c/h1\u003e\n\u003ch2 id=\"卡尔曼滤波\"\u003e卡尔曼滤波\u003c/h2\u003e\n\u003ch2 id=\"互补滤波\"\u003e互补滤波\u003c/h2\u003e","title":"滤波器"},{"content":"三维重建 第一章: 摄像机几何 三维世界是怎么通过摄像机几何映射成二维的?\n针孔模型 \u0026amp; 透镜 针孔摄像机 image.png\n针孔摄像机 image.png\nimage.png\n随着光圈减小,成像效果如何变化? 越来越清晰,越来越暗 如何应对到达胶片的光线变少? 增加透镜: 透镜将多余光线聚焦到胶片上,增加了照片的亮度 image.png\n摄像机 \u0026amp; 透镜 image.png\n近轴折射模型 image.png\n透镜问题: 径向畸变 image.png\n像平面到像素平面 image.png\n像素坐标系 image.png\np到p(撇)的变换是线性的吗? 不是线性变换,z不是常数\n齐次坐标 image.png\n齐次坐标系中的投影变换 image.png\n摄像机投影矩阵 image.png\n摄像机偏斜 image.png\n摄像机几何 摄像机坐标系下的摄像机模型 image.png\nK有多少个自由度? 5个自由度\n摄像机几何 image.png\n各个符号的物理意义及其维度分别是什么? image.png\n投影矩阵有多少个自由度? 11个自由度 5个摄像机内参数+6个摄像机外参数 = 11个自由度\nP(撇)转换成欧式坐标该如何写 image.png\n规范化摄像机 image.png\n定理: image.png\n投影变换的性质: 1.点投影为点 2.线投影为线 3.近大远小 4.角度不再保持 5.平行线相交\n其他摄像机模型 弱透视投影摄像机: image.png\n各种摄像机模型的应用场合: 正交投影: 更多应用在建筑设计或者工业设计行业 弱透视投影在数学方面更简单 当物体较小且较远时准确,常用于图像识别任务 透视投影对于3D到2D映射的建模更为准确 用于运动恢复结构或SLAM\n第二章: 摄像机标定 第三章: 单视图重建\n第四章: 三维重建基础与极几何\n第五章: 双目立体视觉重建\n第六章: 多视图重建 第七章: 运动恢复结构(SFM)系统设计 第八章: 同时定位与建图(SLAM)系统设计 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E4%B8%89%E7%BB%B4%E9%87%8D%E5%BB%BA/","summary":"\u003ch1 id=\"三维重建\"\u003e三维重建\u003c/h1\u003e\n\u003ch2 id=\"第一章-摄像机几何\"\u003e第一章: 摄像机几何\u003c/h2\u003e\n\u003cp\u003e三维世界是怎么通过摄像机几何映射成二维的?\u003c/p\u003e\n\u003ch3 id=\"针孔模型--透镜\"\u003e针孔模型 \u0026amp; 透镜\u003c/h3\u003e\n\u003cp\u003e针孔摄像机\nimage.png\u003c/p\u003e\n\u003cp\u003e针孔摄像机\nimage.png\u003c/p\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003cp\u003e随着光圈减小,成像效果如何变化?\n越来越清晰,越来越暗\n如何应对到达胶片的光线变少?\n增加透镜:\n透镜将多余光线聚焦到胶片上,增加了照片的亮度\nimage.png\u003c/p\u003e","title":"三维重建"},{"content":"数据库作业 第一章 1.6 什么是数据模型？数据模型的基本要素有哪些？为什么需要数据模型？ （1）. 数据模型定义 数据模型是对现实世界数据特征的抽象，用来描述数据、组织数据和对数据进行操作，是数据库中用来对现实世界进行抽象的工具，是数据库系统的核心和基础。 （2）. 三大基本要素 数据结构：描述数据库的组成对象以及对象之间的联系，是对系统静态特性的描述。 数据操作：指对数据库中各种对象允许执行的操作集合，包含查询、增删改，是系统动态特性描述。 完整性约束条件：一组完整性规则，限定数据及数据间联系，保证数据正确、有效、相容。 （3）. 需要数据模型的原因 现实世界信息复杂，计算机无法直接识别，数据模型完成现实世界→信息世界→机器世界的逐级抽象转换； 统一规范数据的组织、存储与操作方式，方便用户、开发人员、计算机理解数据； 作为数据库设计依据，支撑数据库存储、查询、维护等全部管理功能； 屏蔽底层硬件存储细节，实现数据独立，降低数据管理复杂度。\n1.7 为什么数据模型分为概念、逻辑、物理三类？分别解释三类模型 一、划分原因 现实世界到计算机存储需要多层抽象，不同阶段面向不同使用者、解决不同问题： 概念层面向业务人员，描述业务逻辑； 逻辑层面向开发人员，描述数据库逻辑结构； 物理层面向 DBA / 底层存储，描述磁盘存储细节； 分层拆分降低建模复杂度，实现数据独立性，各司其职互不干扰。 二、三类模型解释 概念模型（信息模型） 面向现实世界、业务需求，独立于数据库与硬件，用于数据库需求分析，描述实体、属性、实体间联系，常用 E-R 图表示。不关心存储，只描述业务信息。 逻辑模型 面向数据库实现，介于信息世界与机器世界之间，包含关系、层次、网状模型。描述数据库逻辑结构（表、字段、关系、约束），是程序员设计表结构的依据，屏蔽磁盘物理细节。 物理模型 面向计算机底层硬件，描述数据在磁盘上的存储结构、存取路径、索引、分区、块大小等物理存储细节，和操作系统、存储设备强相关，供 DBA 优化性能使用。\n1.10 为什么 DBMS 要对数据抽象？分为哪几级抽象？ 抽象的原因 屏蔽底层复杂的硬件、存储、操作系统细节，让用户不用关心数据怎么存在磁盘； 分层隔离，实现逻辑独立性和物理独立性； 区分不同用户视角：普通用户只看视图，开发看全局逻辑，管理员看物理存储； 简化数据操作，上层用户只操作抽象数据，底层存储改动不影响上层程序。 三级抽象（对应三级模式） 物理级抽象（内模式）：数据物理存储结构； 逻辑级抽象（模式 / 概念模式）：全局逻辑数据结构； 视图级抽象（外模式）：局部用户视图。 1.11 解释数据库三级模式、两层映像；为什么需要三级模式 + 两层映像 一、三级模式 外模式（子模式 / 用户模式） 数据库用户能看见、使用的局部数据视图，是模式子集，面向应用程序，一个数据库可多个外模式。 模式（概念模式） 数据库全局逻辑结构，描述所有实体、属性、关系、约束，唯一，是全体数据逻辑视图。 内模式（存储模式） 数据库物理存储描述，记录存储结构、索引、磁盘块分配，唯一，面向底层存储。 二、两层映像 外模式 / 模式映像 定义外模式与模式之间对应关系，每个外模式对应一个该映像。 作用：保证逻辑独立性—— 模式全局逻辑修改，外模式无需改动，应用程序不受影响。 模式 / 内模式映像 唯一，定义全局逻辑结构与物理存储结构对应关系。 作用：保证物理独立性—— 物理存储结构调整，模式不用修改，上层应用不受影响。 三、为什么需要三级模式两层映像 核心目标是实现数据独立性： 三层模式划分多用户视角，隔离普通用户、开发、管理员，权限与视图分离，简化使用； 两层映像作为中间映射，解耦上层逻辑和底层物理存储； 物理存储优化、全局表结构调整时，不用修改上层业务程序，大幅降低维护成本； 数据安全隔离：不同用户仅能访问自身外模式，实现访问控制。\n1.12 三级模式与三层数据模型的联系与区别 一、联系 二者都是数据分层抽象思想，一一对应： 概念模型 ↔ 模式（概念模式）； 逻辑模型 ↔ 模式、外模式（全局 + 局部逻辑）； 物理模型 ↔ 内模式； 都服务数据库设计全流程：需求阶段用概念模型，设计库表用逻辑模型，存储优化用物理模型；三级模式是 DBMS 运行时的数据分层视图。 二、区别 定义角度不同 三层数据模型：数据库设计阶段的建模工具，是建模方法； 三级模式：DBMS 运行阶段的数据体系结构，是数据库系统内置的分层架构。 使用对象不同 概念模型：业务分析师、需求人员；逻辑模型：开发工程师；物理模型：DBA； 外模式：应用程序员 / 终端用户；模式：数据库设计人员；内模式：DBA。 作用不同 三层模型：用于从现实世界逐步设计出数据库结构； 三级模式：数据库运行时，提供多视图隔离，依靠两层映像保障数据独立性。 数量规则不同 三层模型是设计流程的三个阶段，一套库对应一套概念、逻辑、物理模型； 三级模式：模式、内模式各一个，外模式可以有多个。\n1.14 DBMS 的主要组成部分、主要功能 一、DBMS 主要组成部分 数据定义语言（DDL）及其编译程序：定义库、表、视图、索引； 数据操纵语言（DML）及其编译 / 解释程序：实现增删改查； 数据库运行控制管理模块：并发控制、事务管理、权限安全、完整性检查； 数据库组织、存储、管理模块：缓冲区、文件、索引管理； 数据库建立与维护程序：备份、恢复、导入导出、性能监控； 数据通信接口：支持应用程序、网络多端访问数据库。 二、DBMS 核心功能 数据定义：使用 DDL 定义数据库、表、视图、约束、索引； 数据操纵：提供 DML 实现查询、插入、更新、删除数据； 数据库运行管理（核心） 事务管理与并发控制； 数据完整性约束校验； 安全访问控制、用户权限管理； 故障恢复； 数据存储与组织管理：管理磁盘文件、内存缓冲区、索引，优化存取效率； 数据库维护：备份恢复、数据导入导出、性能分析、重组优化； 数据交互接口：对接应用程序、编程语言、网络客户端。\n第二章 2.1 (1) 域、笛卡尔积、关系、元组和属性 定义 域：一组具有相同数据类型的值的集合，是属性的取值范围。 笛卡尔积：给定若干域，将各域取值进行全部组合生成的元组集合。 关系：笛卡尔积中取有限子集，形成一张二维表。 元组：关系中的一行，对应笛卡尔积里一条取值组合。 属性：关系中的一列，每一列对应一个域，列名即为属性名。 联系与区别 联系：域是基础；域构造笛卡尔积；笛卡尔积筛选后得到关系；关系由元组（行）、属性（列）构成。 区别：域是取值范围；笛卡尔积是所有可能组合；关系是有效二维数据表；元组代表单条记录；属性代表记录的字段。 (2) 关系模式、关系数据库模式、关系数据库 定义 关系模式：对单个关系（单张表）的结构描述，格式 R(U,D,DOM,F) ，定义属性、数据类型、约束。 关系数据库模式：一个数据库内所有关系模式的集合，是整套数据表的结构定义。 关系数据库：某一时刻，关系数据库模式对应的所有关系实例（表里真实存储的数据）。 联系与区别 联系：关系模式是单表结构模板；数据库模式是全部表模板集合；关系数据库是模板对应的实际数据。 区别：模式是静态结构定义；数据库是动态数据内容；关系模式仅描述一张表，数据库模式描述整套库表。 (3) 超码、候选码、主码、外码 定义 超码：可以唯一标识关系内每一条元组的属性集合，允许包含多余属性。 候选码：最小超码，去掉集合内任意一个属性，就无法唯一标识元组。 主码：人为从候选码中选定、作为表唯一标识的候选码，一张表仅一个主码。 外码：本表的一组属性，取值匹配另一张表的主码，用于建立两张表之间关联。 联系与区别 联系：候选码一定属于超码，主码从候选码中选出；外码依靠其他表主码实现表间关联。 区别：超码可存在冗余属性；候选码无冗余；主码是选定的唯一主键；外码不唯一标识本表元组，用于表关联。 (4) 为什么需要空值 NULL 字段对应的值未知、暂时未录入； 该属性不适用于当前元组（如未选课学生无成绩）； 区分 “数值为 0 / 无” 和 “信息未知”，保证数据语义准确。\n2.3 关系模型的数据完整性约束有哪些 实体完整性：主码全部属性不能取空值，主码值唯一，保证每条记录可区分。 参照完整性：外码取值要么为空，要么等于被参照表主码已存在的值，保证表间关联合法。 用户定义完整性：用户根据业务自定义约束规则，如成绩区间 0~100、性别仅允许男 / 女。\n2.4 关系代数的主要操作有哪些 传统集合运算（两关系结构完全一致） 并运算∪、差运算−、交运算∩、笛卡尔积× 专门关系运算（面向二维表） 选择σ（筛选行）、投影Π （筛选列）、连接⋈、除÷ 拓展连接运算 等值连接、自然连接、左外连接、右外连接、全外连接 2.6 等值连接与自然连接的区别与联系 联系 二者都属于内连接，通过属性相等条件匹配两张表元组； 运算底层都是先做笛卡尔积，再筛选满足相等条件的元组。 区别 匹配条件：等值连接可任意两属性相等，属性名可不同；自然连接仅匹配同名、同类型属性。 重复列处理：等值连接保留两张表全部字段，匹配属性重复出现；自然连接自动删除重复同名字段，仅保留一列。 书写形式：等值连接必须显式书写相等判断条件；自然连接无需写条件，自动匹配同名列。\n2.8 基础数据表结构 Student(studentNo,studentName,birthday,nation,className,gender) Score(studentNo,courseNo,score) Course(courseNo,courseName,teacherNo,year,term,credit,priorCourse) （1）查找籍贯为 “上海” 的全体学生 σ(birthday=\u0026rsquo; 上海 \u0026lsquo;)(Student) （2）查找 2005 年元旦以后出生的全体男同学 σ(birthday\u0026gt;\u0026lsquo;2005-01-01\u0026rsquo; ∧ gender=\u0026rsquo; 男 \u0026lsquo;)(Student) （3）查找信息学院非汉族同学的学号、姓名、性别及民族 Π(studentNo,studentName,gender,nation)(σ(nation≠\u0026rsquo; 汉 \u0026rsquo; ∧ className like \u0026rsquo; 信息学院 %\u0026rsquo;)(Student)) （4）查找 2022—2023 学年第二学期 (22232) 开设课程的课程号、课程名和学分 Π(courseNo,courseName,credit)(σ(year=\u0026lsquo;2022-2023\u0026rsquo; ∧ term=\u0026lsquo;22232\u0026rsquo;)(Course)) （5）查找选修了 “操作系统” 的学生学号、成绩及姓名 Π(studentNo,studentName,score)(Student ⋈ Score ⋈ σ(courseName=\u0026rsquo; 操作系统 \u0026lsquo;)(Course)) （6）查找班级名称为 “会计学 21 (3) 班” 的学生在 2021—2022 学年第一学期 (21221) 选课情况，显示学生姓名、课程号、课程名和成绩 Π(studentName,Course.courseNo,courseName,score)(σ(className=\u0026rsquo; 会计学 21 (3) 班 \u0026lsquo;)(Student) ⋈ Score ⋈ σ(year=\u0026lsquo;2021-2022\u0026rsquo; ∧ term=\u0026lsquo;21221\u0026rsquo;)(Course)) （7）查找至少选修了一门其直接先修课号为 CS012 的课程的学生学号和姓名 Π(studentNo,studentName)(Student ⋈ Score ⋈ Π(courseNo)(σ(priorCourse=\u0026lsquo;CS012\u0026rsquo;)(Course))) （8）查找选修了 2022—2023 学年第一学期 (22231) 开设的全部课程的学生学号和姓名 Π(studentNo,studentName)( (Π(studentNo,courseNo)(Score) ÷ Π(courseNo)(σ(year=\u0026lsquo;2022-2023\u0026rsquo; ∧ term=\u0026lsquo;22231\u0026rsquo;)(Course))) ⋈ Student ) （9）查找至少选修了学号为 2103010 的学生所选课程的学生学号和姓名 Π(studentNo,studentName)( (Π(studentNo,courseNo)(Score) ÷ Π(courseNo)(σ(studentNo=\u0026lsquo;2103010\u0026rsquo;)(Score))) ⋈ Student )\n2.9 新增数据表 Teacher (teacherNo,teacherName) 补充 Course 字段：classNo,time,location （1）查找 2022 级蒙古族学生信息：学号、姓名、性别、班级 Π(studentNo,studentName,gender,className)(σ(nation=\u0026rsquo; 蒙古族 \u0026rsquo; ∧ className like \u0026lsquo;2022 级 %\u0026rsquo;)(Student)) （2）查找 “C 语言程序设计” 课程的教学班号、上课时间、上课地点 Π(classNo,time,location)(σ(courseName=\u0026lsquo;C 语言程序设计 \u0026lsquo;)(Course)) （3）查找以 “计算机概论” 为先修课的课程号、课程名、课程学分 Π(courseNo,courseName,credit)(σ(priorCourse=\u0026rsquo; 计算机概论 \u0026lsquo;)(Course)) （4）查找李勇老师 2022—2023 学年第二学期 (22232) 开设课程号、课程名、学分 Π(courseNo,courseName,credit)(σ(teacherName=\u0026rsquo; 李勇 \u0026rsquo; ∧ year=\u0026lsquo;2022-2023\u0026rsquo; ∧ term=\u0026lsquo;22232\u0026rsquo;)(Teacher ⋈ Course)) （5）查找信息学院学生选课情况：学生姓名、课程号、课程名、教学班号、成绩、任课教师姓名 Π(studentName,Course.courseNo,courseName,classNo,score,teacherName)(σ(className like \u0026rsquo; 信息学院 %\u0026rsquo;)(Student) ⋈ Score ⋈ Course ⋈ Teacher)\n第三章 3.1 查询 1991 年出生的读者的姓名、工作单位和身份证号 SELECT readerName, workUnit, identitycard FROM Reader WHERE YEAR(identitycard) = 1991;\n3.2 查询图书名称中含有 “数据库” 的图书的详细信息 SELECT * FROM Book WHERE bookName LIKE \u0026lsquo;%数据库%\u0026rsquo;;\n3.3 查询 2019—2020 年入库的图书编号、出版时间、入库时间和图书名称，并按入库时间的降序排列输出 SELECT bookNo, publishingDate, shopDate, bookName FROM Book WHERE YEAR(shopDate) BETWEEN 2019 AND 2020 ORDER BY shopDate DESC;\n3.4 查询读者喻自强借阅的图书编号、图书名称、借阅日期和归还日期 SELECT b.bookNo, b.bookName, br.borrowDate, br.returnDate FROM Reader r JOIN Borrow br ON r.readerNo = br.readerNo JOIN Book b ON br.bookNo = b.bookNo WHERE r.readerName = \u0026lsquo;喻自强\u0026rsquo;;\n3.5 查询借阅了清华大学出版社出版的图书的读者编号、姓名、图书名称、借阅日期和归还日期 SELECT r.readerNo, r.readerName, b.bookName, br.borrowDate, br.returnDate FROM Reader r JOIN Borrow br ON r.readerNo = br.readerNo JOIN Book b ON br.bookNo = b.bookNo JOIN Publisher p ON b.publisherNo = p.publisherNo WHERE p.publisherName = \u0026lsquo;清华大学出版社\u0026rsquo;;\n第四章 4.1 术语简要解释 实体：现实世界中客观存在、可以相互区分的事物，例如职工、商品、客户。 实体集：同一类型实体的集合，全体职工构成职工实体集。 属性：实体具有的特征，职工的职工号、姓名都是属性。 域：属性的取值范围，如性别域只能取 “男、女”。 联系：实体之间的关联关系，如客户购买商品。 联系集：同类联系的全部集合，所有 “客户 - 购买 - 商品” 构成购买联系集。 多联系：三元及以上实体间的联系，供应商、商品、仓库三者的供应入库属于多联系。 角色：同一实体在联系中承担的不同身份，如职工既是员工又是部门管理者，管理者是角色。 映射基数：联系两端实体的数量对应关系，分为一对一 (1:1)、一对多 (1:N)、多对多 (M:N)。 超码：能唯一标识实体集中每个实体的属性集，可包含多余属性。 候选码：最小超码，去掉任意一个属性就无法唯一标识实体。 主码：人为选定的一个候选码，作为实体唯一标识。 多值联系：同一对实体之间存在多条联系记录，同一个商品多次存入同一仓库。 多值联系集：由全部多值联系构成的集合。 依赖约束：弱实体必须依赖强实体才能存在，弱实体主码包含所依赖强实体主码。 参与约束：分为全部参与、部分参与；全部参与表示实体集中每个实体都必须参与该联系，部分参与表示可有可无。 弱实体集：自身无独立候选码，必须依赖另一个强实体集才能唯一标识的实体，如贷款明细。 类层次：存在继承关系的实体集层次，如人员分为职工、客户，职工又分为医生、销售员。 聚合：将一个联系整体当作一个实体，再和其他实体建立新联系。\n4.4 销售公司数据库设计 (1) 实体集及属性 ① 职工（职工实体集，强实体） 属性：职工号（主码）、姓名、性别、电话、住址 ② 供货商（供货商实体集，强实体） 属性：制造商编号（主码）、制造商名称、联系电话、通信地址 ③ 商品（商品实体集，强实体） 属性：商品编号（主码）、商品名称、型号、计量单位、进货单价、库存数量、销售单价 ④ 客户（客户实体集，强实体） 属性：客户编号（主码）、客户名称、联系电话、通信地址 ⑤ 供应（M:N 联系转弱实体：供货记录） 属性：制造商编号、商品编号、供货单价；联合主码 (制造商编号，商品编号) ⑥ 购买（M:N 联系转弱实体：订单） 属性：客户编号、商品编号、购买数量、成交单价；联合主码 (客户编号，商品编号) (2) E-R 模型说明（映射基数） 供货商 — 供应 — 商品 映射基数：供货商 (1,N)，商品 (M,1)，多对多 M:N；联系属性：供货单价。 业务：一个供货商供应多种商品，一种商品可由多个供货商供货。 客户 — 购买 — 商品 映射基数：客户 (1,N)，商品 (M,1)，多对多 M:N；联系属性：购买数量、成交单价。 业务：一个客户购买多种商品，一种商品销售给多个客户。 实体：职工（仅基础信息，不参与核心购销联系）。 (3) 转换为关系模式（标注主码 PK、外码 FK） 职工 (职工号，姓名，性别，电话，住址) PK：职工号 供货商 (制造商编号，制造商名称，联系电话，通信地址) PK：制造商编号 商品 (商品编号，商品名称，型号，计量单位，进货单价，库存数量，销售单价) PK：商品编号 供货记录 (制造商编号，商品编号，供货单价) PK：(制造商编号，商品编号) FK：制造商编号 → 供货商 (制造商编号) FK：商品编号 → 商品 (商品编号) 订单 (客户编号，商品编号，购买数量，成交单价) PK：(客户编号，商品编号) FK：客户编号 → 客户 (客户编号) FK：商品编号 → 商品 (商品编号) 客户 (客户编号，客户名称，联系电话，通信地址) PK：客户编号\n4.8 医院门诊开方、缴费开票数据库设计 (1) 实体集及属性 ① 职工（医生 / 开票人员，强实体） 属性：职工号 (PK)、姓名、性别、电话、住址 ② 药品（强实体） 属性：药品号 (PK)、药品名、计量单位、进货单价、库存数量、销售单价 ③ 病人（强实体） 属性：病人号 (PK)、姓名、性别、出生日期、联系电话 ④ 处方（强实体，医生开具） 属性：处方号 (PK)、开具日期、处方金额、职工号 (开具医生 FK)、病人号 (FK) ⑤ 处方明细（弱实体，依赖处方） 属性：处方号 (FK)、药品号 (FK)、药品数量、单价、单项金额、每日用药次数 PK：(处方号，药品号) ⑥ 发票（缴费票据，强实体） 属性：发票号 (PK)、开票日期、业务摘要、发票金额、职工号 (开票职工 FK) ⑦ 缴费明细（弱实体，关联处方与发票） 属性：发票号 (FK)、处方号 (FK)、本次缴费金额 PK：(发票号，处方号) (2) 局部 E-R 模型 映射基数与联系 职工 — 开具 — 处方 一对多 (1:N)：一名医生可开多张处方，一张处方仅由一名医生开具；全部参与约束。 病人 — 持有 — 处方 一对多 (1:N)：一个病人有多张处方，一张处方仅属于一位病人；全部参与。 处方 — 包含 — 药品（处方明细） 多对多 M:N，转化弱实体处方明细；联系属性：数量、单价、金额、用药次数。 职工 — 开具 — 发票 一对多 (1:N)：一名开票职工开多张发票，一张发票仅一个开票人。 处方 — 缴费生成 — 发票（缴费明细） 多对多 M:N，转化弱实体缴费明细；业务：一张处方可分多次缴费（多张发票），一张发票可包含多个处方缴费记录。 (3) 转换关系模式（PK 主码，FK 外码） 职工 (职工号，姓名，性别，电话，住址) PK：职工号 药品 (药品号，药品名，计量单位，进货单价，库存数量，销售单价) PK：药品号 病人 (病人号，姓名，性别，出生日期，联系电话) PK：病人号 处方 (处方号，开具日期，处方金额，职工号，病人号) PK：处方号 FK：职工号 → 职工 (职工号) FK：病人号 → 病人 (病人号) 处方明细 (处方号，药品号，药品数量，单价，单项金额，每日用药次数) PK：(处方号，药品号) FK：处方号 → 处方 (处方号) FK：药品号 → 药品 (药品号) 发票 (发票号，开票日期，业务摘要，发票金额，职工号) PK：发票号 FK：职工号 → 职工 (职工号) 缴费明细 (发票号，处方号，本次缴费金额) PK：(发票号，处方号) FK：发票号 → 发票 (发票号) FK：处方号 → 处方 (处方号)\n第五章 5.1 数据冗余引发的问题 + 异常实例 数据冗余的危害 同一数据在多条元组中重复存储，浪费存储空间，还会引发三类操作异常。 三类异常实例（以学生选课表：S (学号，姓名，系名，课程号，成绩) 为例） 插入异常 新增一个还未选课的新生，因无课程号，主码 (学号，课程号) 为空，无法插入该学生基础信息。 删除异常 某系最后一名学生退学，删除该生选课记录时，连带把该系的系名信息全部删除，丢失系数据。 更新异常 某学生转系，若该生选了 3 门课，需要修改 3 条记录的系名字段；若漏改某一条，会出现同一学生对应两个系名的数据不一致。 5.2 术语完整解释 函数依赖 设R(U)为属性集U上的关系模式，X,Y⊆U。若对于R中任意两条元组，X上属性值相等则Y上属性值必然相等，称X→Y，即X函数决定Y。 平凡 / 非平凡函数依赖 平凡：X→Y且Y⊆X（如AB→A），必然成立，无业务意义； 非平凡：X→Y且Y⊈X（如学号→姓名），具备实际业务语义。 完全函数依赖、部分函数依赖 设X→Y，X′是X的任意真子集： 完全：任意X′都不满足X′→Y，必须X全部属性才能决定Y； 部分：存在某一个X′满足X′→Y，仅X部分属性即可决定Y。 传递函数依赖 满足X→Y、Y↛X、Y→Z，则X传递决定Z，记传递。 函数依赖集闭包F+ 由函数依赖集F，通过自反、增广、传递三条公理推导出的全部函数依赖的集合。 属性集闭包XF+ 给定属性集X，由F能推导出的所有属性构成的集合。 无损连接分解 将R分解为多个子模式，对R任意实例，分解后子模式自然连接可以还原出原关系，不会产生多余虚假元组。 保持依赖分解 分解后所有子模式上的函数依赖投影合并，等价于原F，所有约束不会丢失。 1NF 关系中每个属性的值都是不可再分的原子值，不允许嵌套表、多值单元格。 2NF 满足 1NF，消除所有非主属性对候选码的部分函数依赖。 3NF 满足 2NF，消除所有非主属性对候选码的传递函数依赖；即不存在X→Y,Y→Z，Z是非主属性。 BCNF（巴斯 - 科德范式） 满足 3NF，消除主属性对候选码的部分 / 传递依赖；任意非平凡函数依赖X→Y，X一定是超码。\n5.3 术语解释：无关属性、左无关、右无关、正则覆盖 无关属性 在函数依赖X→Y中，X或Y内可删除、删除后不改变F+的属性。 左无关属性 X→Y中，属性A∈X，满足(X−{A})F+​⊇Y，去掉A后依赖依然成立，A是左无关属性。 右无关属性 X→Y中，属性A∈Y，满足XF+​⊇(Y−{A})，去掉A后依赖依然成立，A是右无关属性。 正则覆盖Fc 满足两个条件的最简依赖集： ① 任意依赖无左、右无关属性； ② 不存在两条完全相同的函数依赖。\n5.4 图 5-19 关系实例，求所有非平凡最简函数依赖 实例元组： 表格\nA\nB\nC\nD\nE\n1\n2\n3\n4\n5\n1\n4\n3\n4\n5\n1\n2\n4\n4\n1\n2\n4\n5\n5\n2 最简非平凡函数依赖： A→D：A 相同则 D 一定相同 AC→B：A、C 共同确定 B AC→E：A、C 共同确定 E D无其他决定，B、C、E 无单独决定关系\n5.7 R(A,B,C,D,E),F={A→BC, CD→E, B→D, E→A} (1) 求A+、B+ A+： A→BC → 得 B、C；B→D → 得 D；CD→E → 得 E A+={A,B,C,D,E} B+： B→D；无其他推导，B+={B,D} (2) 求全部候选码 逐个计算属性闭包： A+=ABCDE → A 是候选码 E+：E→A→BC→D → E+=ABCDE → E 是候选码 CD+：CD→E→A→BC → CD+=ABCDE → CD 是候选码 候选码：、、\n5.8 证明分解R1(ABC),R2(ADE)是无损连接分解 无损连接判定定理： 分解R1(U1),R2(U2)无损的充要条件：U1∩U2→U1​−U2 或 U1∩U2→U2​−U1 交集：U1∩U2={A} U2​−U1={D,E} 由F：A+=ABCDE，A→DE，满足A→U2​−U1 因此该分解为无损连接分解。\n5.10 R(A,B,C,D)，三组依赖分别求解 (1) F1={C→D, C→A, B→C} ① 候选码：B B→C→D,A，B+=ABCD ② 范式判断： B→C，、，存在非主属性传递依赖，最高2NF，不满足 3NF ③ BCNF 分解： R1(B,C), R2(C,A), R3(C,D) (2) F2={ABC→D, D→A} ① 候选码：ABC ABC+=ABCD；无其他属性闭包覆盖全集 ② 范式判断： 存在主属性 A 依赖非超码 D，仅1NF ③ BCNF 分解： R1(D,A), R2(B,C,D) (3) F3={A→B, BC→D} ① 候选码：AC AC→B→D，AC+=ABCD ② 范式判断： A→B，B 是主属性，但无部分 / 传递非主属性依赖，满足3NF；不满足 BCNF（A→B左部 A 不是超码） ③ BCNF 分解： R1(A,B), R2(A,C,D)\n5.11 R(A,B,C,D,E,G)，两组依赖求解 (1) F1={A→BDE, B→AE, AC→G, BC→AD} ① 候选码：AC AC→G；A→BDE，AC+=ABCDEG ② 3NF 判断： 所有依赖左部均包含候选码 / 候选码子集，无传递、部分非主属性依赖，满足 3NF，无需分解。 (2) F2={A→CDG, G→A, AE→C, EG→BD} ① 候选码：、 AE+：AE→C，A→CDG，G→A，EG→BD → 全集 EG+：EG→BD，G→A→CD → 全集 ② 3NF 判断：无传递 / 部分非主属性依赖，满足 3NF，无需分解。\n第七章 7.2 BookDB 图书管理数据库 SQL (1) 将 “经济类” 图书的单价提高 10% UPDATE Book b JOIN BookClass c ON b.classNo = c.classNo SET b.price = b.price * 1.10 WHERE c.className = \u0026lsquo;经济类\u0026rsquo;; (2) 将入库数量最多的图书单价下调 5% UPDATE Book SET price = price * 0.95 WHERE shopNum = (SELECT MAX(shopNum) FROM Book); (3) 删除读者 “张小娟” 的借书记录 DELETE br FROM Borrow br JOIN Reader r ON br.readerNo = r.readerNo WHERE r.readerName = \u0026lsquo;张小娟\u0026rsquo;; (4) 创建视图 v_reader_60：在借图书总价 60 元以上读者（未归还 = returnDate IS NULL） CREATE VIEW v_reader_60 AS SELECT r.readerNo, r.readerName, SUM(b.price) AS totalPrice FROM Reader r JOIN Borrow br ON r.readerNo = br.readerNo JOIN Book b ON br.bookNo = b.bookNo WHERE br.returnDate IS NULL GROUP BY r.readerNo, r.readerName HAVING SUM(b.price) \u0026gt; 60; (5) 创建视图 v_reader_25_35：年龄 25~35 岁读者借书信息 sql CREATE VIEW v_reader_25_35 AS SELECT r.readerNo, r.readerName, TIMESTAMPDIFF(YEAR,STR_TO_DATE(SUBSTRING(r.identitycard,7,8),\u0026rsquo;%Y%m%d\u0026rsquo;),CURDATE()) AS age, r.workUnit, b.bookName, br.borrowDate FROM Reader r JOIN Borrow br ON r.readerNo = br.readerNo JOIN Book b ON br.bookNo = b.bookNo WHERE TIMESTAMPDIFF(YEAR,STR_TO_DATE(SUBSTRING(r.identitycard,7,8),\u0026rsquo;%Y%m%d\u0026rsquo;),CURDATE()) BETWEEN 25 AND 35; (6) 创建视图 v_tsinghua_computer：清华出版社 2019-2020 计算机类图书 sql CREATE VIEW v_tsinghua_computer AS SELECT b.* FROM Book b JOIN Publisher p ON b.publisherNo = p.publisherNo JOIN BookClass c ON b.classNo = c.classNo WHERE p.publisherName = \u0026lsquo;清华大学出版社\u0026rsquo; AND c.className = \u0026lsquo;计算机类\u0026rsquo; AND YEAR(b.publishingDate) BETWEEN 2019 AND 2020; (8) 在 Reader 表按 workUnit 建立索引 readerUnitIdx CREATE INDEX readerUnitIdx ON Reader(workUnit);\n7.3 ScoreDB 学生成绩数据库存储过程 (1) 输入课程号，返回选课人数、平均分 DELIMITER // CREATE PROCEDURE proc_course_stat(IN in_courseNo CHAR(10), OUT out_count INT, OUT out_avg DECIMAL(5,2)) BEGIN SELECT COUNT(DISTINCT studentNo), AVG(score) INTO out_count, out_avg FROM Score WHERE courseNo = in_courseNo; END // DELIMITER ; \u0026ndash; 调用示例 CALL proc_course_stat(\u0026lsquo;C001\u0026rsquo;, @cnt, @avg); SELECT @cnt AS 选课人数, @avg AS 平均分; (2) 删除重复选课，仅保留每学生每门课最高分记录 思路：分组取最高分，删除分数低于最高分的重复行 DELIMITER // CREATE PROCEDURE proc_del_dup_score() BEGIN DELETE s1 FROM Score s1 JOIN Score s2 WHERE s1.studentNo = s2.studentNo AND s1.courseNo = s2.courseNo AND s1.score \u0026lt; s2.score; END // DELIMITER ; \u0026ndash; 调用 CALL proc_del_dup_score(); (3) 无聚合函数，统计各学院选课人数、平均分，按学院升序输出 思路：使用变量累加计数、总分，替代 COUNT/AVG DELIMITER // CREATE PROCEDURE proc_class_stat_no_agg() BEGIN DECLARE cur_class VARCHAR(50); DECLARE cur_stu CHAR(10); DECLARE cur_sc DECIMAL(5,2); DECLARE done INT DEFAULT 0; DECLARE class_cur CURSOR FOR SELECT DISTINCT className FROM Student ORDER BY className; DECLARE stu_cur CURSOR FOR SELECT s.studentNo, sc.score FROM Student s JOIN Score sc ON s.studentNo=sc.studentNo WHERE s.className = cur_class; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;\nOPEN class_cur; class_loop: LOOP FETCH class_cur INTO cur_class; IF done=1 THEN LEAVE class_loop; END IF; SET @stu_cnt = 0; SET @sum_score = 0; SET done = 0; OPEN stu_cur; stu_loop: LOOP FETCH stu_cur INTO cur_stu, cur_sc; IF done=1 THEN LEAVE stu_loop; END IF; SET @stu_cnt = @stu_cnt + 1; SET @sum_score = @sum_score + cur_sc; END LOOP stu_loop; CLOSE stu_cur; SELECT cur_class AS 学院名称, @stu_cnt AS 选课人数, ROUND(@sum_score/@stu_cnt,2) AS 平均分; END LOOP class_loop; CLOSE class_cur; END // DELIMITER ; \u0026ndash; 调用 CALL proc_class_stat_no_agg(); (4) 无聚合函数，按格式逐门课程输出学生明细、选课人数、平均分 DELIMITER // CREATE PROCEDURE proc_course_print_no_agg() BEGIN DECLARE cur_cno CHAR(10); DECLARE cur_cname VARCHAR(40); DECLARE cur_stu CHAR(10); DECLARE cur_sname VARCHAR(20); DECLARE cur_sc DECIMAL(5,2); DECLARE done INT DEFAULT 0; DECLARE course_cur CURSOR FOR SELECT DISTINCT c.courseNo, c.courseName FROM Course c JOIN Score sc ON c.courseNo=sc.courseNo; DECLARE stu_cur CURSOR FOR SELECT s.studentNo, s.studentName, sc.score FROM Student s JOIN Score sc ON s.studentNo=sc.studentNo WHERE sc.courseNo = cur_cno; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1;\nOPEN course_cur; course_loop: LOOP FETCH course_cur INTO cur_cno, cur_cname; IF done=1 THEN LEAVE course_loop; END IF; -- 打印课程标题 SELECT CONCAT('课程名 ', cur_cname) AS output; SELECT '学号 姓名 成绩' AS output; SET @stu_cnt = 0; SET @sum_sc = 0; SET done = 0; OPEN stu_cur; stu_loop: LOOP FETCH stu_cur INTO cur_stu, cur_sname, cur_sc; IF done=1 THEN LEAVE stu_loop; END IF; SET @stu_cnt = @stu_cnt + 1; SET @sum_sc = @sum_sc + cur_sc; SELECT CONCAT(cur_stu, ' ', cur_sname, ' ', cur_sc) AS output; END LOOP stu_loop; CLOSE stu_cur; -- 统计汇总 SELECT CONCAT('选课人数：', @stu_cnt) AS output; SELECT CONCAT('平均分：', ROUND(@sum_sc/@stu_cnt,2)) AS output; SELECT '----------------------------------------' AS output; END LOOP course_loop; CLOSE course_cur; END // DELIMITER ; \u0026ndash; 调用 CALL proc_course_print_no_agg();\n第八章 8.10 顺序索引、散列索引、主索引、辅助索引、稠密索引、稀疏索引定义 顺序索引：基于有序排序的搜索码建立的索引，记录按搜索码有序存储，支持范围查询。 散列索引：利用散列函数将搜索码映射到存储桶，直接定位存储位置，适合等值查询，不擅长范围查询。 主索引：建立在有序主码上的索引，索引项与数据块一一对应，数据文件按搜索码物理有序。 辅助索引（二级索引）：建立在非排序字段上的索引，搜索码无序，每条索引项指向对应记录。 稠密索引：数据文件中每一条记录都对应一条索引项，每条记录都有索引条目。 稀疏索引：仅为每个数据块建立一条索引项，一个块只存一条索引，索引数量更少。 稠密索引与稀疏索引区别 索引条目数量：稠密索引每条记录一条索引；稀疏索引每个数据块一条索引。 查找效率：稠密索引可直接定位记录，查找更快；稀疏索引找到块后需在块内顺序查找。 存储开销：稠密索引占用存储空间更大；稀疏索引空间开销小。 更新代价：稠密索引插入 / 删除需维护大量索引项；稀疏索引修改索引操作更少。 8.11 为什么需要多级索引？多级索引结构 多级索引产生原因 当数据量巨大时，单层稀疏索引自身文件也会超出内存容量，无法一次性载入内存检索；将索引本身再分块、建立上层索引，形成多层结构，每次仅加载少量索引块到内存，减少磁盘 I/O，提升查询速度。 多级索引整体结构 底层（叶级索引）：一级稀疏索引，对应原始数据块，索引项为「搜索码值 + 数据块地址」； 上层（非叶级）：多层稀疏索引，每一层为下一层索引块建立索引； 顶层（根节点）：最高一级索引，仅有一个索引块，检索入口。 检索时从根逐层向下，直到叶级索引，再访问数据块。 8.12 什么是 B⁺树索引？B⁺树索引优缺点 B⁺树索引定义 B⁺树是一种平衡多路搜索树，作为数据库主流多级顺序索引；所有记录数据指针仅存于叶节点，非叶节点只存索引键值与子块指针，天然实现多级稀疏索引，数据有序，支持等值、范围查询。 2. 优点 整棵树严格平衡，任意查询磁盘 I/O 次数稳定，性能波动小； 叶节点通过双向链表串联，高效支持区间、排序、全表扫描； 插入、删除仅局部调整节点分裂 / 合并，平衡维护代价低； 支持等值、范围、排序、模糊前缀多种查询。 3. 缺点 树节点存在空闲空间，有一定存储冗余； 等值单点查询性能弱于散列索引，散列仅一次 I/O，B⁺树需遍历树高多层节点； 频繁随机更新会触发节点分裂，带来少量磁盘写开销。\n8.13 B⁺树根、非叶、叶节点结构相同，区别是什么？ 结构共性 三类节点存储结构格式一致：存储多个（键值，指针）二元组，节点有最大键值容量上限。 核心区别 存储指针类型不同 非叶节点（含根节点）：指针全部指向子索引块，无原始数据记录指针；仅用于索引跳转。 叶节点：指针分为两类：①下一个叶节点的链表指针；②指向磁盘原始数据记录 / 数据块的指针。 键值作用不同 非叶节点键值：仅作为分界值，划分左右子树检索区间，不对应真实记录； 叶节点键值：完整包含所有搜索码，每条键值对应真实数据记录。 链表连接 只有叶节点带有双向链表指针，实现有序遍历；根、非叶节点无链表。 数据存储位置 全部真实记录地址仅保存在叶节点；根与非叶节点只存导航索引，不存储记录指针。 8.14 如何利用 B⁺树索引查找？B⁺树文件组织与 B⁺树索引区别 一、B⁺树索引查找步骤 等值查找 ① 从根节点进入，用待查键与节点内分界键对比，选择匹配区间的子节点指针； ② 逐层向下遍历非叶节点，直到抵达叶节点； ③ 在叶节点内顺序查找目标键，通过记录指针读取磁盘数据。 范围查找（区间查询） ① 先按等值查找找到区间下界对应的叶节点； ② 利用叶节点双向链表，向后遍历所有满足区间条件的叶节点，读取全部符合条件记录。 二、B⁺树文件组织 和 B⁺树索引区别 定义层级不同 B⁺树索引：仅索引结构，独立于数据文件，只存储键与指针，不存储原始数据；数据文件可堆文件、顺序文件。 B⁺树文件组织：完整存储方案，数据记录全部存放于 B⁺树叶节点中，索引与数据合为一体，整个文件就是一棵 B⁺树。 数据存放位置 B⁺树索引：原始数据独立存储在数据文件，叶节点仅存记录指针； B⁺树文件组织：叶节点直接保存完整数据记录，无外部数据文件。 适用场景 B⁺树索引：作为辅助索引附加在已有数据文件上； B⁺树文件组织：作为表的主存储结构，整张表以 B⁺树形式落地磁盘。 IO 流程差异 B⁺树索引：检索完叶节点后，还需要额外一次 IO 读取外部数据块； B⁺树文件组织：找到叶节点即读取完整数据，无需额外访问数据文件。\n第九章 9.1 一、数据库安全性概念 数据库安全性是指保护数据库，防止未经授权的用户非法访问、修改、删除或破坏数据库中数据，避免数据泄露、篡改、丢失，保障数据合法、可控访问的技术与管理机制。 核心目标：区分合法 / 非法用户，控制用户可执行的操作，保障数据不被恶意窃取、破坏。 二、数据库安全保护措施及实现方式\n用户标识与鉴别（身份认证） 作用：验证访问者身份，确认是否为合法用户，是第一道安全屏障。 实现：账号 + 密码、生物识别、动态验证码、数字证书等；登录时校验身份，不通过则拒绝连接数据库。 存取控制（自主 / 强制存取控制） 1）自主存取控制 DAC（主流，SQL 标准 GRANT/REVOKE） 作用：DBA 为用户分配权限，用户可自主将自身权限转授他人；区分用户能访问哪些表、执行增删改查 / 建表等操作。 实现：通过GRANT授予权限、REVOKE回收权限，建立用户 - 权限映射表。 2）强制存取控制 MAC（高安全场景：军工、政务） 作用：给数据、用户划分安全密级（绝密 / 机密 / 公开），仅用户密级≥数据密级才能访问。 实现：系统自动对比密级，不受用户自主授权干扰。 视图机制 作用：为不同用户创建定制视图，隐藏底层敏感字段、无关数据，用户仅能访问视图可见内容。 实现：CREATE VIEW筛选数据，仅授予用户视图访问权限，不开放基表权限。 审计机制 作用：记录所有数据库访问、修改操作日志，事后追溯非法操作，追责溯源。 实现：开启审计功能，自动保存登录、增删改、权限变更记录，存入审计日志表。 数据加密 作用：防止数据脱库、磁盘被盗后明文泄露。 实现： 存储加密：磁盘文件整体加密； 传输加密：客户端与数据库通信 SSL/TLS 加密； 字段加密：身份证、手机号等敏感字段单独加密存储。 触发器安全审计 作用：对关键表的增删改操作实时拦截、记录，限制非法修改。 实现：编写触发器，操作前校验操作者身份、操作范围，违规则回滚并写入审计记录。 操作系统与网络安全辅助 作用：从底层阻断非法连接，防护 Web、服务器漏洞。 实现：防火墙限制数据库端口访问、操作系统账户权限隔离、Web 应用防 SQL 注入。 9.5 数据库完整性概念 + DBMS 实现完整性约束的机制 一、数据库完整性概念 数据库完整性是指数据库中数据的正确性、有效性、相容性： 正确性：数据符合业务逻辑，如年龄不能为负数； 有效性：数据在规定取值范围内，如性别只能男 / 女； 相容性：表之间关联数据一致，外码必须引用已存在的主码。 完整性防止输入不合规、错误、矛盾的数据，区分于安全性（安全防非法访问，完整防错误数据）。 二、DBMS 实现数据完整性约束的手段\n四类静态完整性约束（定义表时声明，DBMS 自动校验） 实体完整性（主键约束 PRIMARY KEY） 每张表主码非空、唯一；插入 / 更新时 DBMS 自动校验主码重复、空值，违规拒绝操作。 参照完整性（外键约束 FOREIGN KEY） 外码要么为空，要么引用另一张表已存在的主码；删除 / 更新主码时可配置级联更新、级联删除、置空、拒绝操作，DBMS 自动校验关联一致性。 域完整性（列级约束） 包括数据类型、长度、非空 NOT NULL、唯一 UNIQUE、默认值 DEFAULT、检查 CHECK 约束；限定单个字段取值规则，字段写入时即时校验。 用户自定义完整性（元组 / 表级 CHECK 约束） 多字段联合约束，例如 “上海户籍学生年龄≥17”；整条记录插入 / 修改完成后，DBMS 校验多字段逻辑关系。 动态完整性约束：触发器 TRIGGER 静态约束仅校验写入瞬间字段规则；触发器实现动态业务完整性： 在增 / 删 / 改操作前 / 后触发自定义逻辑； 可实现跨表联动（修改学生学号同步更新成绩表学号）、业务限额（借书数量不能超上限）、复杂多表校验； 违规时回滚事务，阻止非法数据写入。 DBMS 完整性约束统一执行流程 执行 INSERT/UPDATE/DELETE 操作； 先执行列级域约束（字段类型、非空、CHECK）； 再执行元组级表约束（多字段联合 CHECK）； 校验实体完整性（主键唯一、非空）； 校验参照完整性（外码关联合法性）； 触发对应触发器，执行复杂业务校验 / 联动更新； 任意一层校验失败，回滚整条事务，数据不写入数据库。 第十章 10.1 事务 ACID 特性及 DBMS 保障机制 一、事务四大 ACID 特性 原子性（Atomicity） 事务是不可分割的最小执行单元，事务内所有操作要么全部成功提交，要么全部回滚撤销，不存在部分执行完成的状态。 一致性（Consistency） 事务执行前后，数据库始终保持业务完整性约束的一致状态；事务执行过程可临时不一致，但提交 / 回滚后必须恢复合法一致。 隔离性（Isolation） 多个并发事务之间互相隔离，一个事务看不到其他事务未提交的中间数据，各事务感知不到彼此的并行执行。 持久性（Durability） 事务一旦成功提交，它对数据库的修改永久生效，后续系统崩溃、断电等故障不会丢失已提交的数据变更。 二、DBMS 分别如何保证四大特性 原子性：日志 + 回滚机制（Undo 日志） 事务执行前记录撤销日志，若事务中途失败，DBMS 读取 Undo 日志反向执行所有操作，撤销已完成的修改；正常提交则清空对应撤销日志，实现全部回滚或全部生效。 一致性：完整性约束 + 原子性 + 隔离性共同支撑 静态约束：主键、外键、CHECK、唯一约束自动校验； 动态约束：触发器校验业务规则； 依托原子性避免半完成事务破坏数据，依托隔离性避免并发脏数据导致不一致。 隔离性：并发控制（锁机制 / MVCC 多版本并发控制） 封锁方案：共享锁、排他锁、意向锁，读写互斥、写写互斥，限制事务访问未提交数据； MVCC：为数据生成多版本快照，读事务读取历史快照，不阻塞写事务，实现不同隔离级别（读未提交、读已提交、可重复读、串行化）。 持久性：重做日志（Redo 日志）+ 数据备份 事务提交时先把变更写入 Redo 日志再刷新磁盘数据；系统崩溃后，DBMS 扫描 Redo 日志重做已提交事务的修改；配合数据库备份、检查点机制，保障提交数据永久保存。\n10.2 数据库为什么需要并发控制 并发访问的业务需求 数据库是多用户共享系统，大量客户端同时读写数据库（如电商下单、学生同时查成绩、图书馆多人借书）。若强制串行排队执行，系统吞吐量极低、响应缓慢，无法支撑多用户同时访问，必须允许多事务并发执行提升性能。 无并发控制会产生三类数据不一致问题 丢失更新 两个事务同时读取同一数据，各自基于初始值修改，后提交的事务覆盖先提交事务的修改，造成更新丢失。 读脏数据 事务 T1 修改数据未提交，事务 T2 读取了 T1 未提交的中间值；若 T1 回滚，T2 读取的数据就是无效脏数据。 不可重复读 同一事务 T1 内两次读取同一数据，中间 T2 修改并提交该数据，导致 T1 两次查询结果不一致。 并发控制的核心作用 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BD%9C%E4%B8%9A/","summary":"\u003ch1 id=\"数据库作业\"\u003e数据库作业\u003c/h1\u003e\n\u003ch1 id=\"第一章\"\u003e第一章\u003c/h1\u003e\n\u003ch2 id=\"16-什么是数据模型数据模型的基本要素有哪些为什么需要数据模型\"\u003e1.6 什么是数据模型？数据模型的基本要素有哪些？为什么需要数据模型？\u003c/h2\u003e\n\u003cp\u003e（1）. 数据模型定义\n数据模型是对现实世界数据特征的抽象，用来描述数据、组织数据和对数据进行操作，是数据库中用来对现实世界进行抽象的工具，是数据库系统的核心和基础。\n（2）. 三大基本要素\n数据结构：描述数据库的组成对象以及对象之间的联系，是对系统静态特性的描述。\n数据操作：指对数据库中各种对象允许执行的操作集合，包含查询、增删改，是系统动态特性描述。\n完整性约束条件：一组完整性规则，限定数据及数据间联系，保证数据正确、有效、相容。\n（3）. 需要数据模型的原因\n现实世界信息复杂，计算机无法直接识别，数据模型完成现实世界→信息世界→机器世界的逐级抽象转换；\n统一规范数据的组织、存储与操作方式，方便用户、开发人员、计算机理解数据；\n作为数据库设计依据，支撑数据库存储、查询、维护等全部管理功能；\n屏蔽底层硬件存储细节，实现数据独立，降低数据管理复杂度。\u003c/p\u003e","title":"数据库作业"},{"content":"自瞄赛季总结 给老师的汇报 自瞄技术历经人员流失与两年断续迭代，今年虽终得稳定落地，但7v7实战效果未达预期。当前流水火控仅适配爆发模式与高弹频哨兵，面对前哨站及冷却模式步兵命中效果大幅下滑，底层PID控制滞后严重，视觉组已开发的MPC先进火控受限于目前pid控制算法的落后难以落地，亟需为前哨站设计专用火控逻辑。 硬件层面，pitch轴6020电机小角度控制超调，轴承磨损与电机老化引入回差；微机平台老化严重，内存接口脱落导致帧率骤降，串口与相机掉线重连长达4和14秒，关键对局直接断供，013相机清晰度与主流016系列存在代差。传统底盘地形适应性差，复杂地形下车身剧烈晃动，自瞄难以持续锁定，算法能力无从发挥。 必须同步推进轮腿机器人研发以提升瞄准平台稳定性，并将p轴更换为4310电机、更新微机与相机，为MPC火控及专用击打逻辑扫清部署障碍，方能将已有识别跟踪能力转化为赛场实打实的命中效果。\n英雄: 最终实际投入人力(人天): 900(3人*300天)\n项目推进及赛场暴露的能力缺口: 跨组交流与系统理解薄弱：视觉与机械、电控组之间缺乏常态化技术对接，对机械结构（如云台刚度、电机选型回差）、电控硬件（实时性瓶颈、通信延时来源）的系统性知识培训与了解不足，导致算法设计脱离硬件实际，先进控制方案难以落地。 代码开发非系统化、非协作化：缺乏统一的代码规范、版本管理流程与模块化设计，基本是各写各的，无人负责整体架构与接口定义；缺少代码评审、单元测试、持续集成，调试时互相依赖“口头传导”。 技术栈陈旧，缺乏系统传承：仍使用古老的ROS1框架，未迁移至ROS2，图像传输延迟大、实时性差；过往两年的算法成果（如卡尔曼滤波、弹道解算、降自由度优化）未形成文档、设计图或可复用的库，新人接手几乎推倒重来，导致迭代效率极低，同一问题反复踩坑。\n下赛季人才培养/招新重点建议: 招新要求： 熟悉ROS2、现代C++/Python工程化实践（CMake、Git工作流、单元测试）；具备跨学科思维，愿意主动学习机械/电控基础知识（如电机特性、通信总线、时延来源）。\n培训与机制改革： 建立技术文档与代码管理规范：每个算法模块需附带可复现的实验记录和接口说明，统一用代码仓库管理，每周提交简短进度报告，确保经验可传承。 定期组织视觉、电控、机械三组联合分析会，针对云台响应慢、通信延时等系统瓶颈一起排查原因，制定各方协同的优化方案，避免各自为战。 推行协作开发流程：强制使用分支开发、合并请求和代码审查，至少一人审核后方可合并，逐步改变各写各代码的习惯。\n本机器人软件核心得失 本赛季自瞄系统最核心的突破是降自由度Yaw角优化。其核心思想是：roll和pitch通过标定预先确定，将这两个自由度固定后，仅对yaw角进行独立建模与优化。我们深入对比了上交三分法与同济暴力搜索方案，最终选定上交方案进行工程化落地。配合fitLine最小二乘法对灯条角点进行亚像素级优化，成功将装甲板在0°正对附近的Yaw角跳变从原先的30°~60°压制到5°以内，从根本上解决了角度跳变导致的0度观测抖振问题，为卡尔曼滤波提供了稳定、连续的观测输入。 另一核心亮点是系统时延链路优化。我们基于Eigen手写了脱离ROS的坐标变换器，避免TF消息传递带来的时间错位；实现了帧率控制器与插值补偿机制，将图像采集到处理的时间差严格对齐到50帧系统节拍。这套体系使核心跟踪节点跑通100Hz以上处理能力，并在保证时间戳一致性的前提下将有效输出稳定在50Hz，在实时性与稳定性间取得最优平衡。 今年在卡尔曼滤波方面针对步兵、前哨两种车型分别设计了独立的观测器，各自匹配不同的状态转移矩阵和过程噪声参数。观测噪声中距离项乘以与yaw角度相关的增益，目标侧对时测距噪声自动放大，减少了距离跳动对状态估计的影响。调参上配合协方差迹监控来指导Q/R调整，使卡尔曼调参从经验试错转为可追踪、可复现的过程。\n步兵: 最终实际投入人力(人天): 900(3人*300天)\n项目推进及赛场暴露的能力缺口 项目推进中，跨组交流不够，视觉对机械和电控的硬件了解不足，导致算法设计脱离实际，好的控制方案很难落地。代码开发没有统一规范，没有版本管理和模块化设计，大家各写各的，缺乏整体架构和代码审查，调试全靠口头沟通。技术方面还在用老旧的ROS1，没升级到ROS2，图像传输延时大；前两年的算法成果没整理成文档或可复用库，新人接手又要从头开始，效率很低，重复踩坑。\n下赛季人才培养/招新重点建议 招新时要找熟悉ROS2、现代C++/Python开发工具（如CMake、Git、单元测试）的人，还要愿意主动学习机械电控基础知识。培训上要建立文档和代码管理规则，每个算法模块都需附带可复现的实验记录和接口说明，用代码仓库统一管理，每周提交进度报告，方便传承。定期组织视觉、电控、机械三组联合分析会，一起排查云台响应慢、通信延时等问题，制定协同方案。强制推行分支开发、合并请求和代码审查，至少一人审核才能合并，逐步改掉各写各的习惯。\n本机器人软件核心得失 亮点: 本赛季自瞄系统最大突破是Yaw角降自由度优化——固定roll和pitch，只对yaw建模，比较了上交和同济的方案后选上交，配合fitLine优化角点，使0°附近Yaw跳变从30~60度压到5度以内，解决了抖动，为卡尔曼滤波提供了稳定输入。另一个亮点是系统延时优化，手写坐标变换避免TF时间误差，实现帧率控制和插值补偿，使核心跟踪节点处理速度达100Hz以上，稳定输出50Hz，兼顾实时性和稳定性。卡尔曼滤波针对步兵和前哨分别设计观测器，观测噪声中距离项根据yaw角度自动放大，减少距离跳动影响；调参时用协方差迹监控指导Q/R调整，让调参从凭经验变成可追踪可复现的过程。 踩坑点 MPC轨迹规划器最终未能落地。电控发弹延时接近200ms，远超同济开源方案的20ms水平，MPC预测20个时间片后的位置做提前规划，其价值大打折扣。根本原因在于视觉单方面无法解决系统延时瓶颈，需要电控同步实现力控算法并压缩发弹延时，跨组协作不足导致方案悬空。四阶龙格库塔方案因装甲板测距不准同样受限，测距误差在远距离被空气阻力项指数放大，导致旋转目标命中率极低。测距精度是整个弹道解算的前置依赖，单目视觉的物理限制在当前硬件条件下没有好的解法。前哨战采用流水火控的方案由于惯量大跟随差,命中率较低\n战术定位: 项目标题: 自瞄\n最终确定的规划: 识别实现降自由度优化Yaw角，手写坐标变换降低系统延时， 设计多运动模型卡尔曼滤波，实现高精度的稳定跟踪。\n赛场实际达成情况: 自瞄系统在赛场上整体稳定运行，哨兵搭载的自瞄具备稳定推掉前哨1000血的能力，对步兵形成较强威慑力。自瞄DPS高，与人对拼时能更快击杀对手；识别鲁棒性好，无需频繁调曝光；系统稳定性有保障，即便自瞄掉线也能通过重启功能快速恢复。\n优劣势与战术得失: 优势: 自瞄DPS高是赛场上的核心杀伤力来源，比赛中与对手正面交火时往往能更快击杀，形成局部人数优势。识别算法鲁棒性好，不同场地光照条件下无需频繁调曝光，减少了调试负担和赛前准备时间。系统稳定性方面，自瞄具备自动重启功能，即便出现异常也能快速恢复，比赛中掉自瞄后能重新找回目标，不至于全场离线。\n劣势: 云台惯量大导致响应慢，电控调参未适配到位，出现超调问题，尤其在应对高速旋转目标时跟随滞后明显。前哨战三层装甲板击打命中率低，现有步兵方案直接复用效果不佳，前哨战需要独立设计击打逻辑。远距离（5m外）命中率偏低，PnP测距误差随距离增大，落点系统性偏低且随距离变化不稳定，调试依赖现场反复试射，效率低。\n下赛季优化建议: 下赛季优先解决系统延时瓶颈：升级ROS2或共享内存传输降低图像延迟；落地MPC需电控同步实现力控算法并压缩发弹延迟至50ms内，配合历史查询或提前开火补偿；前哨战单独设计火控逻辑,提高前哨击打命中率.\n哨兵: 最终实际投入人力(人天): 900(3人*300天)\n项目推进及赛场暴露的能力缺口: 跨组交流与系统理解薄弱：视觉与机械、电控组之间缺乏常态化技术对接，对机械结构（如云台刚度、电机选型回差）、电控硬件（实时性瓶颈、通信延时来源）的系统性知识培训与了解不足，导致算法设计脱离硬件实际，先进控制方案难以落地。 代码开发非系统化、非协作化：缺乏统一的代码规范、版本管理流程与模块化设计，基本是各写各的，无人负责整体架构与接口定义；缺少代码评审、单元测试、持续集成，调试时互相依赖“口头传导”。 技术栈陈旧，缺乏系统传承：仍使用古老的ROS1框架，未迁移至ROS2，图像传输延迟大、实时性差；过往两年的算法成果（如卡尔曼滤波、弹道解算、降自由度优化）未形成文档、设计图或可复用的库，新人接手几乎推倒重来，导致迭代效率极低，同一问题反复踩坑。\n下赛季人才培养/招新重点建议: 招新要求： 熟悉ROS2、现代C++/Python工程化实践（CMake、Git工作流、单元测试）；具备跨学科思维，愿意主动学习机械/电控基础知识（如电机特性、通信总线、时延来源）。\n培训与机制改革： 建立技术文档与代码管理规范：每个算法模块需附带可复现的实验记录和接口说明，统一用代码仓库管理，每周提交简短进度报告，确保经验可传承。 定期组织视觉、电控、机械三组联合分析会，针对云台响应慢、通信延时等系统瓶颈一起排查原因，制定各方协同的优化方案，避免各自为战。 推行协作开发流程：强制使用分支开发、合并请求和代码审查，至少一人审核后方可合并，逐步改变各写各代码的习惯。\n本机器人软件核心得失 本赛季自瞄系统最核心的突破是降自由度Yaw角优化。其核心思想是：roll和pitch通过标定预先确定，将这两个自由度固定后，仅对yaw角进行独立建模与优化。我们深入对比了上交三分法与同济暴力搜索方案，最终选定上交方案进行工程化落地。配合fitLine最小二乘法对灯条角点进行亚像素级优化，成功将装甲板在0°正对附近的Yaw角跳变从原先的30°~60°压制到5°以内，从根本上解决了角度跳变导致的0度观测抖振问题，为卡尔曼滤波提供了稳定、连续的观测输入。 另一核心亮点是系统时延链路优化。我们基于Eigen手写了脱离ROS的坐标变换器，避免TF消息传递带来的时间错位；实现了帧率控制器与插值补偿机制，将图像采集到处理的时间差严格对齐到50帧系统节拍。这套体系使核心跟踪节点跑通100Hz以上处理能力，并在保证时间戳一致性的前提下将有效输出稳定在50Hz，在实时性与稳定性间取得最优平衡。 今年在卡尔曼滤波方面针对步兵、前哨两种车型分别设计了独立的观测器，各自匹配不同的状态转移矩阵和过程噪声参数。观测噪声中距离项乘以与yaw角度相关的增益，目标侧对时测距噪声自动放大，减少了距离跳动对状态估计的影响。调参上配合协方差迹监控来指导Q/R调整，使卡尔曼调参从经验试错转为可追踪、可复现的过程。\n引用开源 类型: 开源资料 项目名称: WMJAimer (西北工业大学 WMJ) 链接: https://github.com/SnocrashWang/WMJAimer 借鉴内容: EKF + 熵权法多运动模型匹配的整车建模、MPC 轨迹规划器\n项目名称: rm.cv.fans (上海交通大学) 链接:https://github.com/julyfun/rm.cv.fans 借鉴内容: 三分法降自由度 Yaw 角优化、弹道可视化调参工具、坐标变换器设计\n项目名称: sp_vision_25 (同济大学 SuperPower) 链接:https://github.com/TongjiSuperPower/sp_vision_25 借鉴内容: 暴力搜索法降自由度 Yaw 角优化、MPC 轨迹规划器\n项目名称:rm_auto_aim (华南师范大学) 链接:https://github.com/FaterYU/rm_auto_aim 借鉴内容: fitLine 灯条角点优化\n项目名称:FYT2024_vision (中南大学 FYT) 链接:https://github.com/CSU-FYT-Vision/FYT2024_vision 借鉴内容: PCA 灯条角点优化\n技术骨干人才情况 姓名\n所属组别\n核心技能/方向\n典型案例/贡献描述\n实际研发技术点与目标差异原因分析 自瞄系统的实际研发技术点与目标差异分析原因 自瞄系统的实际研发技术点与目标差异原因分析： 由于视觉组对电控底层逻辑（双环PID跟随特性、稳态误差、电控时延等）理解不足，MPC轨迹规划器所需的力控算法配合和电控前馈未能打通，导致MPC以半成品收尾未能落地。当前火控设计主要针对地面移动目标，未针对前哨站固定中心旋转+三装甲板120°分布的特殊运动模式做适配，且自制前哨站与官方前哨站在装甲板安装高度、半径及装配精度上存在差异，导致前哨站击打命中率低，专门的前哨站火控仍处于计划阶段。ROS图像传输延迟大且嵌入式计算资源有限，全向感知（多相机采集调度）仅完成初步部署，未能实现多相机联合调度。\n学术创新 添加桂工自瞄文档\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%87%AA%E7%9E%84%E8%B5%9B%E5%AD%A3%E6%80%BB%E7%BB%93/","summary":"\u003ch1 id=\"自瞄赛季总结\"\u003e自瞄赛季总结\u003c/h1\u003e\n\u003ch2 id=\"给老师的汇报\"\u003e给老师的汇报\u003c/h2\u003e\n\u003cp\u003e自瞄技术历经人员流失与两年断续迭代，今年虽终得稳定落地，但7v7实战效果未达预期。当前流水火控仅适配爆发模式与高弹频哨兵，面对前哨站及冷却模式步兵命中效果大幅下滑，底层PID控制滞后严重，视觉组已开发的MPC先进火控受限于目前pid控制算法的落后难以落地，亟需为前哨站设计专用火控逻辑。\n硬件层面，pitch轴6020电机小角度控制超调，轴承磨损与电机老化引入回差；微机平台老化严重，内存接口脱落导致帧率骤降，串口与相机掉线重连长达4和14秒，关键对局直接断供，013相机清晰度与主流016系列存在代差。传统底盘地形适应性差，复杂地形下车身剧烈晃动，自瞄难以持续锁定，算法能力无从发挥。\n必须同步推进轮腿机器人研发以提升瞄准平台稳定性，并将p轴更换为4310电机、更新微机与相机，为MPC火控及专用击打逻辑扫清部署障碍，方能将已有识别跟踪能力转化为赛场实打实的命中效果。\u003c/p\u003e","title":"自瞄赛季总结"},{"content":"自瞄网页调试器开发日志 开发日志 6月3日 Init commit 自瞄网页调试器demo版本 网页初步跑通 但是还是不能显示摄像头数据\n6月4日 修复图像无法显示的问题\n原因 bug fix: rosbridge 对 uint8[] 类型字段会自动 Base64 编码 数据路径: Python list(jpg_bytes) → rosbridge Base64 编码 → 浏览器收到 Base64 字符串 如果用 new Uint8Array(string) 直接处理 Base64 字符串会得到无效数据 正确做法: atob() 解码 → 逐字符 charCodeAt() → Uint8Array\n6月5日 \u0026amp; 6月6日 \u0026amp; 6月7日 \u0026amp;6月8日 鸽 忙于看导航开源 lab的项目\n6月9日 Claude code使用vue3重构自瞄网页 发布自瞄网页调试器第二版\n问题: 缺少rqt打印和图像录制功能 界面丑陋 视频图像居中丑陋\n6月10日 界面优化 添加rqt打印曲线 添加图像录制功能 补充后续的升级方案\n补充之前相应bug的注释\n6月11日 补充图像无法传输的bug注释\n6月12日 升级了AimScope网页调试器: 重构ROS1/ROS2连接与rosbridge适配,新增Windows摄像头实时发布 Topic状态监控,图像FPS/延迟显示,事件日志报警,录制回放逐帧复盘, 问题标记,片段导出和测试报告导出; 清理无关TinyWebserver示例页面与调试资源 并补充环境配置,最简启动和项目结构文档\n6月13日 添加readme.md的文档\n6月14日 \u0026amp; 6月15日 考试 鸽\n知识点: linux高性能服务器 vue3 canvas\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%87%AA%E7%9E%84%E7%BD%91%E9%A1%B5%E8%B0%83%E8%AF%95%E5%99%A8%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97/","summary":"\u003ch1 id=\"自瞄网页调试器开发日志\"\u003e自瞄网页调试器开发日志\u003c/h1\u003e\n\u003ch2 id=\"开发日志\"\u003e开发日志\u003c/h2\u003e\n\u003ch3 id=\"6月3日\"\u003e6月3日\u003c/h3\u003e\n\u003cp\u003eInit commit\n自瞄网页调试器demo版本\n网页初步跑通 但是还是不能显示摄像头数据\u003c/p\u003e\n\u003ch3 id=\"6月4日\"\u003e6月4日\u003c/h3\u003e\n\u003cp\u003e修复图像无法显示的问题\u003c/p\u003e\n\u003cp\u003e原因\nbug fix: rosbridge 对 uint8[] 类型字段会自动 Base64 编码\n数据路径: Python list(jpg_bytes) → rosbridge Base64 编码 → 浏览器收到 Base64 字符串\n如果用 new Uint8Array(string) 直接处理 Base64 字符串会得到无效数据\n正确做法: atob() 解码 → 逐字符 charCodeAt() → Uint8Array\u003c/p\u003e","title":"自瞄网页调试器开发日志"},{"content":"3v3复盘文档 1.哨兵记得装单发限位 -\u0026gt; 第一发弹丸的发弹延时比较大,低头的时候会漏弹 2.自瞄普遍很近处看不到,即使是6mm相机装甲板在近处已经很大了,pitch轴的俯角不够 -\u0026gt; 目前没有比较好的解决方案 3.全向的弹频等不如麦轮 -\u0026gt; 后续跟进 4.怀疑英雄炸超电后微机出现欠压,可能出现降压没有供电 -\u0026gt; 后续询问硬件组,考虑使用坏掉的超电测试 5.英雄发弹延时严重不稳定 -\u0026gt; 小陀螺转得很慢都打不中 6.所有车的自瞄的发弹延时普遍比较大将近200ms，严重不合理,严查电控拨弹盘代码 -\u0026gt; 尤其是英雄,导致英雄慢速小陀螺的火控都打不中,步兵泼水勉强打得中 7.视觉为什么电控延时普遍给了将近200ms,不合理,严查电控代码,哪里给了延时,跟随稳态误差有点不太对 电控时延给0后,火控期望线和当前云台角差距过大很不合理,严查电控代码 8.重点研究电控云台pid前馈技术,两点原因: (1) 小陀螺高速旋转后,由于中心不在云台yaw轴上,小陀螺后,出现离心力矩,且随着速度的增加,力矩越来越大 偏差方向和云台旋转方向相同 (2)mpc需要视觉计算的速度和加速度作为前馈 -\u0026gt; 计算力矩pid控制算法 9.考虑和贵州师范的人交流自瞄(重点交流英雄自瞄火控和mpc技术) 10.曝光调参困难,且严苛,(适应性训练调不出来可能就祭了i),考虑使用pid算法控制曝光 -\u0026gt; 明年新技术 11.考虑视觉仿真验证模拟器 12.摩擦轮可能存在稳定性问题,高强度比赛后,全向(疑似)和哨兵皆出现问题 -\u0026gt; 影响视觉落点和自瞄散布 13.全向自瞄比赛开始后给微机断电 -\u0026gt; 已解决电控降压接到射击模块 14.全向比赛过程中偶尔出现掉自瞄 -\u0026gt; 更换最新串口线 -\u0026gt; 目前效果还行,后续测试 15.记得备份比赛过程中的代码 16.导航时如果甩头甩得过分可能会出现走偏的问题 17.全向哨兵自瞄存在优先级的问题,经常是先看到谁就瞄谁 ,尤其是全向自瞄经常出现瞄准的不是操作手想要的目标 18.哨兵存在被别人偷屁股的情况 -\u0026gt; 跟进全向感知技术 19.3v3比赛后将重心放在推前哨战的技术上\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/3v3%E5%A4%8D%E7%9B%98%E6%96%87%E6%A1%A3/","summary":"\u003ch1 id=\"3v3复盘文档\"\u003e3v3复盘文档\u003c/h1\u003e\n\u003cp\u003e1.哨兵记得装单发限位 -\u0026gt; 第一发弹丸的发弹延时比较大,低头的时候会漏弹\n2.自瞄普遍很近处看不到,即使是6mm相机装甲板在近处已经很大了,pitch轴的俯角不够 -\u0026gt; 目前没有比较好的解决方案\n3.全向的弹频等不如麦轮 -\u0026gt; 后续跟进\n4.怀疑英雄炸超电后微机出现欠压,可能出现降压没有供电 -\u0026gt; 后续询问硬件组,考虑使用坏掉的超电测试\n5.英雄发弹延时严重不稳定 -\u0026gt; 小陀螺转得很慢都打不中\n6.所有车的自瞄的发弹延时普遍比较大将近200ms，严重不合理,严查电控拨弹盘代码 -\u0026gt; 尤其是英雄,导致英雄慢速小陀螺的火控都打不中,步兵泼水勉强打得中\n7.视觉为什么电控延时普遍给了将近200ms,不合理,严查电控代码,哪里给了延时,跟随稳态误差有点不太对\n电控时延给0后,火控期望线和当前云台角差距过大很不合理,严查电控代码\n8.重点研究电控云台pid前馈技术,两点原因:\n(1) 小陀螺高速旋转后,由于中心不在云台yaw轴上,小陀螺后,出现离心力矩,且随着速度的增加,力矩越来越大\n偏差方向和云台旋转方向相同\n(2)mpc需要视觉计算的速度和加速度作为前馈 -\u0026gt; 计算力矩pid控制算法\n9.考虑和贵州师范的人交流自瞄(重点交流英雄自瞄火控和mpc技术)\n10.曝光调参困难,且严苛,(适应性训练调不出来可能就祭了i),考虑使用pid算法控制曝光 -\u0026gt; 明年新技术\n11.考虑视觉仿真验证模拟器\n12.摩擦轮可能存在稳定性问题,高强度比赛后,全向(疑似)和哨兵皆出现问题 -\u0026gt; 影响视觉落点和自瞄散布\n13.全向自瞄比赛开始后给微机断电 -\u0026gt; 已解决电控降压接到射击模块\n14.全向比赛过程中偶尔出现掉自瞄 -\u0026gt; 更换最新串口线 -\u0026gt; 目前效果还行,后续测试\n15.记得备份比赛过程中的代码\n16.导航时如果甩头甩得过分可能会出现走偏的问题\n17.全向哨兵自瞄存在优先级的问题,经常是先看到谁就瞄谁 ,尤其是全向自瞄经常出现瞄准的不是操作手想要的目标\n18.哨兵存在被别人偷屁股的情况 -\u0026gt; 跟进全向感知技术\n19.3v3比赛后将重心放在推前哨战的技术上\u003c/p\u003e","title":"3v3复盘文档"},{"content":"3v3交流赛复盘文档 红色为优先级高的问题\n1.全向相机焦距比赛后发现已经模糊,怀疑比赛过程中焦距发生了变化 -\u0026gt; 安装时不要动到相机的那两颗螺丝,安装后需要检查 ✔（已督促） 2.经实测发现比赛场地二值化阈值给160 曝光给2300 识别较为合适 （还未有严谨的论证） 3.哨兵使用的是12mm的镜头视野太窄,且进入导航后pitch轴晃动幅度太小,导致近距离都很难锁到敌人 -\u0026gt; 更换8mm镜头,提高pitch轴晃动幅度,yaw轴的转动幅度可以更改回去 ✔ 4.比赛时备场区没法调弹道的硬补偿,只能比赛场地外打弹调好,需要经常调试,每隔几小时变化一次,备场区只能使用激光笔调试, 调试前记得关闭重力补偿,调试完后记得打开重力补偿 ✔(确实需要调落点,但是还未查明原因) 5.微机散热存在问题,微机过热存在cpu降频问题,导致自瞄帧率严重下降 -\u0026gt; 已证明微机温度会显著影响自瞄帧率 √ 6.全向后面的C口和USB口坏了,哨兵的C口坏了,哨兵的usb口接触不良,为什么哨兵的C口还没有修好上学期就在催了,usb口接触不良,导致哨兵频繁出现掉自瞄掉导航的情况 还未解决 7.串口线和C口过于脆弱,经常出现掉串口的情况(包括视觉收不到电控数据,电控收不到视觉数据,usb没有反应) ✔（硬件加焊串口线） 8.目前实测打表硬补偿效果还好 调参不是很好调需要花较多时间,但是效果较好 ✔ 9.C口不够赛场上很难调试 -\u0026gt; 购买了HDMI便携屏(催促硬件修好c口和usb口) ✔ （全向后面的usb口修不好） 10.存在自瞄瞄歪的情况 -\u0026gt; 怀疑pid和坐标系的问题(过颠簸后测试自瞄如若出现该现象，打开rviz查看坐标系,如果坐标系错误,电控查找bug) -\u0026gt; 已找到是掉串口的问题 ✔ 偏移的大致距离: Image_856571903378208.jpg\n11.为什么直到打训练赛的时候才告诉我说没有降压,很早之前就在催了 麦轮的降压还不知道什么情况 12.微机装上后全向和英雄都没有调试pid -\u0026gt; 时间过于仓促,没有时间调试 英雄pid已调 13.训练赛时存在光晕问题(正式比赛时光照充足不会存在),Lp.min_fill_ratio参数 0.8 -\u0026gt; 0.5(仅训练赛使用) ✔ 14.写着平步的微机风扇貌似是坏的 -\u0026gt; 风扇是好的只是风扇的线没有插 ✔ 15.自瞄的近距离识别貌似存在问题,可能和长焦镜头有显著关系 ✔ 是长焦的问题,视场角太小了近距离看不到 16.前哨战卡尔曼未调试收敛慢 -\u0026gt; 3v3结束后再调试 17.调试使用的东西过于多,经常出现找不到的情况,导致手忙脚乱 -\u0026gt; 准备一个收纳箱 ✔ 准备了一个纸箱子 购买了一个塑料收纳箱 18.比赛时英雄微机的保护壳安装位置不够好,导致屁股后面的C口和usb口需要拆下来保护壳后才能插上 -\u0026gt; 给保护壳里面垫东西 ✔ 19.操作手不会使用自瞄,需要教学操作手使用自瞄 -\u0026gt; 出自瞄使用说明书,训练赛的时候教学 ✔(已出自瞄说明书,正在教学中)\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/3v3%E4%BA%A4%E6%B5%81%E8%B5%9B%E5%A4%8D%E7%9B%98%E6%96%87%E6%A1%A3/","summary":"\u003ch1 id=\"3v3交流赛复盘文档\"\u003e3v3交流赛复盘文档\u003c/h1\u003e\n\u003cp\u003e红色为优先级高的问题\u003c/p\u003e\n\u003cp\u003e1.全向相机焦距比赛后发现已经模糊,怀疑比赛过程中焦距发生了变化 -\u0026gt; 安装时不要动到相机的那两颗螺丝,安装后需要检查 ✔（已督促）\n2.经实测发现比赛场地二值化阈值给160 曝光给2300 识别较为合适 （还未有严谨的论证）\n3.哨兵使用的是12mm的镜头视野太窄,且进入导航后pitch轴晃动幅度太小,导致近距离都很难锁到敌人 -\u0026gt;\n更换8mm镜头,提高pitch轴晃动幅度,yaw轴的转动幅度可以更改回去 ✔\n4.比赛时备场区没法调弹道的硬补偿,只能比赛场地外打弹调好,需要经常调试,每隔几小时变化一次,备场区只能使用激光笔调试,\n调试前记得关闭重力补偿,调试完后记得打开重力补偿 ✔(确实需要调落点,但是还未查明原因)\n5.微机散热存在问题,微机过热存在cpu降频问题,导致自瞄帧率严重下降 -\u0026gt; 已证明微机温度会显著影响自瞄帧率\n√\n6.全向后面的C口和USB口坏了,哨兵的C口坏了,哨兵的usb口接触不良,为什么哨兵的C口还没有修好上学期就在催了,usb口接触不良,导致哨兵频繁出现掉自瞄掉导航的情况 还未解决\n7.串口线和C口过于脆弱,经常出现掉串口的情况(包括视觉收不到电控数据,电控收不到视觉数据,usb没有反应) ✔（硬件加焊串口线）\n8.目前实测打表硬补偿效果还好 调参不是很好调需要花较多时间,但是效果较好 ✔\n9.C口不够赛场上很难调试 -\u0026gt; 购买了HDMI便携屏(催促硬件修好c口和usb口) ✔ （全向后面的usb口修不好）\n10.存在自瞄瞄歪的情况 -\u0026gt; 怀疑pid和坐标系的问题(过颠簸后测试自瞄如若出现该现象，打开rviz查看坐标系,如果坐标系错误,电控查找bug) -\u0026gt; 已找到是掉串口的问题 ✔\n偏移的大致距离:\nImage_856571903378208.jpg\u003c/p\u003e","title":"3v3交流赛复盘文档"},{"content":"7v7备赛计划 调前哨战的卡尔曼滤波器 降低发弹延时 , 稳定发弹延时 -\u0026gt; 调英雄自瞄 微机检修 -\u0026gt; 坏掉的口该修就修 靶车\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/7v7%E5%A4%87%E8%B5%9B%E8%AE%A1%E5%88%92/","summary":"\u003ch1 id=\"7v7备赛计划\"\u003e7v7备赛计划\u003c/h1\u003e\n\u003cp\u003e调前哨战的卡尔曼滤波器\n降低发弹延时 , 稳定发弹延时 -\u0026gt; 调英雄自瞄\n微机检修 -\u0026gt; 坏掉的口该修就修\n靶车\u003c/p\u003e","title":"7v7备赛计划"},{"content":"AI辅助解决弹道复现BUG #pragma once #include // ROS相关头文件 #include \u0026ldquo;ros/ros.h\u0026rdquo; #include \u0026ldquo;rm_msgs/Armor.h\u0026rdquo; #include \u0026ldquo;rm_msgs/ArmorArray.h\u0026rdquo; #include \u0026ldquo;rm_msgs/RmSerial.h\u0026rdquo; #include \u0026lt;std_msgs/Float64.h\u0026gt; #include \u0026lt;angles/angles.h\u0026gt; #include \u0026lt;tf2/LinearMath/Transform.h\u0026gt; #include \u0026lt;tf2/LinearMath/Vector3.h\u0026gt; #include \u0026lt;tf2/LinearMath/Quaternion.h\u0026gt; // OpenCV相关头文件 #include \u0026lt;opencv2/opencv.hpp\u0026gt; #include \u0026lt;cv_bridge/cv_bridge.h\u0026gt; // 标准库头文件 #include #include // 时间相关头文件 #include #include #include #include \u0026lt;yaml-cpp/yaml.h\u0026gt; // 自定义头文件 #include \u0026ldquo;Calculater.hpp\u0026rdquo; #include \u0026ldquo;GimbalPos.hpp\u0026rdquo; #include \u0026ldquo;TargetModel.hpp\u0026rdquo; #include \u0026ldquo;visual.hpp\u0026rdquo; #include \u0026ldquo;CoorConverter.hpp\u0026rdquo; #include \u0026ldquo;math.hpp\u0026rdquo; #include \u0026ldquo;MPC.hpp\u0026rdquo; #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; using namespace std; using namespace cv; /* 自瞄文档 标准模型状态向量 X(0) -\u0026gt; 机器人中心的x坐标 X(1) -\u0026gt; 机器人中心x方向速度 X(2) -\u0026gt; 机器人中心的y坐标 X(3) -\u0026gt; 机器人中心y方向速度 X(4) -\u0026gt; 左侧装甲板的固定高度 X(5) -\u0026gt; 右侧装甲板的固定高度 X(6) -\u0026gt; 小装甲板旋转半径（左侧） X(7) -\u0026gt; 大装甲板旋转半径（右侧） X(8) -\u0026gt; 机器人整体偏航角yaw X(9) -\u0026gt; 偏航角速度palstance 前哨站状态向量 X(0): 机器人中心的x坐标 X(1): 机器人中心的y坐标 X(2): 第一块装甲板高度h1 X(3): 第二块装甲板高度h2 X(4): 第三块装甲板高度h3 X(5): 偏航角yaw X(6): 偏航角速度palstance / /* @brief 将弧度约束在[-pi, pi]范围内，大于n，减去2n；小于-n，加上2n @param angle 角度 / #ifndef _std_radian #define std_radian(angle) ((angle) + round((0 - (angle)) / (2 * PI)) * (2 * PI)) #endif //追踪类 template class Tracker{ private: double COMMAND_TIMESPAN; //电控延迟 double local_gravity; //重力加速度 double eTime; //曝光时间 / ======================== 系统参数 ======================== / //ROS相关 ros::NodeHandle nh; //ROS节点句柄 ros::Publisher debugpub; //debug发布者 ros::Publisher debugpub1; //debug1发布者 std_msgs::Float64 debugdate; //debug数据 std_msgs::Float64 debugdate1; //debug1数据 rm_msgs::RmSerial RmSerialData; //接收串口数据 tf2_ros::Buffer tfBuffer_; // TF 缓冲区 tf2_ros::TransformListener tfListener; // 声明一个tf2_ros::TransformListener对象，并传入tfBuffer rm_msgs::ArmorArrayConstPtr m_armors; // 接收装甲板数据 //图像处理相关 cv::Mat frame; //接收的原始图像 cv::Mat frame_; //被处理的图像副本(保护原图像) cv::Mat camera_matrix_; //相机内参(构造函数中) cv::Mat dist_coeffs_; //畸变系数(构造函数中) Image img; //图像工具 //坐标变换工具 CAL::Calculater cal; //计算工具 CoordinateTransformer* coorConverter; //装甲板评分参数 std::vector col; // 数列中的每个数代表矩阵的每一列 std::vector row; // 数列中的每个数代表矩阵的每一行 std::vector tmp_v; // 存储C(n,k)的中间结果 std::vector\u0026lt;std::vector\u0026gt; result; // 存储C(n,k)的结果 std::vector\u0026lt;std::vector\u0026gt; nAfour; // 存储A(n,4)的结果 std::vector\u0026lt;std::vector\u0026gt; fourAfour; // 存储A(4,4)的结果 std::map\u0026lt;int, int\u0026gt; row_col; // 存储最终结果，row_col[i]=j表示矩阵的第i行第j列是要选取的数 //数据关联参数 double min = 0, tmp = 0; /* ======================== 跟踪控制参数 ======================== / //基本跟踪参数 char TrackingID; //跟踪中的装甲板ID bool Switch_Armor; //装甲板切换标识符 double trackTime; //目标丢失判定时间(秒) //角度补偿参数 double pitch_compensation; //pitch补偿 double yaw_compensation; //yaw补偿 //火力控制参数 bool all_fire = false; //完全火力模式(不停止射击) / ======================== 装甲板预测参数 ======================== / //空间参考点参数 Eigen::Vector3d pegPos; //空间标准点(用于装甲板跳变判断) double peg_point_pixel_now; //当前装甲板在像素坐标系的位置 double peg_point_pixel_last; //位于像素标准坐标系的上一个装甲板位置 //子弹参数 double BulletVector = 22; //子弹速度(初值给24m/s) / ======================== 时间戳管理 ======================== / //装甲板跟踪时间 long long Now_Time_armor = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//当前时间戳(用于计算丢失时间) long long Track_Time_armor = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//丢失时间戳(用于计算丢失时间) //帧率计算 long long begin_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//首时间 long long end_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//尾时间 // C++工具时间戳 long long tool_begin_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//首时间 long long tool_end_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//尾时间 // ros工具时间戳 ros::Time tool_begin_Time_ros = ros::Time::now(); ros::Time tool_end_Time_ros = ros::Time::now(); // 获取装甲板tf时间(判断是否是相同帧) ros::Time last_tf_time = ros::Time::now(); / ======================== 标志位 ======================== / bool functional = true; // 射击模式 int m_center_tracked; // 锁中心状态标志位 bool m_track_center; // 是否跟随中心 bool m_fix_on; // 重力补偿开关 std::string config_path_; // 存储配置路径 unique_ptr targetModel; // 目标模型 ros::Publisher AngPub; // 角度话题发布者 std::shared_ptr m_MPC; // 模型预测控制器 // =========================== 阈值 ==================================== double m_score_tolerance; //装甲板匹配得分最大值 double m_switch_threshold; // 更新装甲板切换的角度阈值，角度制 double m_force_aim_palstance_threshold; // 强制允许发射的目标旋转速度最大值，弧度制 double m_aim_angle_tolerance; // 自动击发时目标装甲板相对偏角最大值，角度制 double m_aim_pose_tolerance; // 自动击发位姿偏差最大值，弧度制 double m_aim_center_angle_tolerance; // 跟随圆心自动击发目标偏角判断，角度制 double m_switch_trackmode_threshold; // 更换锁中心模式角速度阈值，弧度制 double m_aim_center_palstance_threshold;// 跟随圆心转跟随装甲板的目标旋转速度最大值，弧度制 // =========================== 可视化 =================================== std::vector\u0026lt;std::vectorEigen::Vector3d\u0026gt; visual_armor_position_pose_temp; // 用于可视化存储观测装甲板的全局变量 GimbalPose m_cur_pose; // 当前位姿 GimbalPose m_target_pose; // 目标位姿 GimbalPose m_target_pose_debug; // 用于debug,比较mpc和传统模式 // =========================== 串口补偿 ======================================= double m_rollOffset = 0; double m_pitchOffset = 0; double m_yawOffset = 0; public: Tracker(ros::NodeHandle\u0026amp; nh,const std::string\u0026amp; config_path) : config_path_(config_path), tfListener(tfBuffer_) { this-\u0026gt;nh = nh; AngPub = nh.advertise\u0026lt;geometry_msgs::Vector3\u0026gt;(\u0026quot;/auto_angle\u0026quot;, 1000); debugpub = nh.advertise\u0026lt;std_msgs::Float64\u0026gt;(\u0026quot;/debugpub\u0026quot;, 1000); debugpub1 = nh.advertise\u0026lt;std_msgs::Float64\u0026gt;(\u0026quot;/debugpub1\u0026quot;, 1000); cal = new CAL::Calculater(this-\u0026gt;nh,this-\u0026gt;img); tfBuffer_.setUsingDedicatedThread(true); m_armors.reset(); // 显式初始化为空 // 初始化时创建 TargetModel targetModel = std::make_unique(config_path_); coorConverter = new CoordinateTransformer(config_path_); m_MPC = std::make_shared(config_path_); // 控制器初始化 // 检查配置文件路径是否有效 if (!config_path_.empty()) { try { setParam(config_path_); ROS_INFO(\u0026ldquo;Successfully loaded parameters from: %s\u0026rdquo;, config_path_.c_str()); } catch (const std::exception\u0026amp; e) { ROS_ERROR(\u0026ldquo;Failed to load parameters: %s\u0026rdquo;, e.what()); } } else { ROS_WARN(\u0026ldquo;No configuration file path provided. Using default parameters.\u0026rdquo;); } } ~Tracker() { delete cal; delete coorConverter; } /*************** * @brief 回调函数: 接受串口信息，并更新内部变量、TF、坐标系 * @param serial ROS 消息的智能指针，包含子弹速度、射击标志、补偿角度等 / void SetSerial(const rm_msgs::RmSerialConstPtr _serial){ if (_serial) { RmSerialData = _serial; // 目前注释掉了(到时候测试一下) // if(_serial-\u0026gt;BulletVec \u0026gt; 10){ // BulletVector = _serial-\u0026gt;BulletVec; // } if(_serial-\u0026gt;ShootFlag == \u0026lsquo;f\u0026rsquo;){ functional = true; }else if(_serial-\u0026gt;ShootFlag == \u0026lsquo;a\u0026rsquo;){ functional = true; }else{ functional = true; } // 从串口获取当前装甲板的姿态 m_cur_pose.roll = RmSerialData.Roll; m_cur_pose.pitch = RmSerialData.Pitch; m_cur_pose.yaw = RmSerialData.Yaw; // debug // ROS_INFO(\u0026ldquo;cur: pitch: %lf\u0026rdquo; , RmSerialData.Pitch); // ROS_INFO(\u0026ldquo;BulletVector: %lf\u0026rdquo; , BulletVector); // std::vectorEigen::Vector3d imuabsPos = cal-\u0026gt;GetPos(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;imu\u0026rdquo;,1); // cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;imuabs\u0026rdquo;,{0.0, 0.0, 0.0},{0.0, 0.0, imuabsPos[1].z()},0); // ROS_WARN(\u0026ldquo;have serial 3333333333333333333333333333\u0026rdquo;); } } /*************** * @brief 回调函数: 接收相机节点发送的图像,用于debug * @param img 图像信息 * @param frame 供算法线程直接使用的原始图 * @param frame_ 额外再 clone 一份，用于可视化 / void doimage(const sensor_msgs::ImageConstPtr img_) { cv_bridge::CvImagePtr cv_ptr; try { cv_ptr = cv_bridge::toCvCopy(img_, sensor_msgs::image_encodings::BGR8); } catch(cv_bridge::Exception\u0026amp; e) { ROS_ERROR(\u0026ldquo;cv_bridge exception: %s\u0026rdquo;, e.what()); return; } frame = cv_ptr-\u0026gt;image.clone(); frame_ = frame.clone(); // ROS_WARN(\u0026ldquo;have image 111111111111111111\u0026rdquo;); } // 回调函数: 接收识别发送的装甲板序列 void doArmors(rm_msgs::ArmorArrayConstPtr armors) { m_armors = armors; // ROS_WARN(\u0026ldquo;have armor 222222222222222222\u0026rdquo;); } /* * @brief 主跟踪循环：每帧调用一次，完成“决策 → 预测 → 补偿 → 发布”全链路 / void Track() { if (!m_armors) { cout \u0026laquo; \u0026ldquo;消息队列为空\u0026rdquo; \u0026laquo; endl; return; } try { //begin_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); if(!functional){ return; } TargetModel targetModel_temp = nullptr; targetModel_temp = armorUpdate(m_armors); if(targetModel_temp != nullptr){ cv::Point2d finAngle = {0.0, 0.0}; GimbalPose target_pose_temp = reconstruction_choose_compensation(); // 调试输出(打印云台需要转动到的角度) // cout \u0026laquo; \u0026ldquo;pitch: \u0026quot; \u0026laquo; target_pose_temp.pitch \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;yaw: \u0026quot; \u0026laquo; target_pose_temp.yaw \u0026laquo; endl; // 打印需要移动的相对角 // finAngle.x = target_pose_temp.pitch - m_cur_pose.pitch; // 计算需要移动的pitch角度 // finAngle.y = target_pose_temp.yaw - m_cur_pose.yaw; // 计算需要移动的yaw角度 // // // 陀螺仪的绝对角 finAngle.x = target_pose_temp.pitch; // 计算需要移动到的pitch角度 finAngle.y = target_pose_temp.yaw; // 计算需要移动到的yaw角度 // debug: 云台跟随效果rqt_plot打印 // yaw角 // debugdate.data = m_cur_pose.yaw; // 当前云台yaw角度 // debugdate1.data = target_pose_temp.yaw; // 计算出云台需要转动的yaw角度 // // pitch角 // debugdate.data = m_cur_pose.pitch; // 当前云台pitch角度 // debugdate1.data = target_pose_temp.pitch; // 计算出云台需要转动的pitch角度 // debug: 自动打弹打印 // cout \u0026laquo; \u0026ldquo;finAngle.x: \u0026quot; \u0026laquo; finAngle.x \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;finAngle.y: \u0026quot; \u0026laquo; finAngle.y \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;是否允许打弹: \u0026quot; \u0026laquo; targetModel_temp-\u0026gt;auto_fire \u0026laquo; endl; // if (targetModel_temp-\u0026gt;auto_fire) { // debugdate.data = 1; // } // else { // debugdate.data = 0; // } Pub_Aangle(true, targetModel_temp-\u0026gt;auto_fire, finAngle); }else{ Pub_Aangle(false); } // 帧率控制 // double time_line = 30.0; // end_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); // double frame_time = (end_Time - begin_Time)/1000.0; // if ((end_Time - begin_Time) - time_line \u0026lt; 0.0)// 帧率控制 // { // int sleep_time_ms = static_cast(time_line - (end_Time - begin_Time)); // std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time_ms)); // //frame_time = 0.03; // end_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); // frame_time = (end_Time - begin_Time)/1000.0; // // cout \u0026laquo; \u0026ldquo;sleep_time_ms:\u0026rdquo; \u0026laquo; sleep_time_ms \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl; // // cout \u0026laquo; \u0026ldquo;frame_time:\u0026rdquo; \u0026laquo; frame_time 1000 \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl; // } // begin_Time = end_Time; // debugdate1.data = frame_time 1000; debugpub.publish(debugdate); debugpub1.publish(debugdate1); } catch (const std::exception\u0026amp; e) { ROS_ERROR(\u0026quot;[Exception] In Track function: %s\u0026rdquo;, e.what()); } } // 重构选板 GimbalPose reconstruction_choose_compensation() { // 目标装甲板 Armor abs_facing_armor; Armor abs_target_armor; double hit_time = 0; double center_hit_time = 0; double m_time_off = COMMAND_TIMESPAN + eTime + 0.025; /** * 严格意义上来说，如果要准确预测击中时刻的装甲板位置的话，需要解一个非线性方程。此处采用一种近似的解法 * 根据当前最近装甲板距离计算击中时间，用于预测目标装甲板出现的位置 * 事实上相当于一步牛顿迭代法，或者说一阶的线性化\n/ // 选板模式 / 这段代码有两种选板模式: 1.锁中心模式: 条件: 目标旋转角速度绝对值超过阈值 2.瞄准最佳装甲板模式: 条件: 目标旋转角速度绝对值低于或接近阈值 / // 计算击打时间 Point3d point_temp1 = Point3d(targetModel-\u0026gt;m_status-\u0026gt;getFacingArmor(0).position.x(),targetModel-\u0026gt;m_status-\u0026gt;getFacingArmor(0).position.y(),targetModel-\u0026gt;m_status-\u0026gt;getFacingArmor(0).position.z()); center_hit_time = getDistance(point_temp1) / BulletVector + m_time_off; Point3d point_temp2 = Point3d(targetModel-\u0026gt;m_status-\u0026gt;getClosestArmor(0, 0).position.x(),targetModel-\u0026gt;m_status-\u0026gt;getClosestArmor(0, 0).position.y(),targetModel-\u0026gt;m_status-\u0026gt;getClosestArmor(0, 0).position.z()); hit_time = getDistance(point_temp2) / BulletVector + m_time_off; // 选板 abs_facing_armor = targetModel-\u0026gt;m_status-\u0026gt;getFacingArmor(center_hit_time); abs_target_armor = targetModel-\u0026gt;m_status-\u0026gt;getClosestArmor(hit_time, m_switch_threshold); // 判断是否锁中心 if (m_track_center \u0026amp;\u0026amp; ((abs(targetModel-\u0026gt;m_status-\u0026gt;palstance) - m_switch_trackmode_threshold) \u0026gt; m_aim_center_palstance_threshold)){ m_center_tracked = true; // 允许锁中心 // ROS_INFO(\u0026ldquo;锁中心\u0026rdquo;); } else { m_center_tracked = false; // 不允许锁中心 // ROS_INFO(\u0026ldquo;不锁中心\u0026rdquo;); } /* * 判断自动击发，条件如下： * 1. 目标旋转速度小于一定阈值，或目标装甲板相对偏角不超过一定范围 * 2. EKF先验稳定 * 3. 没有长时间丢识别 * 4. 当前云台跟随稳定，即当前位姿和目标位姿相近 / cv::Point2d vector_c = targetModel-\u0026gt;m_status-\u0026gt;center; // 目标旋转中心本身的向量 cv::Point2d vector_a = cv::Point2d(targetModel-\u0026gt;m_status-\u0026gt;center.x - abs_target_armor.position.x(), targetModel-\u0026gt;m_status-\u0026gt;center.y - abs_target_armor.position.y()); // 从旋转中心指向当前要瞄准的具体装甲板的向量 double abs_angle = R2D(acos((vector_c.x * vector_a.x + vector_c.y * vector_a.y) / (getDistance(vector_c) * getDistance(vector_a)))); // 计算上述两个向量之间的夹角 // debug // cout \u0026laquo; \u0026ldquo;角速度\u0026rdquo; \u0026laquo; targetModel-\u0026gt;m_status-\u0026gt;palstance \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;角速度阈值\u0026rdquo; \u0026laquo; m_force_aim_palstance_threshold \u0026laquo; endl; // 目标旋转角速度低于强制瞄准阈值 // cout \u0026laquo; \u0026ldquo;夹角\u0026rdquo; \u0026laquo; abs_angle \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;夹角阈值\u0026rdquo; \u0026laquo; m_aim_angle_tolerance \u0026laquo; endl; // 装甲板偏角度 // cout \u0026laquo; \u0026ldquo;云台和目标姿态差\u0026rdquo; \u0026laquo; (m_target_pose - m_cur_pose).norm() \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;姿态差阈值\u0026rdquo; \u0026laquo; m_aim_pose_tolerance \u0026laquo; endl; // 云台和目标姿态差(云台跟随效果) // cout \u0026laquo; \u0026ldquo;!Switch_Armor:\u0026rdquo; \u0026laquo; !Switch_Armor \u0026laquo; endl; // 目标的旋转角度低于强制瞄准阈值或计算出装甲板偏角小于容差,这意味着要么目标转得慢,容易瞄准; 要么即使目标目标在快速旋转,但刚好有一个装甲板转到了非常正对摄像头(夹角很小),也是很好的设计时机 // 观测器,目标持续可见,云台跟随良好(云台当前的实际姿态与需要旋转的目标之间的差异很小,说明云台基本对准目标) // 由于电控发送过来的角度是上面是负下面是正 // 而我们计算出来的角度是上面是正下面是负数 // 由于火控的判断需要和电控发送过来的角度进行比较 // 为了保证逻辑的完整性,所以在火控这里创建局部变量,将pitch角变为负数 // 然后不影响整体逻辑,整体的逻辑在发送角度的地方pitch角度给负号 GimbalPose m_target_pose_temp = m_target_pose; m_target_pose_temp.pitch = -m_target_pose_temp.pitch; // debugdate.data = m_cur_pose.pitch; // rqt预测x车中心 // debugdate1.data = m_target_pose_temp.pitch; // rqt预测x车中心 // debugdate.data = m_cur_pose.yaw; // rqt预测x车中心 // debugdate1.data = m_target_pose_temp.yaw; // rqt预测x车中心 // 自动开火 if ((abs(targetModel-\u0026gt;m_status-\u0026gt;palstance) \u0026lt; m_force_aim_palstance_threshold || abs_angle \u0026lt; m_aim_angle_tolerance) \u0026amp;\u0026amp; targetModel-\u0026gt;ekf-\u0026gt;stable() \u0026amp;\u0026amp; !Switch_Armor \u0026amp;\u0026amp; (m_target_pose_temp - m_cur_pose).norm() \u0026lt; m_aim_pose_tolerance ) { targetModel-\u0026gt;auto_fire = true; if (m_center_tracked \u0026amp;\u0026amp; !(abs_angle \u0026lt; m_aim_center_angle_tolerance)) { targetModel-\u0026gt;auto_fire = false; } } else { targetModel-\u0026gt;auto_fire = false; } // 云台控制模式 bool control_mode = 0; // 0: 普通模式 1: mpc模式 if (control_mode == 0) { if (m_center_tracked) { m_target_pose = getAngle(Point3d(abs_facing_armor.position.x(),abs_facing_armor.position.y(),abs_facing_armor.position.z()), m_cur_pose, BulletVector); // projectMapPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(center_hit_time),visual_armor_position_pose_temp); // 这里打印的是计算的子弹飞行击中的时间的那一刻的重构装甲板 projectMapPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(0),visual_armor_position_pose_temp); // 这里打印的重构装甲板是和观测装甲板同一时间的预测装甲板 // // debug // Armors armors_temp_debug = targetModel-\u0026gt;m_status-\u0026gt;getArmors(center_hit_time); // for (int i = 0; i \u0026lt; armors_temp_debug.size(); i++) { // cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;armor\u0026rdquo; + std::to_string(i), armors_temp_debug[i].position, armors_temp_debug[i].rpy, i); // } // cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;carcenter\u0026rdquo;,{targetModel-\u0026gt;m_status-\u0026gt;center.x,targetModel-\u0026gt;m_status-\u0026gt;center.y,0},{0.0, 0.0, 0.0},0);//上传目标的TF // debug(弹道打印) // 记录当前时间 tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_facing_armor.position); long long draw_visual_now_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); tools::draw_simulated_bullets(this-\u0026gt;coorConverter,shoot_param,frame_,draw_visual_now_time); } else { m_target_pose = getAngle(Point3d(abs_target_armor.position.x(),abs_target_armor.position.y(),abs_target_armor.position.z()), m_cur_pose, BulletVector); // projectMapPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(hit_time),visual_armor_position_pose_temp); // 这里打印的是计算的子弹飞行击中的时间的那一刻的重构装甲板 projectMapPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(0),visual_armor_position_pose_temp); // 这里打印的重构装甲板是和观测装甲板同一时间的预测装甲板 // // debug // Armors armors_temp_debug = targetModel-\u0026gt;m_status-\u0026gt;getArmors(hit_time); // for (int i = 0; i \u0026lt; armors_temp_debug.size(); i++) { // cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;armor\u0026rdquo; + std::to_string(i), armors_temp_debug[i].position, armors_temp_debug[i].rpy, i); // } // cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;carcenter\u0026rdquo;,{targetModel-\u0026gt;m_status-\u0026gt;center.x,targetModel-\u0026gt;m_status-\u0026gt;center.y,0},{0.0, 0.0, 0.0},0);//上传目标的TF // debug(弹道打印) // 记录当前时间 tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_target_armor.position); long long draw_visual_now_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); tools::draw_simulated_bullets(this-\u0026gt;coorConverter,shoot_param,frame_,draw_visual_now_time); } } else { // 西工大轨迹规划器 m_target_pose = m_MPC-\u0026gt;getGimbalSpeed(targetModel-\u0026gt;m_status,m_cur_pose,BulletVector); } // // debug // ///////////////////////////////////////////// // // 云台控制模式 // if (m_center_tracked) { // m_target_pose = getAngle(Point3d(abs_facing_armor.position.x(),abs_facing_armor.position.y(),abs_facing_armor.position.z()), m_cur_pose, BulletVector); // // projectMapPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(0),visual_armor_position_pose_temp); // } // else { // m_target_pose = getAngle(Point3d(abs_target_armor.position.x(),abs_target_armor.position.y(),abs_target_armor.position.z()), m_cur_pose, BulletVector); // // projectMapPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(0),visual_armor_position_pose_temp); // } // // 西工大轨迹规划器 // m_target_pose_debug = m_MPC-\u0026gt;getGimbalSpeed(targetModel-\u0026gt;m_status,m_cur_pose,BulletVector); // // cout \u0026laquo; \u0026ldquo;状态: \u0026quot; \u0026laquo; targetModel-\u0026gt;m_status-\u0026gt;palstance \u0026laquo; endl; // // cout \u0026laquo; \u0026ldquo;弹速: \u0026quot; \u0026laquo; BulletVector \u0026laquo; endl; // // cout \u0026laquo; \u0026ldquo;当前云台状态: \u0026quot; \u0026laquo; \u0026ldquo;pitch: \u0026quot; \u0026laquo; m_cur_pose.pitch \u0026laquo; \u0026ldquo;yaw: \u0026quot; \u0026laquo; m_cur_pose.yaw \u0026laquo; endl; // // debugdate.data = m_target_pose.yaw; // 未规划 // // debugdate1.data = m_target_pose_debug.yaw; // mpc规划 // cout \u0026laquo; \u0026ldquo;未规划yaw: \u0026quot; \u0026laquo; m_target_pose.yaw \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;mpc规划yaw: \u0026quot; \u0026laquo; m_target_pose_debug.yaw \u0026laquo; endl; // // debugdate.data = m_target_pose.pitch; // 未规划 // // debugdate1.data = m_target_pose_debug.pitch; // mpc规划 // // cout \u0026laquo; \u0026ldquo;未规划pitch: \u0026quot; \u0026laquo; m_target_pose.pitch \u0026laquo; endl; // // cout \u0026laquo; \u0026ldquo;mpc规划pitch: \u0026quot; \u0026laquo; m_target_pose_debug.pitch \u0026laquo; endl; // ///////////////////////////////////////////// return m_target_pose; } /* @brief: 根据装甲板坐标、当前云台位姿、射速等信息计算目标位姿 / GimbalPose getAngle(cv::Point3d position, GimbalPose cur_pose, double bullet_speed) { GimbalPose solve_angle; cv::Point3d gun_base = position; double pitch_in_gun = std::atan(gun_base.z / std::sqrt(gun_base.x * gun_base.x + gun_base.y * gun_base.y)); double yaw_in_gun = std::atan(gun_base.y / gun_base.x); solve_angle.pitch = pitch_in_gun; solve_angle.yaw = yaw_in_gun; // 重力补偿 if (m_fix_on) { double dis = sqrt(pow(gun_base.x, 2) + pow(gun_base.z, 2) + pow(gun_base.y, 2)); double angle_fix = 0; // debug ///////////////////////////////////////////////////////////// // cout \u0026laquo; \u0026ldquo;dis: \u0026quot; \u0026laquo; dis \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;disxy: \u0026quot; \u0026laquo; std::sqrt(gun_base.x * gun_base.x + gun_base.y * gun_base.y) \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;position.z\u0026rdquo; \u0026laquo; position.z \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;pitch: \u0026quot; \u0026laquo; solve_angle.pitch \u0026laquo; endl; ////////////////////////////////////////////////////////////////////// if (abs(9.8 * dis * pow(cos(-solve_angle.pitch), 2)) / pow(bullet_speed, 2) - sin(-solve_angle.pitch) \u0026lt;= 1) { angle_fix = 0.5 * (asin((9.8 * dis * pow(cos(-solve_angle.pitch), 2)) / pow(bullet_speed, 2) - sin(-solve_angle.pitch)) - solve_angle.pitch); // cout \u0026laquo; \u0026ldquo;重力补偿角: \u0026quot; \u0026laquo; angle_fix \u0026laquo; endl; } else { std::cout \u0026laquo; \u0026ldquo;[AngleSolver] Wrong distance in angle_fix\u0026rdquo; \u0026laquo; std::endl; } solve_angle.pitch += angle_fix; } // debug 判断枪口是否能够正确瞄准装甲板 // ////////////////////////////////////////////////////////////////////// // std::vectorEigen::Vector3d Postest = cal-\u0026gt;GetPos(\u0026ldquo;gun\u0026rdquo;,\u0026ldquo;DETECT4\u0026rdquo;,1); // cal-\u0026gt;TFUpdata(\u0026ldquo;gun\u0026rdquo;,\u0026ldquo;DETECT6_gun\u0026rdquo;, Postest[0], Postest[1], 1); // 安装误差导致的硬补偿 solve_angle.pitch += pitch_compensation; solve_angle.yaw += yaw_compensation; return solve_angle; } / * @brief 发布最终云台控制角度与开火指令的“一站式”接口 * @param hasAngle 是否成功计算出目标角度（true=有，false=无） * @param auto_fire 本帧是否允许自动开火（默认 false） * @param euler_angles_temp 目标相对角度（弧度），默认 (0,0) / ///////////////////// // 周期性发送debug // int cntflag = 0; // int flag = 0; /////////////////// void Pub_Aangle(bool hasAngle, bool auto_fire = false, const cv::Point2d \u0026amp; euler_angles_temp = cv::Point2d(0, 0)){ /////////////////////////////////////////// // 周期性发送debug // cntflag++; // if (cntflag == 2000) { // if (flag == 0) { // flag = 1; // } // else { // flag = 0; // } // cntflag = 0; // } ///////////////////////////////////////// // debug: 云台跟随效果rqt_plot打印 // // yaw角 // debugdate.data = m_cur_pose.yaw; // 当前云台yaw角度 // debugdate1.data = euler_angles_temp.y; // 计算出云台需要转动的yaw角度 // // pitch角 // debugdate.data = m_cur_pose.pitch; // 当前云台pitch角度 // debugdate1.data = euler_angles_temp.x; // 计算出云台需要转动的pitch角度 geometry_msgs::Vector3 Angle; if(hasAngle \u0026amp;\u0026amp; euler_angles_temp.x != 0.0 \u0026amp;\u0026amp; euler_angles_temp.y != 0.0){ Angle.x = (euler_angles_temp.x) + m_pitchOffset; Angle.y = euler_angles_temp.y - m_yawOffset; Angle.z = 1; // debugdate.data = m_cur_pose.yaw; // 当前云台yaw角度 // debugdate1.data = Angle.y; // 计算出云台需要转动的yaw角度 // 自动打弹 if(auto_fire || (all_fire \u0026amp;\u0026amp; TrackingID != 6 \u0026amp;\u0026amp; TrackingID != -1)){ Angle.z = 3; // 设置设计指令码 } // debug /////////////////////////////////////// // Angle.z = 3; // 测试电控打弹 (一直发射子弹) // Angle.z = 1; // 测试电控打弹 // Angle.z = 0; // 测试电控打弹 ////////////////////////////////////// // 周期性发送debug // if (flag == 1) { // Angle.z = 3; // } // else { // Angle.z = 1; // } //////////////////////////////// AngPub.publish(Angle); }else{ // 如果没有角度信息,返回陀螺仪角度 Angle.x = RmSerialData.Pitch - m_pitchOffset; Angle.y = RmSerialData.Yaw - m_yawOffset; Angle.z = 0; // cout \u0026laquo; \u0026ldquo;RmSerialData.Pitch:\u0026rdquo; \u0026laquo; -RmSerialData.Pitch \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;RmSerialData.Yaw:\u0026rdquo; \u0026laquo; RmSerialData.Yaw \u0026laquo; endl; // debug /////////////////////////////////// // Angle.z = 3; // 测试电控打弹 (一直发射子弹) // Angle.z = 1; // 测试电控打弹 // Angle.z = 0; // 测试电控打弹 ////////////////////////////////// // 周期性发送debug // if (flag == 1) { // Angle.z = 3; // } // else { // Angle.z = 1; // } /////////////////////////////////// AngPub.publish(Angle); } // debug: 自动开火 // debugdate.data = Angle.z; } // 参数配置函数 void setParam(const std::string \u0026amp;file_path) { try { // 1. 文件存在性检查 if (file_path.empty()) { ROS_ERROR(\u0026ldquo;Empty config file path provided\u0026rdquo;); return; } //2. 检查文件是否打开 std::ifstream file_check(file_path); if (!file_check.is_open()) { ROS_ERROR(\u0026ldquo;Failed to open config file: %s. Reason: %s\u0026rdquo;, file_path.c_str(), strerror(errno)); return; } ROS_INFO(\u0026ldquo;Loading parameters from: %s\u0026rdquo;, file_path.c_str()); // 3. 加载 YAML 文件并添加详细错误处理 YAML::Node config; try { config = YAML::LoadFile(file_path); } catch (const YAML::BadFile\u0026amp; e) { ROS_ERROR(\u0026ldquo;Bad YAML file: %s\u0026rdquo;, e.what()); return; } catch (const YAML::ParserException\u0026amp; e) { ROS_ERROR(\u0026ldquo;YAML parsing error at line %d: %s\u0026rdquo;, e.mark.line, e.what()); return; } //加载参数 if (config[\u0026ldquo;Tracker\u0026rdquo;]){ YAML::Node track = config[\u0026ldquo;Tracker\u0026rdquo;]; COMMAND_TIMESPAN = track[\u0026ldquo;COMMAND_TIMESPAN\u0026rdquo;].as(0.11); eTime = track[\u0026ldquo;eTime\u0026rdquo;].as(0.002); local_gravity_ = track[\u0026ldquo;local_gravity_\u0026rdquo;].as(9.80665); trackTime = track[\u0026ldquo;trackTime\u0026rdquo;].as(0.7); pitch_compensation = track[\u0026ldquo;pitch_compensation\u0026rdquo;].as(0); yaw_compensation = track[\u0026ldquo;yaw_compensation\u0026rdquo;].as(0); m_score_tolerance = track[\u0026ldquo;score_tolerance\u0026rdquo;].as(1); m_switch_threshold = track[\u0026ldquo;switch_threshold\u0026rdquo;].as(15); // 添加yawl文件 m_aim_angle_tolerance = track[\u0026ldquo;aim_angle_tolerance\u0026rdquo;].as(40); // 添加yaml文件 m_aim_pose_tolerance = track[\u0026ldquo;aim_pose_tolerance\u0026rdquo;].as(0.05); // 添加yaml文件 m_aim_center_angle_tolerance = track[\u0026ldquo;aim_center_angle_tolerance\u0026rdquo;].as(1); // 添加yaml文件 m_switch_trackmode_threshold = track[\u0026ldquo;switch_trackmode_threshold\u0026rdquo;].as(1.2); // 添加yaml文件 m_aim_center_palstance_threshold = track[\u0026ldquo;aim_center_palstance_threshold\u0026rdquo;].as(6.5); // 添加yaml文件 m_force_aim_palstance_threshold = track[\u0026ldquo;force_aim_palstance_threshold\u0026rdquo;].as(0.8); // 添加yaml文件 m_track_center = track[\u0026ldquo;track_center\u0026rdquo;].as(0); m_fix_on = track[\u0026ldquo;fix_on\u0026rdquo;].as(1); // ROS_INFO(\u0026ldquo;Tracker parameters loaded:\u0026rdquo;); // ROS_INFO(\u0026ldquo;COMMAND_TIMESPAN:%.3f\u0026rdquo;,COMMAND_TIMESPAN); // ROS_INFO(\u0026ldquo;eTime:%.3f\u0026rdquo;,eTime); // ROS_INFO(\u0026ldquo;local_gravity_:%.3f\u0026rdquo;,local_gravity_); // ROS_INFO(\u0026ldquo;trackTime:%.3f\u0026rdquo;,trackTime); // ROS_INFO(\u0026ldquo;pitch_compensation:%.3f\u0026rdquo;,pitch_compensation); // ROS_INFO(\u0026ldquo;yaw_compensation:%.3f\u0026rdquo;,yaw_compensation); // ROS_INFO(\u0026ldquo;track_center: %d\u0026rdquo;, m_track_center); } else { ROS_WARN(\u0026ldquo;No \u0026lsquo;Tracker\u0026rsquo; section in config file\u0026rdquo;); } // 加载相机参数 if (config[\u0026ldquo;Camera\u0026rdquo;]){ YAML::Node camera = config[\u0026ldquo;Camera\u0026rdquo;]; // 加载相机内参矩阵 if (camera[\u0026ldquo;camera_matrix\u0026rdquo;]) { std::vector cam_matrix_array = camera[\u0026ldquo;camera_matrix\u0026rdquo;].as\u0026lt;std::vector\u0026gt;(); if (cam_matrix_array.size() == 9) { // 创建33相机内参矩阵 camera_matrix_ = (cv::Mat_(3, 3) \u0026laquo; cam_matrix_array[0], cam_matrix_array[1], cam_matrix_array[2], cam_matrix_array[3], cam_matrix_array[4], cam_matrix_array[5], cam_matrix_array[6], cam_matrix_array[7], cam_matrix_array[8]); } } else { ROS_WARN(\u0026ldquo;No \u0026lsquo;camera_matrix\u0026rsquo; section in config file\u0026rdquo;); } // 加载畸变系数矩阵 if (camera[\u0026ldquo;dist_coeffs\u0026rdquo;]) { std::vector dist_coeffs_array = camera[\u0026ldquo;dist_coeffs\u0026rdquo;].as\u0026lt;std::vector\u0026gt;(); if (dist_coeffs_array.size() == 5) { dist_coeffs_ = (cv::Mat_(1, 5) \u0026laquo; dist_coeffs_array[0], dist_coeffs_array[1], dist_coeffs_array[2], dist_coeffs_array[3], dist_coeffs_array[4]); } } else { ROS_WARN(\u0026ldquo;No \u0026lsquo;dist_coeffs\u0026rsquo; section in config file\u0026rdquo;); } // 加载补偿误差函数载串口补偿 if (config[\u0026ldquo;InstallOffset\u0026rdquo;]) { YAML::Node mod = config[\u0026ldquo;InstallOffset\u0026rdquo;]; m_rollOffset = mod[\u0026ldquo;rollOffset\u0026rdquo;].as(0); m_pitchOffset = mod[\u0026ldquo;pitchOffset\u0026rdquo;].as(0); m_yawOffset = mod[\u0026ldquo;yawOffset\u0026rdquo;].as(0); // ROS_INFO(\u0026ldquo;InstallOffset parameters loaded:\u0026rdquo;); // ROS_INFO(\u0026ldquo;m_rollOffset:%.3f\u0026rdquo;,m_rollOffset); // ROS_INFO(\u0026ldquo;m_pitchOffset:%.3f\u0026rdquo;,m_pitchOffset); // ROS_INFO(\u0026ldquo;m_yawOffset:%.3f\u0026rdquo;,m_yawOffset); // ROS_INFO(\u0026ldquo;InstallOffset parameters loaded successfully\u0026rdquo;); } else { ROS_WARN(\u0026ldquo;No \u0026lsquo;CoordinateTransformer\u0026rsquo; section in config file\u0026rdquo;); } } } catch (const YAML::Exception\u0026amp; e) { ROS_ERROR(\u0026ldquo;YAML parsing error: %s\u0026rdquo;, e.what()); } catch (const std::exception\u0026amp; e) { ROS_ERROR(\u0026ldquo;Error loading parameters: %s\u0026rdquo;, e.what()); } catch (\u0026hellip;) { ROS_ERROR(\u0026ldquo;Unknown error occurred while loading config file\u0026rdquo;); } } /** * @brief 把世界坐标系下的 3D 点投影到当前图像平面 * @param objectPoints 世界坐标系中的一组 3D 点 * @param frame 用于可视化的 OpenCV 图像 * @param can_show 是否弹出窗口并打印调试信息 * @return 投影到图像上的 2D 像素坐标 / // 重投影这里传入了相机的内参和旋转矩阵,如果重投影的结果有问题(rviz也是这样)(很有可能是识别那里世界坐标系变换到相机坐标系有问题),这很大可能是因为相机内参和畸变系数不准导致的,这时候需要重新标定相机 std::vectorcv::Point2d projectPointsToImage(const std::vectorcv::Point3d\u0026amp; objectPoints, cv::Mat\u0026amp; frame, bool can_show) { cv::Mat rvec = (cv::Mat_(3, 1) \u0026laquo; 0.0f, 0.0f, 0.0f);//旋转向量 cv::Mat tvec = (cv::Mat_(3, 1) \u0026laquo; 0.0f, 0.0f, 0.0f);//平移向量 std::vectorcv::Point2d imagePoints; cv::projectPoints(objectPoints, rvec, tvec, camera_matrix_, dist_coeffs_, imagePoints); if(can_show){ for (size_t i = 0; i \u0026lt; imagePoints.size(); ++i) { cv::circle(frame, imagePoints[i], 7, cv::Scalar(0, 255, 0), -1); //std::cout \u0026laquo; \u0026ldquo;3D Point \u0026quot; \u0026laquo; i \u0026laquo; \u0026ldquo;: \u0026quot; \u0026laquo; objectPoints[i] \u0026laquo; std::endl; //std::cout \u0026laquo; \u0026ldquo;Projected 2D Point \u0026quot; \u0026laquo; i \u0026laquo; \u0026ldquo;: \u0026quot; \u0026laquo; imagePoints[i] \u0026laquo; std::endl; } cv::imshow(\u0026ldquo;Draw Points\u0026rdquo;, frame); cv::waitKey(1); } return imagePoints; } /* * @brief 把“世界坐标系下的三维标准点”重映射到图像二维平面 * @param pegPos 标准点在“map”坐标系下的三维坐标 (x,y,z) * @param cal TF/坐标变换工具类实例 * @param projectPointsToImage 3D→2D 投影函数 * @param frame_ 克隆图形用于可视化 * @return 该点在图像上的像素坐标 (u,v)；若 TF 异常则返回 (0,0) / cv::Point2d project_pegPoints(Eigen::Vector3d\u0026amp; pegPos){ Eigen::Vector3d resultPos; resultPos \u0026laquo; pegPos.x(), pegPos.y(), pegPos.z(); Eigen::Vector3d resultPose; resultPose \u0026laquo; 0, 0, 0; cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;PEG\u0026rdquo;, resultPos, resultPose, 1); auto absPos_temp = cal-\u0026gt;GetPos(\u0026ldquo;cam\u0026rdquo;,\u0026ldquo;PEG\u0026rdquo;,1); if((absPos_temp[0].x() == 0 \u0026amp;\u0026amp; absPos_temp[0].y() == 0 \u0026amp;\u0026amp; absPos_temp[0].z() == 0) || isnan(absPos_temp[1].z())){ cout \u0026laquo; \u0026ldquo;重投影坐标系未更新！\u0026rdquo; \u0026laquo; endl; return cv::Point2d(0.0, 0.0); } cv::Point3d objectPoint( absPos_temp[0].x(), absPos_temp[0].y(), absPos_temp[0].z()); std::vectorcv::Point3d objectPoints = {objectPoint}; std::vectorcv::Point2d imagePoints = projectPointsToImage(objectPoints, frame_, false); return imagePoints[0]; } private: /* * @brief 装甲板处理总入口：决策 → 丢失检测 → 预测 → 返回平滑 3D 状态 * @param armors 当前帧所有检测到的装甲板数组 * @return TargetModel* 目标模型指针（正常状态）或nullptr（异常状态）\n/ TargetModel armorUpdate(rm_msgs::ArmorArrayConstPtr armors) { // 1.时间戳管理 // 获取当前时间戳(毫秒) Now_Time_armor = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); // 2.计算目标丢失时间(毫秒) double Lost_Time = (Now_Time_armor - Track_Time_armor)/1000.0; // 目标丢失状态处理(上一帧的目标状态) if(!Switch_Armor){ // 跟踪中状态:打印跟踪当前ID // std::cout \u0026laquo; \u0026ldquo;追踪中TrackingID: \u0026quot; \u0026laquo; static_cast(TrackingID) \u0026laquo; std::endl; }else{ // 目标丢失状态: 重置跟踪ID和旋转方向 // std::cout \u0026laquo; \u0026ldquo;目标丢失!重置TrackingID!\u0026rdquo; \u0026laquo; std::endl; TrackingID = -1; } // 3.装甲板决策 rm_msgs::Armor* detect = nullptr; if(armors-\u0026gt;armors.size()){ // 决策函数: 从所有装甲板中选择最佳跟踪目标(本帧决策) detect = decision(armors, TrackingID, Switch_Armor); if(detect != nullptr){ // 本帧状态更新 if(Switch_Armor){ // 目标丢失后重新锁定: 更新跟踪ID TrackingID = detect-\u0026gt;number; Switch_Armor = false; // 更新追踪时间戳 Track_Time_armor = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//更新追踪时间戳 }else if(detect-\u0026gt;number == TrackingID){ // 正常跟踪: 更新时间戳 Track_Time_armor = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();//更新追踪时间戳 }else{ // 检测到非跟踪目标: 忽略 detect = nullptr; } } } // 4. 目标丢失判定 if(Lost_Time \u0026gt; trackTime){ // 普通目标: 超过跟踪时间预测判定丢失 if(TrackingID != 6){ Switch_Armor = true; // 前哨站:3倍跟踪时间阈值与判定丢失 }else if(Lost_Time \u0026gt; trackTime 3){ Switch_Armor = true; } } // 运行预测流程 TargetModel targetModel_temp = nullptr; targetModel_temp = runPrediction(Switch_Armor, detect); return targetModel_temp; } /** * @brief 装甲板决策函数 —— 从所有候选装甲板中选出“应被瞄准”的那一块 * @param armors 当前帧所有装甲板数组（ROS 消息） * @param TrackingID_temp 正在跟踪的目标 id（字符形式） * @param Switch_Armor_temp 是否允许切换目标（true = 允许重新选择） * @return rm_msgs::Armor* 选中的装甲板指针；nullptr 表示无有效目标 * @note 决策流程：先按距离/ID 筛，再按旋转方向挑同号板\n/ rm_msgs::Armor decision(rm_msgs::ArmorArrayConstPtr armors, const char\u0026amp; TrackingID_temp, const bool\u0026amp; Switch_Armor_temp){ rm_msgs::Armor* detect_temp = nullptr; double minDisToCenter = std::numeric_limits::max(); int cunt = 0; for(auto\u0026amp; armor:armors-\u0026gt;armors){ if(Eigen::Vector3d({armor.armorPose.position.x,armor.armorPose.position.y,armor.armorPose.position.z}).norm() \u0026gt;= 6.0 || armor.number == 9){ if(armor.number != 6){ continue; }else if(Eigen::Vector3d({armor.armorPose.position.x,armor.armorPose.position.y,armor.armorPose.position.z}).norm() \u0026gt;= 10.0 || armor.number == 9){ continue; } } if(armor.number == TrackingID_temp || (Switch_Armor_temp \u0026amp;\u0026amp; armor.armorToCenterDis \u0026lt; minDisToCenter)){ if (!detect_temp || armor.armorToCenterDis \u0026lt; minDisToCenter) { detect_temp = const_cast\u0026lt;rm_msgs::Armor*\u0026gt;(\u0026amp;armor); minDisToCenter = armor.armorToCenterDis; } } if(armor.number == TrackingID_temp){ cunt++; } } if(detect_temp == nullptr){ //std::cout \u0026laquo; \u0026ldquo;决策结果为空！\u0026rdquo; \u0026laquo; std::endl; return nullptr; } return detect_temp; } /** * @brief 初始化目标模型类型 * @param detect 当前帧检测到的装甲板信息指针 * @return bool true-模型已初始化完成，false-模型未初始化完成\n/ bool initModel(rm_msgs::Armor detect) { //detect-\u0026gt;type删掉,观测不准,detect-\u0026gt;type基地直接瞄，不过卡尔曼 if (detect != nullptr \u0026amp;\u0026amp; targetModel-\u0026gt;m_type_init_cnt \u0026lt; 3) { // 大装甲板1号-\u0026gt;英雄 // 小装甲板2~7号-\u0026gt;普通步兵 if ((detect-\u0026gt;number \u0026gt;= 1 \u0026amp;\u0026amp; detect-\u0026gt;number \u0026lt;= 5) || detect-\u0026gt;number == 7) { // 类型变化时重置计数器 if (targetModel-\u0026gt;m_model_type != KinematicModel::STANDARD) targetModel-\u0026gt;m_type_init_cnt = 0; // 设置为标准模型 targetModel-\u0026gt;m_model_type = KinematicModel::STANDARD; targetModel-\u0026gt;m_type_init_cnt++; } else if (detect-\u0026gt;number == 6) { if (targetModel-\u0026gt;m_model_type != KinematicModel::OUTPOST) targetModel-\u0026gt;m_type_init_cnt = 0; targetModel-\u0026gt;m_model_type = KinematicModel::OUTPOST; targetModel-\u0026gt;m_type_init_cnt++; } // 连续3帧确认后初始化模型 if (targetModel-\u0026gt;m_type_init_cnt \u0026gt;= 3) { setModelType(targetModel-\u0026gt;m_model_type); return true; // 已经初始化 } else { // 不允许击打 targetModel-\u0026gt;auto_fire = false; return false; // 没有初始化 } } if (targetModel-\u0026gt;m_type_init_cnt \u0026lt; 3) { return false; // 没有检测到目标,没有初始化 } else { return true; // 已经初始化,大于3帧，处于跟踪状态,短暂丢失 } } /** * @brief 重置预测模块和目标模型状态 * @param lost_state 目标丢失状态（true-目标丢失，false-目标存在） * @param abnormal 预测状态异常标志（true-状态异常，false-状态正常） * @return bool 返回当前的目标丢失状态 * @note 重置条件：目标丢失(lost_state)或预测状态异常(abnormal) / bool reset(bool lost_state, bool abnormal) { // 检查是否需要重置(目标丢失或状态异常) if(lost_state || abnormal){ // 1. 创建新的目标模型实例(完全重置) targetModel = std::make_unique(config_path_); return lost_state; // 丢失状态 } else { return lost_state; // 跟踪状态 } } /* * @brief 执行预测流程的核心函数\n/ TargetModel runPrediction(bool Switch_Armor_temp, rm_msgs::Armor* detect){ // 检查重置 if (reset(Switch_Armor_temp, targetModel-\u0026gt;abnormal)){ return nullptr; } // 初始化 bool isInitialized = initModel(detect); if (isInitialized){ // 更新目标模型(执行核心预测流程) updateTargetModel(detect); } else{ return nullptr; } // 返回预测结果(异常状态返回nullptr,正常状态返回目标模型指针) if (targetModel-\u0026gt;abnormal) // 标志位异常返回空,正常的话检查是否初始化 { return nullptr; } else { return targetModel.get(); } } /** * @brief 更新目标模型状态 * @param detect 当前帧检测到的装甲板信息指针\n/ void updateTargetModel(rm_msgs::Armor detect) { // 初始化装甲板属性(设置ID,类型) setArmorProps(detect); // 核心预测处理:使用扩展卡尔曼滤波器进行状态预测和更新 updateEKFState(detect); } /** * @brief 设置装甲板基本属性 * @param detect 当前检测到的装甲板消息指针（nullptr 则跳过）\n/ void setArmorProps(rm_msgs::Armor detect){ if(detect != nullptr){ // 设置装甲板ID targetModel-\u0026gt;armor.id = detect-\u0026gt;number; // 设置装甲板类型 targetModel-\u0026gt;armor.armor_type = detect-\u0026gt;type; // 根据装甲板类型设置数量 targetModel-\u0026gt;count = (detect-\u0026gt;number == 6) ? 3 : 4; } } /** * @brief 相机坐标系点重投影可视化 * @param pre_armors 预测的装甲板位置信息 * @param armors_position_pose 观测到的装甲板位置和姿态信息 / void projectCameraPointsToImage(Armors pre_armors,std::vector\u0026lt;std::vectorEigen::Vector3d\u0026gt; armors_position_pose) { std::vectorcv::Point3d objectPoints; // 将观测的装甲板放进预测的装甲板数组来投影观测的装甲板 if (armors_position_pose.size()) { Armor temp; temp.position.x() = armors_position_pose[0][0].x(); temp.position.y() = armors_position_pose[0][0].y(); temp.position.z() = armors_position_pose[0][0].z(); pre_armors.push_back(temp); } // 5个点的重投影(观测点和重构出来的四块装甲板) for (int i = 0; i \u0026lt; pre_armors.size(); i++) { coorConverter-\u0026gt;tfupdate_imu({m_cur_pose.roll,m_cur_pose.pitch,m_cur_pose.yaw}); Eigen::Vector3d temp = coorConverter-\u0026gt;map2Cam(pre_armors[i].position); cv::Point3d point_temp_1 = Point3d(temp.x(),temp.y(),temp.z()); objectPoints.push_back(point_temp_1); } // 重投影装甲板中心点 std::vectorcv::Point2d imagePoints = projectPointsToImage(objectPoints, frame_, 0); for (size_t i = 0; i \u0026lt; imagePoints.size(); i++) { cv::circle(frame_,imagePoints[i],7,cv::Scalar(0,255,0),-1); } int count = targetModel-\u0026gt;m_status-\u0026gt;number; // 为每个装甲板绘制四个角点和对角线 for (int i = 0; i \u0026lt; count; i++) { Eigen::Vector3d rpy = pre_armors[i].rpy; // 装甲板姿态 Eigen::Vector3d center = pre_armors[i].position; // 装甲板中心位置 double yaw = pre_armors[i].rpy.z() - m_cur_pose.yaw; std::vectorEigen::Vector3d cornerpoint = coorConverter-\u0026gt;armor2Corner(center,targetModel-\u0026gt;armor.id,0.2618, yaw); // 将角点从世界系 -\u0026gt; 相机系 for (int i = 0; i \u0026lt; cornerpoint.size(); i++) { cornerpoint[i] = coorConverter-\u0026gt;map2Cam(cornerpoint[i]); } // 调用前进行类型转换 std::vectorcv::Point3d cvPoints; cvPoints.reserve(cornerpoint.size()); for (const auto\u0026amp; point : cornerpoint) { cvPoints.emplace_back(point.x(), point.y(), point.z()); } // 投影每块装甲板的四个角点 std::vectorcv::Point2d cornerPoints2D = projectPointsToImage(cvPoints, frame_, 0); // 绘制四个角点 for (const auto\u0026amp; point : cornerPoints2D) { cv::circle(frame_,point , 5, cv::Scalar(0, 0, 255), -1); // 红色角点 } // 绘制装甲板边框(连接四个角点) for (int j = 0; j \u0026lt; 4; j++) { int next_j = (j + 1) % 4; cv::line(frame_, cornerPoints2D[j], cornerPoints2D[next_j], cv::Scalar(255, 255, 0), 2); // 青色边框 } // 绘制对角线 cv::line(frame_, cornerPoints2D[0], cornerPoints2D[2], cv::Scalar(0, 255, 255), 2); // 黄色对角线1（左上到右下） cv::line(frame_, cornerPoints2D[1], cornerPoints2D[3], cv::Scalar(0, 255, 255), 2); // 黄色对角线2（右上到左下） } // 最后显示图像 cv::imshow(\u0026ldquo;Draw Points\u0026rdquo;, frame_); cv::waitKey(1); } /* * @brief 二维地图投影可视化 * @param pre_armors 预测的装甲板位置信息 * @param armors_position_pose 观测到的装甲板位置和姿态信息\n/ void projectMapPointsToImage (Armors pre_armors,std::vector\u0026lt;std::vectorEigen::Vector3d\u0026gt; armors_position_pose) { // // debug // cout \u0026laquo; \u0026ldquo;预测的第一块装甲板的x坐标: \u0026quot; \u0026laquo; pre_armors[0].position.x() \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;预测的第一块装甲板的y坐标: \u0026quot; \u0026laquo; pre_armors[0].position.y() \u0026laquo; endl; const double PIXELS_PER_METER = 100; // 比例尺 -\u0026gt; 100像素对应实际观测1米 // 创建白色背景图像 Mat whiteImage(800, 800, CV_8UC3, Scalar(255, 255, 255)); // 绘制基准线 for (int i = 1; i \u0026lt;= 8; i++) { if (i == 6) { cv::circle(whiteImage, cv::Point(400, 400), i * PIXELS_PER_METER, cv::Scalar(0, 0, 255), 1); continue; } cv::circle(whiteImage, cv::Point(400, 400), i * PIXELS_PER_METER, cv::Scalar(0, 0, 0), 1); } // 绘制估计装甲板位置 for (int i = 0; i \u0026lt; pre_armors.size(); i++) { // 绘制实心圆点，半径5像素，绿色 cv::circle(whiteImage, cv::Point(-pre_armors[i].position.y() PIXELS_PER_METER + 400, -pre_armors[i].position.x() PIXELS_PER_METER + 400), 3, cv::Scalar(0, 255, 0), -1); // break; // 测试(只打印第一块预测装甲板) } // 绘制观测装甲板位置 for (int i = 0; i \u0026lt; armors_position_pose.size(); i++) { // 绘制观测装甲板 double x = armors_position_pose[i][0].x(); double y = armors_position_pose[i][0].y(); cv::circle(whiteImage, cv::Point(-y PIXELS_PER_METER + 400, -x PIXELS_PER_METER + 400), 3, cv::Scalar(0, 0, 0), -1); double r[2] = {0.25,0.25}; // double yaw = armors_position_pose[0][1].z(); double yaw = armors_position_pose[i][1].z(); double center_x = armors_position_pose[i][0].x() - r[0] * cos(yaw); double center_y = armors_position_pose[i][0].y() - r[0] * sin(yaw); cv::circle(whiteImage, cv::Point(-center_y PIXELS_PER_METER + 400, -center_x PIXELS_PER_METER + 400), 3, cv::Scalar(255, 0, 0), -1); // whiteImage.at(armor_x 20 + 400, armor_y 20 + 400) = Vec3b(0, 255, 0); // 绘制实心圆点，半径5像素，黑色 // // 绘制重构出来的装甲板 蓝色 // // if (i \u0026gt; 1) { // // continue; // // } for (int j = 0; j \u0026lt; 4; j++) { double armor_x = center_x + r[j%2] * cos(yaw + 0.5 * j * PI); double armor_y = center_y + r[j%2] * sin(yaw + 0.5 * j * PI); cv::circle(whiteImage, cv::Point(-armor_y PIXELS_PER_METER + 400, -armor_x*PIXELS_PER_METER + 400), 3, cv::Scalar(255, 0, 0), -1); } } int x = static_cast(-pre_armors[(targetModel-\u0026gt;m_status-\u0026gt;index)].position.y()PIXELS_PER_METER + 400); int y = static_cast(-pre_armors[(targetModel-\u0026gt;m_status-\u0026gt;index)].position.x()PIXELS_PER_METER + 400); // cout \u0026laquo; \u0026ldquo;targetModel-\u0026gt;m_status-\u0026gt;index: \u0026quot; \u0026laquo; targetModel-\u0026gt;m_status-\u0026gt;index \u0026laquo; endl; // 选择距离中心点角度最小的点(这里和实际算法不一样,只是单纯编写一个算法框架) // 根据上一帧选择的重构装甲板index(编号)绘制击打线 // 绘制击打线(击打预测) cv::line(whiteImage, Point(400,400), cv::Point(x,y), cv::Scalar(255, 0, 0), 1, cv::LINE_AA); // 圈出打击目标 cv::circle(whiteImage, cv::Point(x,y), 5, cv::Scalar(0, 0, 255), 1); // 绘制自身车辆位置(地图中心) cv::circle(whiteImage, cv::Point(400, 400), 5, cv::Scalar(0, 0, 0), -1); imshow(\u0026ldquo;White Image with a Black Pixel\u0026rdquo;, whiteImage); waitKey(1); // 等待按键 } / * @brief 使用扩展卡尔曼滤波器更新目标状态 * @param detect 当前帧检测到的装甲板信息（rm_msgs::Armor指针） * - 非nullptr：表示有装甲板检测，执行完整流程 * - nullptr：表示无装甲板检测，执行纯预测\n/ void updateEKFState(rm_msgs::Armor detect) { /* 命名规则: DETECT{id}(基础装甲板) DETECT{id}0(另一块装甲板) */ // 主要通过依赖TF的异常处理来跳过不存在的装甲板 int armor_id = targetModel-\u0026gt;armor.id; // 获取当前装甲板的ID std::string base_armor_frame = \u0026ldquo;DETECT\u0026rdquo; + std::to_string(armor_id); ros::Time unified_time; bool base_transform_found = false; std::vector\u0026lt;std::vectorEigen::Vector3d\u0026gt; all_Armor_Position_Pose; // 存储所有装甲板的位置和姿态 std::vectorEigen::Vector3d base_position_pose; base_position_pose.emplace_back(Eigen::Vector3d::Zero()); // 第一个向量置零 base_position_pose.emplace_back(Eigen::Vector3d::Zero()); // 第二个向量置零 // 声明变量 geometry_msgs::TransformStamped base_transform; geometry_msgs::TransformStamped other_transform; // 在拿装甲板前延时(使得获取装甲板的坐标尽可能精确) // // 帧率控制 end_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); double frame_time = (end_Time - begin_Time)/1000.0; if ((end_Time - begin_Time) - 25.0 \u0026lt; 0.0)// 帧率控制 { int sleep_time_ms = static_cast(25.0 - (end_Time - begin_Time)); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time_ms)); //frame_time = 0.03; end_Time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); frame_time = (end_Time - begin_Time)/1000.0; // cout \u0026laquo; \u0026ldquo;sleep_time_ms:\u0026rdquo; \u0026laquo; sleep_time_ms \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;frame_time:\u0026rdquo; \u0026laquo; frame_time 1000 \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl; } begin_Time = end_Time; // debugdate1.data = frame_time 1000; // 查询第一块装甲板 try{ // 等待缓冲区更新 if (tfBuffer.canTransform(\u0026ldquo;map\u0026rdquo;, base_armor_frame, ros::Time(0), ros::Duration(1))) { base_transform = tfBuffer_.lookupTransform(\u0026ldquo;map\u0026rdquo;, base_armor_frame, ros::Time(0)); } targetModel-\u0026gt;update_count++; // // debug: 计算tf上传时间差 //////////////////////////////////////////////////////////// // tool_begin_Time_ros = base_transform.header.stamp; // ros::Duration test = tool_begin_Time_ros - tool_end_Time_ros; // debugdate.data = test.toSec() * 1000; // cout \u0026laquo; \u0026ldquo;duration_time\u0026rdquo; \u0026laquo; tool_begin_Time_ros - tool_end_Time_ros \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl; // tool_end_Time_ros = tool_begin_Time_ros; // cout \u0026laquo; \u0026ldquo;打印第一块装甲板///////////////////////\u0026rdquo; \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;打印位置x:\u0026rdquo; \u0026laquo; base_transform.transform.translation.x \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;打印位置y:\u0026rdquo; \u0026laquo; base_transform.transform.translation.y \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;打印位置z:\u0026rdquo; \u0026laquo; base_transform.transform.translation.z \u0026laquo; endl; //ROS_DEBUG(\u0026quot;[ArmorQuery] 成功获取基础装甲板 %s 的变换，统一时间戳: %f\u0026rdquo;, base_armor_frame.c_str(), unified_time.toSec()); ////////////////////////////////////////////////////////////// base_transform_found = true; unified_time = base_transform.header.stamp; // 初始化上一块装甲板的时间 if (targetModel-\u0026gt;update_count == 1) { last_tf_time = unified_time; } if (unified_time \u0026lt;= last_tf_time \u0026amp;\u0026amp; targetModel-\u0026gt;update_count != 1) { // ROS_WARN(\u0026ldquo;时间戳相等装甲板未更新\u0026rdquo;); base_transform_found = false; } last_tf_time = unified_time; // 提取装甲板的位置 Eigen::Vector3d base_position( base_transform.transform.translation.x, base_transform.transform.translation.y, base_transform.transform.translation.z ); // 提取装甲板的姿态 tf2::Quaternion qtn(base_transform.transform.rotation.x, base_transform.transform.rotation.y, base_transform.transform.rotation.z, base_transform.transform.rotation.w); tf2::Matrix3x3 matrix(qtn); double roll, pitch, yaw; matrix.getRPY(roll, pitch, yaw); Eigen::Vector3d base_pose; base_pose.x() = roll; base_pose.y() = pitch; base_pose.z() = yaw; base_pose.z() = targetModel-\u0026gt;last_yaw + angles::shortest_angular_distance(targetModel-\u0026gt;last_yaw, base_pose.z());//获得两帧之间的最小角度，更新当前yaw(避免π跳到-π) targetModel-\u0026gt;last_yaw = base_pose.z(); // rqt打印观测值 // debugdate.data = base_position[0]; // rqt打印第一块装甲板观测x // debugdate.data = base_position[1]; // rqt打印第一块装甲板观测y // debugdate.data = base_position[2]; // rqt打印第一块装甲板观测z // debugdate.data = base_pose[2] / 3.14 * 180; //rqt打印第一块装甲板观测yaw // 打印观测值 // cout \u0026laquo; \u0026ldquo;第一块装甲板观测值: \u0026quot; \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;机器人中心的x坐标: \u0026quot; \u0026laquo; base_position[0] \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;机器人中心的y坐标: \u0026quot; \u0026laquo; base_position[1] \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;装甲板的高度: \u0026quot; \u0026laquo; base_position[2]\u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;机器人整体偏航角yaw: \u0026quot; \u0026laquo; base_pose[2] \u0026laquo; endl; // // 计算协方差 (用于给ekf初值R) // double R = calculate_R(base_position[0]); // 计算x的协方差 -\u0026gt; pose // double R = calculate_R(base_position[1]); // 计算y的协方差 -\u0026gt; pose // double R = calculate_R(base_position[2]); // 计算z的协方差 -\u0026gt; pose // double R = calculate_R(base_pose[2]); // 计算yaw的协方差 -\u0026gt; yaw // debugdate.data = R; // cout \u0026laquo; \u0026ldquo;协方差: \u0026quot; \u0026laquo; R \u0026laquo; endl; // 将位置和姿态存储到数据结构中 base_position_pose[0] = base_position; // 位置 base_position_pose[1] = base_pose; // 姿态 /* 如果串口没有发数据,那么坐标系就不会更新就会进这段代码提前返回,已经debug测试过,没有发送串口数据后会进入这段代码 //坐标系异常检测 / if((base_position_pose[0].x() == 0 \u0026amp;\u0026amp; base_position_pose[0].y() == 0 \u0026amp;\u0026amp; base_position_pose[0].z() == 0) || isinf(base_position_pose[0].x()) || isinf(base_position_pose[0].y()) || isinf(base_position_pose[0].z()) || isnan(base_position_pose[0].x()) || isnan(base_position_pose[0].y()) || isnan(base_position_pose[0].z()) || isnan(base_position_pose[1].z()) || isinf(base_position_pose[1].z())){ cout \u0026laquo; \u0026ldquo;惯性系坐标系未更新！\u0026rdquo; \u0026laquo; endl; targetModel-\u0026gt;abnormal = true; return; } // 存储第一块装甲板的位置和姿态 all_Armor_Position_Pose.push_back(base_position_pose); } catch (tf2::TransformException \u0026amp;ex) { ROS_ERROR(\u0026quot;[ArmorQuery] 无法获取基础装甲板 %s 的变换: %s\u0026rdquo;, base_armor_frame.c_str(), ex.what()); // 设置目标状态为异常并返回 targetModel-\u0026gt;abnormal = true; ROS_WARN(\u0026quot;[ArmorQuery] 由于基础装甲板查询失败，跳过本帧处理\u0026rdquo;); return; } // 查询第二块装甲板 if (base_transform_found) { std::string other_armor_frame = \u0026ldquo;DETECT\u0026rdquo; + std::to_string(armor_id) + \u0026quot; \u0026quot; + std::to_string(0); try { // 等待缓冲区更新 other_transform = tfBuffer .lookupTransform(\u0026ldquo;map\u0026rdquo;, other_armor_frame, unified_time, ros::Duration(0)); // 提取装甲板位置 Eigen::Vector3d other_position( other_transform.transform.translation.x, other_transform.transform.translation.y, other_transform.transform.translation.z ); // 提取装甲板姿态 tf2::Quaternion qtn(other_transform.transform.rotation.x, other_transform.transform.rotation.y, other_transform.transform.rotation.z, other_transform.transform.rotation.w); tf2::Matrix3x3 matrix(qtn); double roll, pitch, yaw; matrix.getRPY(roll, pitch, yaw); Eigen::Vector3d other_pose; other_pose.x() = roll; other_pose.y() = pitch; other_pose.z() = yaw; other_pose.z() = targetModel-\u0026gt;last_yaw + angles::shortest_angular_distance(targetModel-\u0026gt;last_yaw, other_pose.z());//获得两帧之间的最小角度，更新当前yaw(避免π跳到-π) targetModel-\u0026gt;last_yaw = other_pose.z(); //debugdate1.data = other_position[0]; // rqt打印第二块装甲板观测x //debugdate1.data = other_position[1]; // rqt打印第二块装甲板观测y //debugdate1.data = other_position[2]; // rqt打印第二块装甲板观测z //debugdate1.data = other_pose[2]; // rqt打印第二块装甲板观测yaw // cout \u0026laquo; \u0026ldquo;第二块装甲板观测值: \u0026quot; \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;机器人中心的x坐标: \u0026quot; \u0026laquo; other_position[0] \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;机器人中心的y坐标: \u0026quot; \u0026laquo; other_position[1] \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;装甲板的高度: \u0026quot; \u0026laquo; other_position[2]\u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;机器人整体偏航角yaw: \u0026quot; \u0026laquo; other_pose[2] \u0026laquo; endl; // 将位置和姿态存储到数据结构中 std::vectorEigen::Vector3d other_position_pose; other_position_pose.emplace_back(Eigen::Vector3d::Zero()); // 第一个向量置零 other_position_pose.emplace_back(Eigen::Vector3d::Zero()); // 第二个向量置零 other_position_pose[0] = other_position; // 位置 other_position_pose[1] = other_pose; // 姿态 //坐标系异常检测 if((other_position_pose[0].x() == 0 \u0026amp;\u0026amp; other_position_pose[0].y() == 0 \u0026amp;\u0026amp; other_position_pose[0].z() == 0) || isinf(other_position_pose[0].x()) || isinf(other_position_pose[0].y()) || isinf(other_position_pose[0].z()) || isnan(other_position_pose[0].x()) || isnan(other_position_pose[0].y()) || isnan(other_position_pose[0].z()) || isnan(other_position_pose[1].z()) || isinf(other_position_pose[1].z())){//坐标系异常检测 cout \u0026laquo; \u0026ldquo;惯性系坐标系未更新！\u0026rdquo; \u0026laquo; endl; targetModel-\u0026gt;abnormal = true; return; } // 存储第二块装甲板的位置和姿态 all_Armor_Position_Pose.push_back(other_position_pose); } catch(tf2::TransformException \u0026amp;ex) { // ROS_INFO(\u0026ldquo;未查询到第二块装甲板yyyyyyyyyyyyy\u0026rdquo;); } } visual_armor_position_pose_temp = all_Armor_Position_Pose; // 用于可视化 // 有装甲板-\u0026gt;上传惯性系+预测+更新+更新目标对象 if (base_transform_found) { if (all_Armor_Position_Pose.empty()) { ROS_WARN(\u0026quot;[ArmorQuery] 未找到任何有效的装甲板数据\u0026rdquo;); targetModel-\u0026gt;abnormal = true; return; } else { ROS_DEBUG(\u0026quot;[ArmorQuery] 成功获取 %zu 块装甲板的数据\u0026rdquo;, all_Armor_Position_Pose.size()); targetModel-\u0026gt;abnormal = false; } // 创建装甲板实例 Armors armors(all_Armor_Position_Pose.size()); for (int i = 0; i \u0026lt; all_Armor_Position_Pose.size(); i++) { armors[i].position = Eigen::Vector3d(all_Armor_Position_Pose[i][0].x(),all_Armor_Position_Pose[i][0].y(),all_Armor_Position_Pose[i][0].z()); armors[i].rpy = Eigen::Vector3d(all_Armor_Position_Pose[i][1].x(),all_Armor_Position_Pose[i][1].y(),all_Armor_Position_Pose[i][1].z()); } // 先验状态预测 std::pair\u0026lt;Eigen::MatrixXd, Eigen::MatrixXd\u0026gt; prior = targetModel-\u0026gt;ekf-\u0026gt;predict(armors); // cout \u0026laquo; \u0026ldquo;输出状态矩阵:\u0026rdquo; \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;///////////////////////////////\u0026rdquo; \u0026laquo; endl; // cout \u0026laquo; prior.first \u0026laquo; endl; // cout \u0026laquo; \u0026ldquo;///////////////////////////////\u0026rdquo; \u0026laquo; endl; // 创建运动模型实例 std::shared_ptr prior_status; if (targetModel-\u0026gt;m_model_type == KinematicModel::STANDARD) { prior_status = std::make_shared(prior.first); } else if (targetModel-\u0026gt;m_model_type == KinematicModel::BALANCE) { prior_status = std::make_shared(prior.first); } else if (targetModel-\u0026gt;m_model_type == KinematicModel::OUTPOST) { prior_status = std::make_shared(prior.first); } // 数据关联匹配(计算检测到的装甲板与预测的装甲板之间的匹配度,在得到评分矩阵后为每个检测到的装甲板找到一个最合适的预测装甲板对应,形成一个最优匹配) Eigen::MatrixXd score = getScoreMat(armors,prior_status-\u0026gt;getArmors(0)); // cout \u0026laquo; \u0026ldquo;输出分数:\u0026rdquo; \u0026laquo; endl; // cout \u0026laquo; score \u0026laquo; endl; std::map\u0026lt;int, int\u0026gt; armor_match = getMatch(score, m_score_tolerance,targetModel-\u0026gt;count); / 正常的数据关联匹配结果： 1.一一对应关系: 每个检测装甲板索引对应唯一的目标装甲板索引 没有重复的匹配关系 2. 索引范围合理 3.得分低于阈值 所有匹配对的得分都低于m_score_tolerance 得分越低表示匹配度越高 1.绿色点(预测)和黑色点(预测)是否在空间上接近 2.匹配的装甲板在图像上是否对齐 / // debug // for (auto it : armor_match) // { // cout \u0026laquo; \u0026ldquo;装甲板序列: \u0026quot; \u0026laquo; it.first \u0026laquo; \u0026ldquo;目标装甲板索引: \u0026quot; \u0026laquo; it.second \u0026laquo; endl; // debugdate.data = it.first; // debugdate1.data = it.second; // } int current_index = targetModel-\u0026gt;m_status-\u0026gt;index; targetModel-\u0026gt;m_status = targetModel-\u0026gt;ekf-\u0026gt;update(armors, prior.first, prior.second,armor_match); targetModel-\u0026gt;m_status-\u0026gt;index = current_index; // debug (用于调试ekf) Eigen::VectorXd current_state = targetModel-\u0026gt;m_status-\u0026gt;getState(); // prior_status-\u0026gt;print(\u0026ldquo;预测的数据\u0026rdquo;); // debugdate1.data = current_state(0); // rqt预测x车中心 // debugdate1.data = targetModel-\u0026gt;m_status-\u0026gt;getArmors(0)[0].position.x(); // rqt打印预测第一块装甲板x坐标 // debugdate1.data = current_state(2); // rqt预测y车中心 // debugdate.data = current_state[4]; // rqt预测z左侧 // debugdate1.data = current_state[5]; // rqt预测z右侧 // debugdate1.data = current_state(8) / 3.14 * 180; // rqt预测yaw // debugdate.data = current_state(6); // rqt打印半径左侧 // debugdate1.data = current_state(7); // rqt打印半径右侧 // 可视化 projectCameraPointsToImage(targetModel-\u0026gt;m_status-\u0026gt;getArmors(0),all_Armor_Position_Pose); } // 无检测目标仅预测+更新目标对象 else { // 用于debug ROS_WARN(\u0026ldquo;无装甲板仅预测\u0026rdquo;); // 执行卡尔曼滤波器(传入空装甲板列表) Armors empty_armors; std::pair\u0026lt;Eigen::MatrixXd, Eigen::MatrixXd\u0026gt; prior = targetModel-\u0026gt;ekf-\u0026gt;predict(empty_armors); std::map\u0026lt;int, int\u0026gt; armor_match; armor_match.clear(); // 后验状态更新 targetModel-\u0026gt;m_status = targetModel-\u0026gt;ekf-\u0026gt;update(empty_armors, prior.first, prior.second,armor_match); // 可视化 Armors no_predict_armos = targetModel-\u0026gt;m_status-\u0026gt;getArmors(0); std::vector\u0026lt;std::vectorEigen::Vector3d\u0026gt; empty_Armor_Position_Pose; projectCameraPointsToImage(no_predict_armos,empty_Armor_Position_Pose); return; } } /** * 工具函数 * 用于实时计算R(协方差) * / // 用于计算R值 double max_num = INT_MIN; double min_num = INT_MAX; double calculate_R(double num) { if(Switch_Armor){ min_num = INT_MAX; max_num = INT_MIN; } // 计算R值的函数 if (num \u0026gt; max_num) { max_num = num; } if (num \u0026lt; min_num) { min_num = num; } double sqrt_R = (max_num - min_num) * 0.5; double R = sqrt_R * sqrt_R; cout \u0026laquo; \u0026ldquo;协方差: \u0026quot; \u0026laquo; R \u0026laquo; endl; return R; } /** * 工具函数 * 用于计算单帧之间的最大角度差值 / // 用于计算R值 double last_yaw_test = 0; double max_between_yaw = 0; int cnt = 0; double calculate_max_yaw(double yaw) { if(cnt == 0){ max_between_yaw = 0; last_yaw_test = yaw; } double yaw_temp = abs(yaw - last_yaw_test); if (yaw_temp \u0026gt; max_between_yaw) { max_between_yaw = yaw_temp; } last_yaw_test = yaw; cnt++; cout \u0026laquo; \u0026ldquo;两帧之间最大角度差: \u0026quot; \u0026laquo; max_between_yaw \u0026laquo; endl; return max_between_yaw; } /* * @brief 基于熵权法计算装甲板匹配得分矩阵 * @param detect_armors 检测到的装甲板集合 * @param standard_armors 预测的标准装甲板集合 * @return Eigen::MatrixXd 匹配得分矩阵(行:检测装甲板, 列:预测装甲板) / Eigen::MatrixXd getScoreMat(const Armors \u0026amp;detect_armors, const Armors \u0026amp;standard_armors) { int m = detect_armors.size(); int n = standard_armors.size(); // 计算两组装甲板之间的坐标差和角度差两个负向指标 Eigen::Matrix\u0026lt;double, Eigen::Dynamic, 2\u0026gt; negative_score; negative_score.resize(m * n, 2); for (int i = 0; i \u0026lt; m; i++) { for (int j = 0; j \u0026lt; n; j++) { Eigen::Vector3d d1(detect_armors[i].position.x(),detect_armors[i].position.y(),detect_armors[i].position.z()); Eigen::Vector3d d2(standard_armors[j].position.x(),standard_armors[j].position.y(),standard_armors[j].position.z()); negative_score(i * n + j, 0) = (d1 - d2).norm(); negative_score(i * n + j, 1) = abs(_std_radian(detect_armors[i].rpy.z() - standard_armors[j].rpy.z())); } } // 数据标准化 Eigen::Matrix\u0026lt;double, Eigen::Dynamic, 2\u0026gt; regular_score; regular_score.resize(m * n, 2); for (int i = 0; i \u0026lt; regular_score.rows(); i++) { regular_score(i, 0) = (negative_score.col(0).maxCoeff() - negative_score(i, 0)) / (negative_score.col(0).maxCoeff() - negative_score.col(0).minCoeff()); regular_score(i, 1) = (negative_score.col(1).maxCoeff() - negative_score(i, 1)) / (negative_score.col(1).maxCoeff() - negative_score.col(1).minCoeff()); } // 计算样本值占指标的比重 Eigen::Matrix\u0026lt;double, Eigen::Dynamic, 2\u0026gt; score_weight; score_weight.resize(m * n, 2); Eigen::VectorXd col_sum = regular_score.colwise().sum(); for (int i = 0; i \u0026lt; score_weight.rows(); i++) { score_weight(i, 0) = regular_score(i, 0) / col_sum(0); score_weight(i, 1) = regular_score(i, 1) / col_sum(1); } // 计算每项指标的熵值 Eigen::Vector2d entropy = Eigen::Vector2d::Zero(); for (int i = 0; i \u0026lt; score_weight.rows(); i++) { if (score_weight(i, 0) != 0) entropy(0) -= score_weight(i, 0) * log(score_weight(i, 0)); if (score_weight(i, 1) != 0) entropy(1) -= score_weight(i, 1) * log(score_weight(i, 1)); } entropy /= log(score_weight.rows()); // 计算权重 Eigen::Vector2d weight = (Eigen::Vector2d::Ones() - entropy) / (2 - entropy.sum()); // 计算匹配得分矩阵(综合评分) Eigen::Matrix\u0026lt;double, Eigen::Dynamic, Eigen::Dynamic\u0026gt; score; score.resize(m, n); for (int i = 0; i \u0026lt; m; i++) { for (int j = 0; j \u0026lt; n; j++) { if (i \u0026lt; detect_armors.size() \u0026amp;\u0026amp; j \u0026lt; standard_armors.size()) { score(i, j) = negative_score.row(i * standard_armors.size() + j) * weight; } } } return score; } /* * @brief 执行装甲板匹配的核心函数 * @param matrix 得分矩阵（rows:检测到的装甲板, cols:目标装甲板） * @param score_max 允许的最大匹配得分阈值 * @param m 目标装甲板数量 * @return std::map\u0026lt;int, int\u0026gt; 匹配结果（key:检测装甲板索引, value:目标装甲板索引） */ std::map\u0026lt;int, int\u0026gt; getMatch(Eigen::MatrixXd matrix, double score_max, int m) { //初始化最终结果 this-\u0026gt;row_col.clear(); for (int i = 0; i \u0026lt; matrix.rows(); i++) { this-\u0026gt;row_col[i] = -1; } //初始化min for (int i = 0; i \u0026lt; matrix.rows() \u0026amp;\u0026amp; i \u0026lt; m; i++) { this-\u0026gt;min += matrix(i, i);//初始化最小总得分 this-\u0026gt;row_col[i] = i; } //初始化行数列 this-\u0026gt;row.clear(); for (int i = 0; i \u0026lt; matrix.rows(); i++) { // 有多少行，行数列就有多少个数 this-\u0026gt;row.emplace_back(i); } /// 初始化列数列，n块装甲板，因此列为n this-\u0026gt;col.clear(); for (int i = 0; i \u0026lt; m; i++) { this-\u0026gt;col.emplace_back(i); } this-\u0026gt;tmp_v.clear(); this-\u0026gt;result.clear(); this-\u0026gt;nAfour.clear(); this-\u0026gt;fourAfour.clear(); //计算C(n,m) if (matrix.rows() \u0026lt;= m) this-\u0026gt;getCombinationsNumbers(this-\u0026gt;row, this-\u0026gt;tmp_v, this-\u0026gt;result, 0, matrix.rows()); else this-\u0026gt;getCombinationsNumbers(this-\u0026gt;row, this-\u0026gt;tmp_v, this-\u0026gt;result, 0, m); //使用上一步的结果计算A(m,m),最终得到A(n,m) for (auto it = this-\u0026gt;result.begin(); it != this-\u0026gt;result.end(); it++) { do{ this-\u0026gt;nAfour.emplace_back(*it); } while (next_permutation(it-\u0026gt;begin(), it-\u0026gt;end())); } //计算A(m,m) do{ fourAfour.push_back(col); } while (next_permutation(col.begin(), col.end())); // stl自带全排列函数\n//使用A(n,m)和A(m,m)求出可能选择的所有行列的组合的结果 for (auto it = this-\u0026gt;nAfour.begin(); it != this-\u0026gt;nAfour.end(); it++) { for (auto it2 = this-\u0026gt;fourAfour.begin(); it2 != this-\u0026gt;fourAfour.end(); it2++){ this-\u0026gt;tmp = 0; for (int i = 0; i \u0026lt; it-\u0026gt;size(); i++) this-\u0026gt;tmp += matrix((*it)[i], (*it2)[i]); if (this-\u0026gt;tmp \u0026lt; this-\u0026gt;min){ this-\u0026gt;min = this-\u0026gt;tmp; this-\u0026gt;row_col.clear(); for (int i = 0; i \u0026lt; it-\u0026gt;size(); i++){ this-\u0026gt;row_col[it-\u0026gt;at(i)] = it2-\u0026gt;at(i); } } } } //删除-1的键值对 for (auto it = this-\u0026gt;row_col.begin(); it != this-\u0026gt;row_col.end();){ if (it-\u0026gt;second == -1 || (it-\u0026gt;second != -1 \u0026amp;\u0026amp; matrix(it-\u0026gt;first, it-\u0026gt;second) \u0026gt; score_max)){ it = this-\u0026gt;row_col.erase(it); } else ++it; } return this-\u0026gt;row_col; } // 辅助函数：递归生成组合 void getCombinationsNumbers(const std::vector\u0026lt;int\u0026gt;\u0026amp; input, std::vector\u0026lt;int\u0026gt;\u0026amp; tmp_v, std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; result, int start, int k) { for (int i = start; i \u0026lt; input.size(); ++i) { tmp_v.emplace_back(input[i]); if (tmp_v.size() == k) { result.emplace_back(tmp_v); tmp_v.pop_back(); continue; } else { getCombinationsNumbers(input, tmp_v, result, i + 1, k); } tmp_v.pop_back(); } } /** * @brief 根据目标类型切换运动模型和观测器，并调整跟踪参数 * @param type 目标运动模型类型（STANDARD/BALANCE/OUTPOST） */ void setModelType(KinematicModel::Type type) { // 设置目标模型类型 targetModel-\u0026gt;m_model_type = type; / 2.运动模型与观测器初始化 / if (type == KinematicModel::STANDARD) { targetModel-\u0026gt;m_status = std::make_shared(); //创建标准模型 targetModel-\u0026gt;ekf = std::make_shared(config_path_); } else if (type == KinematicModel::BALANCE) { targetModel-\u0026gt;m_status = std::make_shared(); //创建平衡步兵模型 targetModel-\u0026gt;ekf = std::make_shared(config_path_); } else if (type == KinematicModel::OUTPOST) { targetModel-\u0026gt;m_status = std::make_shared(); //创建哨兵模型 targetModel-\u0026gt;ekf = std::make_shared(config_path_); } } }; 先大致看一下这段代码\n这段代码是一个自瞄系统中的跟踪器（Tracker）类。它使用ROS框架，从相机获取图像和装甲板检测信息，通过卡尔曼滤波等算法对目标进行跟踪和预测，并计算出云台需要转动的角度，最终通过ROS话题发布控制指令。 由于代码量很大，我将主要对代码的结构和功能进行分析，并指出可能存在的问题和改进建议。 代码结构： 类Tracker：模板类，模板参数是跟踪时间（TimeSpan）。 私有成员：包括ROS相关的发布订阅、图像处理相关变量、坐标变换工具、跟踪控制参数、时间戳管理、标志位等。 公有成员：构造函数、析构函数、回调函数（设置串口信息、图像、装甲板）、主跟踪循环、发布角度等。 主要功能： 通过ROS订阅串口信息、图像和装甲板检测结果。 对装甲板进行决策（选择跟踪目标）、预测（使用扩展卡尔曼滤波）、补偿（重力补偿、安装误差补偿）。 根据预测结果计算云台需要转动的角度，并发布控制指令。 可能存在的问题和改进建议： a. 代码可读性：\n代码中使用了大量的注释，但有些注释是中文，有些是英文，建议统一。 有些代码块较长，可以进一步拆分为更小的函数，提高可读性。 b. 性能： 在updateEKFState函数中，有一个帧率控制的延时（sleep_for）。这可能会影响实时性。建议考虑使用其他方式控制帧率，比如固定频率的循环。 在查询TF变换时，使用了固定的延时等待（ros::Duration(1)），这可能会在TF变换不可用时导致程序阻塞。建议使用更短的超时时间，并处理异常。 c. 异常处理： 在setParam函数中，加载配置文件时如果出现异常，会记录错误并返回，但程序可能继续运行，这可能导致后续使用未初始化的参数。建议在加载失败时采取更严格的措施，比如抛出异常或终止程序。 在TF查询时，如果查询失败，会设置targetModel-\u0026gt;abnormal = true，但后续的处理中，有些地方会直接返回，有些地方会继续执行。需要确保异常状态被正确处理。 d. 资源管理： 在构造函数中，使用new分配了Calculater和CoordinateTransformer，并在析构函数中删除。建议使用智能指针（如std::unique_ptr）来管理这些资源，避免内存泄漏。 同样，targetModel和m_MPC使用了智能指针，但cal和coorConverter没有。 e. 线程安全： 该类在ROS回调函数中设置成员变量（如RmSerialData、m_armors等），并在Track函数中读取。这些操作可能发生在不同的线程中（ROS默认情况下为每个订阅回调创建单独的线程）。因此，需要确保线程安全。可以考虑使用互斥锁（如std::mutex）保护这些共享数据。 f. 代码冗余： 在reconstruction_choose_compensation函数中，有两次计算m_target_pose的代码块（分别对应锁中心和不锁中心），它们非常相似，可以考虑提取为单独的函数。 g. 魔法数字： 代码中存在一些魔法数字，比如0.025、0.11等。建议将这些数字定义为常量，并加上注释说明。 h. 配置参数： 配置文件通过YAML加载，但代码中有些参数没有在配置文件中，比如BulletVector的初始值。建议将更多的参数放到配置文件中。 i. 可视化： 代码中有多处可视化（如projectCameraPointsToImage、projectMapPointsToImage），这些可视化在调试时很有用，但在实际部署时可能会影响性能。建议通过条件编译或运行时标志来控制是否启用可视化。 j. 跟踪逻辑： 在armorUpdate函数中，丢失时间的计算是基于Now_Time_armor和Track_Time_armor的差值。但是，Track_Time_armor只在检测到目标并更新时才会更新。如果长时间没有检测到目标，丢失时间会很大，从而触发重置。但是，对于前哨站（ID=6）的丢失时间阈值是普通目标的3倍。这个逻辑是合理的，但需要注意，如果目标短暂丢失又出现，可能会被当作新目标重新初始化，这可能导致跟踪不稳定。 k. 数据关联： 数据关联部分使用了熵权法计算匹配得分，并使用组合优化寻找最佳匹配。这部分代码比较复杂，且计算量可能较大。如果装甲板数量较多，可能会影响性能。可以考虑使用更简单的匹配方法，比如最近邻匹配，或者使用匈牙利算法。 l. 扩展卡尔曼滤波： 扩展卡尔曼滤波的实现封装在targetModel-\u0026gt;ekf中。需要确保其实现正确，并且噪声参数设置合理。 m. 重力补偿： 在getAngle函数中，重力补偿的计算假设了子弹速度恒定，且只考虑了垂直方向的重力。实际上，子弹速度可能会衰减，且空气阻力等因素也会影响弹道。如果需要更精确的补偿，可以考虑使用更复杂的弹道模型。 n. 安装误差补偿： 在getAngle函数中，直接加上了pitch_compensation和yaw_compensation。这些补偿量需要通过标定得到。 o. 前哨站模型： 前哨站（ID=6）的模型与其他模型不同，具有3块装甲板。在代码中，对于前哨站的处理有一些特殊之处，比如丢失时间阈值更大。需要确保模型和观测器与前哨站的运动特性匹配。 总结： 这段代码实现了一个完整的自瞄跟踪系统，涉及图像处理、目标跟踪、控制等多个方面。代码结构较为清晰，但仍有改进空间，特别是在线程安全、性能、异常处理等方面。建议进行进一步的测试和优化。\n由于代码量很大，无法逐行分析，以上是对整体结构和一些关键点的分析。如果需要更详细的修改，请指出具体部分。 这是一个完整的RoboMaster自瞄系统中的跟踪器（Tracker）类实现，让我分析一下它的主要结构和功能： 一、总体架构 这是一个基于ROS的自瞄系统跟踪器，主要功能包括： 目标跟踪与预测：使用扩展卡尔曼滤波（EKF）预测目标运动 云台控制：计算并发布云台转动角度 弹道补偿：考虑重力、延迟等因素进行弹道补偿 数据关联：匹配观测到的装甲板与预测的装甲板\n二、核心组件 1. 状态管理 支持多种目标模型： STANDARD：标准模型（英雄、步兵） OUTPOST：前哨站模型 BALANCE：平衡步兵模型（代码中提及但未完全实现）\n2. 主要功能模块 决策模块 (decision)：选择最佳跟踪目标 预测模块 (updateEKFState)：EKF预测与更新 补偿模块 (getAngle)：弹道重力补偿 控制模块 (reconstruction_choose_compensation)：云台控制决策\n3. 可视化工具 projectCameraPointsToImage：相机系投影 projectMapPointsToImage：地图系投影 draw_simulated_bullets：弹道模拟\n三、核心算法流程 text 接收数据（串口+图像+装甲板） 装甲板决策（选择目标） EKF预测 选板与补偿 发布控制指令\n四、关键技术点 1. 数据关联 使用熵权法计算匹配得分矩阵，解决观测装甲板与预测装甲板的对应关系。 2. 时间同步 TF变换获取精确时间戳 时间戳比对避免重复处理同一帧 电控延迟补偿\n3. 弹道补偿 考虑： 重力加速度 子弹飞行时间 电控延迟（COMMAND_TIMESPAN） 曝光时间（eTime）\n4. 控制模式 普通模式：直接角度计算 MPC模式：模型预测控制（可选） 锁中心模式：跟随旋转中心\n五、可优化点 1. 代码结构 cpp // 建议将大类拆分为多个小类：- ArmorTracker：装甲板跟踪\nBallisticSolver：弹道解算 Controller：云台控制 Visualizer：可视化 2. 性能优化 cpp // 当前问题：1. TF查询阻塞（ros::Duration(1)）过长 2. 组合数计算可能耗时（getCombinationsNumbers） 3. 频繁的内存分配/释放\n// 建议：1. 使用固定大小容器避免动态分配 2. 预计算组合数 3. 异步TF查询\n3. 线程安全 cpp // 当前：回调函数和Track()可能在多线程环境// 建议：添加互斥锁保护共享数据 std::mutex data_mutex_;\n4. 配置管理 cpp\n// 当前：YAML配置+硬编码混合// 建议：统一配置接口class ConfigManager {static TrackerConfig load(const std::string\u0026amp; path);}; 六、使用建议 1. 参数调优重点 m_score_tolerance：数据关联阈值 m_switch_threshold：装甲板切换阈值 COMMAND_TIMESPAN：电控延迟（关键！） 安装误差补偿（m_pitchOffset, m_yawOffset）\n2. 调试技巧 使用rqt_plot查看角度跟随 可视化投影检查坐标系转换 弹道模拟验证补偿效果\n3. 常见问题排查 TF异常：检查坐标系发布频率 预测发散：调整EKF的Q/R矩阵 延迟过大：优化时间戳管理\n这是一个相当完整的自瞄系统实现，涵盖了从感知到控制的完整链条。代码结构清晰，但复杂度较高，建议在实际使用中逐步调试各个模块。 #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t);\nEigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { double k = 1; // 空气阻力系数 // 计算水平位移 double w = (t - this-\u0026gt;fire_t) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector\u0026lt;IdCircle\u0026gt;{ // 初始化结果向量 std::vector\u0026lt;IdCircle\u0026gt; res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ++it; continue; } // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void { const long long fire_interval = 500; // 发射间隔：200毫秒 if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) { if (bullets.size() \u0026lt; 20) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++, // ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 0.025 + COMMAND_TIMESPAN) ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const std::vector\u0026lt;FlaskLine\u0026gt;\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026gt;\u0026gt;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return *this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param\u0026lt;double\u0026gt;(\u0026quot;auto-aim.debug.flask.map.pixel-per-meter\u0026quot;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param\u0026lt;double\u0026gt;(\u0026quot;auto-aim.debug.flask.map.pixel-per-meter\u0026quot;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param\u0026lt;double\u0026gt;(\u0026quot;auto-aim.ec-simulator.shoot-interval\u0026quot;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time); std::vector\u0026lt;IdCircle\u0026gt; bullets = aim_corrector.get_circles(now_time); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026lt;\u0026lt; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026lt;\u0026lt; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026lt;\u0026lt; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026gt;\u0026gt; img; } } #ifndef TRAJECTORY_VISUALIZER_HPP #define TRAJECTORY_VISUALIZER_HPP #include \u0026ldquo;math.hpp\u0026rdquo; #include \u0026ldquo;CoorConverter.hpp\u0026rdquo; #include \u0026lt;opencv2/opencv.hpp\u0026gt; #include \u0026ldquo;GimbalPos.hpp\u0026rdquo; namespace tools{ const int FLASK_MAP_WIDTH = 1000; // 定义调试地图的水平分辨率 const double FLASK_MAP_PETER_BY_BRIGHT = 1.; // 默认亮度系数 const int FLASK_MAP_MID_X = FLASK_MAP_WIDTH / 2; // 地图的水平中心点,用于坐标变换的参考原点 // 点绘制参数 struct FlaskPoint { FlaskPoint( const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ): pt(pt), color(color), radius(radius), thickness(thickness) {} cv::Point2d pt; // 圆心位置 cv::Scalar color; // 颜色 int radius; // 半径 int thickness; // 线宽 }; struct FlaskLine { FlaskLine( const std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt;\u0026amp; pt_pair, const cv::Scalar\u0026amp; color, const int\u0026amp; thickness ): pt_pair(pt_pair), color(color), thickness(thickness) {} std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt; pt_pair; cv::Scalar color; int thickness; }; // 文本绘制参数 struct FlaskText { FlaskText( const std::string\u0026amp; str, const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const double\u0026amp; scale ): str(str), pt(pt), color(color), scale(scale) {} std::string str; // 文本内容 cv::Point2d pt; // 文本位置 (左下角) cv::Scalar color; // 颜色 double scale; // 字体大小 }; /* 绘制流管理器 @brief: 收集绘制命令: 通过重载的\u0026laquo;操作符接收各种绘制元素 批量执行绘制: 通过\u0026raquo;操作符将所有收集的命令绘制到图形上 命令管理: 可以清空所有收集的绘制命令\n/ class FlaskStream { public: FlaskStream\u0026amp; operator\u0026laquo;(const char str); FlaskStream\u0026amp; operator\u0026laquo;(const std::string\u0026amp; str); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskPoint\u0026amp; pt); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskLine\u0026amp; line); FlaskStream\u0026amp; operator\u0026laquo;(const std::vector\u0026amp; lines); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskText\u0026amp; text); FlaskStream\u0026amp; operator\u0026raquo;(cv::Mat\u0026amp; img); void clear(); private: std::vectorstd::string logs; std::vector pts; std::vector lines; std::vector texts; }; // 用于复现的瞄准参数 // 移植代码的时候将这段代码移植到自瞄那里 struct ShootParam { double v0 = 0.; // 子弹初速度 double aim_angle = 0.; // 发射仰角 // Eigen::Vector3d aim_xyz_i_barrel = Eigen::Vector3d::Zero(); // 枪管坐标系瞄准点 (没有什么作用) Eigen::Vector3d target_xyz_i_camera = Eigen::Vector3d::Zero(); // 相机坐标系目标点 }; // 子弹命中位置信息 struct HitPos { bool hit; Eigen::Vector3d pos; // 子弹在世界坐标系上的位置 }; // 子弹图像投影信息 struct HitCircle { bool hit; math::CircleF circle; // 子弹在图像上的投影圆 }; // 匹配代价评估 struct CaughtCost { bool caught; // 是否满足匹配条件 double cost; // 匹配代价(越小越好) }; // 子弹弹道物理模拟器 class ProjectileSimulator { public: ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t) : coorConverter{coorConverter},shoot_param{shoot_param} ,fire_t{fire_t} {} // 子弹在图像平面上的投影计算 auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle; // 计算在指定时间t的子弹位置 auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos; // 获取开火时间 auto get_fire_t() const -\u0026gt; double; private: const double g { 9.8 }; const long long fire_t; CoordinateTransformer* coorConverter; ShootParam shoot_param; }; // 子弹位置信息 struct IdPos { int id; Eigen::Vector3d pos; }; // 子弹投影圆信息 struct IdCircle { int id; math::CircleF circle; // 子弹在图像平面上的投影圆 }; // 子弹模拟器封装 struct IdProj { int id; ProjectileSimulator proj; // 子弹物理模拟器实例 }; // 自动瞄准误差校准(目前仅用来复现理想弹道) class AimCorrector { public: AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param); // 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆 auto get_circles(long long now_time) -\u0026gt; std::vector; auto update_bullet(long long current_time) -\u0026gt; void; private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0;\nlong long last_fire_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); }; cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z); FlaskPoint pos_to_map_point( const Eigen::Vector3d\u0026amp; pos, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ); // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time); } #endif // TRAJECTORY_VISUALIZER_HPP 可以帮我看一下可能是那里导致弹道复现不对吗?\n我们首先需要分析弹道复现不对的可能原因。弹道复现主要依赖于两个部分：子弹弹道的物理模拟和坐标变换。 在ProjectileSimulator类中，get_pos_by_t函数根据时间计算子弹在枪管坐标系中的位置，然后通过坐标变换转换到相机坐标系，再转换到图像坐标系。 可能的问题点： 物理模型：子弹弹道的物理模型是否正确？目前使用的模型是考虑空气阻力的，但是空气阻力系数k=1是否合理？另外，公式是否正确？ 坐标变换：从枪管坐标系到相机坐标系的变换是否正确？在get_pos_by_t函数中，我们首先将目标点从相机坐标系转换到枪管坐标系，然后以此方向为基准，计算子弹在枪管坐标系中的偏移（水平位移w和高度h），再转换回相机坐标系。这里需要注意的是，我们假设弹道始终在由枪管指向目标点的垂直平面内，这个假设是合理的。 时间基准：开火时间fire_t是毫秒时间戳，而get_pos_by_t函数的参数t也是毫秒时间戳吗？从代码看，get_pos_by_t函数中计算时间差时，直接使用t - this-\u0026gt;fire_t，这里单位应该是毫秒，但公式中时间单位应该是秒。所以需要检查时间单位是否统一。 参数传递：在AimCorrector的update_bullet函数中，创建ProjectileSimulator时传入的fire_t是current_time，而实际上子弹发射时间应该考虑延迟（如曝光时间、电控延迟等）。在之前的代码中，我们看到自瞄代码中计算击中时间时加了m_time_off（包括COMMAND_TIMESPAN + eTime + 0.025）。但是在这个弹道复现中，我们可能只是为了可视化，所以直接用了当前时间。不过，如果是为了复现实际弹道，那么应该考虑这些延迟。 子弹速度：在ShootParam中，v0是子弹初速度，这个值是否准确？是否与自瞄代码中使用的子弹速度一致？ 仰角：在ShootParam中，aim_angle是发射仰角，这个角度应该是云台pitch角加上补偿角。在自瞄代码中，我们计算出的m_target_pose.pitch是云台需要转动的角度（已经包含重力补偿），那么这里传入的aim_angle应该是这个值吗？注意，在自瞄代码中，我们发布的角度是经过补偿的，但是云台实际转动的角度可能因为机械误差而有所不同。 目标点：在ShootParam中，target_xyz_i_camera是相机坐标系下的目标点。在自瞄代码中，我们传入的是coorConverter-\u0026gt;map2Cam(abs_target_armor.position)，这个转换是否正确？需要检查map2Cam函数。 图像投影：在get_circle_by_t函数中，我们计算子弹在图像上的投影圆。这里先通过map2Cam将世界坐标转换到相机坐标，然后通过cam2Map将相机坐标转换回世界坐标？这里似乎有问题。实际上，我们已经有子弹在相机坐标系下的坐标（由get_pos_by_t返回的bullet_xyz_i_camera），那么投影到图像上应该使用相机内参和畸变系数。但是在这段代码中，我们却使用了coorConverter-\u0026gt;map2pixel，这个函数可能是将世界坐标（地图坐标）投影到像素坐标。这可能是错误的。\n仔细看get_circle_by_t函数： cpp Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos);\n注意，这里的bullet.pos是get_pos_by_t返回的HitPos中的pos，这个pos是相机坐标系下的坐标吗？看get_pos_by_t函数，它返回的pos是bullet_xyz_i_camera，即相机坐标系下的坐标。那么，map2Cam函数是将世界坐标转换到相机坐标，但这里传入的是相机坐标，所以可能不对。 再看coorConverter-\u0026gt;map2Cam的实现，它应该是将世界坐标系（map）下的点转换到相机坐标系（cam）。所以，这里我们需要的是世界坐标系下的点，而不是相机坐标系下的点。 所以，问题可能出现在这里：在get_pos_by_t函数中，我们计算的是子弹在相机坐标系下的位置，但是get_circle_by_t函数却把它当作世界坐标系下的点，用map2Cam转换，这显然不对。 应该改为：在get_pos_by_t函数中，我们计算子弹在相机坐标系下的位置，然后直接使用相机内参和畸变系数投影到图像上。但是，在ProjectileSimulator类中，我们没有相机内参和畸变系数。所以，我们需要在ProjectileSimulator中保存相机内参和畸变系数，或者通过coorConverter提供相机投影功能。 但是，看CoordinateTransformer类，它可能已经封装了相机投影。我们查看coorConverter-\u0026gt;map2pixel函数，它可能是将世界坐标（map）投影到像素坐标。所以，如果我们有子弹在世界坐标系下的位置，那么就可以用map2pixel得到像素坐标。 那么，我们需要的是子弹在世界坐标系下的位置，而不是相机坐标系下的位置。所以，在get_pos_by_t函数中，我们应该计算子弹在世界坐标系下的位置。 因此，我们需要重新考虑弹道模拟的坐标系。 实际上，弹道模拟应该在世界坐标系（惯性系）中进行。我们已知： 发射时刻云台的位置和姿态（即相机坐标系到世界坐标系的变换） 子弹在相机坐标系（或枪管坐标系）中的运动轨迹，然后通过云台的外参转换到世界坐标系。\n所以，步骤应为： 在发射时刻，云台（相机）的外参已知（即世界坐标系到相机坐标系的变换矩阵）。 子弹在相机坐标系（或枪管坐标系）中的运动轨迹，通过外参转换到世界坐标系。 然后，将世界坐标系下的子弹位置投影到图像上。\n但是，在ProjectileSimulator中，我们只有coorConverter，它提供了坐标系之间的变换。我们来看get_pos_by_t函数： 它首先将目标点从相机坐标系转换到枪管坐标系（target_xyz_i_barrel），然后以枪管坐标系下的目标方向为基准，计算子弹在枪管坐标系中的位置（bullet_xyz_i_barrel），然后转换回相机坐标系（bullet_xyz_i_camera）。 但是，我们最终需要的是世界坐标系下的位置。所以，我们需要将bullet_xyz_i_camera转换到世界坐标系。这可以通过coorConverter-\u0026gt;cam2Map来实现。 所以，在get_pos_by_t函数中，我们应该返回的是世界坐标系下的位置。我们可以这样修改： cpp\nconst Eigen::Vector3d bullet_xyz_i_map = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet_xyz_i_camera);return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(), bullet_xyz_i_map }; 然后，在get_circle_by_t函数中，我们就可以使用map2Cam将世界坐标转换到相机坐标（实际上，这一步在投影中可能不需要，因为map2pixel可能已经包含了这个过程）。但是，我们注意到在get_circle_by_t函数中，我们使用map2Cam将世界坐标转换到相机坐标，然后为了计算半径，又使用cam2Map转换回世界坐标，这似乎绕路了。实际上，我们可以直接使用世界坐标计算像素坐标。 我们来看coorConverter-\u0026gt;map2pixel函数，它可能是将世界坐标直接投影到像素坐标。如果是这样，我们可以： cpp cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos);\n但是，计算半径时，我们需要在图像上找到一个边缘点。原来代码中，我们是在相机坐标系中，沿着与光心连线垂直的方向偏移一个物理半径（0.0085米）得到边缘点，然后投影到图像上。这个边缘点也需要在世界坐标系中表示，然后通过map2pixel得到像素坐标。 所以，我们需要将边缘点也从相机坐标系转换到世界坐标系。原来的代码是： cpp Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c);\n这里，xyz_c是相机坐标系下的点，edge_xyz_c也是相机坐标系下的点，然后通过cam2Map转换到世界坐标系。这样是可以的。 但是，现在bullet.pos已经是世界坐标系下的点，那么我们可以： cpp Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos); // 世界坐标-\u0026gt;相机坐标 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos);\n这样，我们得到了图像上子弹中心点和边缘点的像素坐标，然后计算距离作为半径。 所以，总结一下，我们需要修改get_pos_by_t函数，使其返回世界坐标系下的位置。然后，在get_circle_by_t函数中，使用这个世界坐标进行投影。 另外，我们还需要检查时间单位。在物理公式中，时间单位应该是秒，所以需要将毫秒转换为秒。 我们修改get_pos_by_t函数： cpp\nauto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {double k = 1; // 空气阻力系数double delta_t = (t - this-\u0026gt;fire_t) / 1000.0; // 将毫秒转换为秒// 计算水平位移double w = delta_t * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle);// 计算高度double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w/ (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))+ this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k/ k;// 弹道轨迹仅取决于目标点(理想弹道)const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 计算基准方向const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized();const Eigen::Vector3d h_norm = { 0., 0., 1. };const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm;const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);// 将子弹位置转换到世界坐标系const Eigen::Vector3d bullet_xyz_i_map = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet_xyz_i_camera);const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0),bullet_xyz_i_barrel(1, 0) };const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0),target_xyz_i_barrel(1, 0) };return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(), bullet_xyz_i_map};} 同时，在get_circle_by_t函数中，我们使用修改后的bullet.pos（现在是世界坐标）进行投影。 另外，我们还需要检查coorConverter-\u0026gt;map2pixel函数是否正确，它应该将世界坐标投影到像素坐标。 如果以上步骤都正确，那么弹道复现应该正确。 但是，如果还是不正确，我们可以通过以下步骤调试： 检查目标点在世界坐标系中的位置是否合理。 检查子弹在世界坐标系中的位置是否合理。 检查投影到图像上的位置是否合理。\n我们可以在get_circle_by_t函数中打印一些中间变量，比如bullet.pos（世界坐标），以及投影后的像素坐标。 此外，还需要注意：在AimCorrector的update_bullet函数中，我们创建ProjectileSimulator时传入的fire_t是current_time，而实际上，子弹发射时间可能不是当前帧的时间，而是未来某个时间（考虑延迟）。在自瞄代码中，我们计算击中时间时加了延迟，那么这里为了模拟实际弹道，也应该加上相同的延迟。所以，我们需要将自瞄代码中的延迟时间传递给AimCorrector。但是，在draw_simulated_bullets函数中，我们直接使用了当前时间，这可能是不准确的。 因此，我们需要修改draw_simulated_bullets函数，使其能够考虑延迟。我们可以将延迟时间作为参数传入，或者在ShootParam中加入延迟时间。 由于问题可能较多，我们首先修改坐标系的问题，确保物理模型和坐标变换正确，然后再调整时间延迟。 由于代码较长，我们只修改了关键部分。如果问题仍然存在，可能需要进一步调试。 由于我们无法直接运行代码，所以只能提供这些修改建议。请根据实际情况调整。 如果还有问题，请提供更多的错误信息，例如弹道投影在图像上的位置与预期偏差多大，等等。 看了你的弹道可视化代码，我发现了几个可能导致弹道复现不正确的问题： 主要问题分析 1. 弹道模型公式错误 在 get_pos_by_t 函数中，弹道模型公式看起来有问题： cpp\ndouble h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w/ (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))+ this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k/ k; 这个公式看起来很复杂且可能不正确。考虑空气阻力的弹道方程通常使用数值积分求解，而不是解析解。 2. 空气阻力系数 k=1 不合理 cpp double k = 1; // 空气阻力系数\n空气阻力系数通常远小于1（如 0.01-0.1 量级），k=1 意味着空气阻力非常大。 3. 坐标变换链不完整 在计算子弹位置时，坐标变换路径是： text\n相机系 -\u0026gt; 枪管系 -\u0026gt; 计算弹道 -\u0026gt; 枪管系 -\u0026gt; 相机系 但缺少从世界坐标系到相机坐标系的完整变换链。 4. 时间单位混淆 cpp const long long fire_interval = 500; // 发射间隔：200毫秒\n注释说是200ms，但实际值是500，存在混淆。 5. 子弹半径参数不准确 cpp Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085;\n17mm子弹的半径应该是0.0085m（8.5mm）吗？应该是直径17mm，半径8.5mm。 建议修正方案 1. 简化弹道模型（不考虑空气阻力） cpp auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {double delta_t = (t - this-\u0026gt;fire_t) / 1000.0; // 转换为秒// 简化的弹道方程（无空气阻力）double vx = this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle);double vy0 = this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle);double x = vx * delta_t; // 水平位移double y = vy0 * delta_t - 0.5 * this-\u0026gt;g * delta_t * delta_t; // 垂直位移// 从枪管系到相机系的变换// 需要确保shoot_param.target_xyz_i_camera是相机坐标系下的目标点const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 基准方向（目标在枪管系中的水平方向） Eigen::Vector3d target_dir_xy = target_xyz_i_barrel; target_dir_xy.z() = 0; // 只取水平分量if (target_dir_xy.norm() \u0026lt; 1e-6) { target_dir_xy = Eigen::Vector3d(1, 0, 0); // 默认方向} else { target_dir_xy.normalize();}// 子弹在枪管系中的位置 Eigen::Vector3d bullet_xyz_i_barrel = target_dir_xy * x + Eigen::Vector3d(0, 0, 1) * y;// 转换回相机系 Eigen::Vector3d bullet_xyz_i_camera = this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);// 判断是否击中：水平距离是否超过目标距离double bullet_horizontal_dist = bullet_xyz_i_barrel.head\u0026lt;2\u0026gt;().norm();double target_horizontal_dist = target_xyz_i_barrel.head\u0026lt;2\u0026gt;().norm();bool hit = (bullet_horizontal_dist \u0026gt;= target_horizontal_dist);return HitPos{hit, bullet_xyz_i_camera};}\n2. 修正坐标变换逻辑 确保你传入的 shoot_param.target_xyz_i_camera 是正确的： 在自瞄代码中应该是： cpp\nshoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(目标世界坐标); 3. 添加调试输出 cpp auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {// \u0026hellip; 计算过程 \u0026hellip;// 调试输出 std::cout \u0026laquo; \u0026ldquo;弹道计算 - 时间: \u0026quot; \u0026laquo; (t - fire_t)/1000.0 \u0026laquo; \u0026ldquo;s, 水平位移: \u0026quot; \u0026laquo; x \u0026laquo; \u0026ldquo;m, 垂直位移: \u0026quot; \u0026laquo; y \u0026laquo; \u0026ldquo;m, 目标水平距离: \u0026quot; \u0026laquo; target_horizontal_dist \u0026laquo; \u0026ldquo;m, 击中: \u0026quot; \u0026laquo; hit \u0026laquo; std::endl;return HitPos{hit, bullet_xyz_i_camera};}\n4. 修正时间管理 cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 固定发射间隔：200毫秒if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) { // 限制显示数量// 注意：这里应该使用实际的发射时间，考虑电控延迟long long actual_fire_time = current_time; // 可以加上延迟 bullets.push_back(IdProj{ next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, actual_fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n5. 检查坐标变换器 确保 CoordinateTransformer 正确实现了以下方法： map2Cam：世界坐标系 → 相机坐标系 cam2Gun：相机坐标系 → 枪管坐标系 gun2Cam：枪管坐标系 → 相机坐标系 cam2Map：相机坐标系 → 世界坐标系 map2pixel：世界坐标系 → 像素坐标系\n快速诊断步骤 打印关键坐标值： cpp // 在draw_simulated_bullets中添加 std::cout \u0026laquo; \u0026ldquo;目标点相机坐标: \u0026quot; \u0026laquo; shoot_param.target_xyz_i_camera.transpose() \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;子弹初速: \u0026quot; \u0026laquo; shoot_param.v0 \u0026laquo; \u0026ldquo;m/s\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;发射仰角: \u0026quot; \u0026laquo; shoot_param.aim_angle \u0026laquo; \u0026ldquo;rad\u0026rdquo; \u0026laquo; \u0026quot; (\u0026rdquo; \u0026laquo; shoot_param.aim_angle * 180/M_PI \u0026laquo; \u0026ldquo;度)\u0026rdquo; \u0026laquo; std::endl; 可视化检查： 弹道点应该在图像中形成一条平滑的抛物线 第一个点应该在枪口位置附近 最后一个点应该在目标装甲板位置 验证坐标变换： cpp // 测试坐标变换 Eigen::Vector3d test_world(3.0, 0.0, 0.0); // 正前方3米 Eigen::Vector3d test_cam = coorConverter-\u0026gt;map2Cam(test_world); Eigen::Vector3d test_back = coorConverter-\u0026gt;cam2Map(test_cam); std::cout \u0026laquo; \u0026ldquo;原始: \u0026quot; \u0026laquo; test_world.transpose() \u0026laquo; \u0026quot; -\u0026gt; 相机: \u0026quot; \u0026laquo; test_cam.transpose()\u0026laquo; \u0026quot; -\u0026gt; 返回: \u0026quot; \u0026laquo; test_back.transpose() \u0026laquo; std::endl;\n最可能的问题是弹道模型公式错误和坐标变换链不完整。建议先用简化的无空气阻力模型，确保基础正确后再考虑空气阻力。 // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ++it; continue; } // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } 现在已经确定了没有进入这个for循环,为什么没有进入这个for循环? 我们注意到，在 update_bullet 函数中，我们向 bullets 添加了新的子弹模拟器，但是在 get_circles 函数中，我们使用 now_time 和子弹的发射时间进行比较，如果 now_time 小于发射时间，则跳过。 问题可能出现在 update_bullet 函数中，我们传入的 fire_t 是 current_time，而 current_time 就是 now_time（在draw函数中传入的当前时间）。 因此，当我们添加子弹时，子弹的发射时间被设置为当前时间，那么在同一个 now_time 下，now_time 可能等于 fire_t，所以不会跳过。\n但是，注意在 get_circles 函数中，我们判断的是 now_time \u0026lt; it-\u0026gt;proj.get_fire_t()，如果相等，则不会跳过，会计算弹道。 所以，如果子弹被添加，那么在同一帧中，now_time 等于 fire_t，不会跳过，应该会进入计算。 但是，如果 update_bullet 没有被调用，或者添加子弹的条件不满足，那么 bullets 可能为空，就不会进入循环。 因此，我们需要检查： update_bullet 是否被正确调用，并且添加了子弹。 添加子弹的条件：current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval 是否满足，并且 bullets.size() \u0026lt; 20。\n另外，注意 update_bullet 函数中，我们使用 last_fire_time 记录上一次发射时间，初始值为当前时间（构造函数中初始化为当前时间）。所以第一帧时，current_time - last_fire_time 可能为0，小于200，所以不会添加子弹。 为了测试，我们可以将 fire_interval 设置为0，确保每次调用 update_bullet 都会添加子弹。 此外，还需要检查 draw_simulated_bullets 函数中，我们创建了 AimCorrector 对象，然后调用了 update_bullet 和 get_circles。但是注意，AimCorrector 对象是局部变量，每次调用 draw_simulated_bullets 都会重新创建，所以之前添加的子弹会被清空。 这可能是问题所在：每次绘制都是重新开始，子弹列表被清空，然后只添加一颗子弹（如果满足条件的话），并且由于时间间隔，可能只添加了一颗。 为了解决这个问题，我们需要将 AimCorrector 对象作为全局变量或静态变量，或者作为类成员，以便持续跟踪子弹状态。 在Tracker类中，我们可以将 AimCorrector 作为成员变量，并在每次更新时传入当前的射击参数和当前时间。 但是，在当前的代码中，draw_simulated_bullets 函数是每次独立创建的，所以子弹列表无法保持。 因此，我建议将 AimCorrector 作为Tracker的成员变量，然后在Track函数中更新子弹模拟器，并在绘制函数中直接使用这个成员变量来获取子弹圆圈。 修改步骤： 在Tracker类中添加成员变量：AimCorrector aim_corrector_;，并在构造函数中初始化（需要CoordinateTransformer和初始射击参数）。 在Track函数中，当计算出射击参数时，更新aim_corrector_的射击参数，并调用update_bullet。 在绘制弹道时，使用aim_corrector_.get_circles获取当前子弹圆圈。\n但是，注意：AimCorrector 的构造函数需要CoordinateTransformer和ShootParam，而ShootParam是每次计算都可能变化的。 所以，我们可以在Tracker的构造函数中初始化aim_corrector_，然后每次更新射击参数，并调用update_bullet。 然而，在Tracker类中，我们已经在reconstruction_choose_compensation函数中计算了射击参数，并调用了draw_simulated_bullets。我们可以将射击参数存储下来，然后更新到aim_corrector_中。 但是，由于弹道模拟是连续的，我们需要在每一帧都更新子弹状态（即使没有新子弹发射，也要更新已有子弹的位置）。 所以，我们可以这样调整： 在Tracker类中： 添加成员变量：ShootParam current_shoot_param_; 和 AimCorrector aim_corrector_; 在构造函数中，用初始参数初始化aim_corrector_（可能为空参数，等待更新） 在reconstruction_choose_compensation函数中，计算出的射击参数赋值给current_shoot_param_，然后调用aim_corrector_.update_bullet(当前时间)，并更新aim_corrector_的射击参数（因为AimCorrector类中需要更新射击参数以创建新的子弹模拟器） 在绘制函数中，使用aim_corrector_.get_circles(当前时间)获取子弹圆圈。\n但是，注意AimCorrector类中的update_bullet函数会创建新的子弹，而我们需要在每一帧都更新所有子弹的位置（在get_circles中已经根据当前时间计算位置）。 另外，AimCorrector类中需要更新射击参数，因为每次发射的子弹参数可能不同（比如目标点变化，仰角变化）。所以，我们可能需要在每次创建新子弹时更新射击参数。 因此，我们修改AimCorrector类，添加一个更新射击参数的函数，并在每次创建新子弹前更新。 但是，在当前的AimCorrector实现中，子弹的创建只依赖于固定的射击参数（构造函数传入的），所以如果我们不更新，那么新子弹会使用旧的射击参数。 所以，我们需要在AimCorrector类中添加一个成员函数来更新射击参数，然后在Tracker中每次计算完新的射击参数后，更新到AimCorrector中。 修改AimCorrector类： 添加一个成员变量ShootParam shoot_param_，并在构造函数中初始化。 添加一个成员函数void update_shoot_param(const ShootParam\u0026amp; shoot_param)，用于更新射击参数。 在update_bullet函数中，创建新子弹时使用当前的shoot_param_。\n然后，在Tracker类中，每次计算出新的射击参数，就调用aim_corrector_.update_shoot_param(current_shoot_param_)。 这样，每次发射的新子弹都会使用最新的射击参数。 但是，注意：在draw_simulated_bullets函数中，我们创建了一个新的AimCorrector对象，这是不对的。我们应该使用Tracker的成员变量aim_corrector_。 所以，我们需要将draw_simulated_bullets函数改为使用Tracker的成员变量aim_corrector_。 但是，draw_simulated_bullets函数是一个独立的函数，它需要访问Tracker的私有成员。我们可以将其改为Tracker的成员函数。 因此，我建议将draw_simulated_bullets函数移到Tracker类中，作为私有成员函数，并直接使用aim_corrector_。 总结修改步骤： 在Tracker类中添加私有成员： ShootParam current_shoot_param_; AimCorrector aim_corrector_; 在Tracker构造函数中初始化aim_corrector_，需要传入coorConverter和初始射击参数（可以默认为0）。 在reconstruction_choose_compensation函数中，计算出的射击参数赋值给current_shoot_param_，然后调用aim_corrector_.update_shoot_param(current_shoot_param_)，并调用aim_corrector_.update_bullet(当前时间)。 将draw_simulated_bullets函数改为Tracker的成员函数，并修改其实现，直接使用aim_corrector_获取子弹圆圈并绘制到图像上。 在Track函数中，调用draw_simulated_bullets时，传入当前图像和当前时间。\n由于修改较大，下面给出部分代码示例： 在Tracker.h中： // 在私有成员中添加 ShootParam current_shoot_param_; AimCorrector aim_corrector_; // 私有成员函数 void draw_simulated_bullets(cv::Mat\u0026amp; img, long long now_time); 在Tracker.cpp构造函数中： // 初始化aim_corrector_，需要coorConverter和初始射击参数 aim_corrector_ = AimCorrector(coorConverter, ShootParam{}); 在reconstruction_choose_compensation函数中，计算射击参数后： current_shoot_param_ = shoot_param; // shoot_param是计算出的当前帧的射击参数 aim_corrector_.update_shoot_param(current_shoot_param_); long long now_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); aim_corrector_.update_bullet(now_time);\n然后，在需要绘制弹道的地方，调用成员函数draw_simulated_bullets。 注意：由于弹道模拟是连续的，我们需要在每一帧都更新子弹状态，所以每一帧都要调用aim_corrector_.update_bullet(now_time)，这样子弹列表才会更新（包括新发射的子弹和旧子弹的移除）。 但是，注意update_bullet函数会检查发射间隔，所以只有满足间隔条件时才会添加新子弹。而旧子弹的移除是在get_circles中进行的（当子弹击中目标后会被移除）。 这样修改后，子弹列表会持续存在，直到击中目标。 但是，由于我们没有在每一帧都调用get_circles，所以击中判断可能不及时。因此，我们可以在update_bullet中也进行子弹的更新和击中判断，或者保证每一帧都调用get_circles。 在当前的修改中，我们每一帧都会调用draw_simulated_bullets，而在这个函数中，我们会调用get_circles，所以子弹的更新和移除是及时的。 所以，现在回到最初的问题：为什么没有进入for循环？ 在修改之后，子弹列表是持续存在的，并且每一帧都会更新，所以应该会进入for循环。 如果还是不能进入，请检查： 是否成功添加了子弹？即update_bullet函数中是否满足添加条件。 子弹的发射时间是否小于等于当前时间？注意我们传入的发射时间是当前时间，所以同一帧中，发射时间等于当前时间，不会跳过。 子弹列表是否为空？如果为空，则不会进入循环。\n为了调试，你可以在get_circles函数中添加日志，输出子弹列表的大小，以及每个子弹的发射时间。 另外，注意时间单位：now_time和fire_t都是毫秒时间戳，所以比较时是相同的单位。 希望这些修改能解决问题。如果还有问题，请继续提供详细信息。 根据代码分析，没有进入for循环的原因主要有以下几个可能： 1. bullets列表为空 这是最常见的原因。检查以下几个方面： 在update_bullet中添加日志： cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 500; // 发射间隔：200毫秒 std::cout \u0026laquo; \u0026ldquo;update_bullet called: current_time=\u0026rdquo; \u0026laquo; current_time \u0026laquo; \u0026ldquo;, last_fire_time=\u0026rdquo; \u0026laquo; this-\u0026gt;last_fire_time \u0026laquo; \u0026ldquo;, diff=\u0026rdquo; \u0026laquo; (current_time - this-\u0026gt;last_fire_time) \u0026laquo; \u0026ldquo;, bullets.size()=\u0026rdquo; \u0026laquo; bullets.size() \u0026laquo; std::endl;if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) { std::cout \u0026laquo; \u0026ldquo;条件满足，尝试添加子弹\u0026hellip;\u0026rdquo; \u0026laquo; std::endl;if (bullets.size() \u0026lt; 20) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)});this-\u0026gt;last_fire_time = current_time; std::cout \u0026laquo; \u0026ldquo;成功添加子弹，id=\u0026rdquo; \u0026laquo; (next_id-1) \u0026laquo; \u0026ldquo;, fire_t=\u0026rdquo; \u0026laquo; current_time \u0026laquo; std::endl;} else { std::cout \u0026laquo; \u0026ldquo;子弹数量已达上限20\u0026rdquo; \u0026laquo; std::endl;}} else { std::cout \u0026laquo; \u0026ldquo;发射间隔未满足\u0026rdquo; \u0026laquo; std::endl;}}\n2. 时间初始化问题 在AimCorrector构造函数中： cpp\nAimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) {this-\u0026gt;shoot_param = shoot_param;// 这里没有初始化 last_fire_time，使用的是成员变量默认初始化} last_fire_time默认初始化可能是一个很大的值，导致条件判断不正确。 修正构造函数： cpp AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) : coorConverter(coorConverter), shoot_param(shoot_param),last_fire_time(0), // 明确初始化为0next_id(0) {}\n3. 时间单位问题 检查传入的current_time和last_fire_time是否使用相同的时间单位。在你的代码中： cpp\n// 在Tracker中使用long long draw_visual_now_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count(); 但在AimCorrector中： cpp long long last_fire_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();\n两个时间单位应该一致（都是毫秒）。 4. 添加调试信息到get_circles cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::cout \u0026laquo; \u0026ldquo;get_circles called: now_time=\u0026rdquo; \u0026laquo; now_time \u0026laquo; \u0026ldquo;, bullets.size()=\u0026rdquo; \u0026laquo; bullets.size() \u0026laquo; std::endl; std::vector res;if (bullets.empty()) { std::cout \u0026laquo; \u0026ldquo;bullets列表为空，直接返回\u0026rdquo; \u0026laquo; std::endl;return res;}int i = 0;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { std::cout \u0026laquo; \u0026ldquo;处理子弹 \u0026quot; \u0026laquo; i \u0026laquo; \u0026ldquo;: id=\u0026rdquo; \u0026laquo; it-\u0026gt;id \u0026laquo; \u0026ldquo;, fire_t=\u0026rdquo; \u0026laquo; it-\u0026gt;proj.get_fire_t() \u0026laquo; \u0026ldquo;, now_time=\u0026rdquo; \u0026laquo; now_time \u0026laquo; std::endl;if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { std::cout \u0026laquo; \u0026ldquo;子弹还未发射，跳过\u0026rdquo; \u0026laquo; std::endl;++it; continue;}// \u0026hellip; 其余代码 \u0026hellip;++i;}return res;}\n5. 在draw_simulated_bullets中添加调试 cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time){ std::cout \u0026laquo; \u0026ldquo;=== draw_simulated_bullets开始 ===\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;传入时间: \u0026quot; \u0026laquo; now_time \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;子弹速度: \u0026quot; \u0026laquo; shoot_param.v0 \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;瞄准角度: \u0026quot; \u0026laquo; shoot_param.aim_angle \u0026laquo; std::endl; FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param);// 更新子弹序列 std::cout \u0026laquo; \u0026ldquo;调用update_bullet前\u0026hellip;\u0026rdquo; \u0026laquo; std::endl; aim_corrector.update_bullet(now_time); std::cout \u0026laquo; \u0026ldquo;调用update_bullet后\u0026hellip;\u0026rdquo; \u0026laquo; std::endl; std::vector bullets = aim_corrector.get_circles(now_time); std::cout \u0026laquo; \u0026ldquo;获取到的子弹数量: \u0026quot; \u0026laquo; bullets.size() \u0026laquo; std::endl;// \u0026hellip; 其余代码 \u0026hellip; std::cout \u0026laquo; \u0026ldquo;=== draw_simulated_bullets结束 ===\u0026rdquo; \u0026laquo; std::endl;}\n6. 可能的原因总结 原因A：条件判断不成立 cpp\n// 如果last_fire_time初始值很大，这个条件永远不会成立if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) 解决方案：在构造函数中初始化last_fire_time = 0 原因B：子弹发射时间比当前时间晚 cpp\n// 如果子弹的fire_t比now_time大，会跳过if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) 检查：在ProjectileSimulator构造函数中传入的fire_t是否正确 原因C：子弹立即被判定为击中 如果子弹一发射就被判定为击中，会立即从列表中删除，导致列表为空。 检查：在get_pos_by_t中，击中判定的逻辑是否过于宽松。 7. 快速诊断测试 添加这个测试函数来诊断问题： cpp void test_bullet_creation() { CoordinateTransformer* dummy_converter = nullptr; // 实际使用时需要真实对象 ShootParam param; param.v0 = 20.0; param.aim_angle = 0.1;long long now_time = 1000; // 测试时间 AimCorrector corrector(dummy_converter, param);// 第一次调用 corrector.update_bullet(now_time);// 等待一段时间后再次调用 std::this_thread::sleep_for(std::chrono::milliseconds(600));long long new_time = now_time + 600; corrector.update_bullet(new_time);// 检查子弹auto circles = corrector.get_circles(new_time); std::cout \u0026laquo; \u0026ldquo;最终子弹数量: \u0026quot; \u0026laquo; circles.size() \u0026laquo; std::endl;}\n最可能的问题是时间初始化不正确或发射间隔条件判断错误。添加上述调试输出后，应该能快速定位问题所在。 log file: /home/rm/.ros/log/cbda0f3a-dffc-11f0-8565-791c79470aa4/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [10756] 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 ^C子弹列表大小: 0 子弹列表大小: 0 [tracker-5] killing on exit [identify-4] killing on exit [serial-3] killing on exit [hikcamera-2] killing on exit 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 子弹列表大小: 0 [rosout-1] killing on e auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 cout \u0026laquo; \u0026ldquo;子弹列表大小: \u0026quot; \u0026laquo; bullets.size() \u0026laquo; endl; for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ++it; continue; } // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } 现在已经确定了这个子弹列表的大小是零 if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) {现在已经确定了是没有进入这个if语句,所以这段代码为什么会出错,我应该怎么实现?\n我们需要确定为什么条件不满足。从日志来看，子弹列表大小始终为0，说明没有成功添加子弹。而条件current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval不满足，可能的原因有： current_time和last_fire_time的初始值问题，导致时间差不够大。 fire_interval的值设置得太大，导致条件难以满足。 时间单位不一致，导致计算的时间差不是预期的毫秒数。\n查看代码中last_fire_time的初始化： 在AimCorrector类中，last_fire_time在构造函数中没有显式初始化，而是在类定义中初始化为当前时间（毫秒）： cpp long long last_fire_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();\n而在update_bullet函数中，我们传入的current_time也是通过同样的方式获取的。但是，由于last_fire_time在构造函数中被初始化为创建AimCorrector对象时的时间，而current_time是调用update_bullet时的时间。如果两次时间间隔小于fire_interval，条件就不满足。 另外，注意在draw_simulated_bullets函数中，我们创建了一个新的AimCorrector对象，然后立即调用update_bullet。这意味着last_fire_time被初始化为创建对象的时间，而传入的now_time是同一个函数被调用时的时间。由于这两者几乎是同时的，所以时间差几乎为0，小于fire_interval（200或500毫秒），因此条件不满足。 因此，我们需要调整逻辑，使得第一次调用update_bullet时就能添加一颗子弹。可以考虑将last_fire_time初始化为一个很早的时间，比如0，这样第一次调用时时间差就会很大，条件满足。 修改AimCorrector构造函数，将last_fire_time初始化为0： cpp AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) : coorConverter(coorConverter), shoot_param(shoot_param),last_fire_time(0), // 初始化为0，这样第一次调用update_bullet时，条件就会满足next_id(0) {}\n同时，在类定义中，移除last_fire_time的默认初始化，改为在构造函数初始化列表中初始化。 注意：在头文件中，last_fire_time的声明应该去掉默认初始化，即： cpp\nlong long last_fire_time; // 在构造函数中初始化 而不是： cpp long long last_fire_time = chrono::time_point_castchrono::milliseconds(chrono::system_clock::now()).time_since_epoch().count();\n这样修改后，第一次调用update_bullet时，current_time（当前时间）减去0会很大，满足条件，从而添加子弹。 另外，还需要注意fire_interval的值，你代码中注释写的是200毫秒，但实际是500。根据你的需求调整。 此外，还需要注意，在update_bullet中，我们添加子弹时传入的发射时间是current_time，而在get_circles中，我们检查子弹是否已发射的条件是now_time \u0026lt; it-\u0026gt;proj.get_fire_t()，这意味着如果当前时间小于子弹的发射时间，子弹就不会被绘制。由于我们传入的current_time是当前时间，所以子弹的发射时间就是当前时间，而get_circles被调用时传入的now_time可能比current_time稍晚，所以子弹应该会被绘制。但是，如果get_circles在update_bullet之前被调用，那么子弹就不会被绘制，因为子弹列表是空的。 在draw_simulated_bullets函数中，我们先调用update_bullet，再调用get_circles，所以顺序是正确的。 总结修改步骤： 修改AimCorrector构造函数，将last_fire_time初始化为0。 修改头文件中last_fire_time的声明，去掉默认初始化。 确保fire_interval的值符合预期（比如200毫秒）。\n修改后，应该就能正常添加子弹了。 如果仍然不行，请添加更多调试信息，比如在update_bullet中打印current_time、last_fire_time和它们的差值，以及fire_interval的值。 根据日志显示子弹列表大小始终为0，说明update_bullet函数中的条件判断没有进入。最可能的原因是时间比较逻辑有问题。让我分析并提供修复方案： 问题分析 1. 时间比较问题 cpp\nif (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) last_fire_time 可能在构造函数中被初始化为当前时间 每次调用 update_bullet 时，current_time 几乎等于 last_fire_time 导致时间差非常小，永远不会超过 fire_interval\n2. 调试发现 你在日志中打印了子弹列表大小，但没有打印时间相关的调试信息。添加以下调试信息： cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 200毫秒 std::cout \u0026laquo; \u0026ldquo;update_bullet调试信息:\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026quot; current_time: \u0026quot; \u0026laquo; current_time \u0026laquo; std::endl; std::cout \u0026laquo; \u0026quot; last_fire_time: \u0026quot; \u0026laquo; this-\u0026gt;last_fire_time \u0026laquo; std::endl; std::cout \u0026laquo; \u0026quot; 时间差: \u0026quot; \u0026laquo; (current_time - this-\u0026gt;last_fire_time) \u0026laquo; std::endl; std::cout \u0026laquo; \u0026quot; fire_interval: \u0026quot; \u0026laquo; fire_interval \u0026laquo; std::endl; std::cout \u0026laquo; \u0026quot; 条件是否满足: \u0026quot; \u0026laquo; ((current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) \u0026laquo; std::endl;if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) { std::cout \u0026laquo; \u0026quot; 进入if语句，添加子弹\u0026hellip;\u0026rdquo; \u0026laquo; std::endl;if (bullets.size() \u0026lt; 10) { // 减少到10颗，更容易观察 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)});this-\u0026gt;last_fire_time = current_time; std::cout \u0026laquo; \u0026quot; 添加子弹成功，id=\u0026rdquo; \u0026laquo; (next_id-1) \u0026laquo; \u0026ldquo;, 当前子弹数量: \u0026quot; \u0026laquo; bullets.size() \u0026laquo; std::endl;}} else { std::cout \u0026laquo; \u0026quot; 条件未满足，不添加子弹\u0026rdquo; \u0026laquo; std::endl;}}\n根本原因和解决方案 方案1：初始化last_fire_time为0 这是最简单的解决方案： cpp // 修改构造函数AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param) : coorConverter(coorConverter), shoot_param(shoot_param),last_fire_time(0), // 初始化为0，确保第一次就能进入ifnext_id(0) {}\n方案2：修改条件判断逻辑 使用 \u0026gt; 而不是 \u0026gt;=，并确保初始值合理： cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 200毫秒// 如果是第一次调用或者时间差足够大if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt; fire_interval) {if (bullets.size() \u0026lt; 10) { bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)});this-\u0026gt;last_fire_time = current_time;}}}\n方案3：强制第一次调用时添加子弹 cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 200毫秒// 如果还没有子弹，或者时间间隔满足，就添加新子弹bool should_add = bullets.empty() || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval;if (should_add \u0026amp;\u0026amp; bullets.size() \u0026lt; 10) { bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)});this-\u0026gt;last_fire_time = current_time;}}\n完整修复代码 cpp // trajectory_visualizer.hpp 中的类定义修改class AimCorrector {public:AimCorrector(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param);// \u0026hellip; 其他成员函数private: std::list bullets; CoordinateTransformer* coorConverter; ShootParam shoot_param;long long next_id = 0;long long last_fire_time = 0; // 初始化为0，不是当前时间}; cpp // trajectory_visualizer.cpp 中的实现修改AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param) : coorConverter(coorConverter), shoot_param(shoot_param),last_fire_time(0), // 明确初始化为0next_id(0) {}auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒// 调试信息 std::cout \u0026laquo; \u0026ldquo;update_bullet调试:\u0026rdquo; \u0026laquo; \u0026ldquo;当前时间=\u0026rdquo; \u0026laquo; current_time \u0026laquo; \u0026ldquo;, 上次发射时间=\u0026rdquo; \u0026laquo; this-\u0026gt;last_fire_time \u0026laquo; \u0026ldquo;, 时间差=\u0026rdquo; \u0026laquo; (current_time - this-\u0026gt;last_fire_time) \u0026laquo; \u0026ldquo;, 子弹数量=\u0026rdquo; \u0026laquo; bullets.size() \u0026laquo; std::endl;// 如果满足发射间隔条件if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) { // 限制最大子弹数量 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)});this-\u0026gt;last_fire_time = current_time; std::cout \u0026laquo; \u0026ldquo;添加子弹成功，id=\u0026rdquo; \u0026laquo; (next_id-1) \u0026laquo; std::endl;}}}\n额外的调试建议 在 draw_simulated_bullets 函数中添加更多调试信息： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img, long long now_time) { std::cout \u0026laquo; \u0026ldquo;\\n=== draw_simulated_bullets 开始 ===\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;当前帧时间: \u0026quot; \u0026laquo; now_time \u0026laquo; std::endl; FlaskStream flask_aim; flask_aim.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter, shoot_param);// 第一次更新 aim_corrector.update_bullet(now_time);// 等待一小段时间模拟long long later_time = now_time + 250; // 250毫秒后 std::cout \u0026laquo; \u0026ldquo;\\n250毫秒后\u0026hellip;\u0026rdquo; \u0026laquo; std::endl; aim_corrector.update_bullet(later_time);// 获取子弹 std::vector bullets = aim_corrector.get_circles(later_time); std::cout \u0026laquo; \u0026ldquo;最终子弹数量: \u0026quot; \u0026laquo; bullets.size() \u0026laquo; std::endl;// \u0026hellip; 绘制代码 \u0026hellip; std::cout \u0026laquo; \u0026ldquo;=== draw_simulated_bullets 结束 ===\\n\u0026rdquo; \u0026laquo; std::endl;}\n最关键的是将 last_fire_time 初始化为 0 而不是当前时间，这样第一次调用时时间差会非常大，条件判断就会成立。 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111111111111111111111111111111\u0026rdquo;); ++it; continue; } 上面的问题解决了,这次我发现程序没有进入这个if语句,为什么?\n我们进入get_circles函数时，子弹列表bullets不为空，但是却没有进入if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t())这个条件，这说明对于列表中的每个子弹，当前时间now_time都大于等于子弹的发射时间fire_t。 可能的原因： 子弹的发射时间fire_t设置不正确，可能设置成了过去的时间，导致now_time总是大于fire_t。 时间单位问题，now_time和fire_t的单位不一致。\n由于我们之前在update_bullet中创建子弹时，将fire_t设置为current_time，而current_time就是调用update_bullet时的当前时间。在get_circles中传入的now_time是当前帧的时间。在正常的程序流程中，我们期望在某一帧创建子弹，然后在后续帧中更新子弹的位置。 但是，如果创建子弹的帧和更新子弹的帧是同一帧，那么now_time可能等于fire_t，这样就不会进入这个if语句。 另外，如果我们在创建子弹后，在下一帧调用get_circles，那么now_time应该大于fire_t，也不会进入这个if语句。 所以，这个if语句只有在子弹的发射时间晚于当前时间（即子弹还没有发射）的情况下才会进入。在我们当前的逻辑中，子弹一旦被创建，就认为是已经发射了（因为fire_t被设置为当前时间），所以不会进入这个if语句。 但是，我们之前为了模拟子弹发射间隔，在update_bullet中设置了发射间隔，并且只有在满足间隔条件时才创建子弹。那么，当我们创建子弹时，我们设置的fire_t就是当前时间，而now_time也是当前时间，所以now_time等于fire_t，不满足now_time \u0026lt; fire_t，因此不会进入这个if语句。 那么，这个if语句的设计意图是什么？看起来是想跳过那些尚未发射的子弹。但是，在我们的设计中，子弹一旦被创建，就认为是已经发射了。所以，这个if语句可能永远不会进入，除非我们在创建子弹时，将fire_t设置为未来的某个时间（比如考虑击发延迟）。 实际上，在真实的射击过程中，从按下射击键到子弹出膛有一个击发延迟，我们可能需要在fire_t上加上这个延迟，这样子弹在出膛前的一段时间内不会被绘制。 但是，根据你的代码，ProjectileSimulator的构造函数中传入的fire_t就是current_time，即子弹的发射时间就是创建模拟器的时间。那么，在绘制第一帧时，子弹已经发射，所以不会进入这个if语句。 所以，问题可能在于：我们是否需要模拟击发延迟？如果需要，那么应该将fire_t设置为未来某个时间（比如current_time + 延迟时间）。这样，在子弹出膛前，now_time \u0026lt; fire_t，就会进入if语句，跳过这颗子弹。 然而，在你的代码中，我注意到在Tracker的reconstruction_choose_compensation函数中，你计算了m_time_off = COMMAND_TIMESPAN + eTime + 0.025，并用于计算击中时间。但是，在创建子弹模拟器时，你并没有考虑这个延迟。 在Tracker中，你创建ProjectileSimulator时使用的是当前时间： cpp ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)\n但是，在真实的射击过程中，子弹的出膛时间应该是当前时间加上击发延迟。所以，你可能需要将fire_t设置为current_time + m_time_off。 然而，在draw_simulated_bullets函数中，你创建AimCorrector并调用update_bullet时，并没有传入这个延迟。所以，你需要修改AimCorrector的设计，使其能够考虑击发延迟。 但是，请注意，AimCorrector类目前只有一个ShootParam参数，没有延迟参数。你可能需要将延迟参数作为ShootParam的一部分，或者在AimCorrector的构造函数中增加一个延迟参数。 另一种思路是：在Tracker中，你已经计算了击中时间，那么你可以将子弹的发射时间设置为当前时间，然后子弹的飞行时间就是击中时间减去当前时间。但是，在子弹模拟器中，我们是从发射开始模拟的，所以发射时间就是当前时间加上延迟。 由于你的代码中已经有一个固定的延迟COMMAND_TIMESPAN + eTime + 0.025，我建议在创建子弹模拟器时，将这个延迟加上。 修改AimCorrector的update_bullet函数，将子弹的发射时间设置为当前时间加上延迟。但是，这个延迟需要从外部传入。 由于AimCorrector是在draw_simulated_bullets函数中创建的，而draw_simulated_bullets是在Tracker的reconstruction_choose_compensation函数中调用的，并且Tracker中已经计算了m_time_off，所以你可以将m_time_off作为参数传递给draw_simulated_bullets，然后再传递给AimCorrector。 但是，这可能需要修改多个函数。另一种简单的方法是：在ShootParam结构体中增加一个fire_delay字段，然后在创建子弹模拟器时使用。 由于时间关系，我建议先修改ShootParam结构体，增加延迟字段，然后在创建子弹模拟器时加上这个延迟。 步骤： 在ShootParam结构体中增加一个成员变量，比如fire_delay，表示击发延迟（单位：毫秒）。 在Tracker中设置shoot_param时，将m_time_off赋值给fire_delay（注意单位转换，m_time_off是秒，而fire_delay可能是毫秒）。 在AimCorrector的update_bullet函数中，创建子弹模拟器时，将fire_t设置为current_time + shoot_param.fire_delay。\n这样，子弹的发射时间就是当前时间加上延迟，那么在第一帧时，如果延迟大于0，那么now_time可能小于fire_t，就会进入if语句，跳过这颗子弹，直到时间超过延迟，子弹才真正发射。 但是，请注意，你的get_circles函数中，对于每一颗子弹，如果now_time \u0026lt; fire_t，就跳过，这意味着在延迟期间，这颗子弹不会被绘制。这符合预期吗？ 另外，你的get_pos_by_t函数中，计算子弹位置时使用的t是传入的时间，它应该大于等于fire_t。如果t \u0026lt; fire_t，那么子弹还没有发射，应该返回什么位置？可能是枪口位置？但是，根据你的代码，get_pos_by_t函数没有处理t \u0026lt; fire_t的情况，所以可能返回错误的位置。 因此，我建议在get_pos_by_t函数中，如果t \u0026lt; fire_t，则返回枪口位置（即初始位置）。但是，你的代码中，子弹的初始位置是哪里？在get_pos_by_t函数中，当t等于fire_t时，子弹应该刚好在枪口。 由于你的弹道模型是从枪口开始计算的，所以当t \u0026lt; fire_t时，子弹应该还在枪膛内，我们可能不想绘制它。所以，在get_circles函数中跳过t \u0026lt; fire_t的子弹是正确的。 但是，为了确保get_pos_by_t函数在t \u0026lt; fire_t时不会出现异常（比如对数函数中真数为负数），我们可以在get_pos_by_t函数中判断，如果t \u0026lt; fire_t，则返回一个特殊的HitPos，其中hit为false，位置为枪口位置（或者零位置）。不过，由于我们在get_circles中已经跳过了，所以可以假设get_pos_by_t不会被用于t \u0026lt; fire_t的情况。 综上所述，修改方案： 在ShootParam中增加fire_delay（单位：毫秒）。 在Tracker中，设置shoot_param.fire_delay = (COMMAND_TIMESPAN + eTime + 0.025) * 1000; // 转换为毫秒 在AimCorrector的update_bullet中，创建子弹时，将发射时间设置为当前时间加上延迟： cpp bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + this-\u0026gt;shoot_param.fire_delay)});\n这样，在子弹延迟期间，now_time \u0026lt; fire_t，就会进入if语句，跳过这颗子弹。 但是，请注意，你的get_circles函数中，now_time是调用时传入的当前时间，而子弹的fire_t是发射时间。这样，当now_time小于fire_t时，子弹不会显示，直到now_time大于等于fire_t。 另外，你的ProjectileSimulator的get_pos_by_t函数中，计算弹道时使用的t是绝对时间（毫秒），而fire_t也是绝对时间（毫秒）。所以，当t大于等于fire_t时，计算的是子弹发射后的位置。 这样修改后，你应该能看到子弹在延迟一段时间后才出现。 但是，请注意，你的AimCorrector的update_bullet函数中，每次满足发射间隔时就会创建一颗子弹，而每颗子弹都有相同的延迟。这样，第一颗子弹会在current_time + fire_delay时发射，第二颗子弹会在current_time + fire_interval + fire_delay时发射，以此类推。 现在，让我们回到最初的问题：为什么没有进入if语句？因为之前我们没有加延迟，所以fire_t等于current_time，而now_time是当前帧的时间，可能等于或大于fire_t。现在加上延迟后，fire_t大于current_time，而now_time可能小于fire_t，这样就会进入if语句。 但是，请注意，在draw_simulated_bullets函数中，你创建AimCorrector后立即调用update_bullet，然后调用get_circles，传入的now_time是同一个时间。所以，如果now_time小于fire_t，那么子弹不会被绘制。 为了测试，你可以将fire_delay设置为一个较小的值，比如50毫秒，然后观察子弹是否在50毫秒后出现。 由于你的代码中，draw_simulated_bullets每帧都会被调用，所以你可以看到子弹在一段时间后出现。 但是，请注意，你的update_bullet函数中，发射间隔是500毫秒，所以每500毫秒才会创建一颗子弹。而第一颗子弹的延迟是fire_delay，所以第一颗子弹会在程序开始后的fire_delay毫秒后出现，然后每500毫秒出现一颗。 另外，你需要确保fire_delay小于500毫秒，否则第二颗子弹可能会在第一颗子弹之前出现（因为第二颗子弹的发射时间是current_time + fire_interval + fire_delay，而第一颗是current_time + fire_delay）。 最后，由于你是在Tracker的每一帧中调用draw_simulated_bullets，所以每帧都会创建新的AimCorrector对象，这样之前的状态（子弹列表）就会丢失。你可能希望AimCorrector对象能够持续存在，而不是每帧都新建。这可能需要你将AimCorrector作为Tracker的成员变量，而不是在函数内部创建。 但是，根据你的需求，如果你只是想在每一帧中绘制当前帧的子弹弹道，那么每帧新建也是可以的，只不过这样子弹列表会被重置，你只能看到当前帧发射的子弹（而且由于发射间隔，可能很多帧都不会有子弹）。 所以，为了保持子弹的状态，你应该将AimCorrector作为Tracker的成员变量，并在Tracker的构造函数中初始化它，然后在每一帧中更新和绘制。 由于这是一个较大的改动，你需要根据你的项目需求来决定。 如果你只是想调试弹道，那么每帧新建可能问题不大，但是要注意，这样你只能看到当前帧发射的子弹，而且由于发射间隔，你可能需要等待一段时间才能看到子弹。 为了快速看到效果，你可以暂时将发射间隔fire_interval设置得小一些，比如100毫秒。 总结一下，要解决进入if语句的问题，你需要： 在ShootParam中增加fire_delay。 在Tracker中设置shoot_param.fire_delay为击发延迟（毫秒）。 在创建子弹模拟器时，将发射时间设置为当前时间加上延迟。\n这样，在延迟时间内，now_time会小于fire_t，就会进入if语句跳过。 但是，请注意，如果你的目的是为了模拟真实的子弹发射（包括击发延迟），那么这样做是正确的。如果你的目的只是为了可视化弹道，不考虑击发延迟，那么你可以不加延迟，而是直接将发射时间设置为当前时间，这样就不会进入if语句，子弹会立即被绘制。 所以，是否进入if语句取决于你的需求。如果你希望子弹立即被绘制，那么就不要加延迟，这样now_time等于fire_t，不会进入if语句。如果你希望模拟击发延迟，那么加上延迟，这样在延迟期间，now_time小于fire_t，就会进入if语句跳过。 根据你的问题，你发现没有进入if语句，那么说明你没有加延迟，而且now_time大于等于fire_t。如果你希望进入if语句，那么就需要加上延迟。 但是，请记住，get_circles函数中，对于每一颗子弹，如果now_time \u0026lt; fire_t，就会跳过，这颗子弹就不会被绘制。所以，如果你加了延迟，那么在延迟期间，子弹是不可见的。 所以，请根据你的需求决定。 由于你之前的问题已经解决（子弹列表不为空），现在的问题是没有进入if语句，那么你可以选择是否要加延迟。如果你不需要延迟，那么你可以删除这个if语句，或者将条件改为now_time \u0026lt;= fire_t，这样在发射时间等于当前时间时也会跳过（但实际上，子弹在发射瞬间就应该被绘制，所以可能不应该跳过）。 我认为，在子弹发射的瞬间，我们应该绘制子弹，所以当now_time == fire_t时，不应该跳过。所以，你可以将条件改为now_time \u0026lt; fire_t，这样在发射时间等于当前时间时，就会绘制子弹。 所以，如果你不想加延迟，而且希望子弹在创建后立即被绘制，那么你应该确保fire_t小于等于now_time。在创建子弹时，将fire_t设置为当前时间，那么now_time等于fire_t，不会进入if语句，子弹会被绘制。 因此，你的代码中，如果没有加延迟，那么fire_t等于current_time，而now_time是同一帧的时间，所以相等，不会进入if语句。 所以，现在你可以选择： 不加延迟，并且希望子弹立即绘制：保持原样，不用进入if语句。 加延迟，模拟击发延迟：按照上述修改，这样在延迟期间，子弹不会绘制，直到延迟结束。\n根据你的需求，如果你想模拟击发延迟，那么请按照上述步骤修改。如果不想，那么你可以删除这个if语句，或者将其条件改为now_time \u0026lt;= fire_t，这样在发射时间等于当前时间时也会跳过（但这样发射瞬间的子弹就不会被绘制）。 但是，我认为在发射瞬间，子弹应该被绘制在枪口位置。所以，我建议不要跳过now_time == fire_t的情况。 所以，你可以将条件改为now_time \u0026lt; fire_t，这样在等于的时候就会绘制。 这样，即使你不加延迟，子弹也会在创建的第一帧被绘制。 所以，现在的问题是你希望子弹在什么时候被绘制？如果你希望子弹在创建的第一帧就被绘制，那么你应该确保fire_t小于等于now_time。由于你创建子弹时使用的是current_time，而now_time是同一帧的时间，所以相等，不会进入if语句，会被绘制。 因此，你的代码目前的行为是：子弹在创建的第一帧就会被绘制。如果你希望这样，那么没有问题。 如果你希望子弹在创建后延迟一段时间再绘制，那么就需要加延迟。 根据你的问题描述，你发现没有进入if语句，这是因为你的fire_t设置成了当前时间，而now_time等于当前时间。所以，如果你希望进入if语句，就需要让fire_t大于now_time，即设置一个未来的发射时间。 所以，请根据你的需求决定是否修改。 最后，请注意，你的get_pos_by_t函数在t等于fire_t时，计算出的子弹位置应该是枪口位置。所以，在第一帧绘制时，子弹在枪口。 希望这能帮助你理解问题所在。 这个问题说明当前时间 now_time 已经大于等于子弹的发射时间 fire_t，也就是说子弹已经发射了。让我们分析一下为什么会出现这种情况： 原因分析 1. 时间创建顺序 在你的代码中，子弹的创建和获取发生在同一帧： cpp // 在draw_simulated_bullets函数中： aim_corrector.update_bullet(now_time); // 创建子弹，fire_t = now_time std::vector bullets = aim_corrector.get_circles(now_time); // 同一时间查询\n这意味着： update_bullet(now_time) 创建子弹，fire_t = now_time get_circles(now_time) 查询子弹，now_time == fire_t 所以 now_time \u0026lt; fire_t 为 false\n2. 时间精度问题 即使你希望子弹稍后发射，但如果你使用的是毫秒级时间戳，在同一函数调用中，now_time 几乎不可能小于 fire_t。 解决方案 方案1：添加发射延迟 这是最合理的解决方案。子弹应该有发射延迟（考虑击发时间）： cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒const long long fire_delay = 50; // 发射延迟：50毫秒if (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) {// 添加发射延迟：子弹会在50毫秒后才真正发射long long actual_fire_time = current_time + fire_delay; bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, actual_fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n方案2：在Tracker中添加延迟 在Tracker的调用中添加延迟更符合实际情况： cpp // 在Tracker的reconstruction_choose_compensation函数中： tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_target_armor.position);// 添加发射延迟（电控延迟+击发延迟）double m_time_off = COMMAND_TIMESPAN + eTime + 0.025; // 你之前计算的总延迟long long draw_visual_now_time = chrono::time_point_castchrono::milliseconds( chrono::system_clock::now()).time_since_epoch().count();// 子弹将在未来发射，所以使用未来的时间long long fire_time = draw_visual_now_time + static_cast(m_time_off * 1000); tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, fire_time);\n方案3：修改判断逻辑 如果你想让子弹立即显示，可以修改判断逻辑： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {// 修改为：如果子弹还没到发射时间，使用发射时间进行计算long long effective_time = now_time;if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) {// 子弹还未发射，但我们仍然可以显示（在枪口位置） effective_time = it-\u0026gt;proj.get_fire_t();} HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(effective_time);if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else { res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n完整的调试代码 cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;ROS_INFO(\u0026ldquo;get_circles调用 - now_time: %lld, 子弹数量: %zu\u0026rdquo;, now_time, bullets.size());int bullet_index = 0;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end(); ++bullet_index) {long long fire_t = it-\u0026gt;proj.get_fire_t();ROS_INFO(\u0026ldquo;子弹[%d] - id: %d, fire_t: %lld, now_time: %lld, now_time \u0026lt; fire_t: %s\u0026rdquo;, bullet_index, it-\u0026gt;id, fire_t, now_time, (now_time \u0026lt; fire_t) ? \u0026ldquo;true\u0026rdquo; : \u0026ldquo;false\u0026rdquo;);if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026ldquo;子弹[%d]还未发射，跳过\u0026rdquo;, bullet_index);++it; continue;}// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中 -\u0026gt; 已击中删除if (hit_circle.hit) {ROS_INFO(\u0026ldquo;子弹[%d]已击中，删除\u0026rdquo;, bullet_index); it = this-\u0026gt;bullets.erase(it);} else {// 处理未击中的子弹 -\u0026gt; 未击中添加到结果ROS_INFO(\u0026ldquo;子弹[%d]未击中，添加到结果\u0026rdquo;, bullet_index); res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}ROS_INFO(\u0026ldquo;get_circles返回 - 有效子弹数量: %zu\u0026rdquo;, res.size());return res;}\n推荐的解决方案 最佳方案是方案1 + 方案2的组合： 在AimCorrector中添加固定的发射延迟： cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔const long long fire_delay = 100; // 发射延迟100msif (current_time - this-\u0026gt;last_fire_time \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) { bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + fire_delay) // 延迟发射});this-\u0026gt;last_fire_time = current_time;}}} 在draw_simulated_bullets中确保使用正确的时间： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time) {// 注意：now_time应该是当前图像的时间戳 AimCorrector aim_corrector = AimCorrector(coorConverter, shoot_param);// 更新子弹（可能创建新子弹） aim_corrector.update_bullet(now_time);// 获取子弹在当前时刻的状态 std::vector bullets = aim_corrector.get_circles(now_time);// \u0026hellip; 绘制代码 \u0026hellip;}\n这样，子弹会有100ms的发射延迟，在子弹创建后的前100ms内，now_time \u0026lt; fire_t 会成立，你就能看到111111\u0026hellip;的警告信息了。100ms后子弹才会开始运动并被绘制。 #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { double k = 1; // 空气阻力系数 // 计算水平位移 double w = (t - this-\u0026gt;fire_t) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111111111111111111111111111111\u0026rdquo;); ++it; continue; } // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 200; // 发射间隔：200毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() \u0026lt; AIM_CORRECTOR_BULLETS_MAX_SZ) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++,\nProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 0.025 + COMMAND_TIMESPAN) // ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const std::vector\u0026lt;FlaskLine\u0026gt;\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026lt;\u0026lt;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026gt;\u0026gt;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return *this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param\u0026lt;double\u0026gt;(\u0026quot;auto-aim.debug.flask.map.pixel-per-meter\u0026quot;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param\u0026lt;double\u0026gt;(\u0026quot;auto-aim.debug.flask.map.pixel-per-meter\u0026quot;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param\u0026lt;double\u0026gt;(\u0026quot;auto-aim.ec-simulator.shoot-interval\u0026quot;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector\u0026lt;IdCircle\u0026gt; bullets = aim_corrector.get_circles(now_time,eTime,COMMAND_TIMESPAN); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026lt;\u0026lt; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026lt;\u0026lt; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026lt;\u0026lt; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026gt;\u0026gt; img; } } #ifndef TRAJECTORY_VISUALIZER_HPP #define TRAJECTORY_VISUALIZER_HPP #include \u0026ldquo;math.hpp\u0026rdquo; #include \u0026ldquo;CoorConverter.hpp\u0026rdquo; #include \u0026lt;opencv2/opencv.hpp\u0026gt; #include \u0026ldquo;GimbalPos.hpp\u0026rdquo; #include \u0026ldquo;ros/ros.h\u0026rdquo; namespace tools{ const int FLASK_MAP_WIDTH = 1000; // 定义调试地图的水平分辨率 const double FLASK_MAP_PETER_BY_BRIGHT = 1.; // 默认亮度系数 const int FLASK_MAP_MID_X = FLASK_MAP_WIDTH / 2; // 地图的水平中心点,用于坐标变换的参考原点 // 点绘制参数 struct FlaskPoint { FlaskPoint( const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ): pt(pt), color(color), radius(radius), thickness(thickness) {} cv::Point2d pt; // 圆心位置 cv::Scalar color; // 颜色 int radius; // 半径 int thickness; // 线宽 }; struct FlaskLine { FlaskLine( const std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt;\u0026amp; pt_pair, const cv::Scalar\u0026amp; color, const int\u0026amp; thickness ): pt_pair(pt_pair), color(color), thickness(thickness) {} std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt; pt_pair; cv::Scalar color; int thickness; }; // 文本绘制参数 struct FlaskText { FlaskText( const std::string\u0026amp; str, const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const double\u0026amp; scale ): str(str), pt(pt), color(color), scale(scale) {} std::string str; // 文本内容 cv::Point2d pt; // 文本位置 (左下角) cv::Scalar color; // 颜色 double scale; // 字体大小 }; /* 绘制流管理器 @brief: 收集绘制命令: 通过重载的\u0026laquo;操作符接收各种绘制元素 批量执行绘制: 通过\u0026raquo;操作符将所有收集的命令绘制到图形上 命令管理: 可以清空所有收集的绘制命令\n/ class FlaskStream { public: FlaskStream\u0026amp; operator\u0026laquo;(const char str); FlaskStream\u0026amp; operator\u0026laquo;(const std::string\u0026amp; str); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskPoint\u0026amp; pt); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskLine\u0026amp; line); FlaskStream\u0026amp; operator\u0026laquo;(const std::vector\u0026amp; lines); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskText\u0026amp; text); FlaskStream\u0026amp; operator\u0026raquo;(cv::Mat\u0026amp; img); void clear(); private: std::vectorstd::string logs; std::vector pts; std::vector lines; std::vector texts; }; // 用于复现的瞄准参数 // 移植代码的时候将这段代码移植到自瞄那里 struct ShootParam { double v0 = 0.; // 子弹初速度 double aim_angle = 0.; // 发射仰角 // Eigen::Vector3d aim_xyz_i_barrel = Eigen::Vector3d::Zero(); // 枪管坐标系瞄准点 (没有什么作用) Eigen::Vector3d target_xyz_i_camera = Eigen::Vector3d::Zero(); // 相机坐标系目标点 }; // 子弹命中位置信息 struct HitPos { bool hit; Eigen::Vector3d pos; // 子弹在世界坐标系上的位置 }; // 子弹图像投影信息 struct HitCircle { bool hit; math::CircleF circle; // 子弹在图像上的投影圆 }; // 匹配代价评估 struct CaughtCost { bool caught; // 是否满足匹配条件 double cost; // 匹配代价(越小越好) }; // 子弹弹道物理模拟器 class ProjectileSimulator { public: ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t) : coorConverter{coorConverter},shoot_param{shoot_param} ,fire_t{fire_t} {} // 子弹在图像平面上的投影计算 auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle; // 计算在指定时间t的子弹位置 auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos; // 获取开火时间 auto get_fire_t() const -\u0026gt; double; private: const double g { 9.8 }; const long long fire_t; CoordinateTransformer* coorConverter; ShootParam shoot_param; }; // 子弹位置信息 struct IdPos { int id; Eigen::Vector3d pos; }; // 子弹投影圆信息 struct IdCircle { int id; math::CircleF circle; // 子弹在图像平面上的投影圆 }; // 子弹模拟器封装 struct IdProj { int id; ProjectileSimulator proj; // 子弹物理模拟器实例 }; // 自动瞄准误差校准(目前仅用来复现理想弹道) class AimCorrector { public: AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param); // 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆 auto get_circles(long long now_time) -\u0026gt; std::vector;\nauto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); private: std::list\u0026lt;IdProj\u0026gt; bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0; long long last_fire_time = 0; }; cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z); FlaskPoint pos_to_map_point( const Eigen::Vector3d\u0026amp; pos, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ); // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN); } #endif // TRAJECTORY_VISUALIZER_HPP rm@rm-NUC11PAHi7:/ws_glut_vison$ catkin_make Base path: /home/rm/ws_glut_vison Source space: /home/rm/ws_glut_vison/src Build space: /home/rm/ws_glut_vison/build Devel space: /home/rm/ws_glut_vison/devel Install space: /home/rm/ws_glut_vison/install Running command: \u0026ldquo;make cmake_check_build_system\u0026rdquo; in \u0026ldquo;/home/rm/ws_glut_vison/build\u0026rdquo; Running command: \u0026ldquo;make -j8 -l8\u0026rdquo; in \u0026ldquo;/home/rm/ws_glut_vison/build\u0026rdquo; [ 0%] Built target std_msgs_generate_messages_py [ 0%] Built target geometry_msgs_generate_messages_py [ 0%] Built target std_msgs_generate_messages_cpp [ 0%] Built target geometry_msgs_generate_messages_eus [ 0%] Built target geometry_msgs_generate_messages_cpp [ 0%] Built target std_msgs_generate_messages_eus [ 5%] Built target hikcamera [ 5%] Built target std_msgs_generate_messages_lisp [ 5%] Built target geometry_msgs_generate_messages_lisp [ 5%] Built target std_msgs_generate_messages_nodejs [ 5%] Built target _rm_msgs_generate_messages_check_deps_Debug [ 5%] Built target _rm_msgs_generate_messages_check_deps_ArmorArray [ 5%] Built target _rm_msgs_generate_messages_check_deps_Armor [ 5%] Built target geometry_msgs_generate_messages_nodejs [ 5%] Built target _rm_msgs_generate_messages_check_deps_RmSerial [ 7%] Building CXX object rm_serial/CMakeFiles/serial.dir/src/serial.cpp.o [ 20%] Built target rm_msgs_generate_messages_py [ 30%] Built target rm_msgs_generate_messages_cpp [ 40%] Built target rm_msgs_generate_messages_lisp [ 55%] Built target rm_msgs_generate_messages_eus [ 57%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/include/math.cpp.o [ 60%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/include/CoorConverter.cpp.o [ 60%] Building CXX object rm_identify/CMakeFiles/identify.dir/src/identify.cpp.o [ 62%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/src/tracker.cpp.o [ 65%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/include/MPC.cpp.o [ 75%] Built target rm_msgs_generate_messages_nodejs [ 77%] Building CXX object rm_identify/CMakeFiles/identify_test.dir/src/identify_test.cpp.o [ 77%] Built target rm_msgs_generate_messages [ 80%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/include/trajectory_visualizer.cpp.o In file included from /home/rm/ws_glut_vison/src/rm_serial/src/serial.cpp:2: /home/rm/ws_glut_vison/src/rm_serial/include/Serial.h: In member function ‘long long int SerialPort::timestamp(unsigned char, unsigned char, unsigned char, unsigned char, unsigned char, unsigned char, unsigned char, unsigned char)’: /home/rm/ws_glut_vison/src/rm_serial/include/Serial.h:545:28: warning: left shift count \u0026gt;= width of type [-Wshift-count-overflow] 545 | temp = (uint32_t)h_32 \u0026laquo; 32 | (uint32_t) l_32; | ^ /home/rm/ws_glut_vison/src/rm_serial/src/serial.cpp: At global scope: /home/rm/ws_glut_vison/src/rm_serial/src/serial.cpp:15:32: warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings] 15 | constexpr char* SerialPrefix = \u0026ldquo;/dev/\u0026rdquo;; | ^~~~~~~ In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: error: extra qualification ‘tools::AimCorrector::’ on member ‘update_bullet’ [-fpermissive] 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:81:10: error: no declaration matches ‘void tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 81 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { | ^~~~~~~~~~~~ In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: note: candidate is: ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:155:11: note: ‘class tools::AimCorrector’ defined here 155 | class AimCorrector { | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp: In function ‘void tools::draw_simulated_bullets(CoordinateTransformer*, const tools::ShootParam\u0026amp;, cv::Mat\u0026amp;, long long int, long long int, long long int)’: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:230:45: error: no matching function for call to ‘tools::AimCorrector::update_bullet(long long int\u0026amp;)’ 230 | aim_corrector.update_bullet(now_time); | ^ In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: note: candidate: ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: note: candidate expects 3 arguments, 1 provided /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:232:98: error: no matching function for call to ‘tools::AimCorrector::get_circles(long long int\u0026amp;, long long int\u0026amp;, long long int\u0026amp;)’ 232 | std::vector bullets = aim_corrector.get_circles(now_time,eTime,COMMAND_TIMESPAN); | ^ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:50:10: note: candidate: ‘std::vectortools::IdCircle tools::AimCorrector::get_circles(long long int)’ 50 | auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:50:10: note: candidate expects 1 argument, 3 provided [ 82%] Linking CXX executable /home/rm/ws_glut_vison/devel/lib/rm_serial/serial [ 82%] Built target serial In file included from /home/rm/ws_glut_vison/src/rm_identify/include/new_Rmidentify.hpp:9, from /home/rm/ws_glut_vison/src/rm_identify/src/identify_test.cpp:2: /home/rm/ws_glut_vison/src/rm_identify/include/Light.h: In member function ‘float Light::cal_ang(cv::Point2f, cv::Point2f, cv::Point2f)’: /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:246:13: warning: NULL used in arithmetic [-Wpointer-arith] 246 | if (B != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h: In member function ‘std::vector\u0026lt;cv::Point_ \u0026gt; Light::refine_the_armor(cv::Mat, int)’: /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:19: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:37: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:55: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:73: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ make[2]: *** [rm_tracker/CMakeFiles/tracker.dir/build.make:132：rm_tracker/CMakeFiles/tracker.dir/include/trajectory_visualizer.cpp.o] 错误 1 make[2]: *** 正在等待未完成的任务\u0026hellip;. In file included from /home/rm/ws_glut_vison/src/rm_identify/include/RmIdentify.hpp:10, from /home/rm/ws_glut_vison/src/rm_identify/src/identify.cpp:2: /home/rm/ws_glut_vison/src/rm_identify/include/Light.h: In member function ‘float Light::cal_ang(cv::Point2f, cv::Point2f, cv::Point2f)’: /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:246:13: warning: NULL used in arithmetic [-Wpointer-arith] 246 | if (B != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h: In member function ‘std::vector\u0026lt;cv::Point_ \u0026gt; Light::refine_the_armor(cv::Mat, int)’: /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:19: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:37: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:55: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ /home/rm/ws_glut_vison/src/rm_identify/include/Light.h:367:73: warning: NULL used in arithmetic [-Wpointer-arith] 367 | if (angle1 != NULL \u0026amp;\u0026amp; angle2 != NULL \u0026amp;\u0026amp; angle3 != NULL \u0026amp;\u0026amp; angle4 != NULL) | ^~~~ In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/RmTracker.hpp:34, from /home/rm/ws_glut_vison/src/rm_tracker/src/tracker.cpp:3: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: error: extra qualification ‘tools::AimCorrector::’ on member ‘update_bullet’ [-fpermissive] 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ In file included from /home/rm/ws_glut_vison/src/rm_identify/include/new_Rmidentify.hpp:12, from /home/rm/ws_glut_vison/src/rm_identify/src/identify_test.cpp:2: /home/rm/ws_glut_vison/src/rm_identify/include/new_detector.h: In function ‘float new_Identify::cal_ang(cv::Point2f, cv::Point2f, cv::Point2f)’: /home/rm/ws_glut_vison/src/rm_identify/include/new_detector.h:568:13: warning: NULL used in arithmetic [-Wpointer-arith] 568 | if (B != NULL) | ^~~~ make[2]: *** [rm_tracker/CMakeFiles/tracker.dir/build.make:76：rm_tracker/CMakeFiles/tracker.dir/src/tracker.cpp.o] 错误 1 make[1]: *** [CMakeFiles/Makefile2:2125：rm_tracker/CMakeFiles/tracker.dir/all] 错误 2 make[1]: *** 正在等待未完成的任务\u0026hellip;. [ 85%] Linking CXX executable /home/rm/ws_glut_vison/devel/lib/rm_identify/identify /usr/bin/ld: warning: libopencv_imgcodecs.so.4.2, needed by /opt/ros/noetic/lib/libcv_bridge.so, may conflict with libopencv_imgcodecs.so.407 /usr/bin/ld: warning: libopencv_features2d.so.4.2, needed by /usr/lib/x86_64-linux-gnu/libopencv_calib3d.so.4.2.0, may conflict with libopencv_features2d.so.407 /usr/bin/ld: warning: libopencv_imgproc.so.407, needed by /usr/local/lib/libopencv_imgcodecs.so.4.7.0, may conflict with libopencv_imgproc.so.4.2 /usr/bin/ld: warning: libopencv_core.so.407, needed by /usr/local/lib/libopencv_imgcodecs.so.4.7.0, may conflict with libopencv_core.so.4.2 [ 90%] Built target identify [ 92%] Linking CXX executable /home/rm/ws_glut_vison/devel/lib/rm_identify/identify_test /usr/bin/ld: warning: libopencv_imgcodecs.so.4.2, needed by /opt/ros/noetic/lib/libcv_bridge.so, may conflict with libopencv_imgcodecs.so.407 /usr/bin/ld: warning: libopencv_features2d.so.4.2, needed by /usr/lib/x86_64-linux-gnu/libopencv_calib3d.so.4.2.0, may conflict with libopencv_features2d.so.407 /usr/bin/ld: warning: libopencv_imgproc.so.407, needed by /usr/local/lib/libopencv_imgcodecs.so.4.7.0, may conflict with libopencv_imgproc.so.4.2 /usr/bin/ld: warning: libopencv_core.so.407, needed by /usr/local/lib/libopencv_imgcodecs.so.4.7.0, may conflict with libopencv_core.so.4.2 [ 97%] Built target identify_test make: *** [Makefile:146：all] 错误 2 Invoking \u0026ldquo;make -j8 -l8\u0026rdquo; failed rm@rm-NUC11PAHi7:~/ws_glut_vison$ catkin_make Base path: /home/rm/ws_glut_vison Source space: /home/rm/ws_glut_vison/src Build space: /home/rm/ws_glut_vison/build Devel space: /home/rm/ws_glut_vison/devel Install space: /home/rm/ws_glut_vison/install Running command: \u0026ldquo;make cmake_check_build_system\u0026rdquo; in \u0026ldquo;/home/rm/ws_glut_vison/build\u0026rdquo; Running command: \u0026ldquo;make -j8 -l8\u0026rdquo; in \u0026ldquo;/home/rm/ws_glut_vison/build\u0026rdquo; [ 0%] Built target geometry_msgs_generate_messages_py [ 0%] Built target std_msgs_generate_messages_py [ 0%] Built target std_msgs_generate_messages_cpp [ 5%] Built target hikcamera [ 5%] Built target geometry_msgs_generate_messages_cpp [ 5%] Built target std_msgs_generate_messages_eus [ 5%] Built target geometry_msgs_generate_messages_eus [ 5%] Built target _rm_msgs_generate_messages_check_deps_RmSerial [ 5%] Built target _rm_msgs_generate_messages_check_deps_ArmorArray [ 5%] Built target geometry_msgs_generate_messages_lisp [ 5%] Built target std_msgs_generate_messages_nodejs [ 5%] Built target std_msgs_generate_messages_lisp [ 5%] Built target geometry_msgs_generate_messages_nodejs [ 5%] Built target _rm_msgs_generate_messages_check_deps_Armor [ 5%] Built target _rm_msgs_generate_messages_check_deps_Debug [ 15%] Built target rm_msgs_generate_messages_cpp [ 20%] Built target serial [ 32%] Built target rm_msgs_generate_messages_eus [ 45%] Built target rm_msgs_generate_messages_py [ 55%] Built target identify [ 65%] Built target identify_test [ 85%] Built target rm_msgs_generate_messages_nodejs [ 85%] Built target rm_msgs_generate_messages_lisp [ 85%] Built target rm_msgs_generate_messages [ 87%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/src/tracker.cpp.o [ 90%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/include/trajectory_visualizer.cpp.o In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: error: extra qualification ‘tools::AimCorrector::’ on member ‘update_bullet’ [-fpermissive] 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:81:10: error: no declaration matches ‘void tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 81 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { | ^~~~~~~~~~~~ In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: note: candidate is: ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:155:11: note: ‘class tools::AimCorrector’ defined here 155 | class AimCorrector { | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp: In function ‘void tools::draw_simulated_bullets(CoordinateTransformer*, const tools::ShootParam\u0026amp;, cv::Mat\u0026amp;, long long int, long long int, long long int)’: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:230:68: error: use of ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ before deduction of ‘auto’ 230 | aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); | ^ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:232:98: error: no matching function for call to ‘tools::AimCorrector::get_circles(long long int\u0026amp;, long long int\u0026amp;, long long int\u0026amp;)’ 232 | std::vector bullets = aim_corrector.get_circles(now_time,eTime,COMMAND_TIMESPAN); | ^ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:50:10: note: candidate: ‘std::vectortools::IdCircle tools::AimCorrector::get_circles(long long int)’ 50 | auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:50:10: note: candidate expects 1 argument, 3 provided make[2]: *** [rm_tracker/CMakeFiles/tracker.dir/build.make:132：rm_tracker/CMakeFiles/tracker.dir/include/trajectory_visualizer.cpp.o] 错误 1 make[2]: *** 正在等待未完成的任务\u0026hellip;. In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/RmTracker.hpp:34, from /home/rm/ws_glut_vison/src/rm_tracker/src/tracker.cpp:3: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: error: extra qualification ‘tools::AimCorrector::’ on member ‘update_bullet’ [-fpermissive] 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ make[2]: *** [rm_tracker/CMakeFiles/tracker.dir/build.make:76：rm_tracker/CMakeFiles/tracker.dir/src/tracker.cpp.o] 错误 1 make[1]: *** [CMakeFiles/Makefile2:2125：rm_tracker/CMakeFiles/tracker.dir/all] 错误 2 make: *** [Makefile:146：all] 错误 2 Invoking \u0026ldquo;make -j8 -l8\u0026rdquo; failed 这里为什么报错?\n根据编译错误信息，有以下几个问题需要解决： 头文件中的函数声明与实现不匹配： 在头文件 trajectory_visualizer.hpp 中，AimCorrector 类的 update_bullet 成员函数声明为三个参数，但是实现时（在 trajectory_visualizer.cpp 中）却定义了一个参数的版本（get_circles 也有类似问题，但错误信息显示是调用时参数不匹配，实际上get_circles在头文件中声明为一个参数，但实现时也是一个参数，但是在draw_simulated_bullets中调用时却传了三个参数）。 多余的作用域限定符： 在头文件中，函数声明时使用了 auto AimCorrector::update_bullet(\u0026hellip;); 这种形式，这是错误的。在类内部声明成员函数时，不需要加上类作用域。 函数调用时的参数数量不匹配： 在 draw_simulated_bullets 函数中，调用 aim_corrector.update_bullet 和 aim_corrector.get_circles 时传递的参数数量与函数声明不一致。\n让我们逐一修正： 修正步骤： 修正头文件中的函数声明（trajectory_visualizer.hpp） 在类 AimCorrector 中，声明函数时不要加类作用域，并且确保参数列表与实现一致。 原头文件中的错误声明： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN);\n应改为（在类内部）： cpp\nauto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; 同时，get_circles 在头文件中声明为一个参数，但是我们在调用时却传了三个参数。根据实现，get_circles 确实只需要一个参数 now_time。因此，我们需要修改调用处，使其只传递一个参数。 修正实现文件中的函数定义（trajectory_visualizer.cpp） 在实现文件中，函数定义应该与头文件中的声明一致。 对于 update_bullet，我们已经在头文件中改为三个参数，因此实现部分也需要相应修改（实际上在实现部分已经是三个参数，但函数定义前的类作用域写法有误）。 在实现文件中，定义成员函数时应该使用类作用域，但不要加 auto 关键字在返回类型前（除非使用尾置返回类型，这里我们使用了尾置返回类型，所以保留 auto，但要去掉头文件中声明时的类作用域）。 修正函数调用（trajectory_visualizer.cpp 中的 draw_simulated_bullets） 在 draw_simulated_bullets 函数中，我们调用 aim_corrector.update_bullet 时应该传递三个参数，而调用 aim_corrector.get_circles 时只传递一个参数。 具体修改： 修改头文件（trajectory_visualizer.hpp）： cpp\n// 在 AimCorrector 类中，修改 update_bullet 的声明auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;// get_circles 的声明保持不变（一个参数）auto get_circles(long long now_time) -\u0026gt; std::vector; 修改实现文件（trajectory_visualizer.cpp）： cpp\n// 修改 update_bullet 函数定义，去掉类作用域（但实现时要用类作用域）auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {// 函数体}// get_circles 函数定义保持不变auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{// 函数体} 修改 draw_simulated_bullets 函数中的调用： cpp // 更新子弹序列，传入三个参数 aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN);// 获取子弹，只传入一个参数 std::vector bullets = aim_corrector.get_circles(now_time);\n注意： 头文件中的函数声明不需要加 AimCorrector::，因为这是在类内部。 实现文件中的函数定义需要加 AimCorrector:: 以指明这是类的成员函数。 函数调用时，参数数量必须与函数声明一致。\n根据以上分析，我们需要修改头文件和实现文件中的相关部分，并确保调用时传递正确数量的参数。 由于错误信息还指出 draw_simulated_bullets 函数调用 get_circles 时传递了三个参数，但函数只接受一个参数，因此需要修改调用方式。 另外，在头文件中，我们声明了 update_bullet 和 get_circles，但是它们的参数列表需要和实现一致。根据你的代码，update_bullet 需要三个参数（current_time, eTime, COMMAND_TIMESPAN），而 get_circles 只需要一个参数（now_time）。 因此，在 draw_simulated_bullets 中，调用 get_circles 时只传递一个参数，即当前时间 now_time。 修改后的代码示例： trajectory_visualizer.hpp 中的 AimCorrector 类： cpp\nclass AimCorrector {public:AimCorrector(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param);// 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆auto get_circles(long long now_time) -\u0026gt; std::vector;auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;private:// \u0026hellip; 其他成员}; trajectory_visualizer.cpp 中的函数定义： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; AIM_CORRECTOR_BULLETS_MAX_SZ) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 0.025 + COMMAND_TIMESPAN)// ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time)});this-\u0026gt;last_fire_time = current_time;}}}auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{// 函数体不变}\ndraw_simulated_bullets 函数中的调用： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){// \u0026hellip; 其他代码 aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); // 只传递一个参数// \u0026hellip; 绘制代码}\n经过以上修改，应该可以解决编译错误。请注意，在修改头文件后，需要确保包含该头文件的源文件重新编译。因此，建议进行完整的重新构建（clean 然后 build）。 根据编译错误，有多个问题需要解决。让我逐一分析并给出修复方案： 主要错误分析 1. 类作用域限定符错误 cpp\n// 错误：在类声明内部使用了作用域限定符auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN);// 正确：应该在类声明内部直接声明auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; 2. 函数参数不匹配 get_circles 声明为一个参数，但调用时传入三个参数 函数签名不一致\n修复方案 1. 修改头文件 (trajectory_visualizer.hpp) cpp #ifndef TRAJECTORY_VISUALIZER_HPP#define TRAJECTORY_VISUALIZER_HPP#include \u0026ldquo;math.hpp\u0026rdquo;#include \u0026ldquo;CoorConverter.hpp\u0026rdquo;#include \u0026lt;opencv2/opencv.hpp\u0026gt;#include \u0026ldquo;GimbalPos.hpp\u0026rdquo;#include \u0026ldquo;ros/ros.h\u0026quot;namespace tools{const int FLASK_MAP_WIDTH = 1000; // 定义调试地图的水平分辨率const double FLASK_MAP_PETER_BY_BRIGHT = 1.; // 默认亮度系数const int FLASK_MAP_MID_X = FLASK_MAP_WIDTH / 2; // 地图的水平中心点,用于坐标变换的参考原点// 点绘制参数struct FlaskPoint {FlaskPoint(const cv::Point2d\u0026amp; pt,const cv::Scalar\u0026amp; color,const int\u0026amp; radius,const int\u0026amp; thickness):pt(pt),color(color),radius(radius),thickness(thickness) {} cv::Point2d pt; // 圆心位置 cv::Scalar color; // 颜色int radius; // 半径int thickness; // 线宽};struct FlaskLine {FlaskLine(const std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt;\u0026amp; pt_pair,const cv::Scalar\u0026amp; color,const int\u0026amp; thickness):pt_pair(pt_pair),color(color),thickness(thickness) {} std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt; pt_pair; cv::Scalar color;int thickness;};// 文本绘制参数struct FlaskText {FlaskText(const std::string\u0026amp; str,const cv::Point2d\u0026amp; pt,const cv::Scalar\u0026amp; color,const double\u0026amp; scale):str(str),pt(pt),color(color),scale(scale) {} std::string str; // 文本内容 cv::Point2d pt; // 文本位置 (左下角) cv::Scalar color; // 颜色double scale; // 字体大小};/* 绘制流管理器 @brief: 收集绘制命令: 通过重载的\u0026laquo;操作符接收各种绘制元素 批量执行绘制: 通过\u0026raquo;操作符将所有收集的命令绘制到图形上 命令管理: 可以清空所有收集的绘制命令 /class FlaskStream {public: FlaskStream\u0026amp; operator\u0026laquo;(const char str); FlaskStream\u0026amp; operator\u0026laquo;(const std::string\u0026amp; str); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskPoint\u0026amp; pt); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskLine\u0026amp; line); FlaskStream\u0026amp; operator\u0026laquo;(const std::vector\u0026amp; lines); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskText\u0026amp; text); FlaskStream\u0026amp; operator\u0026raquo;(cv::Mat\u0026amp; img);void clear();private: std::vectorstd::string logs; std::vector pts; std::vector lines; std::vector texts;};// 用于复现的瞄准参数// 移植代码的时候将这段代码移植到自瞄那里struct ShootParam {double v0 = 0.; // 子弹初速度double aim_angle = 0.; // 发射仰角// Eigen::Vector3d aim_xyz_i_barrel = Eigen::Vector3d::Zero(); // 枪管坐标系瞄准点 (没有什么作用) Eigen::Vector3d target_xyz_i_camera = Eigen::Vector3d::Zero(); // 相机坐标系目标点};// 子弹命中位置信息struct HitPos {bool hit; Eigen::Vector3d pos; // 子弹在世界坐标系上的位置 };// 子弹图像投影信息struct HitCircle {bool hit; math::CircleF circle; // 子弹在图像上的投影圆};// 匹配代价评估struct CaughtCost {bool caught; // 是否满足匹配条件double cost; // 匹配代价(越小越好)};// 子弹弹道物理模拟器class ProjectileSimulator {public:ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t) : coorConverter{coorConverter},shoot_param{shoot_param} ,fire_t{fire_t} {}// 子弹在图像平面上的投影计算auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle;// 计算在指定时间t的子弹位置auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos;// 获取开火时间auto get_fire_t() const -\u0026gt; double;private:const double g { 9.8 };const long long fire_t; CoordinateTransformer* coorConverter; ShootParam shoot_param;};// 子弹位置信息struct IdPos {int id; Eigen::Vector3d pos;};// 子弹投影圆信息struct IdCircle {\nint id; math::CircleF circle; // 子弹在图像平面上的投影圆};// 子弹模拟器封装struct IdProj {int id; ProjectileSimulator proj; // 子弹物理模拟器实例};// 自动瞄准误差校准(目前仅用来复现理想弹道)class AimCorrector {public:AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param);// 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆auto get_circles(long long now_time) -\u0026gt; std::vector;auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0; long long last_fire_time = 0;}; cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z); FlaskPoint pos_to_map_point(const Eigen::Vector3d\u0026amp; pos,const cv::Scalar\u0026amp; color,const int\u0026amp; radius,const int\u0026amp; thickness);// 绘制模拟发射的子弹void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN);}#endif // TRAJECTORY_VISUALIZER_HPP\n2. 修改实现文件 (trajectory_visualizer.cpp) cpp #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo;#include namespace tools{auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos);// 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized();// 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos);double radius = math::get_dis(edge_xy_u, center_xy_u);return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) };}auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {double k = 1; // 空气阻力系数// 计算水平位移double w = (t - this-\u0026gt;fire_t) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle);// 计算高度double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w/ (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))+ this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k/ k;// 弹道轨迹仅取决于目标点(理想弹道)const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 计算基准方向const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized();const Eigen::Vector3d h_norm = { 0., 0., 1. };const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm;const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0),bullet_xyz_i_barrel(1, 0) };const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0),target_xyz_i_barrel(1 Invoking \u0026ldquo;make -j8 -l8\u0026rdquo; failed rm@rm-NUC11PAHi7:/ws_glut_vison$ catkin_make Base path: /home/rm/ws_glut_vison Source space: /home/rm/ws_glut_vison/src Build space: /home/rm/ws_glut_vison/build Devel space: /home/rm/ws_glut_vison/devel Install space: /home/rm/ws_glut_vison/install Running command: \u0026ldquo;make cmake_check_build_system\u0026rdquo; in \u0026ldquo;/home/rm/ws_glut_vison/build\u0026rdquo; Running command: \u0026ldquo;make -j8 -l8\u0026rdquo; in \u0026ldquo;/home/rm/ws_glut_vison/build\u0026rdquo; [ 0%] Built target std_msgs_generate_messages_py [ 0%] Built target geometry_msgs_generate_messages_py [ 0%] Built target std_msgs_generate_messages_cpp [ 0%] Built target std_msgs_generate_messages_eus [ 0%] Built target std_msgs_generate_messages_lisp [ 0%] Built target geometry_msgs_generate_messages_eus [ 0%] Built target _rm_msgs_generate_messages_check_deps_ArmorArray [ 5%] Built target hikcamera [ 5%] Built target _rm_msgs_generate_messages_check_deps_Armor [ 5%] Built target _rm_msgs_generate_messages_check_deps_RmSerial [ 5%] Built target geometry_msgs_generate_messages_cpp [ 5%] Built target _rm_msgs_generate_messages_check_deps_Debug [ 5%] Built target std_msgs_generate_messages_nodejs [ 5%] Built target geometry_msgs_generate_messages_nodejs [ 5%] Built target geometry_msgs_generate_messages_lisp [ 17%] Built target rm_msgs_generate_messages_py [ 27%] Built target rm_msgs_generate_messages_cpp [ 37%] Built target identify_test [ 47%] Built target identify [ 52%] Built target serial [ 65%] Built target rm_msgs_generate_messages_eus [ 75%] Built target rm_msgs_generate_messages_nodejs [ 85%] Built target rm_msgs_generate_messages_lisp [ 85%] Built target rm_msgs_generate_messages [ 87%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/src/tracker.cpp.o [ 90%] Building CXX object rm_tracker/CMakeFiles/tracker.dir/include/trajectory_visualizer.cpp.o In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: error: extra qualification ‘tools::AimCorrector::’ on member ‘update_bullet’ [-fpermissive] 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:81:10: error: no declaration matches ‘void tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 81 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { | ^~~~~~~~~~~~ In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:1: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: note: candidate is: ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:155:11: note: ‘class tools::AimCorrector’ defined here 155 | class AimCorrector { | ^~~~~~~~~~~~ /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp: In function ‘void tools::draw_simulated_bullets(CoordinateTransformer*, const tools::ShootParam\u0026amp;, cv::Mat\u0026amp;, long long int, long long int, long long int)’: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.cpp:230:68: error: use of ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ before deduction of ‘auto’ 230 | aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); | ^ make[2]: *** [rm_tracker/CMakeFiles/tracker.dir/build.make:132：rm_tracker/CMakeFiles/tracker.dir/include/trajectory_visualizer.cpp.o] 错误 1 make[2]: *** 正在等待未完成的任务\u0026hellip;. In file included from /home/rm/ws_glut_vison/src/rm_tracker/include/RmTracker.hpp:34, from /home/rm/ws_glut_vison/src/rm_tracker/src/tracker.cpp:3: /home/rm/ws_glut_vison/src/rm_tracker/include/trajectory_visualizer.hpp:162:14: error: extra qualification ‘tools::AimCorrector::’ on member ‘update_bullet’ [-fpermissive] 162 | auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); | ^~~~~~~~~~~~ make[2]: *** [rm_tracker/CMakeFiles/tracker.dir/build.make:76：rm_tracker/CMakeFiles/tracker.dir/src/tracker.cpp.o] 错误 1 make[1]: *** [CMakeFiles/Makefile2:2125：rm_tracker/CMakeFiles/tracker.dir/all] 错误 2 make: *** [Makefile:146：all] 错误 2 Invoking \u0026ldquo;make -j8 -l8\u0026rdquo; failed rm@rm-NUC11PAHi7:/ws_glut_vison$ 我已经修改了上面的代码,这里为什么报错? 不要生成一大堆代码 我们之前修改了头文件，但是在头文件中的类声明里，我们仍然错误地使用了作用域限定符。 错误出现在头文件的第162行：auto AimCorrector::update_bullet(\u0026hellip;); 在类定义内部，我们声明成员函数时不应该加上类名限定。应该直接写函数声明。 让我们检查一下头文件中的AimCorrector类的声明部分。根据错误信息，头文件第162行是： auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); 这行代码在类AimCorrector的内部，这是错误的。应该改为： auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;\n另外，我们还需要确保在类声明中，这个函数的声明与实现文件的函数签名一致。 同时，我们注意到在实现文件中，函数定义时也错误地加上了作用域限定符，但是是在类外部，这是正确的。不过，由于我们修改了头文件，实现文件的函数定义应该与头文件中的声明匹配。 但是，错误信息显示实现文件中的函数定义与头文件中的声明不匹配。实际上，我们刚才在头文件中修改了，但是实现文件中的函数定义也需要修改，因为之前我们在实现文件中定义函数时，函数签名可能不一致。 让我们重新整理： 头文件中AimCorrector类的声明： class AimCorrector { public: AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param); auto get_circles(long long now_time) -\u0026gt; std::vector; auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; // 修改这一行，去掉类名限定 private: \u0026hellip; };\n实现文件中，函数定义应该为： auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { \u0026hellip; // 函数体 }\n但是，我们注意到错误信息中还有另一个问题：在draw_simulated_bullets函数中，调用update_bullet时，我们传递了三个参数，但是现在update_bullet函数已经修改为三个参数，所以调用方式应该是正确的。 然而，错误信息还提示“use of ‘auto tools::AimCorrector::update_bullet(\u0026hellip;)’ before deduction of ‘auto’”。这通常是因为函数声明为auto，但定义时没有正确指定返回类型（或者定义和声明不匹配）。我们已经在声明和定义中都指定了返回类型为void（通过尾返回类型），所以这个错误可能是由于之前的声明错误导致的。 另外，我们还需要检查头文件中的函数声明是否与实现文件中的定义一致。特别是，我们修改了头文件，但实现文件中的定义可能还是旧的。 根据你提供的错误信息，头文件第162行仍然是错误的，说明头文件没有修改成功。请确保你已经将头文件中的那一行修改为： auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;\n如果头文件已经修改，但错误仍然存在，请检查是否保存了头文件，或者是否在多个地方有重复的声明。 由于我们无法直接修改你的文件，请按照上述步骤修改头文件和实现文件。 如果问题仍然存在，请提供修改后的头文件和实现文件的相关部分，以便进一步分析。 另外，错误信息中还提到在draw_simulated_bullets函数中调用update_bullet时使用了三个参数，而update_bullet函数现在已经接受三个参数，所以这里应该没有问题。但是，注意在draw_simulated_bullets函数中，我们调用的是： aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN);\n这没有问题。 但是，我们注意到在实现文件中的draw_simulated_bullets函数中，我们调用了aim_corrector.get_circles(now_time,eTime,COMMAND_TIMESPAN)，但是get_circles函数只接受一个参数。这可能是导致另一个错误的原因。请检查： 在头文件中，get_circles函数声明为： auto get_circles(long long now_time) -\u0026gt; std::vector;\n所以它只能接受一个参数。但是在draw_simulated_bullets函数中，我们传递了三个参数。这显然是错误的。 因此，我们需要修改draw_simulated_bullets函数中调用get_circles的方式，只传递一个参数。 总结需要修改的地方： 头文件中AimCorrector类的update_bullet成员函数声明，去掉类名限定。 确保update_bullet函数在头文件中声明为三个参数，在实现文件中定义也为三个参数。 修改draw_simulated_bullets函数中调用get_circles的方式，只传递一个参数（now_time）。\n请按照上述步骤修改，然后重新编译。 如果还有问题，请提供修改后的代码。 错误依然存在，主要问题是头文件中第162行的类成员函数声明格式错误。让我直接指出并修复这个问题： 问题所在 在你的头文件中第162行： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN);\n这个写法是错误的，因为在类声明内部，你不能使用作用域限定符 AimCorrector::。 修复方案\n修改头文件 (trajectory_visualizer.hpp) 找到第162行，修改为： cpp auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;\n完整的类声明应该如下： cpp // 自动瞄准误差校准(目前仅用来复现理想弹道)class AimCorrector {public:AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param);// 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆auto get_circles(long long now_time) -\u0026gt; std::vector;// 修正这里：移除\u0026quot;AimCorrector::\u0026ldquo;auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0; long long last_fire_time = 0;};\n2. 更新实现文件 (trajectory_visualizer.cpp) 确保实现文件中的函数定义与头文件声明一致： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) { // 限制子弹数量 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 0.025 + COMMAND_TIMESPAN)});this-\u0026gt;last_fire_time = current_time;}}}\n3. 检查draw_simulated_bullets函数调用 在实现文件中，确保调用的参数正确： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) { FlaskStream flask_aim; flask_aim.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter, shoot_param);// 更新子弹序列 aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); // 注意：这里只传一个参数for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img;}\n4. 检查Tracker中的调用 在Tracker中调用draw_simulated_bullets时，确保参数正确： cpp // 在Tracker的reconstruction_choose_compensation函数中： tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, static_cast(eTime * 1000), // 转换为毫秒static_cast(COMMAND_TIMESPAN * 1000)); // 转换为毫秒\n快速修复步骤 打开头文件，找到第162行，将： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN); 改为： cpp auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; 检查draw_simulated_bullets函数声明，确保在头文件中声明为： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN); 清理并重新编译： bash rm -rf build devel catkin_make\n主要问题就是头文件中的类成员函数声明格式错误。在类声明内部，只需要写函数名，不能写作用域限定符。 #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { double k = 1; // 空气阻力系数 // 计算水平位移 double w = (t - this-\u0026gt;fire_t) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111111111111111111111111111111\u0026rdquo;); ++it; continue; } // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 200; // 发射间隔：200毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() \u0026lt; AIM_CORRECTOR_BULLETS_MAX_SZ) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++, ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 0.025 + COMMAND_TIMESPAN) // ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::vector\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return this; } FlaskStream\u0026amp; FlaskStream::operator\u0026raquo;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param(\u0026ldquo;auto-aim.ec-simulator.shoot-interval\u0026rdquo;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026laquo; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026raquo; img; } } #ifndef TRAJECTORY_VISUALIZER_HPP #define TRAJECTORY_VISUALIZER_HPP #include \u0026ldquo;math.hpp\u0026rdquo; #include \u0026ldquo;CoorConverter.hpp\u0026rdquo; #include \u0026lt;opencv2/opencv.hpp\u0026gt; #include \u0026ldquo;GimbalPos.hpp\u0026rdquo; #include \u0026ldquo;ros/ros.h\u0026rdquo; namespace tools{ const int FLASK_MAP_WIDTH = 1000; // 定义调试地图的水平分辨率 const double FLASK_MAP_PETER_BY_BRIGHT = 1.; // 默认亮度系数 const int FLASK_MAP_MID_X = FLASK_MAP_WIDTH / 2; // 地图的水平中心点,用于坐标变换的参考原点 // 点绘制参数 struct FlaskPoint { FlaskPoint( const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ): pt(pt), color(color), radius(radius), thickness(thickness) {} cv::Point2d pt; // 圆心位置 cv::Scalar color; // 颜色 int radius; // 半径 int thickness; // 线宽 }; struct FlaskLine { FlaskLine( const std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt;\u0026amp; pt_pair, const cv::Scalar\u0026amp; color, const int\u0026amp; thickness ): pt_pair(pt_pair), color(color), thickness(thickness) {} std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt; pt_pair; cv::Scalar color; int thickness; }; // 文本绘制参数 struct FlaskText { FlaskText( const std::string\u0026amp; str, const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const double\u0026amp; scale ): str(str), pt(pt), color(color), scale(scale) {} std::string str; // 文本内容 cv::Point2d pt; // 文本位置 (左下角) cv::Scalar color; // 颜色 double scale; // 字体大小 }; / 绘制流管理器 @brief: 收集绘制命令: 通过重载的\u0026laquo;操作符接收各种绘制元素 批量执行绘制: 通过\u0026raquo;操作符将所有收集的命令绘制到图形上 命令管理: 可以清空所有收集的绘制命令\n/ class FlaskStream { public: FlaskStream\u0026amp; operator\u0026laquo;(const char str); FlaskStream\u0026amp; operator\u0026laquo;(const std::string\u0026amp; str); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskPoint\u0026amp; pt); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskLine\u0026amp; line); FlaskStream\u0026amp; operator\u0026laquo;(const std::vector\u0026amp; lines); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskText\u0026amp; text); FlaskStream\u0026amp; operator\u0026raquo;(cv::Mat\u0026amp; img); void clear(); private: std::vectorstd::string logs; std::vector pts; std::vector lines; std::vector texts; }; // 用于复现的瞄准参数 // 移植代码的时候将这段代码移植到自瞄那里 struct ShootParam { double v0 = 0.; // 子弹初速度 double aim_angle = 0.; // 发射仰角 // Eigen::Vector3d aim_xyz_i_barrel = Eigen::Vector3d::Zero(); // 枪管坐标系瞄准点 (没有什么作用) Eigen::Vector3d target_xyz_i_camera = Eigen::Vector3d::Zero(); // 相机坐标系目标点 }; // 子弹命中位置信息 struct HitPos { bool hit; Eigen::Vector3d pos; // 子弹在世界坐标系上的位置 }; // 子弹图像投影信息 struct HitCircle { bool hit; math::CircleF circle; // 子弹在图像上的投影圆 }; // 匹配代价评估 struct CaughtCost { bool caught; // 是否满足匹配条件 double cost; // 匹配代价(越小越好) }; // 子弹弹道物理模拟器 class ProjectileSimulator { public: ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t) : coorConverter{coorConverter},shoot_param{shoot_param} ,fire_t{fire_t} {} // 子弹在图像平面上的投影计算 auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle; // 计算在指定时间t的子弹位置 auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos; // 获取开火时间 auto get_fire_t() const -\u0026gt; double; private: const double g { 9.8 }; const long long fire_t; CoordinateTransformer* coorConverter; ShootParam shoot_param; }; // 子弹位置信息 struct IdPos { int id; Eigen::Vector3d pos; }; // 子弹投影圆信息 struct IdCircle { int id; math::CircleF circle; // 子弹在图像平面上的投影圆 }; // 子弹模拟器封装 struct IdProj { int id; ProjectileSimulator proj; // 子弹物理模拟器实例 }; // 自动瞄准误差校准(目前仅用来复现理想弹道) class AimCorrector { public: AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param); // 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆 auto get_circles(long long now_time) -\u0026gt; std::vector; auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0; long long last_fire_time = 0; }; cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z); FlaskPoint pos_to_map_point( const Eigen::Vector3d\u0026amp; pos, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ); // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN); } #endif // TRAJECTORY_VISUALIZER_HPP if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111111111111111111111111111111\u0026rdquo;); ++it; continue; } 现在这段代码的问题是没有进入这段代码\n根据编译错误信息，问题出现在头文件中第162行： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN);\n在类声明内部，不能使用作用域限定符。应该直接写函数声明。 但是，从你给出的代码中，我看到头文件中的第162行是： cpp auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;\n这看起来是正确的。然而，编译器报错信息却显示它看到了一个带有作用域限定符的版本。 这可能是因为编译器在编译时读取的头文件版本与你展示的版本不一致。请确保你修改并保存了头文件，并且编译器正在使用这个最新版本。 另外，错误信息中还提到： cpp\nerror: use of ‘auto tools::AimCorrector::update_bullet(long long int, long long int, long long int)’ before deduction of ‘auto’ 这是因为在实现文件中，函数定义使用了auto作为返回类型，但是函数定义在头文件中已经声明了返回类型为void，所以应该将实现文件中的函数定义改为返回void，或者确保返回类型一致。 在实现文件中，我们看到： cpp\nauto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void 这里使用了尾置返回类型，并且返回类型为void，这与头文件中的声明一致，所以没有问题。 但是，请注意，在头文件中，函数声明和类定义在同一文件中，而实现文件是分开编译的。如果头文件没有正确更新，编译器可能会使用旧的头文件，导致声明不匹配。 解决方案： 确保头文件已正确修改并保存。检查头文件中第162行，确保它是： cpp auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; 清理并重新编译。由于可能使用了旧的编译缓存，建议清理构建目录并重新编译。 如果问题仍然存在，检查是否有其他头文件包含了旧版本的头文件，或者头文件被多次包含导致宏重定义等问题。\n关于没有进入if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t())这段代码，可能是因为条件不满足。也就是说，当前时间now_time已经大于等于子弹的发射时间fire_t。这可能是由于你设置的发射时间延迟不够大，或者当前时间戳now_time增长过快。 在update_bullet函数中，你设置了子弹的发射时间为： cpp\ncurrent_time + eTime + 0.025 + COMMAND_TIMESPAN 注意，这里的eTime和COMMAND_TIMESPAN是long long类型，你传入的是毫秒吗？在Tracker中调用时，你传入的eTime和COMMAND_TIMESPAN是毫秒吗？如果不是，需要转换为毫秒。 另外，在draw_simulated_bullets函数中，你传入的now_time是当前时间，而子弹的发射时间是在当前时间的基础上加上了延迟。因此，当now_time小于发射时间时，子弹不会发射，所以不会进入那个条件分支。 如果你希望看到子弹发射，可以尝试增大延迟，或者确保在子弹发射后再调用get_circles。你也可以在update_bullet函数中打印调试信息，看看是否成功添加了子弹，以及子弹的发射时间是多少。 总结步骤： 修改头文件，确保类声明中的函数没有作用域限定符。 清理并重新编译整个项目。 检查时间单位，确保延迟参数的单位一致（都是毫秒）。 添加调试信息，打印子弹的发射时间和当前时间，以判断条件是否满足。\n如果编译问题解决了，但逻辑问题（没有进入if）仍然存在，那么请关注时间参数和延迟设置，确保子弹发射时间在当前时间之后足够长，以便在调用get_circles时，now_time仍然小于发射时间。 根据你提供的代码和问题描述，你已经解决了编译错误，但现在遇到了逻辑问题：程序没有进入 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) 这段代码。 问题分析\n原因： 子弹的发射时间 fire_t 已经等于或早于当前时间 now_time。 在你的代码中，子弹的发射时间计算为： cpp ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 0.025 + COMMAND_TIMESPAN)\n但是，这里的 eTime 和 COMMAND_TIMESPAN 是 long long 类型，而它们应该是毫秒值。如果 eTime 和 COMMAND_TIMESPAN 是秒（double类型），那么你需要将它们转换为毫秒： 修复方案\n1. 确保时间单位一致 所有时间应该使用相同的单位（毫秒）： cpp auto AimCorrector::update_bullet(long long current_time, double eTime_sec, double command_timespan_sec) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime_sec * 1000);long long command_timespan_ms = static_cast(command_timespan_sec * 1000);long long additional_delay = 25; // 0.025秒 = 25毫秒if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) { // 限制子弹数量// 计算发射时间（添加延迟）long long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms; std::cout \u0026laquo; \u0026ldquo;创建子弹: current_time=\u0026rdquo; \u0026laquo; current_time \u0026laquo; \u0026ldquo;, fire_time=\u0026rdquo; \u0026laquo; fire_time \u0026laquo; \u0026ldquo;, 延迟=\u0026rdquo; \u0026laquo; (eTime_ms + additional_delay + command_timespan_ms) \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; std::endl; bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n2. 修改调用方式 在Tracker中调用时，传入秒值： cpp // 在Tracker的reconstruction_choose_compensation函数中： tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_target_armor.position);long long draw_visual_now_time = chrono::time_point_castchrono::milliseconds( chrono::system_clock::now()).time_since_epoch().count();// 传入秒值，函数内部会转换为毫秒 tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, eTime, // 秒 COMMAND_TIMESPAN); // 秒\n3. 添加调试信息 在get_circles函数中添加详细的时间调试信息： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res; std::cout \u0026laquo; \u0026ldquo;\\n=== get_circles ===\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;当前时间(now_time): \u0026quot; \u0026laquo; now_time \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;子弹数量: \u0026quot; \u0026laquo; bullets.size() \u0026laquo; std::endl;int bullet_index = 0;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {long long fire_t = it-\u0026gt;proj.get_fire_t(); std::cout \u0026laquo; \u0026ldquo;子弹[\u0026rdquo; \u0026laquo; bullet_index \u0026laquo; \u0026ldquo;] - id: \u0026quot; \u0026laquo; it-\u0026gt;id \u0026laquo; \u0026ldquo;, fire_t: \u0026quot; \u0026laquo; fire_t \u0026laquo; \u0026ldquo;, now_time \u0026lt; fire_t? \u0026quot; \u0026laquo; (now_time \u0026lt; fire_t) \u0026laquo; \u0026ldquo;, 差值: \u0026quot; \u0026laquo; (fire_t - now_time) \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; std::endl;if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026ldquo;子弹[%d]还未发射，fire_t=%lld, now_time=%lld, 还有%lldms\u0026rdquo;, bullet_index, fire_t, now_time, (fire_t - now_time));++it; continue;} HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else { res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}++bullet_index;} std::cout \u0026laquo; \u0026ldquo;返回有效子弹数量: \u0026quot; \u0026laquo; res.size() \u0026laquo; std::endl;return res;}\n4. 修改draw_simulated_bullets函数 cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, double eTime_sec, double command_timespan_sec) { std::cout \u0026laquo; \u0026ldquo;\\n=== draw_simulated_bullets ===\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;现在时间: \u0026quot; \u0026laquo; now_time \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;eTime: \u0026quot; \u0026laquo; eTime_sec \u0026laquo; \u0026ldquo;s\u0026rdquo; \u0026laquo; std::endl; std::cout \u0026laquo; \u0026ldquo;COMMAND_TIMESPAN: \u0026quot; \u0026laquo; command_timespan_sec \u0026laquo; \u0026ldquo;s\u0026rdquo; \u0026laquo; std::endl; FlaskStream flask_aim; flask_aim.clear(); AimCorrector aim_corrector(coorConverter, shoot_param);// 更新子弹序列 aim_corrector.update_bullet(now_time, eTime_sec, command_timespan_sec);// 获取子弹 std::vector bullets = aim_corrector.get_circles(now_time); std::cout \u0026laquo; \u0026ldquo;获取到的可绘制子弹数量: \u0026quot; \u0026laquo; bullets.size() \u0026laquo; std::endl;for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img; std::cout \u0026laquo; \u0026ldquo;=== draw_simulated_bullets结束 ===\\n\u0026rdquo; \u0026laquo; std::endl;}\n5. 检查时间参数值 在Tracker中检查你的时间参数： cpp // 在Tracker构造函数中 cout \u0026laquo; \u0026ldquo;eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; \u0026quot; seconds\u0026rdquo; \u0026laquo; endl; // 应该是0.002左右 cout \u0026laquo; \u0026ldquo;COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; \u0026quot; seconds\u0026rdquo; \u0026laquo; endl; // 应该是0.11左右// 在调用draw_simulated_bullets之前 cout \u0026laquo; \u0026ldquo;调用draw_simulated_bullets前: now_time=\u0026rdquo; \u0026laquo; draw_visual_now_time \u0026laquo; \u0026ldquo;, eTime=\u0026rdquo; \u0026laquo; eTime \u0026laquo; \u0026ldquo;s, COMMAND_TIMESPAN=\u0026rdquo; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; \u0026ldquo;s\u0026rdquo; \u0026laquo; endl;\n关键检查点 时间单位转换：确保所有时间都是毫秒，或者正确转换 延迟计算：eTime + 0.025 + COMMAND_TIMESPAN 应该至少是125毫秒（如果eTime=2ms, COMMAND_TIMESPAN=110ms） 当前时间：确保draw_visual_now_time是当前时间的毫秒值\n最可能的问题是时间单位不匹配。如果 eTime 和 COMMAND_TIMESPAN 是秒（如0.002和0.11），那么你需要将它们转换为毫秒才能在 update_bullet 函数中使用。 #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { double k = 1; // 空气阻力系数 // 计算水平位移 double w = (t - this-\u0026gt;fire_t) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ++it; continue; } // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 200; // 发射间隔：200毫秒 // 将秒转换为毫秒 long long eTime_ms = static_cast(eTime * 1000); long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); long long additional_delay = 25; // 0.025秒 = 25毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() \u0026lt; AIM_CORRECTOR_BULLETS_MAX_SZ) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++, ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime_ms + additional_delay + command_timespan_ms) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::vector\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return this; } FlaskStream\u0026amp; FlaskStream::operator\u0026raquo;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param(\u0026ldquo;auto-aim.ec-simulator.shoot-interval\u0026rdquo;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026laquo; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026raquo; img; } } #ifndef TRAJECTORY_VISUALIZER_HPP #define TRAJECTORY_VISUALIZER_HPP #include \u0026ldquo;math.hpp\u0026rdquo; #include \u0026ldquo;CoorConverter.hpp\u0026rdquo; #include \u0026lt;opencv2/opencv.hpp\u0026gt; #include \u0026ldquo;GimbalPos.hpp\u0026rdquo; #include \u0026ldquo;ros/ros.h\u0026rdquo; namespace tools{ const int FLASK_MAP_WIDTH = 1000; // 定义调试地图的水平分辨率 const double FLASK_MAP_PETER_BY_BRIGHT = 1.; // 默认亮度系数 const int FLASK_MAP_MID_X = FLASK_MAP_WIDTH / 2; // 地图的水平中心点,用于坐标变换的参考原点 // 点绘制参数 struct FlaskPoint { FlaskPoint( const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ): pt(pt), color(color), radius(radius), thickness(thickness) {} cv::Point2d pt; // 圆心位置 cv::Scalar color; // 颜色 int radius; // 半径 int thickness; // 线宽 }; struct FlaskLine { FlaskLine( const std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt;\u0026amp; pt_pair, const cv::Scalar\u0026amp; color, const int\u0026amp; thickness ): pt_pair(pt_pair), color(color), thickness(thickness) {} std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt; pt_pair; cv::Scalar color; int thickness; }; // 文本绘制参数 struct FlaskText { FlaskText( const std::string\u0026amp; str, const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const double\u0026amp; scale ): str(str), pt(pt), color(color), scale(scale) {} std::string str; // 文本内容 cv::Point2d pt; // 文本位置 (左下角) cv::Scalar color; // 颜色 double scale; // 字体大小 }; / 绘制流管理器 @brief: 收集绘制命令: 通过重载的\u0026laquo;操作符接收各种绘制元素 批量执行绘制: 通过\u0026raquo;操作符将所有收集的命令绘制到图形上 命令管理: 可以清空所有收集的绘制命令\n/ class FlaskStream { public: FlaskStream\u0026amp; operator\u0026laquo;(const char str); FlaskStream\u0026amp; operator\u0026laquo;(const std::string\u0026amp; str); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskPoint\u0026amp; pt); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskLine\u0026amp; line); FlaskStream\u0026amp; operator\u0026laquo;(const std::vector\u0026amp; lines); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskText\u0026amp; text); FlaskStream\u0026amp; operator\u0026raquo;(cv::Mat\u0026amp; img); void clear(); private: std::vectorstd::string logs; std::vector pts; std::vector lines; std::vector texts; }; // 用于复现的瞄准参数 // 移植代码的时候将这段代码移植到自瞄那里 struct ShootParam { double v0 = 0.; // 子弹初速度 double aim_angle = 0.; // 发射仰角 // Eigen::Vector3d aim_xyz_i_barrel = Eigen::Vector3d::Zero(); // 枪管坐标系瞄准点 (没有什么作用) Eigen::Vector3d target_xyz_i_camera = Eigen::Vector3d::Zero(); // 相机坐标系目标点 }; // 子弹命中位置信息 struct HitPos { bool hit; Eigen::Vector3d pos; // 子弹在世界坐标系上的位置 }; // 子弹图像投影信息 struct HitCircle { bool hit; math::CircleF circle; // 子弹在图像上的投影圆 }; // 匹配代价评估 struct CaughtCost { bool caught; // 是否满足匹配条件 double cost; // 匹配代价(越小越好) }; // 子弹弹道物理模拟器 class ProjectileSimulator { public: ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t) : coorConverter{coorConverter},shoot_param{shoot_param} ,fire_t{fire_t} {} // 子弹在图像平面上的投影计算 auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle; // 计算在指定时间t的子弹位置 auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos; // 获取开火时间 auto get_fire_t() const -\u0026gt; double; private: const double g { 9.8 }; const long long fire_t; CoordinateTransformer* coorConverter; ShootParam shoot_param; }; // 子弹位置信息 struct IdPos { int id; Eigen::Vector3d pos; }; // 子弹投影圆信息 struct IdCircle { int id; math::CircleF circle; // 子弹在图像平面上的投影圆 }; // 子弹模拟器封装 struct IdProj { int id; ProjectileSimulator proj; // 子弹物理模拟器实例 }; // 自动瞄准误差校准(目前仅用来复现理想弹道) class AimCorrector { public: AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param); // 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆 auto get_circles(long long now_time) -\u0026gt; std::vector; auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0; long long last_fire_time = 0; }; cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z); FlaskPoint pos_to_map_point( const Eigen::Vector3d\u0026amp; pos, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ); // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN); } #endif // TRAJECTORY_VISUALIZER_HPP 现在的问题是程序可以运行了,但是子弹没有打印重投影出来\n我们注意到在代码中，子弹的绘制是通过draw_simulated_bullets函数进行的，但是子弹没有显示出来。可能的原因有： 子弹的发射时间计算有误，导致子弹在创建后立即被判断为已发射（即now_time可能大于等于fire_t），从而进入了后续处理，但可能因为弹道计算问题导致绘制的位置不对，或者没有绘制。 子弹的弹道计算有误，导致投影到图像上的位置超出了图像范围，或者半径太小看不见。 绘制子弹的代码没有正确执行，可能因为get_circles返回的子弹列表为空，或者绘制函数没有被正确调用。\n我们需要逐步排查。 首先，在get_circles函数中，我们添加一些调试信息，看看子弹是否被正确创建，以及子弹的发射时间和当前时间的关系。 修改get_circles函数，添加打印信息： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{// 初始化结果向量 std::vector res;// 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表ROS_INFO(\u0026ldquo;Bullets list size: %zu\u0026rdquo;, bullets.size());for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {// 检查子弹是否已发射// 当前图像时间 \u0026lt; 子弹发射时间// 是 -\u0026gt; 子弹还未发射,跳过// 否 -\u0026gt; 子弹已发射,继续处理ROS_INFO(\u0026ldquo;Bullet id: %d, fire_t: %lld, now_time: %lld\u0026rdquo;, it-\u0026gt;id, it-\u0026gt;proj.get_fire_t(), now_time);if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) {ROS_WARN(\u0026ldquo;Bullet not fired yet, skipping.\u0026rdquo;);++it; continue;}// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中 -\u0026gt; 已击中删除if (hit_circle.hit) {ROS_INFO(\u0026ldquo;Bullet hit, erasing.\u0026rdquo;); it = this-\u0026gt;bullets.erase(it);} else {// 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器ROS_INFO(\u0026ldquo;Bullet not hit, adding to result.\u0026rdquo;); res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n同时，在update_bullet函数中也添加一些调试信息，确保子弹被创建： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000);long long additional_delay = 25; // 0.025秒 = 25毫秒ROS_INFO(\u0026ldquo;update_bullet: current_time=%lld, last_fire_time=%lld, interval=%lld\u0026rdquo;, current_time, last_fire_time, fire_interval);if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {ROS_INFO(\u0026ldquo;Condition met, trying to add bullet.\u0026rdquo;);if (bullets.size() \u0026lt; AIM_CORRECTOR_BULLETS_MAX_SZ) { // 最多显示10颗子弹long long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms;ROS_INFO(\u0026ldquo;Adding bullet with fire_time: %lld (current_time=%lld, delay=%lld)\u0026rdquo;, fire_time, current_time, eTime_ms+additional_delay+command_timespan_ms); bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n另外，在draw_simulated_bullets函数中，添加打印信息，看看获取到的子弹数量： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param);// 更新子弹序列// 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);ROS_INFO(\u0026ldquo;Number of bullets to draw: %zu\u0026rdquo;, bullets.size());for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img;}\n运行程序，观察日志输出。根据日志输出，我们可以判断： 子弹是否被创建（update_bullet中是否打印添加子弹的信息） 子弹的发射时间是否正确（fire_time是否大于当前时间now_time） 在get_circles中，子弹是否因为未发射而被跳过，或者是否被判断为击中而删除。\n如果子弹被创建，但是get_circles返回的子弹列表为空，那么可能是以下原因： 子弹的发射时间设置得太晚，导致now_time \u0026lt; fire_t始终为真，子弹一直处于未发射状态，被跳过。 子弹的发射时间设置得太早，导致子弹一创建就被判断为已发射，但可能因为弹道计算问题，子弹立即被判断为击中（hit为真）而被删除。\n根据你的描述，我们之前的问题是没有进入if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t())，现在可能已经进入了，但是子弹没有显示，所以我们需要进一步检查弹道计算和击中判断。 在ProjectileSimulator::get_pos_by_t函数中，我们计算了子弹的位置，并判断是否击中。这里的击中判断是：bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm()，即子弹在枪管坐标系中的水平距离是否大于等于目标点的水平距离。如果是，则认为击中。 但是，请注意，这个判断可能过于严格，因为子弹可能一开始就大于目标距离（比如目标距离为0，或者子弹的初始位置就不对）。此外，弹道计算可能有问题，导致子弹位置计算错误。 我们可以添加调试信息，打印子弹位置和目标位置，以及击中判断的结果。 修改ProjectileSimulator::get_pos_by_t函数： cpp\nauto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {double k = 1; // 空气阻力系数// 计算水平位移double w = (t - this-\u0026gt;fire_t) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle);// 计算高度double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w/ (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))+ this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k/ k;// 弹道轨迹仅取决于目标点(理想弹道)const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 计算基准方向const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized();const Eigen::Vector3d h_norm = { 0., 0., 1. };const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm;const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0),bullet_xyz_i_barrel(1, 0) };const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0),target_xyz_i_barrel(1, 0) };bool hit = bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm();ROS_INFO(\u0026ldquo;ProjectileSimulator: t=%f, fire_t=%lld, bullet_xy_i_barrel.norm()=%f, target_xy_i_barrel.norm()=%f, hit=%d\u0026rdquo;, t, fire_t, bullet_xy_i_barrel.norm(), target_xy_i_barrel.norm(), hit);return HitPos { hit, bullet_xyz_i_camera};} 同时，在get_circle_by_t函数中，我们也可以打印投影后的像素坐标，看看是否在图像范围内： cpp auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;map2Cam(bullet.pos);// 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized();// 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos);double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型ROS_INFO(\u0026ldquo;ProjectileSimulator: center_xy_u=(%f,%f), radius=%f\u0026rdquo;, center_xy_u.x, center_xy_u.y, radius);return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) };}\n运行程序，观察日志输出。通过日志，我们可以判断： 子弹在枪管坐标系中的水平距离和目标水平距离，以及击中判断。 子弹投影到图像上的像素坐标和半径。\n如果子弹的像素坐标不在图像范围内（比如负数或大于图像宽度/高度），那么绘制可能不会显示。如果半径为0或负数，也不会显示。 另外，请注意，在draw_simulated_bullets函数中，我们创建了一个局部的AimCorrector对象，然后调用update_bullet和get_circles。但是，每次调用draw_simulated_bullets时，都会创建一个新的AimCorrector对象，这意味着之前的状态（子弹列表）不会被保留。也就是说，每一帧都是重新开始模拟子弹，这可能导致子弹还没有发射（因为发射延迟）就被丢弃了（因为局部对象在函数结束时销毁）。 你可能需要将AimCorrector对象作为全局变量或者作为某个对象的成员变量，以便在多次调用draw_simulated_bullets时保持子弹的状态。 根据你的代码，draw_simulated_bullets是在Tracker的每一帧中被调用的，而Tracker是一个持续运行的对象。因此，你应该将AimCorrector作为Tracker的成员变量，而不是在每次调用draw_simulated_bullets时都创建一个新的。 修改Tracker类，添加一个成员变量： cpp class Tracker{private:// \u0026hellip; 其他成员变量 tools::AimCorrector aim_corrector; // 子弹模拟器};\n并在Tracker的构造函数中初始化它（可能需要传递参数）。然后在draw_simulated_bullets函数中，使用这个成员变量，而不是局部变量。 但是，请注意，draw_simulated_bullets函数目前是独立函数，它创建了一个局部的AimCorrector。我们需要修改这个设计。 由于时间关系，我们先尝试在draw_simulated_bullets函数中增加子弹状态的持续性，我们可以将AimCorrector声明为静态变量，这样它的生命周期会持续到程序结束，但这样会导致所有调用共享同一个AimCorrector实例，可能会产生冲突。 更好的方法是，将AimCorrector作为Tracker的成员，然后在Tracker的reconstruction_choose_compensation函数中调用aim_corrector.update_bullet和aim_corrector.get_circles，并将子弹绘制到图像上。 由于修改较大，我们这里先采用静态局部变量的方法，看看子弹是否能够持续存在并显示。修改draw_simulated_bullets函数： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){static AimCorrector aim_corrector(coorConverter, shoot_param); // 静态变量，保持状态 FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear();// 更新子弹序列// 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);ROS_INFO(\u0026ldquo;Number of bullets to draw: %zu\u0026rdquo;, bullets.size());for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img;}\n注意：静态局部变量只会在第一次调用时初始化，后续调用都会使用同一个实例。这样，子弹的状态就会在多次调用中保持。 但是，这种方法有一个问题：如果coorConverter指针发生变化，或者shoot_param发生变化，那么静态变量在后续调用中就不会更新这些参数。所以，我们需要在每次调用时更新aim_corrector中的这些参数。我们可以在AimCorrector类中添加设置函数，或者修改update_bullet函数，同时更新这些参数。 考虑到shoot_param可能会变化，我们可以将shoot_param作为update_bullet的参数，而不是在构造函数中传入。但是，这样需要修改AimCorrector的设计。 由于时间有限，我们先假设shoot_param不变，而coorConverter也不变。如果变化，我们需要重新设计。 另一种方法是，将AimCorrector的创建放在Tracker的成员函数中，作为Tracker的成员变量，这样Tracker可以控制其生命周期，并在每次需要时更新参数。 由于修改较大，我们先尝试静态局部变量的方法，看看子弹是否能显示。如果子弹能够显示，说明问题在于之前每次调用都创建新的AimCorrector导致状态丢失。 如果仍然不能显示，请根据日志输出进一步分析。 如果日志显示子弹被创建，但是get_circles返回的子弹列表为空，那么可能是子弹在第一次调用get_circles时就被判断为击中而删除了。我们可以通过日志查看击中判断的情况。 请运行添加了日志的程序，并观察输出。 根据你提供的代码，我发现几个关键问题导致子弹没有被重投影出来。让我逐一分析并提供解决方案： 主要问题分析\n1. 子弹立即被判定为击中 在 get_pos_by_t 函数中，击中判断逻辑有问题： cpp\nreturn HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(), bullet_xyz_i_camera }; 问题：子弹创建后立即判断，水平距离可能已经大于等于目标距离，导致子弹立即被标记为\u0026quot;击中\u0026quot;而被删除。 2. 时间单位问题 get_pos_by_t 函数参数 t 是毫秒，但弹道计算中直接使用 (t - this-\u0026gt;fire_t) 作为秒来计算，导致时间差错误。 3. 弹道公式错误 空气阻力系数 k=1 不合理，而且弹道公式复杂且可能有误。 4. 缺少调试信息 无法知道子弹的状态和位置。 修复方案\n1. 修正弹道模型（简化无空气阻力） cpp auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t_millis) const -\u0026gt; HitPos {// 将毫秒转换为秒double delta_t = (t_millis - this-\u0026gt;fire_t) / 1000.0;if (delta_t \u0026lt;= 0) {// 子弹还未发射，返回初始位置（枪口位置） Eigen::Vector3d gun_pos = Eigen::Vector3d::Zero(); // 枪口在枪管系中的位置 Eigen::Vector3d bullet_xyz_i_camera = this-\u0026gt;coorConverter-\u0026gt;gun2Cam(gun_pos);return HitPos { false, bullet_xyz_i_camera };}// 简化的弹道模型（无空气阻力）double v0 = this-\u0026gt;shoot_param.v0;double theta = this-\u0026gt;shoot_param.aim_angle;// 水平位移double x = v0 * cos(theta) * delta_t;// 垂直位移（考虑重力）double z = v0 * sin(theta) * delta_t - 0.5 * this-\u0026gt;g * delta_t * delta_t;// 获取目标在枪管系中的方向const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 水平方向（目标方向） Eigen::Vector2d target_dir_2d(target_xyz_i_barrel.x(), target_xyz_i_barrel.y());if (target_dir_2d.norm() \u0026lt; 1e-6) { target_dir_2d = Eigen::Vector2d(1, 0); // 默认向前} target_dir_2d.normalize();// 子弹在枪管系中的位置 Eigen::Vector3d bullet_xyz_i_barrel( target_dir_2d.x() * x, target_dir_2d.y() * x, z);// 转换到相机系 Eigen::Vector3d bullet_xyz_i_camera = this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);// 击中判断：子弹在枪管系中的水平距离是否超过目标距离double bullet_distance = bullet_xyz_i_barrel.head\u0026lt;2\u0026gt;().norm();double target_distance = target_xyz_i_barrel.head\u0026lt;2\u0026gt;().norm();bool hit = (bullet_distance \u0026gt;= target_distance) \u0026amp;\u0026amp; (delta_t \u0026gt; 0.1); // 添加最小飞行时间return HitPos { hit, bullet_xyz_i_camera };}\n2. 添加调试输出 cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;ROS_INFO(\u0026ldquo;get_circles: now_time=%lld, bullet_count=%zu\u0026rdquo;, now_time, bullets.size());int index = 0;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {double fire_t = it-\u0026gt;proj.get_fire_t();ROS_INFO(\u0026rdquo; Bullet[%d]: id=%d, fire_t=%.0f, now_time=%lld, delta=%.0fms\u0026rdquo;, index, it-\u0026gt;id, fire_t, now_time, now_time - fire_t);if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026rdquo; Not fired yet\u0026rdquo;);++it; ++index;continue;} HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);if (hit_circle.hit) {ROS_INFO(\u0026rdquo; Hit! Removing bullet\u0026rdquo;); it = this-\u0026gt;bullets.erase(it);} else {ROS_INFO(\u0026rdquo; Not hit, drawing at (%.1f, %.1f) radius=%.1f\u0026rdquo;, hit_circle.circle.center.x, hit_circle.circle.center.y, hit_circle.circle.r); res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}++index;}ROS_INFO(\u0026rdquo; Returning %zu bullets to draw\u0026rdquo;, res.size());return res;}\n3. 修正update_bullet中的时间延迟 cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000);long long additional_delay = 25; // 0.025秒 = 25毫秒// 计算总延迟long long total_delay = eTime_ms + additional_delay + command_timespan_ms;ROS_INFO(\u0026ldquo;update_bullet: current_time=%lld, last_fire_time=%lld, total_delay=%lldms\u0026rdquo;, current_time, last_fire_time, total_delay);if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) { // 减少数量便于观察long long fire_time = current_time + total_delay;ROS_INFO(\u0026rdquo; Creating bullet id=%d, fire_time=%lld (current_time=%lld + delay=%lld)\u0026rdquo;, next_id, fire_time, current_time, total_delay); bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n4. 在draw_simulated_bullets中添加参数检查 cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) {ROS_INFO(\u0026quot;=== draw_simulated_bullets ===\u0026rdquo;);ROS_INFO(\u0026ldquo;now_time: %lld\u0026rdquo;, now_time);ROS_INFO(\u0026ldquo;shoot_param: v0=%.2f, aim_angle=%.4f rad (%.2f deg)\u0026rdquo;, shoot_param.v0, shoot_param.aim_angle, shoot_param.aim_angle * 180.0 / M_PI);ROS_INFO(\u0026ldquo;target_xyz_i_camera: [%.3f, %.3f, %.3f]\u0026rdquo;, shoot_param.target_xyz_i_camera.x(), shoot_param.target_xyz_i_camera.y(), shoot_param.target_xyz_i_camera.z()); FlaskStream flask_aim; flask_aim.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter, shoot_param); aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);ROS_INFO(\u0026ldquo;Bullets to draw: %zu\u0026rdquo;, bullets.size());for (auto\u0026amp; bullet: bullets) {// 检查坐标是否在图像范围内if (bullet.circle.center.x \u0026gt;= 0 \u0026amp;\u0026amp; bullet.circle.center.x \u0026lt; img.cols \u0026amp;\u0026amp; bullet.circle.center.y \u0026gt;= 0 \u0026amp;\u0026amp; bullet.circle.center.y \u0026lt; img.rows) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);ROS_INFO(\u0026rdquo; Drawing bullet %d at (%.1f, %.1f)\u0026rdquo;, bullet.id, bullet.circle.center.x, bullet.circle.center.y);} else {ROS_WARN(\u0026rdquo; Bullet %d out of bounds: (%.1f, %.1f), image size: %dx%d\u0026rdquo;, bullet.id, bullet.circle.center.x, bullet.circle.center.y, img.cols, img.rows);}} flask_aim \u0026raquo; img;ROS_INFO(\u0026quot;=== End draw_simulated_bullets ===\\n\u0026rdquo;);}\n5. 确保ProjectileSimulator::get_fire_t返回正确类型 cpp\nauto ProjectileSimulator::get_fire_t() const -\u0026gt; double {return static_cast(this-\u0026gt;fire_t); // 确保返回double} 关键测试点 检查时间延迟是否正确： eTime 应该是秒（如 0.002） COMMAND_TIMESPAN 应该是秒（如 0.11） 总延迟应该约为 137ms (2+25+110) 检查目标坐标： shoot_param.target_xyz_i_camera 应该在相机系中有合理的值 如果目标在3米外，x坐标应该在3.0左右 检查子弹参数： 子弹速度 v0 应该合理（如 20-30 m/s） 瞄准角度 aim_angle 应该合理（正角度表示向上）\n快速诊断 添加以下代码到Tracker中，检查传入参数： cpp // 在调用draw_simulated_bullets之前ROS_INFO(\u0026ldquo;Calling draw_simulated_bullets with:\u0026rdquo;);ROS_INFO(\u0026rdquo; BulletVector: %.2f m/s\u0026rdquo;, BulletVector);ROS_INFO(\u0026rdquo; Pitch angle: %.4f rad (%.2f deg)\u0026rdquo;, m_target_pose.pitch, m_target_pose.pitch * 180/M_PI);ROS_INFO(\u0026rdquo; Target position in map: (%.3f, %.3f, %.3f)\u0026rdquo;, abs_target_armor.position.x(), abs_target_armor.position.y(), abs_target_armor.position.z()); Eigen::Vector3d target_in_cam = coorConverter-\u0026gt;map2Cam(abs_target_armor.position);ROS_INFO(\u0026rdquo; Target in camera: (%.3f, %.3f, %.3f)\u0026rdquo;, target_in_cam.x(), target_in_cam.y(), target_in_cam.z()); tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = target_in_cam;\n主要问题很可能是： 子弹立即被判断为击中（修改击中判断逻辑） 弹道计算错误（使用简化弹道模型） 时间单位不匹配（确保毫秒/秒转换正确）\n建议先用简化弹道模型，确保基础功能正常后再优化。 #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { ROS_WARN(\u0026ldquo;22222222222222222222222222222222\u0026rdquo;); HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { ROS_WARN(\u0026ldquo;33333333333333333333333333333333333333333333333\u0026rdquo;); // 将毫秒转换成毫秒 double t_s = static_cast(t * 0.001); double fire_t_s = static_cast(this-\u0026gt;fire_t * 0.001); double k = 1; // 空气阻力系数 // 计算水平位移 double w = (t_s - fire_t_s) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; cout \u0026laquo; \u0026ldquo;w: \u0026quot; \u0026laquo; w \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;h: \u0026quot; \u0026laquo; h \u0026laquo; endl; ROS_WARN(\u0026ldquo;444444444444444444444444444444444444\u0026rdquo;); // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111\u0026rdquo;); ++it; continue; } ROS_WARN(\u0026ldquo;66666666666666666666666666666666\u0026rdquo;); // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 200; // 发射间隔：200毫秒 // 将秒转换为毫秒 long long eTime_ms = static_cast(eTime * 1000); long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); long long additional_delay = 25; // 0.025秒 = 25毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() \u0026lt; AIM_CORRECTOR_BULLETS_MAX_SZ) { // 最多显示10颗子弹 bullets.push_back(IdProj { next_id++, ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime_ms + additional_delay + command_timespan_ms) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::vector\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026raquo;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param(\u0026ldquo;auto-aim.ec-simulator.shoot-interval\u0026rdquo;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026laquo; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026raquo; img; } } if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111\u0026rdquo;); ++it; continue; } 发现这段代码执行不到continue的下面,也就是说没有打印66666666666666666666，为什么?\n根据你的代码和调试信息，你已经解决了子弹列表为空的问题，但现在发现程序进入了if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t())这个条件，然后执行了continue，所以没有打印后面的\u0026quot;6666666666\u0026rdquo;。这意味着当前时间now_time小于子弹的发射时间fire_t，所以子弹还没有发射，因此跳过了后续处理。 为什么now_time会小于fire_t呢？让我们分析一下： 在update_bullet中，你创建子弹时传入的发射时间（fire_t）为： cpp\ncurrent_time + eTime_ms + additional_delay + command_timespan_ms 这意味着你给子弹设置了一个未来的发射时间。然后，在get_circles中，你用当前时间now_time（注意：这个now_time就是调用draw_simulated_bullets时传入的now_time，与创建子弹时的current_time是同一个值）去和子弹的发射时间比较。 由于发射时间被设置为未来（当前时间加上延迟），所以now_time（即当前时间）小于fire_t（未来时间），因此子弹被判定为未发射，跳过了后续的弹道计算和绘制。 但是，你希望的是：子弹在创建后，经过一段延迟（发射时间）才被发射，而在发射之后，你希望绘制出子弹的弹道。 然而，在draw_simulated_bullets函数中，你创建子弹和检查子弹状态使用的是同一个时间now_time（即当前帧的时间）。这样，在同一帧中，你创建了子弹（设置发射时间为未来），然后立即检查子弹状态，由于子弹的发射时间还在未来，所以它不会被绘制。 你想要的效果可能是：子弹在未来的某个时间点发射，然后随着时间的推移，在后续的帧中绘制出子弹的飞行轨迹。但是，在当前的实现中，draw_simulated_bullets每次被调用时都会创建一个新的AimCorrector对象（局部变量），然后更新子弹（可能创建新的子弹），并立即获取当前帧的子弹状态。由于子弹是同一帧创建的，发射时间在未来，所以不会被绘制。 为了看到子弹的飞行轨迹，你需要让AimCorrector对象持续存在，而不是每次调用draw_simulated_bullets都新建一个。这样，子弹列表才能跨帧保持。然后，随着时间的推移，子弹的发射时间会达到（即now_time会大于等于fire_t），然后子弹就会被绘制。 所以，问题在于你的AimCorrector对象是局部变量，每次调用draw_simulated_bullets都会新建，子弹列表被重置。你应该将AimCorrector对象作为类成员变量，在Tracker中持续存在，这样子弹才能跨帧存在。 但是，从你的代码来看，draw_simulated_bullets函数是静态的，每次调用都会新建AimCorrector，然后更新子弹（创建子弹）并获取子弹状态。这只能显示在同一帧内创建的子弹，而且由于发射时间在未来，所以同一帧内不会绘制。 为了测试，你可以暂时将子弹的发射时间设置为当前时间（即不添加延迟），看看子弹是否会被绘制。或者，你可以修改AimCorrector，让它成为一个持续存在的对象（例如，在Tracker中作为成员变量），这样子弹就可以跨帧存在。 不过，从你的使用场景来看，你可能是想在同一帧内模拟子弹的飞行轨迹（比如，从枪口到目标点的弹道）。如果是这样，那么你不需要延迟发射，而应该让子弹的发射时间就是当前时间，然后计算从当前时间开始，经过一段时间后的子弹位置（比如，每帧计算子弹从发射到当前帧的时间差，然后计算位置）。但是，你的代码中，子弹的发射时间被设置为未来（当前时间+延迟），而计算子弹位置时使用的是当前时间，这会导致子弹还没有发射（因为当前时间小于发射时间），所以你可能需要重新考虑时间逻辑。 根据你的需求，我建议： 如果你想要模拟的是子弹从发射到击中目标的整个过程，那么你应该让子弹的发射时间就是当前时间（即不添加延迟），然后计算从发射到当前时间（可能是未来某个时间）的子弹位置。这样，在子弹发射后，随着时间的推移，子弹位置会不断更新。 如果你想要模拟的是子弹在未来的发射（比如，考虑电控延迟），那么你应该将子弹的发射时间设置为未来，然后在未来的时间点（当now_time大于等于fire_t）才开始计算子弹位置。\n但是，在每一帧中，你都需要根据当前的时间now_time来计算子弹的位置。所以，在get_circles函数中，只有当now_time大于等于fire_t时，才计算子弹位置（即子弹已经发射）。否则，子弹还没有发射，不进行绘制。 现在，你的代码中，子弹的发射时间被设置为未来（current_time + 延迟），而draw_simulated_bullets被调用时传入的now_time就是current_time（同一帧的时间），所以now_time小于fire_t，子弹不会发射。 为了看到效果，你可以尝试将发射时间设置为当前时间（即不加延迟）： cpp\nProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time) // 不加延迟 这样，在同一帧中，now_time等于fire_t，条件now_time \u0026lt; fire_t为假，就会进入弹道计算和绘制。 或者，如果你希望延迟发射，那么你需要让draw_simulated_bullets在未来的某一帧被调用（即传入的now_time大于子弹的发射时间）。但是，由于你的AimCorrector对象是局部变量，每次调用都会新建，子弹列表不会保存到下一帧。所以，你需要将AimCorrector对象持久化。 根据你的需求，我建议修改代码结构，将AimCorrector作为Tracker的成员变量，这样子弹列表可以跨帧存在。然后在每一帧的Tracker::Track()函数中，调用draw_simulated_bullets时，传入当前时间，并更新子弹状态。 但是，从你提供的代码中，我看到draw_simulated_bullets是静态函数，而且没有使用持久化的AimCorrector。所以，如果你只是临时测试弹道计算是否正确，可以先将发射时间设置为当前时间（不加延迟），看看弹道绘制是否正确。 另外，你的弹道计算函数get_pos_by_t中有一些潜在问题： 时间单位：t和fire_t是毫秒，你将其转换为秒，这是正确的。 弹道公式：你使用了空气阻力模型，但是空气阻力系数k=1可能过大，导致弹道异常。你可以先使用无空气阻力的模型测试，或者调整k值。 击中判断：bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm()，这个条件可能在子弹发射后很快就满足（特别是当目标距离很近时），导致子弹很快被标记为击中并从列表中删除。\n所以，你可以先简化问题，使用无空气阻力模型，并确保目标距离足够远，以便观察弹道。 综上所述，要解决当前问题，你可以： 修改发射时间，暂时去掉延迟，使用当前时间作为发射时间，以便在同一帧中看到子弹。 将AimCorrector对象持久化，使其跨帧存在，这样子弹可以在未来的帧中被绘制。\n根据你的实际情况选择修改方案。如果你只是测试弹道计算，可以选择方案1。如果你需要模拟真实的延迟发射，并希望看到子弹飞行过程，那么需要方案2。 由于你没有提供Tracker中调用draw_simulated_bullets的完整上下文，我假设你是在每一帧中调用这个函数。那么，为了方案2，你需要在Tracker类中添加一个成员变量： cpp\ntools::AimCorrector aim_corrector; 然后在构造函数中初始化它（需要传入coorConverter和shoot_param）。但是，shoot_param是每一帧都可能变化的（因为目标位置和云台角度在变），所以你可能需要在每一帧更新aim_corrector的shoot_param。或者，你可以将aim_corrector作为局部变量，但是使用静态变量来保存子弹列表（不推荐，因为这样会使得子弹列表在多次调用之间共享，可能不是你想要的）。 鉴于你的代码结构，我建议先使用方案1测试弹道计算是否正确，然后再考虑如何将子弹模拟与你的跟踪系统整合。 修改update_bullet函数中创建子弹的部分，去掉延迟： cpp bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time) // 使用当前时间，不加延迟});\n这样，子弹的发射时间就是当前时间，那么在get_circles中，now_time（同一帧的时间）等于fire_t，条件now_time \u0026lt; fire_t为假，就会执行后面的弹道计算和绘制。 试试看，这样应该能打印出\u0026quot;66666666666666666666\u0026rdquo;，并进入弹道计算。 根据你的调试信息，程序进入了 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) 这个条件判断，但是没有打印\u0026quot;6666666666\u0026rdquo;，这说明子弹的发射时间 fire_t 大于当前时间 now_time，所以子弹还没有发射，直接 continue 跳过了。 让我分析一下原因： 问题分析\n1. 时间延迟过大 在你的代码中，子弹的发射时间被设置为： cpp current_time + eTime_ms + additional_delay + command_timespan_ms\n假设： current_time = 当前时间（毫秒） eTime = 0.002秒 = 2毫秒 additional_delay = 25毫秒 COMMAND_TIMESPAN = 0.11秒 = 110毫秒\n总延迟 = 2 + 25 + 110 = 137毫秒 这意味着： 你创建子弹时，设置它的发射时间是 当前时间 + 137毫秒 在创建子弹的同一帧，你立即调用 get_circles(now_time) 由于 now_time 小于 fire_t（差137毫秒），所以子弹被认为是\u0026quot;未发射\u0026quot;状态\n2. 实时性考虑 这个逻辑在实时系统中是合理的：子弹需要时间才能发射出去（击发延迟、电控延迟等）。 但是，对于弹道可视化，你可能希望看到的是： 子弹已经发射，正在飞行中 或者你想预测未来的弹道轨迹 解决方案\n方案1：调整时间逻辑（适用于实时弹道显示） 如果你想显示已经发射的子弹，可以修改时间判断： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {// 方法1：稍微放宽条件，允许子弹\u0026quot;提前\u0026quot;显示long long fire_t = it-\u0026gt;proj.get_fire_t();// 如果子弹将在未来50ms内发射，也显示它（作为\u0026quot;即将发射\u0026quot;的视觉效果）if (now_time \u0026lt; fire_t - 50) {ROS_WARN(\u0026ldquo;子弹未发射: now_time=%lld, fire_t=%lld, 差值=%lldms\u0026rdquo;, now_time, fire_t, fire_t - now_time);++it; continue;}ROS_WARN(\u0026ldquo;子弹已发射或即将发射\u0026rdquo;);// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else { res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n方案2：修改子弹创建逻辑（测试用） 如果你想立即看到子弹，可以去掉延迟： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 200; // 发射间隔：200毫秒if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() \u0026lt; 10) {// 测试：使用当前时间作为发射时间，立即显示子弹long long fire_time = current_time; // 去掉延迟 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;ROS_WARN(\u0026ldquo;创建子弹: id=%d, fire_time=%lld (无延迟)\u0026rdquo;, next_id-1, fire_time);}}}\n方案3：最佳方案：分阶段处理 这是最合理的方法，区分子弹的\u0026quot;发射前\u0026quot;和\u0026quot;飞行中\u0026quot;状态： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {long long fire_t = it-\u0026gt;proj.get_fire_t();// 子弹发射前：显示在枪口位置if (now_time \u0026lt; fire_t) {// 可以显示\u0026quot;准备发射\u0026quot;的标记，比如在枪口显示一个小点// 或者跳过不显示ROS_WARN(\u0026ldquo;子弹[%d]准备发射: 还有%lldms\u0026rdquo;, it-\u0026gt;id, fire_t - now_time);++it;continue;}// 子弹已发射：计算弹道并显示ROS_WARN(\u0026ldquo;子弹[%d]已发射: 已飞行%lldms\u0026rdquo;, it-\u0026gt;id, now_time - fire_t); HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);if (hit_circle.hit) {ROS_WARN(\u0026ldquo;子弹[%d]已击中目标\u0026rdquo;, it-\u0026gt;id); it = this-\u0026gt;bullets.erase(it);} else { res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n方案4：在Tracker中调整调用时机 如果你希望看到预测的弹道，而不是实际发射的子弹，可以在Tracker中使用未来的时间： cpp // 在Tracker的reconstruction_choose_compensation函数中：long long draw_visual_now_time = chrono::time_point_castchrono::milliseconds( chrono::system_clock::now()).time_since_epoch().count();// 使用未来的时间（比如100ms后）来显示弹道预测long long future_time = draw_visual_now_time + 100; // 100毫秒后 tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, future_time, // 传入未来时间static_cast(eTime * 1000),static_cast(COMMAND_TIMESPAN * 1000));\n快速测试方案 为了立即看到效果，最简单的方法是： 修改 update_bullet 函数： cpp\n// 将发射时间设置为当前时间，去掉延迟long long fire_time = current_time; // 而不是 current_time + 延迟 或者修改 get_circles 函数： cpp\n// 放宽条件，让子弹\u0026quot;提前\u0026quot;显示if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t() - 50) { // 50ms的容差++it;continue;} 根本原因总结 你的代码逻辑是正确的，但不符合你的期望： 实际逻辑：子弹有发射延迟（137ms），所以在创建后的137ms内，子弹处于\u0026quot;未发射\u0026quot;状态 你的期望：子弹创建后立即显示飞行轨迹\n选择哪个方案取决于你想要的效果： 实时弹道模拟：显示实际发射的子弹轨迹 → 保持当前逻辑，等待137ms后就能看到子弹 弹道预测显示：显示如果现在发射，子弹会如何飞行 → 使用方案4（传入未来时间） 即时测试：立即看到弹道效果 → 使用方案2（去掉延迟）\n建议先使用方案2进行测试，确认弹道计算正确后，再根据实际需求选择合适的方案。 #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { ROS_WARN(\u0026ldquo;22222222222222222222222222222222\u0026rdquo;); HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { ROS_WARN(\u0026ldquo;33333333333333333333333333333333333333333333333\u0026rdquo;); // 将毫秒转换成毫秒 double t_s = static_cast(t * 0.001); double fire_t_s = static_cast(this-\u0026gt;fire_t * 0.001); double k = 0.1; // 空气阻力系数 // 计算水平位移 double w = (t_s - fire_t_s) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; cout \u0026laquo; \u0026ldquo;w: \u0026quot; \u0026laquo; w \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;h: \u0026quot; \u0026laquo; h \u0026laquo; endl; ROS_WARN(\u0026ldquo;444444444444444444444444444444444444\u0026rdquo;); // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111\u0026rdquo;); ++it; continue; } ROS_WARN(\u0026ldquo;66666666666666666666666666666666\u0026rdquo;); // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 50; // 发射间隔：50毫秒 // 将秒转换为毫秒 long long eTime_ms = static_cast(eTime * 1000); long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); long long additional_delay = 25; // 0.025秒 = 25毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) { bullets.push_back(IdProj { next_id++, ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime_ms + additional_delay + command_timespan_ms) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::vector\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026raquo;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param(\u0026ldquo;auto-aim.ec-simulator.shoot-interval\u0026rdquo;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026laquo; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026raquo; img; } } 我现在是这么实现的,每50ms往发射队列放一颗子弹,表示视觉将要发射子弹,接着在各种延迟(曝光,开火延迟,自瞄程序运行耗时等影响下) 作为子弹的发射时间， 当当前时间小于子弹发射时间的时候表示子弹已经发射，当当前时间小于子弹发射时间的时候,表示子弹还没有发射,当当前时间超过子弹发射时间的时候表示子弹已经发射,此时开始计算这颗子弹的坐标\ne1766498503009 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.009312748]: 111111111111111111111 now_time1766498503032 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.032824389]: 111111111111111111111 now_time1766498503058 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.058838090]: 111111111111111111111 now_time1766498503082 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.082508863]: 111111111111111111111 now_time1766498503107 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.107763172]: 111111111111111111111 now_time1766498503133 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.133792247]: 111111111111111111111 now_time1766498503159 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.159381971]: 111111111111111111111 now_time1766498503182 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.182102586]: 111111111111111111111 ^Cnow_time1766498503207 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.207126988]: 111111111111111111111 now_time1766498503232 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.232881250]: 111111111111111111111 now_time1766498503257 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [ WARN] [1766498503.257718857]: 111111111111111111111 [tracker-5] killing on exit [identify-4] killing on exit [serial-3] killing on exit [hikcamera-2] killing on exit now_time1766498503282 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 now_time1766498503306 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 now_time1766498503333 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 now_time1766498503357 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 now_time1766498503382 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 now_time1766498503408 it-\u0026gt;proj.get_fire_t(): 1.7665e+12 [rosout-1] killing on exit [master] killing on exit shutting down processing monitor\u0026hellip; \u0026hellip; shutting down processing monitor com 这里打印出来的如何打印出来数据而不是科学计数法\n我们注意到在日志中，it-\u0026gt;proj.get_fire_t() 打印出来是科学计数法（例如1.7665e+12），这不利于我们直观地比较时间。我们需要将其转换为普通的十进制整数格式来打印。 另外，从日志中可以看到，now_time 打印的是普通整数（例如1766498503009），而 it-\u0026gt;proj.get_fire_t() 打印的是科学计数法，这是因为 get_fire_t() 返回的是 double 类型，而 now_time 是 long long 类型。 我们可以通过以下两种方式解决： 在打印时，将 double 类型的 fire_t 转换为 long long 类型，然后使用 %lld 格式化输出。 或者，使用 std::fixed 和 std::setprecision(0) 来避免科学计数法。\n由于 fire_t 本身是 long long 类型，但在 ProjectileSimulator 中存储为 double，我们可以修改 get_fire_t() 返回 long long 类型，或者保持返回 double 但在打印时进行转换。 根据代码，fire_t 在 ProjectileSimulator 中定义为 const long long fire_t，但是 get_fire_t() 返回 double。我们可以将 get_fire_t() 的返回类型改为 long long，这样打印时就可以直接使用 %lld。 但是，注意到在 get_pos_by_t 函数中，我们使用了 this-\u0026gt;fire_t 并转换为秒（除以1000），所以返回 long long 也是合理的。 修改步骤： 在头文件中，将 get_fire_t() 的返回类型改为 long long。 在实现文件中，将 get_fire_t() 的返回语句改为 return this-\u0026gt;fire_t;，因为 fire_t 已经是 long long 类型。\n同时，在 get_circles 函数中，我们打印时使用 %lld 来打印 fire_t，而 now_time 也是 long long，这样就能正常显示。 但是，我们注意到在 get_circles 函数中，我们使用了 ROS_WARN 打印，而 ROS_WARN 使用的是类似于 printf 的格式，所以我们需要确保格式匹配。 由于 fire_t 是 long long，我们使用 %lld 来打印。 我们修改 ProjectileSimulator 类的 get_fire_t() 函数，返回 long long。 另外，在 get_circles 函数中，我们打印 now_time 和 fire_t 时，使用 %lld。 但是，注意：在 get_circles 函数中，我们调用 it-\u0026gt;proj.get_fire_t() 并赋值给 long long fire_t，然后打印。 修改后的 get_circles 函数中，我们将 fire_t 定义为 long long 类型，然后使用 %lld 打印。 同时，我们修改头文件中 get_fire_t() 的声明，返回 long long。 修改如下： 在 trajectory_visualizer.hpp 中，修改 ProjectileSimulator 类的 get_fire_t() 声明： cpp\nauto get_fire_t() const -\u0026gt; long long; 在 trajectory_visualizer.cpp 中，修改 get_fire_t() 的实现： cpp\nauto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {return this-\u0026gt;fire_t;} 在 get_circles 函数中，我们打印时使用 %lld，注意 now_time 也是 long long，所以我们可以这样打印： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {long long fire_t = it-\u0026gt;proj.get_fire_t();if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026ldquo;now_time: %lld, fire_t: %lld, 子弹未发射\u0026rdquo;, now_time, fire_t);++it; continue;}// \u0026hellip; 其他代码}return res;}\n这样修改后，打印出来的时间就是整数形式，而不是科学计数法了。 但是，注意：在 get_pos_by_t 函数中，我们将 fire_t 转换为秒时，需要先将 fire_t 转换为 double，然后再除以1000。因为 fire_t 是毫秒，而 t 也是毫秒，我们在计算时转换为秒。 所以，在 get_pos_by_t 函数中，我们这样转换： cpp double t_s = static_cast(t) * 0.001; double fire_t_s = static_cast(this-\u0026gt;fire_t) * 0.001;\n这样修改后，应该能正常打印和计算。 另外，我们注意到在 get_circle_by_t 函数中，我们使用 coorConverter-\u0026gt;cam2Map 将子弹位置转换到地图坐标系，然后计算半径。但是，我们传入的 bullet.pos 是相机坐标系下的坐标，而 cam2Map 是将相机坐标系转换到地图坐标系，这是正确的。 但是，我们计算半径时，使用的是地图坐标系下的点投影到像素坐标，这依赖于 coorConverter-\u0026gt;map2pixel 函数。请确保这个函数能正确工作。 最后，我们修改代码以打印出整数时间，并检查时间逻辑。 由于你的日志显示 now_time 和 fire_t 非常接近，但 now_time 一直小于 fire_t，所以一直打印未发射。这可能是因为你的 fire_t 设置得比当前时间大（即延迟发射），所以你需要等待一段时间（比如137毫秒）后，now_time 才会大于等于 fire_t。 如果你希望立即看到子弹发射，可以调整 update_bullet 中的延迟，比如将延迟设置为0，或者将发射时间设置为当前时间。 但是，根据你的需求，你是想模拟实际延迟，那么就需要等待。你可以通过打印 fire_t - now_time 来查看还有多少毫秒发射。 修改 get_circles 中的打印，以显示时间差： text\nif (now_time \u0026lt; fire_t) { ROS_WARN(\u0026ldquo;now_time: %lld, fire_t: %lld, 还有 %lld 毫秒发射\u0026rdquo;, now_time, fire_t, fire_t - now_time); ++it; continue; }\n这样，你可以看到每颗子弹还需要多久发射。 根据你的日志，now_time 和 fire_t 都是大约1.7665e+12，也就是1766500000000，而你的 now_time 打印为1766498503009，相差大约1.5秒。注意，你的 fire_t 是科学计数法，而 now_time 是整数，它们可能相差不大，但需要精确比较。 修改后，我们期望打印出整数，这样就能清楚看到时间差。 另外，注意你的 now_time 打印时没有空格，所以看起来像是一个字符串，实际上它是数字。在ROS日志中，我们使用%lld打印，它会正确显示。 现在，我们总结一下修改： 修改 ProjectileSimulator::get_fire_t() 返回 long long。 在 get_circles 中，使用 long long fire_t 接收，并用 %lld 打印。 在 get_pos_by_t 中，将 fire_t 转换为 double 时使用 static_cast(this-\u0026gt;fire_t)。\n由于修改涉及头文件和实现文件，请确保修改一致。 如果还有问题，请检查 update_bullet 中计算 fire_time 时，是否真的加了延迟。根据你的代码，延迟为 eTime_ms + additional_delay + command_timespan_ms，如果这些值都是正数，那么 fire_time 就会比 current_time 大，所以子弹不会立即发射。 希望这些修改能帮助你调试。 根据日志输出，问题很明显了：it-\u0026gt;proj.get_fire_t() 打印出来的是科学计数法（1.7665e+12），而 now_time 打印的是普通十进制（1766498503009）。这两个时间实际上是非常接近的！ 让我分析并修复这个问题： 问题分析\n从日志可以看到： now_time: 1766498503009 fire_t: 1.7665e+12 = 1766500000000（科学计数法）\n差值 = 1766500000000 - 1766498503009 = 约 1496991 纳秒 ≈ 1.5 毫秒 这意味着： 子弹确实有延迟发射：fire_t 比 now_time 大约 1.5 毫秒 时间单位问题：科学计数法显示导致难以直观比较 解决方案\n1. 修改 get_fire_t() 返回类型 当前返回的是 double，但存储的是 long long。应该统一类型： cpp\n// 在ProjectileSimulator类中auto get_fire_t() const -\u0026gt; long long; // 改为返回 long long cpp\n// 实现auto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {return this-\u0026gt;fire_t;} 2. 修改调试输出格式 在 get_circles 函数中，使用正确的格式打印： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {long long fire_t = it-\u0026gt;proj.get_fire_t();// 使用整数格式打印，避免科学计数法ROS_WARN(\u0026ldquo;now_time: %lld, fire_t: %lld, 差值: %lldms\u0026rdquo;, now_time, fire_t, fire_t - now_time);if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026ldquo;子弹未发射，还需等待 %lld 毫秒\u0026rdquo;, fire_t - now_time);++it; continue;}ROS_WARN(\u0026ldquo;子弹已发射，已飞行 %lld 毫秒\u0026rdquo;, now_time - fire_t);// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中 -\u0026gt; 已击中删除if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else {// 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n3. 或者在ROS中使用合适的格式 如果你坚持使用 double 类型，可以这样格式化输出： cpp // 方法1：使用 std::fixed 和 std::setprecision#include #include std::stringstream ss; ss \u0026laquo; std::fixed \u0026laquo; std::setprecision(0) \u0026laquo; fire_t_double;ROS_WARN(\u0026ldquo;fire_t: %s\u0026rdquo;, ss.str().c_str());// 方法2：直接使用 printf 风格的格式化ROS_WARN(\u0026ldquo;fire_t: %.0f\u0026rdquo;, fire_t_double);\n4. 完整修改方案 这是最简洁的修改方案： cpp #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo;#include #include #include namespace tools{auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet.pos);// 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized();// 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos);double radius = math::get_dis(edge_xy_u, center_xy_u);return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) };}auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {// 将毫秒转换成秒double t_s = static_cast(t * 0.001);double fire_t_s = static_cast(this-\u0026gt;fire_t * 0.001);double k = 0.1; // 空气阻力系数// 计算水平位移double w = (t_s - fire_t_s) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle);// 计算高度double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w/ (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))+ this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k/ k;// 弹道轨迹仅取决于目标点(理想弹道)const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 计算基准方向const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized();const Eigen::Vector3d h_norm = { 0., 0., 1. };const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm;const Eigen::Vector3d bullet_xyz_i_camera = this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0),bullet_xyz_i_barrel(1, 0) };const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0),target_xyz_i_barrel(1, 0) };return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(), bullet_xyz_i_camera };}// 修改这里：返回 long long 而不是 doubleauto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {return this-\u0026gt;fire_t;}AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) {this-\u0026gt;shoot_param = shoot_param;}auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {// 获取子弹发射时间long long fire_t = it-\u0026gt;proj.get_fire_t();// 打印详细的时间信息ROS_WARN(\u0026ldquo;时间比较: now_time=%lld, fire_t=%lld, 差值=%lldms\u0026rdquo;, now_time, fire_t, fire_t - now_time);if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026ldquo;子弹未发射，还需等待 %lld 毫秒\u0026rdquo;, fire_t - now_time);++it; continue;}ROS_WARN(\u0026ldquo;子弹已发射！已飞行 %lld 毫秒\u0026rdquo;, now_time - fire_t);// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中 -\u0026gt; 已击中删除if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else {// 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000);long long additional_delay = 25; // 0.025秒 = 25毫秒if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) {long long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms;ROS_WARN(\u0026ldquo;创建新子弹: id=%lld, current_time=%lld, fire_time=%lld, 延迟=%lldms\u0026rdquo;, next_id, current_time, fire_time, eTime_ms + additional_delay + command_timespan_ms); bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}// \u0026hellip; 其他函数保持不变 \u0026hellip;}\n5. 修改头文件中的声明 cpp // 在trajectory_visualizer.hpp中class ProjectileSimulator {public:// \u0026hellip; 其他函数 \u0026hellip;// 修改这里auto get_fire_t() const -\u0026gt; long long;private:const double g { 9.8 };const long long fire_t; // 存储为long long CoordinateTransformer* coorConverter; ShootParam shoot_param;};\n关键修改点 统一时间类型：get_fire_t() 返回 long long 而不是 double 清晰的时间比较：使用 %lld 格式打印，避免科学计数法 添加时间差信息：打印子弹还需等待的时间或已飞行的时间\n问题根源 根据你的日志，子弹的 fire_t 比 now_time 大约 1.5毫秒。这意味着： 每50毫秒创建一个新子弹 每个子弹有大约137毫秒的发射延迟（eTime + additional_delay + COMMAND_TIMESPAN） 但由于时间精度问题，有时候差值只有1-2毫秒\n这就是为什么你一直看到\u0026quot;子弹未发射\u0026rdquo;：因为子弹确实还没有到发射时间！ 测试建议 运行程序，观察日志输出，你会看到： 开始时，子弹需要等待约137毫秒 随着时间的推移，差值逐渐减小 当 now_time \u0026gt;= fire_t 时，子弹开始显示\n如果你希望立即看到效果，可以在 get_circles 中暂时放宽条件： cpp // 临时测试：如果差值小于5毫秒，也认为子弹已发射if (now_time \u0026lt; fire_t - 5) { // 原来是 if (now_time \u0026lt; fire_t)ROS_WARN(\u0026ldquo;子弹未发射，还需等待 %lld 毫秒\u0026rdquo;, fire_t - now_time);++it; continue;}\n这样你就可以看到子弹在发射前5毫秒就开始显示了。 starting process process[hikcamera-2]: started with pid [19118] [ WARN] [1766498740.512420935]: now_time: 1766498740512, fire_t: 4627730092099895296, 差值: 1766498740512ms [ WARN] [1766498740.512486911]: 111111111111111111111 [ WARN] [1766498740.536194465]: now_time: 1766498740536, fire_t: 4627730092099895296, 差值: 1766498740536ms [ WARN] [1766498740.536237521]: 111111111111111111111 [ WARN] [1766498740.561442872]: now_time: 1766498740561, fire_t: 4627730092099895296, 差值: 1766498740561ms [ WARN] [1766498740.561490286]: 111111111111111111111 [ WARN] [1766498740.584018819]: now_time: 1766498740583, fire_t: 4627730092099895296, 差值: 1766498740583ms [ WARN] [1766498740.584055293]: 111111111111111111111 [ WARN] [1766498740.609506649]: now_time: 1766498740609, fire_t: 4627730092099895296, 差值: 1766498740609ms [ WARN] [1766498740.609528872]: 111111111111111111111 [ WARN] [1766498740.635323791]: now_time: 1766498740635, fire_t: 4627730092099895296, 差值: 1766498740635ms [ WARN] [1766498740.635359674]: 111111111111111111111 [ WARN] [1766498740.660263821]: now_time: 1766498740660, fire_t: 4627730092099895296, 差值: 1766498740660ms [ WARN] [1766498740.660293805]: 111111111111111111111 [ WARN] [1766498740.684476247]: now_time: 1766498740684, fire_t: 4627730092099895296, 差值: 1766498740684ms [ WARN] [1766498740.684511049]: 111111111111111111111 [ WARN] [1766498740.709618933]: now_time: 1766498740709, fire_t: 4627730092099895296, 差值: 1766498740709ms [ WARN] [1766498740.709640828]: 111111111111111111111 [ WARN] [1766498740.734034179]: now_time: 1766498740733, fire_t: 4627730092099895296, 差值: 1766498740733ms [ WARN] [1766498740.734063029]: 111111111111111111111 [ WARN] [1766498740.760499042]: now_time: 1766498740760, fire_t: 4627730092099895296, 差值: 1766498740760ms [ WARN] [1766498740.760541135]: 111111111111111111111 [ WARN] [1766498740.783394642]: now_time: 1766498740783, fire_t: 4627730092099895296, 差值: 1766498740783ms [ WARN] [1766498740.783433629]: 111111111111111111111 [hikcamera-2] process has died [pid 19118, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/732c0256-e008-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/732c0256-e008-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [19126] [ WARN] [1766498740.811048380]: now_time: 1766498740810, fire_t: 4627730092099895296, 差值: 1766498740810ms [ WARN] [1766498740.811102979]: 111111111111111111111 [ WARN] [1766498740.835822391]: now_time: 1766498740835, fire_t: 4627730092099895296, 差值: 1766498740835ms [ WARN] [1766498740.835880668]: 111111111111111111111 [ WARN] [1766498740.862840799]: now_time: 1766498740862, fire_t: 4627730092099895296, 差值: 1766498740862ms [ WARN] [1766498740.862874236]: 111111111111111111111 [ WARN] [1766498740.884836234]: now_time: 1766498740884, fire_t: 4627730092099895296, 差值: 1766498740884ms [ WARN] [1766498740.884871186]: 111111111111111111111 [ WARN] [1766498740.910968607]: now_time: 1766498740910, fire_t: 4627730092099895296, 差值: 1766498740910ms [ WARN] [1766498740.911007120]: 111111111111111111111 [ WARN] [1766498740.937332803]: now_time: 1766498740937, fire_t: 4627730092099895296, 差值: 1766498740937ms [ WARN] [1766498740.937383112]: 111111111111111111111 [ WARN] [1766498740.960806543]: now_time: 1766498740960, fire_t: 4627730092099895296, 差值: 1766498740960ms [ WARN] [1766498740.960835921]: 111111111111111111111 [ WARN] [1766498740.986319291]: now_time: 1766498740986, fire_t: 4627730092099895296, 差值: 1766498740986ms [ WARN] [1766498740.986374925]: 111111111111111111111 [ WARN] [1766498741.013358055]: now_time: 1766498741013, fire_t: 4627730092099895296, 差值: 1766498741013ms [ WARN] [1766498741.013391168]: 111111111111111111111 [ WARN] [1766498741.036542575]: now_time: 1766498741036, fire_t: 4627730092099895296, 差值: 1766498741036ms [ WARN] [1766498741.036668343]: 111111111111111111111 [ WARN] [1766498741.060504611]: now_time: 1766498741060, fire_t: 4627730092099895296, 差值: 1766498741060ms [ WARN] [1766498741.060533966]: 111111111111111111111 [ WARN] [1766498741.086116337]: now_time: 1766498741086, fire_t: 4627730092099895296, 差值: 1766498741086ms [ WARN] [1766498741.086147190]: 111111111111111111111 [hikcamera-2] process has died [pid 19126, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/732c0256-e008-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/732c0256-e008-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [19135] [ WARN] [1766498741.112363029]: now_time: 1766498741112, fire_t: 4627730092099895296, 差值: 1766498741112ms [ WARN] [1766498741.112423522]: 111111111111111111111 [ WARN] [1766498741.139008850]: now_time: 1766498741138, fire_t: 4627730092099895296, 差值: 1766498741138ms [ WARN] [1766498741.139050461]: 111111111111111111111 [ WARN] [1766498741.164081259]: now_time: 1766498741164, fire_t: 4627730092099895296, 差值: 1766498741164ms [ WARN] [1766498741.164117424]: 111111111111111111111 ^C[ WARN] [1766498741.187219952]: now_time: 1766498741187, fire_t: 4627730092099895296, 差值: 1766498741187ms [ WARN] [1766498741.187260017]: 111111111111111111111 [identify-4] killing on exit [tracker-5] killing on exit 这是什么情况? 问题分析： 从日志中可以看到，fire_t 的值异常巨大，达到了 4627730092099895296，而 now_time 是正常的时间戳（大约1.76e12）。这导致差值计算错误（实际上是负数，但因为是无符号长整型，所以显示为正数）。\n这个问题的根本原因是：在构造ProjectileSimulator对象时，传入的fire_t参数类型不匹配。 在update_bullet函数中，我们计算了fire_time（long long类型），然后传递给ProjectileSimulator构造函数。但是，在构造函数中，我们将其存储为const long long fire_t。然而，在get_fire_t函数中，我们返回的是double类型（或者现在改成了long long？），但实际存储的是long long。 但是，从日志中看，fire_t的值异常大，这很可能是由于类型转换错误或者内存错误导致的。 检查代码： 在update_bullet函数中，我们计算fire_time： cpp\nlong long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms; 然后，我们将其传递给ProjectileSimulator构造函数。在构造函数中，我们使用成员初始化列表来初始化fire_t。 但是，注意：在ProjectileSimulator类中，fire_t是const long long类型。这应该没问题。 然而，在get_fire_t函数中，我们返回this-\u0026gt;fire_t。如果我们在头文件中将get_fire_t的返回类型声明为double，但在实现中返回long long，就会导致类型不匹配，从而产生未定义行为。 但是，根据之前的修改，我们已经将get_fire_t的返回类型改为long long。那么，为什么日志中打印的fire_t值如此巨大呢？ 让我们检查一下打印的代码： cpp\nROS_WARN(\u0026ldquo;时间比较: now_time=%lld, fire_t=%lld, 差值=%lldms\u0026rdquo;, now_time, fire_t, fire_t - now_time); 这里使用了%lld来打印，说明fire_t是long long类型。但是，打印出来的fire_t值是4627730092099895296，这个值大约为4.6e18，而now_time大约是1.76e12。这个差值大约是4.6e18，这显然不对。 可能的原因： 在ProjectileSimulator构造函数中，传入的fire_t参数已经是错误的。 内存错误，导致fire_t成员变量被错误地写入。\n让我们检查ProjectileSimulator构造函数的调用： cpp bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});\n这里，fire_time是long long类型，然后传递给ProjectileSimulator的构造函数。在构造函数中，我们使用成员初始化列表来初始化fire_t。 但是，注意：在ProjectileSimulator类中，fire_t是const long long类型。这意味着它必须在构造函数初始化列表中初始化，并且一旦初始化就不能改变。 在构造函数的实现中，我们是这样写的： cpp ProjectileSimulator(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param, const long long\u0026amp; fire_t) : coorConverter{coorConverter}, shoot_param{shoot_param}, fire_t{fire_t} {}\n这里，fire_t是参数，同时也是成员变量。注意：参数名和成员变量名相同，这会导致在初始化列表中，fire_t{fire_t}中的两个fire_t分别指代什么？在C++中，初始化列表中的fire_t是成员变量，而括号中的fire_t是参数。这应该没问题。 但是，如果我们没有在初始化列表中初始化fire_t，那么fire_t会被默认初始化，然后我们在构造函数体内对其赋值（因为它是const，所以不能赋值）。但是，我们的代码使用了初始化列表，所以应该没有问题。 另一种可能性：在ProjectileSimulator类的定义中，fire_t成员变量的类型可能是double，而不是long long。检查头文件： 在头文件中，我们定义ProjectileSimulator类： cpp class ProjectileSimulator {public:ProjectileSimulator(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param, const long long\u0026amp; fire_t);// \u0026hellip;auto get_fire_t() const -\u0026gt; long long;private:const double g { 9.8 };const long long fire_t; // 注意：这里是long long CoordinateTransformer* coorConverter; ShootParam shoot_param;};\n这里，fire_t是long long类型。但是，在构造函数中，我们传入的参数是const long long\u0026amp; fire_t，所以类型匹配。 那么，为什么fire_t的值会如此巨大呢？可能是由于在update_bullet中计算fire_time时发生了整数溢出？让我们计算一下： current_time大约是1.76e12（毫秒），加上延迟（假设eTime_ms、additional_delay、command_timespan_ms都是毫秒），总延迟大约137毫秒，所以fire_time应该是current_time + 137，大约是1.76e12，而不是4.6e18。 所以，整数溢出的可能性不大，因为1.76e12远小于long long的最大值（9.22e18）。 另一种可能性：在get_fire_t函数中，我们返回了错误的值。但是，我们只是返回this-\u0026gt;fire_t，所以应该没问题。 那么，可能是ROS打印时格式错误？我们使用%lld打印long long，而now_time也是long long，打印出来正常，所以格式应该没问题。 让我们检查一下get_fire_t的实现： cpp auto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {return this-\u0026gt;fire_t;}\n这个函数返回long long，而我们在打印时也使用%lld，所以匹配。 但是，日志中fire_t的值是4627730092099895296，这个值对应的十六进制是0x4038000000000000，这恰好是一个double类型的数据，表示的是12.0（因为0x4038000000000000是double的12.0的IEEE754表示）。这提示我们，可能在某些地方错误地将fire_t解释为double类型了。 检查头文件中get_fire_t的声明： cpp auto get_fire_t() const -\u0026gt; long long;\n在实现中，我们返回的是long long，但是，如果头文件中声明为返回double，而实现中返回long long，那么就会导致问题。但是，我们已经修改了头文件，声明为返回long long。 不过，注意：我们修改了头文件，但是编译过程中可能没有重新编译所有依赖的文件。因此，可能存在编译不一致的情况。 解决方案： 清理并重新编译整个项目。 在get_fire_t函数中添加调试输出，直接打印fire_t的值，确保在函数内部值是正确的。\n让我们修改get_fire_t函数，在返回之前打印一下： cpp\nauto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {ROS_WARN(\u0026ldquo;get_fire_t: fire_t = %lld\u0026rdquo;, this-\u0026gt;fire_t);return this-\u0026gt;fire_t;} 然后重新运行，看看打印出来的值。 但是，从日志中已经可以看到，在get_circles函数中，我们打印的fire_t值异常，所以问题可能不在get_fire_t函数，而是fire_t成员变量本身在构造时就被赋予了错误的值。 那么，我们在构造函数中也添加打印： cpp ProjectileSimulator::ProjectileSimulator(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param, const long long\u0026amp; fire_t) : coorConverter{coorConverter}, shoot_param{shoot_param}, fire_t{fire_t} {ROS_WARN(\u0026ldquo;ProjectileSimulator构造函数: fire_t = %lld\u0026rdquo;, fire_t);}\n这样，我们可以知道在构造时传入的fire_t参数是否正确。 另外，检查update_bullet函数中计算fire_time的代码： cpp\nlong long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms;ROS_WARN(\u0026ldquo;创建新子弹: id=%lld, current_time=%lld, fire_time=%lld, 延迟=%lldms\u0026rdquo;, next_id, current_time, fire_time, eTime_ms + additional_delay + command_timespan_ms); 我们已经添加了打印，但是日志中没有看到这些打印信息。这说明update_bullet函数可能没有被调用，或者没有进入创建子弹的条件分支。 但是，我们确实看到了子弹列表中有子弹（因为get_circles被调用并打印了子弹信息），所以update_bullet一定被调用了，而且创建了子弹。 然而，我们看到的日志中并没有update_bullet中的打印信息，这说明我们可能在update_bullet函数中没有添加打印，或者打印的日志级别不同。让我们检查一下，我们在update_bullet中添加的打印是ROS_WARN，应该会输出。 但是，在提供的日志中，我们只看到了get_circles中的打印，没有update_bullet中的打印。所以，可能是update_bullet没有被调用，或者子弹是在其他地方创建的。 实际上，我们是在draw_simulated_bullets函数中创建AimCorrector对象，然后调用update_bullet。每次调用draw_simulated_bullets都会创建一个新的AimCorrector对象，这意味着子弹列表是空的，然后调用update_bullet可能会创建一颗子弹。但是，由于last_fire_time初始化为0，所以第一次调用update_bullet时，条件this-\u0026gt;last_fire_time == 0为真，会创建一颗子弹。 但是，我们每次调用draw_simulated_bullets都会创建一个新的AimCorrector对象，所以每次都是第一次调用update_bullet，都会创建子弹。然后，在同一个函数中，我们调用get_circles来获取子弹并绘制。 但是，由于AimCorrector对象是局部的，每次调用draw_simulated_bullets都会创建新的对象，所以不会积累子弹。这可能是设计上的问题，但这不是当前问题的关键。 关键问题是，为什么fire_t的值异常巨大？ 让我们在draw_simulated_bullets函数中添加打印，看看传入的参数： cpp\nvoid draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ROS_WARN(\u0026ldquo;draw_simulated_bullets: now_time=%lld, eTime=%lld, COMMAND_TIMESPAN=%lld\u0026rdquo;, now_time, eTime, COMMAND_TIMESPAN);// \u0026hellip; 其余代码 \u0026hellip;} 这样，我们可以知道传入的时间参数是否正确。 另外，在ProjectileSimulator构造函数中，我们打印传入的fire_t参数，以及成员变量fire_t。 但是，由于fire_t成员变量是const，我们只能在初始化列表中初始化，所以构造函数体内的打印只能打印参数，不能打印成员变量（因为成员变量在初始化列表之后才可用，但打印参数是可以的）。 所以，我们在构造函数体内打印参数fire_t，在get_fire_t函数中打印成员变量this-\u0026gt;fire_t。 综合以上，我们进行以下修改： 在draw_simulated_bullets函数中，打印传入的时间参数。 在update_bullet函数中，打印计算出的fire_time。 在ProjectileSimulator构造函数中，打印传入的fire_t参数。 在get_fire_t函数中，打印成员变量this-\u0026gt;fire_t。\n然后重新编译运行，观察日志。 由于日志中已经显示了now_time是正常的，而fire_t异常，所以我们重点关注fire_t是如何被传递和存储的。 另外，注意：在ProjectileSimulator类中，fire_t是const long long类型，这确保了它不会被修改，所以只要构造函数中传入的值正确，那么get_fire_t返回的值就应该正确。 但是，从日志中看，fire_t的值是4.6e18，这可能是由于未初始化或内存错误导致的。所以，我们怀疑在构造函数中，传入的fire_t参数就已经是错误的。 那么，让我们检查update_bullet函数中计算fire_time的表达式： cpp long long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms;\n这里，eTime_ms和command_timespan_ms是由eTime和COMMAND_TIMESPAN乘以1000得到的。而eTime和COMMAND_TIMESPAN是long long类型，但是它们原本是秒，乘以1000后转换成毫秒。 注意：eTime和COMMAND_TIMESPAN是long long类型，但是它们可能很小（例如0.002秒和0.11秒），乘以1000后是2和110，所以不会溢出。 但是，如果eTime和COMMAND_TIMESPAN是浮点数，那么这里就会有问题。因为函数参数是long long，而在Tracker中调用draw_simulated_bullets时，传入的是eTime和COMMAND_TIMESPAN，这两个变量在Tracker中是double类型。然后，在draw_simulated_bullets函数中，它们被接收为long long，这会导致类型转换，将浮点数转换为整数。 在Tracker中，我们这样调用： cpp tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, static_cast(eTime * 1000), // 注意：这里eTime是double，乘以1000后是double，然后转换为long longstatic_cast(COMMAND_TIMESPAN * 1000));\n在draw_simulated_bullets函数中，参数是long long eTime和long long COMMAND_TIMESPAN，但实际上我们已经传入了乘以1000后的值，所以函数内部不需要再乘以1000。 但是，在update_bullet函数中，我们又进行了一次乘以1000： cpp\nlong long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); 这会导致eTime和COMMAND_TIMESPAN被错误地放大了1000倍，因为实际上它们已经是毫秒了。所以，这里应该直接使用，而不是再乘以1000。 在Tracker中，我们已经将秒转换为毫秒，然后传入draw_simulated_bullets，所以eTime和COMMAND_TIMESPAN已经是毫秒。因此，在update_bullet中，我们不应该再乘以1000。 修改update_bullet函数： cpp\nauto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 注意：eTime和COMMAND_TIMESPAN已经是毫秒，所以不需要再乘以1000long long eTime_ms = eTime;long long command_timespan_ms = COMMAND_TIMESPAN;long long additional_delay = 25; // 0.025秒 = 25毫秒// \u0026hellip; 其余代码不变 \u0026hellip;} 但是，为了清晰起见，我们可以重命名参数，或者修改调用处。 实际上，在Tracker中，我们传入的是eTime和COMMAND_TIMESPAN乘以1000后的值，所以它们已经是毫秒。因此，在update_bullet中，我们直接使用即可。 所以，将update_bullet函数中的转换去掉，直接使用参数。 另外，检查在Tracker中调用draw_simulated_bullets时，传入的eTime和COMMAND_TIMESPAN是否确实是乘以1000后的值。从之前的代码中，我们看到： cpp tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, static_cast(eTime * 1000), static_cast(COMMAND_TIMESPAN * 1000));\n所以，传入的是毫秒，因此draw_simulated_bullets函数中的参数eTime和COMMAND_TIMESPAN已经是毫秒，那么在update_bullet中就不应该再乘以1000。 因此，修改update_bullet函数： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime_ms, long long command_timespan_ms) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒long long additional_delay = 25; // 0.025秒 = 25毫秒if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) {long long fire_time = current_time + eTime_ms + additional_delay + command_timespan_ms;ROS_WARN(\u0026ldquo;创建新子弹: id=%lld, current_time=%lld, fire_time=%lld, 延迟=%lldms\u0026rdquo;, next_id, current_time, fire_time, eTime_ms + additional_delay + command_timespan_ms); bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n同时，修改函数声明，将参数名改为eTime_ms和command_timespan_ms，以表明它们已经是毫秒。 然后，重新编译运行，观察日志。 如果问题仍然存在，那么我们需要在构造函数中打印传入的fire_t，在get_fire_t中打印成员变量，以确定问题出在哪里。 但是，从日志中看，fire_t的值是4.6e18，这个值很可能是由于未初始化或内存错误导致的。所以，我们也要检查是否在某个地方有内存越界，覆盖了fire_t的值。 另外，注意：在IdProj结构中，我们存储了ProjectileSimulator对象，而ProjectileSimulator有const成员，这可能会导致复制问题。但是，我们使用push_back将IdProj对象放入bullets列表，这会调用复制构造函数。如果ProjectileSimulator的复制构造函数没有正确复制fire_t，那么就会有问题。 但是，ProjectileSimulator有一个自定义的构造函数，编译器会自动生成复制构造函数，因为所有成员都可以复制。fire_t是const long long，所以复制构造函数会复制它的值。所以，这应该没问题。 但是，为了安全起见，我们可以为ProjectileSimulator实现一个复制构造函数，确保复制正确。 不过，根据C++规则，const成员和引用成员会导致编译器删除默认的复制赋值运算符，但复制构造函数仍然会被生成（除非有其他原因导致不生成）。但是，由于fire_t是const，复制构造函数会复制其值。 所以，我们先按照上述修改，重新编译运行，看看问题是否解决。 如果问题仍然存在，我们可以在ProjectileSimulator类中添加复制构造函数，并打印信息，以确保复制过程中值被正确传递。 但是，从日志中看，子弹列表中有子弹，所以ProjectileSimulator对象被创建了。问题可能在于fire_t的值在构造时就已经是错误的。 因此，我们首先在构造函数中打印传入的fire_t，在get_fire_t中打印成员变量，以确定问题出在哪个环节。 修改ProjectileSimulator构造函数和get_fire_t函数： cpp ProjectileSimulator::ProjectileSimulator(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param, const long long\u0026amp; fire_t) : coorConverter{coorConverter}, shoot_param{shoot_param}, fire_t{fire_t} {ROS_WARN(\u0026ldquo;ProjectileSimulator构造函数: 传入的fire_t = %lld, 成员fire_t = %lld\u0026rdquo;, fire_t, this-\u0026gt;fire_t);}auto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {ROS_WARN(\u0026ldquo;get_fire_t: 返回fire_t = %lld\u0026rdquo;, this-\u0026gt;fire_t);return this-\u0026gt;fire_t;} 然后重新编译运行，观察日志。\n注意：由于每次调用get_circles都会遍历子弹，所以会多次调用get_fire_t，可能会打印很多日志。我们可以暂时减少日志，或者只打印一次。 但是，为了调试，我们可以先这样，找到问题后再删除。 另外，在update_bullet中，我们创建子弹时，打印fire_time，然后在构造函数中打印，这样就可以看到从计算到传递的过程。 让我们总结一下修改步骤： 修改update_bullet函数，去掉不必要的乘以1000，直接使用毫秒参数。 在update_bullet中，打印计算出的fire_time。 在ProjectileSimulator构造函数中，打印传入的fire_t和成员变量this-\u0026gt;fire_t。 在get_fire_t函数中，打印返回的this-\u0026gt;fire_t。 然后重新编译运行，观察日志。\n如果问题仍然存在，那么可能是其他问题，比如内存错误。但是，从日志中看，now_time是正常的，所以时间获取函数应该没问题。 另外，检查fire_t在ProjectileSimulator类中是否被正确初始化。如果构造函数初始化列表中的顺序与声明顺序不一致，也可能导致问题。但是，我们的初始化列表顺序与声明顺序一致：先coorConverter，然后shoot_param，然后fire_t。而声明顺序是：g、fire_t、coorConverter、shoot_param。注意，g是const double，它先被初始化，然后fire_t，然后coorConverter，然后shoot_param。在初始化列表中，我们初始化了coorConverter、shoot_param和fire_t，但是g在fire_t之前被初始化。这应该没问题。 但是，为了排除初始化顺序的问题，我们可以将fire_t的声明放在g之后，其他成员之前，这样在初始化列表中，我们可以先初始化fire_t，然后其他成员。但是，由于fire_t是const，它必须在初始化列表中初始化，而且初始化顺序与声明顺序一致，所以只要声明顺序正确，初始化列表中的顺序不影响。 在我们的类声明中，fire_t在g之后，在coorConverter和shoot_param之前，所以初始化列表中，fire_t应该在coorConverter和shoot_param之前初始化。但是，我们的初始化列表顺序是coorConverter、shoot_param、fire_t，这会导致coorConverter和shoot_param在fire_t之前初始化，但是它们的声明在fire_t之后，所以这违反了初始化顺序。虽然编译器可能会警告，但不会报错，并且实际初始化的顺序是按照声明顺序，而不是初始化列表的顺序。所以，实际上fire_t会在coorConverter和shoot_param之前初始化，因为它的声明在它们之前。 因此，初始化顺序不会导致fire_t的值错误。 所以，我们按照上述修改，重新编译运行，看看日志输出。 注意：由于日志中已经显示fire_t异常，所以我们重点关注构造函数中打印的传入的fire_t是否异常。 如果构造函数中传入的fire_t就已经是异常值，那么问题在update_bullet中的计算。如果构造函数中传入的fire_t正常，但成员变量this-\u0026gt;fire_t异常，那么问题在初始化过程。如果构造函数中打印的成员变量正常，但get_fire_t中打印的异常，那么问题在复制或其他地方。 由于我们每次调用draw_simulated_bullets都会创建新的AimCorrector对象，所以不会涉及复制问题，因为子弹列表是新的，子弹也是新创建的。 所以，我们逐步调试，看问题出在哪里。 最后，如果问题仍然无法解决，我们可以考虑将fire_t改为非const，并在构造函数中赋值，以排除const成员可能带来的问题。 但是，根据日志，问题很可能是在update_bullet中计算fire_time时，由于参数类型转换错误，导致eTime_ms和command_timespan_ms计算错误，从而使得fire_time计算错误。 所以，我们首先修改update_bullet函数，确保传入的eTime和COMMAND_TIMESPAN是毫秒，并且不再进行乘以1000的转换。 修改后，重新编译运行，观察日志。 如果问题解决，那么我们就找到了原因。如果问题仍然存在，那么我们需要进一步调试。 由于问题可能比较复杂，我们可以先简化问题，去掉空气阻力，使用简单的弹道模型，并确保时间参数正确。 另外，检查在Tracker中，eTime和COMMAND_TIMESPAN的值是多少，确保它们是正确的秒数，然后乘以1000转换为毫秒。 在Tracker中，eTime和COMMAND_TIMESPAN是double类型，我们乘以1000后转换为long long，然后传入draw_simulated_bullets。 所以，在draw_simulated_bullets中，参数eTime和COMMAND_TIMESPAN就是毫秒，因此update_bullet中不应该再乘以1000。 所以，修改update_bullet函数，去掉乘以1000的操作，直接使用参数。 让我们修改代码并重新编译运行。 问题确实很严重！从日志可以看到，fire_t 的值变成了一个巨大的数字 4627730092099895296（约 4.6e18），这完全不合理。这明显是一个内存错误或类型转换错误。 问题分析\n1. 时间值异常 now_time: 1766498740512（约 1.76e12）这是正常的时间戳（毫秒） fire_t: 4627730092099895296（约 4.6e18）这是明显错误的值\n这个巨大的 fire_t 值看起来像是： 未初始化的内存 类型转换错误（如 double 转 long long 时的 bit 位错误） 构造函数初始化问题 [ WARN] [1766498932.050833846]: now_time: 1766498932050, fire_t: 4627730092099895296, 差值: 1766498932050ms [ WARN] [1766498932.050845193]: 111111111111111111111 [ WARN] [1766498932.076612934]: 构造函数: 1766498932101 [ WARN] [1766498932.076653073]: now_time: 1766498932076, fire_t: 4627730092099895296, 差值: 1766498932076ms [ WARN] [1766498932.076660062]: 111111111111111111111 [ WARN] [1766498932.100832963]: 构造函数: 1766498932125 [ WARN] [1766498932.100876434]: now_time: 1766498932100, fire_t: 4627730092099895296, 差值: 1766498932100ms [ WARN] [1766498932.100887201]: 111111111111111111111 [ WARN] [1766498932.125072994]: 构造函数: 1766498932150 [ WARN] [1766498932.125109539]: now_time: 1766498932125, fire_t: 4627730092099895296, 差值: 1766498932125ms [ WARN] [1766498932.125119513]: 111111111111111111111 [hikcamera-2] process has died [pid 19796, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/e5c2d8e4-e008-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/e5c2d8e4-e008-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [19804] [ WARN] [1766498932.148736428]: 构造函数: 1766498932173 [ WARN] [1766498932.148775653]: now_time: 1766498932148, fire_t: 4627730092099895296, 差值: 1766498932148ms [ WARN] [1766498932.148785387]: 111111111111111111111 [ WARN] [1766498932.176111516]: 构造函数: 1766498932201 [ WARN] [1766498932.176142995]: now_time: 1766498932176, fire_t: 4627730092099895296, 差值: 1766498932176ms [ WARN] [1766498932.176152548]: 111111111111111111111 [ WARN] [1766498932.199018796]: 构造函数: 1766498932223 [ WARN] [1766498932.199053724]: now_time: 1766498932198, fire_t: 4627730092099895296, 差值: 1766498932198ms [ WARN] [1766498932.199063767]: 111111111111111111111 [ WARN] [1766498932.224095546]: 构造函数: 1766498932249 [ WARN] [1766498932.224133666]: now_time: 1766498932224, fire_t: 4627730092099895296, 差值: 1766498932224ms [ WARN] [1766498932.224144365]: 111111111111111111111 [ WARN] [1766498932.250528742]: 构造函数: 1766498932275 [ WARN] [1766498932.250567302]: now_time: 1766498932250, fire_t: 4627730092099895296, 差值: 1766498932250ms [ WARN] [1766498932.250579169]: 111111111111111111111 [ WARN] [1766498932.273693262]: 构造函数: 1766498932298 [ WARN] [1766498932.273741385]: now_time: 1766498932273, fire_t: 4627730092099895296, 差值: 1766498932273ms [ WARN] [1766498932.273753304]: 111111111111111111111 [ WARN] [1766498932.303325250]: 构造函数: 1766498932328 [ WARN] [1766498932.303367334]: now_time: 1766498932303, fire_t: 4627730092099895296, 差值: 1766498932303ms [ WARN] [1766498932.303378704]: 111111111111111111111 [ WARN] [1766498932.326920889]: 构造函数: 1766498932351 [ WARN] [1766498932.326956853]: now_time: 1766498932326, fire_t: 4627730092099895296, 差值: 1766498932326ms [ WARN] [1766498932.326965930]: 111111111111111111111 [ WARN] [1766498932.351962080]: 构造函数: 1766498932376 [ WARN] [1766498932.352003250]: now_time: 1766498932351, fire_t: 4627730092099895296, 差值: 1766498932351ms [ WARN] [1766498932.352014403]: 111111111111111111111 [ WARN] [1766498932.377069650]: 构造函数: 1766498932401 [ WARN] [1766498932.377115896]: now_time: 1766498932376, fire_t: 4627730092099895296, 差值: 1766498932376ms [ WARN] [1766498932.377125047]: 111111111111111111111 [ WARN] [1766498932.401698379]: 构造函数: 1766498932426 [ WARN] [1766498932.401728560]: now_time: 1766498932401, fire_t: 4627730092099895296, 差值: 1766498932401ms [ WARN] [1766498932.401737200]: 111111111111111111111 [ WARN] [1766498932.428047482]: 构造函数: 1766498932453 [ WARN] [1766498932.428077758]: now_time: 1766498932428, fire_t: 4627730092099895296, 差值: 1766498932428ms [ WARN] [1766498932.428089862]: 111111111111111111111 [hikcamera-2] process has died [pid 19804, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/e5c2d8e4-e008-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/e5c2d8e4-e008-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [19813] [ WARN] [1766498932.451674896]: 构造函数: 1766498932476 [ WARN] [1766498932.451705229]: now_time: 1766498932451, fire_t: 4627730092099895296, 差值: 1766498932451ms [ WARN] [1766498932.451715403]: 111111111111111111111 [ WARN] [1766498932.476933569]: 构造函数: 1766498932501 [ WARN] [1766498932.476987175]: now_time: 1766498932476, fire_t: 4627730092099895296, 差值: 1766498932476ms [ WARN] [1766498932.476999295]: 111111111111111111111 [ WARN] [1766498932.501469224]: 构造函数: 1766498932526 [ WARN] [1766498932.501505862]: now_time: 1766498932501, fire_t: 4627730092099895296, 差值: 1766498932501ms [ WARN] [1766498932.501514848]: 111111111111111111111 [ WARN] [1766498932.527582532]: 构造函数: 1766498932552 [ WARN] [1766498932.527624555]: now_time: 1766498932527, fire_t: 4627730092099895296, 差值: 1766498932527ms [ WARN] [1766498932.527634859]: 111111111111111111111 [ WARN] [1766498932.552224344]: 构造函数: 1766498932577 [ WARN] [1766498932.552269646]: now_time: 1766498932552, fire_t: 4627730092099895296, 差值: 1766498932552ms [ WARN] [1766498932.552280347]: 111111111111111111111 [ WARN] [1766498932.577441673]: 构造函数: 1766498932602 [ WARN] [1766498932.577478296]: now_time: 1766498932577, fire_t: 4627730092099895296, 差值: 1766498932577ms [ WARN] [1766498932.577486076]: 111111111111111111111 [ WARN] [1766498932.602829840]: 构造函数: 1766498932627 [ WARN] [1766498932.602860160]: now_time: 1766498932602, fire_t: 4627730092099895296, 差值: 1766498932602ms [ WARN] [1766498932.602870199]: 111111111111111111111 [ WARN] [1766498932.628344562]: 构造函数: 1766498932653 [ WARN] [1766498932.628414040]: now_time: 1766498932628, fire_t: 4627730092099895296, 差值: 1766498932628ms [ WARN] [1766498932.628436720]: 111111111111111111111 [ WARN] [1766498932.652841657]: 构造函数: 1766498932677 [ WARN] [1766498932.652907587]: now_time: 1766498932652, fire_t: 4627730092099895296, 差值: 1766498932652ms [ WARN] [1766498932.652919195]: 111111111111111111111 [ WARN] [1766498932.677990332]: 构造函数: 1766498932702 [ WARN] [1766498932.678044958]: now_time: 1766498932677, fire_t: 4627730092099895296, 差值: 1766498932677ms [ WARN] [1766498932.678054217]: 111111111111111111111 [ WARN] [1766498932.703493548]: 构造函数: 1766498932728 [ WARN] [1766498932.703545353]: now_time: 1766498932703, fire_t: 4627730092099895296, 差值: 1766498932703ms [ WARN] [1766498932.703556584]: 111111111111111111111 [ WARN] [1766498932.727852595]: 构造函数: 1766498932752 [ WARN] [1766498932.727890161]: now_time: 1766498932727, fire_t: 4627730092099895296, 差值: 1766498932727ms [ WARN] [1766498932.727900079]: 111111111111111111111 [hikcamera-2] process has died [pid 19813, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/e5c2d8e4-e008-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/e5c2d8e4-e008-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [19822] [ WARN] [1766498932.755455937]: 构造函数: 1766498932780 [ WARN] [1766498932.755495986]: now_time: 1766498932755, fire_t: 4627730092099895296, 差值: 1766498932755ms [ WARN] [1766498932.755507286]: 111111111111111111111 [ WARN] [1766498932.780055565]: 构造函数: 1766498932804 [ WARN] [1766498932.780087135]: now_time: 1766498932779, fire_t: 4627730092099895296, 差值: 1766498932779ms [ WARN] [1766498932.780095156]: 111111111111111111111 [ WARN] [1766498932.803273863]: 构造函数: 1766498932828 [ WARN] [1766498932.803333806]: now_time: 1766498932803, fire_t: 4627730092099895296, 差值: 1766498932803ms [ WARN] [1766498932.803347274]: 111111111111111111111 [ WARN] [1766498932.827820312]: 构造函数: 1766498932852 [ WARN] [1766498932.827896675]: now_time: 1766498932827, fire_t: 4627730092099895296, 差值: 1766498932827ms [ WARN] [1766498932.827912546]: 111111111111111111111 [ WARN] [1766498932.852794653]: 构造函数: 1766498932877 [ WARN] [1766498932.852828188]: now_time: 1766498932852, fire_t: 4627730092099895296, 差值: 1766498932852ms [ WARN] [1766498932.852840463]: 111111111111111111111 [ WARN] [1766498932.878690888]: 构造函数: 1766498932903 [ WARN] [1766498932.878733784]: now_time: 1766498932878, fire_t: 4627730092099895296, 差值: 1766498932878ms [ WARN] [1766498932.878745968]: 111111111111111111111 [ WARN] [1766498932.904411201]: 构造函数: 1766498932929 [ WARN] [1766498932.904460846]: now_time: 1766498932904, fire_t: 4627730092099895296, 差值: 1766498932904ms [ WARN] [1766498932.904473375]: 111111111111111111111 ^C[ WARN] [1766498932.928906407]: 构造函数: 1766498932953 [ WARN] [1766498932.928934181]: now_time: 1766498932928, fire_t: 4627730092099895296, 差值: 1766498932928ms [ WARN] [1766498932.928941213]: 111111111111111111111 [tracker-5] killing on exit [identify-4] killing on exit [hikcamera-2] killing on exit [rosout-1] killing on exit [master] killing on exit shutting down processing monitor\u0026hellip; \u0026hellip; shutting down processing monitor complete done rm@rm-NUC11PAHi7:~$ #ifndef TRAJECTORY_VISUALIZER_HPP #define TRAJECTORY_VISUALIZER_HPP #include \u0026ldquo;math.hpp\u0026rdquo; #include \u0026ldquo;CoorConverter.hpp\u0026rdquo; #include \u0026lt;opencv2/opencv.hpp\u0026gt; #include \u0026ldquo;GimbalPos.hpp\u0026rdquo; #include \u0026ldquo;ros/ros.h\u0026rdquo; namespace tools{ const int FLASK_MAP_WIDTH = 1000; // 定义调试地图的水平分辨率 const double FLASK_MAP_PETER_BY_BRIGHT = 1.; // 默认亮度系数 const int FLASK_MAP_MID_X = FLASK_MAP_WIDTH / 2; // 地图的水平中心点,用于坐标变换的参考原点 // 点绘制参数 struct FlaskPoint { FlaskPoint( const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ): pt(pt), color(color), radius(radius), thickness(thickness) {} cv::Point2d pt; // 圆心位置 cv::Scalar color; // 颜色 int radius; // 半径 int thickness; // 线宽 }; struct FlaskLine { FlaskLine( const std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt;\u0026amp; pt_pair, const cv::Scalar\u0026amp; color, const int\u0026amp; thickness ): pt_pair(pt_pair), color(color), thickness(thickness) {} std::pair\u0026lt;cv::Point2f, cv::Point2f\u0026gt; pt_pair; cv::Scalar color; int thickness; }; // 文本绘制参数 struct FlaskText { FlaskText( const std::string\u0026amp; str, const cv::Point2d\u0026amp; pt, const cv::Scalar\u0026amp; color, const double\u0026amp; scale ): str(str), pt(pt), color(color), scale(scale) {} std::string str; // 文本内容 cv::Point2d pt; // 文本位置 (左下角) cv::Scalar color; // 颜色 double scale; // 字体大小 }; /* 绘制流管理器 @brief: 收集绘制命令: 通过重载的\u0026laquo;操作符接收各种绘制元素 批量执行绘制: 通过\u0026raquo;操作符将所有收集的命令绘制到图形上 命令管理: 可以清空所有收集的绘制命令\n/ class FlaskStream { public: FlaskStream\u0026amp; operator\u0026laquo;(const char str); FlaskStream\u0026amp; operator\u0026laquo;(const std::string\u0026amp; str); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskPoint\u0026amp; pt); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskLine\u0026amp; line); FlaskStream\u0026amp; operator\u0026laquo;(const std::vector\u0026amp; lines); FlaskStream\u0026amp; operator\u0026laquo;(const FlaskText\u0026amp; text); FlaskStream\u0026amp; operator\u0026raquo;(cv::Mat\u0026amp; img); void clear(); private: std::vectorstd::string logs; std::vector pts; std::vector lines; std::vector texts; }; // 用于复现的瞄准参数 // 移植代码的时候将这段代码移植到自瞄那里 struct ShootParam { double v0 = 0.; // 子弹初速度 double aim_angle = 0.; // 发射仰角 // Eigen::Vector3d aim_xyz_i_barrel = Eigen::Vector3d::Zero(); // 枪管坐标系瞄准点 (没有什么作用) Eigen::Vector3d target_xyz_i_camera = Eigen::Vector3d::Zero(); // 相机坐标系目标点 }; // 子弹命中位置信息 struct HitPos { bool hit; Eigen::Vector3d pos; // 子弹在世界坐标系上的位置 }; // 子弹图像投影信息 struct HitCircle { bool hit; math::CircleF circle; // 子弹在图像上的投影圆 }; // 匹配代价评估 struct CaughtCost { bool caught; // 是否满足匹配条件 double cost; // 匹配代价(越小越好) }; // 子弹弹道物理模拟器 class ProjectileSimulator { public: ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t) : coorConverter{coorConverter},shoot_param{shoot_param} ,fire_t{fire_t} { ROS_WARN(\u0026ldquo;构造函数: %lld\u0026rdquo;,fire_t); } // 子弹在图像平面上的投影计算 auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle; // 计算在指定时间t的子弹位置 auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos; // 获取开火时间 auto get_fire_t() const -\u0026gt; double; private: const double g { 9.8 }; const long long fire_t; CoordinateTransformer* coorConverter; ShootParam shoot_param; }; // 子弹位置信息 struct IdPos { int id; Eigen::Vector3d pos; }; // 子弹投影圆信息 struct IdCircle { int id; math::CircleF circle; // 子弹在图像平面上的投影圆 }; // 子弹模拟器封装 struct IdProj { int id; ProjectileSimulator proj; // 子弹物理模拟器实例 }; // 自动瞄准误差校准(目前仅用来复现理想弹道) class AimCorrector { public: AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param); // 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆 auto get_circles(long long now_time) -\u0026gt; std::vector; auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void; private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 std::string config_path_; // 存储配置路径 ShootParam shoot_param; long long next_id = 0; long long last_fire_time = 0; }; cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z); FlaskPoint pos_to_map_point( const Eigen::Vector3d\u0026amp; pos, const cv::Scalar\u0026amp; color, const int\u0026amp; radius, const int\u0026amp; thickness ); // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN); } #endif // TRAJECTORY_VISUALIZER_HPP #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo; #include namespace tools{ auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { ROS_WARN(\u0026ldquo;22222222222222222222222222222222\u0026rdquo;); HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet.pos); // 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized(); // 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos); double radius = math::get_dis(edge_xy_u, center_xy_u); // 这里数学库要记得改成double类型,这里数学库应该还是float类型 // 这里数学库的这个函数已经更改成double类型 return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) }; } auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos { ROS_WARN(\u0026ldquo;33333333333333333333333333333333333333333333333\u0026rdquo;); // 将毫秒转换成毫秒 double t_s = static_cast(t * 0.001); double fire_t_s = static_cast(this-\u0026gt;fire_t * 0.001); double k = 0.1; // 空气阻力系数 // 计算水平位移 double w = (t_s - fire_t_s) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle); // 计算高度 double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w / (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle)) + this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k / k; cout \u0026laquo; \u0026ldquo;w: \u0026quot; \u0026laquo; w \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;h: \u0026quot; \u0026laquo; h \u0026laquo; endl; ROS_WARN(\u0026ldquo;444444444444444444444444444444444444\u0026rdquo;); // 弹道轨迹仅取决于目标点(理想弹道) const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera); // 计算基准方向 const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized(); const Eigen::Vector3d h_norm = { 0., 0., 1. }; const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm; const Eigen::Vector3d bullet_xyz_i_camera =this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel); const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0), bullet_xyz_i_barrel(1, 0) }; const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0) }; return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(),bullet_xyz_i_camera}; } auto ProjectileSimulator::get_fire_t() const -\u0026gt; double { return this-\u0026gt;fire_t; } AimCorrector::AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param) { this-\u0026gt;shoot_param = shoot_param; } auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ // 初始化结果向量 std::vector res; // 开始遍历子弹列表 bullets: 存储所有活跃子弹模拟器的链表 for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) { // 检查子弹是否已发射 // 当前图像时间 \u0026lt; 子弹发射时间 // 是 -\u0026gt; 子弹还未发射,跳过 // 否 -\u0026gt; 子弹已发射,继续处理 ROS_WARN(\u0026ldquo;now_time: %lld, fire_t: %lld, 差值: %lldms\u0026rdquo;, now_time, it-\u0026gt;proj.get_fire_t(), it-\u0026gt;proj.get_fire_t() - now_time); if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111\u0026rdquo;); ++it; continue; } ROS_WARN(\u0026ldquo;66666666666666666666666666666666\u0026rdquo;); // 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time); // 检查子弹是否已击中 -\u0026gt; 已击中删除 if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it); } else { // 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle }); ++it; } } return res; } // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 50; // 发射间隔：50毫秒 // 将秒转换为毫秒 long long eTime_ms = static_cast(eTime * 1000); long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); long long additional_delay = 25; // 0.025秒 = 25毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) { bullets.push_back(IdProj { next_id++, ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime_ms + additional_delay + command_timespan_ms) }); this-\u0026gt;last_fire_time = current_time; } } } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const char* str) { this-\u0026gt;logs.emplace_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::string\u0026amp; str) { this-\u0026gt;logs.push_back(str); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskPoint\u0026amp; pt) { this-\u0026gt;pts.push_back(pt); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskLine\u0026amp; line) { this-\u0026gt;lines.push_back(line); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const std::vector\u0026amp; lines) { for (const auto\u0026amp; line: lines) { this-\u0026gt;lines.push_back(line); } return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026laquo;(const FlaskText\u0026amp; text) { this-\u0026gt;texts.push_back(text); return *this; } FlaskStream\u0026amp; FlaskStream::operator\u0026raquo;(cv::Mat\u0026amp; img) { int cnt = 0; for (auto\u0026amp; str: this-\u0026gt;logs) { cv::putText( img, str, { 20, 80 + cnt * 24 }, cv::FONT_HERSHEY_DUPLEX, 0.8, { 0, 0, 255 } ); ++cnt; } for (auto\u0026amp; pt: this-\u0026gt;pts) { cv::circle(img, pt.pt, pt.radius, pt.color, pt.thickness); } for (auto\u0026amp; line: this-\u0026gt;lines) { cv::line(img, line.pt_pair.first, line.pt_pair.second, line.color, line.thickness); } for (auto\u0026amp; text: this-\u0026gt;texts) { cv::putText( img, text.str, { int(text.pt.x), int(text.pt.y) }, cv::FONT_HERSHEY_DUPLEX, text.scale, text.color ); } return this; } void FlaskStream::clear() { this-\u0026gt;logs.clear(); this-\u0026gt;pts.clear(); this-\u0026gt;lines.clear(); this-\u0026gt;texts.clear(); } cv::Scalar heightened_color(const cv::Scalar\u0026amp; color, const double\u0026amp; z) { cv::Scalar res; for (int i = 0; i \u0026lt; 3; ++i) { res[i] = z \u0026gt;= 0. ? 255. - (255. - color[i]) * std::pow(0.5, z / FLASK_MAP_PETER_BY_BRIGHT) : color[i] * std::pow(0.5, -z / FLASK_MAP_PETER_BY_BRIGHT); } return res; } // FlaskPoint pos_to_map_point( // const Eigen::Vector3d\u0026amp; pos, // const cv::Scalar\u0026amp; color, // const int\u0026amp; radius, // const int\u0026amp; thickness // ) { // return FlaskPoint( // { float( // FLASK_MAP_MID_X // + pos(0, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ), // float( // FLASK_MAP_MID_Y // - pos(1, 0) * base::get_param(\u0026ldquo;auto-aim.debug.flask.map.pixel-per-meter\u0026rdquo;) // ) }, // heightened_color(color, pos(2, 0)), // radius, // thickness // ); // } // auto Stm32Shoot::add(const int\u0026amp; id, const double\u0026amp; img_t) -\u0026gt; void { // // 时间超过 t + latency 后可以发射 // if (this-\u0026gt;pending_signals.size() + 1 \u0026lt;= Stm32Shoot::MAX_SZ) { // this-\u0026gt;pending_signals.push_back(Stm32Shoot::IdT { id, img_t }); // } // } // auto Stm32Shoot::get_last_shoot_id(const double\u0026amp; img_t) -\u0026gt; int { // // 实际上是传输过去有延迟， // while (!this-\u0026gt;pending_signals.empty() // \u0026amp;\u0026amp; img_t \u0026gt;= this-\u0026gt;pending_signals.front().img_t + Stm32Shoot::SHOOT_LATENCY) // { // // 信号已经到达，进行信号处理 // if (this-\u0026gt;pending_signals.front().img_t \u0026gt;= this-\u0026gt;last_shoot.img_t // + base::get_param(\u0026ldquo;auto-aim.ec-simulator.shoot-interval\u0026rdquo;)) // { // this-\u0026gt;last_shoot = this-\u0026gt;pending_signals.front(); // } // this-\u0026gt;pending_signals.pop_front(); // } // return this-\u0026gt;last_shoot.id; // } // 绘制模拟发射的子弹 void draw_simulated_bullets(CoordinateTransformer const coorConverter,const ShootParam\u0026amp; shoot_param,cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN){ FlaskStream flask_aim; FlaskStream flask_map; flask_aim.clear(); flask_map.clear(); AimCorrector aim_corrector = AimCorrector(coorConverter,shoot_param); // 更新子弹序列 // 传入当前帧的时间和当前帧的瞄准姿态 aim_corrector.update_bullet(now_time,eTime,COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time); for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center, { 0, 0, 255 }, bullet.circle.r, 2 ); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id), { bullet.circle.center.x + 20.f, bullet.circle.center.y }, { 0, 0, 255 }, 0.8 ); // flask_map \u0026laquo; pos_to_map_point(bullet.pos,{0, 0, 255}, 4,-1); } flask_aim \u0026raquo; img; } } 分析一下是什么问题导致的?\n从日志可以看到，构造函数中打印的fire_t是正常的（比如1766498932101，和当前时间now_time接近，加上延迟后大约为当前时间+延迟毫秒数）。但是在get_circles中打印的it-\u0026gt;proj.get_fire_t()却是一个巨大的数4627730092099895296。 注意到，在ProjectileSimulator类中，fire_t成员变量是const long long类型，并且在构造函数中初始化。但是，在get_fire_t()函数中，返回的是double类型。这里可能存在类型转换问题。 另外，在get_circles函数中，我们打印时使用的是%lld格式，但是it-\u0026gt;proj.get_fire_t()返回的是double，这会导致未定义行为（因为printf期望的是long long，但传入的是double，可能按double的二进制表示解释为整数，从而得到一个非常大的数）。 解决方案： 将get_fire_t()的返回类型改为long long，以匹配fire_t的类型。 确保在打印时使用正确的格式。\n但是，注意在get_circles函数中，我们使用了ROS_WARN打印，其中%lld期望的是long long，而如果get_fire_t()返回double，则会导致问题。 让我们检查一下： 在头文件中，get_fire_t()声明为返回double，但实际存储的fire_t是long long。在实现中，get_fire_t()返回this-\u0026gt;fire_t，这里将long long隐式转换为double。\n在get_circles函数中，我们使用%lld打印一个double，这是错误的。 有两种修改方式： 方式一：修改get_fire_t()返回long long，这样打印和比较都用long long。 方式二：在打印时将double转换为long long，但注意double可能无法精确表示这么大的整数（因为double的尾数只有53位，而long long是64位，可能会丢失精度）。\n由于fire_t是时间戳（毫秒），可能超过double的精确表示范围（2^53约等于9e15），而我们的fire_t大约1e12，还在精确范围内，但是为了安全和一致性，我们使用long long。 因此，我建议修改get_fire_t()返回long long。 修改步骤： 在头文件中，将get_fire_t()的返回类型改为long long。 在实现文件中，将get_fire_t()的返回类型改为long long，并返回fire_t。 确保在get_circles函数中，使用%lld打印long long。\n另外，在get_pos_by_t函数中，我们使用了this-\u0026gt;fire_t，它也是long long，所以在计算时转换为double。 同时，注意在update_bullet函数中，我们传递的fire_t是long long，所以没有问题。 让我们修改代码。 根据日志，问题非常明显了！让我分析一下： 问题分析\n1. 构造函数中的时间正常 从日志可以看到： 构造函数打印的 fire_t 是正常的，如 1766498932101（这是正常的时间戳） 这个值大约是当前时间 now_time 加上延迟（50ms间隔 + 各种延迟）\n2. 但是 get_fire_t() 返回的是错误的巨大值 在 get_circles 函数中，it-\u0026gt;proj.get_fire_t() 返回的是 4627730092099895296（约4.6e18），这完全不对！ 根本原因 问题出在 头文件和实现文件之间的类型不匹配： 头文件声明： cpp\nauto get_fire_t() const -\u0026gt; double; // 声明返回 double 实现文件实现： cpp\nauto ProjectileSimulator::get_fire_t() const -\u0026gt; double {return this-\u0026gt;fire_t; // fire_t 是 long long，隐式转换为 double} 问题：当 long long 转换为 double 时，由于 double 的精度问题（只有53位尾数），而 long long 是64位整数，可能会发生精度丢失，导致值变成完全不同的数字。 解决方案\n方案1：统一使用 long long 类型（推荐） cpp\n// 修改头文件auto get_fire_t() const -\u0026gt; long long; // 改为返回 long long cpp\n// 修改实现文件auto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {return this-\u0026gt;fire_t;} 方案2：统一使用 double 类型 cpp\n// 修改头文件class ProjectileSimulator {public:ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const double\u0026amp; fire_t) // 参数改为 double: coorConverter{coorConverter}, shoot_param{shoot_param}, fire_t{fire_t} {ROS_WARN(\u0026ldquo;构造函数: %.0f\u0026rdquo;, fire_t);}private:const double fire_t; // 改为 double}; 完整修复代码\n修改头文件 (trajectory_visualizer.hpp) cpp #ifndef TRAJECTORY_VISUALIZER_HPP#define TRAJECTORY_VISUALIZER_HPP#include \u0026ldquo;math.hpp\u0026rdquo;#include \u0026ldquo;CoorConverter.hpp\u0026rdquo;#include \u0026lt;opencv2/opencv.hpp\u0026gt;#include \u0026ldquo;GimbalPos.hpp\u0026rdquo;#include \u0026ldquo;ros/ros.h\u0026quot;namespace tools {// \u0026hellip; 其他结构体定义保持不变 \u0026hellip;// 子弹弹道物理模拟器class ProjectileSimulator {public:ProjectileSimulator(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param,const long long\u0026amp; fire_t): coorConverter{coorConverter}, shoot_param{shoot_param}, fire_t{fire_t} {ROS_WARN(\u0026ldquo;构造函数: %lld\u0026rdquo;, fire_t);}// 子弹在图像平面上的投影计算auto get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle;// 计算在指定时间t的子弹位置auto get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos;// 修改这里：返回 long longauto get_fire_t() const -\u0026gt; long long;private:const double g { 9.8 };const long long fire_t; // long long 类型 CoordinateTransformer* coorConverter; ShootParam shoot_param;};// \u0026hellip; 其他类定义保持不变 \u0026hellip;}#endif // TRAJECTORY_VISUALIZER_HPP 修改实现文件 (trajectory_visualizer.cpp) cpp #include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo;#include namespace tools {auto ProjectileSimulator::get_circle_by_t(const double\u0026amp; t) const -\u0026gt; HitCircle { HitPos bullet = this-\u0026gt;get_pos_by_t(t); Eigen::Vector3d xyz_c = this-\u0026gt;coorConverter-\u0026gt;cam2Map(bullet.pos);// 沿着正 y 轴与视角的叉积方向得到一个边缘坐标，以计算半径 Eigen::Vector3d crossed = Eigen::Vector3d(0., 1., 0.).cross(xyz_c).normalized();// 这里用到的参数应该是小弹丸的半径 Eigen::Vector3d edge_xyz_c = xyz_c + crossed * 0.0085; Eigen::Vector3d edge_xyz_i = this-\u0026gt;coorConverter-\u0026gt;cam2Map(edge_xyz_c); cv::Point2d edge_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(edge_xyz_i); cv::Point2d center_xy_u = this-\u0026gt;coorConverter-\u0026gt;map2pixel(bullet.pos);double radius = math::get_dis(edge_xy_u, center_xy_u);return HitCircle { bullet.hit, math::CircleF(edge_xy_u, radius) };}auto ProjectileSimulator::get_pos_by_t(const double\u0026amp; t) const -\u0026gt; HitPos {// 将毫秒转换成秒double t_s = t * 0.001;double fire_t_s = this-\u0026gt;fire_t * 0.001;double k = 0.1; // 空气阻力系数// 计算水平位移double w = (t_s - fire_t_s) * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle);// 计算高度double h = (k * this-\u0026gt;shoot_param.v0 * std::sin(this-\u0026gt;shoot_param.aim_angle) + this-\u0026gt;g) * k * w/ (k * k * this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))+ this-\u0026gt;g * std::log(1. - (k * w) / (this-\u0026gt;shoot_param.v0 * std::cos(this-\u0026gt;shoot_param.aim_angle))) / k/ k;// 弹道轨迹仅取决于目标点(理想弹道)const Eigen::Vector3d target_xyz_i_barrel = this-\u0026gt;coorConverter-\u0026gt;cam2Gun(this-\u0026gt;shoot_param.target_xyz_i_camera);// 计算基准方向const Eigen::Vector3d w_norm = Eigen::Vector3d(target_xyz_i_barrel(0, 0), target_xyz_i_barrel(1, 0), 0).normalized();const Eigen::Vector3d h_norm = { 0., 0., 1. };const Eigen::Vector3d bullet_xyz_i_barrel = w * w_norm + h * h_norm;const Eigen::Vector3d bullet_xyz_i_camera = this-\u0026gt;coorConverter-\u0026gt;gun2Cam(bullet_xyz_i_barrel);const Eigen::Vector2d bullet_xy_i_barrel = { bullet_xyz_i_barrel(0, 0),bullet_xyz_i_barrel(1, 0) };const Eigen::Vector2d target_xy_i_barrel = { target_xyz_i_barrel(0, 0),target_xyz_i_barrel(1, 0) };return HitPos { bullet_xy_i_barrel.norm() \u0026gt;= target_xy_i_barrel.norm(), bullet_xyz_i_camera };}// 修改这里：返回 long longauto ProjectileSimulator::get_fire_t() const -\u0026gt; long long {return this-\u0026gt;fire_t;}// \u0026hellip; 其他函数实现保持不变 \u0026hellip;} 3. 修改调试输出 在 get_circles 函数中，确保打印格式正确： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {// 获取发射时间long long fire_t = it-\u0026gt;proj.get_fire_t();// 打印详细信息ROS_WARN(\u0026ldquo;时间比较: now_time=%lld, fire_t=%lld, 差值=%lldms\u0026rdquo;, now_time, fire_t, fire_t - now_time);if (now_time \u0026lt; fire_t) {ROS_WARN(\u0026ldquo;子弹未发射，还需等待 %lld 毫秒\u0026rdquo;, fire_t - now_time);++it; continue;}ROS_WARN(\u0026ldquo;子弹已发射！已飞行 %lld 毫秒\u0026rdquo;, now_time - fire_t);// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中 -\u0026gt; 已击中删除if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else {// 处理未击中的子弹 -\u0026gt; 未击中添加到结果,迭代器 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;} 关键修改点 类型一致性：get_fire_t() 现在返回 long long，与存储的类型一致 避免精度丢失：不再进行 long long 到 double 的隐式转换 正确的格式说明符：使用 %lld 打印 long long 类型\n为什么会出现这个错误？ 当 long long 值被当作 double 传递给 printf 的 %lld 格式时，会发生未定义行为。printf 会从内存中读取64位整数，但由于传递的是 double（8字节，但内部表示不同），它读取到的就是错误的数据。 简而言之：你告诉 printf 要读取一个 long long，但实际上传递了一个 double，导致内存解释错误。 修复后，程序应该能正确显示子弹发射时间，并且当 now_time \u0026gt;= fire_t 时，子弹就会开始显示飞行轨迹。 [ WARN] [1766499224.339994458]: 111111111111111111111 [ WARN] [1766499224.365333418]: 构造函数: 1766499224390 [ WARN] [1766499224.365381748]: now_time: 1766499224365, fire_t: 1766499224390, 差值: 25ms [ WARN] [1766499224.365392342]: 111111111111111111111 [hikcamera-2] process has died [pid 21047, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/8b0b56d2-e009-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/8b0b56d2-e009-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [21055] [ WARN] [1766499224.391052007]: 构造函数: 1766499224416 [ WARN] [1766499224.391097736]: now_time: 1766499224391, fire_t: 1766499224416, 差值: 25ms [ WARN] [1766499224.391112294]: 111111111111111111111 [ WARN] [1766499224.416360678]: 构造函数: 1766499224441 [ WARN] [1766499224.416391542]: now_time: 1766499224416, fire_t: 1766499224441, 差值: 25ms [ WARN] [1766499224.416399873]: 111111111111111111111 [ WARN] [1766499224.440360583]: 构造函数: 1766499224465 [ WARN] [1766499224.440400402]: now_time: 1766499224440, fire_t: 1766499224465, 差值: 25ms [ WARN] [1766499224.440410683]: 111111111111111111111 [ WARN] [1766499224.466583353]: 构造函数: 1766499224491 [ WARN] [1766499224.466615975]: now_time: 1766499224466, fire_t: 1766499224491, 差值: 25ms [ WARN] [1766499224.466625743]: 111111111111111111111 [ WARN] [1766499224.491281160]: 构造函数: 1766499224516 [ WARN] [1766499224.491366704]: now_time: 1766499224491, fire_t: 1766499224516, 差值: 25ms [ WARN] [1766499224.491378868]: 111111111111111111111 [ WARN] [1766499224.516487291]: 构造函数: 1766499224541 [ WARN] [1766499224.516521151]: now_time: 1766499224516, fire_t: 1766499224541, 差值: 25ms [ WARN] [1766499224.516531777]: 111111111111111111111 [ WARN] [1766499224.539867514]: 构造函数: 1766499224564 [ WARN] [1766499224.539899906]: now_time: 1766499224539, fire_t: 1766499224564, 差值: 25ms [ WARN] [1766499224.539910284]: 111111111111111111111 [ WARN] [1766499224.565722170]: 构造函数: 1766499224590 [ WARN] [1766499224.565766930]: now_time: 1766499224565, fire_t: 1766499224590, 差值: 25ms [ WARN] [1766499224.565778946]: 111111111111111111111 [ WARN] [1766499224.590320500]: 构造函数: 1766499224615 [ WARN] [1766499224.590361457]: now_time: 1766499224590, fire_t: 1766499224615, 差值: 25ms [ WARN] [1766499224.590370662]: 111111111111111111111 [ WARN] [1766499224.615417853]: 构造函数: 1766499224640 [ WARN] [1766499224.615466837]: now_time: 1766499224615, fire_t: 1766499224640, 差值: 25ms [ WARN] [1766499224.615480620]: 111111111111111111111 [ WARN] [1766499224.639006819]: 构造函数: 1766499224663 [ WARN] [1766499224.639049653]: now_time: 1766499224638, fire_t: 1766499224663, 差值: 25ms [ WARN] [1766499224.639058831]: 111111111111111111111 [ WARN] [1766499224.664996044]: 构造函数: 1766499224689 [ WARN] [1766499224.665024559]: now_time: 1766499224664, fire_t: 1766499224689, 差值: 25ms [ WARN] [1766499224.665033664]: 111111111111111111111 [ WARN] [1766499224.691008541]: 构造函数: 1766499224715 [ WARN] [1766499224.691054708]: now_time: 1766499224690, fire_t: 1766499224715, 差值: 25ms [ WARN] [1766499224.691068111]: 111111111111111111111 [hikcamera-2] process has died [pid 21055, exit code 255, cmd /home/rm/ws_glut_vison/devel/lib/rm_hikcamera/hikcamera __name:=hikcamera __log:=/home/rm/.ros/log/8b0b56d2-e009-11f0-881b-033937e15f81/hikcamera-2.log]. log file: /home/rm/.ros/log/8b0b56d2-e009-11f0-881b-033937e15f81/hikcamera-2*.log [hikcamera-2] restarting process process[hikcamera-2]: started with pid [21064] [ WARN] [1766499224.716570775]: 构造函数: 1766499224741 [ WARN] [1766499224.716618554]: now_time: 1766499224716, fire_t: 1766499224741, 差值: 25ms [ WARN] [1766499224.716633023]: 111111111111111111111 [ WARN] [1766499224.741707545]: 构造函数: 1766499224766 [ WARN] [1766499224.741756017]: now_time: 1766499224741, fire_t: 1766499224766, 差值: 25ms [ WARN] [1766499224.741767048]: 111111111111111111111 [ WARN] [1766499224.765959369]: 构造函数: 1766499224790 [ WARN] [1766499224.766089369]: now_time: 1766499224765, fire_t: 1766499224790, 差值: 25ms [ WARN] [1766499224.766110943]: 111111111111111111111 [ WARN] [1766499224.792502752]: 构造函数: 1766499224817 [ WARN] [1766499224.792547161]: now_time: 1766499224792, fire_t: 1766499224817, 差值: 25ms [ WARN] [1766499224.792561376]: 111111111111111111111 [ WARN] [1766499224.816675912]: 构造函数: 1766499224841 [ WARN] [1766499224.816736832]: now_time: 1766499224816, fire_t: 1766499224841, 差值: 25ms [ WARN] [1766499224.816750511]: 111111111111111111111 [ WARN] [1766499224.841117446]: 构造函数: 1766499224866 [ WARN] [1766499224.841215303]: now_time: 1766499224841, fire_t: 1766499224866, 差值: 25ms [ WARN] [1766499224.841235442]: 111111111111111111111 [ WARN] [1766499224.867828799]: 构造函数: 1766499224892 [ WARN] [1766499224.867885115]: now_time: 1766499224867, fire_t: 1766499224892, 差值: 25ms [ WARN] [1766499224.867899800]: 111111111111111111111 [ WARN] [1766499224.893400233]: 构造函数: 1766499224918 [ WARN] [1766499224.893437171]: now_time: 1766499224893, fire_t: 1766499224918, 差值: 25ms [ WARN] [1766499224.893445229]: 111111111111111111111 [ WARN] [1766499224.918721369]: 构造函数: 1766499224943 [ WARN] [1766499224.918763103]: now_time: 1766499224918, fire_t: 1766499224943, 差值: 25ms [ WARN] [1766499224.918773842]: 111111111111111111111 [ WARN] [1766499224.942702635]: 构造函数: 1766499224967 [ WARN] [1766499224.942731168]: now_time: 1766499224942, fire_t: 1766499224967, 差值: 25ms [ WARN] [1766499224.942738151]: 111111111111111111111 [ WARN] [1766499224.967903158]: 构造函数: 1766499224992 [ WARN] [1766499224.967943012]: now_time: 1766499224967, fire_t: 1766499224992, 差值: 25ms [ WARN] [1766499224.967952577]: 111111111111111111111 [ WARN] [1766499224.993416062]: 构造函数: 1766499225018 [ WARN] [1766499224.993469889]: now_time: 1766499224993, fire_t: 1766499225018, 差值: 25ms 我已经统一成double 了为什么还是没有跑到那个 if (now_time \u0026lt; it-\u0026gt;proj.get_fire_t()) { ROS_WARN(\u0026ldquo;111111111111111111111\u0026rdquo;); ++it; continue; }的下面打印出666666666666 问题分析： 从日志中可以看出，now_time 和 fire_t 的差值一直是25毫秒，且now_time始终小于fire_t，所以每次都会进入if语句，打印111111111，然后continue，不会执行后面的代码。 为什么差值总是25毫秒？ 根据你的设计，在update_bullet中，你设置的发射间隔是50毫秒，并且你设置了延迟：eTime_ms + additional_delay + command_timespan_ms。但是，从日志看，构造函数中打印的fire_t和当前的now_time差值都是25毫秒，而不是你预期的延迟（应该是eTime+0.025+COMMAND_TIMESPAN，单位是秒，转换成毫秒后应该不止25毫秒）。 可能的原因： 你在update_bullet中计算延迟时，传入的eTime和COMMAND_TIMESPAN可能是0，或者很小，导致总延迟只有25毫秒（即additional_delay）。 你的now_time是当前时间，而fire_t是当前时间加上25毫秒，所以总是差25毫秒。\n但是，即使差25毫秒，由于now_time \u0026lt; fire_t，所以子弹仍然被认为是未发射，所以不会进入后面的代码。 你想要的效果是：子弹在创建后，经过一段延迟（比如137毫秒）再发射。但是，从日志看，你创建的子弹的发射时间（fire_t）只比当前时间晚25毫秒，所以你需要等待25毫秒，子弹才会发射。 然而，由于你的程序运行很快，每50毫秒创建一个新子弹，每个子弹的发射时间都是创建时间+25毫秒。但是，你的now_time是当前时间，它也在增长。所以，你可能会看到，当now_time增长到超过某个子弹的fire_t时，那个子弹就会发射。 但是，从日志看，你打印的时间差都是25毫秒，这意味着你每次检查的时候，子弹都还需要25毫秒才发射。这可能是因为你的程序运行频率很高，每25毫秒左右就检查一次，而子弹的发射时间总是当前时间+25毫秒，所以总是差25毫秒。 实际上，从日志的时间戳看，两次打印的时间间隔大约是25毫秒（例如1766499224.339994到1766499224.365333，间隔约25毫秒）。所以，每次检查时，子弹的发射时间都是当前时间+25毫秒，所以永远不会发射。 这可能是由于你计算fire_t的方式不对。在update_bullet中，你计算fire_t为： cpp current_time + eTime_ms + additional_delay + command_timespan_ms\n但是，你传入的eTime和COMMAND_TIMESPAN可能是0，所以实际上只加了25毫秒（additional_delay）。 你需要检查在Tracker中调用draw_simulated_bullets时传入的eTime和COMMAND_TIMESPAN是多少。可能是这两个参数都是0，所以总延迟只有25毫秒。 另外，你可能需要调整你的逻辑：如果你希望子弹在创建后立即发射，那么就不应该加延迟。如果你希望模拟真实的延迟，那么应该加上正确的延迟值。 从你的描述中，你希望子弹有发射延迟（曝光、开火延迟、自瞄程序运行耗时等），所以你应该将这些延迟正确地计算出来并传入。 现在，由于延迟只有25毫秒，而你的程序运行间隔大约是25毫秒，所以每次检查时，子弹都还没有到发射时间。 解决方法： 确保传入的eTime和COMMAND_TIMESPAN是正确的值（单位是秒）。在Tracker中，你从配置文件中读取了这些值，应该将它们传入。 如果你希望子弹在创建后立即发射，可以将延迟设置为0。\n另外，注意：你的发射间隔是50毫秒，而子弹的发射延迟是25毫秒，所以子弹会在创建后的25毫秒发射。但是，由于你的检查频率很高（大约25毫秒一次），所以第一次检查时，子弹还需要25毫秒发射，第二次检查时，子弹可能已经发射了（因为两次检查间隔25毫秒，而子弹的发射延迟也是25毫秒）。 但是，从日志看，每次检查的时间间隔和子弹的剩余延迟时间几乎相同，所以总是差25毫秒。这可能是因为你的系统时间戳精度问题，或者你的循环运行频率刚好是40Hz（25毫秒一次）。 你可以尝试在get_circles函数中，将条件改为小于等于0，即： cpp\nif (now_time \u0026lt;= it-\u0026gt;proj.get_fire_t()) { 这样，当时间相等时，也会认为子弹已发射。 或者，你可以将发射时间设置得比当前时间稍微提前一点，比如减去1毫秒，以抵消时间戳的误差。 但是，更根本的问题是，你的延迟计算可能不对。请检查在Tracker中调用draw_simulated_bullets时传入的eTime和COMMAND_TIMESPAN的值。 如果这些值都是0，那么总延迟就是25毫秒。而你的循环周期大约是25毫秒，所以每次检查时，子弹都还没有到发射时间。 你可以尝试将additional_delay设置为0，这样子弹的发射时间就是当前时间，那么下次检查时，now_time应该大于等于fire_t，就会进入后面的代码。 总结： 检查传入的eTime和COMMAND_TIMESPAN参数。 考虑调整条件判断，使用now_time \u0026lt;= fire_t。 考虑减少延迟，或者调整循环频率，使得子弹能够在预期的时间内发射。\n另外，注意你的hikcamera进程一直在重启，可能是相机驱动问题，但这可能不影响你的逻辑。 现在，为了快速测试，你可以将additional_delay设置为0，然后看看子弹是否会发射。如果还是不行，可能是你的时间戳单位或计算有问题。 请尝试修改update_bullet中的延迟计算，将fire_t设置为current_time，即立即发射，然后观察是否能够进入后面的代码。 如果立即发射可以，那么说明问题在于延迟计算。如果立即发射还是不行，那么可能是其他问题，比如时间戳的单位不一致等。 从日志可以看到，现在时间值已经正常了！但问题是： 子弹的发射时间 fire_t 总是比当前时间 now_time 大25毫秒，所以 now_time \u0026lt; fire_t 始终成立，子弹一直被判定为\u0026quot;未发射\u0026quot;状态。 问题分析\n日志显示： now_time: 1766499224365 fire_t: 1766499224390 差值: 25毫秒\n这意味着你设置的发射延迟正好是25毫秒，导致每次检查时子弹都还需要等待25毫秒才能发射。 为什么子弹一直不发射？ 在你的代码中，update_bullet 函数计算发射时间为： cpp current_time + eTime_ms + additional_delay + command_timespan_ms\n从日志看，总延迟 = 25毫秒。这很可能是： eTime_ms = 0 (eTime = 0秒) command_timespan_ms = 0 (COMMAND_TIMESPAN = 0秒) additional_delay = 25 (固定25毫秒) 解决方案\n方案1：放宽发射条件 让子弹在接近发射时间时就显示： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {long long fire_t = it-\u0026gt;proj.get_fire_t();long long time_diff = fire_t - now_time;// 如果子弹将在未来10毫秒内发射，就认为它已经发射了if (time_diff \u0026gt; 10) { // 原来是 if (now_time \u0026lt; fire_t)ROS_WARN(\u0026ldquo;子弹未发射，还需等待 %lld 毫秒\u0026rdquo;, time_diff);++it; continue;}ROS_WARN(\u0026ldquo;子弹已发射或即将发射！\u0026rdquo;); HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else { res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n方案2：减少发射延迟 修改 update_bullet 中的延迟计算： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000);long long additional_delay = 0; // 改为0毫秒，立即发射if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) {// 使用当前时间，不加延迟long long fire_time = current_time; // 立即发射ROS_WARN(\u0026ldquo;创建子弹: 立即发射，fire_time = now_time = %lld\u0026rdquo;, fire_time); bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n方案3：在Tracker中检查传入的参数 检查你在Tracker中调用 draw_simulated_bullets 时传入的参数： cpp // 在Tracker的reconstruction_choose_compensation函数中： cout \u0026laquo; \u0026ldquo;eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; \u0026ldquo;秒\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; \u0026ldquo;秒\u0026rdquo; \u0026laquo; endl;// 如果这些值都是0，那么总延迟就是25毫秒 tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, eTime, // 检查这个值 COMMAND_TIMESPAN); // 检查这个值\n方案4：最佳方案 - 允许子弹显示发射前的状态 即使子弹还没发射，也可以显示它的\u0026quot;准备状态\u0026rdquo;： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {long long fire_t = it-\u0026gt;proj.get_fire_t();// 总是计算子弹位置，不管是否已发射 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 如果子弹已击中，删除它if (hit_circle.hit) { it = this-\u0026gt;bullets.erase(it);} else {// 子弹未击中，添加到结果// 可以根据发射状态设置不同的颜色 res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}return res;}\n最简单的测试方法 直接在 get_circles 中强制让所有子弹都\u0026quot;已发射\u0026rdquo;： cpp auto AimCorrector::get_circles(long long now_time) -\u0026gt; std::vector{ std::vector res;ROS_WARN(\u0026quot;=== get_circles 开始，子弹数量: %zu ===\u0026rdquo;, bullets.size());for (auto it = this-\u0026gt;bullets.begin(); it != this-\u0026gt;bullets.end();) {// 强制认为所有子弹都已发射（测试用）ROS_WARN(\u0026ldquo;处理子弹 %d，强制认为已发射\u0026rdquo;, it-\u0026gt;id);// 获取子弹在当前时刻的投影圆 HitCircle hit_circle = it-\u0026gt;proj.get_circle_by_t(now_time);// 检查子弹是否已击中 -\u0026gt; 已击中删除if (hit_circle.hit) {ROS_WARN(\u0026ldquo;子弹 %d 已击中，删除\u0026rdquo;, it-\u0026gt;id); it = this-\u0026gt;bullets.erase(it);} else {// 处理未击中的子弹 -\u0026gt; 未击中添加到结果ROS_WARN(\u0026ldquo;子弹 %d 未击中，添加到绘制列表\u0026rdquo;, it-\u0026gt;id); res.push_back(IdCircle { it-\u0026gt;id, hit_circle.circle });++it;}}ROS_WARN(\u0026quot;=== get_circles 结束，返回 %zu 个子弹 ===\u0026rdquo;, res.size());return res;} 根本原因总结\n你的代码逻辑是正确的，但存在一个小问题： 时间精度问题：子弹有25毫秒的发射延迟，而你的程序检查频率大约也是每25毫秒一次。这导致： 第一次检查：子弹还需25毫秒发射\n子弹的发射时间 fire_t 总是比当前时间 now_time 大25毫秒，所以 now_time \u0026lt; fire_t 始终成立，子弹一直被判定为\u0026quot;未发射\u0026quot;状态。 为什么子弹的发射时间总是比当前时间大25ms? 根据日志，子弹的发射时间总是比当前时间大25ms，这是因为在update_bullet函数中，计算发射时间时，你使用了固定的额外延迟25ms（additional_delay = 25），并且可能eTime和COMMAND_TIMESPAN都是0，所以总延迟就是25ms。 但是，这里有一个关键问题：你是在同一帧中创建子弹并立即检查。也就是说，你调用update_bullet时传入的current_time是当前时间，然后你设置子弹的发射时间为current_time + 25ms。紧接着你调用get_circles(now_time)，这里的now_time与current_time相同（或者非常接近，相差几毫秒）。因此，在检查时，子弹的发射时间总是比当前时间大25ms左右。 为了看到子弹发射后的轨迹，你需要等待至少25ms。但是你的程序是实时运行的，每一帧都在创建新的子弹（每50ms创建一颗），并且每一帧都在检查。由于发射延迟（25ms）小于创建间隔（50ms），所以你会不断创建新的子弹，但每一颗子弹在创建后的25ms内都不会被绘制。 如果你希望子弹创建后立即显示（即没有发射延迟），那么你应该将additional_delay设为0，并且确保eTime和COMMAND_TIMESPAN也是0。 但是，从你的需求描述来看，你希望模拟真实的发射延迟（曝光、开火延迟等）。如果是这样，那么子弹在发射延迟内不会显示是正常的。然而，你可能希望看到的是子弹发射后的轨迹，而不是延迟内的等待。 根据你的需求，你有两种选择： 如果你想模拟真实情况，即子弹有发射延迟，那么你需要在子弹发射延迟过后才能看到它的轨迹。这意味着在子弹创建后的前25ms内，你不会看到它，25ms后你才能看到它的飞行轨迹。但是，由于你的程序是连续运行的，你会看到每一颗子弹在创建25ms后开始显示，并且显示它的飞行轨迹直到击中目标。 如果你只是想测试弹道计算，希望子弹立即显示，那么你应该将发射延迟设为0。\n从你的日志来看，你似乎希望子弹立即显示，因为你不理解为什么子弹一直没有显示。所以，我建议你先将发射延迟设为0，确保弹道计算正确，然后再考虑添加延迟。 修改方法： 在update_bullet函数中，将additional_delay设为0，同时检查eTime和COMMAND_TIMESPAN是否为0。如果它们不为0，并且你希望立即显示，那么也应该将它们设为0。 但是，请注意，在Tracker中调用draw_simulated_bullets时，传入的eTime和COMMAND_TIMESPAN可能不是0。你需要检查Tracker中这两个参数的值。 如果你确定希望子弹立即显示，可以这样修改： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000);long long additional_delay = 0; // 改为0毫秒，立即发射if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) {long long fire_time = current_time; // 不加延迟 bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n这样修改后，子弹的发射时间就是当前时间，在get_circles中，now_time应该等于或略大于fire_t（因为创建子弹和检查可能在同一帧，但now_time可能稍微大一点，因为代码执行需要时间），从而进入绘制流程。 如果修改后仍然没有进入绘制流程，请检查now_time和fire_t的打印值，确保now_time大于等于fire_t。 另外，注意你的程序中有多个ROS_WARN打印，可能会影响性能，建议调试完成后减少打印。 最后，如果你希望保留发射延迟，但又想看到子弹在延迟期间的位置（比如在枪口等待），那么你需要修改get_circles函数，即使子弹未发射，也计算其位置（在枪口位置），这样你就可以看到子弹在发射前停留在枪口，发射后再飞出去。但这需要修改弹道计算，在未发射时返回枪口位置。 这是一个很好的问题！让我详细分析为什么子弹的发射时间总是比当前时间大25ms： 原因分析\n1. 查看你的 update_bullet 函数 cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime * 1000);long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000);long long additional_delay = 25; // 0.025秒 = 25毫秒 ← 这里！if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) { bullets.push_back(IdProj { next_id++,// 发射时间 = 当前时间 + 25msProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime_ms + additional_delay + command_timespan_ms)});this-\u0026gt;last_fire_time = current_time;}}}\n2. 数学计算 假设： eTime = 0.002秒 = 2毫秒 COMMAND_TIMESPAN = 0.11秒 = 110毫秒 additional_delay = 25毫秒 总延迟 = 2 + 25 + 110 = 137毫秒\n但是从日志看，差值只有25毫秒！这说明： 很可能 eTime 和 COMMAND_TIMESPAN 都是 0 或非常小！ 3. 在Tracker中检查参数值 你需要检查Tracker中传入的参数： cpp // 在Tracker的reconstruction_choose_compensation函数中 cout \u0026laquo; \u0026ldquo;DEBUG - eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; \u0026quot; (秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;DEBUG - COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; \u0026quot; (秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;DEBUG - eTime_ms: \u0026quot; \u0026laquo; static_cast(eTime * 1000) \u0026laquo; \u0026quot; (毫秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;DEBUG - COMMAND_TIMESPAN_ms: \u0026quot; \u0026laquo; static_cast(COMMAND_TIMESPAN * 1000) \u0026laquo; \u0026quot; (毫秒)\u0026rdquo; \u0026laquo; endl;// 然后调用 tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, eTime, // 很可能是0 COMMAND_TIMESPAN); // 很可能是0\n4. 最可能的情况 从日志差值固定为25ms来看，几乎可以肯定： eTime * 1000 = 0（eTime = 0秒） COMMAND_TIMESPAN * 1000 = 0（COMMAND_TIMESPAN = 0秒） 所以总延迟只有 additional_delay = 25毫秒\n验证方法 方法1：在Tracker中打印参数 cpp\n// 在Tracker构造函数或setParam函数中ROS_WARN(\u0026ldquo;eTime: %.6f秒, COMMAND_TIMESPAN: %.6f秒\u0026rdquo;, eTime, COMMAND_TIMESPAN); 方法2：在draw_simulated_bullets中添加调试 cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) {ROS_WARN(\u0026ldquo;传入参数: eTime=%lld, COMMAND_TIMESPAN=%lld\u0026rdquo;, eTime, COMMAND_TIMESPAN);// \u0026hellip; 其余代码 \u0026hellip;}\n方法3：检查配置文件 检查你的YAML配置文件： yaml Tracker:eTime: 0.002 # 应该是0.002秒 = 2毫秒COMMAND_TIMESPAN: 0.11 # 应该是0.11秒 = 110毫秒 解决方案\n方案1：修复参数传递 如果参数确实是0，修复它们： cpp // 在Tracker中确保使用正确的参数 tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, static_cast(eTime * 1000), // 确保转换为毫秒static_cast(COMMAND_TIMESPAN * 1000));\n方案2：修改update_bullet函数接受秒参数 cpp\nauto AimCorrector::update_bullet(long long current_time, double eTime_sec, double command_timespan_sec) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 将秒转换为毫秒long long eTime_ms = static_cast(eTime_sec * 1000);long long command_timespan_ms = static_cast(command_timespan_sec * 1000);long long additional_delay = 25; // 0.025秒 = 25毫秒ROS_WARN(\u0026ldquo;延迟计算: eTime_ms=%lld, command_timespan_ms=%lld, 总延迟=%lld\u0026rdquo;, eTime_ms, command_timespan_ms, eTime_ms + additional_delay + command_timespan_ms);// \u0026hellip; 其余代码 \u0026hellip;} 方案3：最简方案：直接使用毫秒 cpp auto AimCorrector::update_bullet(long long current_time) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒const long long total_delay = 137; // 固定总延迟137毫秒（2+25+110）if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) {long long fire_time = current_time + total_delay;ROS_WARN(\u0026ldquo;创建子弹: now=%lld, fire=%lld, 延迟=%lldms\u0026rdquo;, current_time, fire_time, total_delay); bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n立即测试的方法 要立即看到子弹，最简单的是： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50;// 临时：设为0延迟long long fire_time = current_time; // 立即发射if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) { bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, fire_time)});this-\u0026gt;last_fire_time = current_time;}}}\n总结 根本原因：你的 eTime 和 COMMAND_TIMESPAN 参数很可能是0，所以总延迟只有固定的25ms（来自 additional_delay）。 解决方案： 检查并修复Tracker中传入的参数 或者修改代码让子弹立即发射（用于测试） 或者等待25ms让子弹自然发射\n由于25ms的延迟很短，如果你运行程序足够长时间，子弹最终会发射（当 now_time 超过 fire_t 时）。但你的程序可能每帧都在创建新子弹，而每颗新子弹又有25ms延迟，所以看起来子弹永远不发射。 快速测试：将 additional_delay 设为0，就能立即看到子弹了！ [identify-4] killing on exit [hikcamera-2] killing on exit eTime_ms: 0 command_timespan_ms: 0 eTime_ms: 0 command_timespan_ms: 0 eTime_ms: 0 command_timespan_ms: 0 eTime_ms: 0 command_timespan_ms: 0 eTime_ms: 0 command_timespan_ms: 0 eTime_ms: 0 command_timespan_ms: 0 [rosout-1] killing on exit [master] killing on exit // 将秒转换为毫秒 long long eTime_ms = static_cast(eTime * 1000); long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); long long additional_delay = 25; // 0.025秒 = 25毫秒 cout \u0026laquo; \u0026ldquo;eTime_ms: \u0026quot; \u0026laquo; eTime_ms \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;command_timespan_ms: \u0026quot; \u0026laquo; command_timespan_ms \u0026laquo; endl; 为什么这两个打印出来是零? 问题：在update_bullet函数中，eTime_ms和command_timespan_ms打印出来都是0。 原因：传入的eTime和COMMAND_TIMESPAN参数本身是0，或者转换过程中出现了问题。\n根据日志，打印出来的都是0，说明传入的eTime和COMMAND_TIMESPAN都是0。 我们需要检查在Tracker中调用draw_simulated_bullets时传入的eTime和COMMAND_TIMESPAN参数是否正确。 在Tracker中，这两个参数应该从配置文件中读取，分别对应eTime和COMMAND_TIMESPAN。 检查Tracker的构造函数和setParam函数，确保正确读取了这两个参数。 另外，注意在Tracker的reconstruction_choose_compensation函数中调用draw_simulated_bullets时传入的参数。 可能的原因： 配置文件没有正确设置，导致eTime和COMMAND_TIMESPAN为0。 在Tracker中，没有将这两个参数传递给draw_simulated_bullets。\n解决步骤： 步骤1：在Tracker的构造函数中打印这两个参数，确认是否从配置文件中正确读取。 步骤2：在调用draw_simulated_bullets之前打印这两个参数。 步骤3：确保在调用draw_simulated_bullets时传入的是正确的变量。 根据之前Tracker的代码，我们看到在reconstruction_choose_compensation函数中调用draw_simulated_bullets时，传入的是eTime和COMMAND_TIMESPAN。 但是，注意在Tracker类中，这两个成员变量是否已经正确赋值？在setParam函数中，我们从配置文件中读取了这两个值，所以应该检查setParam函数是否正确读取。 如果配置文件中的键名不对，或者类型不对，可能导致读取失败，使用默认值0。 因此，我们需要检查配置文件和setParam函数。 例如，在setParam函数中，我们使用如下代码读取： COMMAND_TIMESPAN = track[\u0026ldquo;COMMAND_TIMESPAN\u0026rdquo;].as(0.11); eTime = track[\u0026ldquo;eTime\u0026rdquo;].as(0.002);\n如果配置文件中没有这些键，则会使用默认值（0.11和0.002），但如果我们打印出来是0，说明读取到的就是0。 可能配置文件中的节和键名不匹配。请检查配置文件。 假设配置文件路径正确，并且格式如下： Tracker: COMMAND_TIMESPAN: 0.11 eTime: 0.002\n那么，请检查在setParam函数中，是否使用了正确的节点路径。 另外，注意在Tracker的构造函数中，我们调用了setParam函数，所以如果配置文件路径不对，也会导致使用默认值0。 我们可以通过打印来确认。 在Tracker的setParam函数中，添加打印： text COMMAND_TIMESPAN = track[\u0026ldquo;COMMAND_TIMESPAN\u0026rdquo;].as(0.11); eTime = track[\u0026ldquo;eTime\u0026rdquo;].as(0.002); ROS_INFO(\u0026ldquo;COMMAND_TIMESPAN: %f, eTime: %f\u0026rdquo;, COMMAND_TIMESPAN, eTime);\n如果打印出来是0，说明配置文件中没有正确设置，或者节点名称不对。 另外，注意在Tracker类中，这两个变量是double类型，而在调用draw_simulated_bullets时，我们将其转换为long long类型（毫秒）。 如果这两个double类型的值本身是0，那么转换后就是0。 所以，问题很可能出现在配置文件的读取上。 请检查配置文件，确保有以下内容： Tracker: COMMAND_TIMESPAN: 0.11 eTime: 0.002\n并且注意缩进，YAML文件对缩进敏感。 如果以上都正确，那么可能是配置文件路径不对，导致没有读取到，而是使用了默认值0（注意，我们在as中给出的默认值是0.11和0.002，但如果读取失败，可能会返回0？实际上，如果键不存在，as会返回我们指定的默认值，所以应该是0.11和0.002）。 因此，如果打印出来是0，那说明读取的键存在，但值为0。可能是配置文件中写成了0。 请仔细检查配置文件。 另外，注意在Tracker类中，这两个变量是double类型，我们将其转换为毫秒时，乘以1000，然后转换为long long。 如果eTime=0.002，乘以1000等于2，转换为long long后是2，不为0。 所以，如果打印出0，说明eTime和COMMAND_TIMESPAN本身就是0。 因此，问题在于配置文件读取。 我们可以在Tracker的setParam函数中，打印出整个track节点，看看是否还有其他键值对。 例如： YAML::Node track = config[\u0026ldquo;Tracker\u0026rdquo;]; for(YAML::const_iterator it=track.begin();it!=track.end();++it) { std::cout \u0026laquo; it-\u0026gt;first.asstd::string() \u0026laquo; \u0026quot; = \u0026quot; \u0026laquo; it-\u0026gt;second.as() \u0026laquo; std::endl; }\n这样可以看到Tracker节点下所有的键值对。 如果发现没有COMMAND_TIMESPAN和eTime，那么说明配置文件写错了。 如果发现有，但是值是0，那么配置文件中的值就是0。 根据上述分析，请检查配置文件。 如果配置文件正确，那么可能是YAML文件的格式问题，比如使用了Tab缩进，应该使用空格。 建议使用空格缩进，并且确保缩进正确。 由于时间关系，我们可以先临时修改Tracker的代码，将这两个参数设置为非零值，测试子弹是否能正常发射。 例如，在Tracker的构造函数中，直接赋值： eTime = 0.002; COMMAND_TIMESPAN = 0.11;\n然后运行，看看是否还有问题。 如果这样修改后，子弹仍然不发射，那么问题可能出在其他地方。 如果修改后子弹发射了，那么问题就是配置文件读取的问题。 请按照上述步骤排查。 很好！现在问题明确了：eTime_ms 和 command_timespan_ms 都是0。这意味着传入的 eTime 和 COMMAND_TIMESPAN 参数都是0。 根本原因\neTime 和 COMMAND_TIMESPAN 在Tracker中都是0，所以计算出的毫秒值也是0。 检查Tracker中的参数 1. 检查Tracker构造函数中的参数初始化 cpp // 在Tracker的构造函数中 cout \u0026laquo; \u0026ldquo;Tracker构造函数 - eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;Tracker构造函数 - COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; endl;\n2. 检查setParam函数是否正确读取 在你的Tracker的setParam函数中，应该有这样的代码： cpp void Tracker::setParam(const std::string \u0026amp;file_path) {// \u0026hellip;if (config[\u0026ldquo;Tracker\u0026rdquo;]){ YAML::Node track = config[\u0026ldquo;Tracker\u0026rdquo;]; eTime = track[\u0026ldquo;eTime\u0026rdquo;].as(0.002); // 默认值0.002 COMMAND_TIMESPAN = track[\u0026ldquo;COMMAND_TIMESPAN\u0026rdquo;].as(0.11); // 默认值0.11 cout \u0026laquo; \u0026ldquo;读取参数: eTime=\u0026rdquo; \u0026laquo; eTime \u0026laquo; \u0026ldquo;, COMMAND_TIMESPAN=\u0026rdquo; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; endl;}// \u0026hellip;}\n3. 检查配置文件内容 你的YAML配置文件应该是这样的： yaml\nTracker:eTime: 0.002 # 曝光时间，单位秒COMMAND_TIMESPAN: 0.11 # 电控延迟，单位秒# 其他参数\u0026hellip; 4. 检查参数传递路径 在Tracker中，调用draw_simulated_bullets时： cpp // 在reconstruction_choose_compensation函数中 cout \u0026laquo; \u0026ldquo;调用前 - eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; \u0026ldquo;, COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;调用前 - eTime1000: \u0026quot; \u0026laquo; eTime * 1000 \u0026laquo; \u0026ldquo;, COMMAND_TIMESPAN1000: \u0026quot; \u0026laquo; COMMAND_TIMESPAN * 1000 \u0026laquo; endl; tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, eTime, // 这里应该是0.002 COMMAND_TIMESPAN); // 这里应该是0.11\n可能的解决方案 方案1：修复参数读取 如果配置文件没有问题，可能是读取代码有问题： cpp void Tracker::setParam(const std::string \u0026amp;file_path) {try { YAML::Node config = YAML::LoadFile(file_path);if (config[\u0026ldquo;Tracker\u0026rdquo;]) { YAML::Node track = config[\u0026ldquo;Tracker\u0026rdquo;];// 检查键是否存在if (track[\u0026ldquo;eTime\u0026rdquo;]) { eTime = track[\u0026ldquo;eTime\u0026rdquo;].as(); cout \u0026laquo; \u0026ldquo;成功读取 eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; endl;} else { eTime = 0.002; // 默认值 cout \u0026laquo; \u0026ldquo;使用默认 eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; endl;}if (track[\u0026ldquo;COMMAND_TIMESPAN\u0026rdquo;]) { COMMAND_TIMESPAN = track[\u0026ldquo;COMMAND_TIMESPAN\u0026rdquo;].as(); cout \u0026laquo; \u0026ldquo;成功读取 COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; endl;} else { COMMAND_TIMESPAN = 0.11; // 默认值 cout \u0026laquo; \u0026ldquo;使用默认 COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; endl;}}} catch (\u0026hellip;) {// 错误处理}}\n方案2：使用硬编码值测试 暂时绕过配置文件问题： cpp // 在Tracker构造函数中Tracker::Tracker(ros::NodeHandle\u0026amp; nh,const std::string\u0026amp; config_path) : config_path_(config_path), tfListener(tfBuffer_) {this-\u0026gt;nh = nh;// 硬编码测试值 eTime = 0.002; // 2毫秒 COMMAND_TIMESPAN = 0.11; // 110毫秒 cout \u0026laquo; \u0026ldquo;硬编码参数: eTime=\u0026rdquo; \u0026laquo; eTime \u0026laquo; \u0026ldquo;, COMMAND_TIMESPAN=\u0026rdquo; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; endl;// \u0026hellip; 其余初始化代码}\n方案3：修改draw_simulated_bullets调用 如果参数确实是0，可以修改调用方式： cpp // 在Tracker的reconstruction_choose_compensation函数中// 使用固定值而不是成员变量double test_eTime = 0.002;double test_COMMAND_TIMESPAN = 0.11; cout \u0026laquo; \u0026ldquo;使用测试参数: eTime=\u0026rdquo; \u0026laquo; test_eTime \u0026laquo; \u0026ldquo;, COMMAND_TIMESPAN=\u0026rdquo; \u0026laquo; test_COMMAND_TIMESPAN \u0026laquo; endl; tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, test_eTime, test_COMMAND_TIMESPAN);\n方案4：修改update_bullet函数使用固定延迟 最简单直接的解决方案： cpp auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void {const long long fire_interval = 50; // 发射间隔：50毫秒// 忽略传入的参数，使用固定值const long long fixed_eTime_ms = 2; // 2毫秒const long long fixed_command_timespan_ms = 110; // 110毫秒const long long additional_delay = 25; // 25毫秒long long total_delay = fixed_eTime_ms + additional_delay + fixed_command_timespan_ms; cout \u0026laquo; \u0026ldquo;使用固定延迟: \u0026quot; \u0026laquo; total_delay \u0026laquo; \u0026ldquo;ms (2+25+110)\u0026rdquo; \u0026laquo; endl;if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) {if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) { bullets.push_back(IdProj { next_id++,ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + total_delay)});this-\u0026gt;last_fire_time = current_time;}}}\n快速诊断脚本 在Tracker中添加这个函数来诊断问题： cpp void Tracker::debugParams() { cout \u0026laquo; \u0026ldquo;=== 参数诊断 ===\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;配置文件路径: \u0026quot; \u0026laquo; config_path_ \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;eTime: \u0026quot; \u0026laquo; eTime \u0026laquo; \u0026quot; (秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;eTime * 1000: \u0026quot; \u0026laquo; eTime * 1000 \u0026laquo; \u0026quot; (毫秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;COMMAND_TIMESPAN: \u0026quot; \u0026laquo; COMMAND_TIMESPAN \u0026laquo; \u0026quot; (秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;COMMAND_TIMESPAN * 1000: \u0026quot; \u0026laquo; COMMAND_TIMESPAN * 1000 \u0026laquo; \u0026quot; (毫秒)\u0026rdquo; \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;static_cast(eTime * 1000): \u0026quot; \u0026laquo; static_cast(eTime * 1000) \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;static_cast(COMMAND_TIMESPAN * 1000): \u0026quot; \u0026laquo; static_cast(COMMAND_TIMESPAN * 1000) \u0026laquo; endl; cout \u0026laquo; \u0026ldquo;=== 诊断结束 ===\u0026rdquo; \u0026laquo; endl;}// 在Track()函数开始处调用void Track() {debugParams(); // 添加这行// \u0026hellip; 其余代码}\n最可能的根本原因 配置文件路径错误：config_path_ 可能指向了错误的文件 配置文件格式错误：YAML文件格式不正确 参数名不匹配：配置文件中使用了不同的键名 构造函数调用顺序：在读取配置文件之前使用了参数\n推荐立即采用的解决方案： 使用方案4（固定延迟），这样无论Tracker的参数是什么，子弹都会有合理的延迟（2+25+110=137毫秒）。这样你就能立即看到子弹效果，然后再去解决Tracker的参数读取问题。 // 这里写的很简略,只能看静止弹道对不对 // 每隔一段时间就放一颗弹丸,假想一个发弹时间固定的模拟器 const std::size_t AIM_CORRECTOR_BULLETS_MAX_SZ = 200u; auto AimCorrector::update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void { const long long fire_interval = 50; // 发射间隔：50毫秒 if (this-\u0026gt;last_fire_time == 0 || (current_time - this-\u0026gt;last_fire_time) \u0026gt;= fire_interval) { if (bullets.size() + 1u \u0026lt;= AIM_CORRECTOR_BULLETS_MAX_SZ) { bullets.push_back(IdProj { next_id++, ProjectileSimulator(coorConverter, this-\u0026gt;shoot_param, current_time + eTime + 25 + COMMAND_TIMESPAN) }); this-\u0026gt;last_fire_time = current_time; } } } 我发现这里面始终只有一颗子弹也就是说这个bullets变量没有维护,程序每跑到这里就创建一个全新的\n我们看到问题：每次调用draw_simulated_bullets时，都会创建一个新的AimCorrector对象，然后调用update_bullet。由于AimCorrector对象是局部变量，所以每次都会重新开始，子弹列表bullets也是空的，然后创建一颗子弹，但是这颗子弹的发射时间设置在未来（当前时间+延迟），所以在同一帧的get_circles调用中，子弹还未发射，因此不会被绘制。然后这个AimCorrector对象就被销毁了。下一帧又是同样的过程。 换句话说，子弹列表没有被持久化，每一帧都是一个新的AimCorrector，所以每一帧都只有一颗新创建的子弹，而且这颗子弹还未发射。 为了解决这个问题，我们需要将AimCorrector对象作为持久化的对象，而不是每次调用draw_simulated_bullets都新建一个。 建议修改：将AimCorrector作为Tracker类的成员变量，这样它就可以持续维护子弹列表。 具体步骤： 在Tracker类中添加一个AimCorrector成员变量，例如aim_corrector。 在Tracker的构造函数中初始化aim_corrector，但注意需要传入coorConverter和shoot_param，而shoot_param是每次计算都会变化的，所以不能只初始化一次。 或者，我们可以在每次调用draw_simulated_bullets时，使用同一个AimCorrector对象，但更新其内部的shoot_param。\n但是，注意AimCorrector的构造函数需要coorConverter和shoot_param，而shoot_param是随着目标变化而变化的。所以我们可以这样做： 修改AimCorrector类，使其可以更新shoot_param，并且update_bullet函数不再创建新的子弹模拟器，而是根据当前的shoot_param和当前时间创建子弹。 但是，我们也可以不修改AimCorrector，而是在Tracker中维护一个AimCorrector对象，并在每次调用draw_simulated_bullets时，传入当前的shoot_param，并让AimCorrector更新这个参数。 由于AimCorrector现在需要更新子弹，而且子弹的创建依赖于shoot_param，我们可以这样做： 修改AimCorrector，添加一个成员函数来更新shoot_param，然后在update_bullet中使用最新的shoot_param。 但是，考虑到我们每次调用draw_simulated_bullets时，shoot_param可能不同，所以每次都应该用最新的shoot_param来创建子弹。 然而，子弹的创建是时间触发的（每50ms），所以我们需要在AimCorrector内部维护一个shoot_param，并允许外部更新它。 修改AimCorrector类： 添加一个成员函数update_shoot_param，用于更新shoot_param。 在update_bullet中，使用当前存储的shoot_param来创建子弹。\n同时，我们需要在Tracker中创建一个持久的AimCorrector对象，并在每次调用draw_simulated_bullets时，先更新aim_corrector的shoot_param，然后调用update_bullet和get_circles。 但是，注意AimCorrector的构造函数需要coorConverter，这个在Tracker中已经存在，可以传入。 具体代码修改： 在Tracker类中添加成员变量： cpp\nstd::unique_ptrtools::AimCorrector aim_corrector_; 在Tracker的构造函数中初始化： cpp\naim_corrector_ = std::make_uniquetools::AimCorrector(coorConverter, tools::ShootParam()); 在reconstruction_choose_compensation函数中，设置shoot_param并更新子弹： cpp tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_target_armor.position);// 更新aim_corrector_的shoot_param aim_corrector_-\u0026gt;update_shoot_param(shoot_param);// 更新子弹 aim_corrector_-\u0026gt;update_bullet(draw_visual_now_time, eTime, COMMAND_TIMESPAN);// 绘制子弹 tools::draw_simulated_bullets_with_corrector(aim_corrector_.get(), frame_, draw_visual_now_time);\n注意，我们需要修改draw_simulated_bullets函数，改为接受一个AimCorrector指针，而不是每次都新建一个。或者，我们直接使用aim_corrector_来获取子弹并绘制，而不通过draw_simulated_bullets。 为了最小改动，我们可以修改draw_simulated_bullets，使其接受一个AimCorrector对象，并在内部调用它的get_circles。 但是，我们也可以不修改draw_simulated_bullets，而是让Tracker直接调用aim_corrector_-\u0026gt;get_circles，然后绘制。 考虑到代码结构，我们可能希望将绘制函数与AimCorrector分离，所以我们可以保留draw_simulated_bullets，但修改其参数，接受一个AimCorrector对象。 修改draw_simulated_bullets函数： cpp\nvoid draw_simulated_bullets(AimCorrector* aim_corrector, cv::Mat\u0026amp; img, long long now_time); 这样，在Tracker中就可以这样调用： cpp\ntools::draw_simulated_bullets(aim_corrector_.get(), frame_, draw_visual_now_time); 但是，原来的draw_simulated_bullets函数中还有创建AimCorrector和更新子弹的逻辑，现在这些逻辑应该移到外面。 因此，我们修改draw_simulated_bullets函数，只负责绘制子弹，而子弹的更新由外部完成。 总结修改步骤： 修改AimCorrector类，添加update_shoot_param函数，并修改update_bullet函数，使用成员变量shoot_param。 在Tracker中添加AimCorrector成员变量，并在构造函数中初始化。 在Tracker的reconstruction_choose_compensation函数中，更新aim_corrector_的shoot_param，并调用update_bullet。 修改draw_simulated_bullets函数，接受AimCorrector指针和当前时间，从中获取子弹并绘制。\n由于改动较大，我们一步步来。 首先，修改AimCorrector类（头文件）： cpp class AimCorrector {public:AimCorrector(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param);// 更新射击参数void update_shoot_param(const ShootParam\u0026amp; shoot_param);// 获取所有已经发射但尚未\u0026quot;击中\u0026quot;的子弹在当前时刻的图像投影圆auto get_circles(long long now_time) -\u0026gt; std::vector;auto update_bullet(long long current_time, long long eTime, long long COMMAND_TIMESPAN) -\u0026gt; void;private: std::list bullets; // 活跃子弹容器模拟器 CoordinateTransformer* coorConverter; // 坐标变换器 ShootParam shoot_param; // 当前射击参数long long next_id = 0; long long last_fire_time = 0;};\n实现update_shoot_param： cpp\nvoid AimCorrector::update_shoot_param(const ShootParam\u0026amp; new_shoot_param) {this-\u0026gt;shoot_param = new_shoot_param;} 然后，修改draw_simulated_bullets函数，使其接受一个AimCorrector指针： cpp void draw_simulated_bullets(AimCorrector* aim_corrector, cv::Mat\u0026amp; img, long long now_time);\n实现： cpp void draw_simulated_bullets(AimCorrector* aim_corrector, cv::Mat\u0026amp; img, long long now_time) { FlaskStream flask_aim; flask_aim.clear();// 从aim_corrector获取子弹 std::vector bullets = aim_corrector-\u0026gt;get_circles(now_time);for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img;}\n在Tracker中，添加成员变量并初始化： cpp class Tracker {private: std::unique_ptrtools::AimCorrector aim_corrector_;// \u0026hellip;};Tracker::Tracker(ros::NodeHandle\u0026amp; nh,const std::string\u0026amp; config_path) : config_path_(config_path), tfListener(tfBuffer_) {// \u0026hellip; 其他初始化// 初始化aim_corrector_，初始shoot_param可以随意，因为后面会更新 aim_corrector_ = std::make_uniquetools::AimCorrector(coorConverter, tools::ShootParam());}\n在reconstruction_choose_compensation函数中，更新aim_corrector_并绘制： cpp // 计算shoot_param tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_target_armor.position);// 更新aim_corrector_的射击参数 aim_corrector_-\u0026gt;update_shoot_param(shoot_param);// 更新子弹（注意：这里传入的eTime和COMMAND_TIMESPAN需要是毫秒，但你的函数现在接受的是long long，且已经乘以1000，所以这里直接传入毫秒值） aim_corrector_-\u0026gt;update_bullet(draw_visual_now_time, static_cast(eTime * 1000), static_cast(COMMAND_TIMESPAN * 1000));// 绘制子弹 tools::draw_simulated_bullets(aim_corrector_.get(), frame_, draw_visual_now_time);\n这样，子弹列表就会在aim_corrector_中持续维护，而不是每次调用都新建。 注意：原来的draw_simulated_bullets函数有多个参数，现在改为接受AimCorrector指针，所以需要修改调用方式。 另外，由于AimCorrector的update_bullet函数中，子弹的创建依赖于传入的延迟参数（eTime, COMMAND_TIMESPAN）和当前的shoot_param，所以每次更新射击参数后，新创建的子弹都会使用最新的参数。 这样修改后，子弹就会在多次调用中持续存在，并且会按照设定的间隔（50ms）创建新的子弹，直到达到最大数量。 注意：原来的draw_simulated_bullets函数中还有eTime和COMMAND_TIMESPAN参数，现在这些参数在Tracker中调用update_bullet时传入，所以不再需要。 由于改动较大，请确保在修改后编译通过，并测试功能。 如果不想大幅改动，也可以将AimCorrector对象作为静态局部变量放在draw_simulated_bullets函数内部，这样也可以保持子弹列表的持久化，但这样不利于多目标等情况。考虑到自瞄系统通常只有一个目标，所以也可以使用静态局部变量。 但是，使用静态局部变量会使得函数不可重入，并且如果同时有多个目标需要绘制弹道，就会有问题。因此，建议使用成员变量的方式。 根据你的实际情况选择。如果你希望简单修改，可以使用静态局部变量，但这样可能会带来其他问题。 这里给出使用静态局部变量的修改方法（不推荐，但快速）： 修改draw_simulated_bullets函数： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter, const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img, long long now_time, long long eTime, long long COMMAND_TIMESPAN) { FlaskStream flask_aim; flask_aim.clear();static AimCorrector aim_corrector(coorConverter, shoot_param); // 静态局部变量// 更新射击参数 aim_corrector.update_shoot_param(shoot_param);// 更新子弹序列 aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img;}\n这样，aim_corrector在第一次调用时被初始化，之后每次调用都使用同一个对象，从而维护了子弹列表。 注意：静态局部变量只初始化一次，所以第一次调用时传入的coorConverter和shoot_param被用于初始化，后续调用update_shoot_param来更新射击参数。 但是，这种方法有一个问题：如果coorConverter指针发生变化（比如指向了另一个对象），那么后续的调用就会使用错误的coorConverter。所以，请确保coorConverter在程序运行期间是有效的。 根据你的代码，coorConverter在Tracker中是一个成员变量，并且在Tracker的生命周期内是有效的，所以可以使用。 两种方法都可以，但成员变量方法更面向对象，更安全。静态局部变量方法简单，但需要注意上述问题。 请根据你的情况选择修改方式。 你说得非常对！这就是核心问题所在。 问题分析\n每次调用 draw_simulated_bullets 时，都会创建一个全新的 AimCorrector 对象： cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) { AimCorrector aim_corrector = AimCorrector(coorConverter, shoot_param); // 每次都新建！ aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);// \u0026hellip;} 这意味着： 第1帧：创建 AimCorrector 对象 A，添加子弹1，检查子弹（未发射），子弹列表 = [子弹1] 第2帧：创建 新的 AimCorrector 对象 B，添加子弹2，检查子弹（未发射），子弹列表 = [子弹2] 第3帧：创建 新的 AimCorrector 对象 C，添加子弹3，检查子弹（未发射），子弹列表 = [子弹3]\n每个 AimCorrector 对象都是独立的，它们之间不共享子弹列表！ 解决方案\n方案1：将 AimCorrector 作为静态变量（最简单） cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) {static AimCorrector aim_corrector(coorConverter, shoot_param); // 静态变量，只初始化一次！ aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);// \u0026hellip;} 优点：最简单，只需要改一个关键字 static 缺点：只有一个全局的子弹列表，如果多次调用可能会有问题\n方案2：将 AimCorrector 作为 Tracker 的成员变量（推荐） cpp // 在Tracker.h中class Tracker {private:// 添加成员变量 tools::AimCorrector* aim_corrector_;// \u0026hellip;}; cpp // 在Tracker.cpp的构造函数中Tracker::Tracker(ros::NodeHandle\u0026amp; nh,const std::string\u0026amp; config_path) : config_path_(config_path), tfListener(tfBuffer_) {this-\u0026gt;nh = nh;// 初始化aim_corrector aim_corrector_ = new tools::AimCorrector(coorConverter, tools::ShootParam());// \u0026hellip;} cpp\n// 在Tracker的析构函数中Tracker::~Tracker() {delete cal;delete coorConverter;delete aim_corrector_; // 释放内存} cpp // 在reconstruction_choose_compensation函数中 tools::ShootParam shoot_param; shoot_param.v0 = BulletVector; shoot_param.aim_angle = m_target_pose.pitch + m_pitchOffset; shoot_param.target_xyz_i_camera = coorConverter-\u0026gt;map2Cam(abs_target_armor.position);// 更新aim_corrector的射击参数 aim_corrector_-\u0026gt;update_shoot_param(shoot_param); // 需要添加这个方法// 更新子弹 aim_corrector_-\u0026gt;update_bullet(draw_visual_now_time, static_cast(eTime * 1000),static_cast(COMMAND_TIMESPAN * 1000));// 获取并绘制子弹 std::vectortools::IdCircle bullets = aim_corrector_-\u0026gt;get_circles(draw_visual_now_time);// 绘制代码\u0026hellip;\n方案3：修改 AimCorrector 使其可更新参数 首先在 AimCorrector 类中添加更新参数的方法： cpp // 在trajectory_visualizer.hpp中class AimCorrector {public:// \u0026hellip; 其他方法 \u0026hellip;// 添加这个方法void update_shoot_param(const ShootParam\u0026amp; new_param) { shoot_param = new_param;}// \u0026hellip;};\n方案4：修改 draw_simulated_bullets 接收 AimCorrector 引用 cpp // 修改函数签名void draw_simulated_bullets(AimCorrector\u0026amp; aim_corrector, // 改为引用const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) {// 更新射击参数 aim_corrector.update_shoot_param(shoot_param); aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);// \u0026hellip;}\n完整推荐方案（方案1 + 方案3） 这是最简单有效的解决方案： 步骤1：在 AimCorrector 中添加参数更新方法 cpp // trajectory_visualizer.hppclass AimCorrector {public:AimCorrector(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param);// 更新射击参数void update_shoot_param(const ShootParam\u0026amp; new_param) { shoot_param = new_param;}// \u0026hellip; 其他方法 \u0026hellip;};\n步骤2：修改 draw_simulated_bullets 使用静态变量 cpp void draw_simulated_bullets(CoordinateTransformer* const coorConverter,const ShootParam\u0026amp; shoot_param, cv::Mat\u0026amp; img,long long now_time, long long eTime, long long COMMAND_TIMESPAN) { FlaskStream flask_aim; flask_aim.clear();// 使用静态变量，只初始化一次static AimCorrector aim_corrector(coorConverter, shoot_param);// 更新射击参数（因为shoot_param可能每帧都变化） aim_corrector.update_shoot_param(shoot_param);// 更新子弹序列 aim_corrector.update_bullet(now_time, eTime, COMMAND_TIMESPAN); std::vector bullets = aim_corrector.get_circles(now_time);// 绘制子弹for (auto\u0026amp; bullet: bullets) { flask_aim \u0026laquo; FlaskPoint( bullet.circle.center,{ 0, 0, 255 }, bullet.circle.r,2); flask_aim \u0026laquo; FlaskText( std::to_string(bullet.id),{ bullet.circle.center.x + 20.f, bullet.circle.center.y },{ 0, 0, 255 },0.8);} flask_aim \u0026raquo; img;}\n步骤3：在Tracker中修复参数传递问题 cpp // 在Tracker的reconstruction_choose_compensation函数中// 确保传入正确的参数（毫秒）long long eTime_ms = static_cast(eTime * 1000); // 应该是2long long command_timespan_ms = static_cast(COMMAND_TIMESPAN * 1000); // 应该是110// 如果参数为0，使用固定值if (eTime_ms == 0) eTime_ms = 2;if (command_timespan_ms == 0) command_timespan_ms = 110; tools::draw_simulated_bullets(this-\u0026gt;coorConverter, shoot_param, frame_, draw_visual_now_time, eTime_ms, // 传入毫秒 command_timespan_ms); // 传入毫秒\n关键修复点 使用 static 关键字：确保 AimCorrector 对象只创建一次 添加参数更新方法：因为每帧的射击参数可能不同 修复参数传递：确保传入正确的毫秒值\n这样修改后，子弹列表会被持续维护，子弹会按照50ms的间隔持续添加到列表中，并且会随着时间推移逐渐发射。 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/ai%E8%BE%85%E5%8A%A9%E8%A7%A3%E5%86%B3%E5%BC%B9%E9%81%93%E5%A4%8D%E7%8E%B0bug/","summary":"\u003ch1 id=\"ai辅助解决弹道复现bug\"\u003eAI辅助解决弹道复现BUG\u003c/h1\u003e\n\u003cp\u003e#pragma once\n#include \u003cvector\u003e\n// ROS相关头文件\n#include \u0026ldquo;ros/ros.h\u0026rdquo;\n#include \u0026ldquo;rm_msgs/Armor.h\u0026rdquo;\n#include \u0026ldquo;rm_msgs/ArmorArray.h\u0026rdquo;\n#include \u0026ldquo;rm_msgs/RmSerial.h\u0026rdquo;\n#include \u0026lt;std_msgs/Float64.h\u0026gt;\n#include \u0026lt;angles/angles.h\u0026gt;\n#include \u0026lt;tf2/LinearMath/Transform.h\u0026gt;\n#include \u0026lt;tf2/LinearMath/Vector3.h\u0026gt;\n#include \u0026lt;tf2/LinearMath/Quaternion.h\u0026gt;\n// OpenCV相关头文件\n#include \u0026lt;opencv2/opencv.hpp\u0026gt;\n#include \u0026lt;cv_bridge/cv_bridge.h\u0026gt;\n// 标准库头文件\n#include \u003ciostream\u003e\n#include \u003ccmath\u003e\n// 时间相关头文件\n#include \u003cchrono\u003e\n#include \u003cthread\u003e\n#include \u003cfstream\u003e\n#include \u0026lt;yaml-cpp/yaml.h\u0026gt;\n// 自定义头文件\n#include \u0026ldquo;Calculater.hpp\u0026rdquo;\n#include \u0026ldquo;GimbalPos.hpp\u0026rdquo;\n#include \u0026ldquo;TargetModel.hpp\u0026rdquo;\n#include \u0026ldquo;visual.hpp\u0026rdquo;\n#include \u0026ldquo;CoorConverter.hpp\u0026rdquo;\n#include \u0026ldquo;math.hpp\u0026rdquo;\n#include \u0026ldquo;MPC.hpp\u0026rdquo;\n#include \u0026ldquo;trajectory_visualizer.hpp\u0026rdquo;\nusing namespace std;\nusing namespace cv;\n/*\n自瞄文档\n标准模型状态向量\nX(0) -\u0026gt; 机器人中心的x坐标\nX(1) -\u0026gt; 机器人中心x方向速度\nX(2) -\u0026gt; 机器人中心的y坐标\nX(3) -\u0026gt; 机器人中心y方向速度\nX(4) -\u0026gt; 左侧装甲板的固定高度\nX(5) -\u0026gt; 右侧装甲板的固定高度\nX(6) -\u0026gt; 小装甲板旋转半径（左侧）\nX(7) -\u0026gt; 大装甲板旋转半径（右侧）\nX(8) -\u0026gt; 机器人整体偏航角yaw\nX(9) -\u0026gt; 偏航角速度palstance\n前哨站状态向量\nX(0): 机器人中心的x坐标\nX(1): 机器人中心的y坐标\nX(2): 第一块装甲板高度h1\nX(3): 第二块装甲板高度h2\nX(4): 第三块装甲板高度h3\nX(5): 偏航角yaw\nX(6): 偏航角速度palstance\n\u003cem\u003e/\n/\u003c/em\u003e*\n@brief 将弧度约束在[-pi, pi]范围内，大于n，减去2n；小于-n，加上2n\n@param angle 角度\n\u003cem\u003e/\n#ifndef _std_radian\n#define \u003cem\u003estd_radian(angle) ((angle) + round((0 - (angle)) / (2 * PI)) * (2 * PI))\n#endif\n//追踪类\ntemplate\u003cTimeSpan _trackTime\u003e\nclass Tracker{\nprivate:\ndouble COMMAND_TIMESPAN;            //电控延迟\ndouble local_gravity\u003c/em\u003e;              //重力加速度\ndouble eTime;                       //曝光时间\n/\u003c/em\u003e ======================== 系统参数 ======================== \u003cem\u003e/\n//ROS相关\nros::NodeHandle nh;                 //ROS节点句柄\nros::Publisher debugpub;            //debug发布者\nros::Publisher debugpub1;           //debug1发布者\nstd_msgs::Float64 debugdate;        //debug数据\nstd_msgs::Float64 debugdate1;       //debug1数据\nrm_msgs::RmSerial RmSerialData;     //接收串口数据\ntf2_ros::Buffer tfBuffer_;            // TF 缓冲区\ntf2_ros::TransformListener tfListener; // 声明一个tf2_ros::TransformListener对象，并传入tfBuffer\nrm_msgs::ArmorArrayConstPtr m_armors;  // 接收装甲板数据\n//图像处理相关\ncv::Mat frame;          //接收的原始图像\ncv::Mat frame_;         //被处理的图像副本(保护原图像)\ncv::Mat camera_matrix_; //相机内参(构造函数中)\ncv::Mat dist_coeffs_;   //畸变系数(构造函数中)\nImage img;              //图像工具\n//坐标变换工具\nCAL::Calculater\u003c/em\u003e cal;   //计算工具\nCoordinateTransformer* coorConverter;\n//装甲板评分参数\nstd::vector\u003cint\u003e col;                       // 数列中的每个数代表矩阵的每一列\nstd::vector\u003cint\u003e row;                       // 数列中的每个数代表矩阵的每一行\nstd::vector\u003cint\u003e tmp_v;                     // 存储C(n,k)的中间结果\nstd::vector\u0026lt;std::vector\u003cint\u003e\u0026gt; result;       // 存储C(n,k)的结果\nstd::vector\u0026lt;std::vector\u003cint\u003e\u0026gt; nAfour;       // 存储A(n,4)的结果\nstd::vector\u0026lt;std::vector\u003cint\u003e\u0026gt; fourAfour;    // 存储A(4,4)的结果\nstd::map\u0026lt;int, int\u0026gt; row_col;                 // 存储最终结果，row_col[i]=j表示矩阵的第i行第j列是要选取的数\n//数据关联参数\ndouble min = 0, tmp = 0;\n/* ======================== 跟踪控制参数 ======================== \u003cem\u003e/\n//基本跟踪参数\nchar TrackingID;            //跟踪中的装甲板ID\nbool Switch_Armor;          //装甲板切换标识符\ndouble trackTime;           //目标丢失判定时间(秒)\n//角度补偿参数\ndouble pitch_compensation;  //pitch补偿\ndouble yaw_compensation;    //yaw补偿\n//火力控制参数\nbool all_fire = false;      //完全火力模式(不停止射击)\n/\u003c/em\u003e ======================== 装甲板预测参数 ======================== \u003cem\u003e/\n//空间参考点参数\nEigen::Vector3d pegPos;         //空间标准点(用于装甲板跳变判断)\ndouble peg_point_pixel_now;     //当前装甲板在像素坐标系的位置\ndouble peg_point_pixel_last;    //位于像素标准坐标系的上一个装甲板位置\n//子弹参数\ndouble BulletVector = 22;           //子弹速度(初值给24m/s)\n/\u003c/em\u003e ======================== 时间戳管理 ======================== \u003cem\u003e/\n//装甲板跟踪时间\nlong long Now_Time_armor = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();//当前时间戳(用于计算丢失时间)\nlong long Track_Time_armor = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();//丢失时间戳(用于计算丢失时间)\n//帧率计算\nlong long begin_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();//首时间\nlong long end_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();//尾时间\n// C++工具时间戳\nlong long tool_begin_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();//首时间\nlong long tool_end_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();//尾时间\n// ros工具时间戳\nros::Time tool_begin_Time_ros = ros::Time::now();\nros::Time tool_end_Time_ros = ros::Time::now();\n// 获取装甲板tf时间(判断是否是相同帧)\nros::Time last_tf_time = ros::Time::now();\n/\u003c/em\u003e ======================== 标志位 ======================== \u003cem\u003e/\nbool functional = true;                 // 射击模式\nint m_center_tracked;                   // 锁中心状态标志位\nbool m_track_center;                    // 是否跟随中心\nbool m_fix_on;                          // 重力补偿开关\nstd::string config_path_;               // 存储配置路径\nunique_ptr\u003cTargetModel\u003e targetModel;    // 目标模型\nros::Publisher AngPub;                  // 角度话题发布者\nstd::shared_ptr\u003cMPC\u003e m_MPC;             // 模型预测控制器\n// =========================== 阈值 ====================================\ndouble m_score_tolerance;               //装甲板匹配得分最大值\ndouble m_switch_threshold;              // 更新装甲板切换的角度阈值，角度制\ndouble m_force_aim_palstance_threshold; // 强制允许发射的目标旋转速度最大值，弧度制\ndouble m_aim_angle_tolerance;           // 自动击发时目标装甲板相对偏角最大值，角度制\ndouble m_aim_pose_tolerance;            // 自动击发位姿偏差最大值，弧度制\ndouble m_aim_center_angle_tolerance;    // 跟随圆心自动击发目标偏角判断，角度制\ndouble m_switch_trackmode_threshold;    // 更换锁中心模式角速度阈值，弧度制\ndouble m_aim_center_palstance_threshold;// 跟随圆心转跟随装甲板的目标旋转速度最大值，弧度制\n// =========================== 可视化 ===================================\nstd::vector\u0026lt;std::vector\u003ca href=\"Eigen::Vector3d\"\u003eEigen::Vector3d\u003c/a\u003e\u0026gt; visual_armor_position_pose_temp;  // 用于可视化存储观测装甲板的全局变量\nGimbalPose m_cur_pose;                  // 当前位姿\nGimbalPose m_target_pose;               // 目标位姿\nGimbalPose m_target_pose_debug;         // 用于debug,比较mpc和传统模式\n// =========================== 串口补偿 =======================================\ndouble m_rollOffset = 0;\ndouble m_pitchOffset = 0;\ndouble m_yawOffset = 0;\npublic:\nTracker(ros::NodeHandle\u0026amp; nh,const std::string\u0026amp; config_path) : config_path_(config_path), tfListener(tfBuffer_)\n{\nthis-\u0026gt;nh = nh;\nAngPub = nh.advertise\u0026lt;geometry_msgs::Vector3\u0026gt;(\u0026quot;/auto_angle\u0026quot;, 1000);\ndebugpub = nh.advertise\u0026lt;std_msgs::Float64\u0026gt;(\u0026quot;/debugpub\u0026quot;, 1000);\ndebugpub1 = nh.advertise\u0026lt;std_msgs::Float64\u0026gt;(\u0026quot;/debugpub1\u0026quot;, 1000);\ncal = new CAL::Calculater(this-\u0026gt;nh,this-\u0026gt;img);\ntfBuffer_.setUsingDedicatedThread(true);\nm_armors.reset(); // 显式初始化为空\n// 初始化时创建 TargetModel\ntargetModel = std::make_unique\u003cTargetModel\u003e(config_path_);\ncoorConverter = new CoordinateTransformer(config_path_);\nm_MPC = std::make_shared\u003cMPC\u003e(config_path_); // 控制器初始化\n// 检查配置文件路径是否有效\nif (!config_path_.empty()) {\ntry {\nsetParam(config_path_);\nROS_INFO(\u0026ldquo;Successfully loaded parameters from: %s\u0026rdquo;, config_path_.c_str());\n} catch (const std::exception\u0026amp; e) {\nROS_ERROR(\u0026ldquo;Failed to load parameters: %s\u0026rdquo;, e.what());\n}\n} else {\nROS_WARN(\u0026ldquo;No configuration file path provided. Using default parameters.\u0026rdquo;);\n}\n}\n~Tracker() {\ndelete cal;\ndelete coorConverter;\n}\n/\u003c/em\u003e***************\n* @brief 回调函数: 接受串口信息，并更新内部变量、TF、坐标系\n* @param \u003cem\u003eserial ROS 消息的智能指针，包含子弹速度、射击标志、补偿角度等\n\u003cem\u003e/\nvoid SetSerial(const rm_msgs::RmSerialConstPtr _serial){\nif (_serial) {\nRmSerialData = \u003cem\u003e_serial;\n// 目前注释掉了(到时候测试一下)\n// if(_serial-\u0026gt;BulletVec \u0026gt; 10){\n//     BulletVector = _serial-\u0026gt;BulletVec;\n// }\nif(_serial-\u0026gt;ShootFlag == \u0026lsquo;f\u0026rsquo;){\nfunctional = true;\n}else if(_serial-\u0026gt;ShootFlag == \u0026lsquo;a\u0026rsquo;){\nfunctional = true;\n}else{\nfunctional = true;\n}\n// 从串口获取当前装甲板的姿态\nm_cur_pose.roll = RmSerialData.Roll;\nm_cur_pose.pitch = RmSerialData.Pitch;\nm_cur_pose.yaw = RmSerialData.Yaw; \u003cbr\u003e\n// debug\n// ROS_INFO(\u0026ldquo;cur: pitch:  %lf\u0026rdquo; , RmSerialData.Pitch);\n// ROS_INFO(\u0026ldquo;BulletVector:  %lf\u0026rdquo; , BulletVector);\n// std::vector\u003ca href=\"Eigen::Vector3d\"\u003eEigen::Vector3d\u003c/a\u003e imuabsPos = cal-\u0026gt;GetPos(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;imu\u0026rdquo;,1);\n// cal-\u0026gt;TFUpdata(\u0026ldquo;map\u0026rdquo;,\u0026ldquo;imuabs\u0026rdquo;,{0.0, 0.0, 0.0},{0.0, 0.0, imuabsPos[1].z()},0);\n// ROS_WARN(\u0026ldquo;have serial 3333333333333333333333333333\u0026rdquo;);\n}\n}\n/\u003c/em\u003e\u003c/em\u003e***************\n* @brief 回调函数: 接收相机节点发送的图像,用于debug\n* @param img\u003c/em\u003e 图像信息\n* @param frame  供算法线程直接使用的原始图\n* @param frame_ 额外再 clone 一份，用于可视化\n\u003cem\u003e/\nvoid doimage(const sensor_msgs::ImageConstPtr img_)\n{\ncv_bridge::CvImagePtr cv_ptr;\ntry\n{\ncv_ptr =  cv_bridge::toCvCopy(img_, sensor_msgs::image_encodings::BGR8);\n}\ncatch(cv_bridge::Exception\u0026amp; e)\n{\nROS_ERROR(\u0026ldquo;cv_bridge exception: %s\u0026rdquo;, e.what());\nreturn;\n}\nframe = cv_ptr-\u0026gt;image.clone();\nframe_ = frame.clone();\n// ROS_WARN(\u0026ldquo;have image 111111111111111111\u0026rdquo;);\n}\n// 回调函数: 接收识别发送的装甲板序列\nvoid doArmors(rm_msgs::ArmorArrayConstPtr armors)\n{\nm_armors = armors;\n// ROS_WARN(\u0026ldquo;have armor 222222222222222222\u0026rdquo;);\n}\n/\u003c/em\u003e*\n* @brief  主跟踪循环：每帧调用一次，完成“决策 → 预测 → 补偿 → 发布”全链路\n\u003cem\u003e/\nvoid Track()\n{\nif (!m_armors) {\ncout \u0026laquo; \u0026ldquo;消息队列为空\u0026rdquo; \u0026laquo; endl;\nreturn;\n}\ntry\n{\n//begin_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();\nif(!functional){\nreturn;\n}\nTargetModel\u003c/em\u003e targetModel_temp = nullptr;\ntargetModel_temp = armorUpdate(m_armors);\nif(targetModel_temp != nullptr){\ncv::Point2d finAngle = {0.0, 0.0};\nGimbalPose target_pose_temp = reconstruction_choose_compensation();\n// 调试输出(打印云台需要转动到的角度)\n// cout \u0026laquo; \u0026ldquo;pitch: \u0026quot; \u0026laquo; target_pose_temp.pitch \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;yaw: \u0026quot; \u0026laquo; target_pose_temp.yaw \u0026laquo; endl;\n// 打印需要移动的相对角\n// finAngle.x = target_pose_temp.pitch - m_cur_pose.pitch; // 计算需要移动的pitch角度\n// finAngle.y = target_pose_temp.yaw - m_cur_pose.yaw;     // 计算需要移动的yaw角度\n// // // 陀螺仪的绝对角\nfinAngle.x = target_pose_temp.pitch; // 计算需要移动到的pitch角度\nfinAngle.y = target_pose_temp.yaw;     // 计算需要移动到的yaw角度\n// debug: 云台跟随效果rqt_plot打印\n// yaw角\n// debugdate.data = m_cur_pose.yaw;            // 当前云台yaw角度\n// debugdate1.data = target_pose_temp.yaw;     // 计算出云台需要转动的yaw角度\n// // pitch角\n// debugdate.data = m_cur_pose.pitch;          // 当前云台pitch角度\n// debugdate1.data = target_pose_temp.pitch;   // 计算出云台需要转动的pitch角度\n// debug: 自动打弹打印\n// cout \u0026laquo; \u0026ldquo;finAngle.x: \u0026quot; \u0026laquo; finAngle.x \u0026laquo; \u0026quot; \u0026quot; \u0026laquo; \u0026ldquo;finAngle.y: \u0026quot; \u0026laquo; finAngle.y \u0026laquo; endl;\n// cout \u0026laquo; \u0026ldquo;是否允许打弹: \u0026quot; \u0026laquo; targetModel_temp-\u0026gt;auto_fire \u0026laquo; endl;\n// if (targetModel_temp-\u0026gt;auto_fire) {\n//     debugdate.data = 1;\n// }\n// else {\n//     debugdate.data = 0;\n// }\nPub_Aangle(true, targetModel_temp-\u0026gt;auto_fire, finAngle);\n}else{\nPub_Aangle(false);\n}\n// 帧率控制\n// double time_line = 30.0;\n// end_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();\n// double frame_time = (end_Time - begin_Time)/1000.0;\n// if ((end_Time - begin_Time) - time_line \u0026lt; 0.0)// 帧率控制\n// {\n//     int sleep_time_ms = static_cast\u003cint\u003e(time_line - (end_Time - begin_Time));\n//     std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time_ms));\n//     //frame_time = 0.03;\n//     end_Time = chrono::time_point_cast\u003ca href=\"chrono::milliseconds\"\u003echrono::milliseconds\u003c/a\u003e(chrono::system_clock::now()).time_since_epoch().count();\n//     frame_time = (end_Time - begin_Time)/1000.0;\n//     // cout \u0026laquo; \u0026ldquo;sleep_time_ms:\u0026rdquo; \u0026laquo; sleep_time_ms \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl;\n//     // cout \u0026laquo; \u0026ldquo;frame_time:\u0026rdquo; \u0026laquo; frame_time\n1000 \u0026laquo; \u0026ldquo;ms\u0026rdquo; \u0026laquo; endl;\n// }\n// begin_Time = end_Time;\n// debugdate1.data = frame_time\n1000;\ndebugpub.publish(debugdate);\ndebugpub1.publish(debugdate1);\n}\ncatch (const std::exception\u0026amp; e)\n{\nROS_ERROR(\u0026quot;[Exception] In Track function: %s\u0026rdquo;, e.what());\n}\n}\n// 重构选板\nGimbalPose reconstruction_choose_compensation() {\n// 目标装甲板\nArmor abs_facing_armor;\nArmor abs_target_armor;\ndouble hit_time = 0;\ndouble center_hit_time = 0;\ndouble m_time_off = COMMAND_TIMESPAN + eTime + 0.025;\n/**\n* 严格意义上来说，如果要准确预测击中时刻的装甲板位置的话，需要解一个非线性方程。此处采用一种近似的解法\n* 根据当前最近装甲板距离计算击中时间，用于预测目标装甲板出现的位置\n* 事实上相当于一步牛顿迭代法，或者说一阶的线性化\u003c/p\u003e","title":"AI辅助解决弹道复现BUG"},{"content":"ai使用指南 Deepseek\n一，推理模型与指令模型 指令模型v1,豆包\u0026hellip; 你需要使用结构化的指令 推理模型o1,r1\u0026hellip;. 你只需要清晰明确的表达你的需求\n二，理解大语言模型的本质 特点1：大模型在训练时是将内容token化的，大模型看到和理解的世界与你不一样 特点2：大模型知识是存在截止时间的 r1的截止时间大概在2023年的10月到12月 问题： 行业认知断代问题 解决方法：打开联网搜索，或者上传文档 特点3：大模型缺乏自我认知/自我意识 多数模型都不知道自己是什么模型，除非在部署的时候在系统提示词做了对应设定 特点4： 大模型有记忆限制（64k/128k） 中文字大概最多3到4万字 对话轮数过多，可能会遗忘最初的聊天问题 特点5： 输出长度有限2000-4000\n三，有效的7大r1使用技巧 提出明确要求 要求特定风格 提供充分的任务背景信息 主动标注自己的知识状态 定义目标，而非过程 提供AI不具备的知识背景 从开放到内敛\n四，r1无效的提示词技巧 思维链提示 结构化提示 要求扮演专家角色 假装完成任务后给奖励 少示例提示 角色扮演 对已知概念进行解释\nai使用场景\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/ai%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/","summary":"\u003ch1 id=\"ai使用指南\"\u003eai使用指南\u003c/h1\u003e\n\u003cp\u003eDeepseek\u003c/p\u003e\n\u003ch2 id=\"一推理模型与指令模型\"\u003e一，推理模型与指令模型\u003c/h2\u003e\n\u003cp\u003e指令模型v1,豆包\u0026hellip;\n你需要使用结构化的指令\n推理模型o1,r1\u0026hellip;.\n你只需要清晰明确的表达你的需求\u003c/p\u003e\n\u003ch2 id=\"二理解大语言模型的本质\"\u003e二，理解大语言模型的本质\u003c/h2\u003e\n\u003cp\u003e特点1：大模型在训练时是将内容token化的，大模型看到和理解的世界与你不一样\n特点2：大模型知识是存在截止时间的\nr1的截止时间大概在2023年的10月到12月\n问题：\n行业认知断代问题\n解决方法：打开联网搜索，或者上传文档\n特点3：大模型缺乏自我认知/自我意识\n多数模型都不知道自己是什么模型，除非在部署的时候在系统提示词做了对应设定\n特点4：\n大模型有记忆限制（64k/128k）\n中文字大概最多3到4万字\n对话轮数过多，可能会遗忘最初的聊天问题\n特点5：\n输出长度有限2000-4000\u003c/p\u003e","title":"ai使用指南"},{"content":"C/C++代码中积累的语法 C/C++中的数据类型转换 数据类型自动转换 当不同类型的变量同时运算时就会发生数据类型的自动转换，以常见的 char、short、int、long、float、double 这些类型为例，如果 char 和 int 两个类型的变量相加时，就会把 char 先转换成 int 再进行加法运算，如果是 int 和 double 类型的变量相乘就会把 int 转换成 double 再进行运算。\nC语言中的强制类型转换 前面说了自动转换，从这里开始聊聊强制类型转换，需要强制类型转换往往程序不那么智能了，需要人工进行干预。比如把一个int 类型的变量赋值给 char 类型的变量，或者说把两个 int 相乘时可能会得到一个很大的数，所以需要先把 int 强制转换成 double 计算防止溢出。C++中的强制类型转换\nC++中的强制类型转换 在C++语言中新增了四个用于强制类型转换的关键字，分别是 static_cast、 dynamic_cast, const_cast、 和 reinterpret_cast，使用语法为 xxxx_cast\u0026lt;new_type_name\u0026gt;(expression)。\nstatic_cast 这个关键字的作用主要表现在 static 上，是一种静态的转换，在编译期就能确定的转换，可以完成C语言中的强制类型转换中的大部分工作，但需要注意的是，它不能转换掉表达式的 const、volitale 或者 __unaligned 属性。\ndynamic_cast 从名字上看，这个关键字与 static_cast 的静态转换是对立的，这是一个“动态”转换函数，只能对指针和引用的进行转换，并且只用于类继承结构中基类和派生类之间指针或引用的转换，可以进行向上、向下，或者横向的转换。 相比于 static_cast 的编译时转换， dynamic_cast 的转换还会在运行时进行类型检查，转换的条件也比较苛刻，必须有继承关系的类之间才能转换，并且在基类中有虚函数才可以，有一种特殊的情况就是可以把类指针转换成 void* 类型。\nconst_cast 一种类型转换符,一种移除指针或引用的const或 volatile限定符\nreinterpret_cast 它被用于不同类型指针或引用之间的转换，或者指针和整数之间的转换，是对比特位的简单拷贝并重新解释，因此在使用过程中需要特别谨慎，比如前面提到的一个例子，static_cast 不能将 int* 直接强转成 char*，使用reinterpret_cast就可以办到。\nC/C++ 中 volatile 关键字详解 volatile 关键字是一种类型修饰符，用它声明的类型变量表示可以被某些编译器未知的因素更改，比如：操作系统、硬件或者其它线程等。遇到这个关键字声明的变量，编译器对访问该变量的代码就不再进行优化，从而可以提供对特殊地址的稳定访问。声明时语法：int volatile vInt; 当要求使用 volatile 声明的变量的值的时候，系统总是重新从它所在的内存读取数据，即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。\nC++中的inline关键字 C++中的inline关键字主要用于向编译器建议将函数或变量进行内联展开 核心目的: 减少函数调用开销(压栈,跳转,返回),提高小型,频繁调用函数的执行效率 -\u0026gt; 适用于函数体小 工作原理: 编译期间尝试将函数体代码直接插入到每一个调用处,类似\u0026quot;赋值\u0026quot;\n优点: 提升性能,类型安全,易于调试(相比宏),可替代宏定义 缺点: 可能导致代码膨胀,从而降低缓存命中率 -\u0026gt; 适用于复杂函数,递归函数或虚函数\n内联展开: 当编译器处理一个被建议内联的函数时,它会尝试将该函数体直接赋值粘贴到每一个调用它的地方\n缓存命中率: 衡量系统效率的一个关键指标 , 计算公式： 缓存命中次数 / 总访问次数 * 100\nC++ override关键字 是一个显示标识符,用于明确告诉编译器,派生类的这个成员函数意图重写(Override)基类的虚函数\n当你使用override 关键字时,编译器会进行严格检查,确保派生类中的函数缺失成功重写了基类的虚函数,如果没有找到匹配的基类虚函数,编译器会直接报错\nstd::optional 用于表达: 可能有值,也可能没有值,std::optional 是现代 C++ 中表达“可选值”的 标准、安全、清晰 的方式 主要成员函数和操作\nstd::atomic C++11 引入的模板类 std::atomic = 多线程环境下的\u0026quot;安全锁变量\u0026quot;,让多个线程同时修改一个数时不会出错\nperror()函数 perror()是C语言标准库中的一个错误处理函数,用于将错误信息输出到标准错误流\n函数原型\n#include \u0026lt;stdio.h\u0026gt; void perror(const char *str); 功能说明: 参数: str是一个自定义的提示字符串 输出格式: str:系统错误描述信息\\n 输出位置: 标准错误流\n工作原理: perror会根据全局变量errno的当前值,查找对应的系统错误描述信息,并与你提供的字符串组合输出\n异步线程 // 1. 线程创建和lambda表达式 thread th_process(\u0026amp;rs{ while(ros::ok()){ ros::spinOnce(); } });\n// 2.线程分离 th_process.detach();\n// 3. 主线程循环 while(ros::ok()) { rs.Prorgess(); // 注意：可能是Progress的拼写错误 }\n数据类型 size_t: size_t 是一种在 C 和 C++ 编程语言中广泛使用的数据类型，通常用于表示对象的大小\n定义 size_t 是一种无符号整数类型，定义在多个标准库头文件中，例如 \u0026lt;stddef.h\u0026gt;、\u0026lt;stdio.h\u0026gt;、\u0026lt;stdlib.h\u0026gt; 等。它的大小取决于目标平台的架构： 在 32 位系统中，size_t 通常是 32 位无符号整数（unsigned int）。 在 64 位系统中，size_t 通常是 64 位无符号整数（unsigned long 或 unsigned long long）。 用途 size_t 的主要用途是表示内存大小或对象的大小，例如： 在函数中表示数组或缓冲区的大小，如 malloc、calloc、realloc、memcpy 等函数的参数。 用于存储对象的字节大小，例如 sizeof 运算符的结果。 为什么使用 size_t 跨平台兼容性：size_t 的大小会根据目标平台自动调整，确保代码在不同架构（如 32 位和 64 位系统）上都能正确运行。 安全性：由于 size_t 是无符号类型，它可以避免在处理大小时出现负值，从而减少潜在的错误。 语义清晰：使用 size_t 明确表示变量用于表示大小，使代码更具可读性。 智能指针 std::unique_ptr: 独占所有权 1.std::unique_ptr是一种独占所有权的智能指针,确保同一时间只有一个unique_ptr拥有对象的所有权.它不可复制,但可以通过std::move转移所有权 使用场景: 管理不需要共享的动态资源,如局部动态对象,工厂函数返回值 注意: unique_ptr几乎零开销,性能与裸指针相当,是多数情况下的首选\n2.std::shared_ptr：共享所有权 std::shared_ptr通过引用计数机制管理共享所有权.多个shared_ptr可以指向同一对象,当最后一个shared_ptr被销毁时,对象才会被释放 循环引用问题\n3.std::weak_ptr: 弱引用(不拥有) std::weak_ptr是一种不控制对象生命周期的智能指针,它指向由shared_ptr管理的对象,但不增加引用计数,主要用于解决而不影响其生命周期,或解决循环引用 使用场景: 观察shared_ptr管理的对象而不影响其生命周期,或解决循环引用\n构造函数: 默认构造函数: 有参构造函数: 略\n拷贝构造函数: 拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本\n默认拷贝构造函数： 如果你不定义拷贝构造函数,编译器会自动生成一个\n编译器对待拷贝构造函数和普通构造函数的不同: 1.调用时机； 普通构造函数的调用: 编译器明确直到调用普通构造函数 拷贝构造函数的自动调用: 编译器自动判断调用拷贝构造函数的场景\n2.自动生成规则不同: 普通构造函数的生成规则: 如果没有声明任何构造函数,编译器会自动生成默认构造函数 拷贝构造函数的生成规则: 即使声明了其他构造函数，编译器仍会生成拷贝构造函数\n深拷贝vs浅拷贝 浅拷贝: 仅复制指针的值(内存地址) 深拷贝: 为新对象分配新内存,并复制指针所指的内容\nclass Example { int x; std::string name; int* data; public: Example(int val) : x(val), data(new int[10]) {}\n// 即使声明了其他构造函数，编译器仍会生成拷贝构造函数 // 除非显式声明或删除 };\nExample e1(10); Example e2 = e1; // 调用编译器生成的拷贝构造函数（浅拷贝！危险！）为什么危险？ 危险的原因: 编译器生成的默认拷贝构造函数进行的是浅拷贝 这会导致多个对象共享同一块动态内存,从而引发一系列严重的问题\n1.重复释放导致程序崩溃: 当e1和e2的生命周期结束时,它们的析构函数会被自动调用。由于你没有自定义析构函数,编译器也会被自动调用,由于e1的data和e2的data指向的是同一块内存,这块内存被释放.紧接着e2析构时,它会尝试再次释放同一块已经归还给系统的内存,这会导致未定义行为,通常直接造成程序崩溃\n2.数据意外修改: 修改一个会改另一个\n3.悬挂指针 e1被销毁了,e2还在，但是它的data成员已经指向无效内存地址的悬挂指针\n解决方法； 使用深拷贝\nclass Example { int x; std::string name; int* data; size_t size; // 记录数组大小，确保拷贝正确\npublic: Example(int val) : x(val), size(10), data(new int[10]) {}\n// 1. 深拷贝构造函数 Example(const Example\u0026amp; other) : x(other.x), name(other.name), size(other.size), data(new int[other.size]) { // 分配新内存 // 复制数据内容 std::copy(other.data, other.data + other.size, data); } // 2. 深拷贝赋值运算符 Example\u0026amp; operator=(const Example\u0026amp; other) { if (this != \u0026amp;other) { // 关键：防止自赋值 (e.g., e1 = e1) delete[] data; // 释放自己的旧资源 x = other.x; name = other.name; size = other.size; data = new int[size]; // 分配新内存 std::copy(other.data, other.data + size, data); // 复制数据 } return *this; } // 3. 析构函数 ~Example() { delete[] data; // 安全释放自己独占的内存 } };\n左值右值： std::move的本质 作用：将左值转换为右值引用\n// std::move 的简化实现 template typename std::remove_reference::type\u0026amp;\u0026amp; move(T\u0026amp;\u0026amp; arg) noexcept { return static_cast\u0026lt;typename std::remove_reference::type\u0026amp;\u0026amp;\u0026gt;(arg); } 特别注意: 当一个对象使用std::move后,就表示你承诺不再使用它(除非重新赋值),因为它的资源可能已经被移走,状态是未定义的: 引发难以调试的bug\n左值引用(T\u0026amp;) (是别名,旨在共享和操作持久对象) 绑定对象: 左值(有标识,有持久性的对象)\nint\u0026amp; ref = variable; 右值引用(T\u0026amp;\u0026amp;) (旨在高效转移临时对象的资源) 绑定对象: 右值(临时,即将消亡的对象)\nint\u0026amp;\u0026amp; ref = 10; int\u0026amp;\u0026amp; ref = std::move(variable); // 或者\nRVO和NRVO(编译器优化) 这两种优化的核心思想都是\u0026quot;偷梁换柱\u0026quot;: 编译器通过在调用出为接收返回值的对象分配内存,然后将这块内存的地址作为一个隐藏参数传递给被调函数.被调函数会直接在这块预先分配好的内存上构造本应返回的对象,从而完全避免了中间临时对象的创建和拷贝\nRVO(返回值优化) 优化对象: 返回匿名临时对象\nNRVO(具名返回值优化) 优化对象: 返回具名的局部对象\ntr1::function/std::function function是一个通用的,多态的函数封装器,它是一个类模板,可以容纳(保存,拷贝,调用)几乎所有类型的可调用对象\n简单来说: 有点像一个函数指针的超级升级版\n一个普通的函数指针只能指向一个特定签名的函数.而function对象可以容纳任何具有兼容签名的可调用实体 例如: 1.普通函数 2.函数对象(重载了operator()的类,即Functor) 3.Lambda表达式(C++11引入,但TR1时期还没有) 4.类的成员函数(需要配合st d::bind或tr1::bind) 5.类的数据成员指针(需要配合std::bind或tr1::bind)\nhypot()函数 作用: 计算直角三角形的斜边长 -\u0026gt; 勾股定理\n计算二维或三维空间中的距离（例如，点 (x, y) 到原点 (0, 0) 的距离）。 在图形学、物理模拟、机器学习、信号处理等任何需要用到向量长度的领域。 避免在手动计算 sqrt(xx + yy) 时可能出现的数值计算问题。\n#include \u0026lt;math.h\u0026gt; // C #include // C++\ndouble hypot(double x, double y); float hypotf(float x, float y); // 单精度版本 long double hypotl(long double x, long double y); // 长双精度版本\nC语言中关于内存的一些函数 malloc() - 内存动态分配 典型用途: 动态数据结构\nint* arr = (int*)malloc(10 * sizeof(int));\nrealloc() - 内存重新分配 典型用途: 动态数组扩容 调整已分配内存块的大小(可扩大或缩小)\nint* arr = (int*)malloc(5 * sizeof(int)); // 使用arr\u0026hellip;\n// 扩容到10个元素 int* new_arr = (int*)realloc(arr, 10 * sizeof(int)); if (new_arr == NULL) { // 注意：如果realloc失败，原指针ptr依然有效！ free(arr); // 清理原内存 perror(\u0026ldquo;realloc failed\u0026rdquo;); exit(1); } arr = new_arr; // 使用新指针\n// 缩小到3个元素 arr = (int*)realloc(arr, 3 * sizeof(int)); // 注意：缩小通常成功，但可能返回不同的地址\nmemset() - 内存填充 典型用途: 清零,初始化 将内存块的每个字节设置为指定值\nstruct Student {int id;char name[20];float grade;};\nstruct Student s;memset(\u0026amp;s, 0, sizeof(struct Student));\nchar buffer[1024];memset(buffer, \u0026lsquo;A\u0026rsquo;, 100); // 前100字节设为\u0026rsquo;A\u0026rsquo;memset(buffer + 100, 0, 924); // 其余部分清零\nmemcpy() - 内存复制 - 不检查重叠 典型用途: 高效数据复制\n从源内存地址复制指定字节数到目标内存地址\nint src[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};int dest[10];memcpy(dest, src, sizeof(src));\n与strcpy()的区别 strcpy(dest1, src); // 遇到\u0026rsquo;\\0\u0026rsquo;停止，dest1 = \u0026ldquo;Hello\u0026rdquo; memcpy(dest2, src, 11); // 复制11个字节，包括中间的\u0026rsquo;\\0\u0026rsquo;\nmemmove - 内存复制 - 检查重叠 典型用途: 重叠内存复制\n// 将前13个字符复制到从第10个字符开始的位置 memmove(buffer1 + 9, buffer1, 13);\ncalloc - 分配内存 典型用途: 需要零初始化的分配\nvoid* calloc(size_t num, size_t size); 分配num个size字节的连续内存块，并初始化为全0。\nint* numbers = (int*)calloc(10, sizeof(int)); 封装普通函数\n#include #include int add(int a, int b) {return a + b;}\nint main() { std::function\u0026lt;int(int, int)\u0026gt; func;// 将普通函数 func = add; std::cout \u0026laquo; \u0026ldquo;Result: \u0026quot; \u0026laquo; result \u0026laquo; std::endl;\nreturn 0; }\n封装函数对象\n#include #include struct Multiplier {int factor;Multiplier(int f) : factor(f) {}int operator()(int value) {return value * factor;}};\nint main() { std::function\u0026lt;int(int)\u0026gt; func; Multiplier times5(5); func = times5;\nstd::cout \u0026lt;\u0026lt; func(10) \u0026lt;\u0026lt; std::endl; }\nstd::future是一个模板类 std::future 是一个模板类，尖括号里的 int 表示这个异步任务最终会返回一个整数。 它是连接\u0026quot;主线程\u0026quot;和\u0026quot;后台工作线程\u0026quot;的桥梁。后台线程负责计算,算完把结果填进去;主线程拿着future,等需要的时候取出来\neg. 你去餐厅点餐后,服务员给你的那张取餐小票 当你点餐(调用enqueue)时: 厨师(线程池里的线程)开始在后台做饭。饭还没做好,但服务员 立即给了你一张小票\n这张小票就是std::future: 它代表了一个未来会产生的结果。现在它是空的,但只要你拿着它,将来饭做好了,你就能取到饭\n.get()方法: 这就是你去柜台凭票取餐的动作 如果饭做好了,你立马拿走 如果饭还没做好,你就要在柜台死等(阻塞),知道饭做好为止\nsprintf_s和sprintf函数的区别 strcpy_s - 安全字符串拷贝 将源字符串src(包括结尾的'\\0')完整复制到目标缓冲区dest中. 如果目标缓冲区太小而无法包容源字符串,不会发生缓冲区溢出 将 dest[0] 设置为 '\\0'（空字符串） 调用错误处理函数（默认会触发断言并终止程序，或可通过 _set_invalid_parameter_handler 自定义） 返回非零错误码（EINVAL 或 ERANGE） sprintf_s – 安全格式化输出到字符串 将格式化数据（类似 printf）写入到字符串缓冲区 buffer 中， 并在末尾自动添加 '\\0'。会检查缓冲区是否足够大， 如果格式化后的字符串（包括结尾空字符）超过 buffer_size，则： image.png\nCPP读书笔记 C/C++primer plus 第十章对象和类 面向对象编程 首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作\n默认构造函数 通过函数重载,可以创建多个同名的析构函数,条件是每个函数的特征标都不同。 如果没有提供任何构造函数，则C++将自动提供默认构造函数 如果提供了非默认构造函数，但没有提供默认构造函数，则直接声明会出错。\n析构函数 不应该在代码中显示地调用析构函数 1.如果创建的是静态存储类对象，则析构将在程序结束时自动被调用 2.如果创建的是自动存储类对象，则其析构函数将在代码块时自动被调用 3.如果对象是new创建的，则它将驻留在栈内存或自由存储区中。\n成员名和参数名 在数据成员名使用m_前缀或加_后缀\nC++11列表初始化 只要提供与某个构造函数的参数列表匹配的内容，并用构造函数将它们括起。\nconst成员函数 例如 void show() const保证函数不会修改调用对象\nthis指针 ?没看懂\n对象数组 初始化对象数组的方案 首先使用默认构造函数创建数组对象，然后花括号中的构造函数将创建临时对象，然后将临时对象的内容复制到相应的元素中。\n类作用域 在类中定义的名称的作用域都为整个类，作用域为整个类的名称只在类该类中是已知的，在类外是不可知的。因此可以在不同类中使用相同的类成员名而不会引起冲突\n作用域为类的常量 行不通，因为声明类只是描述了对象的形式，并没有创建对象。因此，在创建对象前，讲没有用于存储值的空间。\n作用域内枚举 enum class C++还提高了作用域馁枚举的类型安全。但在有些情况下，常规枚举将自动转换为整型，如将其赋值给int变量或用于比较表达式，但作用域内枚举不能隐式地转换为整型。\n抽象数据类型 略\n第十一章使用类 不要害怕犯错误，因为在解决问题的过程中学到的知识,比生搬硬套而不犯错误要多得多. 学习C++的 运算符重载 不要返回指向局部变量或临时变量的引用 局部变量:在函数结束时被销毁,内存被回收 临时变量:在创建它们的表达式结束时销毁 返回的引用指向无效内存:这些变量在函数返回后不再存在导致引用指向\u0026quot;\u0026ldquo;垃圾数据\n添加加法运算符 operator+()\n重载限制 1.重载后的运算符必须有一个操作数是用户定义的类型 2.使用运算符时不能违反运算符原来的句法规则 3.不能 创建新运算符 4.不能重载下面的运算符 略 5.= () [] = -\u0026gt;这四个运算符只能通过成员函数进行重载\n友元 友元有三种: 友元函数,友元类,友元成员函数\n友元函数:一类特殊的非成员函数可以访问类的私有成员，他们被称为友元函数。 创建友元 第一步在原型前加上friend关键字 第二步编写函数定义。因为他不是成员函数，因此不能用成员函数来调用。 虽然是在类声明种声明的，但它不是成员函数，因此不能使用成员运算符来调用。 虽然不是成员函数，但它与成员函数的访问权限相同。\n只有类声明可以决定哪一个函数是友元，因此类声明任然控制了哪些函数可以访问私有数据。\nEffective C++ 1.让自己习惯C++ 条款01: 视C++为一个语言联邦 条款02: 尽量以const,enum,inline替换#define 条款03: 尽可能使用const 条款04: 确认对象被使用前已被初始化 2.构造/析构/赋值运算 条款05: 了解C++默默编写并调用哪些函数 条款06: 若不幸使用编译器自动生成的函数,就该明确拒绝 条款07：为多态基类声明virtual析构函数 C++中通过基类指针或引用删除派生类对象时,编译器需要根据析构函数是否为virtual来决定调用哪个析构函数: 静态绑定: 当基类析构函数非虚时,编译器根据指针的静态类型(即基类类型)在编译期就确定调用基类的析构函数.它无法感知指针的实际指向的派生类对象,因此不会调用派生类的析构函数 动态绑定: 当基类析构函数为virtual时,C++通过虚函数表(vtable)机制实现运行时多态.每个包含虚函数的类都有一个vtable,其中存放着虚函数的地址.派生类会覆盖基类的虚析构函数项.当删除对象时,系统会根据对象的实际类型查找vtable，找到并调用派生类的析构函数.派生类的析构函数执行完毕后,会自动调用基类的析构函数,从而形成完整的析构链 -\u0026gt; 即使派生类的析构函数没有显式地写上virtual,派生类的析构函数便会自动成为析构函数\nvptr -\u0026gt; 虚指针 vtable -\u0026gt; 虚函数表\nvptr指针指向一个由函数指针构成的数组,称为vtbl (主要用来在运行期决定哪一个virtual函数该被调用)\n析构函数运作方式,最深层派生的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用\n条款08: 别让异常逃离析构函数 条款09: 绝不在构造和析构过程中调用virtual函数 条款10: 令operator= 返回一个reference to *this 条款11: 在operator= 中处理\u0026quot;自我赋值\u0026rdquo; 3.资源管理 条款13 : 以对象管理资源 资源: 内存,文件描述器,互斥锁,图形界面中的字型和笔刷,网络sockets 无论哪一种资源,重要的是,当你不再使用它的时候,必须将它还给系统\n无端地将所有classes的将所有classes的析构函数声明为virtual，就像从未声明它们为virtual，都是错误的. \u0026mdash;\u0026mdash;\u0026gt; 添加vptr会大量添加对象的大小\n函数内多重回传路径: 如果一个函数声明了非void的返回类型,那么它就必须在所有可能的执行路径上都提供一个返回值.否则,编译器会报错\ncopying函数: -\u0026gt; 通常指的是对象拷贝相关的特殊成员函数,主要用于管理对象的复制过程,它们对于确保资源正常管理,避免内存泄漏和数据混乱至关重要\n1.拷贝构造函数: 拷贝构造函数用于用一个已存在的对象来初始化一个新对象\n2.拷贝赋值运算符: 用于在对象已存在的情况下,将同一个类对象的值赋值给它.他通过operator=来实现,定义了当使用赋值操作=时对象的行为\nclass MyClass { public: // \u0026hellip; // 拷贝赋值运算符 MyClass\u0026amp; operator=(const MyClass\u0026amp; other) { // 赋值操作逻辑 if (this != \u0026amp;other) { // 1. 自我赋值检查 // 2. 释放当前对象的资源（如果需要） // 3. 复制 other 对象的值 value = other.value; } return *this; // 4. 返回当前对象的引用(为了支持链式赋值) } private: int value; // \u0026hellip; 其他成员 };\n因为return和异常可能导致delete没有被执行,资源没有被释放(我们泄漏的不只是内含投资对象的那块内存,还包括投资对象所保存的任何资源) 把资源放进对象内,我们便可依赖C++的\u0026quot;析构函数自动调用机制\u0026quot;确保资源被释放\n以对象管理资源的两个关键想法： 1.获得资源后立即放进管理对象 -\u0026gt; (资源取得时机便是初始化实际) RAII 2.管理对象运用析构函数确保资源被释放\n注意 auto_ptr被销毁时会自动删除它所指之物,别让auto_ptr同时指向同一对象,否则对象会被删除一次以上,程序会引发未定义行为\nRCSP (引用计数型智能指针): RCSP持续追踪共有多少对象指向某笔资源,并在无人指向它的时自动删除该资源.RCSP提供的行为类似垃圾回收,不同的是RCSP无法大伯环状引用,例如两个起始已经没有被使用的对象彼此互指,因而好像还处在被使用的状态\n4.设计与声明 条款19: 设计class犹如设计type C++就像在其他OOP（面向对象编程）语言一样，当你定义一个新class,也就定义了一个新的type\n1.新type的对象应该如何被创建和销毁? 会影响到你的构造函数和析构函数以及内存分配函数和释放函数\n2.对象的初始化和对象的赋值该有什么样的差别? 因为它们对应不同的函数调用\n3.新type的对象如果passed by value(以值传递),意味着什么?\n4.什么是新type的\u0026quot;合法值\u0026rdquo;?\n5.你的新type需要配合某个继承图系(Inheritance graph)吗? 如果你允许其他classes继承你的class，那会影响你所声明的函数\u0026mdash;\u0026mdash;尤其时析构函数\u0026mdash;\u0026mdash;-是否为virtual\n6.你的新type需要什么样的转换?\n7.什么样的操作符和函数对此type而言是合理的?\n8.什么样的标准函数应该被驳回? 那些正是你必须声明为private者\n9.谁该取用新type的成员?\n10.什么是新type的\u0026quot;未声明接口\u0026quot;(undeclared interface)? 它对效率,异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证? 你在这些方面提供的保证将为你的class实现代码加上响应的约束条件\n11.你的新type有多么一般化? 或许你其实并非定义一个新type，而是定义一个新type,而是定义一整个type家族.果真如此你就不该定义一个新class，而是定义一个新的class template\n12.你真的需要一个新type吗? 如果只是定义新的deriver class 以便为既有的class添加机能,那么说不定一或多个non-member函数或templates,更能够到达目标\n条款20: 宁以pass-by-reference-to-const 替换 pass-by-value 尽量以pass-by-value-to-const 替换 pass-by-value. 前者通常比较高效,并可避免切割问题 切割问题: 发生在将派生类对象按值传递给期望基类参数的函数时,派生类特有的部分被切割掉,只保留基类部分\n条款21: 必须返回对象时,别妄想返回其reference 5.实现 6.继承与面向对象设计 条款32: 确定你的public继承塑膜出is-a关系 Public 继承 意味着is - a,适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象\n条款33: 避免遮掩继承而来的名称 内层作用域的名称会遮掩外围作用域的名称\n编译器遭遇名称x，现在local作用域内查找是否有什么东西带着这个名称.如果找到就不再找其他作用域,否则就逐层的向外查找,，直到查找到global scope\n条款34: 区分接口继承和实现继承 函数接口继承\n函数实现继承\n接口继承和实现继承不同,在public继承之下,derived classes总是继承base class的接口 Pure virtual函数只具体指定接口继承 简朴的impure virtual函数具体指定接口继承及缺省实现继承 non-virtual函数具体指定接口继承以及强制性实现继承\n条款35: 考虑virtual函数以外的其他选择 tr1::function\n条款36: 绝不重新定义继承而来的non-virtual函数 non-virtual函数: 调用版本取决于指针/引用的静态类型(编译时决定) - 静态绑定 virtual函数: 调用的版本取决于对象的实际类型(运行时决定) - 动态绑定\n1.破坏了is-a关系 共有继承应该意味着\u0026quot;派生类对象是一个基类对象\u0026quot; 但对于non-virtual函数,同样的对象表现出不同的行为,这违背了\u0026quot;is-a\u0026quot;原则 2.导致不一致的行为 3.违反里氏替换原则(LSP) 派生类应该能够替换基类而不影响程序的正确性 重新定义non-virtual函数破坏了这个原则\nnon-virual函数的定义: 当你在基类中将一个函数声明为non-virtual,你实际上是在向所有派生类传达: 这个函数的行为是不可变的,不可定制的。它对所有派生类对象都有相同的意义和实现\nvirtual函数的定义: 当你在基类中将一个函数声明为virtual，你实际上是在说: 这个函数的行为是可以被定制的.各个派生类可以根据自己的需要来重写它\n条款37: 绝不重新定义继承而来的缺省参数值 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数\u0026mdash;你唯一应该覆写的东西\u0026mdash;却是动态绑定\n7.模板与泛型编程 8.定制new 和 delete 9.杂项讨论 深度求索C++对象模型 第一章关于对象 关于对象是整本书的基石.这一章主要为了确立大局观: 当你从C语言的struct走向C++的class时,为了获得面向对象的特性,你到底付出了什么代价? 底层模型又是长什么样的?\n1.封装的额外成本: 其实是零 C++ 在布局和存取时间上的主要额外开销，仅仅是由虚拟（Virtual）机制带来的（包括虚函数和虚拟继承）。普通的非静态数据成员（Non-static data members）在内存中的布局和 C 语言的 struct 完全一样。普通的成员函数无论是声明多少个，都不会占用对象实例哪怕 1 个字节的内存空间！\n2.C++对象模型 对象内部: 只有非静态数据成员（Non-static data members），以及为了支持多态而可能被编译器偷偷安插的虚函数表指针（vptr）。\n对象外部（全程序共享）: 所有的静态数据成员（Static data members）、普通的成员函数（Non-static function members）和静态成员函数（Static function members）都被放在对象之外。\n虚函数表: 每个包含虚函数的类都会产生一个独立的虚函数表，里面存放着该类所有虚函数的地址，以及支持运行时类型信息（RTTI）的 type_info 对象。\n3.多态的威力与限制 指针与引用是前提： C++ 仅仅通过指针（Pointers）和引用（References）来支持多态。当你用一个基类指针指向派生类对象时，编译器不会在编译期写死调用的函数，而是在运行期通过 vptr 找到真正的函数去执行。\n对象切割（Slicing）： 如果你直接把一个派生类对象（而不是指针或引用）赋值给一个基类对象，多态就会瞬间失效。编译器会把派生类对象中多出来的部分直接“切掉”，只保留基类部分的数据进行按位拷贝。这就是为什么面向对象的多态绝不能通过传值（Pass by Value）来实现。\n4.面向对象(OO)与基于对象(OB) Object-Oriented (OO)： 包含继承和多态。由于具体对象的类型在编译期无法确定，所以必须在**运行期（Runtime）**通过指针或引用来进行动态绑定。设计非常灵活，但存在 vptr 内存占用和虚函数间接访问的性能开销。\nObject-Based (OB)： 只使用了类的封装特性，可能包含非多态的数据类型（比如原生的 string 类）。没有任何虚函数。所有的解析和函数调用在**编译期（Compile-time）**就完全确定了（静态绑定）。运行速度极快，内存紧凑，但缺乏面向对象的动态扩展性。\n第二章构造函数语意学 编译器到底在我们的构造函数背后偷偷做了哪些手脚?\n1.默认构造函数的真相 编译器只有在真正需要的时候,才会合成出一个nontrivial(非平凡的/有用的) 默认构造函数.如果不符合条件,编译器根本懒得管,它什么都不会生成\n以下四种情况: 1.带有默认构造函数的成员对象: 2.继承自带有默认构造函数的基类 3.带有虚函数的类 4.带有虚基类的类\n2.拷贝构造函数与Bitwise Copy (The Copy Constructor) 当我们用一个对象去初始化另一个同类型对象时，默认情况下，C++ 编译器会展现出 Bitwise Copy Semantics（位逐次拷贝语意），也就是简单粗暴地把内存里的数据按位原封不动地复制过去。这种做法效率极高。\n什么时候 Bitwise Copy 会失效？ 当简单的按位拷贝会破坏程序逻辑时，编译器就必须合成出一个 nontrivial 的拷贝构造函数 来执行逐成员初始化（Memberwise Initialization）。同样也是四种情况：\n1.成员对象带有拷贝构造函数。 2.基类带有拷贝构造函数。 3.类声明了虚函数： 这是最关键的考点！假设你有一个派生类对象，并把它拷贝给一个基类对象（发生切片）。如果执行按位拷贝，基类对象的 vptr 就会被错误地指向派生类的虚函数表，这会引发灾难。因此编译器必须介入，确保新对象的 vptr 指向正确的虚表。 4.类派生自虚拟继承体系。\n3.程序与NRV优化 这一部分探讨了对象在按值传递和按值返回时,编译器在底层优化,最著名的就是NRV优化 编译器会在底层修改函数签名，直接把你外层用来接收返回值的那个对象的内存地址作为隐藏参数传进函数里。函数内部的代码直接在这块外部内存上构造对象。这样就完美消除了一次拷贝构造和析构的开销。\n4.成员初始化列表的陷阱 必须使用初始化列表的情况 1.初始化一个Reference(引用)成员时 2.初始化一个const成员时 3.调用一个成员对象的构造函数，并且它拥有一组参数时 4.调用一个成员对象的构造函数,并且它拥有一组参数时\n最大陷阱: 成员变量的初始化的顺序,完全是由它们在类中声明的顺序决定的\n第三章Data语意学 当我们定义个C++类并实例化对象时,这个对象的数据在内存中到底是怎么排列的 -\u0026gt; 编译器在背后为了实现面向对象所做的脏活累活\n1.一个C++对象到底多大? 空类(Empty Class): 如果你定义一个没有任何数据成员的空类,它实例化后的大小通常是1byte. 这是编译器为了保证该类的每一个对象在内存中都有一个独一无二的地址\n内存对齐(Memory Alignment): 为了让CPU更高效地读取数据,编译器会在数据成员之间插入填充字节.比如一个char(1字节) 跟着一个int(4字节),它们可能总共占据8个字节\n语言支持的额外开销(Overhead): 如果你的类声明了虚函数,或者使用了虚拟继承,编译器会在对象内部偷偷插入指针(比如指向虚函数表地vptr,或指向虚基类表地vptr)\n2.数据成员的布局（Data Member Layout） C++中的数据成员分为静态(static)和非静态(non-static)\nStatic 数据成员： 它们不存放在类的对象中，而是存放在程序的全局数据段。不管你创建了 100 个对象还是 0 个对象，静态数据成员永远只有一份实体。\nNon-static 数据成员： 它们存放在每一个类的对象内部。 在同一个访问控制区（比如都在同一个 private: 下）内，成员在内存中的排列顺序与它们在类中被声明的顺序完全一致。 如果有多个访问控制区（比如先写了一段 public，又写了一段 private，再写一段 public），C++ 标准并不强制规定不同区域的先后顺序，但目前主流编译器通常还是按照声明的顺序连续存放在内存中。\n3.继承对数据布局的影响 单一继承(不含虚函数): 派生类（Derived class）的对象包含了基类（Base class）的子对象。通常，基类的数据会被放在派生类对象的最前面（低地址处），然后接着放派生类自己的数据成员。\n加上多态(添加了虚函数): 只要类继承体系中出现了虚函数，编译器就会在对象中安插一个虚函数表指针（vptr）。至于这个 vptr 放在对象的开头还是结尾，取决于具体的编译器（GCC 和 MSVC 的处理方式不同）。\n多重继承: 如果类 C 同时继承了类 A 和类 B。在类 C 的对象中，会先存放 A 的子对象，紧接着存放 B 的子对象，最后存放 C 自己的成员。 这里有一个关键点： 如果你把一个 C 对象的地址赋值给一个 B 类型的指针，编译器必须在底层对指针的地址进行 偏移量调整（Offset adjustment），以确保指针指向的是 B 子对象的正确起始位置。\n补充: cpp通过虚继承来解决菱形继承问题,菱形继承指的是两个派生类（如 B 和 C）共同继承同一个基类（A），然后又一个类（D）同时继承这两个派生类，导致 D 中包含两份 A 的子对象，访问 A 的成员时会产生二义性，且浪费内存。\n虚拟继承: 菱形继承（A 派生出 B 和 C，D 同时继承 B 和 C）会导致 A 的数据在 D 中出现两份。虚拟继承就是为了解决这个问题。 在虚拟继承的底层实现中，编译器通常会使用指针（或者虚基类表偏移量）来追踪共享的那个基类（A）。这使得访问虚基类的数据变得相对间接，带来了一定的性能开销，但保证了数据的唯一性。\nclass A { public: int value; };\nclass B : virtual public A { }; // 虚继承 class C : virtual public A { }; // 虚继承\nclass D : public B, public C { }; // D 中只有一份 A\nD d; d.value = 10; // 无二义性，直接访问共享的那一份 A\n实现原理: 编译器会为虚继承的类生成一个虚基类指针(vbtr),指向虚基类表(vbtable) 表中记录了虚基类对象相对于当前对象地址的偏移量.通过这个偏移量,所有派生类共享同一个虚基类实例 因此D中的B和C不再各自保存独立的A,而是通过偏移量找到共同的A子对象\n4.数据成员的存取 访问 Static 数据： 就跟访问普通的全局变量一样，在编译时地址就已经确定了，没有任何运行时的额外开销。\n访问 Non-static 数据： 编译器会通过对象的首地址加上该成员在类中的 偏移量（Offset） 来计算出实际的内存地址。 如果是通过具体的对象（obj.x）访问，偏移量在编译期就完全确定了。 如果是通过指针或引用（ptr-\u0026gt;x）访问，并且这个类涉及了虚拟继承，那么具体的偏移量可能要到运行期才能确定，这就产生了一定的延迟。\n第四章Function语意学 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/c-c-%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%A7%AF%E7%B4%AF%E7%9A%84%E8%AF%AD%E6%B3%95/","summary":"\u003ch1 id=\"cc代码中积累的语法\"\u003eC/C++代码中积累的语法\u003c/h1\u003e\n\u003ch2 id=\"cc中的数据类型转换\"\u003eC/C++中的数据类型转换\u003c/h2\u003e\n\u003cp\u003e数据类型自动转换\n当不同类型的变量同时运算时就会发生数据类型的自动转换，以常见的 char、short、int、long、float、double 这些类型为例，如果 char 和 int 两个类型的变量相加时，就会把 char 先转换成 int 再进行加法运算，如果是 int 和 double 类型的变量相乘就会把 int 转换成 double 再进行运算。\u003c/p\u003e","title":"C/C++代码中积累的语法"},{"content":"Debug segmentation fault 段错误\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/debug/","summary":"\u003ch1 id=\"debug\"\u003eDebug\u003c/h1\u003e\n\u003cp\u003esegmentation fault 段错误\u003c/p\u003e","title":"Debug"},{"content":"docker ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/docker/","summary":"\u003ch1 id=\"docker\"\u003edocker\u003c/h1\u003e","title":"docker"},{"content":"Git分布式版本控制工具 1.基本命令行 ls/ll 查看当前目录\ncat 查看文件内容\ntouch 创建文件\nvi vi编辑器\n2.创建.bashrc文件 打开用户目录,创建.bashrc文件 在.bashrc文件中输入\n用于输出git提交日志 alias git-log=\u0026lsquo;git log \u0026ndash;pretty=oneline \u0026ndash;all \u0026ndash;graph \u0026ndash;abbrev-commit\u0026rsquo;\n用于输出当前目录所有文件及其基本信息 alias ll=\u0026lsquo;ls -al\u0026rsquo;\n3.解决GitBash乱码问题 1.打开GitBash执行下面指令 Git config \u0026ndash;global core.quotepath false\n2.\u0026amp;{git_home}/etc/bash.bashrc 文件最后加下面两行\nexport LANG=\u0026ldquo;zh_CN.UTF-8\u0026rdquo; export LC_ALL=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\n4 获取本地仓库 在电脑的任意位置创建一个空目录作为我们的本地Git仓库 进入这个目录中,电机右键Git bash窗口 执行命令git init 如果创建成功后可在文件夹下看到隐藏的.git目录 基础操作指令 Git工作目录下对文件的修改(增加,删除,更新)会存在几个状态,这些修改的状态会随着我们执行Git的命令而发生变化 image.png\n1.git add (工作区 -\u0026gt; 暂存区) git add . 将所有修改加入暂存区 2.git commit -m \u0026ldquo;注释内容\u0026rdquo; (暂存区 -\u0026gt; 本地仓库)\n仓库(repositor) 暂存区(index) 工作区(workspace) 查看修改的状态(status) 作用: 查看的修改的状态 (残存区,工作区) 命令形式: git status\n3.git log[options] (查看日志) 作用:查看提交记录 \u0026ndash;all 显示所有分支 \u0026ndash;pretty=oneline 将所有信息显示为一行 \u0026ndash;abbrev-commit 使得输出的commit更简短 \u0026ndash;graph 以图的形式显示\n版本回退 git reset \u0026ndash;hard commitID (commitID可以使用git-log或git log指令查看) git reflog (查看已经删除的记录) 5.添加文件至忽略列表 \u0026ndash;pretty=oneline\n5.修改用户名和邮箱地址 修改用户名:\ngit config \u0026ndash;global user.name \u0026ldquo;hydarealman\u0026rdquo; 修改邮箱地址:\ngit config \u0026ndash;global user.email 2281306133@qq.com\n6.如何查看git的邮箱地址和用户名是否配置成功 可以在这个路径下面找到: 路径地址 C:\\Users\\dong\n朋友给的笔记 git init //初始化仓库\ngit add .\ngit commit -m \u0026ldquo;message\u0026rdquo; //先保存本地的工作进度，避免被pull覆盖\ngit pull origin main //从远程仓库拉取代码到本地仓库分支，与远程仓库的新代码合并 [主机名][分支名] git push origin main\n将我的代推送到我的仓库 第一次提交 git init git branch -M main git add . git commit -m \u0026ldquo;\u0026hellip;\u0026rdquo; git remote add origin https://github.com/hydarealman/ws_glut_vision.git // 网址自定义 git push -u origin main\n再添加 git add . git commit -m \u0026ldquo;\u0026hellip;\u0026rdquo; git push\n使用git diff查看两段代码的差异 使用git diff对比两个文件的差异 文件A为旧文件 文件B为新文件\ncode \u0026ndash;diff 文件夹A/ 文件夹B/\n虚拟机上使用SSH git推送到github仓库 在 Ubuntu 虚拟机中执行 1. 清除 Git 代理 git config \u0026ndash;global \u0026ndash;unset http.proxy git config \u0026ndash;global \u0026ndash;unset https.proxy\n2. 生成 SSH 密钥（如果已有可以跳过） ssh-keygen -t ed25519 -C \u0026ldquo;your_email@example.com\u0026rdquo;\n3. 查看公钥并复制输出 cat ~/.ssh/id_ed25519.pub\n4. 在浏览器中登录 GitHub，进入 Settings -\u0026gt; SSH and GPG keys -\u0026gt; New SSH key，粘贴并保存 5. 修改本地仓库的远程地址为 SSH 格式 git remote set-url origin git@github.com:hydarealman/AimScope.git\n6. 推送 git push -u origin main\n代码回滚: 查看版本历史,找到目标提交的哈希值\ngit log \u0026ndash;oneline\n输出示例 a1b2c3d (HEAD -\u0026gt; main) 最新提交 e4f5g6h 上一个提交 i7j8k9l 更早的提交 ← 目标版本\n两种回滚方式 方法一: git reset(强硬回滚,会 丢失后续提交)\n将分支指针、暂存区和工作区都回滚到目标提交 git reset \u0026ndash;hard i7j8k9l\n方法二: git revert(安全回滚,保留历史)\ngit revert \u0026ndash;no-commit i7j8k9l..HEAD git commit -m \u0026ldquo;回滚到 i7j8k9l\u0026rdquo;\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/git%E5%88%86%E5%B8%83%E5%BC%8F%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6%E5%B7%A5%E5%85%B7/","summary":"\u003ch1 id=\"git分布式版本控制工具\"\u003eGit分布式版本控制工具\u003c/h1\u003e\n\u003ch2 id=\"1基本命令行\"\u003e1.基本命令行\u003c/h2\u003e\n\u003cp\u003els/ll   查看当前目录\u003c/p\u003e\n\u003cp\u003ecat    查看文件内容\u003c/p\u003e\n\u003cp\u003etouch  创建文件\u003c/p\u003e\n\u003cp\u003evi       vi编辑器\u003c/p\u003e\n\u003ch2 id=\"2创建bashrc文件\"\u003e2.创建.bashrc文件\u003c/h2\u003e\n\u003cp\u003e打开用户目录,创建.bashrc文件\n在.bashrc文件中输入\u003c/p\u003e\n\u003ch1 id=\"用于输出git提交日志\"\u003e用于输出git提交日志\u003c/h1\u003e\n\u003cp\u003ealias git-log=\u0026lsquo;git log \u0026ndash;pretty=oneline \u0026ndash;all \u0026ndash;graph \u0026ndash;abbrev-commit\u0026rsquo;\u003c/p\u003e\n\u003ch1 id=\"用于输出当前目录所有文件及其基本信息\"\u003e用于输出当前目录所有文件及其基本信息\u003c/h1\u003e\n\u003cp\u003ealias ll=\u0026lsquo;ls -al\u0026rsquo;\u003c/p\u003e\n\u003ch2 id=\"3解决gitbash乱码问题\"\u003e3.解决GitBash乱码问题\u003c/h2\u003e\n\u003cp\u003e1.打开GitBash执行下面指令\nGit config \u0026ndash;global core.quotepath false\u003c/p\u003e","title":"Git分布式版本控制工具"},{"content":"labelme https://blog.csdn.net/Natsuago/article/details/143815956\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/labelme/","summary":"\u003ch1 id=\"labelme\"\u003elabelme\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://blog.csdn.net/Natsuago/article/details/143815956\"\u003ehttps://blog.csdn.net/Natsuago/article/details/143815956\u003c/a\u003e\u003c/p\u003e","title":"labelme"},{"content":"lab相关文献阅读 阅读方法: image.png\n笔记归档 image.png\n文献一: 人工智能驱动的作物表型解析: 进展与挑战 这是一篇综述文章，本身不提出新算法，而是系统梳理了AI在六个表型维度上的已有技术方案和应用进展： image.png\n关键结论: 低通量,二维 -\u0026gt; 高通量,三维 技术成熟度不同 多模态融合成为趋势: 单一传感器信息有限,RGB+光谱+三维点云+环境数据的融合建模\n涉及的技术栈: 三维重建: MVS（多视图立体） SfM（运动恢复结构） LIDAR SLAM\n机器学习: 传统机器学习：随机森林、支持向量机(SVM)、岭回归(BLUP) 深度学习：基于多层神经网络的端到端学习方法\n深度学习: 卷积神经网络 (CNN)：ResNet、U-Net、YOLO系列、Mask R-CNN 循环神经网络 (RNN)：LSTM、GRU Transformer：ViT (Vision Transformer)、Cropformer 点云神经网络：PointNet++ 时序卷积网络：TCN\n计算机视觉 目标检测与实例分割: YOLO系列,Mask R-CNN 语义分割: U-Net,DeepLabV3+ 实例分割：Mask R-CNN 图像分类与特征提取: ResNet,ViT (Vision Transformer) 密度估计: 密度回归网络 时序建模: LSTM GRU TCN (时间卷积网络)\n多模态融合: 多任务学习 跨模态注意力机制 数据融合 / 特征融合 / 结果融合\n前沿AI： 视觉-语言大模型(VLM) 自监督学习 迁移学习 边缘计算\n文献二:MobilePheno3D | 小魔环重建植物点云世界坐标，从\u0026quot;无根之木\u0026quot;到\u0026quot;有据可依\u0026quot; 单目3D重建（如SfM, MVS）-\u0026gt; 尺度缺失 MobilePheno3D系统: 只需要一部普通智能手机,环绕植物拍摄一段视频,即可全自动完成从\u0026quot;原始视频\u0026quot; 到带\u0026quot;物理尺度,标准坐标系的三维点云\u0026quot;的转化\n文献三: 三维作物重建: 高光谱和多光谱方法综述 高光谱成像(HSI) 将HSI与诸如光探测和测距,红色,绿色,蓝色和深度相机等深度传感模式以及诸如摄影测量等计算重建技术相结合\n文献四: 以物体为中心的三维高斯溅射法在草莓植株重建和表型分析中的应用 2026-02-03 三维高斯溅射法 PCA主成分分析法 DBSCAN聚类\n文献五: 基于无人机的单目3D全景制图在果园果形完善中的应用 2026-02-05 GroundedSAM 2用于多目标跟踪和分割（multi-object tracking and segmentation，MOTS） 摄影测量结构从运动用于3D场景重建 DeepSDF，一种隐式神经表示，为了用神经网络完成遮挡水果的几何形状\n文献六: 基于三维点云获取和分析茉莉花植株表型 Kinect v2相机\nICP点云配准算法 \u0026amp; 随机抽样一致性粗配准算法（RANSAC）\n文献七: 基于多视角的水稻密植秧苗三维重建 2025-11-14\n多视图图像的重建方法依赖于图像配准和特征匹配,容易受到纹理相似和角度 异等问题的影响，导致匹配误差和关键结构信息的丢失。这可能导致局部缺陷和重建模型的精度降低\n本研究采用基于深度学习的特征提取和匹配方法，利用SuperPoint网络提高特征点检测和描述过程的鲁棒性，并引入LightGlue算法提高匹配的准确性和稳定性\n多视图图像采集 三维重建(含前端与后端处理) 方向与尺度校准 + 表型分析\n前端(特征提取与匹配): SuperPoint：提取图像特征点 LightGlue：特征匹配 估计基础矩阵（Fundamental Matrix） 构建图像连接图（哪些图像之间有匹配）\n后端(增量式重建): 选择最佳匹配图像对 三角化生成三维点 光束法平差（Bundle Adjustment）优化 逐步添加新图像，估计其相机位姿 离群点过滤\n最终输出: 稀疏重建点云 + 相机位姿\n文献八: 基于三维点云获取和分析茉莉花植株表型 2025-10-12 Kinect v2\n增强的随机抽样一致性粗配准算法(RANSAC) 和 一种基于目标对称的ICP精配算法\n文献九: 基于三维点云的油茶幼苗表型自动无损分型 2025-10-5 实现了循环分割策略，将幼苗点云分割成多个区域。在每次迭代中，都会对特定区域进行独立的形态分析和重新分类，从而增强传统聚类算法对局部形态特征变化的敏感性。通过在每次迭代中仅提取主茎或特定分支，保持了整体分割结果的完整性，从而实现从冠层分割到茎叶精确分割的过渡。\n文献十: 使用无人机系统的激光雷达点云和RGB图像检测开放棉铃 2025-9-6 RGB传感器 LiDAR传感器\n通过测量重叠图像的多个点,使用摄影测量将RGB图像转换为点云\n文献十一: 基于PointNeXt和Quickshift++的三维植物器官实例自动分割方法 2025-8-17\n目前大多数点云分割方法通常是针对特定作物设计,很难同时适用于显著结构差异的单子叶和双子叶作物\n点云分割\n本研究提出了一种基于PointNeXt和Quickshift++的具有更高泛化能力的两阶段单株植物器官实例分割方法。\n该数据集包括122个自采甘蔗、49个开放访问的玉米和77个开放访问的番茄的点云。对改进的PointNeXt模型进行训练，实现了茎和叶的语义分割。\n文献十二: 一种用于三维点云木质-叶片分离的无监督语义分割网络 2025-8-12\n传统方法: 大量标注点云数据来训练有监督的语义分割网络 -\u0026gt; 无监督语义分割网络,能够直接从三维点云中提取木质与叶片组分\n双节点注意力模块 点云特征卷积积分器\n文献十三: 3D植物分割：2D-to-3D分割方法 2025-7-1 2D-to-3D的重新投影方法，并与三种最先进的3D分割算法（Swin3D-s、Point Transformer v3和MinkUNet34C）进行了比较。2D到3D方法使用Mask2Former分割图像，将预测重新投影到点云，并使用多数投票算法合并多个预测\n文献十四: 基于点云多重聚类的玉米幼苗重建及空间分布分析 2025-6-18 一种基于地基激光三维点云扫描技术的玉米幼苗重建和空间分布分析方法 用高精度地面激光扫描(TLS)，从多个玉米幼苗地块收集3D点云数据 然后使用Trimble Realworks进行详细的预处理和分析 提出了基于玉米幼苗生长特性的回归经验公式。该公式有效地缓解了密集种植条件下叶片遮挡的挑战 本研究结合了DBSCAN和K-means聚类算法，有效地克服了点云数据中植物密集分布、叶片遮挡和噪声带来的挑战，可以准确识别植物的位置和分布，优化行和列间距计算，并实现植物缺失检测功能\n文献十五: 无人机高光谱-热-激光雷达融合在表型分析中的应用 2025-6-7 无人机高光谱-热-激光雷达融合检测和分类 随机森林\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/lab%E7%9B%B8%E5%85%B3%E6%96%87%E7%8C%AE%E9%98%85%E8%AF%BB/","summary":"\u003ch1 id=\"lab相关文献阅读\"\u003elab相关文献阅读\u003c/h1\u003e\n\u003cp\u003e阅读方法:\nimage.png\u003c/p\u003e\n\u003cp\u003e笔记归档\nimage.png\u003c/p\u003e\n\u003ch2 id=\"文献一-人工智能驱动的作物表型解析-进展与挑战\"\u003e文献一: 人工智能驱动的作物表型解析: 进展与挑战\u003c/h2\u003e\n\u003cp\u003e这是一篇综述文章，本身不提出新算法，而是系统梳理了AI在六个表型维度上的已有技术方案和应用进展：\nimage.png\u003c/p\u003e","title":"lab相关文献阅读"},{"content":"leetcode //to_string()//数字转字符串 //stoi()字符串转数字 //string(1,char)字符转数字//string构造函数\n1LL会在运算时把后面的临时数据扩容成long long类型，再在赋值给左边时转回int类型。\narray\u0026lt;int, 26\u0026gt; array：这是 C++ 标准库中的一个模板类，位于 头文件中。它是一个固定大小的容器，类似于传统的 C++ 数组，但提供了更多功能和安全性。 int：表示数组中存储的元素类型是整数。 26：表示数组的大小，即数组中有 26 个元素\nunordered_map 在C++中，std::unordered_map 是一个关联容器，用于存储键值对（key-value pairs）。如果你想检查 std::unordered_map 是否包含某个键（key），可以使用 find() 方法或 count() 方法。虽然 C++20 引入了 contains() 方法，但如果你使用的是 C++20 之前的版本，就需要用其他方式来实现。 使用 find() 方法 find() 方法会在容器中查找指定的键。如果找到了，返回一个指向该键值对的迭代器；如果没有找到，返回 end() 迭代器。\n2.使用 count() 方法 count() 方法会返回键在容器中出现的次数（对于 unordered_map，只能是 0 或 1）\n3.使用 C++20 的 contains() 方法 如果你使用的是 C++20 或更高版本，可以直接使用 contains() 方法。它会返回一个布尔值，表示容器是否包含指定的键。 // 注意不要直接 += cnt[sj-k]，如果 sj-k 不存在，会插入 sj-k\nstatic constexpr int directions[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; 这种定义通常用于二维平面的搜索问题，如迷宫搜索、棋盘游戏、网格路径搜索等。 通过遍历这个数组，可以方便地实现从当前位置向四个方向的移动。 constexpr 的作用： constexpr 表示这个数组是一个编译时常量，它的值在编译时就已经确定，不能在运行时修改。 这样可以提高代码的效率和安全性，同时避免在运行时动态分配内存。 constexpr 是 C++11 引入的一个非常强大的关键字，它的作用是声明一个“编译时常量表达式”，即在编译阶段就能确定其值的变量或函数。使用 constexpr 可以显著提高代码的效率和安全性，同时还能让代码更加清晰和易于维护。 具体用法略\nlambada表达式 这段代码是一个使用C++17标准的lambda表达式，并且使用了递归和参数化捕获的特性 [\u0026amp;]表示捕获当前作用域中的所有变量 this auto\u0026amp;\u0026amp; dfs：这是C++17中引入的参数化捕获特性。this auto\u0026amp;\u0026amp;表示捕获当前lambda对象的引用，允许lambda在递归调用时引用自身。 -\u0026gt; void：表示这个lambda表达式没有返回值 auto dfs = [\u0026amp;](this auto\u0026amp;\u0026amp; dfs, int i) -\u0026gt; void { if (i == nums.size()) { ans++; return; } }; Lambda 表达式是一种强大的工具，它结合了简洁性、匿名性、闭包特性以及与 STL 算法的无缝集成。它在现代 C++ 编程中被广泛应用，尤其是在需要定义简单函数、捕获上下文或实现递归时。\n简洁性 捕获上下文 Lambda 表达式可以捕获外部变量（通过 [\u0026amp;] 或 [=]），这使得它能够直接访问和修改外部作用域的变量，而无需通过参数传递。 匿名性 支持闭包 Lambda 表达式本质上是一种闭包，它可以捕获外部变量并将其封装起来。这使得 Lambda 表达式可以作为函数对象（functor）使用，而无需显式定义类。 5.支持递归 从 C++17 开始，Lambda 表达式可以通过 this auto\u0026amp;\u0026amp; 捕获自身，从而实现递归调用。这使得 Lambda 表达式可以用于复杂算法（如深度优先搜索、动态规划等） 6.与 STL 算法结合 Lambda 表达式与 C++ 标准库中的算法（如 std::sort、std::for_each、std::transform 等）结合得非常好，可以实现非常简洁的代码。 减少代码冗余 在某些场景下，使用 Lambda 表达式可以避免定义多个小函数，从而减少代码冗余。例如，在多线程编程中，Lambda 表达式可以直接捕获线程需要的上下文。 emplace_back contains 在C++中，std::unordered_set 是一个关联容器，用于存储唯一的元素。从C++20开始，std::unordered_set 提供了一个成员函数 contains，用于检查容器中是否包含某个元素。这是一个非常方便的函数，可以替代之前的 find 或 count 方法。\nstd::unordered_set::contains 的用法 函数原型 cpp复制\nbool contains(const key_type\u0026amp; key) const; 参数：key 是要检查的元素。 返回值：如果容器中包含该元素，则返回 true；否则返回 false。\nlower_bound(nums.begin(),nums.end(),target); lower_bound() 是 C++ 标准库中的一个函数，它在有序容器（如 std::vector、std::array、std::deque 等）中查找不小于给定值的第一个元素。这个函数使用二分查找算法，因此它的查找效率是 O(log n)。\nmemset 它定义在 （C++）或 \u0026lt;string.h\u0026gt;（C）头文件中 memset 用于在内存中填充指定的字节值. void* memset(void* dest, int value, size_t count); dest: 指向目标内存区域的指针。 该内存区域将被填充。 value: 要填充的字节值。 注意：value 是一个 int 类型，但它会被解释为一个 单字节值（即只使用其最低的 8 位）。因此，value 的有效范围是 0 到 255。 count: 要填充的字节数 指定从dest开始的内存区域中有多少字节需要被填充 返回值: 返回目标内存区域的指针,方便链式调用 是一个底层的内存操作函数\n常见用途: 初始化内存区域 清空内存\nsizeof sizeof 是 C++ 中的一个运算符，用于获取变量、类型或表达式的大小（以字节为单位）。它是一个编译时运算符，因此它的结果在编译时就已经确定，不会在运行时计算。\n函数的声明如下： Vector 1.assign assign 用于重新分配容器的内容，会清空当前容器，并用新的内容填充。\n2.resize resize 用于调整容器的大小，同时可以指定新元素的默认值。\n3.reverse reverse 并不是 std::vector 的成员函数，而是 C++ 标准库中的一个算法，用于反转容器中的元素顺序。\n4.reserve reserve 用于预留容器的内存空间，但不会改变容器的大小\nmax_element 在 C++ 中，max_element 是标准模板库（STL）中的一个算法函数，定义在头文件 中。它用于查找指定范围内的最大元素。 max_element 的第一个和第二个参数分别是迭代器，表示要查找的范围的开始和结束。 它返回一个迭代器，指向范围内的最大元素。\nreduce std::reduce 是C++17中引入的一个算法，它位于 头文件中。它类似于 std::accumulate，但提供了更好的并行化支持。 ：将一个范围内的元素通过指定的二元操作符进行归并（reduce） //默认使用加法\n在代码中，dfs 函数的参数 const string\u0026amp; s 使用了引用传参，这是出于性能优化和语义清晰的考虑。以下是详细解释： 性能优化：避免不必要的拷贝 在C++中，传递大型对象（如字符串、向量等）时，直接传递会触发拷贝构造函数，导致对象被复制一份。对于字符串 s，如果直接传递，每次递归调用都会复制整个字符串，这会带来不必要的开销，尤其是在字符串较长时。 例如： void dfs(string s, int i); // 直接传递 每次调用 dfs 时，都会复制整个字符串 s，这会导致时间复杂度和空间复杂度显著增加。\n而使用引用传参： void dfs(const string\u0026amp; s, int i); // 引用传参 这种方式不会复制字符串，而是直接传递原始字符串的引用。这样可以显著减少内存占用和拷贝时间，提高程序的运行效率。\n语义清晰：明确字符串不会被修改 在 dfs 函数中，字符串 s 是输入参数，且在递归过程中不需要修改它。使用 const string\u0026amp; 表示： 只读访问：const 修饰符表明 s 在函数内部不会被修改，这有助于代码的可读性和安全性。 明确意图：引用传参表明 s 是一个共享的输入数据，而不是每次递归调用时的独立副本。 这种写法清晰地表达了函数的语义：dfs 函数只是对输入字符串 s 进行读取操作，而不会修改它。\n对比：直接传递 vs 引用传递 假设字符串 s 的长度为 n，递归深度为 n： 直接传递：每次递归调用都会复制整个字符串，总的时间复杂度为 O(n^2)，空间复杂度也为 O(n^2)。 引用传递：每次递归调用只是传递一个引用，时间复杂度为 O(n)，空间复杂度为 O(n)（主要来自递归栈）。 因此，使用引用传参可以显著优化性能，尤其是在处理大字符串时。\n总结 在 dfs 函数中，使用 const string\u0026amp; s 的原因如下： 性能优化：避免不必要的字符串拷贝，减少时间和空间开销。 语义清晰：明确字符串是只读的输入参数，不会被修改。 最佳实践：在C++中，对于大型对象（如字符串、向量等），通常推荐使用引用传参，以提高效率。 这种写法是C++编程中的常见优化技巧，尤其适用于递归函数和深度优先搜索场景。\nperror函数 perror 是一个在 C 语言中常用的函数，用于打印错误信息。它属于标准库 \u0026lt;stdio.h\u0026gt;，主要用于将错误信息输出到标准错误输出（通常是屏幕）。 void perror(const char *s); perror 常用于处理系统调用或库函数失败时的错误。当这些函数失败时，它们通常会将错误码存储在 errno 中，而 perror 可以帮助开发者快速定位问题。\ntypdef函数 C语言允许用户使用 typedef 关键字来定义自己习惯的数据类型名称，来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等\n为基本数据类型定义新的类型名 为自定义数据类型（结构体、共用体和枚举类型）定义简洁的类型名称 typedef struct tagNode { char *pItem; pNode pNext; } *pNode; 其实问题并非在于 struct 定义的本身，大家应该都知道，C 语言是允许在结构中包含指向它自己的指针的，我们可以在建立链表等数据结构的实现上看到很多这类例子。那问题在哪里呢？其实，根本问题还是在于 typedef 的应用。\n在上面的代码中，新结构建立的过程中遇到了 pNext 声明，其类型是 pNode。这里要特别注意的是，pNode 表示的是该结构体的新别名。于是问题出现了，在结构体类型本身还没有建立完成的时候，编译器根本就不认识 pNode，因为这个结构体类型的新别名还不存在，所以自然就会报错。因此，我们要做一些适当的调整，比如将结构体中的 pNext 声明修改成如下方式： 解决办法 1.在struct前加typdef 2.将struct与typdef分开定义\n为数组定义简洁的类型名称 为指针定义简洁的名称 typedef 是用来定义一种类型的新别名的，它不同于宏，不是简单的字符串替换\nassert函数 assert 是 C 语言中一个非常有用的调试工具，用于在程序运行时检查条件是否为真。如果条件为假（即表达式的结果为 0），程序会终止运行，并打印一条错误信息，指出断言失败的位置。\nC语言和C++的最大数据结构和最小数据结构 头文件都问 \u0026lt;limits.h\u0026gt; C语言最大数据结构 INT_MAX C语言最小数据结构 INT_MIN C++语言最大数据结构 INT32_MAX C++语言最小数据结构 INT32_MIN\nuint64_t 是一种数据类型，通常用于表示无符号的64位整数\n定义 它是C语言和C++语言中定义的一种标准整数类型。 在C语言中，uint64_t 是通过头文件 \u0026lt;stdint.h\u0026gt; 定义的。 在C++语言中，uint64_t 是通过头文件 定义的。\n特点 无符号：uint64_t 是无符号整数类型，这意味着它不能表示负数，只能表示非负整数。 64位：uint64_t 占用64位（8字节）的存储空间，因此它可以表示的数值范围是从0到 264−1，即从0到18446744073709551615。 平台无关性：uint64_t 是一种固定宽度的整数类型，它的大小在所有支持它的平台上都是固定的，不会因平台的不同而改变。\n使用场景 大整数计算：当需要处理较大的整数时，uint64_t 是一个合适的选择。例如，在处理大文件的偏移量、大数组的索引或者大范围的计数器时，uint64_t 可以提供足够的存储空间。 跨平台开发：在跨平台的程序中，使用 uint64_t 可以确保整数的大小在不同的平台上保持一致，避免因平台差异导致的错误。 性能优化：在某些情况下，使用 uint64_t 可以提高程序的性能。例如，在进行位运算或整数运算时，64位整数的运算速度可能会比32位整数更快。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/leetcode/","summary":"\u003ch1 id=\"leetcode\"\u003eleetcode\u003c/h1\u003e\n\u003cp\u003e//to_string()//数字转字符串\n//stoi()字符串转数字\n//string(1,char)字符转数字//string构造函数\u003c/p\u003e\n\u003cp\u003e1LL会在运算时把后面的临时数据扩容成long long类型，再在赋值给左边时转回int类型。\u003c/p\u003e","title":"leetcode"},{"content":"Leetcode和蓝桥算法思路整理 数据结构 链表 双向链表\n循环链表\n链表栈\n堆链表\n栈\n堆\n队列\n二叉树 平衡二叉树\n二叉搜索树\n平衡二叉搜索树\n哈夫曼树\n堆\u0026mdash;优先队列\n最小堆\n最大堆\nB树\nB+树\n并查集 红黑树\n线段树\n邻接表\n邻接矩阵\n集合\n哈希表 有序集合\n字典树\n数组 二分查找 移除元素 双指针 快慢指针 双向指针 前后指针 区间和 滑动窗口 链表\n设计链表 反转链表 删除链表 删除链表倒数第N个结点 链表相交 两两交换链表的结点 约瑟夫问题 环形链表 哈希表\n字符串 栈与队列 逆波兰表达式求值 二叉树\ndfs 前序遍历 中序遍历 后序遍历 bfs 层序遍历 非递归遍历\u0026mdash;迭代法 二叉树 相同 对称 平衡 二叉树最近公共祖先 递归 汉诺塔 分治 回溯算法 暴力剪枝 子集问题 组合数 全排列 N皇后 贪心算法 启发式算法\n动态规划 从记忆化搜索到递推 背包 01背包 完全背包 多重背包 线性dp 树形dp 状态机dp 区间dp 滚动数组 编辑距离 图论 图的数据结构实现 dfs\nbfs\n岛屿问题 Dijastra算法 floyd算法 bellman_ford算法 Spfa算法 a*算法 最小生成树之kruskal算法 最小生成树之prim算法 并查集\n有向图\u0026mdash;无向图 冗余连接 拓扑排序 枚举右维护左 欧拉质数筛 循环数组 添加哨兵节点：在每个元素的位置列表前后各添加一个哨兵节点。前哨兵是最后一个出现位置减去数组长度，后哨兵是第一个出现位置加上数组长度。这样处理是为了将数组视为循环结构，方便处理边界情况。 for (auto\u0026amp; [_, p] : indices) { // 前后各加一个哨兵 int i0 = p[0]; p.insert(p.begin(), p.back() - n); p.push_back(i0 + n); } 由于 nums 是循环数组：\n在下标列表前面添加 4−n=−3，相当于认为在 −3 下标处也有一个 1。 在下标列表末尾添加 0+n=7，相当于认为在 7 下标处也有一个 1\n螺旋矩阵套路 有空结合hot100和代码随想录整理\n模拟 按层模拟\n单调栈套路 单调队列套路 入（元素进入队尾，同时维护队列单调性） 出（元素离开队首） 记录/维护答案（根据队首） 移除最左边的元素 移除最右边的元素 双端队列 在最右边插入元素 单调队列 从队首到队尾单调递减 单调性\n总结:及时去掉无用数据,保证双端队列有序 当前数字 \u0026gt;= 队尾,弹出队尾(和单调栈一样) 弹出队首不在窗口内的元素\n对角线遍历 3446 51N皇后\nBM算法 坏字符规则 1.模式串中没有出现文本串中的那个坏字符d，将模式串整体对齐到这个字符的后方，继续比较 2.模式串有对应的坏字符，而且有两个 让模式串中最靠右的对应字符与坏字符相对 好后缀规则 1.如果模式串中存在已经匹配成功的好后缀，则把目标串与好后缀对齐， 2.如果无法找到匹配好的后缀，找一个匹配的最长的前缀，让目标串与最长的前缀对齐\nKMP算法 辗转相除法\u0026mdash;gcd\u0026mdash;lcm 位运算 数学技巧 博弈论 模拟\n排序 1.插入排序 2.冒泡排序 3.选择排序 4.堆排序 5.希尔排序 6.归并排序 7.桶排序 8.基数排序 9.物理排序 10.拓扑排序\n例题 回文子串 中心扩展法 本题最容易想到的一种方法应该就是 中心扩散法。 中心扩散法怎么去找回文串？ 枚举所有回文中心并且尝试扩展 从每一个位置出发，向两边扩散即可。遇到不是回文的时候结束。举个例子，str=acdbbdaa 我们需要寻找从第一个 b（位置为 3）出发最长回文串为多少。怎么寻找？ 首先往左寻找与当期位置相同的字符，直到遇到不相等为止。 然后往右寻找与当期位置相同的字符，直到遇到不相等为止。 最后左右双向扩散，直到左和右不相等。 由于回文子串存在以单个字符和两个连续字符为中心的回文子串所以有两种中心扩展法 dp 状态定义dp[i][j]i到j的字符串是否是回文子串 遍历方向从下到上，从左到右 初始值全为false 递推公式如果dp[i] == dp[j]如果j - i \u0026lt;= 1说明为回文子串,如果dp[i+1][j-1]是回文子串所以dp[i][j]也是回文子串\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/leetcode%E5%92%8C%E8%93%9D%E6%A1%A5%E7%AE%97%E6%B3%95%E6%80%9D%E8%B7%AF%E6%95%B4%E7%90%86/","summary":"\u003ch1 id=\"leetcode和蓝桥算法思路整理\"\u003eLeetcode和蓝桥算法思路整理\u003c/h1\u003e\n\u003ch1 id=\"数据结构\"\u003e数据结构\u003c/h1\u003e\n\u003ch1 id=\"链表\"\u003e链表\u003c/h1\u003e\n\u003cp\u003e双向链表\u003c/p\u003e\n\u003cp\u003e循环链表\u003c/p\u003e\n\u003cp\u003e链表栈\u003c/p\u003e\n\u003cp\u003e堆链表\u003c/p\u003e\n\u003cp\u003e栈\u003c/p\u003e\n\u003cp\u003e堆\u003c/p\u003e\n\u003cp\u003e队列\u003c/p\u003e\n\u003ch1 id=\"二叉树\"\u003e二叉树\u003c/h1\u003e\n\u003cp\u003e平衡二叉树\u003c/p\u003e\n\u003cp\u003e二叉搜索树\u003c/p\u003e\n\u003cp\u003e平衡二叉搜索树\u003c/p\u003e\n\u003cp\u003e哈夫曼树\u003c/p\u003e\n\u003cp\u003e堆\u0026mdash;优先队列\u003c/p\u003e\n\u003cp\u003e最小堆\u003c/p\u003e\n\u003cp\u003e最大堆\u003c/p\u003e\n\u003cp\u003eB树\u003c/p\u003e\n\u003cp\u003eB+树\u003c/p\u003e\n\u003ch2 id=\"并查集\"\u003e并查集\u003c/h2\u003e\n\u003cp\u003e红黑树\u003c/p\u003e\n\u003cp\u003e线段树\u003c/p\u003e\n\u003cp\u003e邻接表\u003c/p\u003e\n\u003cp\u003e邻接矩阵\u003c/p\u003e\n\u003cp\u003e集合\u003c/p\u003e\n\u003ch1 id=\"哈希表\"\u003e哈希表\u003c/h1\u003e\n\u003cp\u003e有序集合\u003c/p\u003e\n\u003cp\u003e字典树\u003c/p\u003e\n\u003ch1 id=\"数组\"\u003e数组\u003c/h1\u003e\n\u003ch2 id=\"二分查找\"\u003e二分查找\u003c/h2\u003e\n\u003ch2 id=\"移除元素\"\u003e移除元素\u003c/h2\u003e\n\u003ch2 id=\"双指针\"\u003e双指针\u003c/h2\u003e\n\u003ch3 id=\"快慢指针\"\u003e快慢指针\u003c/h3\u003e\n\u003ch3 id=\"双向指针\"\u003e双向指针\u003c/h3\u003e\n\u003ch2 id=\"前后指针\"\u003e前后指针\u003c/h2\u003e\n\u003ch2 id=\"区间和\"\u003e区间和\u003c/h2\u003e\n\u003ch2 id=\"滑动窗口\"\u003e滑动窗口\u003c/h2\u003e\n\u003cp\u003e链表\u003c/p\u003e","title":"Leetcode和蓝桥算法思路整理"},{"content":"Linux操作系统 linux常用命令行 一.文件与目录操作 二.文本处理 三.权限管理 四.进程管理 五.系统信息与资源 六.网络相关 七.压缩与解压 八.快捷键与作业控制 九.包管理(不同发行版本) 十.实用技巧 管道 |: 将一个命令的输出作为另一个命令的输入 重定向:\n覆盖写入文件, \u0026raquo; 追加写入 2\u0026gt; 重定向错误输出, \u0026amp;\u0026gt; 重定向所有输出 通配符: * 匹配任一字符,? 匹配单个字符, [abc]匹配集合内字符 命令替换: \u0026amp;(command) 或反引号 \u0026lsquo;command\u0026rsquo; ，例如 echo \u0026ldquo;今天十\u0026amp;(date)\u0026rdquo;\n查看man手册 man ls\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/linux%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/","summary":"\u003ch1 id=\"linux操作系统\"\u003eLinux操作系统\u003c/h1\u003e\n\u003ch2 id=\"linux常用命令行\"\u003elinux常用命令行\u003c/h2\u003e\n\u003ch3 id=\"一文件与目录操作\"\u003e一.文件与目录操作\u003c/h3\u003e\n\u003ch3 id=\"二文本处理\"\u003e二.文本处理\u003c/h3\u003e\n\u003ch3 id=\"三权限管理\"\u003e三.权限管理\u003c/h3\u003e\n\u003ch3 id=\"四进程管理\"\u003e四.进程管理\u003c/h3\u003e\n\u003ch3 id=\"五系统信息与资源\"\u003e五.系统信息与资源\u003c/h3\u003e\n\u003ch3 id=\"六网络相关\"\u003e六.网络相关\u003c/h3\u003e\n\u003ch3 id=\"七压缩与解压\"\u003e七.压缩与解压\u003c/h3\u003e\n\u003ch3 id=\"八快捷键与作业控制\"\u003e八.快捷键与作业控制\u003c/h3\u003e\n\u003ch3 id=\"九包管理不同发行版本\"\u003e九.包管理(不同发行版本)\u003c/h3\u003e\n\u003ch3 id=\"十实用技巧\"\u003e十.实用技巧\u003c/h3\u003e\n\u003cp\u003e管道 |: 将一个命令的输出作为另一个命令的输入\n重定向:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e覆盖写入文件, \u0026raquo; 追加写入\n2\u0026gt; 重定向错误输出, \u0026amp;\u0026gt; 重定向所有输出\n通配符: * 匹配任一字符,? 匹配单个字符, [abc]匹配集合内字符\n命令替换: \u0026amp;(command) 或反引号 \u0026lsquo;command\u0026rsquo; ，例如 echo \u0026ldquo;今天十\u0026amp;(date)\u0026rdquo;\u003c/p\u003e","title":"Linux操作系统"},{"content":"MPC MPC的核心思想是： 预测：使用系统模型预测未来一段时间内的状态 优化：求解一个有限时域的最优控制问题 反馈：只实施第一个控制量，然后重新计算\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/mpc/","summary":"\u003ch1 id=\"mpc\"\u003eMPC\u003c/h1\u003e\n\u003cp\u003eMPC的核心思想是：\n预测：使用系统模型预测未来一段时间内的状态\n优化：求解一个有限时域的最优控制问题\n反馈：只实施第一个控制量，然后重新计算\u003c/p\u003e","title":"MPC"},{"content":"opencv知识库\u0026mdash;整理 基本概念 预处理 按我的话来说，所谓预处理，就是在图像还未真正进行识别处理前，对图像进行简易、全局的处理。这一块通常在产生图片时进行操作，不属于我们视觉识别的重点（但是预处理真的很重要）。这里仅简单介绍我们使用的两个预处理过程。 降低曝光度 通过相机，我们能够源源不断地获取到当前的画面，也就是一帧帧的图像。自瞄算法处理的对象，就是这每一张图像。 在实战中，由于环境光的干扰，如果直接对图片进行算法处理，会错误的提取多余的特征，导致算法的准确度和速度大大降低。由于灯条自己会发光，可以有效和将灯条与环境光很好的区分开来。为了便于分离灯条与其他光线，一般将曝光设置得很低，如下图为相机获取到的画面：（RoboMaster视觉教程（1）摄像头）\n常见关键字 数据类型\u0026mdash;类对象 cv::Point2f cv::Point2f 是一个二维点，包含两个浮点数（x 和 y），用于表示二维空间中的点。 加法和减法：可以直接对 Point2f 进行加法和减法操作。 计算距离：使用 cv::norm 函数计算两点之间的欧几里得距离。 例如： float distance = norm(point1 - point2);\ncv::Point3f cv::Point3f 是一个三维点，包含三个浮点数（x、y 和 z），用于表示三维空间中的点。 加法和减法：可以直接对 Point3f 进行加法和减法操作。 计算距离：使用 cv::norm 函数计算两点之间的欧几里得距离。 例如： float distance = norm(point1 - point2);\ncv::Point2i 和 cv::Point3i 除了浮点数版本的 Point2f 和 Point3f，OpenCV 还提供了整数版本的点类型： cv::Point2i：二维整数点，包含两个整数（x 和 y）。 cv::Point3i：三维整数点，包含三个整数（x、y 和 z）。 它们的用法与浮点数版本类似，只是数据类型为整数。\nMat赋初值（经常忘记） cv::Mat cameraMatrix = (cv::Mat_(3, 3) \u0026laquo; 1200.0, 0.0, 640.0, 0.0, 1200.0, 360.0, 0.0, 0.0, 1.0;\n函数 常见程序设计流程 PNP测距 robomaster装甲板识别代码讲解 需求:用方框框住装甲板 分析: 观察视频，我发现装甲板有两条竖直平行的灯条在装甲板的左右两端。所以大致分析，解决该问题的关键就是通过筛选出视频中的灯条，通过计算像素坐标，来绘制矩形框住装甲板。 视频中的灯条可以看成一个旋转矩形RotatedRect，为了方便后续对灯条几何特征（角度/中心点等）进行成对匹配，我将矩形的各个属性：宽，长，中心，角度，面积，封装成一个灯条类。\n将VideoCaputure初始化，用于读取视频中的每一帧\n为了方便对图像中灯条的提取 我首先对图像进行了预处理 1.由于视频中的灯条是红色的，选择红色通道进行二值化 2.将图片进行阈值处理，灯条是图片中亮度较高的区域，阈值220可以有效提取明亮区域 3.利用高斯模糊消除微小噪声点 4.对图像进行膨胀操作，连接断裂的灯条区域，将灯条扩大，方便提取，使用5x5矩形结构元素element增强膨胀元素\n接着检测灯条的轮廓 利用findContours函数读取预处理好的图像，获得存储灯条轮廓的点集 hierarchy表示输出各个轮廓的继承关系 RETR_TREE表示检测所有轮廓，并且建立所有的继承关系，CHAIN_APPROX_NONE表示把轮廓的所有点存储\n对轮廓进行处理与筛选 将面积太小，点数太少，长宽比太大也就是太过细长的灯条筛选掉\n使用椭圆拟合函数fitEllipse:返回旋转矩形\nopencv常用100个API 图像操作 cv2.imread(filename, flags) - 读取图像。 cv2.imwrite(filename, img) - 保存图像。 cv2.imshow(window_name, img) - 显示图像。 cv2.cvtColor(src, code) - 转换图像颜色空间。 cv2.resize(src, dsize, fx, fy, interpolation) - 缩放图像。 cv2.rotate(src, rotateCode) - 旋转图像。 cv2.flip(src, flipCode) - 翻转图像。 cv2.split(src) - 拆分通道。 cv2.merge(mv) - 合并通道。 cv2.copyMakeBorder(src, top, bottom, left, right, borderType, value) - 添加边框。 图像变换 cv2.warpAffine(src, M, dsize) - 仿射变换。 cv2.getAffineTransform(srcPoints, dstPoints) - 获取仿射变换矩阵。 cv2.warpPerspective(src, M, dsize) - 透视变换。 cv2.getPerspectiveTransform(srcPoints, dstPoints) - 获取透视变换矩阵。 cv2.remap(src, map1, map2, interpolation) - 重映射。 cv2.resize(src, dsize) - 调整大小。 cv2.getRotationMatrix2D(center, angle, scale) - 获取旋转矩阵。 cv2.invertAffineTransform(M) - 仿射矩阵求逆。 cv2.convertScaleAbs(src, alpha, beta) - 调整对比度和亮度。 cv2.normalize(src, dst, alpha, beta, norm_type) - 归一化。 绘图功能 cv2.line(img, pt1, pt2, color, thickness) - 画线。 cv2.rectangle(img, pt1, pt2, color, thickness) - 画矩形。 cv2.circle(img, center, radius, color, thickness) - 画圆。 cv2.ellipse(img, center, axes, angle, startAngle, endAngle, color, thickness) - 画椭圆。 cv2.polylines(img, pts, isClosed, color, thickness) - 画多边形。 cv2.fillPoly(img, pts, color) - 填充多边形。 cv2.putText(img, text, org, fontFace, fontScale, color, thickness) - 添加文本。 图像阈值 cv2.threshold(src, thresh, maxval, type) - 图像二值化。 cv2.adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C) - 自适应阈值。 cv2.inRange(src, lowerb, upperb) - 范围筛选。 图像平滑与滤波 cv2.blur(src, ksize) - 均值滤波。 cv2.GaussianBlur(src, ksize, sigmaX) - 高斯滤波。 cv2.medianBlur(src, ksize) - 中值滤波。 cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace) - 双边滤波。 cv2.filter2D(src, ddepth, kernel) - 任意核卷积。 边缘检测与轮廓 cv2.Canny(image, threshold1, threshold2) - 边缘检测。 cv2.findContours(image, mode, method) - 查找轮廓。 cv2.drawContours(image, contours, contourIdx, color, thickness) - 绘制轮廓。 cv2.arcLength(contour, closed) - 计算轮廓周长。 cv2.contourArea(contour) - 计算轮廓面积。 cv2.approxPolyDP(curve, epsilon, closed) - 多边形逼近。 cv2.boundingRect(points) - 计算矩形边界。 cv2.minEnclosingCircle(points) - 最小包围圆。 cv2.convexHull(points) - 凸包。 cv2.isContourConvex(contour) - 判断是否为凸形。 形态学操作 cv2.erode(src, kernel, iterations) - 腐蚀。 cv2.dilate(src, kernel, iterations) - 膨胀。 cv2.morphologyEx(src, op, kernel) - 形态学操作（开闭运算等）。 cv2.getStructuringElement(shape, ksize) - 获取结构元素。 图像直方图 cv2.calcHist(images, channels, mask, histSize, ranges) - 计算直方图。 cv2.equalizeHist(src) - 直方图均衡化。 cv2.createCLAHE(clipLimit, tileGridSize) - 自适应直方图均衡化。 特征检测与描述 cv2.SIFT_create() - SIFT特征检测。 cv2.ORB_create() - ORB特征检测。 cv2.FastFeatureDetector_create() - FAST特征检测。 cv2.MSER_create() - MSER特征检测。 cv2.BRISK_create() - BRISK特征检测。 cv2.SimpleBlobDetector_create() - 简单Blob检测。 cv2.goodFeaturesToTrack(src, maxCorners, qualityLevel, minDistance) - 检测角点。 特征匹配 cv2.BFMatcher(normType) - 暴力匹配器。 cv2.FlannBasedMatcher() - FLANN匹配器。 cv2.drawMatches(img1, kp1, img2, kp2, matches, outImg) - 绘制匹配结果。 视频操作 cv2.VideoCapture(source) - 打开视频文件或摄像头。 cv2.VideoWriter(filename, fourcc, fps, frameSize) - 保存视频。 cap.read() - 读取视频帧。 cap.isOpened() - 检查视频是否打开。 cap.release() - 释放视频资源。 几何变换与数学操作 cv2.addWeighted(src1, alpha, src2, beta, gamma) - 图像加权。 cv2.bitwise_and(src1, src2) - 按位与。 cv2.bitwise_or(src1, src2) - 按位或。 cv2.bitwise_not(src) - 按位取反。 cv2.bitwise_xor(src1, src2) - 按位异或。 cv2.minMaxLoc(src) - 最值定位。 cv2.reduce(src, dim, rtype) - 归约操作。 模板匹配 cv2.matchTemplate(image, templ, method) - 模板匹配。 cv2.minMaxLoc(result) - 获取匹配位置。 深度学习相关 cv2.dnn.readNetFromCaffe(protoTxt, model) - 读取Caffe模型。 cv2.dnn.readNetFromTensorflow(model, config) - 读取TensorFlow模型。 cv2.dnn.readNetFromONNX(model) - 读取ONNX模型。 cv2.dnn.blobFromImage(image, scalefactor, size, mean, swapRB, crop) - 图像转换为深度学习输入。 基本工具 cv2.waitKey(delay) - 等待键盘输入。 cv2.destroyAllWindows() - 销毁所有窗口。 cv2.getTickCount() - 获取时间戳。 cv2.getTickFrequency() - 获取时间频率。 cv2.setMouseCallback(window_name, callback) - 设置鼠标回调。 深入功能 cv2.calcOpticalFlowFarneback(prev, next, flow, pyrScale, levels, winsize, iterations, polyN, polySigma, flags) - 光流计算。 cv2.cornerHarris(src, blockSize, ksize, k) - Harris角点检测。 cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria) - 亚像素角点优化。 自定义与扩展 cv2.getTrackbarPos(trackbarname, winname) - 获取滑块值。 cv2.createTrackbar(trackbarname, winname, value, count, onChange) - 创建滑块。 cv2.fillConvexPoly(img, points, color) - 填充凸多边形。 cv2.fillPoly(img, pts, color) - 填充多边形。 图像与视频编码解码 cv2.imencode(ext, img) - 编码图像。 cv2.imdecode(buf, flags) - 解码图像。 cv2.VideoWriter_fourcc(c1, c2, c3, c4) - 获取视频编码器。 其他实用功能 cv2.phase(x, y) - 计算幅角。 cv2.cartToPolar(x, y) - 笛卡尔坐标到极坐标转换。 cv2.polarToCart(magnitude, angle) - 极坐标到笛卡尔坐标转换。 cv2.kmeans(data, K, bestLabels, criteria, attempts, flags) - KMeans 聚类。 cv2.connectedComponents(image) - 连通域分析。\n补充API 1.glob void cv::glob(cv::String pattern, std::vectorcv::String\u0026amp; result, bool recursive = false); pattern：文件路径模式，支持通配符（如 * 和 ?）。例如，\u0026quot;./data/*.jpg\u0026quot; 表示获取 data 文件夹下所有扩展名为 .jpg 的文件。 result：用于存储匹配路径的容器，类型为 std::vectorcv::String。 recursive：是否递归搜索子文件夹。默认为 false，表示仅搜索当前目录。\n2.Size cv::Size 指定图像尺寸 cv::Size 是一个简单的结构体，包含两个成员变量：width 和 height。它通常用于指定图像的尺寸，例如在 cv::resize 函数中 cv::Size(-1, -1) 的含义 在 OpenCV 的 cv::resize 函数中，cv::Size 的参数用于指定目标图像的大小。如果将 cv::Size 的宽度和高度都设置为 -1，这通常意味着目标图像的大小是通过缩放比例（fx 和 fy）来计算的，而不是直接指定目标尺寸。 例如: cv::resize(src, dst, cv::Size(-1, -1), fx, fy, interpolation);\n3.findChessboardCorners 在 OpenCV 中，cv::findChessboardCorners 是一个用于检测棋盘格角点的函数，广泛应用于相机标定和三维重建等任务中 bool cv::findChessboardCorners( InputArray image, // 输入图像，必须是8位灰度或彩色图像 Size patternSize, // 棋盘格的尺寸，表示内部角点的数量（例如8x6的棋盘格，patternSize为(7,5)） OutputArray corners, // 检测到的角点坐标 int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE // 操作标志 ); image：输入图像，必须是8位灰度或彩色图像。 patternSize：棋盘格的尺寸，表示内部角点的数量（例如8x6的棋盘格，patternSize为(7,5)）。 corners：检测到的角点坐标，存储为 std::vectorcv::Point2f。 flags：操作标志，可以组合以下值： CALIB_CB_ADAPTIVE_THRESH：使用自适应阈值。 CALIB_CB_NORMALIZE_IMAGE：对图像进行归一化。 CALIB_CB_FAST_CHECK：快速检查图像是否包含棋盘格，如果未找到则提前退出\n4.cornerSubPix//用于优化角点坐标//亚像素级精确定位 void cv::cornerSubPix( InputArray image, // 输入图像，通常是单通道灰度图像。 InputOutputArray corners, // 输入角点的初始坐标（例如由 findChessboardCorners 或 goodFeaturesToTrack 检测到的角点），优化后的角点坐标将直接输出到此参数[^23^][^24^]。 Size winSize, // 搜索窗口的一半尺寸。例如，Size(5, 5) 表示搜索窗口大小为 (52+1)×(52+1)=11×11[^21^][^23^]。 Size zeroZone, // 死区的一半尺寸，用于避免搜索区域的中心部分。值为 (-1, -1) 表示没有死区[^21^][^23^]。 TermCriteria criteria // 迭代过程的终止条件，可以是最大迭代次数或精度阈值[^23^]。 ); 参数说明 image：输入图像，必须是单通道灰度图像。 corners：角点的初始坐标（输入）和优化后的坐标（输出）。初始坐标通常由 findChessboardCorners 或 goodFeaturesToTrack 提供。 winSize：搜索窗口的一半尺寸，决定了角点优化时考虑的区域范围。 zeroZone：死区的一半尺寸，用于避免搜索区域的中心部分。值为 (-1, -1) 表示没有死区。 criteria：迭代终止条件，通常设置为 TermCriteria::EPS + TermCriteria::MAX_ITER，表示达到指定精度或最大迭代次数时停止。\n5.calibrateCamera 在 OpenCV 中，cv::calibrateCamera 是一个用于相机标定的函数，通过一系列棋盘格图像来计算相机的内参和外参，以及畸变系数。以下是关于 cv::calibrateCamera 的使用方法和一个完整的示例代码。 double cv::calibrateCamera( InputArrayOfArrays objectPoints, // 三维空间中的点坐标（通常是棋盘格的角点） InputArrayOfArrays imagePoints, // 图像中的对应点坐标（棋盘格角点的图像坐标） Size imageSize, // 图像的尺寸 InputOutputArray cameraMatrix, // 输出的相机内参矩阵 InputOutputArray distCoeffs, // 输出的畸变系数 OutputArrayOfArrays rvecs, // 每幅图像的旋转向量 OutputArrayOfArrays tvecs, // 每幅图像的平移向量 int flags = 0, // 标定选项 TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON) // 迭代终止条件 ); 参数说明 objectPoints：三维空间中的点坐标，通常是棋盘格的角点。对于每幅图像，这些点的坐标是相同的。 imagePoints：检测到的棋盘格角点的图像坐标。 imageSize：图像的尺寸（宽和高）。 cameraMatrix：相机内参矩阵，输出结果。 distCoeffs：畸变系数，输出结果。 rvecs：每幅图像的旋转向量，表示相机的旋转。 tvecs：每幅图像的平移向量，表示相机的平移。 flags：标定选项，例如 CALIB_FIX_PRINCIPAL_POINT、CALIB_FIX_ASPECT_RATIO 等。 criteria：迭代优化的终止条件。\n6.find4QuardCornerSubpix//用于优化角点坐标 cv::find4QuadCornerSubpix 是一个用于精确定位四边形四个角点亚像素位置的函数。它通常在已经通过其他方法（如 cv::goodFeaturesToTrack 或 cv::cornerHarris）粗略定位角点之后使用，以提高角点检测的准确性 bool cv::find4QuadCornerSubpix( InputArray img, InputOutputArray corners, Size region_size ); img：输入图像，应为灰度图，类型为 8-bit 或浮点型的单通道图像。 corners：输入/输出参数。初始的角点坐标作为输入，优化后的角点坐标作为输出。这是一个包含 (x, y) 坐标的浮点数向量。 region_size：搜索窗口大小。对于每个角点，将在这个区域内的子窗口中寻找更准确的位置。\n7.drawChessboardCorners cv::drawChessboardCorners 是 OpenCV 中用于在图像上绘制检测到的棋盘格角点的函数。它通常用于相机标定过程中，帮助可视化检测到的角点，以验证角点检测的准确性 void cv::drawChessboardCorners( InputOutputArray image, // 目标图像，必须是8位彩色图像 Size patternSize, // 棋盘格的内角点数，格式为 cv::Size(columns, rows) InputArray corners, // 检测到的角点数组，由 findChessboardCorners 函数输出 bool patternWasFound // 指示是否成功检测到完整的棋盘格，应传入 findChessboardCorners 的返回值 ); image：目标图像，必须是8位彩色图像。 patternSize：棋盘格的内角点数，格式为 cv::Size(columns, rows)，其中 columns 和 rows 分别是棋盘格的列数和行数（注意是内角点数，而非方格数）。 corners：检测到的角点数组，由 findChessboardCorners 函数输出。 patternWasFound：指示是否成功检测到完整的棋盘格，应传入 findChessboardCorners 的返回值。\n8.getAffineTransform 计算仿射变换矩阵 仿射变换是一种二维坐标到二维坐标的线性变换，它保持了直线和平行性，但可以改变形状和大小。 需要三个点 cv::Mat cv::getAffineTransform(const Point2f src[], const Point2f dst[]); cv::Mat cv::getAffineTransform(InputArray src, InputArray dst); src：源图像中三角形顶点的坐标，需要提供三个点。 dst：目标图像中相应三角形顶点的坐标，与 src 中的点一一对应。 返回值：一个 2×3 的浮点型矩阵，表示从 src 到 dst 的仿射变换矩阵 使用步骤 计算仿射变换矩阵：通过 getAffineTransform 函数计算出源图像和目标图像之间的仿射变换矩阵。 应用仿射变换：使用 warpAffine 函数将计算出的仿射变换矩阵应用到图像上，实现图像的仿射变换。\n9.getPerspectiveTransform 计算透视变换矩阵 透视变换是一种更复杂的变换，它将一个平面映射到另一个平面，可以改变直线的平行性，从而实现更复杂的几何变换。 透视变换矩阵是一个 3×3 的矩阵，形式如下： 需要四个点 OpenCV 中用于计算透视变换矩阵的函数。它通过给定的四个点对（源点和目标点）来计算从源图像到目标图像的透视变换矩阵。这个矩阵可以用于将图像从一个平面映射到另一个平面，实现更复杂的几何变换，例如将矩形图像映射为平行四边形或梯形。 cv::Mat cv::getPerspectiveTransform(const Point2f src[], const Point2f dst[]); cv::Mat cv::getPerspectiveTransform(InputArray src, InputArray dst);\nsrc：源图像中的四个点的坐标，这些点必须是不共线的。 dst：目标图像中对应的四个点的坐标，与 src 中的点一一对应。 返回值：一个 3×3 的浮点型矩阵，表示从 src 到 dst 的透视变换矩阵。 使用步骤 定义源点和目标点：选择源图像和目标图像中的四个点。 计算透视变换矩阵：使用 getPerspectiveTransform 函数计算透视变换矩阵。 应用透视变换：使用 warpPerspective 函数将计算出的透视变换矩阵应用到图像上，实现图像的透视变换。\n10.warpAffine 是 OpenCV 中用于应用仿射变换的函数。它通过一个 2×3 的仿射变换矩阵，将输入图像映射到输出图像。这种变换可以实现平移、旋转、缩放和剪切等操作。 仿射变换（Affine Transformation）： 仿射变换是一种二维坐标到二维坐标的线性变换，保持直线和平行性，但可以改变形状和大小。 可以实现平移、旋转、缩放和剪切等操作。 矩阵维度：2×3 的矩阵。 warpAffine： 适用于简单的几何变换，如平移、旋转、缩放和剪切。 常用于局部变换，例如将图像的一部分旋转或缩放后嵌入到另一幅图像中。 示例：将图像的一部分旋转 45 度并缩放 0.5 倍。 需要三个点对 三个点不能共线 void cv::warpAffine( InputArray src, // 输入图像 OutputArray dst, // 输出图像 InputArray M, // 2×3 的仿射变换矩阵 Size dsize, // 输出图像的大小 int flags = INTER_LINEAR,// 插值方法 int borderMode = BORDER_CONSTANT, // 边界填充模式 const Scalar\u0026amp; borderValue = Scalar() // 边界填充值 ); 参数说明 src：输入图像，可以是任意通道数的单通道或多通道图像。 dst：输出图像，其大小由 dsize 参数决定，类型与输入图像相同。 M：2×3 的仿射变换矩阵，通常由 getAffineTransform 或其他方式计算得到。 dsize：输出图像的大小，格式为 cv::Size(width, height)。 flags：插值方法，常用的有： INTER_LINEAR：双线性插值（默认值）。 INTER_NEAREST：最近邻插值。 INTER_CUBIC：双三次插值。 borderMode：边界填充模式，常用的有： BORDER_CONSTANT：用指定的 borderValue 填充边界。 BORDER_REPLICATE：复制边缘像素。 BORDER_REFLECT：反射边缘像素。 borderValue：当 borderMode 为 BORDER_CONSTANT 时，用于填充边界的值，默认为黑色（0）。\n11.warpPerspective 适用于更复杂的几何变换，如透视校正、文档扫描、3D 效果等。 常用于将图像从一个平面映射到另一个平面，例如将倾斜的文档图像校正为正面视图。 示例：将矩形图像变换为梯形或平行四边形。 需要四个点对 这四个点不能共线,且不能共面 透视变换（Perspective Transformation）： 透视变换是一种更复杂的变换，可以改变直线的平行性，从而实现更复杂的几何变换，例如将矩形变换为梯形或平行四边形。 适用于模拟三维空间中的视角变化，例如文档扫描、透视校正等。 矩阵维度：3×3 的矩阵。 warpPerspective 是 OpenCV 中用于应用透视变换的函数。它通过一个 3×3 的透视变换矩阵，将输入图像映射到输出图像。这种变换可以实现图像的倾斜、扭曲或视角变化，通常用于模拟三维空间中的视角变化 void cv::warpPerspective( InputArray src, // 输入图像 OutputArray dst, // 输出图像 InputArray M, // 3×3 的透视变换矩阵 Size dsize, // 输出图像的大小 int flags = INTER_LINEAR,// 插值方法 int borderMode = BORDER_CONSTANT, // 边界填充模式 const Scalar\u0026amp; borderValue = Scalar() // 边界填充值 ); 参数说明 src：输入图像，可以是任意通道数的单通道或多通道图像。 dst：输出图像，其大小由 dsize 参数决定，类型与输入图像相同。 M：3×3 的透视变换矩阵，通常通过 getPerspectiveTransform 函数计算得到。 dsize：输出图像的大小，格式为 cv::Size(width, height)。 flags：插值方法，常用的有： INTER_LINEAR：双线性插值（默认值）。 INTER_NEAREST：最近邻插值。 INTER_CUBIC：双三次插值。 borderMode：边界填充模式，常用的有： BORDER_CONSTANT：用指定的 borderValue 填充边界。 BORDER_REPLICATE：复制边缘像素。 borderValue：当 borderMode 为 BORDER_CONSTANT 时，用于填充边界的值，默认为黑色（0）。 计算透视变换矩阵：使用 getPerspectiveTransform 函数计算 3×3 的透视变换矩阵。 调用 warpPerspective：将计算得到的矩阵应用到输入图像上，生成输出图像。\n12.norm cv::norm 函数用于计算矩阵或向量的范数。它是一个非常有用的工具，可以用于测量向量的长度、矩阵的大小，或者计算两个矩阵之间的差异 double cv::norm(InputArray src1, int normType = NORM_L2, InputArray mask = noArray()); src1：输入矩阵或向量。 normType：范数类型，默认为 NORM_L2，即欧几里得范数。其他常见范数类型包括： NORM_L1：L1 范数，即绝对值之和。 NORM_L2：L2 范数，即欧几里得范数。 NORM_INF：无穷范数，即最大绝对值。 NORM_HAMMING：汉明距离。 mask：可选参数，用于指定计算范数时的掩码。\n12.solvePnP bool solvePnP(InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess = false, int flags = SOLVEPNP_ITERATIVE); objectPoints：目标物体的 3D 点坐标，类型为 vector 或 Mat。 imagePoints：目标物体在图像中的 2D 投影点坐标，类型为 vector 或 Mat。 cameraMatrix：相机的内参矩阵，类型为 Mat。 distCoeffs：相机的畸变系数，类型为 Mat。 rvec：输出的旋转向量（Rodrigues 表示法），类型为 Mat。 tvec：输出的平移向量，类型为 Mat。 useExtrinsicGuess：是否使用初始的外参估计值。如果为 true，则 rvec 和 tvec 会被用作初始猜测值。 flags：指定 PnP 算法的类型，常见的选项包括： SOLVEPNP_ITERATIVE：使用非线性优化方法（默认）。 SOLVEPNP_P3P：使用 P3P 算法（至少需要 3 个点）。 SOLVEPNP_UPNP：使用 UPnP 算法。 SOLVEPNP_DLS：使用 DLS 算法（至少需要 2 个点）。 SOLVEPNP_AP3P：使用 AP3P 算法。\n13.norm 在 OpenCV 的 C/C++ 接口中，norm 函数用于计算数组的范数，它在图像处理和计算机视觉中非常有用，例如用于计算图像之间的差异或特征向量的长度。以下是关于 norm 函数的详细说明： double cv::norm(InputArray src1, InputArray src2 = noArray(), int normType = NORM_L2, InputArray mask = noArray()); 参数说明： src1：输入数组（图像或矩阵）。 src2：可选的第二个输入数组，如果提供，则计算两个数组之间的范数。 normType：范数类型，默认为 NORM_L2，可选值包括： NORM_INF：无穷范数，即最大绝对值。 NORM_L1：L1 范数，即绝对值之和。 NORM_L2：L2 范数，即欧几里得范数。 NORM_L2SQR：L2 范数的平方。 NORM_HAMMING：汉明范数，适用于二进制数据。 NORM_HAMMING2：汉明范数的变体。 NORM_MINMAX：归一化范数。 mask：可选的掩码，用于指定哪些元素参与计算。\nAPI区别和异同 minAreaRect 和 fitEllipse 是 OpenCV 中用于轮廓拟合的两种不同方法，它们的主要区别如下： 功能定义 minAreaRect：用于计算能够完全包围输入点集（通常是轮廓）的最小面积矩形。这个矩形可以是旋转的，因此能够更好地适应不规则形状。 fitEllipse：用于拟合一个椭圆，使其最优地匹配输入的点集（通常是轮廓）。这个椭圆能够更好地描述轮廓的形状特征。\n返回值 minAreaRect：返回一个 RotatedRect 对象，包含以下信息： 矩形的中心点坐标。 矩形的宽度和高度。 矩形的旋转角度。 fitEllipse：返回一个椭圆的参数，包括： 椭圆的中心点坐标。 椭圆的主轴和次轴长度。 椭圆的旋转角度。\n使用场景 minAreaRect：适用于需要最小面积矩形来包围轮廓的场景，例如目标检测、物体定位等。它能够提供更紧凑的包围形状。 fitEllipse：适用于需要描述轮廓的形状特征或进行椭圆拟合的场景，例如在医学图像分析中拟合细胞形状。\n输入要求 minAreaRect：输入为一组二维点集（通常是轮廓的点集）。 fitEllipse：输入同样为一组二维点集，但要求点集的数量至少为 5 个。\n输出形状 minAreaRect：输出是一个旋转矩形，可以通过 cv2.boxPoints 获取其四个顶点。 fitEllipse：输出是一个椭圆，可以通过 cv2.ellipse 绘制。\n总结 如果目标是找到最小面积的矩形包围框，选择 minAreaRect。 如果目标是拟合一个椭圆来描述轮廓的形状，选择 fitEllipse。\n希望这些信息能帮助你理解两者的区别。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/opencv%E7%9F%A5%E8%AF%86%E5%BA%93-%E6%95%B4%E7%90%86/","summary":"\u003ch1 id=\"opencv知识库整理\"\u003eopencv知识库\u0026mdash;整理\u003c/h1\u003e\n\u003ch2 id=\"基本概念\"\u003e基本概念\u003c/h2\u003e\n\u003cp\u003e预处理\n按我的话来说，所谓预处理，就是在图像还未真正进行识别处理前，对图像进行简易、全局的处理。这一块通常在产生图片时进行操作，不属于我们视觉识别的重点（但是预处理真的很重要）。这里仅简单介绍我们使用的两个预处理过程。\n降低曝光度\n通过相机，我们能够源源不断地获取到当前的画面，也就是一帧帧的图像。自瞄算法处理的对象，就是这每一张图像。\n在实战中，由于环境光的干扰，如果直接对图片进行算法处理，会错误的提取多余的特征，导致算法的准确度和速度大大降低。由于灯条自己会发光，可以有效和将灯条与环境光很好的区分开来。为了便于分离灯条与其他光线，一般将曝光设置得很低，如下图为相机获取到的画面：（RoboMaster视觉教程（1）摄像头）\u003c/p\u003e","title":"opencv知识库---整理"},{"content":"OpenVINO OpenVINO 是做什么的？ OpenVINO 是英特尔（Intel）开源的一套工具包，专门用于加速 AI 模型的推理（Inference）。 它的核心职责： 它不负责训练模型（那是 PyTorch 或 TensorFlow 的工作），它只负责使用模型。它能把你训练好的模型进行底层指令集的优化，让它在 Intel 的硬件（比如 CPU、集成显卡 iGPU、或者最新的 NPU）上跑得飞快。 为什么用 C++： 虽然 Python 开发快，但在工业落地（如自动驾驶、安防监控、医疗设备）中，通常要求极低的延迟和极高的性能，这时候 C++ + OpenVINO 就是黄金搭档。\n头文件\nov::Core core;\n1.ov::Core - 初始化核心 这是程序的起点,用于管理当前设备上的所有硬件资源\n// 初始化核心对象 ov::Core core;\n2.read_model() - 读取模型 把你的 AI 模型（通常是 .xml 格式，也原生支持 .onnx 格式）加载到内存中。\n// 返回的是一个指向模型图的智能指针 std::shared_ptrov::Model model = core.read_model(\u0026ldquo;your_model.xml\u0026rdquo;);\n3.compile_model() - 编译模型 将模型针对特定硬件（如 \u0026ldquo;CPU\u0026rdquo; 或 \u0026ldquo;GPU\u0026rdquo;）进行底层优化和编译。\n// 将模型编译到 CPU 上 ov::CompiledModel compiled_model = core.compile_model(model, \u0026ldquo;CPU\u0026rdquo;);\n4.create_infer_request - 创建推理请求\nov::InferRequest infer_request = compiled_model.create_infer_request();\n5.infer() - 处理数据并执行\n// 1. 获取输入 Tensor (张量) ov::Tensor input_tensor = infer_request.get_input_tensor(); // \u0026hellip; 在这里用你的图像数据填充 input_tensor \u0026hellip;\n// 2. 执行推理 (同步调用) infer_request.infer();\n// 3. 获取输出结果 ov::Tensor output_tensor = infer_request.get_output_tensor(); // \u0026hellip; 获取 output_tensor 的数据并进行后处理 \u0026hellip;\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/openvino/","summary":"\u003ch1 id=\"openvino\"\u003eOpenVINO\u003c/h1\u003e\n\u003cp\u003eOpenVINO 是做什么的？\nOpenVINO 是英特尔（Intel）开源的一套工具包，专门用于加速 AI 模型的推理（Inference）。\n它的核心职责： 它不负责训练模型（那是 PyTorch 或 TensorFlow 的工作），它只负责使用模型。它能把你训练好的模型进行底层指令集的优化，让它在 Intel 的硬件（比如 CPU、集成显卡 iGPU、或者最新的 NPU）上跑得飞快。\n为什么用 C++： 虽然 Python 开发快，但在工业落地（如自动驾驶、安防监控、医疗设备）中，通常要求极低的延迟和极高的性能，这时候 C++ + OpenVINO 就是黄金搭档。\u003c/p\u003e","title":"OpenVINO"},{"content":"Prompt Engineering 提示工程 1.什么是提示词工程 当前是AGI时代 AGI Artificial General Intelligence 通用人工智能\n我们在提示工程上的优势 我们懂原理,所以知道 为什么有的指令有效,有的指令无效 为什么同样的指令有时有效,有时无效 怎么提升指令的有效的概率\n我们懂编程: 知道哪些问题用提示词工程解决更高效,哪些用传统编程 能完成和业务系统的对接,把效能发挥到极致\n使用Prompt的两种目的 1.获得具体问题的具体结果 2.固化一套Prompt到程序,成为系统功能的一部分\n后者更难,掌握后能轻松搞定前者 后者是我们的独特优势\nPrompt调优 找到好的prompt是个持续迭代的过程,需要不断调优\n高质量prompt核心要点: 具体,丰富,少歧义\nimage.png\n2.Prompt的典型构成 模板建议1 -\u0026gt; 来自吴恩达: 角色: 给AI定义一个最匹配任务的角色, 比如: 你是一位软件工程师 你是一位小学老师\nimage.png\n证明: image.png\n放在开头的影响最大 放在结尾的影响也比较大 放在中间的影响最小\n大模型对prompt开头和结尾的内容更敏感\n先定义角色,其实就是在开头把问题收窄,减少二义性\n指示: 对任务进行描述\n上下文: 给出与任务相关的其他背景信息(尤其在多轮交互中)\n例子: 必要时给出举例,学术中称为 one shot learning , few-shot learnig context learning; 实践证明对输出正确性有很大帮助\n输入: 任务的输入信息;在提示词中明确地标识出输入\n输出: 输出的格式描述,以便后续模块自动解析模型的输出\n模板建议2 -\u0026gt; 来自字节: 浅层的提示词工程 1.明确目标: 首先确定你希望大模型或者机器人为你做什么是写一个营销方案还是智能回答 2.优化提示: 我们可以给大模型更加具体的提示,让大模型知道自己是干啥的 3.评估并迭代: 通过不同的提示词来问同样的问题,看大模型是如何反馈的,如果不满意的话可以修改提示词,然后再次尝试,不要怕麻烦,直到它可以反馈出让我们满意的答案或者反馈出更适合应用场景的答案\n深层的提示词工程 -\u0026gt; 属于开发层面\n提示词的两个核心技术: N-gram 通过统计计算N个词共同出现的概率来预测下一个词\n深度学习 深度学习模型是由多层神经网络组成的，可以自动从数据中去学习这些特征 让模型不断地自我学习不断进步不断成长\n3.如何编写提示词 gemini自己生成的自己的Prompt模板:\n当前 Lab 与任务： [例如：Lab 3 Page tables，任务 2：A kernel page table per process] 我的目标： [例如：我想在 allocproc() 中为每个进程分配一个独立的内核页表，并拷贝全局内核页表的内容。] 遇到的问题 (Expected vs. Actual)： [例如：编译通过了，但在运行 make qemu 时，系统在启动 init 进程时发生了 Panic。] 报错日志 / 终端输出： Plaintext [粘贴你的 QEMU panic 信息、usertrap/kerneltrap 报错、或者 make grade 的失败提示。包含 scause, sepc, stval 等寄存器信息非常关键！] 相关的代码片段： C // [在这里贴上你修改过的代码，最好带上函数名和一点上下文]struct trapframe *trapframe = p-\u0026gt;trapframe; // \u0026hellip;你的代码\u0026hellip; 我的思考与尝试 (非常重要)： [例如：我怀疑是因为在 scheduler() 切换页表时，satp 寄存器没有正确刷新，但我加了 sfence.vma 还是不行。] 我的诉求： [例如：请帮我指出代码逻辑的漏洞 / 请给我一个 debug 的思路 / 请解释一下 walk() 函数的第三个参数是什么意思。] 4.进阶技巧 思维链 image.png\n自洽性 (Self-Consistency) image.png\n思维树(Tree-of-thought,ToT) image.png\nimage.png\nimage.png\n核心思路: 把输入的自然语言对话,转成结构化的表示 从结构化的表示,生成策略 把策略转化成自然语言输出\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/prompt-engineering-%E6%8F%90%E7%A4%BA%E5%B7%A5%E7%A8%8B/","summary":"\u003ch1 id=\"prompt-engineering-提示工程\"\u003ePrompt Engineering 提示工程\u003c/h1\u003e\n\u003ch1 id=\"1什么是提示词工程\"\u003e1.什么是提示词工程\u003c/h1\u003e\n\u003cp\u003e当前是AGI时代\nAGI Artificial General Intelligence 通用人工智能\u003c/p\u003e\n\u003ch2 id=\"我们在提示工程上的优势\"\u003e我们在提示工程上的优势\u003c/h2\u003e\n\u003cp\u003e我们懂原理,所以知道\n为什么有的指令有效,有的指令无效\n为什么同样的指令有时有效,有时无效\n怎么提升指令的有效的概率\u003c/p\u003e","title":"Prompt Engineering 提示工程"},{"content":"rm第四阶段学习\u0026mdash;自瞄 image.png\nimage.png\nimage.png\nopencv书 第八章检测兴趣点 这个概念的原理是，从图像中选取某些特征点并对 图像进行局部分析（即提取局部特征），而非观察整幅图像（即提取全局特征）。 视觉不变性 目前略\n检测图像中的角点 检测角点的经典方法：Harris 特征检测 基本函数:cv::cornerHarris image.png\nTHRESH_BINARY_INV用黑色表示被检测的角点 THRESH_BINARY用白色表示被检测的角点 image.png\n检测Harris角点需要两个步骤： 1.计算每个像素的Harris值 2.然后用指定的阈值获得特征点\n8.3 快速检测特征 这种算子专门用来快速检测兴趣点——只需对比几个像素，就可以判断它是 否为关键点。 略\n8.4 尺度不变特征的检测 不仅在任何尺度下拍摄的物体都能检测到一致的关键点，而且每个被检测的特征点都对应一个尺 度因子。\n8.5 多尺度FAST特征的检测 BRISK（Binary Robust Invariant Scalable Keypoints，二元稳健恒定可扩展关键点）检测法，它 基于上一节介绍的FAST特征检测法。本节还将讨论另一种检测方法ORB（\n第九章描述和匹配兴趣点 鲁棒性（Robustness）是指算法在面对各种变化和干扰时仍能保持稳定性能的能力。这些变化可能包括光照条件的变化、视角的变化、噪声的干扰、目标的遮挡等\n局部模板匹配 matchTemplate() 描述并匹配局部强度值模式\n用二值描述子匹配关键点\n第十章估算图像之间的投影关系\n计算图像之间的基础矩阵\n用RANSAC（随机抽样一致性）算法匹配图像\n基地任务 基地第一次网课 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/rm%E7%AC%AC%E5%9B%9B%E9%98%B6%E6%AE%B5%E5%AD%A6%E4%B9%A0-%E8%87%AA%E7%9E%84/","summary":"\u003ch1 id=\"rm第四阶段学习自瞄\"\u003erm第四阶段学习\u0026mdash;自瞄\u003c/h1\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003ch1 id=\"opencv书\"\u003eopencv书\u003c/h1\u003e\n\u003ch2 id=\"第八章检测兴趣点\"\u003e第八章检测兴趣点\u003c/h2\u003e\n\u003cp\u003e这个概念的原理是，从图像中选取某些特征点并对 图像进行局部分析（即提取局部特征），而非观察整幅图像（即提取全局特征）。\n视觉不变性 目前略\u003c/p\u003e","title":"rm第四阶段学习---自瞄"},{"content":"ROS1 学习网址 https://bluesnie.github.io/Learning-notes/ROS2/%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%AD%A6%E7%AF%87/%E7%AC%AC7%E7%AB%A0-ROS2%E8%BF%90%E5%8A%A8%E5%AD%A6/001-TF2%E4%BB%8B%E7%BB%8D%E5%8F%8ARVIZ-TF%E7%BB%84%E4%BB%B6.html\n创建工作空间功能包流程 遇到的bug: 最开始改CMakeLists.txt文件时改错文件了,而且居然改的时候有使用超级用户权限,应该改的文件是功能包的CMakeLists.txt文件，而不是工作空间的CMakeLists.txt文件,这个文件是自动生成的,后来意识到这个问题,但是改的时候一直没有意识到权限不够,我以为我改了实际上我没有更改成功,最后知道新开了一个工作空间重新编译才发现这个报了一摸一样的错误,才发现,之前一直以为是路径错误.\n1.创建工作空间目录结构\nmkdir -p ~/catkin_ws/src 2.初始化工作空间\ncd ~/catkin_ws/src catkin_init_workspace 3.编译工作空间\ncd ~/catkin_ws catkin_make 4.创建功能包 （1.）进入src目录:\ncd ~/catkin_ws/src (2.) 创建功能包\ncatkin_create_pkg my_first_pkg roscpp rospy std_msgs\n5.编写C++节点代码\ncd ~/catkin_ws/src/my_first_pkg touch src/hello_ros.cpp 6.示例代码\n#include \u0026ldquo;ros/ros.h\u0026rdquo; // ROS 核心头文件\nint main(int argc, char **argv) { // 初始化 ROS 节点，节点名必须唯一 ros::init(argc, argv, \u0026ldquo;hello_ros_node\u0026rdquo;); // 创建节点句柄，用于管理节点资源 ros::NodeHandle nh; // 在控制台输出信息 ROS_INFO(\u0026ldquo;Hello, ROS World!\u0026rdquo;); return 0; } 7.配置编译规则 1.找到add_executable部分,添加你的可执行文件生成规则\nadd_executable(hello_ros_node src/hello_ros.cpp) hello_ros_node: 可执行文件的目标名称(在CMake内部使用) -\u0026gt; 不是源文件名(这个名字是自己定义的) src/hello_ros.cpp: 源文件路径(相对于CMakeLists.txt)\n2.找到target_link_libraries部分,为你的可执行文件链接必要的catkin库:\ntarget_link_libraries(hello_ros_node ${catkin_LIBRARIES}) hello_ros_node: 之前add_executable定义的目标名称 \u0026amp;{catkin_LIBRARIES}: CMake变量,包含所有需要的ROS库\n8.编译并运行节点 (1)回到工作空间根目录重新编译\ncd ~/catkin_ws catkin_make (2)在运行节点前,必须启动ROS核心,打开一个新的终端标签页或窗口\nroscore (3)配置当前终端的环境变量,使其能够找到你工作空间编译好的功能包和节点\nsource ~/catkin_ws/devel/setup.bash (4)运行节点\nrosrun my_first_pkg hello_ros_node\n9.补充,设置环境变量(持久化)\necho \u0026ldquo;source ~/catkin_ws/devel/setup.bash\u0026rdquo; \u0026raquo; ~/.bashrc source ~/.bashrc # 使更改立即生效\n2.初始化 创建功能包: 1.进入工作空间的src目录:\ncd ~/catkin_ws/src 2.使用 catkin_create_pkg 命令创建功能包\ncatkin_create_pkg \u0026lt;your_package_name\u0026gt; [depend1] [depend2] [depend3] \u0026hellip; 3.整理功能包结构:\ncd my_robot mkdir scripts src launch msg srv 4.回到工作空间根目录编译功能包\ncd ~/catkin_ws catkin_make 刷新当前终端的环境变量\nsource devel/setup.bash 5.验证功能包创建成功 可以使用 rospack 命令来查找或查看你的包信息：\nrospack find my_robot # 查找功能包路径 roscd my_robot # 切换到功能包目录 rosls my_robot # 列出功能包内容\nros::spin()和ros::spinOnce()的区别 ros::spin() 阻塞行为: 阻塞,调用后不返回,持续处理回调 循环机制: 内部自带无限循环,直到节点关闭 使用场景: 节点仅需要处理回调函数,无其他周期性任务 使用场景: 节点仅需要处理回调函数,无其他周期性任务 控制灵活性: 低,无法在循环内添加其他任务 后续代码: 其后的代码不会被执行(除非节点关闭)\nros::spinceOnce() 阻塞行为：非阻塞,调用后立即返回,只处理一次当前回调队列 循环机制: 需外部循环(while(ros::ok()))配合 使用场景: 节点需同时处理回调函数和其他周期性任务 控制灵活性: 高,可在调用前后执行自定义代码 后续代码: 其后的代码会继续执行\nroscore的作用 roscore是ROS系统的总指挥部和信息中心,它提供了节点之间通信所必须的基础设施 ROS Master: 作用: 管理所有节点的注册,发现和连接,充当节点通信的\u0026quot;名称服务\u0026quot;和\u0026quot;协调员\u0026quot; Parameter Server: 一个全局的键值存储服务器,用于节点间共享配置参数和初始设置 rosout节点: 收集所有节点的日志输出,并提供统一的日志记录和查看接口\nroscore为ROS的分布式计算提供了核心的通信基础设施.在ROS1中,任何节点在启动时都需要向ROS Master注册,并通过它来发现其他节点,从而建立点对点的直接通信,没有roscore,节点就像失去了通讯录和电话号码,节点就像失去了通讯录和电话号码的员工，无法找到彼此并进行协作\nros::spinceOnce()和ros::spin()的区别 ros::spin() 阻塞式: 进入一个无限循环,持续处理ROS回调 不会返回: 一旦调用,程序会已知停留在这个函数中,知道节点被关闭 适用于: 只需要处理回调而不需要执行其他任务的简单节点\nros::spinceOnce() 非阻塞式: 处理当前时刻所有挂起的回调,然后立即返回 继续执行：调用后程序会继续执行后面的代码 适用于: 需要在循环中同时处理回到和执行其他任务的节点\nROS的并发模型 ros::spin()不会阻塞其他节点: 每个节点是独立进程： ROS节点是独立的进程,每个节点有自己的执行线程 ros的通信是异步的: 节点间的消息传递通过ROS Master和话题/服务实现,不依赖对方节点的执行装填\nros::spin()只阻塞当前节点: 它只影响调用它的节点,不会影响系统中其他节点的运行\nROS工具类: 1.ros::Rate ros::Rate是ROS中一个非常重要的实用工具类,用于控制循环(尤其是主循环)的执行频率(速率). 它的目的是让循环以尽可能接近的固定频率运行\n没有速率控制: 1.循环会尽可能快的运行: 这可能导致CPU占用率过高,浪费计算资源 2.执行时间不稳定: 每次循环执行的任务耗时可能不同,导致循环的实际时间波动很大 3.难以与其他节点同步: 如果你的节点需要以特定频率发布数据或执行控制,没有速率控制就无法保证这个频率\nros::Rate loop_rate(10); // 期望以10Hz(每秒10次)运行\nwhile(ros::ok()) { loop_rate.sleep(); // 关键! 在这里睡眠以控制速率\n2.时间相关 ros::Time： 表示一个时间点，它由秒和纳秒两部分组成,通常用于时间戳\nros::Duration: 表示一个时间段或事件间隔, 它也由秒和纳秒组成\nros::Timer: 用于创建周期性的定时回调，类似于ros::Rate但更面向事件，你不需要自己写while循环,只需设置一个回调函数和周期,ROS就会定期调用它 与ros:: Rate的区别: Timer在后台线程中触发回调,不会阻塞你的主线程.而ros::Rate通常用于阻塞主循环以控制其频率\nvoid timerCallback(const ros::TimerEvent\u0026amp; event) { // 这个函数会被定期调用 ROS_INFO(\u0026ldquo;Timer called. Expected period: %.4f, Actual last period: %.4f\u0026rdquo;, event.current_expected.toSec(), event.last_duration.toSec()); }\nint main(int argc, char** argv) { ros::init(argc, argv, \u0026ldquo;timer_node\u0026rdquo;); ros::NodeHandle nh;\n// 创建一个定时器，每 1.0 秒调用一次 timerCallback 函数 ros::Timer timer = nh.createTimer(ros::Duration(1.0), timerCallback);\n// 进入自旋，等待回调 ros::spin(); return 0; }\n3.tf tf2_ros::TransformListener 用于侦听和缓存由 TransformBroadcaster发布的坐标变换(tf)数据\ntf2_ros::TransformBroadcaster 用于发布坐标变换关系\ntf2::doTransform()和tfBuffer.transform() 用于执行实际的坐标变换,将一个点或姿态从一个坐标系转换到另一个坐标系\n4.日志与诊断 ROS_INFO(),ROS_WARN(),ROS_ERROR(),ROS_FATAL()（通常用于导致节点无法继续运行的致命错误） 这些是宏,而不是类,但极其常用,它们提供了不同级别的日志输出功能，替代了std::cout 输出会带有时间戳,节点名,消息级别等信息,非常利于调试\n5.节点句柄与参数服务 ros::NodeHandle 这是你与ROS系统交互的主要入口,几乎所有操作(创建发布者/订阅者,获取/设置参数,创建定时器等)都需要通过它 它提供了访问参数服务器的方法\n6.多线程处理(Spinners) 用于处理回调函数,特别是在有多个订阅或服务时 ros::MultiThreadedSpinner spinner(4); 使用一个线程池来处理回调,可以并行处理多个回调。如果一个回调函数执行时间很长,它不会阻塞其他回调的执行\nros::AsyncSpinner 类似于MultiThreadedSpinner ，但更灵活,可以随时启动和通知\nros::NodeHandle nh; ros::NodeHandle private_nh(\u0026quot;~\u0026quot;); // 私有节点句柄，用于访问私有参数\nstd::string default_name = \u0026ldquo;robot\u0026rdquo;; // 从参数服务器获取参数，如果不存在则使用默认值 nh.paramstd::string(\u0026ldquo;robot_name\u0026rdquo;, robot_name, default_name); // 设置参数 nh.setParam(\u0026ldquo;control_frequency\u0026rdquo;, 30.0);\n录制与回放数据步骤 1.启动节点并列出话题\n//1.启动核心 roscore //2.启动各种节点 \u0026hellip; //3.source工作空间 source catkin_ws/devel/setup.bash //4.列出话题 rostopic list -v //或者 rostopic list 2.创建文件并开始录制\n//1.创建文件 mkdir bagfiles //2.进入文件路径 cd bagfiles //3.录制当前发布的所有话题数据 rosbag record -a 3.检查并回放bag文件\n//在bag包所在目录下执行命令 //查看bag包 rosbag info //回放bag文件以再现系统运行过程 rosbag play 4.录制数据子集\nrosbag record -o subset /turtle/command_velocity /turtlel/pose\n5.录制指定话题\nrosbag record \u0026lt;话题1\u0026gt; \u0026lt;话题2\u0026gt; \u0026hellip; \u0026lt;话题N\u0026gt;\n录制单个话题 rosbag record /turtle1/cmd_vel\n录制多个话题 rosbag record /turtle1/cmd_vel /turtle1/pose /odom\n录制不同类型的话题 rosbag record /scan /tf /camera/rgb/image_raw\nrosbag的局限性 在前述部分中你可能已经注意到了turtle的路径可能并没有完全映射到原先通过键盘控制时产生的路径\u0026mdash;-整体形状应该是差不多的,但没有完全一样,造成该问题的原因是turtlesim的移动路径对系统定时精度的变化非常敏感。rosbag受制于其本身的性能无法完全复制录制时的系统运行行为,rosplay也一样,对于turtlesim这样的节点,当 处理消息的过程中系统定时发生极小变化时也会使其行为发生微妙变化,用户不应该期望能够完美的模仿系统行为\n超级终端 安装:\nsudo apt install terminator\n快捷键 0ad8c37504e42b9e10a9eab9b806f3b.jpg\n快捷键冲突bug解决 ibus-setup 把占用的快捷键给去除掉\nROSTF广播 一,核心概念： 1.坐标系(Frame): .以字符串 .构成树状结构 2.变换: .包含平移(x,y,z) 和旋转(四元数qx,qy,qz,qw) .描述子坐标系相对于父坐标系的位置姿态\n二,广播变换: 1.静态变换广播\n#include \u0026lt;tf2_ros/static_transform_broadcaster.h\u0026gt;\ngeometry_msgs::TransformStamped static_transform; static_transform.header.stamp = ros::Time::now(); static_transform.header.frame_id = \u0026ldquo;parent_frame\u0026rdquo;; static_transform.child_frame_id = \u0026ldquo;child_frame\u0026rdquo;; static_transform.transform.translation.x = 0.5; static_transform.transform.translation.y = 0.0; static_transform.transform.translation.z = 0.2; static_transform.transform.rotation = tf::createQuaternionMsgFromYaw(M_PI/4); // 45度\ntf2_ros::StaticTransformBroadcaster broadcaster; broadcaster.sendTransform(static_transform); 动态变换广播\n#include \u0026lt;tf2_ros/transform_broadcaster.h\u0026gt;\ntf2_ros::TransformBroadcaster broadcaster; geometry_msgs::TransformStamped transform;\nvoid publishTransform() { transform.header.stamp = ros::Time::now(); transform.header.frame_id = \u0026ldquo;odom\u0026rdquo;; transform.child_frame_id = \u0026ldquo;base_link\u0026rdquo;; transform.transform.translation.x = x_position; transform.transform.rotation = tf::createQuaternionMsgFromRollPitchYaw(roll, pitch, yaw); broadcaster.sendTransform(transform); }\n三.监听变换 1.查询最新变换\n#include \u0026lt;tf2_ros/transform_listener.h\u0026gt; #include \u0026lt;geometry_msgs/TransformStamped\u0026gt;\ntf2_ros::Buffer tfBuffer; tf2_ros::TransformListener tfListener(tfBuffer);\ngeometry_msgs::TransformStamped transform; try { transform = tfBuffer.lookupTransform(\u0026ldquo;target_frame\u0026rdquo;, \u0026ldquo;source_frame\u0026rdquo;, ros::Time(0)); // 获取最新可用变换 double x = transform.transform.translation.x; // 使用变换数据\u0026hellip; } catch (tf2::TransformException \u0026amp;ex) { ROS_ERROR(\u0026quot;%s\u0026quot;, ex.what()); } 2.指定时间戳查询\n// 查询特定时刻的变换（需保证时间戳在tf树范围内） transform = tfBuffer.lookupTransform(\u0026ldquo;map\u0026rdquo;, \u0026ldquo;base_link\u0026rdquo;, ros::Time(ros::Time::now() - ros::Duration(1.0)));\n3.等待可用变换\n// 阻塞等待直到变换可用（超时2秒） tfBuffer.canTransform(\u0026ldquo;map\u0026rdquo;, \u0026ldquo;base_link\u0026rdquo;, ros::Time::now(), ros::Duration(2.0));\n四,常用工具函数 1.欧拉角 \u0026lt;-\u0026gt; 四元数转换\n#include \u0026lt;tf2/LinearMath/Quaternion.h\u0026gt; #include \u0026lt;tf2_geometry_msgs/tf2_geometry_msgs.h\u0026gt;\n// 欧拉角转四元数 tf2::Quaternion quat; quat.setRPY(roll, pitch, yaw); geometry_msgs::Quaternion quat_msg = tf2::toMsg(quat);\n// 四元数转欧拉角 tf2::fromMsg(quat_msg, quat); tf2::Matrix3x3(quat).getRPY(roll, pitch, yaw); 2.坐标点变换\ngeometry_msgs::PointStamped point_in, point_out; point_in.header.frame_id = \u0026ldquo;camera\u0026rdquo;; point_in.point.x = 1.0;\ntfBuffer.transform(point_in, point_out, \u0026ldquo;map\u0026rdquo;); // 将点从camera系转换到map系\n五,调试命令 1.查看坐标系树\nrosrun tf2_tools view_frames.py # 生成frames.pdf 2.检查特定变换\nrosrun tf tf_echo source_frame target_frame 3.可视化坐标系(RViz)\n六,常见问题 1.时间戳不匹配 错误: Lookup would require extrapolation into the past 解决: 确保广播时间戳 \u0026gt;= 监听器查询的时间 2.坐标系未连接 错误: Could not find a connection between \u0026lsquo;map\u0026rsquo; and \u0026lsquo;base_link\u0026rsquo; 3.使用tf2替代旧版tf\nTF时间戳 一，时间戳的核心作用: 1.时空一致性 每个变换,必须携带时间戳,表示该位姿数据的有效时刻 2.避免时间外推错误\n二，时间戳的四种使用场景 查询最新变换 (零延迟)\n// 优先使用：获取最新发布的变换（即使时间戳稍旧） transform = tfBuffer.lookupTransform(\u0026ldquo;target\u0026rdquo;, \u0026ldquo;source\u0026rdquo;, ros::Time(0));\n指定历史时刻变换\n// 查询特定时刻的位姿（如匹配传感器数据时间戳） ros::Time sensor_stamp = scan_msg-\u0026gt;header.stamp; transform = tfBuffer.lookupTransform(\u0026ldquo;map\u0026rdquo;, \u0026ldquo;base_link\u0026rdquo;, sensor_stamp);\n3.时间旅行查询\n// 组合：查询从source_time到target_time的完整变换链 transform = tfBuffer.lookupTransform(\u0026ldquo;target_frame\u0026rdquo;, ros::Time::now(), \u0026ldquo;source_frame\u0026rdquo;, sensor_stamp, \u0026ldquo;fixed_frame\u0026rdquo;); // 固定参考系\n4.等待未来变换\n// 阻塞等待直到指定变换可用（超时5秒） if (tfBuffer.canTransform(\u0026ldquo;map\u0026rdquo;, \u0026ldquo;base_link\u0026rdquo;, ros::Time::now(), ros::Duration(5.0))) { transform = tfBuffer.lookupTransform(\u0026ldquo;map\u0026rdquo;, \u0026ldquo;base_link\u0026rdquo;, ros::Time::now()); }\n清理ROS日志 1.检查当前日志大小:\nrosclean check\n2.清理所有ROS日志:\nrosclean purge 3.执行后系统会提醒你确认是否删除,输入y并按回车即可\n4.ros日志文件的作用: 记录节点运行状态 辅助调试俄故障排除\n可以使用less,cat,tail-f 或grep 等linux命令直接查看或过滤日志文件\n查看实时日志流:\nrostopic echo /rosout\n检查ROS日志配置 rosrun rqt_console rqt_console\nROS常用组件 演示小乌龟 roslaunch turtle_tf2 turtle_tf2_demo_cpp.launch 或者(上面的是cpp节点写的,下面的是python节点写的) roslaunch turtle_tf2 turtle_tf2_demo.launch\nTF坐标变换(TransForm Frame) 概述 TF坐标变换: 实现不同类别的坐标系之间的转换 (因为不可以将物体相对于该传感器的方位信息,等价于机器人系统或机器人其他组件的方位信息) -\u0026gt; 所以需要坐标系之间的变换\n概念: tf: TransForm Frame 坐标系: ROS中是通过坐标系开标定物体的,确切的将是通过右手坐标系来标定的 作用: 在ROS中用于实现不同坐标系之间的点或向量的转换\n说明: 在ROS中坐标变换最初对应的是tf,不过在hyfro版本开始,tf被废弃,迁移到tf2,后者更为简洁高效,tf2对应的常用功能包有: tf2_geometry_msgs 可以将ROS消息转换为tf2消息 tf2: 封装了坐标系变换常用消息 四元数 \u0026lt;-\u0026gt; 欧拉角 tf2_ros: 为tf2提供了rscpp和rospy绑定,封装了坐标变换常用的API\n坐标msg消息 在坐标转换实现中常用的msg: geometry_msgs/TransformStamped 和 geometry_msgs/PointStamped 前者用于传输坐标系相关位置信息,后者用于传输某个坐标系内坐标点的信息,在坐标变换中,频繁的需要使用坐标系的相对位置以及坐标点的信息\n1.geometry_msgs/TransformStamped 命令行键入: rosmsg info geometry_msgs/TransformStamped\nstd_msgs/Header header # 头信息 uint32 seq #|\u0026ndash; 序列号 time stamp #|\u0026ndash; 时间戳 string frame_id #|\u0026ndash; 坐标 ID string child_frame_id #子坐标系的id geometry_msgs/Transform transform #坐标信息 geometry_msgs/Vector3 translation #偏移量 float64 x #|\u0026ndash;x 方向的偏移量 float64 y #|\u0026ndash;y 方向的偏移量 float64 z #|\u0026ndash;z 方向的偏移量 geometry_msgs/Quaternion rotation # 四元数 float64 x float64 y float64 z float64 w 四元数用于表示坐标的相对姿态 2.geometry_msgs/PointStamped 命令行键入: rosmsg info geometry_msgs/PointStamped\nstd_msgs/Header header # 头信息 uint32 seq #| \u0026ndash; 序列号 time stamp #| \u0026ndash; 时间戳 string frame_id #| \u0026ndash; 所属坐标系的id geometry_msgs/Point point # 点坐标 (x,y,z坐标) float64 x float64 y float64 z\n静态坐标变换 指两个坐标系之间的相对位置是固定的 实现分析: 1.坐标系相对关系,可以通过发布方发布 2.订阅方,订阅到发布的坐标系相对关系,再传入坐标点信息(可以写死),然后借助于tf实现坐标变换,并将结果输出\n实现流程: 1.新建功能包,添加依赖 (创建项目功能包依赖于 tf2 tf2_ros tf2_geometry_msgs roscpp rospy std_msgs geometry_msgs) 2.编写发布方实现 3.编写订阅方实现 4.执行并查看结果\n发布方:\n/* 静态坐标变换发布方: 发布关于laser 坐标系的位置信息\n实现流程: 1.包含头文件 2.初始化ROS节点 3.创建静态坐标转换广播器 4.创建坐标信息 5.广播器发布坐标信息 6.spin() */\n#include \u0026ldquo;ros/ros.h\u0026rdquo; // ros核心功能 #include \u0026ldquo;tf2_ros/static_transform_broadcaster.h\u0026rdquo; // 静态变换广播器 #include \u0026ldquo;geometry_msgs/TransformStamped.h\u0026rdquo; // 变换消息结构 #include \u0026ldquo;tf2/LinearMath/Quaternion.h\u0026rdquo;// 四元数操作\nint main(int argc, char *argv[]) { setlocale(LC_ALL,\u0026quot;\u0026quot;); // 支持中文字符 ros::init(argc,argv,\u0026ldquo;static_pub\u0026rdquo;); // 初始化节点,名为\u0026quot;static_pub\u0026quot; ros::NodeHandle nh; // 创建节点句柄\ntf2_ros::StaticTransformBroadcaster pub; // 创建静态变换广播器 // 设置变换消息 geometry_msgs::TransformStamped tfs; tfs.header.stamp = ros::Time::now(); // 当前时间戳 tfs.header.frame_id = \u0026quot;base_link\u0026quot;; // 父坐标系 tfs.child_frame_id = \u0026quot;laser\u0026quot;; // 子坐标系 tfs.transform.translation.x = 0.2; // x轴偏移0.2米 tfs.transform.translation.y = 0.0; // y轴无偏移 tfs.transform.translation.z = 0.5; // z轴偏移0.5米 // 设置旋转 tf2::Quaternion qtn; qtn.setRPY(0,0,0); tfs.transform.rotation.x = qtn.getX(); tfs.transform.rotation.y = qtn.getY(); tfs.transform.rotation.z = qtn.getZ(); tfs.transform.rotation.w = qtn.getW(); pub.sendTransform(tfs); // 发布静态变换 ros::spin(); // 保持节点运行 return 0; }\n订阅方\n#include \u0026ldquo;ros/ros.h\u0026rdquo; #include \u0026ldquo;tf2_ros/transform_listener.h\u0026rdquo; #include \u0026ldquo;tf2_ros/buffer.h\u0026rdquo; #include \u0026ldquo;geometry_msgs/PointStamped.h\u0026rdquo; #include \u0026ldquo;tf2_geometry_msgs/tf2_geometry_msgs.h\u0026rdquo;\nint main(int argc, char* argv[]) {\nsetlocale(LC_ALL,\u0026quot;\u0026quot;); ros::init(argc,argv,\u0026quot;static_sub\u0026quot;); ros::NodeHandle nh; tf2_ros::Buffer buffer; tf2_ros::TransformListener listener(buffer); geometry_msgs::PointStamped ps; ps.header.frame_id = \u0026quot;laser\u0026quot;; ps.header.stamp = ros::Time::now(); ps.point.x = 2.0; ps.point.y = 3.0; ps.point.z = 5.0; ros::Duration(2).sleep(); // 方案1: 在调用转换函数前,执行休眠 ros::Rate rate(10); while (ros::ok) { geometry_msgs::PointStamped ps_out; /* 调用了该 buffer 的转换函数 transform 参数1: 被转换的坐标点 参数2: 目标坐标系 返回值；输出的坐标点 PS1: 调用时必须包含头文件 tf2_geometry_msgs/tf2_geometry_msgs.h PS2: 运行时存在的问题: 抛出异常 base_link 不存在 原因: 订阅数据是一个耗时操作,可能再调用 transform 转换函数,坐标系的 相对关系还没有订阅到,因此出现异常 解决: 方案1: 在调用转换函数前,执行休眠 方案2: 进行异常处理(放一个异常捕获)，捕获后随便做一些处理再让他进入循环 -\u0026gt; 直 到不抛出异常了就正常完成操作 */ try // 方案2: 执行异常捕获 { ps_out = buffer.transform(ps,\u0026quot;base_link\u0026quot;); ROS_INFO(\u0026quot;转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s\u0026quot;, ps_out.point.x, ps_out.point.y, ps_out.point.z, ps_out.header.frame_id.c_str() ); } catch (const std::exception\u0026amp; e) { // std::cerr \u0026lt;\u0026lt; e.what(ps.what() \u0026lt;\u0026lt; '\\n'); ROS_INFO(\u0026quot;异常消息: %s\u0026quot;,e.what()); } rate.sleep(); ros::spinOnce(); } return 0; }\npackage.xml - 功能包清单文件 作用: 1.定义功能包名称,版本,描述等元数据 2.声明依赖关系 3.指定维护者信息和许可证 创建方式: catkin_create_pkg命令时会自动生成模板 ， 开发者需要手动编辑填充具体内容， 位于功能包的根目录\nCMakeLists.txt - 构建系统文件(功能包目录下) 作用: 定义如何编译源代码 指定可执行文件和库的生成规则\n创建功能包模板命令行 创建一个新功能包 catkin_create_pkg tf01_static roscpp tf2 tf2_ros\n运行节点命令行:\nrosrun tf01_static demo01_static_pub rosrun: ROS的核心命令,用于运行节点 tf01_static: 功能包名 demo01_static_pub: 节点名 定义位置: 在代码中通过ros::init()函数设置 注意: 实际运行的是编译后的可执行文件名,与代码中的节点名可以不同\n核心概念区分: 可执行文件名 - 操作系统层面 定义在CMakeLists.txt中 是磁盘的二进制文件名 通过rosrun \u0026lt;包名\u0026gt; \u0026lt;可执行文件名\u0026gt; 执行 节点名 - ROS系统层面: 定义在代码中ros::init()函数 是ROS图中的标识符 通过rosnode list查看 这里面还有很深的门道: 略\n补充1: 当坐标系之间的相对位置固定时,那么所需参数也是固定的,父坐标系名称,子坐标系名称,x偏移量,y偏移量,x翻滚角度,y俯仰角度,z偏航角度,实现逻辑相同,参数不同,ROS系统已经封装好了专门的节点:\n命令行 // rosrun tf2_ros static_transform_publisher x偏移量 y偏移量 z偏移量 z偏航角度 y俯仰角度 x翻滚角度 父级坐标系 子级坐标系\nrosrun tf2_ros static_trasform_publisher 0.2 0 05 0 0 0 /baselink/laser\n补充2: 可以借助于rviz显示坐标系关系,具体操作 新建窗口输入命令: rviz 在启动的rviz中设置Fixed Frame 为base_link 点击左下的add按钮,在弹出的窗口中选择TF组件,即可显示坐标关系\n配置安装规则: 安装后: 编辑: package.xml\ntf01_static 0.0.0 Static TF broadcaster package Your Name BSD 编辑:\ntf01_static 0.0.0 Static TF broadcaster package Your Name BSD\n代码编写： 需求: 发布两个坐标系的相对关系 流程: 1.包含头文件; 2.设置编码 节点初始化 NodeHandle; 3. 创建发布对象; 4.组织被发布的消息 5.发布数据; 6.spin();\n动态坐标变换 实现流程: 1.新建功能包,添加依赖 2.创建坐标相对关系发布方(同时需要订阅乌龟位姿信息) 3.创建坐标相对关系订阅方 4.执行\nC++ 实现 1.创建功能包 创建项目功能包依赖于: tf2, tf2_ros, tf2_geometry_msgs, roscpp rospy std_msgs geometry_msgs\n2.发布方\nrviz tf: x轴: 红色 前方 y轴: 绿色 左方 z轴：蓝色 上方\n查看特定话题 rqt_plot /debugpub/data rqt_plot /debugpub1/data\nrqt_plot 话题名称\n同时查看多个特定话题 rqt_plot /debugpub/data/data /debugpub1/data/data\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/ros1/","summary":"\u003ch1 id=\"ros1\"\u003eROS1\u003c/h1\u003e\n\u003cp\u003e学习网址\n\u003ca href=\"https://bluesnie.github.io/Learning-notes/ROS2/%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%AD%A6%E7%AF%87/%E7%AC%AC7%E7%AB%A0-ROS2%E8%BF%90%E5%8A%A8%E5%AD%A6/001-TF2%E4%BB%8B%E7%BB%8D%E5%8F%8ARVIZ-TF%E7%BB%84%E4%BB%B6.html\"\u003ehttps://bluesnie.github.io/Learning-notes/ROS2/%E6%9C%BA%E5%99%A8%E4%BA%BA%E5%AD%A6%E7%AF%87/%E7%AC%AC7%E7%AB%A0-ROS2%E8%BF%90%E5%8A%A8%E5%AD%A6/001-TF2%E4%BB%8B%E7%BB%8D%E5%8F%8ARVIZ-TF%E7%BB%84%E4%BB%B6.html\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"创建工作空间功能包流程\"\u003e创建工作空间功能包流程\u003c/h2\u003e\n\u003cp\u003e遇到的bug:\n最开始改CMakeLists.txt文件时改错文件了,而且居然改的时候有使用超级用户权限,应该改的文件是功能包的CMakeLists.txt文件，而不是工作空间的CMakeLists.txt文件,这个文件是自动生成的,后来意识到这个问题,但是改的时候一直没有意识到权限不够,我以为我改了实际上我没有更改成功,最后知道新开了一个工作空间重新编译才发现这个报了一摸一样的错误,才发现,之前一直以为是路径错误.\u003c/p\u003e","title":"ROS1"},{"content":"ROS2 第一章ROS2介绍 暂无 第二章准备环境与安装ROS2 暂无\n第三章动手学ROS2基础 工作空间 工作空间是一个存放项目开发类相关文件的文件夹，是开发过程的大本营\nsrc 代码空间 Install 安装空间 build 编译空间 Log 日志空间\n创建工作空间： mkdir -p ~/dev-ws/src\n节点：机器人的工作细胞 执行具体任务的进程 独立运行的可执行文件 可使用不同的编程语言 可分布式运行在不同主机 通过节点名称进行管理 节点操作 ros2 node list //当前正在运行的节点信息 ros2 node info //查看该节点信息 ros2 topic //话题 ros2 bag //录制\nros2基本使用 1.Terminal ctrl+alt+t 2.当前终端所在位置 pwd 3.当前路径下所在的文件或文件夹 Ls 4.隐藏文件 ls-A 5.创建文件 Mkdir + 文件名 6.进入路径 Cd 7.创建文件 Touch 8.删除文件 Rm 9.退回上一级目录 cd\u0026hellip; 10.递归删除（删除文件夹） rm-R 11.安装功能包 Sudo apt install//提升使用当前用户权限为管理员权限//应用//安装\nros2功能包的相关使用 1.创建功能包\n2.列出可执行文件 ros2 pkg executables 列出某个功能包 ros2 pkg executables\n3.列出所有功能包 ros2 pkg list\nros2 pkg list | grep vill//过滤 列出vill开头的功能包\n4.列出某个包所在的路径前缀\n列出包的清单描述文件 每一个功能包都有一个标配的manifest.xml文件,用于记录这个包的名字,构建工具,编译信息,拥有者，干啥用的信息 通过这个信息，就可以自动为该功能包安装依赖，构建时确立编译顺序 ros2 pkg xml\n自动安装依赖：\n编译工作空间： Colon build //在工作空间的根目录下进行编译 设置环境变量：\n安装获取 Sudo apt install ros--package_name 功能包相关命令行ros2 pkg Create executables lists prefix xml\nros2构建工具colcon 作用：功能包构建工具 编译代码 1.安装colcon Sudo apt -get install python3-colcon-common-extensions 2.编译工程 Colcon build 3.运行一个自己编的节点 (1)打开一个终端使用cd colcon_test进入我们刚刚创建的工作空间,先source一下资源 source install/setup.bash (2)运行一个订杂志节点,你将看不到任何打印功能,因为没有发布者 ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function (3)打开一个新的终端,先source,再运行一个发行杂志节点 source install/setup.bash ros2 run examples_rclcpp_minimal_publisher publisher_member_function\ncolcon常用指令 1.只编译一个包 colcon test \u0026ndash;packages-select YOUR_PKG_NAME 2.不编译测试单元 colcon test \u0026ndash;packages-select YOUR_PKG_NAME \u0026ndash;cmake-args -DBUILD_TESTING=8 3.运行编译的包的测试 colcon test 4.允许通过更改src下的部分文件来改变install(重要) colcon build \u0026ndash;symlink-install\n第四章ROS2通信机制-话题与服务 ROS2话题介绍\n1.话题的发布订阅模型(Topic通信模型) 发布 订阅 Node1\u0026mdash;\u0026mdash;\u0026ndash;\u0026gt;话题\u0026mdash;\u0026mdash;\u0026mdash;-\u0026gt;Node2\n2.话题通信有哪些需要注意的规则 规则： 话题名字是关键,发布订阅接口类型要相同,发布的是字符串,接受也要用字符串来接收 同一个节点可以订阅多个话题,同时也可以发布多个话题,就像是一本书的作者也可以是另外一本书的读者 同一个话题可以有多个发布者 可以1对n，n对1，n对n\n3.相关工具 3.1 RQT工具之rqt_graph ROS2作为一个强大的工具，在运行过程中，我们是通过命令来看到节点和节点之间的数据关系的 运行第二章你说我听小demo。依次打开三个终端,分别输入下面三个命令 ros2 run demo_nodes_py listener ros2 run demo_nodes_cpp talker rqt_graph\n3.2 ROS2话题相关命令行界面(CLI)工具 ros2 topic -h\n3.2.1 ros2 topic list返回系统中当前活动的所有主题的列表 ros2 topic list\n3.2.2 ros2 topic list -t 增加消息类型 3.2.3 ros2 topic echo 打印实时话题内容 ros2 topic echo /chatter\n3.2.4 ros2 topic info 查看主题信息 ros2 topic info /chatter\n3.2.5 ros2 interface show 查看消息类型 ros2 interface show std_msgs/msg/String 3.2.6 ros2 topic pub arg手动发布命令 ros2 topic pub /chatter std_msgs/msg/String \u0026lsquo;data: \u0026ldquo;123\u0026rdquo;\u0026rsquo; 编写话题发布者:见实战案例 编写话题订阅者:见实战案例\n4.接口介绍与自定义接口 4.1ROS2通信接口介绍 接口:interface\n1.什么是接口 接口其实是一种规范 字符串:std_msgs/msg/String 32位二进制的整型数据:std_msgs/msg/String\n使用接口的好处 不同语言对字符串的定义是不同的，接口可以抹平这种语言差异 方便程序的适配\n2.ROS2接口介绍 使用ros2 interface package sensor_msgs 命令可以查看某一个接口包下所有的接口 比如传感器类的消息包:sensor_msgs\n3.ROS2自定义接口 ROS2的四种通信方式 话题-Topics 服务-Services 动作-Actions 参数-Parameters 除了参数之外,话题,服务和动作(Action)都支持自定义接口,每一种通信方式所适用的场景各不相同，所定义的接口也被分为话题接口,服务接口,动作接口三种\n话题接口格式：xxx.msg int64 num//发布的话题组合\n服务接口格式：xxx.srv int64 a int64 b//请求 int64 sum//发布\n动作接口格式：xxx.action int32 order//目标 int32[] sequence//反馈 int32[] partial_sequence//结果\n转换过程 msg,srv,action \u0026mdash;\u0026mdash;\u0026mdash;\u0026gt; ROS2-IDL转换器 \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-\u0026gt; python的py,C++的.h头文件\n4.ROS2接口常用CLI命令 4.1查看接口列表 ros2 interface list 4.2查看所有接口包 ros2 interface packages 4.3查看某一个包下的所有接口 ros2 interface package std_msgs 4.4查看某一个接口详细的内容 ros2 interface show std_msgs/msg/String 4.5输出某一个接口的所有属性 ros2 interface proto sensor_msgs/msg/Image\n4.2 ROS2自定义话题接口 话题是一种单向通信的接口,同一个话题只能由发布者将数据传递给订阅者，所以定义话题接口也只需要定义发布者所要发布的类型即可.在实际的工程中为了减少功能包之间的互相依赖,通常会将接口定义在一个独立的功能包中 有了功能包之后,我们就可以新建话题接口了,新建方法如下: 新建msg文件夹,并在文件夹下新建xxx.msg（大写字母开头） 在xxx.msg下编写消息内容并保存 在CmakeList.txt添加依赖和msg文件目录 在package.xml中添加xxx.msg所需要的依赖 编译功能包即可生成python与C++头文件\n2.1新建功能空间 ros2 pkg create village_interfaces \u0026ndash;build-type ament_cmake\n2.2新建msg文件夹和Novel.msg(小说类型) cd village_interface mkdir msg touch Novel.msg\n2.3编写Novel.msg内容 我们的目的是给李四的小说的每一章增加一张图片,原来李四写小说是对外发布一个 std_msgs/msg/String字符串类型的数据.而发布图片的格式,我们需要采用ros自带的传感器消息接口中的图片 sensor_msgs/msg/Image 数据类型,所以我们新的消息文件的内容就是将两者合并,在ROS2中可以写做这样：\n方法一 在msg文件中可以使用#号添加注释\n#标准消息接口std_msgs下的String类型 std_msgs/String content #图像消息,调用sensor_msgs下的Image类型 sensor_msgs/Image image\n方法二 #直接使用ROS2原始的数据类型 String content #图像消息,调用sensor_msgs下的Image类型 sensor_msgs/Image image\n说明 通过下面的指令查看std_msgs/String是由基础数据类型string组成的 ros2 interface show std_msgs/msg/String\nROS2中的原始数据类型 bool byte字节类型 char字符类型 float32,float64 int8,uint16 int32,uint32 int64,uint64 string\n2.4修改CMakeList.txt 完成代码编写还不够,我们还需要在CmakeLists.txt中告诉编译器,你要给我把Novel.msg转换成python库和C++库\n#添加对sensor_msg的 find_package(sensor_msgs REQUIRED) find_package(rosidl_default_generators REQUIRED) #添加消息文件和依赖 rosidl_generate_interfaces(\u0026amp;{PROJECT_NAME} \u0026ldquo;msg/Novel.msg\u0026rdquo; DEPENDENCIES sensor_msgs )\nfind_package用于查找rosidl_default_generators位置,下面rosidl_generate_interfaces就是声明msg文件所属的工程名字,文件位置以及依赖的DEPENDENCIES 踩坑报告:重点强调一下依赖部分DEPENDENCIES ,我们消息中用到的依赖这里必须写上,即使不写编译器也不会报错,知道运行的时候才会报错\n4.4ROS2服务介绍（序号乱了不知道为什么就这样没有改） 服务通信介绍与体验 1.启动服务端 这个命令用于运行一个服务节点，这个服务的功能是将两个数字相加,给定a,b两个数返回sum就是ab之和 ros2 run examples_rclpy_minimal_service service 2.使用命令查看列表 ros2 service list 3.3手动调用服务 再启动一个终端，输入下面的命令 ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts \u0026ldquo;{a: 5,b: 10}\u0026rdquo;\n服务相关CLI工具 1.查看服务列表 ros2 service list 2.手动调用服务 ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts \u0026ldquo;{a: 1,b: 5}\u0026rdquo; 3.查看服务接口类型 ros2 service type /add_two_inits 4.查找使用某一接口的服务 ros2 service find example_interfaces/srv/AddTwoInts\n4.5自定义服务接口 话题是发布订阅模型，主要是单向传输数据,只能由发布者发布，接收者接收（同一话题，发布者接收者都可以有多个） 服务是客户服务端（请求响应）模型 由客户端发送请求，服务端处理请求，然后返回处理结果（同一服务，客户端可以有多个，服务端只能有一个）\n如何创建自己的服务接口 新建srv文件夹，并在文件夹下新建xxx.srv 在xxx.srv下编写服务接口内容并保存 在CmakeLists.txt添加依赖和srv文件目录 在package.xml中添加xxx.srv所需依赖 编译功能包即可生成python与C++头文件\n4.5.2python服务通信实现(李三借钱) 服务端: 1.导入服务接口 2.创建服务端回调函数 3.声明并创建服务端 4.编写回调函数逻辑处理请求\n4.6.3创建客户端李三节点 1.导入服务接口 2.创建请求结果接收回调函数 3.声明并 创建客户端 4.编写结果接收逻辑 5.调用客户端发送请求\n4.6话题服务对比 1.话题 1.话题是单向的,而且不需要等待服务端上线，直接发就行,数据的实时性比较高 频率高,实时性强的传感器数据的传递一般使用话题实现\n2.服务 服务是双向的,客户端发送请求后,服务端有 响应,可以得知服务端的处理结果 频率较低,强调服务特性和反馈的场景一般使用服务实现\nGit 安装git Sudo apt install git //下载别人的代码 Git clone 地址\n第五章 2.参数是节点的一个配置,你可以任务参数是一个节点的设置 3.论参数组成成分 参数是由键值对组成,键值对指的就是名字和数值.\n1.ros2查看节点有哪些参数(设置) ros2 param list\n2.详细查看一个参数的信息 ros2 param describe \u0026lt;node_name\u0026gt; \u0026lt;param_name\u0026gt;\n3.查看参数值 例子: ros2 param get /turtlesim background_b\n4.设置参数 例子: ros2 param set /turtlesim background_b 83\n5.保存参数值 ros2 param dump /turtlesim\n6.启动节点时加载参数快照 ros2 run \u0026lt;package_name\u0026gt; \u0026lt;executable_name\u0026gt; \u0026ndash;ros-args \u0026ndash;param-file \u0026lt;file_name\u0026gt;\nros2相关代码 函数名称 描述 declare_parameter 声明和初始化一个参数 declare_parameters 声明和初始化一堆参数 get_parameter 通过参数名字获取一个参数 get_parameter 通过多个参数名字获取多个参数 set_parameters 设置一组参数的值\n编写CPP参数 声明参数 获取并设置参数 Action通信介绍 话题适用于节点间单向的频繁的数据传输, 服务则适用于节点间双向的数据传输, 而参数则适用于动态调整节点的设置\nAction的组成部分 目标:Action客户端告诉服务端要做什么,服务端针对该目标要有响应,解决了不能确认服务端接收并处理目标的问题 反馈:Action服务端告诉客户端此时做的进度如何(类似工作汇报).解决了执行过程中没有反馈问题 结果:Action服务端最终告诉客户端执行结果,结果最后返回,用于表示任务最终执行情况\n参数是由服务构建出来了,而Action是由话题和服务共同构建出来的(一个Action = 三个服务+两个话题)\n三个服务分别是: 1.目标传递服务 2.结果传递服务 3.取消执行服务\n两个话题: 1.反馈话题(服务发布,客户端订阅) 2.状态话题(服务端发布,客户端订阅)\nAction的CLI工具 action list 获取目前系统中的action列表\naction info 查看action信息\naction send_goal 发送请求到服务端\nros2通信机制的总结 1.话题 话题是单向的而且不需要等待服务器上线,直接发就行,数据的实时性比较高. 频率高,实时性强的传感器数据的传递一般使用话题实现\n2.服务 服务是双向的,客户端发送请求后,服务端有响应,可以得知服务端的处理结果,频率较低,强调服务特性和反馈的场景一般使用服务实现\n3.参数 参数是节点的设置,用于配置节点,原理基于服务\n4.动作 动作适用于实时反馈的场景，原理基于服务\n第六章-ROS2工具介绍 1.launch 1.ros2节点管理之launch文件 为什么需要launch文件 1.1需要启动的节点太多 1.2节点之间有依赖关系 launch文件类似于一个脚本文件来管理节点的启动 launch文件允许我们同时启动和配置多个包含ros2节点的可执行文件\n2.编写ros2的launch文件 ros2中可以使用python文件来编写launch文件 1.导入头文件 2.定义函数 3.创建节点函数 4.launch文件描述\n3.测试launch文件 ros2 launch 节点(启动launch文件)\npython略\ncmake编译类型功能包的launch文件安装 install(DIRECTORY launch DESTIMATION share/${PROJECT_NAME} )\n4.通过launch修改参数 例子: parameters=[{\u0026ldquo;writer_timer_period\u0026rdquo; : 1}]\n2.rosbag2 rosbag2介绍与安装 CLI工具:命令行接口工具 ros2中常用的一个CLI工具\u0026ndash;rosbag2,这个工具用于记录话题的数据 (我们做一个真实机器人的时候非常有用,比如我们可以录制一段机器人发生问题的话题数据,录制完成后可以多次发布出来进行测试和实验,也可以将话题数据分享给别人用于验证算法)\n常用指令 1.记录一个话题 ros2 bag record /sexy_girl\n2.记录多个话题的数据 ros2 bag record topic-name1 topic-name2\n3.记录所有话题 ros2 bag record -a\n4.其他选项 -o name 自定义输出文件的名字 ros2 bag record -o fille-name topic-name -s存储格式 目前仅支持sqlite3，其他还带扩展\n查看录制出话题的信息 (比如话题记录的时间,大小,类型,数量) ros2 bag info bag-file\n播放话题数据 ros2 bag play xxx.db3\n3.RQT工具 RQT是一个GUI框架,通过插件的方式实现了各种各样的界面工具\n命令行: rqt\n4.数据可视化工具RVIZ2 数据:各种调试机器人时常用的数据,比如:图像数据,三维点云数据,地图数据,TF数据,机器人模型数据 可视化:可视化就是让你直观的看到数据,比如说一个三维的点(100,100,100)，通过RVIZ可以将其显示在空间中\n注意:RVIZ强调将数据可视化出来,是已有数据的情况下，把数据显示出来而已,而后面讲的gazebo仿真软件是通过真实环境产生数据,两者用途并不一样\nGazebo集成ROS2 Gazebo是一个独立的应用程序,可以独立于ros2或ros使用 Gazebo与ros版本的集成通过一组叫做gazebo_ros_pkgs的包完成的,gazebo_ros_pkgs将Gazebo和ROS2连接起来\ngazebo_dev:开发Gazebo插件可以用的API gazebo_msgs:定义的ROS2和Gazebo之间的接口（Topic/Service/Action） gazebo_ros:提供方便的C++类和函数,可供其他插件使用,例如转换和测试使用程序.他还提供一些通常有用的插件 gazebo_plugins:一系列Gazebo插件,将传感器和其他功能暴露给ROS2\ngazebo_ros_camera 发布ROS2图像 gazebo_ros_diff_drive 通过ROS2控制和获取两轮驱动机器人的接口\n5.ros2命令行工具总结 第七章 1.miniconda 退出conda conda deactivate\n2.jupyter安装 pip3 install jupyter -i https://pypi.tuna.tsinghua.edu.cn/simple\n激活ros_venv环境 source ros_venv/bin/activate\n然后在这里面: 启动jupyter jupyter-notebook\n3.Numpy Numpy是一个功能强大的python库,主要用于对多维数组执行计算。\n使用numpy定义矩阵 1.创建单位矩阵 np.identity(3)\n2.创建零矩阵 np.zeros([3,3])\n3.创建随机矩阵 np.random.rand(3,5)\n4.从已有的数组创建矩阵 np.asarray([1,2,3,4]).reshape(2,2)\n5.判断两个矩阵是否相等 numpy的allclose方法，比较两个array是不是每一个元素都相等,默认在1e-05的误差范围内\nnumpy进行矩阵运算 矩阵加法/减法 加法使用np.add 减法使用np.subtract\n矩阵乘法 np.dot\n矩阵求逆 np.linalg.inv\n矩阵转置 矩阵转置在矩阵后使用.T即可\n4.空间位置姿态 描述三维空间中的姿态\u0026mdash;\u0026mdash;旋转矩阵 旋转矩阵与位置矢量概念\u0026mdash;\u0026mdash;略\n1.平移矩阵\u0026mdash;-略\n2.旋转矩阵\u0026mdash;-坐标变换 image.png\n3.平移旋转复合矩阵 拆分 例如:可以将坐标变换拆分成先绕参考坐标系旋转,再绕参考坐标系平移,这样就得到了复合变换方程\n左右手坐标系的区别\n5.使用numpy表示位置和姿态 3*3单位矩阵表示没有姿态变换（注意不是零矩阵）\n常见问题即解决方案 死锁 1.检查进程状态 如果进程存在且正常运行,等待它完成(尤其是系统更新时) 如果进程无响应或已结束但仍占用锁继续下一步\n2.终止占用锁的进程(谨慎操作) 强制终止进程:\nsudo kill -9 4800 使用-9强制终止卡死进程 注意:确保没有关键系统进程被误杀\n3.检查并删除锁文件 查找并删除软件包管理锁文件: sudo rm/var/lib/dpkg/lock sudo rm/var/lib/apt/lists/lock sudo rm/var/cache/apt/archives//lock 删除前建议检查是否有进程占用锁\n4.修复软件包管理状态 //修复中断的dpkg操作 Sudo dpkg \u0026ndash;configure -a //修复依赖关系 sudo apt-get install -f //清理并更新缓存 sudo apt-get clean sudo apt-get update\n5.重新运行原命令 附加说明 预防措施:避免同时运行多个包管理命令 系统日志:若问题反复出现,检查日志 //实时查看dpkg日志 Tail -f/var/log/dpkg.log\nlinux大小写敏感 全屏虚拟机 Ctrl + alt + enter\nvscode切分终端 Ctrl + shift + 5\n综合案例 1.手撸一个节点 python版 1.创建工作空间 mkdir -p town_ws/src cd town_ws/src code ./ 当前目录下打开vscode 2.创建功能包 创建一个名字叫village_li pythpn版本的功能包 ros2 pkg create village_li \u0026ndash;build-type ament_python \u0026ndash;dependencies rclpy pkg create 是创建包的意思 \u0026ndash;build-type 用来指定该包的编译类型, 一共三个可选项 ament_python,ament_cmake,cmake \u0026ndash;dependencies 指的是这个包的依赖,这里小鱼给一个ros2的python客户端接口rclpy build-type什么都不写,ros2会默认为ament_cmake 3.创建节点文件 在__init__.py同级别目录下创建一个叫做li4.py的文件(在vscode中右击新建就行) 编写ROS2节点的一般步骤 1.导入库文件 2.初始化客户端库 3.新建节点 4.spin循环节点 5.关闭客户端库 import rclpy from rclpy.node import Node\n源代码见vscode 配置 在setup.py console_scripts里面添加 \u0026ldquo;li4_node=village_li.li4:main\u0026rdquo;\n在工作空间下编译： colcon build 为了让系统找到功能包和节点：source install/setup.bash 最后在工作空间下运行节点： ros2 run village_li li4_node\nros2 node list ros2 node info /li4\nimport rclpy from rclpy .node import Node from std_msgs.msg import String\n\u0026quot;\u0026quot;\u0026quot; 导入消息类型\n声明并创建发布者\n编写发布逻辑发布数据 \u0026quot;\u0026quot;\u0026quot;\nclass WriterNode(Node): def __init(self,name): super().init(name) self.get_logger.info(\u0026ldquo;大家好,我是%s,我是一名作家!\u0026quot;%name) self.pub_novel = self.create_publisher(String,\u0026ldquo;sexy_girl\u0026rdquo;,10)\nself.count = 0 self.timer_period = 5 self.timer = self.create_timer(self.timer_period,self.timer_callback) def timer_callback(self): msg = String() msg.data = \u0026quot;第%d回连掩胡 %d 次偶遇呼延娘\u0026quot; % (self.count,self.count) self.pub_novel.publish(msg) #让发布者发布消息 self.get_logger().info(\u0026quot;发布了一个章节的小说内容是%s\u0026quot; % msg.data) self.count += 1 def main(args=None): \u0026quot;\u0026rdquo;\u0026quot; 入口函数 1.ros2运行该节点的入口函数 2.编写ROS2的一般步骤 3,新建节点对象 4.spin循环节点 5.关闭客户端库 \u0026quot;\u0026quot;\u0026quot; rclpy.init(args=args) #初始化rclpy li4_node = WriterNode(\u0026ldquo;li4\u0026rdquo;) #新建一个节点 rclpy.spin(li4_node) #保持节点运行,检测是否收到退出指令 rclpy.shutdown() #关闭客户端库\nC++版 1.创建王家村功能包 王二居住在王家村，王家村和李家村不一样，是使用ament_cmake作为编译类型 所以王家村建立指令像下面这样,依赖变成rclcpp 新建工作空间（新建功能包） ros2 pkg create village_wang \u0026ndash;build-type ament_cmake \u0026ndash;dependencies rclcpp 2.创建节点 在village_wang/src下创建一个wang2.cpp文件\n配置rclcpp/rcpcpp.hpp路径\n源代码见vscode\n修改CMakeList.txt文件 在CMakeList.txt最后一行加入下面两行代码 add_executable(wang2_node src/wang2.cpp) ament_target_dependencies(wang2_node rclcpp)\n添加两行代码的目的是让编译器编译wang2.cpp这个文件,不然不会主动编译.接着在上面两行代码下面添加下面代码 install(TARGETS wang2_node DESTINATION lib/${PROJECT_NAME} ) 这个是C++比python更麻烦的地方,需要手动将编译好的文件安装到install/village_wang/lib/village_wang\n编译运行节点 在工作空间下 colcon build \u0026ndash;packages-select village_wang\nros2 pkg list | grep vill\nsource install/setup.bash\nros2 run village_wang wang2_node\n2.编写python话题发布者 1.导入消息类型 2.声明并创建发布者 3.编写发布逻辑发布数据 编写cpp话题发布者\n3.编写python话题订阅者 1.导入订阅的话题接口类型 2.创建订阅回调函数 3.声明并创建回调者 4.编写订阅回调处理逻辑\n4.编写cpp话题发布者 1.创建一个话题订阅者的能力,用于拿到艳娘传奇的数据 2.创建一个话题发布者的能力,用于给李四送稿费 3.获取日志打印器的能力\n5.编写cpp话题订阅者 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/ros2/","summary":"\u003ch1 id=\"ros2\"\u003eROS2\u003c/h1\u003e\n\u003ch2 id=\"第一章ros2介绍\"\u003e第一章ROS2介绍\u003c/h2\u003e\n\u003ch3 id=\"暂无\"\u003e暂无\u003c/h3\u003e\n\u003ch2 id=\"第二章准备环境与安装ros2\"\u003e第二章准备环境与安装ROS2\u003c/h2\u003e\n\u003cp\u003e暂无\u003c/p\u003e\n\u003ch2 id=\"第三章动手学ros2基础\"\u003e第三章动手学ROS2基础\u003c/h2\u003e\n\u003ch3 id=\"工作空间\"\u003e工作空间\u003c/h3\u003e\n\u003cp\u003e工作空间是一个存放项目开发类相关文件的文件夹，是开发过程的大本营\u003c/p\u003e\n\u003cp\u003esrc         代码空间\nInstall     安装空间\nbuild       编译空间\nLog         日志空间\u003c/p\u003e\n\u003cp\u003e创建工作空间：\nmkdir -p ~/dev-ws/src\u003c/p\u003e\n\u003ch3 id=\"节点机器人的工作细胞\"\u003e节点：机器人的工作细胞\u003c/h3\u003e\n\u003cp\u003e执行具体任务的进程\n独立运行的可执行文件\n可使用不同的编程语言\n可分布式运行在不同主机\n通过节点名称进行管理\n节点操作\nros2 node list   //当前正在运行的节点信息\nros2 node info  //查看该节点信息\nros2 topic       //话题\nros2 bag         //录制\u003c/p\u003e","title":"ROS2"},{"content":"SLAM 第一讲:预备知识 Simultaneous Localization and Mapping(同时定位与地图构建) 解决: 定位和建图\n常见的库: Eigen:线性代数,Opencv:视觉处理,PCL:点云处理,g2o:slam框架,Ceres:非线性关系求解\nslam系统: 视觉里程计,后端优化,建图,回环检测\n第二讲:初始SLAM slam解决的问题 (早期的slam一种状态估计问题) \u0026ndash; 空间状态不确定性的估计 本质: 对运动主体自身和周围环境空间不确定性的估计 定位: 我在什么地方? 建图: 周围环境是什么样?\n常见传感器:激光传感器,相机,轮式编码器,惯性测量单元(IMU)等\n照片的本质是拍摄某个场景在相机的成像平面上留下的一个投影:它以二维的形式记录 三维的世界.(丢失深度(距离)) -\u0026gt; 可能是很近很小,也可能是很远很大\n相机: 分为双目相机和深度相机\n双目相机: 1.双目相机由两个单目相机组成,但这两个相机之间的距离(基线)是已知的,我们通过这个极限来估计每个像素的空间位置\u0026mdash;这和人眼很相似 2.双目相机测量到的深度范围与基线相关.极限距离越大,能够测量到的物体越远,双目相机的距离是比较左右眼的图像获得的,并不依赖其他传感器设备.\n3.视差的计算非常消耗计算资源,需要GPU和FPGA设备加速\n深度相机:(RGB-D相机): 通过红外结构光或Time-of-Fliagh原理,通过主动向物体发射光接收返回的光测出物体与相机之间的距离\n视觉slam的目标:是通过这样的一些图像,进行定位和地图构建\n视觉slam框架 image.png\n1.传感器信息读取:主要为相机图像信息的读取和预处理,码盘,惯性传感器等信息的读取和同步\n2.前端视觉里程计:视觉里程计的任务是估算相邻图像间相机的运动,以及局部地图的样子.VO又称为前端(Font End)\n3.后端(非线性)优化:后端接受不停时刻视觉里程计测量的相机位姿,以及回环检测的信息,对它们进行优化,得到全局一致的轨迹和地图,由于接在VO之后,又称为后端(Back End)\n4.回环检测:回环检测判断机器人是否到达过先前的位置.如果检测到回环,它会把信息提供给后端进行处理\n5.建图:它更具估计的轨迹,建立与任务要求对应的地图\n视觉里程计: \u0026mdash; 前端 视觉里程计: 视觉里程计能够通过相邻帧间的图像估计相机运动,并恢复尝尽的空间结构.称它为里程计就像一种只有短时记忆的物种\n(图像地特征提取与匹配)\n图像在计算机里知识一个数值矩阵\n一方面:只要把相邻时刻的运动串起来,就构成了机器人的运动轨迹 另一方面:我们根据每个时刻的相机位置,计算出各像素对应的空间点位置,就得到了地图\n仅通过视觉里程计来估计轨迹,将不可避免地出现累积漂移\n累积漂移: 每次估计都带有一定地误差,而由于里程计地工作方式,先前时刻地误差将会传递到下一时刻,导致经过一段时间之后,估计地轨迹将不再准确 -\u0026gt; 导致我i们无法建立一致地地图\n后端优化: 主要处理SLAM过程中地噪声问题 \u0026mdash; 如何让从这些带有噪声地数据中估计整个系统地状态,以及这个状态估计地不确定有多大 \u0026mdash;- 最大后验概率估计\n(滤波与非线性优化算法)\n回环检测 又称闭环检测,主要解决位置估计随时间漂移的问题\n解决方法: 使用某种手段m让机器人知道回到了原点这件事,或者把原点识别出来,我们再把位置估计值拉过去,就可以消除漂移了,这就是所谓的回环检测\n(可以通过判断图像间的相似性来完成回环检测) \u0026mdash; 回环检测成功,则可以显著地减小累积误差\n建图: 构建地图的过程 地图: 是对环境的描述,但这个描述并不是固定的,需要视SLAM的应用而定\n(相机有6个自由度) -\u0026gt; XYZ轴平移,绕XYZ轴旋转\n度量地图: 拓扑地图: 调试: 单步跳过: 执行当前行代码.如果该行包含函数调用,不进入函数内部,直接得到结果并跳到下一行 F10 单步进入: 执行当前行代码,如果该行包含函数调用,会进入该函数内部,并暂停在函数的第一行 F11 单步跳出: 立即执行完当前函数体内剩余的所有代码,并跳出到该函数的下一行语句暂停处 Shift + F11\n1.配置编译任务 按ctrl+shift+p,输入\u0026quot;Tasks: Configure Task\u0026quot; , 选择\u0026quot;Create tasks.json file from templates\u0026quot;,选择\u0026quot;Others\u0026quot;或\u0026quot;C/C++ g++ buikd active file\u0026quot; ,这回生成一个tasks.json,然后问ai生成一个调试文件\n2.配置调试设置(launch.json) 按F5，选择(GDB/LDB),然后选择\u0026quot;g++ build and debug activate file\u0026quot;\n3.开始断点调试 1）.设置断点: 在代码行号左侧单击,出现红点即为断点 2）.启动调试: 按F5 3）.调试操作 4）.查看状态\n4.高级调试技巧 条件断点 日志断点 函数断点\n第三讲 三维空间刚体运动 主要目标: 1.理解三维空间的刚体运动描述方式: 旋转矩阵,变换矩阵,四元数和欧拉角 2.掌握Eigen库的矩阵,集合模块的使用方法\n如何描述刚体在三维空间中的运动 -\u0026gt; 由一次旋转加一次平移组成\n旋转矩阵 点,向量和坐标系 点: 空间中的基本元素,没有长度和体积,用于表示位置 向量: 连接两个点的有向线段,表示为箭头,具有方向和大小(模长).向量本身是空间中的实体,独立于坐标表示 坐标系: 为描述点和向量的位置而定义的框架,由一组基向量(e1,e2,e3)构成.向量在坐标系下的坐标表示为线性组合系数\n右手系​：常见于 OpenGL、3D Max 等库。坐标轴满足右手法则（x×y=z） 左手系​：常见于 Unity、Direct3D 等库。坐标轴满足左手法则（x×y=−z）\n向量的运算: 基本运算： 数乘,加法,减法,内积(描述投影关系,结果与坐标系无关),外积(结果向量垂直于原向量构成的平面,模长等于两向量张成的平行四边形面积)\n坐标系间的欧式变换 例子: 惯性坐标系(世界坐标系) -\u0026gt; 可以认为它是固定不动的 相机或机器人是一个移动坐标系\n刚体运动: 两个坐标系之间的运动由一个旋转加上一个平移组成,这种运动称为刚体运动 ,同一个向量在各个坐标系下的长度和夹角都不会发生变化\n欧式变换: (刚体变换) -\u0026gt; 描述三维空间中刚体运动的一种数学工具.他能保持物体内部任意两点间的距离和角度不变,只改变物体的位置和姿态想象一下你拿起手机移动或旋转：手机本身的形状、大小、各个面的夹角都没变，只是它在空间中的“位置”和“朝向”变了。 我们说手机坐标系到世界坐标之间，相差了一个欧式变换(Euclidean Transform)\n欧式变换由旋转和平移组成\n齐次坐标: 我们在一个三维向量的末尾添加1，将其变成了四维向量,称为齐次坐标,对于这个四维向量,我们可以把旋转和平移写在一个矩阵里,使得整个关系变成线性关系 -\u0026gt; 该矩阵T(Transform Matrix)称为变换矩阵 -\u0026gt; 数学技巧(统一的线性方式来处理旋转和平移) :\nEigen库 简介: 是一个C++开源线性代数库.它提供了快速的有关矩阵的线性代数运算,还包括解方程等功能.许多上层的软件库也使用Eigen进行矩阵运算,包括g20,Sophus等\n安装:\nsudo apt-get install libeigen3-dev 查找头文件安装位置: 默认位置: /usr/include/eigen3/\n终端输入:\nsudo updatedb locate eigen3\nEigen库是一个纯头文件库,这意味它的全部实现都包含在头文件中,而不是预先编译好的二进制文件(如Windows下的.lib,.dll或Linux下的.a,.so) 好处: 1.不需要担心操作系统或编译器的差异 2.更佳的优化潜力 3.避免API问题: 传统库升级时,如果二进制接口(API)发生变化,可能需要编译整个项目, 而Eigen作为头文件库,不存在这个问题\n为了达到更高的效率,在Eigen中需要指定矩阵的大小和类型 -\u0026gt; 完全可以在编译时确定它们的大小和数据类型 Eigen矩阵不支持自动类型提升 -\u0026gt; 在C++程序中,我们可以把一个float数据和double数据相加,相乘,编译器会自动把数据类型转换为最合适的那种,而在Eigen中,出于性能的考虑,必须显式地对矩阵类型进行转换否则 -\u0026gt; YOU MIXED DIFFERENT NUMERIC TYPES(混合了不同地数值类型)\n同理,在计算过程中也需要保证矩阵位数的正确性,否则 -\u0026gt; YOU MIXED MATRICES OF DIFFERENT SIZES(混合了不同尺寸的矩阵)\n6自由度的三维刚体运动 描述一个物体在三维空间中完整运动能力的基石 物体可以进行3个方向的平移 x,y,z 和3个方向的旋转 roll,pitch,yaw\n旋转向量 第四讲: 李群与李代数 第五讲: 相机与图像 第六讲 非线性优化 使用ceres进行曲线拟合 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/slam/","summary":"\u003ch1 id=\"slam\"\u003eSLAM\u003c/h1\u003e\n\u003ch1 id=\"第一讲预备知识\"\u003e第一讲:预备知识\u003c/h1\u003e\n\u003cp\u003eSimultaneous Localization and Mapping(同时定位与地图构建)\n解决: 定位和建图\u003c/p\u003e\n\u003cp\u003e常见的库: Eigen:线性代数,Opencv:视觉处理,PCL:点云处理,g2o:slam框架,Ceres:非线性关系求解\u003c/p\u003e","title":"SLAM"},{"content":"SQL ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/sql/","summary":"\u003ch1 id=\"sql\"\u003eSQL\u003c/h1\u003e","title":"SQL"},{"content":"stm32 stm32基础知识 stm32开发方式 基于寄存器的方式 基于标准库 和基于HAL库的方式\n新建工程 .建立工程文件夹,Keil中新建工程,选择型号 .工程文件夹中建立Start，Library,User等文件夹，复制固件库里面的文件到工程文件夹 .工程里对应建立Start,Library,User等同名称的分组,然后文件夹内的文件添加到工程分组里 .工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹 .工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER .工程选项,Debug,下拉列表选择对应调试器,Settings,Flash,Download里勾选Reset and Run\nGPIO输出 .GPIO通用输入输出口 .可配置为8种输入输出模式 .引脚电平:0V~3.3V,部分引脚可容忍5V .输出模式下可控制端口输出高低电平,用以驱动LED，控制蜂鸣器 模拟通信协议输出时序 .输入模式下可读取端口的高低电平或电压,用于读取按键输入,外接模块电平信号输入,ADC电压采集,模拟通信协议接收数据等\n八种输入输出模式 浮空输入\u0026mdash;-数字输入\u0026mdash;-可读取引脚电平,若引脚悬空,则电平不确定 上拉输入\u0026mdash;\u0026ndash;数字输入\u0026mdash;-可读取引脚电平,内部连接上拉电阻,悬空时默认高电平 下拉输入\u0026mdash;\u0026ndash;数字输入\u0026mdash;\u0026ndash;可读取引脚电平,内部连接下拉电阻,悬空时默认低电平 模拟输入\u0026mdash;\u0026mdash;模拟输入\u0026mdash;\u0026ndash;GPIO无效,引脚直接接入内部ADC 开漏输出\u0026mdash;\u0026ndash;数字输出\u0026mdash;\u0026ndash;可输出引脚电平,高电平直接接VDD，低电平接VSS 推挽输出\u0026mdash;\u0026ndash;数字输出\u0026mdash;\u0026ndash;可输出引脚电平，高电平接VDD，低电平接VSS 复用开漏输入\u0026mdash;\u0026ndash;数字输出\u0026mdash;\u0026ndash;由片上外设控制m,高电平为高组态,低电平接VSS 复用推挽输入\u0026mdash;\u0026ndash;数字输出,由片上外设控制.高电平接VDD，低电平接VSS\nGPIO输出\n1。按键：常见的输入设备，按下导通,松手断开 按键抖动:由于按键内部使用的是机械式弹簧片来进行通断,所以在按下和松手的瞬间会伴随一连串的抖动\n2.传感器:传感器元件(光敏电阻/热敏电阻/红外接受管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出\n调试方式 串口调试: 通过串口通信,将调试信息发送 到电脑端,电脑使用串口助手显示调试信息\n显示屏调试: 直接将显示屏连接到单片机,将 调试 信息打印在显示屏上\nKeil调试模式: 借助Keil软件的调试模式 可使用 单步运行,设置断点,查看寄存器及变量等功能\n对照法\n逐行注释发\n点灯法\n测试程序的基本思想: 缩小范围,控制变量,对比测试\nOLED简介 OLED：有机发光二极管 OLED显示屏: 性能优异的新型显示屏,具有功耗低,响应速度快,宽视角,轻薄柔韧等特点 0.96寸OLED模块: 小巧玲珑，占用接口少,简单易用,是电子设计中非常常见的显示屏模块 供电: 3~3,5V, 通信协议 : I2C/SPI, 分辨率: 128*64\n中断系统 中断: 在主程序运行过程中,出现了特定的中断触发条件(中断源), 使得CPU暂停正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行\n中断优先级: 当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源\n中断嵌套: 当一个中断程序正在运行时, 又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回\nEXTI简介 EXTI外部中断 EXTI可以检测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行对应的中断程序。 支持的触发方式: 上升沿/下降沿/双边沿/软件触发 支持的GPIO口: 所有GPIO口,但相同的Pin不能同时触发中断 通道数: 16个GPIO_Pin，外加PVD输出,RTC闹钟,USB唤醒,以太网唤醒 触发响应方式: 中断响应/事件响应\nNVIC优先级分组 NVIC的中断优先级由寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级 抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队 ,抢占优先级和响应优先级均相同的按中断号排队\nAFIO复用IO口 AFIO主要用于引脚复用功能的选择和重定义 在STM32中,AFIO主要完成两个任务 : 复用功能引脚重映射,中断引脚选择\n旋转编码器介绍 旋转编码器: 用来测量位置,速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向 类型: 机械触点式/霍尔传感器式/光栅式\nTIM定时器 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断 16位计数器,预分频器,自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时 不仅具备基本的定时中断功能,而且还包含外时钟选择,输入捕获,输出比较,编码器接口,主从触发模式等功能 根据复杂度和应用场景分为了高级定时器,通用定时器,基本定时器三种类型\n定时器类型 高级定时器 编号: TIM1,TIMB 总线: APB2 功能: 拥有通用定时器全部功能,并额外具有重复计数器,死区生成,互补输出,刹车输入等功能\n通用定时器 编号: TIM2,TIM3,TIM4,TIM5 总线: APB1 功能: 拥有基本定时器全部功能,并额外具有内外时钟源选择,输入捕获,输出比较,编码器接口,主从触发模式等功能\n基本定时器 编号: TIM6,TIM7 总线: APB1 功能: 拥有定时中断,主模式触发DAC的功能\n补充: STM32F103C8T6定时器资源: TIM1,TIM2,TIM3,TIM4\n定时中断基本结构 image.png\n输出比较简介 OC输出比较 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置0,置1或翻转的操作,用于输出一营频率和占空比的PWM波形 每个高级定时器和通用定时器都拥有4个输出比较通道 高级定时器的前3个通道额外拥有死区生成和互补输出的功能\nPWM简介 PWM脉冲宽度调制 在具有惯性的系统中,可以通过一系列的宽度进行调制,来等效地获得所需要地模拟参量,常用于电机控速等领域 PWM参数: 频率 = 1 / Ts 占空比 = Ton / Ts 分辨率 = 占空比变化步距\n输出比较模式 模式 描述 冻结 CNT=CCR，REF保持为原状态 匹配时置有效电平 CNT=CCR，REF置有效电平 匹配时置无效电平 CNT=CCR,REF置无效电平 匹配时电平翻转 CNT=CCR，REF电平翻转 强制为无效电平 CNT与CCR无效,REF强制为无效电平 强制为有效电平 CNT与CCR无效,REF强制为有效电平 PWM模式1 向上计数: CNT \u0026lt; CCR, REF置有效电平, CNT \u0026gt;= CCR ,REF置无效电平 向下计数: CNR \u0026gt; CCR，REF置无效电平，CNT\u0026lt;=CCR,REF置有效电平 PWM模式2 向上计数: CNT\u0026lt;CCR,REF置无效电平，CNT\u0026gt;=CCR,REF置有效电平 向下计数：CNT\u0026gt;CCRREF置无效电平，CNT\u0026lt;=CCR,REF置无效电平 image.png\n参数计算 PWM频率: Freq = CK_PSC/(PSC + 1) / (ARR + 1) PWM占空比：Duty = CCR / (ARR + 1) PWM分辨率: Reso = 1 / (ARR + 1)\n舵机简介: 舵机是一种根据输入PWM信号占空比来控制输出角度的装置 输入PWM信号要求: 周期为20ms,高电平宽度为0,5ms~2.5ms\n直流电机及驱动简介 直流电机是一种将电能转换为机械能的装置,有两个电极，当电极正接时,电机正转,当电极反接时,电机反转 直流电机输入大功率器件，GPIO口无法直接驱动,需要配合电机驱动电路来操作 TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向\n//这里其实是把PWM波形当成一个通信协议来做的 输入信号脉冲宽度 舵机输出轴转角 0.5ms -90° 1ms -45° 1.5ms -0° 2ms 45° 2.5ms 90° image.png\n输入捕获简介 IC输入捕获 输入捕获模式下，当通道输入引脚出现指定电平跳变,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率,占空比,脉冲间隔,电平持续时间等参数 每个高级定时器和 通用定时器都拥有4个输入捕获通道 可配置为PWMI模式,同时测量频率和占空比 可配合主从触发模式,实现硬件全自动测量\n频率测量 image.png\n输入捕获通道 image.png\n主从触发模式 image.png\n输入捕获基本模式 image.png\nPWMI基本结构 image.png\n编码器接口简介 Encoder Intefface编码器接口 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减从而知识编码器的位置,旋转方向和旋转速度 每个高级定时器和通用定时器都拥有1个编码器接口 两个输入引脚借用了输入捕获的通道1和通道2\n正交编码器 image.png\n编码器接口基本结构 image.png\n工作模式 image.png\nADC简介 ADC模拟-数字转换器 ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字量,建立模拟电路到数字电路的桥梁 12位逐次逼近型ADC,1us转换时间 输入电压范围:03.3V，转换结果范围:04095 18个输入通道,可测量16个外部和2个内部信号源 规则组和注入组两个转换单元 模拟看门狗自动 检测输入电压范围 STMF103C8T6 AD 资源: ADC1,ADC2,10个外部输入通道\n逐次逼近型ADC image.png\nADC基本结构 image.png\n触发控制 image.png\n数据对齐 image.png\n转换时间 image.png\n校准 image.png\nDMA(Direct Memory Access)直接存储器存取 DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须COU干预,节省了CPU资源 12个独立可配置的通道:DMA1(7个通道)，DMA2(5个通道) 每个通道都支持软件触发和特定的硬件触发 每个通道都支持软件触发和特定的硬件触发 STM32F103C8T6 DMA资源:DMA1(7个通道)\n存储器映像 image.png\nDMA基本结构 image.png\nDMA转运的3个条件 1.传输计数器大于0 2.触发源有触发信号 3.DMA使能\n通信接口 通信的目的: 将一个设备的数据传送到另一个设备,扩展硬件系统 通信协议: 指定通信的规则,通信双方按照协议规则进行数据收发 image.png\n串口通信 串口是一种应用十分广泛的通讯接口,串口成本低,容易使用,通信线路简单,可实现两个设备的互相通信 单片机的串口可以使单片机与单片机,单片机与电脑,单片机与各式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力\n串口时序 image.png\nUSART简介 USART 通用同步/异步收发器 USART是STM32内部集成的硬件外设,可根据数据 寄存器的一个字节数据 自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放再数据寄存器里 自带波特率发生器,最高达4.5Mbits/s 可配置数据位长度(8/9)，停止位长度(0.5/1/1.5/2) 可选校验位(无校验.奇校验/偶校验) 支持同步模式,硬件流控制,DMA，智能卡,IrDA,LIN STM32F103C8T6 USART资源 : USART1,USART2,USART3\n串口硬件电路 image.png\n简单双向串口通信有两根通信线(发送端TX和接收端RX) TX与RX交叉连接 当只需要单向的数据传输时,可以只接一根通信线 当电平标准不一致时,需要加电平转换芯片\n电平标准 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系，串口常用的电平标准有如下三种: TTL电平: +3.3V或+5V表示1，0V表示0 RS232电平: -3~-15V表示1， +3~+15V表示0 RS485电平: 两线压差+2~+6V 表示1，-2~-6V表示0(差分信号)\n串口参数及时序 波特率: 串口通信的速率 起始位: 标志一个数据帧的开始,固定为低电平 数据为: 数据帧的有效载荷,1为高电平,0为低电平,低位先行 校验位: 用于数据验证,根据数据位计算得来 停止位: 用于数据帧间隔,固定位高电平 image.png\nUSART简介 USART(Universal Synchronous/Asynchronous Receiver/Transmitter) 通用同步/异步收发器 USART是stm32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里 自带波特率发生器,最高达4.5Mbits/s 可配置数据位长度(8/9),停止位长度(0.5/1/1.5/2) 可选校验位(无校验位/奇校验位/偶校验) 支持同步模式,硬件流控制,DMA，智能卡,IrDA,LIN STM32F103C8T6 USART资源： USART1,USART2,USART3\nUSART基本结构 image.png\n数据帧 image.png\n波特率发生器 image.png\n数据模式 image.png\nC语言整数类型详解 固定宽度类型 image.png\n传统C语言类型 image.png\n转义字符 \\r回车键 \\n换行键 stm32串口打印换行这两个转义字符都需要\nC语言可变参数 允许函数接受不定数量的参数,最典型的例子就是printf()和scanf()函数 基本原理: va_list-用于声明一个变量,该变量将引用参数列表 va_start-初始化va_list变量,使其指向可变参数列表的第一个参数 va_arg-访问参数列表哦中的下一个参数 va_end()-清理va_list变量\n可变参数函数的声明需要在参数列表中使用省略号(\u0026hellip;)\n优点:提供了极大的灵活性,可以创建接受不定数量参数的函数 缺点:缺乏类型安全检查,容易导致运行时错误\nHEX数据包 image.png\n问题一:包头包尾和数据载荷重复 解决方法:1.限制载荷数据的范围 2.如果无法限制包头包尾重复,就尽量 使用固定长度 的数据包 3.增加包头包尾的数量,并且尽量呈现出载荷数据出现不了的状态 问题二:包头包尾并不是 全部都需要的， 1.例如可以只需要包头,受够四个标志位结束,但是这样载荷和包头重复的问题会更 严重一些 问题三:固定包长和可变包长的选择问题 1.如果你的载荷会出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避 免接收错误 2.如果载荷不会和包头包尾重复可以选择可变包长 问题四:各种数据转换位字节流的问题 1.用一个uint8_t的指针指向它，把他们当作一个字节数组发送就行了\n文本数据包接收 image.png\n数据通过编码和译码的形式进行传输,本质还是字节传输 纯文本数据包通常不担心包头,包尾与数据内容重复的问题,因为他们适应分隔符来标记字段和记录 边界,比如空格，逗号,换行符\nHEX数据包接收 image.png\nHEX数据包和文本数据包的优缺点 HEX数据包: 优点: 1.极高的传输效率 2.极快的解析速度 3.结构紧凑且明确 缺点: 1.极度不直观 2.缺乏灵活性 3.存在平台兼容性问题 4.需要复杂的状态机处理\n文本数据包: 优点: 1.人类刻度,极易调试 2.天然跨平台兼容 3.非常灵活 4.实现简单 缺点: 1.传输效率极低 2.解析开销大,速度慢 3.数据精度可能丢失 4.仍需处理边界问题\n上位机和下位机 下位机就像​“四肢”和“感官”​​：它直接接触物理世界，负责执行具体的动作（如控制电机转动、点亮LED灯）和采集数据（如读取温度、检测按键）\n上位机就像​“大脑”​​：它不直接干活，而是负责指挥、决策和显示。它接收下位机传来的数据，进行分析、计算、存储，并以图形化界面（GUI）等方式展示给用户，同时用户也可以通过它向下位机发送控制命令。\n状态机 一个能记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移 1.先根据项目要求定义状态,画几个圈 2.然后考虑好各个状态在什么情况下会进行转移,如何转移，画好线和转移条件 3.最后根据画好的 图来进行编程\n使用杜邦线代替按键 按键的本质是控制单片机GPIO引脚在高电平和低电平之间切换.我们用杜邦线手动练剑不同的线路,就可以模拟这个动作\n关键:用一根杜邦线,将已经配置为上拉输入模式的GPIO引脚瞬间与GND(低电平)短接\n同步和异步 同步: 操作按顺序执行,必须等待前一个操作完成后才能开始下一个操作 异步: 操作发起后立即返回,不等待结果,通过回调/事件/中断等方式在完成后通知\n中断和阻塞的区别 中断: 中断是一种由硬件（或特定软件指令）触发的信号，它迫使CPU暂停当前正在执行的指令序列，转而去执行一个特定的、被称为中断处理程序的函数，处理完毕后再返回原来的地方继续执行。 来源: 由硬件设备产生 异步性: 中断的发生是不可预测的,它可以在指令执行的任何时刻发生,与CPU当前正在执行 的代码无关 目的: 为了响应外部事件,提高CPU和硬件的利用率 上下文: 中断处理程序运行在中断上下文中.这是一个非常特殊的环境,不能休眠,不能调用可 能引起的阻塞的函数(因为它是打断正常流程的,没有进程概念) 层级: 属于底层机制,是操作系统得以运行和设备管理的基石\n阻塞: 阻塞是指一个进程或线程因为等待某个事件（如资源可用、I/O操作完成）而主动停止执行，并让出CPU的行为。操作系统会将其状态置为“阻塞态”，并调度其他就绪的进程来运行。 来源: 由正在运行的进程/线程自己通过系统调用发起的 同步性: 可预见的,主动的 目的: 为了高效的等待.与其进程占着CPU空转,不如让它睡觉,把CPU让给其他需要的进程 上下文: 阻塞发生在进程上下文中.这是程序正常的执行环境 层级: 高层抽象,是应用程序开发中常见的概念,用于多任务和资源共享\n进程和线程的区别 进程: 基本定义: 资源分配的基本单元 资源拥有: 拥有独立的地址空间和系统资源 独立性: 独立性高.一个进程崩溃后,在保护模式下不会影响其他进程 开销: 创建,销毁,切换开销大.需要为它分配独立的系统资源 通信机制: 复杂,需要IPV机制:如管道,消息队列,共享内存,套接字 包含关系: 一个进程可以包含多个线程\n线程: 基本定义: CPU调度的基本单元 资源拥有: 共享其所属进程的地址空间和资源 独立性: 独立性低,一个线程崩溃会导致整个进程崩溃,从而影响同进程下的所有其他线程 开销: 创建,销毁,切换开销小,因为它们共享资源,只需保存少量寄存器装填和栈空间 通信机制: 进程间通信非常简单,因为它们共享全局变量,静态变量等内存空间,但需要同步 机制(互斥锁,信号量)来避免冲突 性能影响: 成本低,创建速度块,资源占用小,能极大提高程序的并发性能,但需要谨慎处理同 步问题,否则易产生bug(如死锁) 包含关系: 线程必须依赖于进程而存在,它是进程的一部分\n启动配置 image.png\n串口下载程序 1.硬件连接 2.配置Boot模式 要让芯片上电后不运行用户程序,而是直接进入内置的Bootloader,需要通过Boot引脚(BOOT0和BOOT1)来设置启动模式 (BOOT0引脚接高电平,BOOT1引脚接低电平) 3.进入BootLoader模式 4.使用软件烧录 5.恢复正常启动模式 (烧录完成后,先点击Disconnect断开连接,给目标板断电,将BOOT0引脚改回接低电平,重新上电,此时STN32将从主闪存Flash启动,运行你刚刚 烧录进去的新程序)\nSTM32一键下载电路 核心思想:利用USB转串口芯片的两个硬件流控制信号: RTS和DTS 通过软件控制这两个信号的电平变化,来模拟我们手动操作复位(RESET)和BooT模式(BOOT0)的动作\n复位 内容目前略 本质:强制将芯片内部几乎所有的重要寄存器和状态恢复到芯片设计时规定的默认值\nROM和RAM ROM(只读存储器) 非易失性:断电后,所有存储的内容都不会丢失 通常用于存储:需要永久或长期保存的数据 RAM(随机存取存储器) 易失性:断电后,里面的数据会立即丢失 通常用于存储:需要高速访问的临时数据\n选项字节 物理位置:它是芯片内部主Flash存储器中的一个特殊部分 内容:它由多对(互补的)16位字组成,用于提高可靠性.每个配置项都有对应的互补项,如两者不匹配,则意味着选项字节错误或未编程 特性:非易失性.一旦通过特殊工具烧写进去,断电后配置也不会丢失,下次上电自动生效\n配置内容: 1.读写保护 2.写保护 3.复位模式 4.看门狗 5.启动配置\nI2C通信 I2C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线 两根通信线:SCL(Serial Clock),SDA(Serial Data) 同步,半双工 带数据应答 支持总线挂载多设备(一主多从,多主多从)\n串口通信的硬件电路: 简单双向串口通信有两个通信线(发送端TX和接收端RX) TX和RX要交叉连接 当只需要单向的数据传输时,可以只接一根通信线 当电平标准不一致时,需要加电平转换芯片 image.png\n实现了读写寄存器就实现了对外挂设备的完全控制\n要求1: 删掉一根通信线,只能在同一根线上进行发送和接收,全双工 -\u0026gt; 半双工 要求2: 增加应答机制 要求3: 一根线上能同时接多个模块 要求4: 异步 -\u0026gt; 同步, 加一条时钟线\n硬件电路: 所有I2C设备的SCL连在一起,SDA连在一起 设备的SCL和SDA均配置成开漏输出模式 SCL和SDA各添加一个上拉电阻,阻值一般为4.7k欧姆左右\nI2C时序基本单元 起始条件: SCL高电平期间,SDA从高电平切换到低电平 终止条件: SCL高电平期间，SDA从低电平切换到高电平 image.png\n发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上(高位先行).然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节 image.png\n接收一个字节: SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL，主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)\n发送应答: 主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答 接收应答: 主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)\n指定地址写 对于指定设备(Slave Address), 在指定地址(Reg Address)下,写入指定数据(Data) image.png\n当前地址读: 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)\n指定地址读: 对于指定设备(Slave Address)，在指定地址(Reg Address)下,读取从机数据(Data)\nMPU6050简介 MPU6050是一个6轴姿态传感器,可以测量芯片自身X,Y,Z轴的加速度,角速度,通过数据融合,可进一步得到姿态角,常应用与平衡车,飞行器等需要自身姿态的场景 3轴加速度计(Accelerometer): 测量X,Y,Z轴的加速度 3轴陀螺仪传感器(Gyroscope): 测量X,Y,Z轴的角速度 补充: 3轴的磁场传感器 1轴的气压传感器 image.png\n加速度计具有静态稳定性,不具有动态稳定性 角速度积分 -\u0026gt; 角度(但是不会受物体运动的影响) -\u0026gt; 动态稳定,静态不稳定 加速度积分 -\u0026gt; 速度 -\u0026gt; 静态稳定,动态不稳定\n零漂: 物体静止时,角速度值会因为噪声无法完全归零,经过积分的不断累积,这个小噪声就会导致计算出来的角度产生缓慢的漂移(角速度积分得发哦的角度经不起时间的考验)\n角速度积分和加速度积分取长补短进行互补滤波，就能融合得到静态和动态都稳定的姿态角\nMPU6050参数 16位ADC采集传感器的模拟信号,量化范围: -32768~32767 加速度计满量程选择: +-2, +-4, +-8, +-16(g) 陀螺仪满量程选择: +- 250 , +- 500, +-1000, +-2000 (度/sec) 可配置的数字低通滤波器 可配置的时钟源 可配置的采样频率\nI2C从机地址: 1101000 (AD0=0) 1101001 (AD0=1)\n硬件电路 image.png\n复位 复位(Reset)是将STM32微控制器的内部状态强制恢复到一个已知的,确定的初始状态的过程,就像电脑的重启,但更彻底\u0026mdash;\u0026ndash;不仅软件重启,硬件状态也重置\n复位时到底发生了什么? 1.时钟系统重置: HSI（内部高速时钟）称为默认系统时钟 PLL,HSE（外部高速时钟）等时钟源被禁用 系统时钟(SYSCLK)切换到HSI AHB，APB总线时钟分频器重置位默认值\n2.CPU核心重置 (1)程序计数器(PC)被设置为复位向量地址 (2)CPU寄存器 (3)中断系统重置\n3.外设重置 (1)大多数外设寄存器恢复默认值 (2)DMA控制器停止,通道禁用 (3)所有挂起的终端标志被清除\n/* 硬件自动完成 */\n从复位向量获取栈指针（SP） → 设置MSP 从复位向量+4获取复位处理函数地址 → 设置PC /* 启动文件（startup_stm32fxxx.s） */ 3. 执行Reset_Handler: a. 调用SystemInit() // 时钟、Flash等待周期等初始化 b. 复制.data段到RAM // 初始化全局变量 c. 清零.bss段 // 清零未初始化全局变量 d. 设置堆栈边界（可选） e. 调用__libc_init_array() // C++全局对象构造函数 f. 调用main() // 用户程序入口\n/* 用户程序 */ 4. main()函数开始执行 a. HAL_Init() // HAL库初始化 b. SystemClock_Config() // 配置系统时钟 c. 外设初始化（GPIO、USART等） d. while(1)主循环\nI2C外设简介 STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成,起始终止条件生成,应答位收发,数据收发等功能,减轻CPU的负担 支持多主机模型 支持7位/10位地址模式 支持不同的通讯速度,标准速度(高达100kHz),快速(高达400kHz) 支持DMA 兼容SMBus协议 STM32F103C8T6硬件I2Cz资源: I2C1,I2C2\nSTM32寄存器: CR,DR,SR寄存器 CR寄存器 - 控制器(Control Register) 用于配置和控制外设哦的工作模式,参数和行为\n常见CR寄存位含义: EN 外设使能 TE 发送使能 RE 接收使能 MODE 工作模式 PS/PH 分频系数\nDR寄存器 - 数据寄存器(Data Register) 用于存放要发送或接收的数据\nSR寄存器 - 状态寄存器(Status Register) 反映外设的当前工作状态和事件标志 何时置1 TXE 发送缓冲区空 发送缓冲区可以接收新数据时 TC 发送完成 最后一个数据发送完成时 RXNE 接收缓冲区非空 接收到新数据时 ORE 过载错误 新数据到来时旧数据未读取 FE 帧错误 接收到的帧格式错误 RE 奇偶校验错误 奇偶校验失败\nI2C框图 image.png\nI2C基本结构 image.png\n主机发送 image.png\n主机接收 image.png\nSPI通信 SPI(Serial Peripheral Interface) 是由Motorola公司开发的一种通用数据总线 四根通信线: SCK(Serial Clock) , MOSI(Master Output Slave Input) , MISO(Master Input Slave Output) , SS(Slave Select) 同步,全双工 支持 总线挂在多设备(一主多从)\nSPI硬件电路 所有的SPI设备的SCK,MOSI,MISO分别连在一起 主机另外引出多条SS控制线,分别连接到各从机的SS引脚 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入 image.png\n移位示意图 image.png\nSPI时序基本单元 起始条件: SS从高电平切换到低电平 终止条件: SS从低电平切换到高电平 image.png\n模式0 CPOL：时钟极性 表示时钟信号在空闲状态的电平\nCPHA：时钟相位 表示数据采样的时钟沿\nimage.png\nW25Q64简介 W25Qxx系列是一种低成本,小型化,使用简单的非易失性存储器,常应用于数据存储,字库存储,固件程序存储等场景 存储介质: Nor Flash(闪存) image.png\nFlash操作注意事项 写入操作时: 写入操作前,必须先进行写使能 每个数据位只用由1改写为0，不能由0改写为1 写入数据前必须先擦除,擦除后,所有数据位变为1 擦除必须按最小擦除单元进行 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入 写入操作结束后,芯片进入忙状态,不响应新的读写操作 读取操作时: 直接调用读取时序,无需使能,无需额外操作,没有页限制 读取操作结束后不会进入忙状态,但不能在忙状态时读取\nSPI外设简介 STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成,数据收发等功能,减轻CPU的负担 可配置8位/16位数据帧,高位先行/低位先行 时钟频率: fpclk(2,4,8,16,32,64,128) 支持多主机模型,主或从操作 可精简为半双工/单工通信 支持DMA 兼容I2S协议\nSTM32F103C8T6硬件SPI资源: SPI1,SPI2\nSPI框图 image.png\nSPI框图 image.png\n非连续传输 每个数据传输单元之间都有间隔 片选信号在每个字节传输后拉高再拉低 时钟信号在每个字节传输后停止\n速度: 较慢 功耗: 较高 时序要求: 宽松 抗干扰: 较好\n连续传输 多个数据传输单元连续发送,无间隔 片选信号在整个传输期间保持有效 时钟信号连续产生,数据流不间断\n速度: 快 功耗: 较低 时序要求: 严格 抗干扰: 较差\nUnix时间戳 image.png\nUTC/GMT image.png\n时间戳转换 image.png\nimage.png\nBKP简介 BKP(Backip Registers)备份寄存器 BKP可用于存储用户应用程序数据.当VDD(2.03.6)电源被切断，他们仍然由VBAT(1.8V3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位,他们也不会被复位 TAMPER引脚产生的侵入事件将所有备份寄存器内容清除 RTC引脚输出RTC校准时钟,RTC闹钟脉冲或秒脉冲 存储RTC时钟校准寄存器 用户数据存储容量: 20字节(中容量和小容量) / 84字节 (大容量和互连型)\nBKP基本结构 image.png\nRTC简介 RTC是一个独立的定时器,可为系统提供时钟和日历的功能 RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.03.6V)断电后可借助VBAT(1.8V3.6V)供电继续走时 32位的可编程计数器,可对应Unix时间戳的秒计数器 20位的可编程预分频器，可适配不同频率的输入时钟\n可选择三种RTC时钟源 HES时钟除以128(通常为8MHz/128) LSE振荡器时钟(通常为32.768KHz) LSI振荡器时钟(40KHz)\nRTC框图 image.png\nRTC基本结构 image.png\nRTC操作注意事项 image.png\nPWR简介 PWR(Power Control) 电源控制 PWR负管理STM32内部的电源供电部分,可以实现可编程电压检测器和低功耗模式的功能 可编程电压检测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阈值以下或上升到PVD阈值之上时,PVD会触发终端,用于执行紧急关闭任务 低功耗模式包括睡眠模式(Sleep),停机模式(Stop)和待机模式(Standby),降低STM32的功耗,延长设备使用时间\n电源框图 image.png\n低功耗模式 image.png\n模式选择 image.png\n睡眠模式 image.png\n停止模式 image.png\n待机模式 image.png\nRCC时钟树 image.png\nWDG简介 WDG(Watchdog)看门狗 看门狗可以监控程序的运行状态,当程序因为设计漏洞,硬件故障,电磁干扰等原因,出现卡死或跑飞现象时,看门狗能及时复位程序,避免程序陷入长时间的罢工状态,保证系统的可靠性和安全性 看门狗本质是一个定时器,当指定时间范围内,程序没有执行喂狗(重置计数器)操作时,看门狗硬件电路就会自动产生复位信号 STM32内置两个看门狗 独立看门狗(IWDG): 独立工作,对事件精度要求低 窗口看门狗(WWDG): 要求看门狗在精确计时窗口起作用\nIWDG框图 image.png\n定时中断基本结构 image.png\nIWDG键寄存器 键寄存器本质是控制寄存器,用于控制硬件电路的工作 在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来 代替控制寄存器写入一位的功能,以降低硬件电路受到干扰的概率\nimage.png\nIWDG超时时间 image.png\nWWDG工作特性 递减计数器T[6:0]的值小于0x40时,WWDG产生复位 递减计数器T[6:0]在窗口W[6:0]外被重新装载时,WWDG产生复位 递减计数器T[6:0]等于0x40时可以产生早期唤醒 中断(EWI)，用于重装载计数器以避免WWDG复位 定期写入WWDG_CR寄存器(喂狗)以避免WWDG复位\nimage.png\nWWDG框图 image.png\nWWDG超时时间 image.png\nIWDG和WWDG对比 image.png\n补充: 预分频器: 作用: 对定时器的输入时钟进行分频,从而改变计数器的计数频率 预分频器决定了计数器跑多快\n重装载寄存器： 作用: 定义自动重装值,即计数器计数的周期 重装载寄存器决定了计数器“数到多少为止\n状态寄存器： 作用: 指示定时器当前的各种状态,特别是中断事件的状态 状态寄存器是告知程序员“定时器发生了什么情况”的信号灯。\n两次喂狗间隔必须大于看门狗计数的最小更新时间: 在初始化阶段，如果我们在启动看门狗后立即喂狗，然后很快进入主循环，主循环中又很快喂狗，但第一次喂狗可能因为LSI不稳定而没有成功，而第二次喂狗虽然成功，但此时计数器已经递减了很多，导致第二次喂狗和第三次喂狗之间的时间间隔可能超过了看门狗的超时时间（因为从启动到第二次喂狗的时间很长，而第二次喂狗后，计数器重新加载，但程序执行到第三次喂狗的时间间隔可能超过了设定值）。\nFLASH简介 STM32F1系列的FLASH包含程序存储器,系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程 读写FLASH的用途: 利用程序存储器的剩余空间来保存掉电不丢失的用户数据 通过在程序中编程(IAP),实现程序的自我更新 在线编程,用于更新程序存储器的全部内容,它通过JTAG,SWD协议或系统加载程序(Bootloader)下载程序 在程序中编程可以使用微控制器支持的任意种通信接口下载程序\n存储器映像 image.png\n闪存模块组织 image.png\nFLASH基本结构 image.png\nFLASH解锁 FPEC共有三个键值: RDPRT键 = 0x000000A5 KEY1 = 0x45670123 KEY2 = 0xCDEF89AB 解锁: 复位后,FPEC被保护,不能写入FLASH_CR 在FLASH_KEYR先写入KEY1，再写入KEY2，解锁 错误的操作序列会在下次复位前锁死FPEC和FLASH_CR 加锁: 设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR\n使用指针访问存储器 image.png\n程序存储器全擦除 image.png\n程序存储器页擦除 image.png\n程序存储器编程 image.png\n选项字节 image.png\n选项字节擦除 image.png\n器件电子签名 image.png\n字,半字,字节 字: 计算机内存的基本单位,由8位(bit)组成 表示范围: 0 ~ 255(无符号) , 或-128 ~ 127(有符号) 典型C语言类型: uint8_t,char\nuint8_t data_byte = 0xFF; // 1字节数据\n半字: 由16位(2字节)组成 表示范围: 0 ~ 65535（无符号），或 -32768 ~ 32767（有符号)\nuint16_t data_halfword = 0x1234; // 2字节数据 典型C语言类型: uint16_t,short\n字: 在ARM架构中,1字 = 32位(4字节) （注意：在x86架构中“字”通常为16位，但在ARM中固定为32位。）\n表示范围: 0 ~ 4294967295（无符号），或 -2147483648 ~ 2147483647（有符号）。\nuint32_t data_word = 0xABCD1234; // 4字节数据 典型C语言类型: uint32_t,int\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/stm32/","summary":"\u003ch1 id=\"stm32\"\u003estm32\u003c/h1\u003e\n\u003ch2 id=\"stm32基础知识\"\u003estm32基础知识\u003c/h2\u003e\n\u003cp\u003estm32开发方式\n基于寄存器的方式\n基于标准库\n和基于HAL库的方式\u003c/p\u003e\n\u003ch2 id=\"新建工程\"\u003e新建工程\u003c/h2\u003e\n\u003cp\u003e.建立工程文件夹,Keil中新建工程,选择型号\n.工程文件夹中建立Start，Library,User等文件夹，复制固件库里面的文件到工程文件夹\n.工程里对应建立Start,Library,User等同名称的分组,然后文件夹内的文件添加到工程分组里\n.工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹\n.工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER\n.工程选项,Debug,下拉列表选择对应调试器,Settings,Flash,Download里勾选Reset and Run\u003c/p\u003e","title":"stm32"},{"content":"Vscode命令行 单文件编译 g++ -o 输出文件名 源文件.cpp 指定输出可执行文件名为\u0026hellip;: 要编译的源文件名 或者 g++ 源文件.cpp -o 输出文件名 将要编译的源文件: 指定输出可执行文件名\n调试支持 g++ -g -o hello hello.cpp # 便于后续使用GDB调试4,9\n运行命令 Windows: .\\输出文件名.exe # 例如 .\\hello.exe1,2 Linux/macOS系统​： ./输出文件名 # 例如 ./hello1,5\n编译与运行组合命令 g++ -o hello hello.cpp \u0026amp;\u0026amp; .\\hello.exe # Windows g++ -o hello hello.cpp \u0026amp;\u0026amp; ./hello # Linux/macOS8\n多文件编译 同时编译多个源文件 g++ -o program main.cpp utils.cpp helper.cpp # 将多个.cpp文件编译为单一可执行文件8 分布编译(适合大型项目) g++ -c utils.cpp # 生成 utils.o g++ -c helper.cpp # 生成 helper.o g++ -o program main.cpp utils.o helper.o # 链接所有对象文件6\n调试: 断点调试 单步跳过: 执行当前行代码.如果该行包含函数调用,不进入函数内部,直接得到结果并跳到下一行 F10 单步进入: 执行当前行代码,如果该行包含函数调用,会进入该函数内部,并暂停在函数的第一行 F11 单步跳出: 立即执行完当前函数体内剩余的所有代码,并跳出到该函数的下一行语句暂停处 Shift + F11\n这两步一般环境配置好过后直接略过就好了\n方式一: 使用vscode进行图形化调试 1.安装插件​：确保已安装 C/C++ 和 ROS (可选，但方便) 插件 2.配置调试环境 (launch.json)​： 在VSCode中打开你的ROS工作空间。 切换到“运行和调试”视图，点击“创建一个launch.json文件”。 选择 C++ (GDB/LLDB)。 替换或修改配置为如下示例（​重点修改 program路径​）： 3.设置断点 4.启动调试： 确保roscore已运行: 在一个终端中手动执行roscore 在vscode中选择你刚配置好的调试配置,按F5启动\n方式二: 使用GDB命令行进行调试(更灵活底层) 1.直接启动调试 直接通过gdb启动节点 gdb \u0026ndash;args /path/to/your/ros/node 2.附加到已运行的进程(非常适合调试已崩溃或卡死的节点)\n// 首先,找到你要调试的节点的进程ID(PID) ps aux | grep your_node_name // 然后,使用gdb附加到该进程 sudo gdb -p 5 # 可能需要sudo权限\n附加后,程序会暂停,你可以设置断点然后输入continue让程序继续 确保程序编译时包含调试信息 3.在.launch文件,在标签中添加launch-prefix属性,这样每次通过roslaunch启动都会自动进入调试模式\n在CMakeLists.txt中确保有 set(CMAKE_BUILD_TYPE Debug)\n或者 set(CMAKE_CXX_FLAGS \u0026ldquo;${CMAKE_CXX_FLAGS} -g\u0026rdquo;)\n重新编译 cd /home/rm/ws_glut_vison catkin_make -DCMAKE_BUILD_TYPE=Debug\n三: 常用调试命令(GDB) 命令1.: break 简写: b 用途: 设置断点(如b main.cpp: 25 myFunction) VSCode中的等效操作: 在行号前点击 示例：\n启动gdb后，设置关键断点 (gdb) break OutpostObserver::update (gdb) break OutpostObserver::predict (gdb) break OutpostObserver::getPredictiveMeasurement (gdb) break OutpostObserver::getMeasurementPD\n运行程序 (gdb) run\n命令2: run 简写: r\n用途: 启动或重新启动程序 VSCode中的等效操作： F5继续\n命令3: next 简写: n 用途: 执行下一行(不进入函数内部) VSCode中的等效操作：F10\n命令4: step 简写: s 用途: 执行下一行(进入函数内部) VSCode中的等效操作: F11\n命令5: print 简写: p 用途: 打印变量的值 VScode: 在调试窗口查看或悬停\n命令6: backtrace 简写: bt 用途: 显示当前的函数调用栈 VSCode: 查看\u0026quot;调用堆栈\u0026quot;窗口 示例:\n查看崩溃时的调用栈 (gdb) bt (gdb) bt full # 显示完整栈帧和局部变量\n查看具体的崩溃位置 (gdb) frame 0 # 查看最顶层的帧\n打印相关变量的值 (gdb) print X_.rows() (gdb) print X_.cols() (gdb) print P_.rows() (gdb) print P_.cols() (gdb) print H.rows() (gdb) print H.cols()\n命令7: info breakpoints 简写: i b 用途: 列出所有断点 VSCode: 在”断点“窗口查看\n命令8: quit 简写: q 用途: 退出GDB VSCode: 停止调试按钮\n调试流程 1.配置编译任务 按ctrl+shift+p,输入\u0026quot;Tasks: Configure Task\u0026quot; , 选择\u0026quot;Create tasks.json file from templates\u0026quot;,选择\u0026quot;Others\u0026quot;或\u0026quot;C/C++ g++ buikd active file\u0026quot; ,这回生成一个tasks.json,然后问ai生成一个调试文件\n2.配置调试设置(launch.json) 按F5，选择(GDB/LDB),然后选择\u0026quot;g++ build and debug activate file\u0026quot;\n3.开始断点调试 1）.设置断点: 在代码行号左侧单击,出现红点即为断点 2）.启动调试: 按F5 3）.调试操作 4）.查看状态\n4.高级调试技巧 条件断点\n在可能越界的矩阵访问处设置条件断点 (gdb) break OutpostObserver.cpp:123 if row \u0026gt;= rows() || col \u0026gt;= cols() 日志断点 函数断点\n常见调试方法 1.打印语句 (快速查看变量值,执行流程) -\u0026gt; 简单逻辑排查,快速确认执行到哪,变量是什么值\nstd::cout \u0026laquo; \u0026ldquo;x = \u0026quot; \u0026laquo; x \u0026laquo; std::endl; 2.断言 (检查假设条件是否成立,捕捉非法状态) -\u0026gt; 检查函数参数有效性,数组越界,不可能出现的状态,调试版本常用\nassert(ptr != nullptr) 3.断点 暂停程序执行,检查现场 -\u0026gt; 任何需要详细检查上下文,调用栈,不可能的状态等,调试版本常用\n在 IDE 中点击行号左侧；GDB: break mainbreak filename:lineno 4.单步调试: 精细控制执行过程,观察每一步变化 -\u0026gt; 逻辑复杂,需要逐行分析;理解代码执行流程;跟踪进入函数内部\nGDB: next(步过), step(步入); IDE: F10, F11 5.查看变量/内存: 检查变量值,指针指向,内存数据 -\u0026gt; 验证数据是否正确,排查内存错误(如越界,坏指针)\nGDB: print variable, print *ptr@10; IDE: 悬停观察，监视(Watch)窗口 6.日志调试: 记录程序运行信息,追踪线上问题 -\u0026gt; 复杂逻辑跟踪,循环体内,无法直接调试的环境,嵌入式\n7.内存检查工具 -\u0026gt; 检查内存泄漏,越界访问等内存错误 -\u0026gt; 程序运行崩溃,怀疑有内存相关错误时\nmake编译 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/vscode%E5%91%BD%E4%BB%A4%E8%A1%8C/","summary":"\u003ch1 id=\"vscode命令行\"\u003eVscode命令行\u003c/h1\u003e\n\u003ch3 id=\"单文件编译\"\u003e单文件编译\u003c/h3\u003e\n\u003cp\u003eg++ -o 输出文件名 源文件.cpp\n指定输出可执行文件名为\u0026hellip;: 要编译的源文件名\n或者 g++ 源文件.cpp -o 输出文件名\n将要编译的源文件: 指定输出可执行文件名\u003c/p\u003e","title":"Vscode命令行"},{"content":"xv6 for RISC-V jyy xv6导读 基础知识 应用视角的操作系统: 对象 + API 把操作系统当提供服务的黑盒子\nxv6:Unix v6的现代\u0026quot;克隆\u0026quot;\n文件关系 源文件 (.c/.cpp) → 编译 → 目标文件 (.o) → 链接 → 可执行文件 ↓ 依赖文件 (.d) → 告诉构建系统何时需要重新编译\n1.源文件 (Source Files) .c - C语言源文件 .cpp/.cc - C++源文件 .h/.hpp - 头文件\n2.目标文件 (Object Files) - .o 文件 编译后的中间二进制文件\n依赖文件 (Dependency Files) - .d 文件 记录编译依赖关系的Makefile片段 编译时同时生成依赖文件 gcc -c main.c -o main.o -MMD -MF main.d\n程序的执行(状态变化序列)有时比代码(状态机)更容易理解 使用工具进行“ tracing ”（追踪） 理解并发Bug 建立确定性心智模型\n阅读makefile文件可以比较好的帮助理解代码\nxv6文件结构 kernel/目录 - 操作系统内核 包含了xv6操作系统的所有内核代码,这是操作系统的核心部分\nmkfs/目录 - 文件系统创建工具 包含创建xv6系统镜像的工具\nuser/目录 - 用户空间程序 包含所有在用户空间运行的程序\nxv6 22个系统调用 image.png\nxv6进程的地址空间 image.png\nxv6系统调用 image.png\nTrampoline(跳板) image.png\n程序的状态 image.png\n虚拟化: 状态的管理 image.png\n状态的封存：Trivial的操作系统实现 image.png\n状态的封存: 体系结构相关的处理 image.png\n再次调试系统调用 image.png\n调用usertrap()后的系统状态 image.png\n小结: 状态机的封存 image.png\ninvariant状态 invariant(不变式)在计算机科学中指在程序的某些关键点上必须始终为真的条件或约束 状态机被封存\n总结 image.png\n操作系统是中断处理程序 操作系统是状态机的管理者\n缓存系统bio.c/bio.h LRU 最近使用的（最新） 最久未用的（最旧） ↓ ↓ [head] ←→ [bufA] ←→ [bufB] ←→ [bufC] ←→ ... ←→ [bufZ] ←→ [head] ↑ 最新使用 最旧使用 ↑ └─────────────────────────────────────────────────────────────┘ 循环连接 xv6的LRU维护特点: 惰性更新 + 只在释放时移动 核心原则： 缓存命中时：不移动缓冲区在LRU中的位置 缓存未命中时：从链表尾部查找空闲缓冲区 缓冲区释放时：只有当引用计数归零 (refcnt == 0) 时才移动到链表头部\n传统LRU 假设: 最近被访问的数据，在不久的将来很可能再次被访问 很久没有被访问的数据，未来也不太可能被访问\n每次访问缓存项时,都会将其移动到链表的头部 当缓存满的时候删除尾部节点(最久未使用)\n传统LRU -\u0026gt; 来自leetcode\n缓存系统 bcache（缓存系统） ╔═══════════════════════════════════╗ ║ struct spinlock lock; ║ ← 保护整个系统 ║ ║ ║ struct buf buf[NBUF]; ║ ← 缓冲区数组 ║ ┌─────────┐ ┌─────────┐ ║ ║ │ buf[0] │ │ buf[1] │ ... ║ ← 每个都是struct buf ║ │ valid=1 │ │ valid=0 │ ║ ║ │ dev=1 │ │ dev=1 │ ║ ║ │ blk=123 │ │ blk=456 │ ║ ║ │ data[] │ │ data[] │ ║ ║ └─────────┘ └─────────┘ ║ ║ ║ ║ struct buf head; ║ ← LRU链表头 ║ prev → buf[28] ║ ║ next → buf[1] ║ ╚═══════════════════════════════════╝ ↑ ↑ ↑ 通过prev/next指针连接所有缓冲区 形成LRU双向循环链表 睡眠锁sleeplock.h/sleeplock.c 补充: 也叫条件锁或长时间持有的锁\n// Long-term locks for processes struct sleeplock { uint locked; // Is the lock held? struct spinlock lk; // spinlock protecting this sleep lock\n// For debugging: char *name; // Name of lock. int pid; // Process holding lock };\n// Sleeping locks\n#include \u0026ldquo;types.h\u0026rdquo; #include \u0026ldquo;riscv.h\u0026rdquo; #include \u0026ldquo;defs.h\u0026rdquo; #include \u0026ldquo;param.h\u0026rdquo; #include \u0026ldquo;memlayout.h\u0026rdquo; #include \u0026ldquo;spinlock.h\u0026rdquo; #include \u0026ldquo;proc.h\u0026rdquo; #include \u0026ldquo;sleeplock.h\u0026rdquo;\nvoid initsleeplock(struct sleeplock *lk, char *name) { initlock(\u0026amp;lk-\u0026gt;lk, \u0026ldquo;sleep lock\u0026rdquo;); lk-\u0026gt;name = name; lk-\u0026gt;locked = 0; lk-\u0026gt;pid = 0; }\nvoid acquiresleep(struct sleeplock *lk) { acquire(\u0026amp;lk-\u0026gt;lk); while (lk-\u0026gt;locked) { sleep(lk, \u0026amp;lk-\u0026gt;lk); } lk-\u0026gt;locked = 1; lk-\u0026gt;pid = myproc()-\u0026gt;pid; release(\u0026amp;lk-\u0026gt;lk); }\nvoid releasesleep(struct sleeplock *lk) { acquire(\u0026amp;lk-\u0026gt;lk); lk-\u0026gt;locked = 0; lk-\u0026gt;pid = 0; wakeup(lk); release(\u0026amp;lk-\u0026gt;lk); }\nint holdingsleep(struct sleeplock *lk) { int r;\nacquire(\u0026amp;lk-\u0026gt;lk); r = lk-\u0026gt;locked \u0026amp;\u0026amp; (lk-\u0026gt;pid == myproc()-\u0026gt;pid); release(\u0026amp;lk-\u0026gt;lk); return r; }\n自旋锁spinlock.c/spinlock.h // Mutual exclusion lock. struct spinlock { uint locked; // Is the lock held?\n// For debugging: char *name; // Name of lock. struct cpu *cpu; // The cpu holding the lock. };\n// Mutual exclusion spin locks.\n#include \u0026ldquo;types.h\u0026rdquo; #include \u0026ldquo;param.h\u0026rdquo; #include \u0026ldquo;memlayout.h\u0026rdquo; #include \u0026ldquo;spinlock.h\u0026rdquo; #include \u0026ldquo;riscv.h\u0026rdquo; #include \u0026ldquo;proc.h\u0026rdquo; #include \u0026ldquo;defs.h\u0026rdquo;\nvoid initlock(struct spinlock *lk, char *name) { lk-\u0026gt;name = name; lk-\u0026gt;locked = 0; lk-\u0026gt;cpu = 0; }\n// Acquire the lock. // Loops (spins) until the lock is acquired. void acquire(struct spinlock *lk) { push_off(); // disable interrupts to avoid deadlock. if(holding(lk)) panic(\u0026ldquo;acquire\u0026rdquo;);\n// On RISC-V, sync_lock_test_and_set turns into an atomic swap: // a5 = 1 // s1 = \u0026amp;lk-\u0026gt;locked // amoswap.w.aq a5, a5, (s1) while(__sync_lock_test_and_set(\u0026amp;lk-\u0026gt;locked, 1) != 0) ;\n// Tell the C compiler and the processor to not move loads or stores // past this point, to ensure that the critical section\u0026rsquo;s memory // references happen strictly after the lock is acquired. // On RISC-V, this emits a fence instruction. __sync_synchronize();\n// Record info about lock acquisition for holding() and debugging. lk-\u0026gt;cpu = mycpu(); }\n// Release the lock. void release(struct spinlock *lk) { if(!holding(lk)) panic(\u0026ldquo;release\u0026rdquo;);\nlk-\u0026gt;cpu = 0;\n// Tell the C compiler and the CPU to not move loads or stores // past this point, to ensure that all the stores in the critical // section are visible to other CPUs before the lock is released, // and that loads in the critical section occur strictly before // the lock is released. // On RISC-V, this emits a fence instruction. __sync_synchronize();\n// Release the lock, equivalent to lk-\u0026gt;locked = 0. // This code doesn\u0026rsquo;t use a C assignment, since the C standard // implies that an assignment might be implemented with // multiple store instructions. // On RISC-V, sync_lock_release turns into an atomic swap: // s1 = \u0026amp;lk-\u0026gt;locked // amoswap.w zero, zero, (s1) __sync_lock_release(\u0026amp;lk-\u0026gt;locked);\npop_off(); }\n// Check whether this cpu is holding the lock. // Interrupts must be off. int holding(struct spinlock *lk) { int r; r = (lk-\u0026gt;locked \u0026amp;\u0026amp; lk-\u0026gt;cpu == mycpu()); return r; }\n// push_off/pop_off are like intr_off()/intr_on() except that they are matched: // it takes two pop_off()s to undo two push_off()s. Also, if interrupts // are initially off, then push_off, pop_off leaves them off.\nvoid push_off(void) { int old = intr_get();\n// disable interrupts to prevent an involuntary context // switch while using mycpu(). intr_off();\nif(mycpu()-\u0026gt;noff == 0) mycpu()-\u0026gt;intena = old; mycpu()-\u0026gt;noff += 1; }\nvoid pop_off(void) { struct cpu *c = mycpu(); if(intr_get()) panic(\u0026ldquo;pop_off - interruptible\u0026rdquo;); if(c-\u0026gt;noff \u0026lt; 1) panic(\u0026ldquo;pop_off\u0026rdquo;); c-\u0026gt;noff -= 1; if(c-\u0026gt;noff == 0 \u0026amp;\u0026amp; c-\u0026gt;intena) intr_on(); }\n自旋锁和睡眠锁对比 ELF/elf.h xv6引导加载器(Boot Loader)/bootmain.c 加载内核: 从硬盘读取 xv6 内核（ELF 格式的可执行文件） 内存布局: 将内核的各部分放到正确内存位置 转交控制权: 跳转到内核的入口点，启动操作系统\n控制台/console.c 环形缓冲区 -\u0026gt; 循环队列 环形缓冲区是一种循环队列数据结构,特别适合处理生产者-消费者场景的数据流 直线缓冲区: 满了就得停，或者移动所有数据 环形缓冲区: 跑到终点后回到起点,可以无限循环使用 image.png\nimage.png\nimage.png\n两个指针之间应该空一个空格 不然WriteIndex和readIndex重合的话就会分不清楚缓冲区是满了还是空了\nxv6的控制台缓冲区是一个支持行编辑的生产者-消费者环形缓冲区: 生产者(consoleintr): 接收键盘中断,写入字符 消费者(consoleread): 响应read系统调用,读取字符 三个指针: 实现复杂的行编辑功能(退格,删除行等)\n串口UART/uart.c UART和USART的区别 UART: 通用异步收发器 功能: 仅支持异步串行通信 特点: 没有时钟线,依赖双方约定的波特率\nUSART: 通用同步/异步收发器 功能: 支持同步和异步两种通信模式 特点: 可以配置为同步模式(有时钟线)或异步模式\n管道/pipe.c 环形缓冲区: image.png\n进程proc.c/proc.h 文件日志系统/log.c 解决: 崩溃恢复(Crash Recovery) 不要直接修改真正的磁盘数据块。先把你打算做的修改.全部写到一个叫做\u0026quot;日志区\u0026quot;的专用地方。只有当日志区完整记录了所有修改后,才把它们搬运到真正的目的地.\n这样，如果断电： 断在日志写完之前：重启后，系统发现日志不完整，直接丢弃。就像操作从未发生过。文件系统完好。 断在日志写完之后：重启后，系统发现日志里有完整的操作记录，于是重新执行这些操作（Replay）。文件系统完好。\n内核主函数/main.c RISC-V 平台级中断控制器（PLIC）的驱动程序/plic.c PLIC -\u0026gt; Platform Level Interrupt Controller\n中断是一种硬件通知机制，允许外部设备或内部异常打断处理器的正常执行流程，让处理器立即处理紧急事件。\n中断的类型: 外部中断（硬件中断） 内部中断（异常/陷阱）\n系统调用/syscall.c/syscall.h 进程相关系统调用/sysproc.c 中断和异常处理/trap.c 用户程序执行ecall指令 ↓ CPU设置stvec寄存器指向uservec（在trampoline中） ↓ CPU跳转到uservec（汇编代码） ↓ uservec保存用户寄存器到trapframe ↓ uservec设置内核栈，跳转到usertrap（C代码） ↓ usertrap调用syscall()处理系统调用 ↓ prepare_return准备返回用户空间 ↓ userret（在trampoline中）恢复用户寄存器 ↓ 返回用户程序继续执行\n文件系统/sysfile.c 虚拟内存管理/vm.c/vm.h xv6: a simple Unix-like teaching operating system xv6在RISC-V架构与x86架构的区别 trampoline.s/蹦床代码 trampoline是一段特殊的汇编代码,位于用户地址空间和内核地址空间的相同虚拟地址处\n主要功能: 1.在用户态和内核态之间安全地切换 2.保存和恢复用户上下文 3.切换页表 4.跳转到内核陷阱处理程序\n共享映射: trampoline页面在内核页表和所有用户页表中都映射到相同的虚拟地址\n（TRAMPOLINE = 0x3ffffff000）\n权限设置： 在用户页表中,trampoline页面被标记为可执行但不可写;在内核页表中,可读可写\n位置固定: 总是位于地址空间的最高页\nxv6的系统调用实现: image.png\ntrampoline（蹦床）和trapframe（陷阱帧）是处理用户态和内核态之间切换的关键机制。它们共同协作以实现安全的上下文切换和系统调用处理。\n协作流程: 1.用户程序通过ecall进入内核 2.trampoline保存用户上下文到trapframe 3.内核处理系统调用/陷阱 4.trampoline从trapframe恢复用户上下文 5.返回用户程序继续执行\ntrampoline.s # # low-level code to handle traps from user space into # the kernel, and returns from kernel to user. # # the kernel maps the page holding this code # at the same virtual address (TRAMPOLINE) # in user and kernel space so that it continues # to work when it switches page tables. # kernel.ld causes this code to start at # a page boundary. #\n#include \u0026ldquo;riscv.h\u0026rdquo; #include \u0026ldquo;memlayout.h\u0026rdquo;\n.section trampsec .globl trampoline .globl usertrap trampoline: .align 4 .globl uservec uservec: # # trap.c sets stvec to point here, so # traps from user space start here, # in supervisor mode, but with a # user page table. #\n# save user a0 in sscratch so # a0 can be used to get at TRAPFRAME. csrw sscratch, a0 # each process has a separate p-\u0026gt;trapframe memory area, # but it's mapped to the same virtual address # (TRAPFRAME) in every process's user page table. li a0, TRAPFRAME # save the user registers in TRAPFRAME sd ra, 40(a0) sd sp, 48(a0) sd gp, 56(a0) sd tp, 64(a0) sd t0, 72(a0) sd t1, 80(a0) sd t2, 88(a0) sd s0, 96(a0) sd s1, 104(a0) sd a1, 120(a0) sd a2, 128(a0) sd a3, 136(a0) sd a4, 144(a0) sd a5, 152(a0) sd a6, 160(a0) sd a7, 168(a0) sd s2, 176(a0) sd s3, 184(a0) sd s4, 192(a0) sd s5, 200(a0) sd s6, 208(a0) sd s7, 216(a0) sd s8, 224(a0) sd s9, 232(a0) sd s10, 240(a0) sd s11, 248(a0) sd t3, 256(a0) sd t4, 264(a0) sd t5, 272(a0) sd t6, 280(a0) # save the user a0 in p-\u0026gt;trapframe-\u0026gt;a0 csrr t0, sscratch sd t0, 112(a0) # initialize kernel stack pointer, from p-\u0026gt;trapframe-\u0026gt;kernel_sp ld sp, 8(a0) # make tp hold the current hartid, from p-\u0026gt;trapframe-\u0026gt;kernel_hartid ld tp, 32(a0) # load the address of usertrap(), from p-\u0026gt;trapframe-\u0026gt;kernel_trap ld t0, 16(a0) # fetch the kernel page table address, from p-\u0026gt;trapframe-\u0026gt;kernel_satp. ld t1, 0(a0) # wait for any previous memory operations to complete, so that # they use the user page table. sfence.vma zero, zero # install the kernel page table. csrw satp, t1 # flush now-stale user entries from the TLB. sfence.vma zero, zero # call usertrap() jalr t0 .globl userret userret: # usertrap() returns here, with user satp in a0. # return from kernel to user.\n# switch to the user page table. sfence.vma zero, zero csrw satp, a0 sfence.vma zero, zero li a0, TRAPFRAME # restore all but a0 from TRAPFRAME ld ra, 40(a0) ld sp, 48(a0) ld gp, 56(a0) ld tp, 64(a0) ld t0, 72(a0) ld t1, 80(a0) ld t2, 88(a0) ld s0, 96(a0) ld s1, 104(a0) ld a1, 120(a0) ld a2, 128(a0) ld a3, 136(a0) ld a4, 144(a0) ld a5, 152(a0) ld a6, 160(a0) ld a7, 168(a0) ld s2, 176(a0) ld s3, 184(a0) ld s4, 192(a0) ld s5, 200(a0) ld s6, 208(a0) ld s7, 216(a0) ld s8, 224(a0) ld s9, 232(a0) ld s10, 240(a0) ld s11, 248(a0) ld t3, 256(a0) ld t4, 264(a0) ld t5, 272(a0) ld t6, 280(a0) # restore user a0 ld a0, 112(a0) # return to user mode and user pc. # usertrapret() set up sstatus and sepc. sret 处理器的虚拟化 为什么死循环不能使计算机被彻底卡死?\n原理上: 1.硬件会发生中断(类似于强行插入的ecall) 2.切换到操作系统代码执行 3.操作系统代码可以切换到另一个进程执行\nxv6面经 惰性分配内存 内存写时复制 内存的超售机制 xv6的物理内存如何分配 定时器回调如何实现 xv6配置环境的时候申请多大物理内存 xv6内存的惰性分配 惰性分配的问题: 频繁触发缺页异常陷入内核态开销很大\n惰性分配的优化方法: 预取,根据进程访问局部性预取相邻的多个页,文件映射时,linux默认readhead为128KB\n文件系统的读写全流程 xv6是什么类型的操作系统介绍一下,是否是实时操作系统 操作系统分几个模块 虚拟地址翻译过程 系统调用的详细过程 进程同步的策略,进程,线程如何同步 有看过linux内核设计吗 考虑后面看一下\n中山OS 短期调度 image.png\n中期调度 767f41d251ad2e9cf2b4af58522ec6c.png\n进程调度总览 image.png\nvscode gdb调试 1.继续:(Continue)/F5 含义: 解除暂停,全速奔跑吧 发生什么: xv6会从当前暂停的地方恢复运行,CPU全速工作 直到遇到下一个断点,它才会再次停下来\n2.逐过程(Step Over)/F10 含义: 往下走一行,但我不想进这个函数里面看 发生什么: 执行当前高亮的一行代码 如果这行代码调用一个函数,调试器会瞬间把那个函数跑完,然后停在下一行\n3.单步调试(Step Into)/F11 含义: 往下走一行,如果有函数,我要钻进去看细节 发生什么: 执行当前行 如果这行函数调用了函数,调试器会跳进那个函数的内部,停在那个函数的第一行\n4.跳出(Step Out)/shift + F11 含义: \u0026ldquo;我不小心钻进来了,或者我看腻了,快带我出去\u0026rdquo; 发生什么: 让我当前所在的函数瞬间执行完剩下的所有代码 然后停在调用它的那行代码的下一行\n光标停在的哪一行,执行了吗? 没有执行 当黄色的高亮条(光标)停在某一行代码上时,代表CPU\u0026quot;正准备\u0026quot;执行这一行代码,但还没下手\nxv6-lab lab1-util Boot xv6/启动xv6 Sleep/睡眠 为xv6实现UNIX的sleep程序;你的sleep程序应暂停用户指定的时钟滴答数.一个滴答是xv6内核 定义的时间单位,即定时器芯片两次中断之间的间隔.你的解决方法应放在文件user/sleep.c中\npingpong/乒乓 编写一个程序,利用UNIX系统调用,通过一对管道(每个方向一个),在两个进程之间\u0026quot;乒乓\u0026quot;传递一个字节 具体流程如下: 1.父进程应该向子进程发送一个字节 2.子进程应该: 接收这个字节 打印: received ping (其中是它的进程ID) 把这个字节写回管道,发给父进程 退出 3.父进程应该: 从子进程读取这个字节 打印: received pong 退出 一些提示: 使用pipe来创建管道 使用fork来创建子进程 使用read从管道读取,使用write向管道写入 使用getpid获取当前进程的ID 别忘了把程序名加入到Makefile的UPROGS列表中 xv6的用户程序能用的库函数很少,你可以查看user/user.h里的列表\n预期输出:\n$ pingpong 4: received ping 3: received pong $\nprimes/质数 素数筛 Sieve of Eratosthenes（埃拉托斯特尼筛法） 编写一个并发版本的\u0026quot;素数筛\u0026quot;程序,通过管道(pipes)来筛选素数.这个创意归功于Unix管道的发明者Doug Mcllroy\n使用pipe和fork来建立一个流水线 1.每一个进程将数字2到35输入到管道中 2.对于每一个新发现的素数,你需要创建一个新的进程 这个新进程从它的左邻居(通过一个管道)读取数据 将筛选后的数据写给它的右邻居(通过另一个管道) 3.因为xv6的资源(文件描述符和进程数)有限,第一个进程只要算到35就可以停止了\n一些提示: 严谨关闭文件描述符: 非常重要! 每个进程必须关闭它不需要的文件描述符.否则,你的程序会在还没算到35之前就耗尽xv6的资源(xv6默认每个进程只能打开16个文件).\n等待子进程: 当第一个进程处理完35后,它应该等待这个流水线结束(包括所有的子进程,孙子进程等).主进程应该在所有输出打印完毕,且所有其他进程都退出之后才退出\n读取结束信号: 当管道的写入端（Write-side）被关闭时，read 函数会返回 0。利用这个特性来判断什么时候结束。\n直接传整数: 最简单的方法是直接向管道写入 32 位（4字节）的 int，而不是像字符串那样用 ASCII 格式读写。\n按需创建: 你应该只在需要的时候(发现新素数时)才创建新的进程,不要一开始就全创建好\n别忘了把程序加入Makefile UPROGS中\n6de8c40ce3633da02e99069d27316c4.jpg\nfind/查找 编写一个简单版本的UNIX find 程序: 在一个目录树(directory tree) 中查找所有具有特定名称的文件. 你的代码应该写在 user/find.c 文件中\n[提示 (Hints) 解析] 看看user/ls.c 是如何读取目录的: 这是最重要的一条提示! 在Unix中，目录其实也是一种\u0026quot;文件\u0026quot;, 里面存的是\u0026quot;目录项\u0026quot;(文件或子目录的名字和它们对应的索引节点号).ls.c里有完整读取目录的代码模板 使用递归(recursion): 因为目录里面可能还有目录,所以当程序遇到目录时,需要调用自己进入子目录继续找 不需要递归遍历.和..: .代表当前目录, ..代表上一级目录.如果不跳过他们,程序就会在原地无限打转 文件系统的更改在qemu运行期间是持久的: 如果你建了文件,下次启动还在. 如果想得到干净的文件系统,可以运行make clean然后再make qemu 你需要使用C语言的字符串: 复习一下C语言里字符串是如何\\0结尾的 注意,不能像Python里那样 == 比较字符串: 必须使用strcmp() 函数 把程序添加到Makefile的UPROGS中: 这样编译系统才会把你的find.c编译成可在xv6里运行的命令\nXARGS 写一个简化版的xargs程序.它需要从标准输入(stdin)一行一行地读取数据,然后对每一行执行指定的命令,把读取到的这一行内容作为参数喂给这个命令\n例子 1：echo hello too | xargs echo bye 左边输出：hello too（通过管道传给右边）。 右边接收：xargs 读取到了 hello too。 xargs 的动作：xargs 后面跟着的命令是 echo bye。它把 hello too 追加到后面，变成了 echo bye hello too。 最终执行：打印出 bye hello too。 例子 2：echo \u0026ldquo;1\\n2\u0026rdquo; | xargs -n 1 echo line (在咱们的 lab 中不需要实现 -n 这个优化，只要按行读取就行)。 第一行是 1，执行 echo line 1。 第二行是 2，执行 echo line 2。\n官方提示(Hints): 用fork和exec对每一行输入调用命令.父进程要用wait等待子进程执行完毕. 逐个字符读取输入,直到遇到换行符\\n kernel/param.h里有一个MAXARG，定义了最大参数个数,声明数组时可以用. 写完时记得把程序加到Makefile的UPROGS\nlab2-system calls 系统调用跟踪（System call tracing）（中等难度） 在本作业中，你将添加一个系统调用跟踪（tracing）功能，这可能会在你调试后续的实验时有所帮助。你将创建一个新的 trace 系统调用来控制跟踪。它应接受一个参数：一个整数“掩码（mask）”，其二进制位指定了要跟踪哪些系统调用。 例如，要跟踪 fork 系统调用，程序需要调用 trace(1 \u0026laquo; SYS_fork)，其中 SYS_fork 是 kernel/syscall.h 中定义的系统调用号。你需要修改 xv6 内核，以便在每次系统调用即将返回时，如果掩码中设置了该系统调用对应的位，则打印出一行信息。这行信息应该包含：进程 ID、系统调用的名称和返回值；你不需要打印系统调用的参数。 trace 系统调用应该为调用它的进程及其随后 fork（派生）出的所有子进程启用跟踪，但不应影响其他无关的进程。 我们提供了一个用户级程序 trace，用于在启用跟踪的情况下运行另一个程序（参见 user/trace.c）。当你完成实验后，你应该能看到如下类似的输出： Bash $ trace 32 grep hello README 3: syscall read -\u0026gt; 1023 3: syscall read -\u0026gt; 966 3: syscall read -\u0026gt; 70 3: syscall read -\u0026gt; 0 $ $ trace 2147483647 grep hello README 4: syscall trace -\u0026gt; 0 4: syscall exec -\u0026gt; 3 4: syscall open -\u0026gt; 3 4: syscall read -\u0026gt; 1023 4: syscall read -\u0026gt; 966 4: syscall read -\u0026gt; 70 4: syscall read -\u0026gt; 0 4: syscall close -\u0026gt; 0 $ $ grep hello README $ $ trace 2 usertests forkforkfork usertests starting test forkforkfork: 407: syscall fork -\u0026gt; 408 408: syscall fork -\u0026gt; 409 409: syscall fork -\u0026gt; 410 410: syscall fork -\u0026gt; 411 409: syscall fork -\u0026gt; 412 410: syscall fork -\u0026gt; 413 409: syscall fork -\u0026gt; 414 411: syscall fork -\u0026gt; 415 \u0026hellip; $\n输出示例解析：()()()()\n在上面的第一个示例中，trace 调用 grep 且仅跟踪 read 系统调用。参数 32 即为 1 \u0026laquo; SYS_read。()()()()\n在第二个示例中，trace 运行 grep 并跟踪所有系统调用；2147483647 这个数字的低 31 位全部为 1。()()()\n在第三个示例中，程序没()有被 trace 运行，因此没有打印任何跟踪输出。()()\n在第四个示例中，usertests 中的 forkforkfork 测试的所()有后代进程的 fork 系统调用都被跟踪了。()\n如果你的程序行为如上所示（进程 ID 具体数字可能不同），那么你的解()决方案就是正确的。\n一些提示（Hints）： 在 Makefile 的 UPROGS 中添加 $U/_trace。 运行 make qemu，你会发现编译器无法编译 user/trace.c，因为该系统调用的用户空间存根（stubs）还不存在： 请在 user/user.h 中为该系统调用添加函数原型。 在 user/usys.pl 中添加存根（stub）。 在 kernel/syscall.h 中添加系统调用编号。 Makefile 会调用 Perl 脚本 user/usys.pl 来生成 user/usys.S（这就是实际的系统调用存根），这些存根使用 RISC-V 的 ecall 指令陷入（transition）到内核。 解决编译问题后，再次运行 make qemu 并在 xv6 shell 中执行 trace 32 grep hello README；它会失败，因为你还没有在内核中实现该系统调用。 在 kernel/sysproc.c 中添加一个 sys_trace() 函数来实现这个新的系统调用。你需要在 proc 结构体（参见 kernel/proc.h）中添加一个新的变量，用来记录传入的参数（掩码）。从用户空间获取系统调用参数的函数位于 kernel/syscall.c 中，你可以在 kernel/sysproc.c 中看到它们的使用示例。 修改 fork() 函数（参见 kernel/proc.c），将跟踪掩码从父进程复制到子进程。 修改 kernel/syscall.c 中的 syscall() 函数以打印跟踪输出。为了打印出系统调用的名称，你需要添加一个包含系统调用名称的字符串数组，以便用系统调用号来进行索引。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/xv6-for-risc-v/","summary":"\u003ch1 id=\"xv6-for-risc-v\"\u003exv6 for RISC-V\u003c/h1\u003e\n\u003ch2 id=\"jyy-xv6导读\"\u003ejyy xv6导读\u003c/h2\u003e\n\u003ch3 id=\"基础知识\"\u003e基础知识\u003c/h3\u003e\n\u003cp\u003e应用视角的操作系统: 对象 + API\n把操作系统当提供服务的黑盒子\u003c/p\u003e\n\u003cp\u003exv6:Unix v6的现代\u0026quot;克隆\u0026quot;\u003c/p\u003e\n\u003cp\u003e文件关系\n源文件 (.c/.cpp)  →  编译  →  目标文件 (.o)  →  链接  →  可执行文件\n↓\n依赖文件 (.d)  →  告诉构建系统何时需要重新编译\u003c/p\u003e","title":"xv6 for RISC-V"},{"content":"安装ch343的驱动 解压并进入驱动目录\nunzip ch343ser_linux-main.zip cd ch343ser_linux-main/driver\n安装编译依赖(如果未安装) 驱动需要内核头文件和编译工具 image.png\n编译驱动模块 在driver目录下执行\nsudo make install\n加载驱动并测试 先卸载可能已经自动加载的老驱动\nsudo modprobe -r ch343\n加载编译的模块\nsudo insmod ch343.ko\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%AE%89%E8%A3%85ch343%E7%9A%84%E9%A9%B1%E5%8A%A8/","summary":"\u003ch1 id=\"安装ch343的驱动\"\u003e安装ch343的驱动\u003c/h1\u003e\n\u003cp\u003e解压并进入驱动目录\u003c/p\u003e\n\u003cp\u003eunzip ch343ser_linux-main.zip\ncd ch343ser_linux-main/driver\u003c/p\u003e\n\u003cp\u003e安装编译依赖(如果未安装)\n驱动需要内核头文件和编译工具\nimage.png\u003c/p\u003e\n\u003cp\u003e编译驱动模块\n在driver目录下执行\u003c/p\u003e\n\u003cp\u003esudo make install\u003c/p\u003e\n\u003cp\u003e加载驱动并测试\n先卸载可能已经自动加载的老驱动\u003c/p\u003e","title":"安装ch343的驱动"},{"content":"安装库遇到的问题 1.requests模块已经安装,vscode下无法导入requests模块 原因:python安装了多个版本,一个版本安装了库,另一个没有，而vscode调用的是另一个， 解决方法:1.ctrl + shift + p 快捷键 2.输入python select intepreter切换版本\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%AE%89%E8%A3%85%E5%BA%93%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98/","summary":"\u003ch1 id=\"安装库遇到的问题\"\u003e安装库遇到的问题\u003c/h1\u003e\n\u003cp\u003e1.requests模块已经安装,vscode下无法导入requests模块\n原因:python安装了多个版本,一个版本安装了库,另一个没有，而vscode调用的是另一个，\n解决方法:1.ctrl + shift + p 快捷键\n2.输入python select intepreter切换版本\u003c/p\u003e","title":"安装库遇到的问题"},{"content":"操作系统 中山OS 复杂系统 多种内核架构 宏内核 微内核 外核 多内核 重要设计原则 策略与机制的分离\n读写锁: 读-读共享: 只要没有人再写,多少个读者可以同时读取数据(不互斥) 读-写共享: 有人在写的时候,别人不能读; 有人在读的时候别人不能写 写-写互斥: 一个人在写的时候,别人也不能写\n由于“读-读”是可以共享的，这就引发了一个问题：当读者和写者都在排队时，我们该让谁先上？这就是“读者优先”和“写者优先”的区别。\n偏向读者: 只要还有读者在读，新来的读者就可以直接“插队”进去读，写者必须无限期等待。\n致命缺点: 写者饿死 如果系统的读取操作非常频繁,读者络绎不绝,写者可能会永远等不到执行的机会,这种情况在操作系统中被称为饿死\n偏向写者 一旦有写者在排队，就不允许新的读者再进入读取区了。\n特点：保证数据实时性 写者不会被无限期拖延，只要写者发出了请求，现有的读者读完后，写者就能立刻执行。这保证了后续的读者能读到最新修改的数据。不过，这可能会导致读者的平均等待时间变长。\n偏向写者: 一旦有写者在排队，就不允许新的读者再进入读取区了。\n操作系统导论(部分笔记 一部分是纸质笔记) 第31章信号量 二值信号量 用信号量作为锁\n读者写者锁 允许多个读者访问共享变量\n如果某个变量要更新数据结构 rwlock_acquire_writelock() 获得写锁 rwlock_release_writelock() 释放锁 生产者-消费者问题(有界缓冲区问题)\n生产者-消费者问题中的死锁 在生产者-消费者问题中，死锁通常发生在生产者和消费者之间相互等待对方释放资源，导致双方都无法继续执行。 消费者线程： 先获得了互斥锁（mutex）。 然后调用 sem_wait(\u0026amp;full)，发现 full 信号量为 0（没有数据可消费），于是阻塞。 但此时消费者仍然持有互斥锁（mutex）。 生产者线程： 尝试获取互斥锁（mutex），但由于消费者已经持有该锁，生产者被阻塞。 生产者无法继续执行，也就无法生产数据并唤醒消费者。\n死锁的原因 消费者：持有互斥锁，等待 full 信号量。 生产者：等待互斥锁，无法生产数据。 循环等待：消费者等待生产者生产数据，生产者等待消费者释放互斥锁。 比喻：冰箱没有食物，消费者站在冰箱面前等待冰箱有食物，但是由于生产者 也就是厨师由于消费者也就是顾客把冰箱占着所以无法把食物放进冰箱中。二者互相等待形成死锁 解决方法打开冰箱前先检查一下厨房的计数器，如果计数器有食物，消费者再去打开冰箱，否则在外面 等待。相反同样的道理 为什么有效 避免交叉等待：消费者和生产者都不再因为持有冰箱门（互斥锁）而等待食物或空格子。他们先检查计数器（信号量），确认条件满足后再打开冰箱门。 减少锁的持有时间：冰箱门（互斥锁）只在取放食物时打开，时间很短，减少了锁的争用。 提高效率：生产者和消费者可以更高效地工作，因为他们不再因为锁而互相等待。\n解决方案 调整信号量的使用顺序 确保生产者和消费者在获取资源时的顺序一致，避免交叉等待。 2.使用条件变量 条件变量比信号量更灵活，可以避免死锁。通过条件变量，生产者和消费者可以更优雅地协调彼此的操作。 使用高级并发库 如果你使用的是现代编程语言（如 Java 或 C++），可以利用内置的并发库来避免死锁。 4.减少锁的作用域 把获取和释放互斥量的操作调整为紧挨着临界区，把full，empty的唤醒和等待操作调整到锁的外面,结果得到了简单而有效的有界缓冲区 哲学家就餐问题 死锁每个哲学家都拿到了左手边的餐叉，他们每个都会阻塞住，并且一直等待另一个餐叉 解决方案:破除依赖 修改某个或者某些哲学家的取餐叉顺序\n第32章常见的并发问题 关键问题:如何处理常见的并发缺陷 并发缺陷会有很多常见的模式。了解这些模式是写出健壮、正确程序的第一步。\n非死锁缺陷 主要有两种:\n违反原子性缺陷 正式的定义:违反了多次内厝访问中预期的可串行代码 解决方案:加锁\n错误顺序 正式的定义:两个内存访问的预期顺序被打破 解决方案:通过强制顺序来修复这种缺陷\u0026mdash;\u0026mdash;条件变量\n死锁缺陷 关键问题：如何对付死锁 我们在实现系统时，如何避免 或者检测，恢复死锁呢？ 这是目前系统中的真实问题吗？\n为什么发生死锁 原因一： 大型的代码库里，组件之间会有复杂的依赖 以操作系统为例：虚拟内存系统在需要访问文件系统才能从磁盘读到内存页；文件系统随后 又要和虚拟内存交互，去申请一页内存，以便存放读到的块。因此在设计大型的文件系统的锁机制时，必须要仔细地去避免循环依赖导致的死锁 原因二： 封装 软件开发者一直倾向于隐藏实现细节，以模块化的方式让软件开发更容易。然而模块化和锁不是 很 契合，某些看起来没有关系的接口可能会导致死锁\n产生死锁的条件 互斥：线程对需要的资源 需要进行互斥访问 持有并等待：线程持有了资源，同时又在等待其他资源 非抢占：线程获得 的 资源，不能被抢占 循环等待：线程之间存在一个环路，环路上每个线程 都额外持有一个资源，而这个资源又是下一个线程 要申请的 上面4个条件的任何一个没有满足，死锁就不会产生\n预防： 循环等待： 解决方案：全序或者偏序 提示：通过锁的地址来强制锁的顺序\n持有并等待 解决方案：可以通过原子的抢锁来避免 不适合封装：因为这方案需要我们 准确地知道抢哪些锁\n非抢占 活锁：两个线程有可能一直重复者一序列，又同时都抢锁失败。在这种情况下系统一直在运行这段代码，但是又不会有进展 解决方案： 可以在循环结束 的时候，先随机等待一个时间，然后再重复整个动作，这样可以降低线程之间的重复互相 干扰。 关于这个方案的最后一\n互斥： 预防方法：完全避免互斥 设计各种无等待数据的结构思想？？？ 无等待同步？？？\n通过调度避免死锁 银行家算法：略\n检查和恢复 允许死锁偶尔发生,检查到死锁时再采取行动\n不是所有值得做的事情都 值得做好\n第33章基于事件的并发 关键问题：不用线程，如何构建并发服务器 事件循环： 处理事件的代码叫做事件处理程序，他是系统中发生的唯一活动，因此调度就是决定接下来处理哪个事件\n重要API：select()或poll() select() select() 是一种古老的多路复用机制，广泛用于各种操作系统（包括Unix、Linux和Windows）。 工作原理 select() 可以同时监视多个文件描述符集合，分别用于读、写和异常条件。它会阻塞当前线程，直到至少有一个文件描述符准备好，或者超时。 文件描述符集合：select() 使用三个文件描述符集合（fd_set）来分别表示可读、可写和异常条件的文件描述符。 超时机制：可以通过timeout参数设置一个超时时间，如果在超时时间内没有任何文件描述符准备好，则返回。 使用场景 select() 适用于文件描述符数量较少的场景。由于它需要遍历所有文件描述符集合来检查状态，因此在文件描述符数量较多时效率较低。 poll() poll() 是另一种多路复用机制，比select()更灵活，效率也更高，尤其是在文件描述符数量较多时。 工作原理 poll() 使用一个pollfd数组来监视文件描述符的状态。每个pollfd结构体包含一个文件描述符和两个标志：events（监视的事件类型）和revents（实际发生的事件）。 无固定限制：poll() 不受select()中文件描述符数量的限制（select()的最大文件描述符数量通常受FD_SETSIZE限制）。 动态数组：pollfd数组的大小可以根据需要动态调整。 使用场景 poll() 适用于文件描述符数量较多的场景，因为它不会像select()那样受到固定大小的限制。 为何更简单？无须锁 使用单个CPU和基于事件的应用程序，并发程序中发现的问题不再存在\n请勿阻塞基于事件的 服务器 基于事件的服务器可以对任务调度进行细粒度的控制。但是，为了保持这种控制，不可以有阻止调 用者执行的调用。如果不遵守这个设计提示，将导致基于事件的服务器阻塞，客户心塞，并严重质疑你 是否读过本书的这部分内容。\n一个问题：阻塞系统调用 但是，使用基于事件的方法时，没有其他线程可以运行：只是主事件循环。这意味着 如果一个事件处理程序发出一个阻塞的调用，整个服务器就会这样做：阻塞直到调用完成。 当事件循环阻塞时，系统处于闲置状态，因此是潜在的巨大资源浪费。因此，我们在基于 事件的系统中必须遵守一条规则：不允许阻塞调用\n解决方案：异步I/O 允许程序在等待I/O操作完成时继续执行其他任务。与传统的同步I/O（Synchronous I/O）不同，异步I/O不会阻塞当前线程，从而提高了程序的效率和响应性。异步I/O广泛应用于高性能网络编程、嵌入式系统和现代编程框架中。\n另一个问题：状态管理 第36章I/O设备 关键问题：如何将I/O集成进计算机系统中 I/O应该如何集成进系统中？ 其中的一般机制是什么？ 如何让它们变得更高效\n系统架构 image.png\n为什么用这样的分层架构？ 因为物理布局及造价成本。越快的总线越短，高性能的总线的造价非常高。略\n标准设备 帮助理解设备交互机制 image.png\n标准协议 一个设备接口包含3个寄存器： 状态寄存器：可以读取并查看设备的当前状态 命令寄存器：用于通知设备执行某个具体任务 数据寄存器：将数据 传给设备或从设备接受数据\n关键问题：如何减少轮询开销 操作系统检查设备状态时如何避免频繁轮询，从而降低管理设备的CPU开销\n利用中断减少CPU开销： 有了中断后，CPU 不再需要不断轮询设备，而是向设备发出一个请求，然后就可以让对应进 程睡眠，切换执行其他任务。当设备完成了自身操作，会抛出一个硬件中断，引发CPU跳 转执行操作系统预先定义好的中断服务例程（Interrupt Service Routine，ISR），或更为简单 的中断处理程序（interrupt handler）。 注意，使用中断并非总是最佳方案。假如有一个非常高性能的设备，它处理请求很快： 通常在CPU第一次轮询时就可以返回结果。此时如果使用中断，反而会使系统变慢：切换到其他进程，处理中断，再切换回之前的进程代价不小。\n提示：中断并非总是比PIO好 1.如果设备非常快：采用轮询 2.如果设备比较慢：采用允许发生重叠的中断更好 3.如果设备的速度未知，或者时快时慢：考虑采用混合策略，先尝试轮询一小段时间，如果设备没 有完成操作，此时再使用中断 4.网络场景最好不要使用中断：网络端收到大量数据包，如果每一个包都发 生一次中断，那么有可能导致操作系统发生活锁（livelock），即不断处理中断而无法处理用 户层的请求。\n另一个基于中断的优化就是合并。设备在抛出中断之前往往会等待一小段 时间，在此期间，其他请求可能很快完成，因此多次中断可以合并为一次中断抛出，从而 降低处理中断的代价。当然，等待太长会增加请求的延迟，这是系统中常见的折中\n利用DMA进行更高效的数据传送 关键问题：如何减少PIO的开销 使用PIO 的方式，CPU 的时间会浪费在向设备传输数据或从设备传出数据的过程中。如何才能分 离这项工作，从而提高CPU的利用率？\n解决方案：使用DMA\n设备交互的方法： 硬件如何如与设备通信？是否需要一些明确的指令？或者其他的方式？ 1.明确的I/O指令（这些指令规定了操作系统将数据发 送到特定设备寄存器的方法，从而允许构造上文提到的协议） 2.内存映射\n纳入操作系统：设备驱动程序 每个设备都有非常具体的接口，如何将它们纳入操作系统\n关键问题：如何实现一个设备无关的操作系统 如何保持操作系统的大部分与设备无关，从而对操作系统的主要子系统隐藏设备交互的细节？ 方法：抽象 在最底层，操作系统的一部分软件清楚地知道设备如何工作，我们将这部分软件称为设备驱动程序，所有设备交互的细节都封装在其中 image.png\n第37章磁盘驱动器 关键问题：如何存储和访问磁盘上的数据 现代磁盘如何存储数据？接口是什么？数据是如何安排和访问的？磁盘调度如何提高性能？\n简单的磁盘驱动器： 单磁道延迟：旋转延迟 多磁道：寻道时间 1.寻道：首先是磁盘臂移动的加速阶段，然后是全速移动而惯性滑动，然后是随着磁盘臂减速而减速。最后，在磁盘小心地放置在正确地磁道时停下来。 2.传输：数据从表面读取或写入表面\n首先寻道，然后等待转动延迟，最后传输\n后写缓存： 数据放入其内存之后回报写入完成 直写缓存： 实际写入磁盘之后，回报写入完成\nI/O数学 image.png\nimage.png\n计算平均寻道时间 image.png\n提示：顺序地使用磁盘\n磁盘调度 1.SSTF:最短寻道时间优先 SSTF按磁道对I/O请求队列排序，选择 在最近地磁道上地请求先完成 产生问题：主机操作系统无法利用驱动器地几何结构，而是只会看到一系列地块 第二个 问题会产生饥饿\n2.电梯（SCAN或C-SCAN） SCAN，简单地以跨越磁道的顺序来服务磁盘请求。我们将一次跨越磁盘称为 扫一遍。因此，如果请求的块所属的磁道在这次扫一遍中已经服务过了，它就不会立即处 理，而是排队等待下次扫一遍。\nF-SCAN， 它在扫一遍时冻结队列以进行维护\nC-SCAN 是另一种常见的变体，即循环SCAN（Circular SCAN）的缩写。不是在一个 方向扫过磁盘，该算法从外圈扫到内圈，然后从内圈扫到外圈，如此下去 长得很像电梯 并没有严格遵守 SJF的原则。具体来说他们忽视了旋转\n关键问题：如何计算磁盘旋转开销 如何同时考虑寻道和旋转，实现更接近SJF的算法\n第38章：廉价冗余磁盘阵列(RAID)\n第44章数据完整性和保护 关键问题：如何确保数据完整性 磁盘故障模式 两种类型的单块故障：\n潜在扇区错误（LSE） 当磁盘扇区（或扇区组）以某种方式讹误时，会出现 LSE 例如：磁头碰撞或宇宙射线\u0026mdash;-磁盘内纠错码确定块中的磁盘位是否完好在某些情况下，修复它们。如果它们不好，并且驱动器没有足够的信息来修复错误，则在 发出请求读取它们时，磁盘会返回错误。\n块讹误 磁盘块出现讹误（corrupt），但磁盘本身无法检测到 有缺陷的磁盘固件可能会将块写入错误的位置 一个块通过有故障的总线 从主机传输到磁盘时，它可能会讹误。 无声的故障（silent fault）。返回故障数据时， 磁盘没有报告问题。\nimage.png\n处理潜在的扇区错误 存储系统应该就用 它具有的任何冗余机制，来返回正确的数据 当全盘故障和LSE接连发生时：增加了额外的冗余度\n检测讹误：校验和 校验和：校验和就是一 410 第44章 数据完整性和保护 个函数的结果，该函数以一块数据（例如4KB块）作为输入，并计算这段数据的函数，产 生数据内容的小概要（比如4字节或8字节）。此摘要称为校验和 校验和函数： 异或（XOR）函数 如果每个校验和单元内相同位置的两 个位发生变化，则校验和将不会检测到讹误\n加法函数 但如果数据被 移位，则不好\n为Fletcher校验和 s1 = s1 + di mod 255 s2 = s2 + s1 mod 255\n循环冗余校验（CRC） 所做的只是将D视为一个大的二进制数（毕竟它只 是一串位）并将其除以约定的值（k）。该除法的其余部分是CRC的值\n两个具有不相同的数据块可能具有相同的校验和，这被称为碰撞 image.png\n使用校验和： 略 发现讹误：如果存储系统有冗余副本就尝试使用它，如果没有，则可能的答案是返回错误\n问题：错误的写入： 正确地将数据写入磁盘，但位置错误 解决方案： 添加物理标识符：包括磁盘号和扇区偏移量\n问题：丢失的写入： 当设备通知上层写入已完成，但事实上它从未持有。因此磁盘上留下的是该块的旧内容 解决方案： 方法一：执行写入验证或写入后读取。 方法二：在系统的其他位置添加校验和，以检测丢失的写入.\n擦净： 通过 定期读取系统的每个块，并检查校验和是否仍然有效，磁盘系统可以减少某个数据项的所 有副本都被破坏的可能性。典型的系统每晚或每周安排扫描。\n校验和的开销 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/","summary":"\u003ch1 id=\"操作系统\"\u003e操作系统\u003c/h1\u003e\n\u003ch1 id=\"中山os\"\u003e中山OS\u003c/h1\u003e\n\u003cp\u003e复杂系统\n多种内核架构\n宏内核\n微内核\n外核\n多内核\n重要设计原则\n策略与机制的分离\u003c/p\u003e\n\u003ch2 id=\"读写锁\"\u003e读写锁:\u003c/h2\u003e\n\u003cp\u003e读-读共享: 只要没有人再写,多少个读者可以同时读取数据(不互斥)\n读-写共享: 有人在写的时候,别人不能读; 有人在读的时候别人不能写\n写-写互斥: 一个人在写的时候,别人也不能写\u003c/p\u003e","title":"操作系统"},{"content":"常用快捷键 vs2022打开视图ctrl + shift + L 某些电脑Fn + win会禁用win功能\nVscode Ctrl + F搜索\nAlt 拖动鼠标可以使用 方框框选\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%B8%B8%E7%94%A8%E5%BF%AB%E6%8D%B7%E9%94%AE/","summary":"\u003ch1 id=\"常用快捷键\"\u003e常用快捷键\u003c/h1\u003e\n\u003cp\u003evs2022打开视图ctrl + shift + L\n某些电脑Fn + win会禁用win功能\u003c/p\u003e\n\u003cp\u003eVscode\nCtrl + F搜索\u003c/p\u003e\n\u003cp\u003eAlt 拖动鼠标可以使用 方框框选\u003c/p\u003e","title":"常用快捷键"},{"content":"程序的并发 进程和线程和协程的区别 进程 线程 协程 线程池 线程安全队列 重排序队列 1.期望序列号: 下一个该输出的序号 2.暂存缓冲区(Buffer): 通常是一个Priority Queue(最小堆) 或Map/Hash 3.水位线/超时(Watermark/Timeout): 如果某个包一直不来,队列不能永远卡死,需要有跳过或触发重传的机制\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%B9%B6%E5%8F%91/","summary":"\u003ch1 id=\"程序的并发\"\u003e程序的并发\u003c/h1\u003e\n\u003ch2 id=\"进程和线程和协程的区别\"\u003e进程和线程和协程的区别\u003c/h2\u003e\n\u003ch3 id=\"进程\"\u003e进程\u003c/h3\u003e\n\u003ch3 id=\"线程\"\u003e线程\u003c/h3\u003e\n\u003ch3 id=\"协程\"\u003e协程\u003c/h3\u003e\n\u003ch2 id=\"线程池\"\u003e线程池\u003c/h2\u003e\n\u003ch2 id=\"线程安全队列\"\u003e线程安全队列\u003c/h2\u003e\n\u003ch2 id=\"重排序队列\"\u003e重排序队列\u003c/h2\u003e\n\u003cp\u003e1.期望序列号: 下一个该输出的序号\n2.暂存缓冲区(Buffer): 通常是一个Priority Queue(最小堆) 或Map/Hash\n3.水位线/超时(Watermark/Timeout): 如果某个包一直不来,队列不能永远卡死,需要有跳过或触发重传的机制\u003c/p\u003e","title":"程序的并发"},{"content":"单目视觉 单目视觉系统一般由一个相机或者一个相机与一个陀螺仪组成。 相机 完成图像识别任务 完成定位任务（一般基于PNP） 将目标定位在相机坐标系内 陀螺仪 将相机坐标系内的坐标转换到世界坐标系\n深度信息 装甲板深度信息是指装甲板相对于机器人摄像头的距离信息，通常以深度图像或点云的形式表示。\nOpenCV中的仿射变换和透视变换 Mat cv::getAffineTransform(InputArray src, InputArray dst) 计算仿射变换矩阵 输入一组原始点（src）和一组对应后的变换点（dst） 需要注意输入为三对点 Mat cv::getPerspectiveTransform(InputArray src, InputArray dst, int solveMethod = DECOMP_LU) 计算透视变换矩阵 输入一组原始点和一组对应后的变换点 需要注意输入的为四对点 void cv::warpAffine(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar \u0026amp; borderValue = Scalar()) 对一张图像进行仿射变换 src为输入的图像 dst为变换后的输出图像 M为仿射变换矩阵 dsize为输出图像的大小 void cv::warpPerspective(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, int borderMode = BORDER_CONSTANT, const Scalar \u0026amp; borderValue = Scalar() ) 对一张图片进行透视变换 具体参数与仿射变换中描述的相同\nPNP测距 在计算机视觉中，我们经常需要测量自己与目标之间的距离，对于单目视觉体系，最常用的方法就是PNP算法。 PNP算法是用来解决PNP问题的。简单来说，PNP问题就是在已知 n 个三维空间点坐标（相对于某个指定的坐标系A）及其二维投影位置的情况下，如何估计相机的位姿（即相机在坐标系A下的姿态）。 P3P算法 PNP算法 PNP算法通过至少四个点的约束，求出世界坐标系到相机坐标系的旋转矩阵和平移向量。 其声明如下： 其中objectPoints为视觉坐标系中的点 imagePoints为像素坐标系中的点 cameraMatrix为相机内参 disCoeffs为相机畸变矩阵 rvec为求出来的旋转向量 tvec为求出来的平移向\n通过图像坐标系中的N个点与世界坐标系中的N的点的对应关系求解相机的世界坐标 世界坐标系的N个对应点需要在同一平面内 这种方法有较好的测距精度，但对于测量角度容易受误差影响\n旋转矩阵与旋转向量 三维空间中的旋转矩阵有 9 个量，而三维空间中的旋转只有 3 个自由度，因此我们很自然的想到，是否可以用更少的量描述一个三维运动。 事实上，对于坐标系的旋转，任意旋转都可以用一个旋转轴和一个旋转角来刻画。于是，我们可以使用一个方向与旋转轴一直，长度等于旋转角的向量描述旋转运动，这个向量成为旋转向量。 通过这样的方式，我们就可以只通过一个三维的旋转向量和一个三维的平移向量描述三维空间中刚体的运动。 旋转矩阵和旋转向量是可以互相转化的，有旋转向量推导旋转矩阵的公式也被成为罗德里格斯公式： (R = cos \\theta I + (1 - cos \\theta ) n n^T + sin \\theta n^{\\wedge}) 而旋转角$ \\theta $也可以有公式 (\\theta = arccos(\\frac{tr(R) - 1}{2})) 计算得到。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%8D%95%E7%9B%AE%E8%A7%86%E8%A7%89/","summary":"\u003ch1 id=\"单目视觉\"\u003e单目视觉\u003c/h1\u003e\n\u003ch2 id=\"单目视觉系统一般由一个相机或者一个相机与一个陀螺仪组成\"\u003e单目视觉系统一般由一个相机或者一个相机与一个陀螺仪组成。\u003c/h2\u003e\n\u003cp\u003e相机\n完成图像识别任务\n完成定位任务（一般基于PNP）\n将目标定位在相机坐标系内\n陀螺仪\n将相机坐标系内的坐标转换到世界坐标系\u003c/p\u003e\n\u003ch2 id=\"深度信息\"\u003e深度信息\u003c/h2\u003e\n\u003cp\u003e装甲板深度信息是指装甲板相对于机器人摄像头的距离信息，通常以深度图像或点云的形式表示。\u003c/p\u003e","title":"单目视觉"},{"content":"导航 感知: PCL pcl(Point Cloud Library,点云库) 是一个 大型,跨平台,开源,免费的 C++库，专门用来处理2D/3D图像和点云数据\nimage.png\nBin Bin是点云地面分割算法中的网络容器,负责存储极坐标网格内的所有点,并实时维护该区域内Z值(高度)最小的点,因为该点最可能是地面点\n代价地图 全局代价地图: 基于静态地图,用于全局路径规划\n局部代价地图 实时更新,用于感知动态障碍物\n障碍物层 一个具体的感知插件,用于处理2D激光雷达或深度相机的数据,实时更新代价地图上的障碍物信息\n状态估计与建图: 地图服务器(Map Server): 负责加载预先构建好的环境地图,为整个导航系统提供先验地图信息\n定位模块: 自适应蒙特卡洛定位（Adaptive Monte Carlo Localization）， 它利用激光雷达数据匹配地图服务器提供的先验地图,实时估算机器人的位姿\nFast-LIO / Point-LIO： Fast-lio Point-lio\n对极几何 对极约束: 描述同一空间点在两个不同视角的相机图像上投影点之间的空间几何关系\n如果我们在第一幅图里找到一个特征点,在第二幅图里寻找它的匹配点时,不需要在整张图片里找,只需要沿着对应的极线找就可以了.这极大地缩小了搜索范围,提高了匹配效率,剔除了错误匹配\n尺度等价性: 仅凭一张或几张二维照片,我们无法判断画面中物体的\u0026quot;真实大小\u0026quot;和\u0026quot;真实距离\u0026quot;\n二级地图 粗地图:经过较大的体素滤波（如 0.4 米），点云稀疏，细节少。用于 快速粗配准，计算量小，可以快速筛选出较好的初始位姿。\n精地图（refine_map_）：经过较小的体素滤波（如 0.1 米），点云密集，细节丰富。用于 精细配准，获得高精度的位姿结果。\n惯性导航和slam导航的区别 惯性导航是推算自己的位置,而SLAM是观察环境来确定自己的位置 惯性导航(INS):完全依赖自身运动,通过测量加速度和角速度,从已知起点开始积分运算,连续推算当前位置,优点是不依赖任何外部信号,在隧道水下GPS环境也能工作;缺点是误差会随时间无限积累,长时间使用后会变得不准\nSLAM(同时定位与地图构建): 依赖外部环境.通过激光雷达,摄像头等传感器观测周围的特征点,在构建地图的同时,反推出自己在地图的位置.优点是能利用环境特征修正累计误差,实现较高精度的定位;缺点是在特征稀少或聚敛运动时容易失效\nICP(Iterative Closest Point,迭代最近点) 一种经典的点云配准算法,用于计算两个点云之间的刚体变换(旋转 + 平移), 使它们尽可能地重合 基本原理: 假设你已经有一个 目标点云（如预先建好的地图）和一个 源点云（如当前雷达扫描得到的点云），以及一个初始的变换估计。 对于源点云中的每个点，在目标点云中找最近点（形成对应点对）。 根据这些对应点对，计算出一个最优的刚体变换（最小化所有点对之间的欧氏距离平方和）。 将变换应用到源点云上，得到新的位置。 重复步骤 2~4，直到收敛（变换变化很小或达到最大迭代次数）。\nslamtool_box 建图阶段： 可以启动slam_toolbox控制机器人探索环境,构建地图 导航阶段: Nav2的规划与控制节点会订阅slam_toolbox发布的地图和实时位置信息来完成导航任务\nGmappig： 轻量,适合小场景,但地图难以续建,动态环境下表现不佳 Cartographer: 谷歌出品,精度高,但配置复杂,编译耗时长\n规划: 全局规划器(Global Planner)： 负责宏观路径规划,它在全局代价地图上,规划一条从起点到终点的全局最优路径, 用于避开静态障碍物,常见的算法插件有A*、Dijkstra等\n局部规划器 (Local Planner)： 负责微观实时规划与避障,它沿着全局路径,高频地规划全局轨迹,以应对动态障碍物,塔吊输出是局部地速度指令\n控制执行: 控制器服务器 (Controller Server)： 接收来自局部规划器的轨迹,并将其转化为最终的控制指令 通过cmd_vel通过话题发布速度命令,驱动机器人底盘运动\n行为树(Behavior Tree, BT)： Nav2的大脑,负责调度整个导航任务的执行逻辑 它通过XML文件配置,管理着从正常导航到豫章绕路. 卡住恢复等一系列高层面的行为决策 image.png\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%AF%BC%E8%88%AA/","summary":"\u003ch1 id=\"导航\"\u003e导航\u003c/h1\u003e\n\u003ch2 id=\"感知\"\u003e感知:\u003c/h2\u003e\n\u003ch3 id=\"pcl\"\u003ePCL\u003c/h3\u003e\n\u003cp\u003epcl(Point Cloud Library,点云库) 是一个 大型,跨平台,开源,免费的 C++库，专门用来处理2D/3D图像和点云数据\u003c/p\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003ch3 id=\"bin\"\u003eBin\u003c/h3\u003e\n\u003cp\u003eBin是点云地面分割算法中的网络容器,负责存储极坐标网格内的所有点,并实时维护该区域内Z值(高度)最小的点,因为该点最可能是地面点\u003c/p\u003e","title":"导航"},{"content":"工程知识 编译: 预编译: 预编译头文件是一种编译器优化技术,用于大幅加速C/C++项目的编译速度 把常用的,不常变化的头文件预先编译成二进制形式,这样每个源文件编译时就不需要重新解析这些头文件\n交叉编译: 交叉编译是指在一个平台上编译生成在另一个不同平台上运行的程序\ncmake: git: ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%B7%A5%E7%A8%8B%E7%9F%A5%E8%AF%86/","summary":"\u003ch1 id=\"工程知识\"\u003e工程知识\u003c/h1\u003e\n\u003ch2 id=\"编译\"\u003e编译:\u003c/h2\u003e\n\u003ch2 id=\"预编译\"\u003e预编译:\u003c/h2\u003e\n\u003cp\u003e预编译头文件是一种编译器优化技术,用于大幅加速C/C++项目的编译速度\n把常用的,不常变化的头文件预先编译成二进制形式,这样每个源文件编译时就不需要重新解析这些头文件\u003c/p\u003e","title":"工程知识"},{"content":"桂工自瞄文档 开发记录: 1.双装甲板通信逻辑 2.帧率控制器,插值法 补偿ros时间(陈君提出) -\u0026gt; (前提解决相机装配误差) 3.卡尔曼滤波器,熵权法,整车建模(最难),多运动模型观测 4.选板逻辑 5.重力补偿弹道解算(西工大(采纳)，同济(未采纳),上交四阶龙格库塔(中科大)) 上交线性空气阻力模型(采纳)(存在问题目前猜测由于pnp测距不准,在较远距离空气阻力击打倾斜效果很差) 无空气阻力的模型，它的角度和距离是线性，但是在远距离的时候，它的速度是随距离指数衰减的，在距离较远的时候，一点点测距误差就会导致比较大的角度偏差 6.三层前哨战卡尔曼修改(卡尔曼参数还需调试) 7.手写脱ros坐标变换器(重点: 角点变换) -\u0026gt; 待整理(目前矩阵命名是相反的) -\u0026gt; 已整理 8.上交三分法降自由度优化yaw角（最后采用这个方案） 9.同济暴力搜索降自由度优化yaw角 （近距离存在问题） 10.识别角点优化(fitLine最小二乘法获得灯条角点 -\u0026gt; 优化后降自由度产生良好优化效果,0度范围角度不再跳变) 11.西工大mpc轨迹规划器(半成品) 12.约束平面求相机装配误差和完整误差补偿 -\u0026gt; 考虑使用RANSAC算法重构 13.弹道重现 -\u0026gt; 考虑后续实现弹道闭环 14.手眼标定(半成品) -\u0026gt; (传统手眼标定, 先进手眼标定(版本不够)) 15.火控流水打弹(500血 30转 3m 9s 70%命中率) 16.空气阻力弹道模型(存在bug未查明) 17.视觉弹频控制(旋转移动命中率到38%~50%) -\u0026gt; 效果不太好会影响dps -\u0026gt; 被废弃 18.同济mpc(未落地) -\u0026gt; 规划存在提前减速的效果 -\u0026gt; 电控和机械跟不上 但是由于: 开火时延是同济的十倍(200ms),20个时间片后的规划和预测云台做火控效果很差,双环pid跟随存在稳态误差需要添加电控时延但是20个时间片的预测效果就很差了,目前旋转移动目标,小弹丸命中率只有传统方法的一半(25%) 未来: 电控机械尽量减少开火延时 mpc规划的速度和加速度作为前馈发送给双环pid作为前馈或者考虑计算力矩控制算法(提升云台的跟随能力) 19.参考BA优化的思想优化pnp算法的测距(失败) 20.PCA主成分分析法(Principal Component Analysis, PCA)方法优化角点(哨兵没部署) 21.识别和相机采集多线程(初步编写) 22.全向感知(多相机采集调度)(仅初步学习线程池) 放弃多线程方案 -\u0026gt; 多进程方案已初步启动两个相机 -\u0026gt; 初步实现等待部署 23.ssh+自瞄网页调试 初步实现 24.修降自由度bug，修流水火控bug(调的参数好的情况下,火控有大的提升，英雄低速目标颗秒，高速静止目标100w麦轮50%命中率) -\u0026gt; 但是参数敏感每隔几个小时参数就会变,而且实战环境复杂,命中困难 25.迭代法求子弹飞行时间(无空气阻力) -\u0026gt; 有空气阻力曾经实现过未通过测试 26.多段测距的弹道硬补偿(弹道标定) 已测试 效果不错 但调参成本大 27.自瞄网页调试器 初步实现 28.自瞄Python单测(计划实现) 29.自瞄ros2重构(计划实现) 30.全向感知技术(半成品) -\u0026gt; (计划实现) 31.针对前哨和远距离目标的新火控(计划实现) -\u0026gt; 远距离(单点) 前哨(跟随上再击打) 32.给自瞄接入gazebo仿真 初步实现\n目前自瞄局限性: 1.自瞄还未考虑发弹延迟 -\u0026gt; (解决方法: 看同济上交补偿) -\u0026gt; 交: 历史查询 同济: mpc提前开火 2.无法测出弹道误差和枪管装配误差 -\u0026gt; (解决方法: 上交弹道闭环, 自研(弹道拟合)) 3.CV模型对于平移目标缺乏良好跟随效果 -\u0026gt; (PID跟随效果(给差分前馈)，CV模型加速度突变的过量预测，电控时延的补偿) -\u0026gt; (解决方法: 合理稳定的CA模型) 4.远距离pnp测距误差带来的落点偏低 -\u0026gt; (解决方法: 长焦 激光测距 等(没钱目前没有比较好的方法) , 双目标定(效果微弱)) pca角点优化 5.弹道解算无空气阻力模型带来的落点偏低(甚至比pnp测距还严重) -\u0026gt; (上交 四阶龙格库塔弹道解算) 由于测装甲板存在问题,又由于远处弹道落点受空气阻力影响指数下降,导致击打旋转目标命中率较低 6.mpc半成品 -\u0026gt; (同济 西工大开源 和电控协商如何调) -\u0026gt; 电控需实现(中科大)力控算法 7.ros图像传输延迟大极大限制自瞄帧率(采集识别处理多线程(共享内存)) 8.前哨战自瞄命中率低\n调试记录: 调全向 难点: 无特殊难点，但是代码开发最开始从全向部署 花了点时间\n调英雄: 难点: 最难调,断代,串口不通,自动开火不对,弹速低高俯仰角看不见目标 低弹速低弹频,自制装甲板一打就烂,打印件枪管一打就歪一打就烂,弹道极度不稳定 机械安装C板和相机倾斜角最大导致视觉坐标系不正确,偶尔卡弹,发弹延迟不稳定 对火控要求异常严格\n调哨兵: 难点: 串口不同部署自瞄花了点时间 -\u0026gt; 可能会存在自瞄和导航串口存在冲突 弹道难调 -\u0026gt; 机械电控已解决\n调麦轮: 代码烧上去就能跑 第一正式测试命中率 30转2Hz的弹频产生59发中50发的命中 80转的转速勉强有60%的命中率(缺陷: 自动开火和发弹延迟,还有时间戳的对齐)\n卡尔曼调参手册: 卡尔曼公式: 1.预测值 2.先验误差协方差矩阵 3.卡尔曼增益 4.最优估计值 5.后验协方差矩阵 IMG_20251127_193011.jpg\n卡尔曼滤波核心参数 过程噪声Q: 模型预测的不确定性 观测噪声R: 传感器测量的不确定性 状态协方差(P): 状态估计的置信度\n基本原则: 响应迟缓 -\u0026gt; 增大Q(过程噪声)或减小R(观测噪声) 过度震荡 -\u0026gt; 减小Q(过程噪声)或增大R(观测噪声) (个人习惯每次调节两个参数,最开始参数10%d的范围浮动,后面5%，再后面3% \u0026hellip;\u0026hellip;)\nQ和R的具体数值应该由过程噪声和测量噪声的外积期望决定,如果我们了解这些噪声的大小,那么可以直接把Q和R算出来,做仿真的时候这是完全可以算出来了,算出来按道理就是最优估计了,后面的就可以不用看了,但实际情况大多是噪声大小知道是什么范围,但又不是特别了解,那就选择一个心目中觉得最有可能的大小,先算出来一下Q和R，带进去，这就是是错的试错点\n调参: 当Q过小时,易发散,当R过小时,误差会变大,同时如果搭配观测量同时看,R过小时滤波输出会趋于观测值 调试时可以先将Q从小往大调整,将R从大往小调整;先固定一个值去调整另外一个值,看收敛速度与波形输出\n协方差具体调参: (协方差矩阵,是通过过程噪声Q和观测噪声R动态计算的)\n协方差发散 （P矩阵值持续增大） : 现象: 估计不确定性增加,跟踪不稳定 措施: 减小过程噪声Q或增大观测噪声R\n协方差过小 (P矩阵快速收敛到零) ： 现象: 滤波器过于自信,不响应新的观测数据\n协方差初始化: 初始化为很小的值\n协方差健康指标: P.trace(): 应该稳定在合理的范围,不是持续增大或减小到零 P.diagonal: 对角线元素应该反映各状态量的不确定 P的收敛速度: 应该在几十个周期内稳定收敛\nTracker: COMMAND_TIMESPAN: 0.11 local_gravity_: 9.80665 eTime: 0.002 begin_radius_: 0.26 trackTime: 0.7 frame_time: 0.02 pitch_compensation: 0.004 yaw_compensation: -0.020 FA: 1.0 rotate_time_max: 0.5 rotate_count_max: 13 score_tolerance: 1.0 switch_threshold: 15 aim_angle_tolerance: 40 aim_pose_tolerance: 0.05 aim_center_angle_tolerance: 1 switch_trackmode_threshold: 1.2 aim_center_palstance_threshold: 6.5\nModelObserver:\n参数调整原则 过程噪声调大: 滤波器更信任测量值,响应块但可能更震荡 过程噪声调小：滤波器更信任预测值,相应更平滑但可能滞后 观测噪声调大: 滤波器更信任预测值,对测量误差不敏感 观测噪声调小: 滤波器更信任测量值,对状态变化更敏感 响应迟缓增大Q减小R,过度震荡减小Q增大R dt: 0.03 # 这个参数的值是固定的,由帧率控制器决定(帧数稳定,协方差矩阵变化小,计算量小,运算速度更快且不会中途改变卡尔曼的数值,卡尔曼更稳定)\nStandard: # 初始化半径 init_radius: 0.2\n# 动态调整距离观测噪声的角度依赖性 # 角度大 + 距离跳动 -\u0026gt; 增大gain # 角度大 + 距离滞后 -\u0026gt; 较小gain # 先调基础参数,再调增益参数 # 大角度距离跳动 # 现象: 目标侧对时距离估计上下跳动+-0.2m # 诊断: 距离噪声角度依赖性不足 -\u0026gt; 增大增益 # 大角度响应滞后 # 现象: 目标转向时距离跟踪明显滞后 # 诊断: 距离噪声角度依赖性过强 # 0角度测量噪声大 # 现象: 角度测量本身不准, gain放大噪声 # 诊断: 需要改善角度测量或调整策略 gain: 25 # 过程噪声(表示模型的不确定性)Q process_noise: # 位移的高阶差分噪声.控制位置和速度变化的不确定性,值越大滤波器对新测量的相应越慢 displace_high_diff: 1.653 # 中等位移噪声,推荐调参范围:() # 角度的高阶差分噪声.控制角度和角速度变化的不确定性 anglar_high_diff: 20.0 # 中等角度噪声,推荐调参范围:() # 高度噪声.机器人高度变化的不确定性 height: 0.0001 # 较小高度噪声,推荐调参范围:() # 旋转半径噪声.机器人旋转半径变化的不确定性 radius: 0.0002 # 较小半径噪声,推荐调参范围:() # 观测噪声系数 R measure_noise: # 位置观测噪声,视觉测量位置的不确定性 pose: 0.001995 # 距离观测噪声,测距的不确定性 distance: 0.01235 # 角度观测网噪声,角度怎两的不确定性 angle: 0.008 Balance: # 初始半径 init_radius: 0.2 # 测距噪声关于角度的增益的函数 gain: 30\n# 状态转移噪声系数 process_noise: # 更小的位移噪声,因为平衡步兵运动模型更可预测 displace_high_diff: 0.2 # 较大的角度噪声,因为平衡步兵旋转更频繁 anglar_high_diff: 10 # 更小的高度噪声,高度变化较小 height: 0.0001 # 更小的半径噪声,旋转半径更稳定 radius: 0.00001 # 观测噪声系数 measure_noise: # 更小的位置噪声,假设视觉检测更准确 pose: 0.002 # 更小的距离噪声,测距更准确 distance: 0.01 # 较大的角度噪声,因为平衡步兵角度变化大 angle: 0.5 Outpost: # 很高的增益,因为前哨战固定,距离测量噪声角度变化影响大 gain: 100\n# 状态转移噪声系数 process_noise: # 极小的位移噪声,因为前哨战基本不动 displace_high_diff: 0.0001 # 较小的角度噪声,前哨战有小幅度固定的旋转 anglar_high_diff: 0.4 # 极小的高度噪声,高度固定不变 height: 0.000001 # 观测噪声系数 measure_noise: # 很小的位置噪声,固定目标检测准确 pose: 0.002 # 中等距离噪声 distance: 0.04 # 较小的角度噪声,角度测量相对准确 angle: 0.1 pose(位置观测噪声): 控制装甲板在x,y平面位置的噪声 在观测噪声矩阵中对应前两个对角线元素 影响因素: 1.视觉检测的2D定位精度 2.相机标定误差 3.图像分辨率和检测算法稳定性\ndistance(距离观测噪声): 控制装甲板高度z轴的观测噪声 在代码中实际对应的是深度方向的观测不确定性 影响因素: 视觉检测的2D定位精度 相机标定误差 相机俯仰角标定误差\n先调pose再调distance再联合调\n重投影调参: 1.预测点抖动 -\u0026gt; 观测噪声过小或过程噪声过大 2.移动过快乱跳 -\u0026gt; 动态响应能力不足 3.高频抖动 -\u0026gt; 滤波器对观测噪声过于敏感 4.快速移动发散 -\u0026gt; 过程噪声不足以描述动态特性\n验证标准: 1.新息应为零均值白噪声 2.协方差矩阵稳定收敛 3.重投影误差在可接受范围内 4.实际跟踪效果良好\n调试输出分析: 新息分析: 新息过大: 模型不匹配需增大Q或R 新息过小: 滤波器过于保守,需减小R 新息有差: 系统模型有偏差\n协方差矩阵分析: P快速收敛: 可能过于拟合 P发散: 模型不稳定,需减小Q或R P震荡: 需平衡Q和R的比值\n注意 1.每次只调节1~2个参数,观察效果后再继续 2.调节一个参数可能影响其他状态量的： (这里需要看代码研究,还没有仔细研究过) :\n3.代码中有已经实现好的卡尔曼滤波器参数的打印函数，和rqt赋值打印的函数,它们被注释了，如果有需要的话再使用\n自瞄核心算法 卡尔曼滤波器理论推导 第一张 23c9b9379c95f215de0b457f7c19c2d.jpg\n第二张 54a1a7ab4834b143b787bbf638a246e.jpg\n第三张 b4e335a86add53ecd136d70faaf13a9.jpg\n第四张 ef674c0bd697f58fe8a2ab2e9c97b81.jpg\n第五张 6071defc362061fca6e391b11014127.jpg\n第六张 7b688e4a3fe2b5e0d14799f0a1e6043.jpg\n补充: 一.初始状态x0的设置 有先验信息的情况 已知初始状态: 如果有确切测量或已知系统启动状态,直接适应该值 部分已知: 已知部分状态分量,未知分量设为0或合理估计值 无先验信息的情况 使用第一次观测值: 如果观测与状态直接相关 设为0或中性值: 对无偏系统,可初始化为0\n二.初始误差协方差的设置 p0的意义: 表示对初始状态估计的不确定性 对角线元素: 各状态分量的方差 非对角线元素: 状态分量间的协方差\n基本原则: 不确定性越大,值设得越大\n坐标变换 后续补充\nyaw角优化(降自由度) 后续补充新降自由度 -\u0026gt; 以及对降自由度重投影代价函数的一些理解\n角点坐标定义重大注意点: IMG_20251130_211216.jpg\n上海交通大学角点坐标变换开源阅读推导草稿: 687962de7ce238c2908b16d6b143c99.jpg\n同济大学角点坐标变换: (和pnp相同) 7e0897896b0dbba1a4f83d5a91b0fbb.jpg\nMPC轨迹规划器 AI说的 b129ecdb7f60c1ad79a494c3cd92616.jpg\nmpc理论推导 第一张 72adb7199447b60a39ec59a5c5b7eed.jpg\n第二张 2b5c17208d0bf44560b9d750d9acd41.jpg\n第三张 ef674c0bd697f58fe8a2ab2e9c97b81.jpg\n西工大轨迹规划器文档 edee712b741620207236090167be7ce.png\n调参\nMPC: debug: 0 # 预测步长 # step太小: 优点: 计算快,响应迅速 缺点: 目光短浅,长时优化差 # step适中: 平衡性能与计算,需要合理调参 # step太大: 优点: 前瞻性好,稳定 缺点: 计算量大,易发散 step: 5\n# 过程误差权重 # Q代表对预测时间段内误差的重视程度 # Q0较大 -\u0026gt; 非常在意pitch角度误差,会快速调整 # Q0较小 -\u0026gt; 可以容忍一定的pitch角度误差 process_error_weight: # pitch角误差权重 # 控制云台在俯仰角方向上的瞄准精度 Q0: 10000 # yaw角度误差权重 Q1: 10000 # pitch角速度误差权重 # 控制俯仰方向的速度平滑性 Q2: 1 # yaw角速度误差权重 # 控制偏航方向的速度平滑性 Q3: 1 # 控制权重 # R代表对预测时间段控制量的重视程度 # R0较大 -\u0026gt; MPC会尽量使用小的控制量,更平滑但响应慢 # R0较小 -\u0026gt; 允许使用更大的控制量,响应块但可能震荡 process_control_weight: # pitch控制量(角加速度)权重 # 惩罚pitch方向的控制努力 -\u0026gt; 避免发送过大的pitch加速度指令 R0: 10 # yaw控制量(角加速度)权重 R1: 1 # 终端误差权重 # F表示对预测时间段后的这一时刻的误差量的重视程度 target_error_weight: F0: 10000 F1: 10000 F2: 1 F3: 1 # 终端权重对控制行为的影响 # F太大: # 行为: MPC变得急功近利 # 表现: 一开始猛打方向盘,试图快速到达终端状态 # 问题: 过程中剧烈震荡 # F太小: # 行为: MPC变得\u0026quot;目光短浅\u0026quot; # 表现: 只关心眼前误差,不计划长远 # 问题: 终端时刻误差大,打不中移动目标 # F合适: # 行为: MPC\u0026quot;眼光长远,脚踏实地\u0026quot; # 表现: 平衡过程跟踪和终端精度 # 结果: 平衡跟踪,最终准确命中 零参考轨迹MPC： 目标： 将系统状态调节到零点 特点: 参考轨迹恒为零 应用: 姿态稳定,平衡控制 损失函数形式\nJ = Σ [x(k+i)^T Q x(k+i) + u(k+i)^T R u(k+i)]\n带参考轨迹MPC： 目标: 跟踪时变的参考轨迹 特点： 参考轨迹是随时间变化的,即x_ref是时变的 应用: 轨迹跟踪,路径跟随 损失函数形式:\nJ = Σ [e(k+i)^T Q e(k+i) + u(k+i)^T R u(k+i)] 其中 e(k+i) = x(k+i) - x_ref(k+i)\nMPC不仅有位置模式,还有速度位置模式\n同济mpc: 08405ad115ad4c90e84a64e4dc1174e.jpg\nd9c7c0c23d26734d2ab93246017d3f6.jpg\n梯度下降法: 后续补充\n共轭梯度法: 共轭梯度法是一种迭代优化算法，专门用于求解: 1.线性方程组: Ax = b (A对称正定) 2.二次规划问题: min1/2xTHx + fTx\n普通梯度下降法在复杂地形中会\u0026quot;之字形\u0026quot;前进 问题: 每次只考虑当前梯度方向,可能重复探索相同方向,收敛慢\n共轭梯度法: 共轭梯度法通过构造一组共轭方向 每个方向只探索一次 在这些方向上达到最优 关键思想: 当前搜索方向与之前所有搜索方向关于H共轭\nfitLine最小二乘法获得灯条角点 e8094fbc42cfb0eb32076ed89f1a994.jpg\n最小二乘法(Least Squares Method): 最小二乘法是一种数学优化技术,通过最小化误差的平方和来寻找数据的最佳函数匹配 简单来说: 找到一条线（或曲线）, 使得所有数据点到这条线(或曲线)的垂直距离的平方和最小\n光束平差法: 在SFM和SLAM中,通过最小化重投影误差优化相机参数和3D点\nPnP算法与PnP问题 PnP(Perspective-n-Points)问题: 给定n个已知3D空间点和它们在2D图像上的投影点,以及相机的内参,求解相机的位姿 image.png\n命名由来: Perspective: 透视关系(不是正交投影) n: 已知3D-2D对应点对数量 Point: 点对应关系\n不同n值的情况: n = 3 P3P 最少点数,有解析解 最多4个解 n = 4 P4P 通常有唯一解 1个或有限个 n \u0026gt;= 4 PnP 最小二乘解 通常唯一 最小点数: 理论上,n\u0026gt;=3即可求解,但实际中n\u0026gt;=4 实际工程中为什么多用4个点: 提高稳定性,减少歧义\n相机安装误差测定逻辑 由于观测存在噪声,该方案已废弃 -\u0026gt; 目前已更换为经验调参 -\u0026gt; 详情见下方 电控视觉(机械硬件)联调测试手册\nIMG_20251206_195538.jpg\n相机结构: 光圈 -\u0026gt; 控制曝光 镜头的相对光圈值 F(相对值): 数值越小代表实际的物理光圈越大,数值越大代表实际的物理光圈值越小 image.png\n入瞳径 (很大程度由光圈决定): 可以理解为从镜头前看向其内部的视觉开孔直径,它并不是真实的光圈叶片所控制的物理孔径,很多大光圈镜头前镜组采用高曲率或高折射率的镜片\n光圈F值与成像面照度是近似平方反比的关系: image.png\n光圈 -\u0026gt; 景深: (光圈和被摄物体与相机的距离都会影响景深) -\u0026gt; 距离的改变影响了光线的角度\n焦点前后一段相对清晰的范围 光圈越小清晰的范围越大,一般称为深景深或大景深 反之光圈越大清晰的范围越小,称为浅景深 image.png\n直接影响景深的是入瞳镜,而非物理光圈 image.png\n容许弥散圆: 摄影和光学领域中一个非常关键的基础概念,它直接关系到我们对图像\u0026quot;清晰\u0026quot;与否的判断标准\n容许弥散圆,焦深,景深的关系 光圈变小，入瞳径变,光线的入瞳夹角变小,原来的光锥变窄,但是容许弥散圆的直径固定不变,此时想要对应到光锥上就要把容许弥散圆向两侧延伸，焦深就变大了,对应的景深就变大了 image.png\n计算景深范围的公式: image.png\n光圈叶片的数量和形状 -\u0026gt; 影响着焦外光斑是否圆润\n光圈狭小造成的衍射会对成像质量有所限制\n快门 -\u0026gt; 控制曝光 前帘: 通常先打开让传感器感光的叫做前帘 后帘: 之后关闭结束感光的叫做后帘\n当按下快门后前帘落下,相机清空CMOS中的电荷,并开始感光，之后前帘抬起,传感器(CMOS)从下至上依次接收光线,到达设定时间后后帘抬起,此时CMOS仍处在感光状态，等所有信号逐行读出完毕,后帘复位,一次拍摄完成\n单幕帘 单程速度 1/250s(恒定的) 曝光时间恒定4ms image.png\n所以相机通过间隔来精确控制曝光 image.png\nimage.png\nx轴为CMOS的像素列 y轴为像素行 z轴为时间长\nimage.png\n前后幕帘开合的间隔越大，CMOS接收光线的时间也就越长 image.png\n果冻效应: 遮盖感光元件 也常称为卷帘快门效应,是使用CMOS传感器相机在拍摄高速运动的物体或快速移动镜头时,图像中出现扭曲,倾斜等变形现象的一种常见图像失真问题.\n解决方法: 减少相对运动 提高读出速度\n全域快门： 可以同时控制电路开启,曝光之后把电子先转移到存储单元,之后再读出数据(信号),这样就不存在曝光时间差,也就能完全避免果冻效应\n由于给每一个像素增加了额外的存储单元,会降低像素接收光线的能力，进而对信噪比和画质产生影响,贵\n动态模糊: 加入适当的动态模糊,可以使视频更流畅 由相对运动轨迹产生的模糊 影响因素: 1.曝光时长 2.物体运动\n帧率决定了快门速度的上限 (快门速度最大也只能使帧率的倒数) image.png\n快门开角: 快门开角,本身是一个传统的摄影与摄像领域专业术语,它描述的是胶片相机中旋转快门叶片开口的大小,直接影响曝光时间\n频闪： 通常是因为快门速度的设定与人造光源的交流电频率不匹配而导致的\n其他快门类型: 机械卷帘快门 电子前帘快门 电子卷帘快门 镜间换门: 在镜头中间光圈旁,在镜头内部,CMOS可以整体感光，不存在时间差,也不存在果冻效应\n火控 状态机: b9c22015315374afe2fb0cb16ccdcf0.jpg\n测试方案: a86070f0b1084c3b9bd86571d494c7e.jpg\n自瞄目前对于开火延时的一些研究:\n(当年我和我学长说的原话) 其实我突然想到，发弹延时大的问题本身并没有解决，但是如果跟随足够好的情况下，就可以保证发出发弹指令后的0.1s(也就是发弹延时的平均大小)云台和期望仍然重合，就可以保证子弹命中。即使这个发弹延时不稳定。 也就是说如果跟随足够好 什么时候开火，是否提前打弹这时候已经不重要了，因为只要开火空间无限大，任何时候开火都可以打得中\nPCA角点优化 c312d390225583c3f73cb8193ceaf91.jpg\n视觉组调参指南(参考别的学校的) 机械装上相机之前： 认真检查相机焦距和光圈是否已经固定住，通常将光圈调整到最大，然后将焦距调整到能清晰看到2 6m左右距离的装甲板数字(使用GalaxyView)。(超过这个范围，就算识别到，PNP解算误差也很大，而 且操作手也不会打蛋，所以不用过多纠结识别距离)。 通常上位机与下位机的接口包含串口和相机，但是由于上位机(TX2或者NUC)的USB接口在多次插拔之 后可能会导致插不紧，此时一定要给连接的USB口打上胶。不能认为平时调试时没有出现过串口断开 或者相机断开的情况就认为不存在USB口松动这个问题，即便是打过胶，也应当在强烈碰撞的条件下 测试(例如两车碰撞，飞坡等)。 注意看相机前的保护镜片是否刮花导致相机成像出问题(这个问题很容易忽略!!)\n相机外参整定: 如果你熟知坐标系变换过程，那么你应该知道，唯一需要填补的参数，就是相机坐标系到枪管坐标系 的平移向量(还有一个旋转向量下面再测)。一般上在设计上相机坐标系到枪管坐标系仅仅差了一个平 移，这里就需要对两个坐标系的中心点位置作分析。相机坐标系不用说中心点一定是在镜头中心位 置，而枪管坐标系的中心点就需要分析了。因为枪管坐标系进行旋转就到了基坐标系，也就是最终的 坐标系。我们在最终的坐标系下进行了抛物线的计算，那么枪管坐标系的中心点应恰好不受到枪管的 支撑力，即在枪管出口位置。通常将x轴平移量设置为0，y轴向前，z轴向天空。y,z轴的值应当让机械 在3维图纸上测，至于值的正负，你可以改变一下符号，看最终的坐标输出自行判断。\n相机内参整定: 推荐使用大恒自带的SDK，里面可以找到一个叫GalaxyView的可执行文件，打开这个可执行文件，可 以很方便地对相机的各种参数进行调节，同时可以很方便地对标定板拍照，作为标定的图片使用。可 以使用Matlab工具箱作为标定工具。至于是否需要标定出 , , ,至今还无定论，有兴趣的可以研究 一下。 p 1 p 2 k 3 相机标定通常有很多技巧，不能随便标，否则后面PNP计算误差就会更大。一个基本原则是标定板在 图片中需要占据一半以上(即使标定格清晰一点),每次拍图片时，可以动云台，也可以直接动标定板， 拍摄多个角度的标定板，标定出来的参数才有普适性。另外，图片的亮度，白平衡等也会对标定结果 产生影响，故标定前需要确保选择自动白平衡模式，图片亮度要至少能清晰看见方格的角点。 GalaxyView内可以看到相机的SN号，如果程序需要的话可以在GalaxyView中查看并且在程序的json中 修改。\n识别调参: 相机识别参数整定: 相机识别参数主要需要调整的: 曝光: 曝光是相机识别参数中最为重要的一个参数,曝光的调整的优劣会直接影响你识别程序效果的好坏。调节曝光的基本原则是: 不能调节得太低,太低的曝光会导致灯条变得很暗,二值化会出问题不说,还会截掉一部分的灯条,导致PNP计算不够准确。其次,越远的物体在相机中看到的亮度越低,如果曝光调节得过低,识别距离就会远远减短,如果识别距离小于4m,那么实战中就很容易断识别 不能调节得太高,如果调节得太高的话,由于形态学对噪点的处理总是有限的。如果曝光调得很高的话,并且分类器不够给力的话,就很有可能产生误识别 总结依据,就是曝光应当在你程序支持不误识别的前提下尽可能往高调\n增益: 增益通常是在曝光满足不了的情况下调节,通常不应该调得很大.所以调节频率也比较低,至于具体效果怎样你可以具体实践体会一下.\n至于相机得其它参数，一般影响不大，可以不用管。值得注意的是相机中内置自动曝光，自动增益这 些功能，这些算法(也包括你所能查到的大部分自动曝光算法)通常是以图像整体观感作为前提实现的， 而我们感兴趣的区域只有装甲板那一小部分，故这些功能都是价值不大的，不太建议去探索。 你可能不太想使用自动白平衡，而非要在赛场上用一张白纸来进行白平衡标定，虽然可能是一个可行 的办法，但我觉得自动白平衡效果已经可以了，没什么必要将调参变得这么麻烦。\n识别算法参数整定: 首先聊一下识别算法，自从2021年上交四点模型横空出世，很多人认为深度学习是RM装甲板识别的最 优解。但是，今年哈工程使用传统视觉识别也展现出了惊人的鲁棒性，这值得令我们深思，是传统视 觉不行，还是我们自己本身对传统视觉的理解并不够。深度学习虽然能避免曝光的影响，但是也会使 模型不透明，导致出现一些莫名其妙的误识别(比如视觉群大佬们讨论的在前哨站附近的装甲板全部识 别为前哨站)。所以我认为，我们既要开发深度学习方向的装甲板识别，也要将这个从RM诞生至此的 传统算法彻底吃透。\n图像预处理: 图像预处理算法通常可以分为四种类型 1.使用灰度图进行二值化,最后对像素判断颜色. 2.使用地方颜色的通道减去己方通道(或者绿色通道) 3.以上两者混合,即使用(灰度图或者通道相减图)然后再二值化. 4.使用HSV进行通道二值化 最后预处理的方案选择了第一种，理由是使用灰度图能够避免灯条断开的问题(如果在较高曝光或者较 高增益的情况下，灯条中心像素值较高，使灯条中心发白，如果使用通道相减或者HSV方法，可能会 导致二值化后的图形断开)，虽然仅仅使用灰度图使后面对每个轮廓的颜色校验增加了一定的计算量， 而且也会导致筛选出来的灯条变多。\n灯条 \u0026amp; 装甲板 特征筛选 为了解决预处理带来的弊端，需要对灯条的几何条件进行严格的约束。这里我感觉在上一个赛季并没 有做得很好，因为在上一个赛季中，对灯条以及装甲板特征的约束，都是给一个比较大的约束范围， 但是，这些较大的约束范围而不是合理的约束范围会使几何特征较差的\u0026quot;灯条\u0026quot;以及几何特征较差的\u0026quot;装 甲板\u0026quot;混入正确的灯条或装甲板中，如果将这些错误的装甲板都丢进分类器内的话，分类器又做不到完 全的识别准确，必然会导致最终识别的不稳定。因此，我觉得，这些几何约束参数应该认真地调整一 下!!!调整的方法也很简单，先给一个比较严格的参数约束，逐步移动目标位置(远近)，或者改变装甲板 类型(大小装甲板),如果识别不到就根据输出数值再稍微扩大一点(至于这个度应该在调参过程自己感 悟)。\n数字分类器 目前使用的分类器是 HOG + SVM 分类器，该分类器的优点是对光照变化有一定的鲁棒性。\n预处理参数 目前将装甲板数字裁剪出来并且经过仿射变换之后，由于图像在低曝光的前提之下较暗，数字可能会 不太明显，故需要对图像进行一次gamma矫正，使图像整体变亮(当然也可以考虑直方图均衡化等预处 理方法)。gamma值越低，数字越亮，但噪点也会越多,故gamma值需要调节到一个较为合适的值，至 少肉眼能够清晰分辨数字。\n卡尔曼滤波: image.png\n观看青工会: 步兵自瞄和英雄前哨视觉算法 (步兵自瞄) 1.pnp精度不够导致角度跳变问题(0度附近有几十度的跳变) image.png\n解决方案: 思路一: 降自由度 image.png\nimage.png\n2.装甲板半径计算 (1).几何半径计算 (2).ceres半径计算\n3.打击目标选择 步兵追求高速打击,利用多段延迟 (-orietation_angle ~ orientation_angle)\n英雄追求高命中率打击,计算orientation_angle = 0时的装甲板状态\n打击衡量指标 image.png\n4.打击衡量指标\n5.自动弹道校正 可视化\n6.延迟组成 img 相机曝光时间的中点 predict 经过神经网络,预测器预处理完成后,开始进行运动和目标解算的时间点 send 预测进程结束,准备发出信号的时间点 control 电控接受到信号后,电机开始运动的时间点 fire 信号所指示的子弹发射的时间点 hit 信号所指示的子弹击中的时间点\n实测发现,control到fire确实存在几十毫秒延迟,我们无法保证电控接收到control命令时,该命令的子弹同时发射,就像发射水流一样.由于视觉总是可以让电控在control时间点达到命令的位置,我们希望control时间点发出的子弹能击中目标\n7.弹道闭环 重现弹道后,识别相机弹丸,将识别到的弹丸和模拟的弹丸进行匹配,一堆匹配点采样了一个理想弹道和实际弹道的误差.将误差放入滤波器即可进行弹道校验\n(英雄反前哨站视觉算法) 1.瞄准绿灯 2.测距(单点激光) (1).长焦 + PNP (2).单点激光 (3).(激光雷达 + 绿灯材质特定反射率) (4).(激光雷达建图 + 标定相机和激光雷达) (5).(激光雷达 + 点云神经网络识别前哨战/基地)\n3.弹道解算 (1.一阶空气阻力模型 (2.四阶龙格-库塔弹道积分算法 image.png\n4.细节修正 (1.激光\u0026ndash;相机陀螺仪修正 (2.相机\u0026ndash;激光安装修正 (3.机械发射修正\n(视觉反旋转前哨) 背景 1.前哨转速固定,陀螺中心固定,陀螺半径固定 2.英雄的云台可能响应较慢\n算法概括: image.png\nimage.png\nimage.png\nimage.png\n（关键: 滤波器2为理想滤波器,预测仅使用0.4r/s或0.2r/s的转速）\n杂谈: 该算法的前提是机械发射散度好,发射延迟稳定 该算法调参只需要调落点和dt 实际在过滤顶板时需要考虑诸多细节,例如绿灯识别结果会跳帧,装甲板识别结果会有顶,底,顶+底三种情况 有时由于各种原因,装甲板识别结果也可能会跳帧漏帧,很可能导致发不出弹,解决办法可以是在没有识别结果时也利用滤波器已经积累的数据进行与预测和发弹\n电控关键保证: 散度(用复写纸来看散度分布),弹速延时(给出发弹指令和裁判系统反馈有测试数据,在这之间做一个时间查看,这时间差波动有多少) -\u0026gt; 通过这两个来验收标准\n散度(8米有一个装甲板的散度) 延迟(正负10%) -\u0026gt; 机械结构没有问题一般波动不会很大\n短板: 1.远距离测距不准,导致落点偏低(pnp测出来的偏近) 2.打陀螺转太快了跟不上\n弹速过热 -\u0026gt; 自适应弹速(电控端做)\nws_glut_vison编译指南 catkin_make \u0026ndash;pkg rm_msgs catkin_make\n电控视觉(机械硬件)联调测试手册 记得应用测试代码（别测了半天发现是老代码） 测试必须一步一步来,必须保证上面的没有问题了再测试下面的\n补充: 一些经验: 1.如果rviz没有上传坐标系: (1).可能是串口掉了 (2).可能是识别死了(由于存在节点重启此时rosnode list的判断其实不准可能其实是节点在不断死掉再重启) 2.如果自瞄重投影在大概率视觉是可以接收到电控的串口(但是不保证电控能接收到视觉的串口，可能是线会存在问题) -\u0026gt; 最新的串口线可以直接看出视觉有没有给电控发数据,电控有没有给视觉发数据\n1.相机标定 标定相机（具体流程见上面） （标定完相机后和机械说千万不要动到相机的焦距和光圈（螺丝要拧紧）） 应用相机标定的参数到代码中(这段代码中需要更改两个地方) (一个在rm_tracker的yaml里面一个在rm_identify的yaml文件里面) -\u0026gt; 注意保证使用的是 该兵钟的yaml文件,两个文件的yaml文件相同\n2.安装摄像头 安装相机 找机械要相对位置 视觉这边更改坐标变换位置 开rviz看上传tf数据和实际安装对不对 相机和C板安装会有装配误差,需要误差函数测算,或者使用手眼标定\n4.部署代码 参考自瞄移植手册 -\u0026gt; 更名为: 自瞄赛前代码检查+备忘录\n5.测试串口通信 电控先测试串口线能不能用: (如果不能用是不是线序的问题,或者线本身的问题)\n先插上串口线,完整启动自瞄代码,此时相机线还没有插上,由于视觉这里代码的逻辑,此时视觉这里是单方面 只接收数据不发数据,这时候打开rviz观察有没有tf坐标上传(串口节点发给自瞄节点的回调函数,传过来坐标数据,在识别主函数上传) , 然后这时候动一下头,pitch和yaw，如果跟着动了说明接收数据没有太大问题，同时对应观察电控那里有没有发送数据过来\n接着在上面的前提下(不关闭程序,不拔串口线)，插上相机线,这时候,触发自瞄这边的逻辑,自瞄这边就会发送数据给电控,然后观察电控有没有接收到数据,他接收到数据后有没有发送数据过来(如果没有: 可能是发送数据到电控那里后代码在它们那边挂掉了(越界,或者优先级问题导致进程饿死了)),同时视觉这边可以\nrostopic list rostopic echo auto_angle 观察自瞄节点有没有发送节点数据给自瞄串口节点(如果视觉串口节点实现没有问题应该会正常发送数据给电控)\n测试视觉接收和发送电控解析出来的数据对不对,同时测试电控接收和发送视觉解析出来的数据对不对 （这步一定要做好,尤其要确定解析出来的顺序对不对,单位转换之间的协议对不对,不然可能会）\n6.调串口补偿: 串口补偿(改两个地方串口的yaml文件和瞄准的yaml文件) 经验调参串口硬补偿(串口那边的yaml参数和瞄准那边的yaml参数应该相同) 前后平移装甲板,当两侧装甲板不会因为前后平移导致重构出来的装甲板往上偏或者往下偏说明参数是正确的了\n7.硬补偿(瞄准yaml文件) 电控机械需保证弹速和视觉给的符合一致,弹道水平 -\u0026gt; 在此基础上才能调弹道 单一硬补偿: pitch补偿 减小往下 增大往上 yaw补偿 增大往左 减小往右\n多段距离硬补偿: 在单一硬补偿的基础上: 根据目标距离分段施加固定的pitch,yaw补偿,用来修正解算模型,安装误差,弹道参数等带来的距离相关系统残差\n在某个约定距离上,调整补偿角直到弹丸命中预定瞄准点,记录此时的附加角度 当PnP/迭代距离\u0026quot;不等于\u0026quot;真实物理距离时怎么办： 用系统自己的距离输出作为查表依据 1.标定时记录\u0026quot;视觉距离\u0026quot;而非真实距离 2.分段阈值用视觉距离来划定\n微调端点时,关注区间内弹着点散布是否集中考虑: 1.给枪管清灰看看效果怎么样 -\u0026gt; 枪管积灰过多会导致散步变大 2.检查电控摩擦轮差速(一般是要求3转以内) 3.检查机械\n提醒: 关摩擦轮时,摩擦轮会掉速会导致落点偏低,此时不要被干扰 摩擦轮打久后会导致摩擦轮温度上升,导致弹速上升(俗称打爽了)\n8.调跟随(视觉预测时间(电控时延(额外发送量)) + pid跟随) (1)调电控时延让电控听话 上交文档: 由于控制的复杂性，我们尚未专门解决非匀速运动目标的问题。我们将所有运动短期近似为匀速运动。其实考虑到打击总延迟为数量级为 100ms，大多数运动确实可以这么近似。 对于匀速目标,再作一近似，也就是电机转动也是匀角速度的,在该情况下,我们给出的yaw命令(标识视觉需要旋转的角度)是一个斜坡函数 当前的电控pid似乎不会对斜坡函数追踪到yaw命令 = 0为止,而是存在一个稳态偏差 比如对于一个角度为10度的目标,视觉持续发送命令,最终视觉的命令会停留在yaw = 2,理想情况下应该是稳定时 yaw = 0 该稳态偏差与对方的角速度yaw_v和某一时间常数有关,可描述为yaw_v * t0与电机性质和pid参数相关,因此视觉错了一个调整,我们计算目标的yaw_v （如果要细究，这是在 prediction_time 时刻的 yaw_v）并把发送给电控的 yaw 改为 yaw + t0 * yaw_v，其中 t0 通过跟踪匀速目标实验来暴力测出 实验发现,这种做法确实可以在稳态下使得视觉期望的yaw = 0,此时发送给电控的yaw 仍然时yaw + to * yaw_v, 不是0 这就意味着,如果对方是匀速运动,视觉总是可以在命令收敛后让电控在control 时间点达到命令的位置\n当前双线火控存在: (1)视觉期望角 (2)视觉预测角(期望角 + 预测时间(电控时延)) (3)当前云台角\n将视觉期望角,视觉预测角和当前云台角使用rqt_plot工具打印出来, 如果视觉期望角和视觉预测角只是单纯存在相位差,其他曲线形式基本一致,为合理 否则说明双线火控可能存在选板不一致的问题\n变量名: COMMAND_TIMESPAN 如果视觉期望角在当前云台角左侧就增加电控时延 如果视觉期望角在当前云台角右侧就减少电控时延 可以先旋转粗调,再分别慢速旋转小陀螺精调参数 -\u0026gt; 该参数较为敏感,10ms命中率就会有明显差别\n最终期望效果: (小陀螺跟随) -\u0026gt; 没有使用mpc和力控算法,该程序的上限就在这里 e36be2406aff417e8977fa7a5a487b5.jpg\n(2)调程序预测时间(对齐时间戳,黑箱(经验调参)) 调参前记得保证落点在装甲板正中心,否则会影响判断 车辆平移运动，自瞄击打目标, 如果子弹落点偏车运动方向的同向,说明预测时间超调,减少预测时间 如果子弹落点偏运动方向的反向,说明预测时间偏小,增加预测时间\n(3)和电控调pid 先调节静止靶的目标,将云台移动到视野的边缘,然后点击自瞄(右键),打印rqt_plot的曲线,先调硬超调后再调软,最后调稳态误差 响应太慢 -\u0026gt; 增大P 震荡/超调 -\u0026gt; 减小p\n最终有固定偏差 -\u0026gt; 增大I 超调后缓慢回调 -\u0026gt; 减小I\n超调/震荡明显 -\u0026gt; 增大D 电机高频尖叫或抖动 -\u0026gt; 减小D\n速度环(内环) 先调 云台电机转动不平滑,速度波动大,响应迟钝 典型现象: 推目标速度时电机抖动,啸叫 速度忽快忽慢 加速或急停时过冲\n位置环(外环) 后调 速度环已经稳定,云台定位不准,角度超调,震荡 典型现象: 转到目标角度后过量超调再回来 在目标位置附近来回摆动 最终角度存在固定偏差(静差)\n核心原则：速度环未调好之前，不要调位置环。 内环是基础，外环是上层。\n9.测试完整的自瞄射击效果 打弹的时候记得叫电控和机械给被击打目标加装保护 （代码性的验证话，选择性的测试,毕竟测试内容有点多） 考虑装甲板的强度可能承受不了大弹丸的威力,打印件支架可能飞出来,电池可能被振飞\n（测试击打车辆） -\u0026gt; (后期可以测试打击小装甲板和大装甲板的效果) 后期会出关于小装甲板和大装甲板的调节阈值，打弹时机的区别 由近到远完整的静止的车辆装甲板效果(直到测试出该弹速的子弹射程)，统计命中率 由近到远完整的慢速旋转车辆装甲板的效果，统计命中率 由近到远的完整的小陀螺旋转车辆装甲板的效果，统计命中率 前后移动的旋转移动的小陀螺 左右移动的旋转移动的小陀螺 (测试前哨战) 注意事项: 前哨站比较高,近距离击打的时候俯仰角可能不够,需要 找出最近的极限击打距离 由近到远完整的静止的前哨战装甲板效果(直到测试出该弹速的子弹射程)，统计命中率 -\u0026gt; 这里的目的主要是为了分析弹道,重补,和世界系补偿 由近到远的完整的旋转前哨战装甲板的效果，统计命中率\n关于硬件对自瞄的影响 -\u0026gt; 微机需要硬件定期检修 1.关于串口线的问题 串口线需要安装驱动才可以使用,目前最新的串口线使用的是ch343的驱动 老的一批串口线使用的是ch340的驱动 -\u0026gt; 兼容ch34n\n2.关于自瞄调试的问题 3.关于微机散热的问题 微机散热存在问题: 风扇转不转,有没有预留通风口 微机温度高,cpu主频下降,自瞄帧率下降\n4.关于掉内存条的问题 现在微机都是两根内存条 如果掉了一根内存条: 内存带宽减半:通道数减少，数据通路变窄 加剧了CPU的搬运工角色,数据排队堵塞,DMA效率降低,CPU被迫补位\n会导致海康相机节点和串口节点的cpu占用率升高 从而导致自瞄帧率下降\n5.nuc震荡后关机的问题 老化的nuc受到震荡后会直接关机, 导航哨兵会转圈圈, 自瞄会直接掉\n6.超电炸了后对自瞄的影响 怀疑,未确定 锁存模式 现象: 超电炸了后,会触发过流保护,一旦触发过流,保护电路会立即永久切断输出,并锁存这个保护状态 恢复条件: 即使过流故障已经移除,只要输入电源不中断,被锁存的保护状态就不会解除 必须彻底断开输入电源,等待电容放电完毕,再重新上电,才能复位保存锁存,恢复供电\n打嗝模式 现象: 触发保护后切断输出,间隔一段时间后尝试重新开启,如果此时故障(如超电短路)依然存在,会再次触发保护,如此循环(打嗝)\n7.哨兵掉网口的问题 哨兵掉网口会转圈圈 老哨兵微机是网口接口坏了导致的\n8.供电对nuc和网口的影响 (1)降压不好的话,过颠簸一瞬间会出现欠压的情况,导致掉网口 (2)曾经有一个电控将: 降压线接入到射击模块,导致裁判端一进入比赛,没有买弹不给微机供电(导致视觉开离线怎么测试都没有测试出来) -\u0026gt; 多了解一些电控接线,裁判系统的知识 (3)怀疑: 超电炸了后也可能会导致微机供电出现问题\n9.关于显卡欺骗器对自瞄的影响 最好插上,否则x11,nomachine,vnc,todesk等远程调试器会出现问题\n10.关于布线，打热熔胶和扎带对自瞄的影响 布线方法: 用扎带固定(相机线,串口线)两端,中间留余量保证车运动的时候不会扯到线 热熔胶给两个usb口(相机和串口)和雷达的网口线打上胶 电源线不可以打胶,可能会短路\n相机的两个螺丝(光圈和焦距) -\u0026gt; 注意不要和东西干涉,否则剧烈运动或震动后可能会导致 螺丝松了\n不建议相机的两个螺丝打热熔胶 -\u0026gt; 打了胶反而会干涉导致焦距和光圈松了 -\u0026gt; 最典型的问题就是光圈和焦距松了导致自瞄识别不到目标,只能近距离才能看到甚至完全识别不到\n11.微机老化接口损坏的问题 ssh+自瞄网页调试器 c口延长线\n12.关于硬盘损坏导致微机死机或无法开机的问题及解决方案 硬盘损坏,存在坏点(原全向微机) -\u0026gt; 根文件系统出现了严重损坏,系统在启动时无法自动修复,因此被迫进入紧急模式的 BusyBox界面\n解决方法: 使用fsck进行修复（自行截图询问ai）\nfsck /dev/nvme0n1p2 yes reboot 或者失败了:exec /sbin/init\n关于自身小陀螺导致自瞄往旋转方向瞄歪的问题 关于哨兵和自瞄串口冲突的问题 关于视觉ros节点重启的问题 发弹延时对自瞄的影响 电控时延对自瞄的影响(让电控听话) 如何火控调参\n关于电控对自瞄的影响 1.pid对自瞄的影响 pid没有调好的表现: 云台转动不平滑、抖动、啸叫 → 自瞄输出角度在跳，云台也在抖，弹道散成一片 转到目标角度后过冲再回来、在目标附近来回摆、存在固定偏差（静差） → 视觉命令收敛到0了，但云台实际还差着几度，枪口没对准 斜坡函数跟随的稳态误差：匀速目标下视觉发给电控的是斜坡命令，PID跟随会存在一个 yaw_v × t0 的固定偏差，视觉端需要把这个偏差补偿回去\n2.电控接线对自瞄的影响 降压线错误接入射击模块 -\u0026gt; 裁判端不买弹就不给微机供电 -\u0026gt; 导致视觉离线怎么测都测不出来原因 串口线不稳 -\u0026gt; 掉串口 或者相机线不稳(包括相机线和cmos的末端螺丝没拧紧 相机过度挤压) -\u0026gt; 都会导致掉相机 自瞄帧率下降\n3.散布(摩擦轮差速)对自瞄的影响 摩擦轮差速过大（超过3转）→ 子弹旋转不一致 → 弹道左右飘，远距离尤其明显\n关于机械对自瞄的影响 1.虚位对自瞄(电控 -\u0026gt; 自瞄)的影响 2.装配误差对自瞄的影响 齿轮/传动机构的间隙 → 视觉发令云台转2°，实际只转了1.7° → 瞄准始终有偏差 虚位大的表现：自瞄在目标附近来回小幅摆动，始终收敛不到0 排查方法: 找机械和电控,它们会告诉你有没有虚位\n3.散布对自瞄的影响 影响散布的各种因素 摩擦轮差速过大（超过3转）→ 子弹旋转不一致 → 弹道左右飘，远距离尤其明显 摩擦轮打久了温度升高 → 弹速上升（俗称\u0026quot;打爽了\u0026quot;）→ 落点往上飘，弹道模型失准 建议写上验收标准：8米距离打装甲板看散度分布，要求散布在一个装甲板大小范围内 枪管积灰也会增大散步,测之前记得先清枪管 -\u0026gt; 俗称给枪管刷牙\n4.机械干涉对自瞄的影响 云台转动时相机线/串口线被拉扯 → 线松了→ 通信断 → 自瞄挂。布线原则：两端固定（扎带），中间留余量 弹舱扩容挤占微机位置 → 自瞄没地方装（升降步兵的坑） 云台极限角度时结构碰撞 → 自瞄命令到达极限位置时机械卡住 → 电机堵转 相机视场被自身结构遮挡 → 某些角度识别不到 -\u0026gt; 参考哨兵的相机\n5.重力补偿对自瞄的影响(对电控pid的影响) 影响pitch轴pid的调试 和pitch轴自瞄的跟随 验收标准: 手抬pitch轴 不管抬到任何程度 头都不会掉下来\n没调好: 重补不够 重补过了\n不够: 头抬不起来 云台俯仰角越大，重力产生的力矩越大 → 同样的PID参数，平射跟得上，高角度跟不上了 表现：打高处目标（前哨站）时云台响应明显比打地面目标慢，落点偏低 本质是电控PID在不同负载下的适应性，视觉了解这个机制后就知道不是弹道的问题，是云台重力补偿的问题\n过了: 头低不下去\n手写笔记 自瞄耗时测试 由于采集延时和相机节点处理耗时,相机话题的最大延时可能达到了20ms，所以帧率给到了50帧也就是20ms，后续再优化 IMG_20251127_211549.jpg\n识别流程 IMG_20251127_211610.jpg\n上交角点坐标变换计算草稿 (算了一个下午) IMG_20251127_211619.jpg\n相机内参和畸变系数的含义 IMG_20251127_211630.jpg\n自瞄通信流程 (附上: 如果电控串口数据没有发送,为什么没有tf坐标上传) IMG_20251127_211643.jpg\n卡尔曼滤波公式 上面有,在这里整理一下 IMG_20251127_193011.jpg\n坐标变换 IMG_20251128_201317.jpg\n角点变换,角点坐标定义重大区别 影响角点顺序 IMG_20251130_211216.jpg\nbug 该内容仅赛季初期书写 涉及的内容很少 许多bug只记录在脑子中未在其中提及： 1.不要将任何延时放进回调函数里面\n2.车在平移的时候两侧的装甲板会同时向上或者向下(并且rviz也有这个现象) -\u0026gt; 重投影存在问题（并且rviz也存在了相同的问题） -\u0026gt;世界坐标系到相机坐标系的变换存在问题 -\u0026gt; 相机内参和畸变系数存在问题 -\u0026gt; 解决方法： 重新相机标定(可能是使用相机的时候动到了相机的焦距) !!!相机一定要经常标定啊\n3.测试的时候,如果是相机插上微机测试的话,那么相机一定要水平放,否侧给的相机是水平的,但是实际上相机是有倾斜角的\nbebaff2ff09794b130f7e9fb0fcb5441_750.png\n相机如果是斜着放的: bebaff2ff09794b130f7e9fb0fcb5441_750.png\n相机斜着放的rviz： bb74d9c00101426fcbaacedf3e224930_750.png\n相机如果是正着放的: 5fe30156e80be686f8cdfba9a1868522_750.png\n相机正着放的rviz: IMG_20251007_214839.jpg\n4.打弹不中-\u0026gt;重力补偿不对-\u0026gt;没有转枪管系\n5.录制bag包观测装甲板tf上传数据不对,时间戳不对,因为时间是从回调函数运行的时候调用的,这时候记录的时间就不是话题的时间,而是当前系统的时间，所以应该传入话题的时间.\n原因: 相机是斜着放的,世界系跟着相机系斜了,由于两侧装甲板的高度没有被更新,高度是不变的,这时候由于世界坐标系相对于建立的正的相机系是斜的, 这时候车前后移动,重投影,两侧没有更新的装甲板就会随着车的移动而错误的上下移动\n参考开源 本项目在开发过程中参考和借鉴了以下开源项目的优秀方法，在此表示诚挚感谢：\n学校/战队 开源仓库 本项目参考的核心技术 西北工业大学 WMJ WMJAimer EKF + 熵权法多运动模型匹配的整车建模、MPC 轨迹规划器 上海交通大学 rm.cv.fans 三分法降自由度 Yaw 角优化、弹道可视化调参工具、坐标变换器设计 同济大学 SuperPower sp_vision_25 暴力搜索法降自由度 Yaw 角优化、MPC 轨迹规划器 华南师范大学 rm_auto_aim fitLine 灯条角点优化 中南大学 FYT FYT2024_vision PCA 灯条角点优化 具体借鉴说明\n整车建模：参考西工大 WMJAimer 的 EKF 多模型融合架构，实现了标准模型、平衡模型和前哨站模型的自动切换，并结合熵权法进行多运动模型的数据关联匹配 Yaw 角降自由度优化：参考上交的三分法和同济的暴力搜索法，在识别模块中实现了对装甲板法向量的角度拟合，将 3 自由度姿态估计降至 1 自由度 MPC 轨迹规划：同时集成了西工大（基于 QP 二次规划）和同济（分离 Pitch/Yaw 轴）两种 MPC 方案，用于云台控制指令的平滑规划 灯条角点优化：参考华南师范的 fitLine 方法和中南大学的 PCA 方法，对灯条角点进行亚像素级矫正，提升 PNP 位姿解算精度 弹道可视化：参考上交的弹道重现方案，实现了模拟子弹的实时绘制与可视化调参 坐标变换器：参考上交的坐标系管理设计，实现了 Map → IMU → Camera → Gun 的级联 TF 变换系统 YOLOv8 模型：基于 Ultralytics 开源框架训练，使用 OpenVINO 推理引擎部署 参考学习资源 上交自瞄青工会：BV1vX4y1W7U7 光圈与景深原理（动画科普）：BV1t24y1k7Ye 相机快门原理（动画科普）：BV1Qu411p7Jj ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%A1%82%E5%B7%A5%E8%87%AA%E7%9E%84%E6%96%87%E6%A1%A3/","summary":"\u003ch1 id=\"桂工自瞄文档\"\u003e桂工自瞄文档\u003c/h1\u003e\n\u003ch2 id=\"开发记录\"\u003e开发记录:\u003c/h2\u003e\n\u003cp\u003e1.双装甲板通信逻辑\n2.帧率控制器,插值法 补偿ros时间(陈君提出) -\u0026gt; (前提解决相机装配误差)\n3.卡尔曼滤波器,熵权法,整车建模(最难),多运动模型观测\n4.选板逻辑\n5.重力补偿弹道解算(西工大(采纳)，同济(未采纳),上交四阶龙格库塔(中科大)) 上交线性空气阻力模型(采纳)(存在问题目前猜测由于pnp测距不准,在较远距离空气阻力击打倾斜效果很差)\n无空气阻力的模型，它的角度和距离是线性，但是在远距离的时候，它的速度是随距离指数衰减的，在距离较远的时候，一点点测距误差就会导致比较大的角度偏差\n6.三层前哨战卡尔曼修改(卡尔曼参数还需调试)\n7.手写脱ros坐标变换器(重点: 角点变换) -\u0026gt; 待整理(目前矩阵命名是相反的) -\u0026gt; 已整理\n8.上交三分法降自由度优化yaw角（最后采用这个方案）\n9.同济暴力搜索降自由度优化yaw角 （近距离存在问题）\n10.识别角点优化(fitLine最小二乘法获得灯条角点 -\u0026gt; 优化后降自由度产生良好优化效果,0度范围角度不再跳变)\n11.西工大mpc轨迹规划器(半成品)\n12.约束平面求相机装配误差和完整误差补偿 -\u0026gt; 考虑使用RANSAC算法重构\n13.弹道重现 -\u0026gt; 考虑后续实现弹道闭环\n14.手眼标定(半成品) -\u0026gt; (传统手眼标定, 先进手眼标定(版本不够))\n15.火控流水打弹(500血 30转 3m 9s 70%命中率)\n16.空气阻力弹道模型(存在bug未查明)\n17.视觉弹频控制(旋转移动命中率到38%~50%) -\u0026gt; 效果不太好会影响dps -\u0026gt; 被废弃\n18.同济mpc(未落地) -\u0026gt; 规划存在提前减速的效果 -\u0026gt; 电控和机械跟不上\n但是由于:\n开火时延是同济的十倍(200ms),20个时间片后的规划和预测云台做火控效果很差,双环pid跟随存在稳态误差需要添加电控时延但是20个时间片的预测效果就很差了,目前旋转移动目标,小弹丸命中率只有传统方法的一半(25%)\n未来:\n电控机械尽量减少开火延时\nmpc规划的速度和加速度作为前馈发送给双环pid作为前馈或者考虑计算力矩控制算法(提升云台的跟随能力)\n19.参考BA优化的思想优化pnp算法的测距(失败)\n20.PCA主成分分析法(Principal Component Analysis, PCA)方法优化角点(哨兵没部署)\n21.识别和相机采集多线程(初步编写)\n22.全向感知(多相机采集调度)(仅初步学习线程池) 放弃多线程方案 -\u0026gt; 多进程方案已初步启动两个相机 -\u0026gt; 初步实现等待部署\n23.ssh+自瞄网页调试 初步实现\n24.修降自由度bug，修流水火控bug(调的参数好的情况下,火控有大的提升，英雄低速目标颗秒，高速静止目标100w麦轮50%命中率) -\u0026gt; 但是参数敏感每隔几个小时参数就会变,而且实战环境复杂,命中困难\n25.迭代法求子弹飞行时间(无空气阻力) -\u0026gt; 有空气阻力曾经实现过未通过测试\n26.多段测距的弹道硬补偿(弹道标定) 已测试 效果不错 但调参成本大\n27.自瞄网页调试器 初步实现\n28.自瞄Python单测(计划实现)\n29.自瞄ros2重构(计划实现)\n30.全向感知技术(半成品) -\u0026gt; (计划实现)\n31.针对前哨和远距离目标的新火控(计划实现) -\u0026gt; 远距离(单点) 前哨(跟随上再击打)\n32.给自瞄接入gazebo仿真  初步实现\u003c/p\u003e","title":"桂工自瞄文档"},{"content":"牛客面经 1.环境变量的作用?\n2.编译器和解释器的区别?\n3.b树和b+树的区别?\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E7%89%9B%E5%AE%A2%E9%9D%A2%E7%BB%8F/","summary":"\u003ch1 id=\"牛客面经\"\u003e牛客面经\u003c/h1\u003e\n\u003cp\u003e1.环境变量的作用?\u003c/p\u003e\n\u003cp\u003e2.编译器和解释器的区别?\u003c/p\u003e\n\u003cp\u003e3.b树和b+树的区别?\u003c/p\u003e","title":"牛客面经"},{"content":"前后端 React https://react.dev/learn\nJavaWeb HTML 基础教程 https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML CSS 基础教程 https://developer.mozilla.org/zh-CN/docs/Learn/CSS/First_steps\n标签 image.png\nimage.png\nDIV 是一个块级容器,就像一个无形的盒子,用来把页面内容分成不同的部分,方便用CSS控制样式和布局 你可以把页头,导航栏,内容区,页脚分别放进不同的里,然后用CSS去控制每个盒子的位置,大小,背景等\nCSS css可以控制元素的大小,颜色,位置,背景等\nJAVA 学习资料: https://www.runoob.com/java/java-tutorial.html\nwebserver: 学习资料推荐: Linux 高性能服务器编程 小林coding\n项目概述: image.png\n在创建服务器时,还必须要指定一个端口号,当一台服务器,同时对外提供多种服务时,比如WEB服务,远程登陆服务等等,就需要使用\u0026quot;端口号\u0026quot;\u0026lt;对不同的服务进行区别.每个服务,都有自己唯一的端口号 image.png\n接收浏览器的WEB请求 image.png\n处理浏览器的请求: 在新线程中,单独处理对应浏览器客户端的请求\nGET请求报文的格式 image.png\n请求报文由4个部分组成: 请求行,请求头部行,空行,请求数据,具体格式如下:\n\\r(回车符): 含义: 将光标移动到当前行的开头 (不换行,只是回到行首,后续输出会覆盖该行开头的内容)\n\\n(换行符) 含义: 将光标移动到下一行 (换到新的一行,但列位置不变)\ncpp中,当我们在字符串中使用\\n时,它在不同平台上的表现可能会有所不同,因为cpp标准库在处理文本文件时可能会进行转换,例如在Windows平台上,当以文本模式打开文件时,写入\\n会被转换为\\r\\n，读取\\r\\r会被转换为\\n,而在二进制模式下,则不会进行转换\n响应报文的格式 服务器发送数据给浏览器时,发送的响应报文,由4个部分组成: 状态行,消息头部,空行和响应正文,格式如下: 组成部分: 说明: 状态行: 版本+空格+状态码+短语+回车换行 消息头部 关键字: 空格 + 值 + 回车换行\n空行 回车换行(\\r\\n) 例如: \\n 响应正文\n常用的关键字有: 关键字: 含义: Content-Type 返回消息的内容类型 Content-Length 返回内容的长度,以字节为单位 Data 返回消息的时间 Server 服务端的软件名称和它的版本号\n执行WEB服务前的准备工作: 1.网络通信初始化 2.创建套接字 3.设置套接字属性,端口可复用 4.绑定套接字属性和网络地址 5.动态分配端口 6.创建监听队列\n为什么要设置端口可复用: // 没有设置端口复用时，重启可能报错： // \u0026ldquo;Bind failed: Address already in use\u0026rdquo;\n1.服务器重启时避免\u0026quot;地址已在使用\u0026quot;错误 当服务器关闭或崩溃后,TCP连接可能还处于TIME_WAIT状态(通常持续2-4分钟).在这期间,如果立即重启服务器,会绑定失败\n2.处理TIME_WAIT状态 TCP协议确保可靠关闭连接,TIME_WAIT状态: 确保最后一个ACK被对方收到 让网络中延迟的数据包过期 防止旧连接的重复数据包干扰新连接\n3.开发调试遍历 在开发阶段,频繁重启服务器测试时,端口复用可以立即重启,无需等待\n源码:\nCGI (Common Gateway Interface, 通用网关接口) 一个标准协议,它定义了Web服务器与外部应用程序(如脚本,可执行程序)之间如何交互,从而动态生成网页内容\nimage.png\nCGI 是一个让 Web 服务器运行外部程序来生成动态网页的古老而经典的协议。虽然现在很少直接使用“裸”CGI（因为性能问题），但它的思想——通过接口把请求传递给独立程序——仍然活跃在 FastCGI、WSGI、Servlets 等现代技术中。理解 CGI 有助于你把握 Web 后端演进的历史脉络。\nimage.png\nTinywebserver B/S模型 Browser（浏览器）：客户端，负责展示界面、发送用户请求（如输入网址、点击按钮）、渲染服务器返回的 HTML/CSS/JS 等资源。 Server（服务器）：服务端，通常是 Web 服务器（如你读的代码），负责接收请求、处理业务逻辑（如查询数据库、调用其他接口）、返回响应数据（如网页内容、JSON 数据）\n线程同步机制包装类 多线程同步,确保任一时刻只能有一个线程能进入关键代码段 信号量 互斥锁 条件变量\nhttp连接处理类 根据状态转移,通过主从状态机封装了http连接类.其中,主状态机在内部调用从状态机.从状态机将处理 状态和数据传给主状态机 客户端发出http连接请求 从状态机读取数据,更新自身状态和接收数据,传给主状态机 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取\n半同步/半反应堆线程池 使用一个工作队列解除了主线程和工作线程的耦合关系;主线程往工作队列中插入任务,工作线程通过竞争来取得任务并执行它 同步I/O模拟proactor模式 半同步/半反应堆 线程池\n定时器处理非活动连接 由于非活跃连接占用了连接资源,严重影响服务器的性能,通过实现了一个服务器定时器,处理这种非活跃连接,释放连接资源.利用alarm函数周期性地触发SIGALRM信号,该信号地信号处理函数利用管道通知主循环执行定时器链表地定时任务 统一事件源 基于升序链表的定时器 处理非活动连接\n同步/异步日志系统 同步/异步日志系统主要涉及了两个模块,一个是日志模块,一个是阻塞队列模块,其中加入阻塞队列模块主要是解决异步写入日志做准备 自定义阻塞队列 单例模式创建日志 同步日志 异步日志 实现按天,超行分类\n校验 \u0026amp; 数据库连接池 数据库连接池 单例模式,保证唯一 list实现连接池 互斥锁实现线程安全\n校验 HTTP请求采用POST方式 登录用户名和密码校验 用户注册及多线程注册安全\n自瞄网页调试器 虚拟机和windows主机Net网络通信: 虚拟机Linux: IP: 192.168.186.136（NAT 背后的内部地址，Windows 无法直接访问） 开了两个服务： HTTP 服务器：监听 0.0.0.0:8000 rosbridge WebSocket 服务器：监听 0.0.0.0:9090\nWindows主机: 浏览器地址栏输入 http://localhost:8000/panel.html 网页里的 JS 连接 ws://localhost:9090\nVMware NAT 端口转发规则： 主机端口 8000 → 虚拟机 192.168.186.136:8000 主机端口 9090 → 虚拟机 192.168.186.136:9090\nLinux高性能服务器 第6章 高级I/O函数 6.1pipe函数 \u0026ndash; 创建管道 6.2dup函数和dup2函数 \u0026ndash; 复制文件描述符 6.3readv函数和writev函数 \u0026ndash; 分散读和集中写 6.4sendfile \u0026ndash; 文件到套接字的零拷贝传输 6.5mmap函数和munmap函数 \u0026ndash; 文件到套接字的零拷贝传输 6.6splice函数 \u0026ndash; 内核空间的数据移动 6.7tee函数 \u0026ndash; 管道数据的零拷贝镜像 6.8fcntl函数 \u0026ndash; 文件描述符的操控 app(开怀) 这是一个基于 React 19 + TypeScript + Vite 的前端项目，UI 组件库使用了 shadcn/ui（基于 Radix UI + Tailwind CSS）。项目采用组件化 + 区块化的目录组织方式，适合构建企业级/产品展示类网站。\n需求: 照片查找不到\n环境配置文档: 一、准备工作 安装 Node.js 访问 Node.js 官网 下载 LTS 版本（如 20.x 或 22.x），Windows 选择 .msi 安装包 安装时一路默认选项（会自动配置环境变量） 安装完成后 重启电脑（或至少重启 VSCode/终端） 验证安装 打开 命令提示符（CMD），输入： cmd node -vnpm -v 如果显示版本号（如 v20.18.0 和 10.8.2），说明安装成功。\n⚠️ 注意：不要使用 PowerShell（可能出现脚本执行策略错误），推荐使用 CMD 或 VSCode 中切换终端为 Command Prompt。\n二、在 VSCode 中打开项目 打开 VSCode 点击 文件 → 打开文件夹，选择项目所在的文件夹（包含 package.json 的目录） 左侧资源管理器中应该能看到 package.json、index.html、src 等文件\n三、安装项目依赖 打开终端cmd 进入项目目录 执行安装命令 npm install\n四、启动开发服务器 npm run dev 终端会显示： text VITE v7.x.x ready in xxx ms ➜ Local: http://localhost:5173/ ➜ Network: use \u0026ndash;host to expose 按住 Ctrl 并点击 http://localhost:5173/，浏览器会自动打开项目页面。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%89%8D%E5%90%8E%E7%AB%AF/","summary":"\u003ch1 id=\"前后端\"\u003e前后端\u003c/h1\u003e\n\u003ch2 id=\"react\"\u003eReact\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://react.dev/learn\"\u003ehttps://react.dev/learn\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"javaweb\"\u003eJavaWeb\u003c/h2\u003e\n\u003cp\u003eHTML 基础教程\n\u003ca href=\"https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML\"\u003ehttps://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML\u003c/a\u003e\nCSS 基础教程\n\u003ca href=\"https://developer.mozilla.org/zh-CN/docs/Learn/CSS/First_steps\"\u003ehttps://developer.mozilla.org/zh-CN/docs/Learn/CSS/First_steps\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"标签\"\u003e标签\u003c/h3\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003ch3 id=\"div\"\u003eDIV\u003c/h3\u003e\n\u003cdiv\u003e是一个块级容器,就像一个无形的盒子,用来把页面内容分成不同的部分,方便用CSS控制样式和布局\n\u003cp\u003e你可以把页头,导航栏,内容区,页脚分别放进不同的\u003cdiv\u003e里,然后用CSS去控制每个盒子的位置,大小,背景等\u003c/p\u003e","title":"前后端"},{"content":"嵌入式自学指南 书籍推荐 C primer plus 大话数据结构 电子元器件\n51单片机 stm32 \u0026mdash;- 野火 \u0026mdash; 基于stm32开发板实战指南 标准库 寄存器\ncc2530 RT-THread freeRTOS 学到这里就可以找实习\n方向1 硬件 画板子 qt\n方向2 物联网 uni-app 微信小程序开发 android移动开发\n再深入网络 GUI 两个方向单片机和linux\nlinux\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%87%AA%E5%AD%A6%E6%8C%87%E5%8D%97/","summary":"\u003ch1 id=\"嵌入式自学指南\"\u003e嵌入式自学指南\u003c/h1\u003e\n\u003cp\u003e书籍推荐\nC primer plus\n大话数据结构\n电子元器件\u003c/p\u003e\n\u003cp\u003e51单片机\nstm32 \u0026mdash;- 野火 \u0026mdash; 基于stm32开发板实战指南\n标准库   寄存器\u003cbr\u003e\ncc2530\nRT-THread\nfreeRTOS\n学到这里就可以找实习\u003c/p\u003e\n\u003cp\u003e方向1   硬件\n画板子   qt\u003c/p\u003e","title":"嵌入式自学指南"},{"content":"深入理解计算机系统 第一章:计算机系统漫游 1.1信息就是位+上下文 基本思想：系统中所有的信息————包括磁盘文件，内存中的程序，内存中存放的用户数据以及网络上传送的数据，都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象的上下文。 C语言的起源： C语言最开始作为一种用于Unix系统的程序语言开发出来的。\n1.2程序被其他程序翻译成不同的格式 image.png\n1.3了解编译系统如何工作是大有益处的 1.优化程序性能 2.理解链接时出现的错误 3.避免安全漏洞\n1.4处理器读并解释 存储在内存中 的指令 shell是一个命令行解释器,它输出一个提示符,等待等待输入一个命令行，然后执行这个命令。如果该命令行的第一个单词不是一个内置的shell命令，那么shell就会假设他是一个可执行文件的名字,他将加载并运行这个文件。\n1.4.1系统的硬件组成 1.总线 贯穿整个系统的是一组电子管道，称做总线，它携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节快,也就是字.现在要么是四字节(32位)要么八字节(64位). 2.I/O设备 每个I/O设备都通过一个控制器或适配器与I/O总线相连，控制器和适配器的区别主要在于他们的封装方式 功能：在I/O设备和I/O设备之间传递信息 3.主存 主存是一个临时存储设备,在处理器执行程序时，用来存放程序和程序处理的数据 4.处理器 解释存储在主存中指令的引擎 image.png\n1.4.2运行hello程序 1.5高速缓存至关重要 image.png\n1.6存储设备形成层次结构 image.png\n1.7操作系统管理硬件 操作系统基本功能: 防止硬件被失控的应用程序滥用 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备\n1.7.1进程 进程: 进程是操作系统对一个正在运行的程序的一种抽象.操作系统会提供一种假象,就好像系统上中i有这个程序在运行\n并发运行: 一个进程的指令和另一个进程的指令是交错执行的\n上下文: 操作系统保持跟进进程运行所需要的所有状态信息.\n上下文切换: 一个CPU看上去都像是在并发的执行多个进程.这是通过处理器在进程见切换来实现的\n1.7.2线程 1.7.3虚拟内存 虚拟内存是一个抽象概念,它为每个进程提供一个家乡,即每个进程都在单独地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间.\n进程和线程的区别 简单来说，进程是资源分配的单位，线程是执行的单位。线程是进程的子单位，线程的切换和通信成本较低，但安全性较差；进程的切换和通信成本较高，但安全性较好。在实际应用中，通常会根据具体需求选择合适的模型，\nimage.png\n地址空间最上面的区域是保留给操作系统中的代码和数据 地址空间的底部区域存放用户进程定义的代码和数据 程序代码和数据 略 堆 略 共享库 略 栈 内核虚拟内存 略\n1.7.4文件 文件就是字节序列 ??? 每个I/O设备，包括磁盘，键盘，显示器，甚至网络，都可以看成是文件。系统中的所有输入输出都是通过使用一小组称为UnixI/O的系统函数调用读写文件来实现的.\n1.8系统之间的网络通信 image.png\nimage.png\n1.9重要主题 1.9.1Amdahl定律 主要思想:当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度 参数含义: Told旧时间,Tnew新时间,a执行时间与该时间的比例，k性能提升比例,S加速比 image.png\nimage.png\n考虑k趋向于∞时的效果，这就意味着，我们可以取系统的某一部分将其加速到一个点，在这个点上，这部分花费的时间可以忽略不计 image.png\n1.9.2并发和并行 并发:同时具有多个活动的系统 并行:用并发来使一个系统运行的更\n1.线程级并发 image.png\nimage.png\n超线程:同时多线程,是一项允许一个CPU执行多个控制流的技术\n2.指令级并行 在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令 级并行\n超标量处理器:处理器可以达到比一个周期一条指令更快的执行效率\n3.单指令,多数据并行 在最低层次上，许多现代处理器拥有特殊的硬件，允许一条指令缠上多个可以并行执行的操作这种方式称为单指令,多数据,即SIMD并行.\n1.9.3计算机系统中抽象的重要性 抽象的使用时计算机科学中最为重要的概念之一 例如:为一组函数规定一个简单的应用程序接口(API)就是一个很好的变成习惯，程序员无需了解它内部的工作便可以使用这些代码 指令集架构提供了对实际处理器硬件的抽象 image.png\n文件是对I/O设备的抽象,虚拟内存是对程序存储器的抽象，而进程是对一个正在运行的程序的抽象，虚拟机，它提供对整个计算机的抽象，包括操作系统，处理器和程序.\n1.10小结 image.png\n操作系统内核时应用程序和硬件之间的媒介.它提供三个基本的抽象:1.文件是对I/O设备的抽象；2.虚拟内存是对主存和磁盘的抽象3.进程是处理器，主存和I/O设备的抽象 网络提供计算机系统之间的通信的首选.从特殊系统的角度来看,网路就是一种I/O设备.\n第二章：信号的表示和处理 无符号编码基于传统的二进制表示法 补码编码是表示有符号整数的最常见的方式有符号整数就是可以为正或者可以为负的数字 浮点数编码是表示实数的科学计数法以2为基数的版本\n2.1信息存储 机器级程序将内存视为一个很大的字节数组,称为虚拟内存.内存的每个字节都是由唯一的数字来标识，称为地址,所有可能地址的集合就称为虚拟地址空间\n2.1.1十六进制表示法 在C语言中,以0x或ox开头的数字常量被认为是十六进制的值,字符\u0026rsquo;A~\u0026lsquo;F‘即可以是大写也可以是小写 将给定二进制数字转换为十六进制,可以首先把它分为每四位一组来转换十六进制。不过，如果总数不是4的位数，最左边的一组可以少于4位，前面用0补足 image.png\n二进制和十六进制之间的转换 二进制和十六进制之间的简便转换 2的n次方 = x, n = i + 4j 变成十六进制就是前面是2的i次方,后面是j个零\n十进制和十六进制之间的转换 取16模 除16\n2.1.2字数据大小 每台计算机都有一个字长,指明指针数据的标称大小 其数据大小是固定的,不随编译器和机器设置而变化: int32_t 4个字节 int64_t 8个字节\n2.1.3寻址和字节顺序 最低有效字节在最前面的方式,称为小端法 最高 有效字节在最前面的方式，称为大端法 image.png\nimage.png\n反汇编器：是一种确定可执行程序文件所表示的指令序列工具\n第三章：程序的机器级表示 第四章：处理器体系结构 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F/","summary":"\u003ch1 id=\"深入理解计算机系统\"\u003e深入理解计算机系统\u003c/h1\u003e\n\u003ch1 id=\"第一章计算机系统漫游\"\u003e第一章:计算机系统漫游\u003c/h1\u003e\n\u003ch2 id=\"11信息就是位上下文\"\u003e1.1信息就是位+上下文\u003c/h2\u003e\n\u003cp\u003e基本思想：系统中所有的信息————包括磁盘文件，内存中的程序，内存中存放的用户数据以及网络上传送的数据，都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象的上下文。\nC语言的起源：\nC语言最开始作为一种用于Unix系统的程序语言开发出来的。\u003c/p\u003e","title":"深入理解计算机系统"},{"content":"视觉完整形态文档 战术定位,核心研发功能点,规划调整 核心研发功能点中的“自瞄预测与控制”的规划有所修改，针对追踪与开火逻辑进行了修改，从原来的EKF预测加双环PID控制，改为运动学模型结合MPC优化，并剥离开火逻辑的方案。\n自瞄方案: 由于目前开火延时有将近200ms和开源的20ms的开火延时有较大差距; 如果继续沿用原方案MPC预测近200ms延时(20个时间片)来判断提前开火极不严谨。故而将开火决策从MPC中剥离,转而由独立火控执行,由于双环PID控制存在稳态误差,而开源使用的是计算力矩算法，鉴于控制算法替换周期紧张、实现复杂度高且存在稳定性风险,故而考虑将含时延提前量的预测较大直接发给MPC进行平滑轨迹优化\n算法设计 1.功能简介与pipeline 自瞄系统是 RoboMaster 机器人视觉火控的核心模块，其目标是在复杂战场环境中快速、准确地识别敌方装甲板，预测目标运动状态，解算弹道，并输出云台控制指令和开火信号，实现自动瞄准与射击。系统需适应步兵、英雄、哨兵等多种车型，支持静止、旋转、平移及小陀螺等运动模式，并在远距离、低弹速等条件下保持较高命中率。\n输入: 相机图像流（实时采集，一般为 YUV 或 RGB 格式） 机器人自身姿态（IMU 数据，通过串口接收） 弹速、摩擦轮状态等火控信息（串口接收）\n输出: 云台期望角度（pitch 和 yaw） 开火标志及打弹时机 调试信息（可视化图像、状态数据等）\n整体 Pipeline: IMG_20260311_232713.jpg\n2.重要算法原理阐述,公式推导 卡尔曼滤波: 预测： image.png\n更新：\nimage.png\n扩展卡尔曼: image.png\n熵权法: image.png\n坐标变换: image.png\nimage.png\nimage.png\n降自由度yaw角优化: image.png\n最小二乘法角点优化: image.png\nPnP算法: image.png\n弹道解算: 无空气阻力弹道解算: image.png\n线性空气阻力弹道解算: image.png\nMPC轨迹规划: 系统模型: image.png\nMPC优化问题: image.png\n参考轨迹生成: image.png\n控制输出: image.png\n3.算法性能,优缺点分析,优化方案 优点 多算法融合提升精度 采用熵权法与扩展卡尔曼滤波（EKF）相结合，在状态估计中动态调整观测权重，有效融合多传感器信息，提升目标状态估计的鲁棒性和准确性。 降自由度 yaw 角优化 针对车辆旋转运动，通过降自由度方法将 yaw 角独立建模，配合最小二乘法对角点进行优化，减少了姿态解算的耦合误差，提高了对旋转目标的跟踪性能。 角点特征优化 引入PCA主成分分析对角点进行降维和去噪，增强了角点检测的稳定性；同时结合打表弹道补偿，将实际射击试验数据用于离线修正，有效补偿了系统误差。 约束平面法误差补偿 利用靶面或场地平面约束，对 PnP 解算出的位姿进行平面投影修正，减小了因深度估计不准引入的垂直方向误差。 模型适配性强 系统支持标准步兵、前哨站等不同运动特性的目标，可通过 YAML 配置文件灵活调整卡尔曼模型、MPC 参数和弹道系数，便于针对不同车型和场景进行调优。 测试体系完善 提供详细的调试指南和测试方案，涵盖从单模块验证到整车集成的全流程，确保算法在实车上的稳定部署。\n缺点与局限性 测距精度不足 单目视觉下 PnP 解算在远距离（\u0026gt;5m）时深度误差急剧增大，导致落点偏低且弹道补偿失效。虽然尝试了双目标定，但改善效果微弱；硬件上受限于成本无法采用激光测距或长焦相机。 弹道模型不完善 当前弹道解算未考虑空气阻力，在远距离下落点偏差严重。即使引入线性阻力模型，也对测距误差极其敏感，无法稳定工作。参考上交方案，需采用四阶龙格-库塔数值积分方法精确求解弹道。 系统延时过大 ROS 节点间的图像传输延迟严重，限制了自瞄帧率；采集、识别、处理未完全多线程化，导致整体延时约 200ms，极大削弱了 MPC 的预测能力（现 MPC 命中率仅为传统方法的 25%），且云台 PID 跟随存在稳态误差。 发弹延迟未补偿 未考虑从决策到子弹击发的延迟（包括机械和电磁阀响应），导致实际开火时刻目标已移动。可借鉴上交的“历史查询”或同济的 MPC 提前开火策略进行补偿。 装配误差未完全标定 相机与枪管之间的装配误差（包括光轴平行度和枪管下垂）未经过精确标定，导致弹道补偿存在系统性偏差，需要引入弹道闭环（如上交方案）或自研弹道拟合方法。 CV 模型对平移目标跟踪不佳 当前采用恒定速度（CV）模型，对于目标加速度突变（如急停、变向）存在过量预测，导致跟随超调。需引入更合理的恒定加速度（CA）模型，并结合电控时延补偿和差分前馈 PID。 MPC 尚未工程化落地 虽然已集成同济开源的 MPC 算法，但因延时大、参数调优困难，目前处于半成品状态，需与电控深度协作，参考西工大等开源方案进行实车适配。 哨兵全向感知能力不足 哨兵节点需处理多路相机数据，当前采集与识别未分离，严重降低帧率。需实现采集和识别多线程（如共享内存），提升全向感知的实时性。 图像传输延迟限制帧率 ROS 节点间的图像话题传输存在较大延迟，建议采用共享内存或零拷贝传输机制（如 ROS 2 或自研传输层）来突破瓶颈。\n4.算法库介绍与接口说明: ROS Melodic：作为通信框架，负责节点间通信、参数服务器、TF坐标变换以及日志记录。 OpenCV 3.4.5：用于图像处理、PnP位姿解算，以及基于HOG特征的SVM数字识别。 Eigen3 3.3.7：提供高效的矩阵运算，支持卡尔曼滤波和坐标变换。 yaml-cpp 0.6.3：用于解析YAML格式的配置文件。 Boost 1.65.1：提供串口通信、多线程和定时功能。 海康SDK (GalaxyView)：控制相机并采集图像。 SVM分类器：自训练模型，用于数字识别（基于HOG特征）。 OpenVINO™：Intel推出的推理优化工具包，用于加速深度学习模型（如目标检测、分类）在边缘设备上的部署与推理。\n5.算法结果(展示图像,图表,中间过程等): 自瞄重投影可视化界面 识别步兵 image.png\n识别英雄 b244b25d1059d13d4a0242697dba0853_720.jpg\n俯视图调试界面 Cache_2c66856de0b533e9.jpg\n软件设计 1.系统架构: 所有机器人使用同一套代码框架,其中算法库及接口主要有: opencv,yolov8,eigen,openvino,ros,tinympc,yaml,海康SDK等\nrm_hikcamera: 海康相机驱动节点,调用相机驱动,发布相机图像 rm_identify: 识别节点,订阅相机图片,进行识别处理后,发布敌方装甲板信息 rm_msgs: 信息机制管理节点,对各个节点间通信内容进行管理 rm_sentry: 哨兵节点,哨兵机器人特有,里面包括哨兵机器人导航相关内容 rm_serial: 串口节点,订阅到要击打的目标信息,发布到串口,与下位机(开发型C板)通信 rm_tracker: 跟踪节点,订阅敌方装甲板信息,进行决策处理,发布期望电控瞄准的yaw角和pitch角 rm_fu: 打符节点,订阅能量机关信息,发布视觉期望瞄准的yaw角和pitch角\n┌─────────────────────────────────────────────────────────────┐ │ 应用层 │ - 主控流程 (main.cpp) │ - 状态机 (开火逻辑/选板) │ - 调试与可视化接口 ├─────────────────────────────────────────────────────────────┤ │ 功能模块层 │ - 识别模块 (ArmorDetector) │ - 跟踪模块 (Tracker/KalmanFilter) │ - 轨迹规划 (MPCController) │ - 弹道解算 (BallisticsSolver) │ - 坐标变换 (CoordinateTransformer) ├─────────────────────────────────────────────────────────────┤ │ 中间件层 │ - ROS Melodic (通信/消息/参数服务器) │ - 配置管理 (yaml-cpp) │ - 日志系统 (rosconsole/自定义) │ - 线程池 (C++11 std::thread) │ - 数值计算库 (Eigen3) │ - 图像处理库 (OpenCV) │ - 串口通信库 (boost::asio) ├─────────────────────────────────────────────────────────────┤ │ 硬件抽象层 │ - 相机驱动 (大恒SDK / V4L2) │ - 串口驱动 (UART) │ - 硬件时间同步 (ROS Time) └─────────────────────────────────────────────────────────────┘\n硬件抽象层 相机驱动：封装大恒相机SDK，提供图像采集接口，支持参数调节（曝光、增益、白平衡）。 串口驱动：基于boost::asio实现串口收发，负责与电控板（C型开发板）通信，接收IMU/弹速数据，发送云台角度和开火指令。 时间同步：使用ROS Time作为统一时间基准，确保各模块时间戳一致性。\n中间件层 ROS Melodic：作为核心通信中间件，提供节点间消息传递（图像、目标位置、调试数据）、参数服务器（动态调参）、TF坐标变换、日志记录功能。 配置管理：采用yaml-cpp解析YAML配置文件，支持分层参数（如不同车型的卡尔曼参数）。 日志系统：利用rosconsole输出分级日志（DEBUG/INFO/WARN/ERROR），并支持rqt_console实时查看。 线程池：自实现简易线程池，用于并行处理图像采集、识别和多相机调度（全向感知）。 数值计算：Eigen3提供高效的矩阵运算，用于卡尔曼滤波、坐标变换、MPC求解。 图像处理：OpenCV实现图像预处理、灯条检测、数字分类器（HOG+SVM）等。 串口通信：boost::asio提供异步串口通信，保证数据收发的实时性和稳定性。\n功能模块层 识别模块 (ArmorDetector)：图像预处理→灯条提取→装甲板匹配→数字识别，输出装甲板候选列表。 跟踪模块 (Tracker)：核心状态估计单元，内含卡尔曼滤波器族（标准/平衡/前哨模型），负责目标滤波、预测、选板逻辑。 轨迹规划 (MPCController)：基于模型预测控制生成平滑的云台期望轨迹，输出速度/加速度前馈。 弹道解算 (BallisticsSolver)：根据目标距离、弹速、重力及空气阻力模型解算云台俯仰角，支持无阻力、线性阻力、四阶龙格库塔积分。 坐标变换 (CoordinateTransformer)：管理相机→云台→枪管→世界坐标系的变换关系，处理装配误差补偿。\n应用层 主控流程 (main.cpp)：初始化各模块，启动ROS节点，循环执行“采集→识别→跟踪→解算→发送”主流程。 状态机：实现自动开火逻辑（如热管冷却、弹频限制、目标切换）、选板优先级管理。 调试接口：提供Rviz可视化（显示目标位置、预测轨迹、角点）、rqt动态参数配置、重投影误差曲线输出。\n2.运行流程: IMG_20260311_232713.jpg\n步骤1: 图像采集和预处理 步骤2: 灯条检测与装甲板匹配 步骤3: 目标筛选与选板 步骤4: 坐标变换 步骤5: 扩展卡尔曼预测与更新 步骤6: 发布可视化与调试输出 步骤7: 弹道解算 步骤8: 开火决策 步骤9: 串口发送控制指令\n3.重点功能: 3.1 多模型自适应卡尔曼滤波器 解决的问题：不同车型（步兵、平衡步兵、前哨战）运动特性差异大，单一模型无法适应；旋转目标角度观测噪声大。 技术方案： 设计三个卡尔曼模型族：Standard（常规步兵）Outpost（前哨战，极小平移噪声）。 模型参数通过熵权法结合实车数据离线优化，并支持在线动态切换。 引入角度相关的距离观测噪声增益（gain参数），当目标侧对时自动增大测距噪声，避免距离跳动。 提供详细的调参指南（Q/R矩阵调节原则）和调试输出（新息分析、协方差监控）。\n3.2 降自由度yaw角优化 解决的问题：PnP解算出的yaw角在0°附近剧烈跳变，影响跟踪稳定性。 技术方案： 实现上交三分法：将旋转分解为三个自由度分别估计，降低耦合。 备用同济暴力搜索：在局部范围内搜索最优yaw使重投影误差最小（适用于近距离）。 结合fitLine最小二乘法优化灯条角点，提升输入数据质量。\n3.3 高精度弹道解算 解决的问题：远距离重力补偿不足导致落点偏低；空气阻力造成弹道非线性。 技术方案： 集成三种弹道模型：无阻力解析解、一阶线性阻力解析解、四阶龙格-库塔数值积分（上交方案）。 通过复写纸测试统计弹着点，实现弹道闭环：将实际落点与理论弹道匹配，在线修正阻力系数和弹速。 考虑枪管装配误差（pitch/yaw补偿），通过激光笔微调确定补偿值。\n3.4 基于MPC的轨迹规划 解决的问题：电控PID跟随存在稳态误差和延时，导致高速目标脱靶。 技术方案： 建立云台角加速度模型，状态量为角度和角速度，控制量为角加速度。 设计带参考轨迹的MPC，参考轨迹来自卡尔曼预测的未来目标位置。 输出期望角度序列的同时，输出速度/加速度前馈，辅助电控PID。 参数可调（预测步长、权重矩阵），以适应不同机械响应特性。 当前局限性：因开火延时过大（200ms），MPC效果受限，需与电控协同优化。\n3.5 坐标变换与误差补偿 解决的问题：相机安装位置与枪管不重合导致瞄准偏差；C板安装倾斜导致世界系歪斜。 技术方案： 维护四个坐标系：相机、云台、枪管、世界，通过齐次变换矩阵关联。 相机→枪管平移量由机械图纸提供，旋转量通过手眼标定或误差函数测定。 实现角点坐标变换：将装甲板角点从像素系转到枪管系，确保PnP输入正确。 支持在线修正补偿值（pitch/yaw offset），通过射击测试微调。\n3.6 火控: 解决的问题：如何在目标运动、弹道下落和系统延迟的综合影响下，精确计算射击时机与射击角度，提高命中率。\n技术方案： 目标状态预测：基于运动模型预测目标在未来时刻的位置和速度，为弹道解算提供准确输入。 弹道模型集成： 无阻力解析解：适用于近距离快速解算。 一阶线性阻力模型：考虑空气阻力对弹道的非线性影响。 发弹延迟补偿： 通过串口接收下位机实际发弹时刻，结合子弹飞行时间，采用历史查询或MPC提前预测将瞄准点对准子弹到达时的目标位置。 射击决策： 根据目标距离、速度、装甲板朝向及是否进入射程，结合安全阈值（如最小开火角度、正对性）控制ShootFlag输出。 对前哨站、哨兵等特殊目标设置差异化射击策略。\n3.7 装甲板识别: 解决的问题：在复杂光照和运动背景下，稳定、准确地检测装甲板并识别数字，为PnP解算提供高精度角点。\n技术方案： 灯条检测： 基于颜色阈值（红/蓝）对图像进行二值化，提取轮廓。 通过轮廓面积、长宽比、填充率、倾斜角度等几何特征筛选灯条）。 使用最小二乘法拟合灯条直线，精确计算上下端点及角度。 灯条匹配： 根据左右灯条长度比、中心距（归一化到灯条长度）、连线角度等约束进行配对。 区分大小装甲板（\u0026rsquo;s\u0026rsquo;/\u0026rsquo;l\u0026rsquo;），为后续透视变换提供依据。 数字识别： 对配对后的装甲板区域进行透视变换（extractNumbers），提取固定尺寸ROI（28×28）。 采用LeNet/MLP模型（ONNX格式）进行分类，输出数字1~8，并过滤背景（类别9）。 角点优化： 对灯条区域进行PCA分析，提取对称轴 沿轴向基于亮度梯度精确定位灯条上下角点，提升PnP解算的输入精度。 颜色判断：统计灯条轮廓内红色/蓝色像素和，确定目标颜色并与自方颜色比对\n4.软件测试: 跟踪程序平均耗时小于10ms image.png\n视觉期望和电控云台跟随rqt yaw角曲线调试 Cache_6eeea17dfa288e86.jpg\n瞄准测试 Cache_318caa54cdc07c94.jpg\n自瞄参数yaml文件 Cache_-77ef51d5fb616750.jpg\n自瞄rviz调试界面 d2b90fee2d71f802bcc1604d19a2ef99_720.jpg\n9b776b3f8b65062ad0c4a848758ea898.jpg\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%A7%86%E8%A7%89%E5%AE%8C%E6%95%B4%E5%BD%A2%E6%80%81%E6%96%87%E6%A1%A3/","summary":"\u003ch1 id=\"视觉完整形态文档\"\u003e视觉完整形态文档\u003c/h1\u003e\n\u003ch2 id=\"战术定位核心研发功能点规划调整\"\u003e战术定位,核心研发功能点,规划调整\u003c/h2\u003e\n\u003cp\u003e核心研发功能点中的“自瞄预测与控制”的规划有所修改，针对追踪与开火逻辑进行了修改，从原来的EKF预测加双环PID控制，改为运动学模型结合MPC优化，并剥离开火逻辑的方案。\u003c/p\u003e","title":"视觉完整形态文档"},{"content":"视觉中期文档 634bebc64df5b4279a6577b9cf918ff.jpg\n今年，我们视觉方案的核心是构建一个能在复杂比赛环境下稳定命中的高帧率自瞄系统。针对去年小陀螺跟踪不稳、远距离命中率低的问题，我们从预测模型、弹道解算到系统延时进行了全链路升级。\n首先在状态估计上，我们将单装甲板的EKF模型扩展为双装甲板模型，并引入熵权法来解决高速旋转下装甲板的匹配难题。我们为自瞄和前哨赛分别建立了优化的运动模型，这使得系统在面对突然变速或切换运动模式的目标时，跟踪更加平滑稳定。在识别与定位环节，我们取得了关键性突破：通过对比测试，最终采纳了上交的“降自由度”yaw角优化算法。这项改进将装甲板在正对相机（0度附近）时的角度解算跳变，从原先的30度甚至60度大幅压制到5度以内，从源头上极大提升了姿态估计的稳定性与准确性，为后续预测提供了可靠输入\n5cccdc193d30d2cb4bdf2f23ad3d820.jpg\n我们在系统底层做了些许修改：将识别算法由神经网络算法调整为更适合运算平台的传统视觉识别，显著提升了处理速度；手写了一套脱离ROS的坐标变换器，彻底避免了ROS消息传递带来的时间错位；同时，我们实施了帧率控制器，并用插值法补偿图像从采集到处理的时间差，确保了每帧数据的时间戳尽可能地对齐。这一系列措施使得核心识别跟踪节点的帧率超过100Hz，考虑到图像传输的延迟达到25ms在保证时间尽可能的对齐下对自瞄瞄准进行帧率控制，控制在50帧,在保证实时性下,也使得系统更为的稳定\n我们开发的“约束平面法”能在线估计相机的俯仰、滚动角装配误差，这为现场快速校准提供了明确指导，大幅降低了调试门槛。目前，系统的主要局限在于超过5米后线性空气阻力模型的偏差会增大，同时，更为复杂的控制-执行延迟问题尚未完全解决：例如，基于模型预测控制（MPC）来补偿子弹飞行与火控延迟的方案，以及在装甲板切换时提前减速以匹配云台电机有限扭矩的动态规划，仍是下一阶段需要集成与攻克的重点。总体而言，今年的方案让系统在“稳、准、快”三个维度上都取得了扎实的进步。\n案例名称: 自瞄火控BUG排查\n使用的AI工具: deepseek\n典型场景描述: 自瞄程序存在火控不打弹或者火控没有效果一直打弹，你需要在庞大的代码库中插入print语句，反复复现问题，手动分析数据流，结合经验猜测可能原因（如坐标转换顺序错误、除法零值、数值溢出等）。 使用AI将核心的错误代码段、相关的变量输出、以及关键的日志信息粘贴给AI。提问为什么自瞄的火控在目标旋转的情况下一直不打弹\n实施效果对比: 传统人工实施: 耗时数小时取决于bug的隐蔽性依赖调试者的经验和运气 AI辅助实施: 几分钟获得多个可能原因的诊断列表，极大提高初步诊断效率，但仍需人工验证。\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%A7%86%E8%A7%89%E4%B8%AD%E6%9C%9F%E6%96%87%E6%A1%A3/","summary":"\u003ch1 id=\"视觉中期文档\"\u003e视觉中期文档\u003c/h1\u003e\n\u003cp\u003e634bebc64df5b4279a6577b9cf918ff.jpg\u003c/p\u003e\n\u003cp\u003e今年，我们视觉方案的核心是构建一个能在复杂比赛环境下稳定命中的高帧率自瞄系统。针对去年小陀螺跟踪不稳、远距离命中率低的问题，我们从预测模型、弹道解算到系统延时进行了全链路升级。\u003c/p\u003e","title":"视觉中期文档"},{"content":"视觉组培训计划 第一阶段: 电控视觉硬件C语言联培 推荐时间: 10.1~11.15 学习内容: 与电控，硬件联合进行C语言培训，每周布置一次作业并进行评分\n推荐学习资料: C primer plus 翁恺C语言\n考核方式: 月底 进行C语言线下考核\n第二阶段: C++考核 推荐时间: 11.15~12.27 学习内容: 自学并每周完成一次c++作业 观看黑马程序员cpp https://www.bilibili.com/video/BV1et411b73Z/?spm_id_from=333.337.search-card.all.click image.png\nP84P109(11.2311.30) P110P126(11.3112.7) P127P146 P185 196(12.812.15) P197-P235(12.1612.23)\n推荐学习资料: 黑马程序员cpp 代码随想录leetcode算法题\n考核方式: 12月末进行线下c++考核。\n第三阶段：寒假学习 一.环境配置 推荐时间: 1.3~1.17（放假之前） 要求在寒假放假之前配置好虚拟机，在虚拟机上配置好ubuntu20.04系统（群文件有），安装好vscode等相关工具，后续代码都在vscode上写 虚拟机建议安装VMware 通过百度网盘分享的文件：VMware-w… 链接:https://pan.baidu.com/s/1ct3Yc1m1CYDk621sNLGKHw?pwd=wa97 提取码:wa97 复制这段内容打开「百度网盘APP 即可获取」 虚拟机配置教程csdn和b站上都有教程，先安装好虚拟机和配置好系统，安装vscode和安装其他软件的教程在ros的教程上面有。\n二.OpenCV学习 推荐时间: 1.17~2.9 学习内容: 寒假期间观看https://www.bilibili.com/video/BV1i54y1m7tw/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6 image.png\nopencv必须掌握重点： Mat类，Scalar类，imshow，imread，imwrite，waitKey函数 cvtColor-色彩空间转换函数,二值化：inRange与threshold 形态学操作 检测轮廓findContours，绘制轮廓drawContours 几何图形绘制，例如：直线line,圆circle,正矩形rect,旋转矩形框Rectangle，minAreaRect函数-最小旋转外接矩形-返回对象Rectangle，boundingRect函数-最小外接正矩形-返回对象rect 读取视频文件与摄像头使用 滚动条操作 鼠标操作-点击图片获取该点的HSV值 Opencv电子书重点 第2，3，5，7章\n推荐学习资料: 《OpenCV计算机视觉编程攻略》 OpenCV4快速入门30讲贾志刚https://www.bilibili.com/video/BV1i54y1m7tw/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6 考核方式: 检查飞书学习笔记和贾志刚网课演示源码\n三.opencv具体考核要求: 推荐时间: 2.10~2.25 在虚拟机的vscode上跑opencv代码，实现识别装甲板视频，识别灯条的四个端点，视频在群文件，寒假结束会安排你们来基地，自己讲解自己代码的思路。代码自己写，不要问chatgpt，自己想办法解决装甲板灯条的配对问题，到时候具体讲解这一部分的思路。识别装甲板示范如下图，用框框住识别出的装甲板（框的颜色不做要求）。 image.png\n考核方式: 完成识别装甲板任务，作业提交时间：回校之前发识别好的视频和源码给学长学姐 推荐优先完成OpenCV识别考核再完成ros学习,OpenCV识别考核分值较大\n要求: 1.代码能跑 2.能够稳定框住装甲板 3.代码规范(可以使用AI辅助,抄网上开源的代码,但是自己要看得懂)\n四.ROS学习 推荐时间: 2.25~3.5\n学习内容: (看我下面发布的第一个视频) ROS1的发布话题服务的发布订阅: p36p75 坐标变换: p180p213\n推荐学习资料: ROS1 https://www.bilibili.com/video/BV1Ci4y1L7ZZ?spm_id_from=333.788.videopod.sections\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6\u0026amp;p=38 ROS2 https://www.bilibili.com/video/BV1gr4y1Q7j5/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6\n考核方式: 检查飞书学习笔记，检查网课演示源码\n第四阶段：核心业务学习 第四阶段总推荐学习时间:\n相机模型: （通识） 相机模型总推荐学习时间:\npnp算法: 推荐学习时间: 学习内容: 1.什么是pnp问题? 2.相机模型与投影方程? 3.pnp问题的常用解法? 4.opencv代码实践?\n推荐学习资料: OpenCV计算机视觉编程攻略 书籍 第11章 三维重建 网课视频: https://www.bilibili.com/video/BV1Rv4y1n7gp/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6\n相机标定.pdf\n考核方式: 在uabantu里面运行你们之前的识别代码,新加功能获取其深度信息,装甲板灯条尺寸可以在官网中找到\neg. IMG_20260305_203258.png\n相机硬件知识: 推荐学习时间:\n学习内容： 了解相机结构: 光圈 焦距 景深 快门 曝光 帧率 懂得使用mvs启动海康相机,懂得海康相机的机械结构\n推荐学习资料 1.群文件 2024赛季第四次培训\u0026mdash;\u0026mdash;-视觉硬件介绍.pdf 2.群文件 2024赛季视觉部第三次培训\u0026mdash;\u0026mdash;相机与陀螺仪.pdf 3.群文件 视觉slam十四讲 第5讲 相机与图像\n网课视频 BV1t24y1k7Ye\nBV1Qu411p7Jj\n考核方式: 下载mvs 使用mvs启动海康相机(学长学姐教学)\n相机标定: 推荐学习时间:\n学习内容: 理论知识: 单目标定 手眼标定 双目标定 实践: 使用matlab标定相机 使用opencv库标定相机\n推荐学习资料: OpenCV计算机视觉编程攻略 书籍 第11章 三维重建(同上) 群文件 相机标定.pdf https://www.bilibili.com/video/BV1Tg411K7RM/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6 https://www.bilibili.com/video/BV1By4y1b7Q7/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6 https://zhuanlan.zhihu.com/p/698229692\n考核方式: 在学会使用mvs启动相机的基础上学会使用matlab标定相机 学会编写opencv代码标定相机\n核心算法: (MPC和PID仅自瞄学习) 核心算法总推荐学习时间:\n卡尔曼滤波器: 推荐学习时间:\n学习内容: 掌握卡尔曼滤波器的基本原理\n推荐学习资料 https://www.bilibili.com/video/BV1Rh41117MT/?spm_id_from=333.337.search-card.all.click https://www.bilibili.com/video/BV1ez4y1X7eR/?spm_id_from=333.337.search-card.all.click\n考核方式: 飞书笔记\nMPC控制算法: 推荐学习时间:\n学习内容： 掌握mpc的基本原理\n推荐学习资料 https://www.bilibili.com/video/BV1cL411n7KV/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6 https://www.bilibili.com/video/BV1U54y1J7wh/?spm_id_from=333.337.search-card.all.click 视觉slam十四讲 第6讲 非线性优化\n考核方式: 飞书笔记\nPID算法(仅了解) 推荐学习时间:\n学习内容 简单了解PID算法\n推荐学习资料 https://www.bilibili.com/video/BV1et4y1i7Gm/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6 https://www.bilibili.com/video/BV12cvsBaE7S/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=d24aeec4a49e8c3e1300fbc5c806ebc6\n考核方式: 飞书笔记\n非线性优化 - ceres开源库 : 推荐学习时间:\n学习内容 ceres开源库的用法\n推荐学习资料 视觉slam十四讲 第6讲 非线性优化\n考核方式: 飞书笔记\n弹道解算: 推荐学习时间:\n学习内容: 无空气阻力模型 一次空气阻力模型 二次空气阻力模型 四阶龙格库塔(RK4)\n推荐学习资料: https://sourcelizi.github.io/202309/ballistic-algorithm/ https://zhuanlan.zhihu.com/p/1970271417149920247\n西工大弹道解算(无空气阻力)\n弹道解算.docx\n考核方式:\n坐标变换: （通识） 坐标变换推荐学习时间:\n坐标变换理论: 推荐学习时间:\n学习内容:\n旋转矩阵 旋转向量和欧拉角 四元数 相似,仿射,射影变换 rqt查看tf关系 rviz2查看坐标系变换 使用tf2库(静态坐标发布和动态坐标发布): 前面第三阶段的学习有布置过(如有遗忘可以复习一下)\n推荐学习资料: slam十四讲 第3讲 三维空间刚体运动 依赖ros发布的坐标变换源码\n考核方式: Rviz tf调试\nEigen矩阵库: 推荐学习时间:\n学习内容: 学会Eigen矩阵库的用法 Eigen矩阵库编写的坐标变换器源码\n推荐学习资料: slam十四讲 第3讲 三维空间刚体运动\n考核方式: 编写Eigen矩阵库坐标变换器\n串口通信: (通识) 推荐学习时间 学习内容: 理解串口通信: 波特率 数据位 停止位 校验位 自瞄串口模块源码 电控串口模块源码 串口调试助手\n推荐学习资料: 自瞄串口模块源码 电控串口模块源码 江协stm32 9-1~9-6 仅了解(看不懂的可跳过)\n考核方式: 调试串口\n神经网络(分流后神经网络学习) Python: 推荐学习时间 略 学习内容 Python基础语法,容器 推荐学习资料 Python速通https://www.bilibili.com/video/BV1Jgf6YvE8e/?spm_id_from=333.337.search-card.all.click\n考核方式 略\n深度学习 推荐学习时间 略 学习内容 学习李沐的课程 推荐学习资料 学习李沐的课程 https://www.bilibili.com/video/BV1oX4y137bC/?spm_id_from=333.1387.favlist.content.click\u0026amp;vd_source=03813710e4c946c3a825d7ea14b1b993 考核方式 略\nJupyter Notebook 推荐学习时间 略\n学习内容\n推荐学习资料 https://blog.csdn.net/m0_68678046/article/details/129703799?ops_request_misc=elastic_search_misc\u0026amp;request_id=46b65f628fb0e534060dcc5448341e16\u0026amp;biz_id=0\u0026amp;utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-129703799-null-null.142^v102^pc_search_result_base4\u0026amp;utm_term=jupyter\u0026amp;spm=1018.2226.3001.4187\n考核方式 略\nlabelme 推荐学习时间\n学习内容\n推荐学习资料 https://www.bilibili.com/video/BV1oX4y137bC/?spm_id_from=333.1387.favlist.content.click\u0026amp;vd_source=03813710e4c946c3a825d7ea14b1b993\n考核方式\nYOLO 推荐学习时间\n学习内容: 本地电脑和云服务器端分别搭建深度学习训练环境，熟练掌握 yolov5、yolov8、yolo11 的 detect（目标检测）和 pose（姿态估计）模型的训练\n根据装甲板识别需求选择合适的模型并完成定制化训练\n推荐学习资料 ultralytics/ultralytics: Ultralytics YOLO 🚀 人工智能新手环境搭建指南anaconda+pytorch+pycharm_哔哩哔哩_bilibili yolov8命令行运行参数详解_yolov8参数-CSDN博客 【手把手带你实战YOLOv5-拓展篇】使用AutoDL服务器进行模型训练_哔哩哔哩_bilibili\n考核方式\nNetron可视化工具 推荐学习时间\n学习内容 Netron模型可视化工具 使用 Netron 工具可视化深度学习模型的底层结构，理解 yolov5、yolov8、yolo11 的网络架构\n推荐学习资料 image.png\nyolo系列目标检测模型训练结果分析_yolo训练结果分析-CSDN博客 Yolov8目标识别——模型训练结果可视化图分析与评估训练结果_yolov8结果解析-CSDN博客 考核方式\n主流框架 推荐学习时间\n学习内容: 掌握 onnxruntime、OpenVINO、TensorRT 三大主流部署框架的使用方法\n推荐学习资料 最细致讲解yolov8模型推理完整代码\u0026ndash;（前处理，后处理）_yolov8代码-CSDN博客 C++ windows下使用openvino部署yoloV8_openvino yolov8-CSDN博客\n考核方式\n第五阶段: 熟悉并上手自瞄(后续补充) 阅读君瞄 陈君论文和陈君直播 上交青工会 自瞄架构学习 阅读西工大自瞄 常用工具链Todesk,git,cmake 常用linux命令行 C++进阶 学习如何调参调车(调曝光,调识别阈值,调卡尔曼滤波器,调mpc,坐标系补偿,)\n同济自瞄框架: image.png\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%A7%86%E8%A7%89%E7%BB%84%E5%9F%B9%E8%AE%AD%E8%AE%A1%E5%88%92/","summary":"\u003ch1 id=\"视觉组培训计划\"\u003e视觉组培训计划\u003c/h1\u003e\n\u003ch2 id=\"第一阶段-电控视觉硬件c语言联培\"\u003e第一阶段: 电控视觉硬件C语言联培\u003c/h2\u003e\n\u003cp\u003e推荐时间: 10.1~11.15\n学习内容: 与电控，硬件联合进行C语言培训，每周布置一次作业并进行评分\u003c/p\u003e\n\u003ch5 id=\"推荐学习资料\"\u003e推荐学习资料:\u003c/h5\u003e\n\u003cp\u003eC primer plus\n翁恺C语言\u003c/p\u003e\n\u003cp\u003e考核方式: 月底 进行C语言线下考核\u003c/p\u003e","title":"视觉组培训计划"},{"content":"数学建模 image.png\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%95%B0%E5%AD%A6%E5%BB%BA%E6%A8%A1/","summary":"\u003ch1 id=\"数学建模\"\u003e数学建模\u003c/h1\u003e\n\u003cp\u003eimage.png\u003c/p\u003e\n\u003cp\u003e\u003cimg\n  src=\"https://hydarealman.github.io/wander/images/feishu/%E6%95%B0%E5%AD%A6%E5%BB%BA%E6%A8%A1/image_842be0.png\"\n  alt=\"图片 1\"\n  loading=\"lazy\"\n  decoding=\"async\"\n  sizes=\"(max-width: 480px) 100vw, (max-width: 768px) 720px, 680px\"\n\u003e\n\u003c/p\u003e","title":"数学建模"},{"content":"数学建模 - matlab matlab 1.matlab界面及基本操作 clc \u0026mdash; 清空命令行 clear \u0026mdash; 清空工作区 按上方向键 \u0026mdash; 调用历史命令 % 后面写的都是注释 命令行敲回车执行 脚本文件 函数文件结尾尾缀.m 实时脚本文件尾缀.mlx 分节符\n2.matlab中两种引号 如果字符串本身有双引号,用双重双引号,让matlab识别双引号 如果字符串本身有单引号,用双重单引号,让matlab识别单引号 双引号得到的是1个string变量,单引号得到的是多个char变量 所以单引号可以用()访问第几个字符\n3.matlab矩阵运算 image.png\n1.plot(b) plot函数作图以索引为横坐标.索引就是该数字在矩阵里是\u0026quot;第几个\u0026quot; 2.grid on添加灰色网格线 3.多维矩阵:以空格或逗号分隔同一行元素,分号分隔各行 4.常见运算：转置A\u0026rsquo;，取逆inv(），求特征值和特征向量eig() 5.矩阵乘法*,和矩阵点乘.* 6.矩阵方程求解 A*x = b X = A\\b %表示A的逆矩阵乘以矩阵b(无论是斜杠还是反斜杠谁在相对下面的位置谁就取逆矩阵) 7.如果一个操作数是标量,而另一个数不是标量,则matlab会将该标量隐式扩展为与另一个操作数具有相同的大小 隐式扩展 image.png\n4.matlab的4种二维图 1.线图 plot函数代码示例\nx = 0:0.05:30; %从0到30,每隔0.05取一次值 y = sin(x) y2 = cos(x) %plot(x,y,\u0026lsquo;r\u0026rsquo;,\u0026lsquo;LineWidth\u0026rsquo;,2) plot(x,y,\u0026lsquo;r\u0026rsquo;,x,y2,\u0026lsquo;g\u0026rsquo;,\u0026lsquo;LineWidth\u0026rsquo;,2) xlabel(\u0026ldquo;横轴标题\u0026rdquo;) ylabel(\u0026ldquo;纵轴标题\u0026rdquo;) grid on %显示网格 axis([0 20 -1.5 1.5]) %设置横纵坐标范围 2.条形图 bar函数创建条形图 barh函数用来创建水平条形图\nt = -3:0.5:3; p = exp(-t.*t) %e的-t的平方 bar(t,p) barh(t,p) 3.极坐标图 polarplot函数用来绘制极坐标图\ntheta = 0:0.01:2pi; %弧度 %abs求绝对值或复数的模 radi = abs(sin(2theta).cos(2theta)); %半径(注意是点乘) polarplot(theta,radi) %括号内是弧度和半径 4.散点图 scatter函数用来绘制x和y值的散点图\n5.matlab三维图和内嵌子图 1.三维曲面图 surf函数可以用来做三维曲面图。一般是展示函数z = z(x,y)的图像. 首先需要用meshgrid创建好空间上(x,y)点\n[X,Y] = meshgrid(-2:0.05:2); %Z = X.^2 + Y.^2 Z = X.*exp(-X.^2-Y.^2); surf(X,Y,Z); colormap hsv % colormap设置颜色,可跟winter,summer等 colorbar 2.子图 使用subplot函数可以在同一窗口的不同子区域显示多个绘图\ntheta = 0:0.01:2pi; %弧度 %abs求绝对值或复数的模 radi = abs(sin(2theta).cos(2theta)); %半径(注意是点乘) Height = randn(1000,1) %生成1000行1列的符合正太分布的随机数 Weight = randn(1000,1)\nsubplot(2,2,1); surf(X.^2); title(\u0026lsquo;1st\u0026rsquo;); subplot(2,2,2); surf(Y.^3); title(\u0026lsquo;2nd\u0026rsquo;); subplot(2,2,3); polarplot(theta,radi); title(\u0026lsquo;2nd\u0026rsquo;); subplot(2,2,4); scatter(Height,Weight); title(\u0026lsquo;4th\u0026rsquo;);\n6.matlab导入数据 导入的范围 导入的数据的范围默认是从第二行开始的，第一行一般是标题行 如果不想导入所有数据，可以按住ctrl键，选择想导入的内容，例如某行，某列 变量名称行也就是导入之后，matlab里表讴歌最上方会显示变量，一般默认选择原文件第一行。但是只能识别英文.如果是汉字则变成VerName\n导入类型 image.png\n处理无法导入的数据 选择替换,则所有字符串都变成NaN 选择排除行，那么某一行只要有字符串，这一行数据都不会被导入 选择排除列，同上\n7.matlab处理缺失值和异常值 算法 线性规划 概念 线性规划就是再一组线性约束条件下,求线性目标函数的最大或最小值. \u0026lsquo;\u0026lsquo;线性\u0026quot;就是所有变量都是一次方 image.png\n关键词:怎样安排/分配，尽量多少，利润最大，最合理\n线性规划-代码实现 模型化为matlab标准型:目标函数最小值,约束条件小于等于号或等号 求y的最大值等价于求-y的最小值\nlinprog 1.求解线性规划问题\nx = linprog(f, A, b, Aeq, beq, lb, ub) f：目标函数的系数向量，表示目标函数 minfTx。 A 和 b：不等式约束 Ax≤b。 Aeq 和 beq：等式约束 Aeqx=beq。 lb 和 ub：变量的上下界，分别表示 lb≤x≤ub。\nintlinprog 2.求解整数线性规划问题\nx = intlinprog(f, intcon, A, b, Aeq, beq, lb, ub)\nlinprog 的其他选项 options：可以通过 optimoptions 设置优化选项，例如算法选择（\u0026lsquo;dual-simplex\u0026rsquo;、\u0026lsquo;interior-point\u0026rsquo; 等）。\noptions = optimoptions(\u0026rsquo;linprog\u0026rsquo;, \u0026lsquo;Algorithm\u0026rsquo;, \u0026lsquo;dual-simplex\u0026rsquo;); x = linprog(f, A, b, Aeq, beq, lb, ub, options);\noptimproblem 4.用于定义优化问题，包括线性规划问题。\nprob = optimproblem; prob.Objective = f\u0026rsquo; * x; prob.Constraints.cons1 = A * x \u0026lt;= b; prob.Constraints.cons2 = Aeq * x == beq; [sol, fval] = solve(prob);\noptimoptions 5.用于设置优化选项，例如算法选择、容差、迭代次数等。\noptions = optimoptions(\u0026rsquo;linprog\u0026rsquo;, \u0026lsquo;Algorithm\u0026rsquo;, \u0026lsquo;interior-point\u0026rsquo;, \u0026lsquo;Display\u0026rsquo;, \u0026lsquo;iter\u0026rsquo;);\n可以转化为线性规划的问题\n线性规划模型建模实战与代码 没搞太懂\n整数规划 ","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E6%95%B0%E5%AD%A6%E5%BB%BA%E6%A8%A1-matlab/","summary":"\u003ch1 id=\"数学建模---matlab\"\u003e数学建模 - matlab\u003c/h1\u003e\n\u003ch1 id=\"matlab\"\u003ematlab\u003c/h1\u003e\n\u003ch2 id=\"1matlab界面及基本操作\"\u003e1.matlab界面及基本操作\u003c/h2\u003e\n\u003cp\u003eclc \u0026mdash; 清空命令行\nclear \u0026mdash; 清空工作区\n按上方向键 \u0026mdash; 调用历史命令\n% 后面写的都是注释\n命令行敲回车执行\n脚本文件 函数文件结尾尾缀.m\n实时脚本文件尾缀.mlx\n分节符\u003c/p\u003e","title":"数学建模 - matlab"},{"content":"完整形态文档: 自瞄开源引用 西工大自瞄: https://github.com/SnocrashWang/WMJAimer/wiki/WMJAimer-Project-Report 结合卡尔曼滤波器和熵权法匹配多运动模型的整车建模\n上交自瞄: https://github.com/julyfun/rm.cv.fans?tab=readme-ov-file 三分法降自由度的yaw角优化 弹道重现的可视化调参 坐标变换器\n同济自瞄: https://github.com/TongjiSuperPower/sp_vision_25/ 暴力搜索法降自由度的yaw角优化 MPC轨迹规划器\n华南师范自瞄: https://github.com/FaterYU/rm_auto_aim fitLine角点优化\n中南大学自瞄 https://github.com/CSU-FYT-Vision/FYT2024_vision pca角点优化\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E5%AE%8C%E6%95%B4%E5%BD%A2%E6%80%81%E6%96%87%E6%A1%A3-%E8%87%AA%E7%9E%84%E5%BC%80%E6%BA%90%E5%BC%95%E7%94%A8/","summary":"\u003ch1 id=\"完整形态文档-自瞄开源引用\"\u003e完整形态文档: 自瞄开源引用\u003c/h1\u003e\n\u003cp\u003e西工大自瞄:\n\u003ca href=\"https://github.com/SnocrashWang/WMJAimer/wiki/WMJAimer-Project-Report\"\u003ehttps://github.com/SnocrashWang/WMJAimer/wiki/WMJAimer-Project-Report\u003c/a\u003e\n结合卡尔曼滤波器和熵权法匹配多运动模型的整车建模\u003c/p\u003e\n\u003cp\u003e上交自瞄:\n\u003ca href=\"https://github.com/julyfun/rm.cv.fans?tab=readme-ov-file\"\u003ehttps://github.com/julyfun/rm.cv.fans?tab=readme-ov-file\u003c/a\u003e\n三分法降自由度的yaw角优化 弹道重现的可视化调参 坐标变换器\u003c/p\u003e\n\u003cp\u003e同济自瞄:\n\u003ca href=\"https://github.com/TongjiSuperPower/sp_vision_25/\"\u003ehttps://github.com/TongjiSuperPower/sp_vision_25/\u003c/a\u003e\n暴力搜索法降自由度的yaw角优化 MPC轨迹规划器\u003c/p\u003e","title":"完整形态文档: 自瞄开源引用"},{"content":"相机标定 相机标定基本概念 OpenCV中的相机标定是计算机视觉中的一个重要任务，它用于确定相机的内部参数（如焦距、主点位置等）和外部参数（如相机相对于世界坐标系的旋转和平移）。 确定内部参数和外部参数\n世界坐标系(world coordinate system)：⽤户定义的三维世界的坐标系，为了描述⽬标物在真实世 界⾥的位置⽽被引⼊，代表真实世界的坐标。单位为m，⽤来表示。\n相机坐标系(camera coordinate system)：在相机上建⽴的坐标系，为了从相机的⻆度描述物体位 置⽽定义，作为沟通世界坐标系和图像/像素坐标系的中间⼀环。代表以相机光学中⼼为原点的坐标 系，⽤ Xc ● Yc, Zc 来表示， 与相机的光轴重合，单位为m。\n图像坐标系(image coordinate system)：为了描述成像过程中物体从相机坐标系到图像坐标系的投 影透射关系⽽引⼊，⽅便进⼀步得到像素坐标系下的坐标。 单位为m。\n像素坐标系(pixel coordinate system)：为描述像素点在矩阵中的位置⽽引⼊，以像素矩阵的左上 ⽅端点作为源点建⽴的坐标系，单位为pixel。 //归一化平面\n陀螺仪参考坐标系 在相机坐标系中有提到，当相机发生旋转运动时，相机坐标系也会随之一起运动。因此，当相机在发生旋转运动时，想要知道物体和相机的相对位置关系变化将会变得更困难。 对此，我们想要找到一个坐标系，在相机旋转时保持不变，这就是陀螺仪参考坐标系。陀螺仪参考系通过固定在相机上的陀螺仪，实时结算相机的位姿，进而得到一个不随相机旋转的坐标系。 重点:不随相机旋转，但随相机位移\n从三维坐标（世界坐标系）到二维坐标（图像坐标系）又可以分为三个步骤： （1）从世界坐标转换到相机坐标；具体过程略 （2）从相机坐标转换到图像坐标；具体过程略 （3）从图像坐标转换到像素坐标。具体过程略\n四大坐标系之间的关系: 世界坐标系 – [平移] –\u0026gt; 陀螺仪坐标系 – [旋转] –\u0026gt; 相机坐标系 – [投影] –\u0026gt; 像素坐标系\n摄影摄像机: 是与现实生活中摄像机硬件设备对应的最普遍的相机模型,可以用摄影几何的工具研究相机模型的构造\n摄像机矩阵 几何模型的参数包含内参数和外参数\n内参数: 是摄像机固有参数，从出厂时刻就伴随而来。如果不发生硬件系统的改变，内参数标定获得之后，可以长期使用\n包括：主距，主点，畸变参数\n外参数: 是 反映摄像机在物理世界坐标系中的位置和姿态参数，是一个和观测任务和观测场景相关的参数\n失真畸形 光线穿过透镜会在感光器件平面上产生非线性失真,将其称为图像的失真畸形 焦距越短,失真越明显 畸变参数：无畸变图像，正径向畸变-桶形，负径向畸变-枕形，切向畸变 径向 半径方向\n透视成像\n镜头畸变的解析方程\n径向畸变参数\n切向畸变参数\n张正友标定法 装甲板识别 image.png\nlabelme安装 Labelme安装及使用教程 Labelme是一款开源的图像标注工具，主要用于神经网络构建前的数据集准备工作。以下是基于Anaconda的安装及使用教程。 安装步骤 创建Anaconda虚拟环境 首先，打开Anaconda Prompt，输入以下命令创建一个名为labelme的虚拟环境，并指定Python版本为3.6： conda create -n labelme python=3.6 创建完成后，激活该环境： conda activate labelme 此时，运行环境已切换到labelme。 安装依赖环境 安装labelme所需的依赖库，可以使用pip或conda命令： conda install pyqt conda install pillow 如果遇到问题，可以尝试使用另一种命令。 安装Labelme 安装Labelme，可以使用以下命令： conda install labelme=3.16.2 如果conda命令失败，可以使用pip命令： pip install labelme==3.16.2 注意：一定要指定版本号3.16.2，否则在后续json到dataset的转换过程中可能会出现异常(1)(2)。 使用教程 启动Labelme 在激活的labelme环境中，输入以下命令启动Labelme： labelme 此时会弹出Labelme的操作界面。 标注图片 点击“Open Dir”按钮，选择待标注图片所在的文件夹。然后可以通过右键选择标注工具，如矩形、圆形、点和线等。标注完成后，点击“Save”按钮保存标注结果，生成的json文件建议与原图保存在同一目录下(1)(3)。 Json转Dataset 将标注好的json文件转化为数据集，首先找到labelme的json_to_dataset.py文件，路径如下： D:\\Anaconda\\envs\\labelme\\Lib\\site-packages\\labelme\\cli 打开该文件，进行如下修改：\nimport argparse import json import os import os.path as osp import warnings import PIL.Image import yaml from labelme import utils import base64\ndef main(): warnings.warn(\u0026ldquo;This script is aimed to demonstrate how to convert the JSON file to a single image dataset, and not to handle multiple JSON files to generate a real-use dataset.\u0026rdquo;) parser = argparse.ArgumentParser() parser.add_argument(\u0026lsquo;json_file\u0026rsquo;) parser.add_argument(\u0026rsquo;-o\u0026rsquo;, \u0026lsquo;\u0026ndash;out\u0026rsquo;, default=None) args = parser.parse_args()\njson_file = args.json_file if args.out is None: out_dir = osp.basename(json_file).replace(\u0026rsquo;.\u0026rsquo;, \u0026lsquo;_\u0026rsquo;) out_dir = osp.join(osp.dirname(json_file), out_dir) else: out_dir = args.out if not osp.exists(out_dir): os.mkdir(out_dir)\ncount = os.listdir(json_file) for i in range(0, len(count)): path = os.path.join(json_file, count[i]) if os.path.isfile(path): data = json.load(open(path)) if data[\u0026lsquo;imageData\u0026rsquo;]: imageData = data[\u0026lsquo;imageData\u0026rsquo;] else: imagePath = os.path.join(os.path.dirname(path), data[\u0026lsquo;imagePath\u0026rsquo;]) with open(imagePath, \u0026lsquo;rb\u0026rsquo;) as f: imageData = f.read() imageData = base64.b64encode(imageData).decode(\u0026lsquo;utf-8\u0026rsquo;) img = utils.img_b64_to_arr(imageData) label_name_to_value = {\u0026rsquo;background\u0026rsquo;: 0} for shape in data[\u0026lsquo;shapes\u0026rsquo;]: label_name = shape[\u0026rsquo;label\u0026rsquo;] if label_name in label_name_to_value: label_value = label_name_to_value[label_name] else: label_value = len(label_name_to_value) label_name_to_value[label_name] = label_value\nlabel_values, label_names = [], [] for ln, lv in sorted(label_name_to_value.items(), key=lambda x: x[1]): label_values.append(lv) label_names.append(ln) assert label_values == list(range(len(label_values)))\nlbl = utils.shapes_to_label(img.shape, data[\u0026lsquo;shapes\u0026rsquo;], label_name_to_value) captions = [\u0026rsquo;{}: {}\u0026rsquo;.format(lv, ln) for ln, lv in label_name_to_value.items()] lbl_viz = utils.draw_label(lbl, img, captions)\nout_dir = osp.basename(count[i]).replace(\u0026rsquo;.\u0026rsquo;, \u0026lsquo;_\u0026rsquo;) out_dir = osp.join(osp.dirname(count[i]), out_dir) if not osp.exists(out_dir): os.mkdir(out_dir)\nPIL.Image.fromarray(img).save(osp.join(out_dir, \u0026lsquo;img.png\u0026rsquo;)) utils.lblsave(osp.join(out_dir, \u0026rsquo;label.png\u0026rsquo;), lbl) PIL.Image.fromarray(lbl_viz).save(osp.join(out_dir, \u0026rsquo;label_viz.png\u0026rsquo;))\nwith open(osp.join(out_dir, \u0026rsquo;label_names.txt\u0026rsquo;), \u0026lsquo;w\u0026rsquo;) as f: for lbl_name in label_names: f.write(lbl_name + \u0026lsquo;\\n\u0026rsquo;)\nwarnings.warn(\u0026lsquo;info.yaml is being replaced by label_names.txt\u0026rsquo;) info = dict(label_names=label_names) with open(osp.join(out_dir, \u0026lsquo;info.yaml\u0026rsquo;), \u0026lsquo;w\u0026rsquo;) as f: yaml.safe_dump(info, f, default_flow_style=False) print(\u0026lsquo;Saved to: %s\u0026rsquo; % out_dir)\nif name == \u0026lsquo;main\u0026rsquo;: main() 将json文件放在一个目录下，然后在命令行中执行以下命令进行批量处理： labelme_json_to_dataset.exe D:\\Spyder\\label_dataset 执行成功后，会在指定目录下生成相应的数据集(1)(2)。\n相机标定实操 启动MVS，左侧连接相机,右侧调整参数\n使用matlab做相机标定 https://blog.csdn.net/weixin_45718019/article/details/105823053\n相机标定\u0026mdash;\u0026mdash;标定图像规范 https://blog.csdn.net/j_shui/article/details/77262947\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A/","summary":"\u003ch1 id=\"相机标定\"\u003e相机标定\u003c/h1\u003e\n\u003ch1 id=\"相机标定基本概念\"\u003e相机标定基本概念\u003c/h1\u003e\n\u003cp\u003eOpenCV中的相机标定是计算机视觉中的一个重要任务，它用于确定相机的内部参数（如焦距、主点位置等）和外部参数（如相机相对于世界坐标系的旋转和平移）。\n确定内部参数和外部参数\u003c/p\u003e","title":"相机标定"},{"content":"自瞄赛前代码检查+备忘录 赛前检查代码+调参: 1.硬补偿(瞄准yaml) 调落点\n2.识别阈值(曝光: 识别yaml + 二值化阈值new_detector.hpp文件中): 基地识别参数: 160 2300 3v3比赛参数: 220 2300(当时英雄给了220 1000 不知道220 2300怎么样没试) 7v7比赛参数: 待定\n3.火控姿态阈值(aim_pose_tolerance)步兵: 0.05 英雄: 0.035\n4.装甲板相对偏角偏差阈值(aim_angle_tolerance) 步兵: 40 英雄: 25\n5.移植代码的时候考虑环境问题,需删除同济MPC和tinyMPC库 以及相应CMakeList.txt\n6.电控时延: 看火控线和云台线 (火控线在云台线的右边就增加时延， 火控线在云台线的左边就增加时延) 先平移 50ms的调让火控线和云台线基本吻合 慢速旋转 看火控线和云台线基本吻合 小陀螺 看火控线和云台线基本吻合\n7.程序延时: 调好电控时延后打平移靶子调 运动同方向超调 减预测时间 运动反方向超调 加预测时间\n8.检查自动查找串口 f637e679cc0c1934e1d98e61fb214ab.jpg\ne92a53244be4704361429cd94fc750e.jpg\n9.卡尔曼参数 半径卡尔曼参数由0.00001 -\u0026gt; 0.0005(相较于3v3扩大了50倍) 51611ae3084b7210cda03377a1d1e67.jpg\n10.是否是新火控代码: 现在所有车都使用了流水火控 8481d0449250cc2422dee2ea0644036.jpg\ncfaefb6388a489b4061dcf0b6aa28b3.jpg\ndcd36a850bf41c0c03e9ffffeff7b84.jpg\n02601d85ae6532a2c725a734acefea4.jpg\n11.是否添加了识别数字判断装甲板类型 否则识别大装甲板旋转目标的时候会出现问题(原先识别不够严谨,大装甲板倾斜时可能会判断为小装甲板) c572f2c570e9e880bbbfde031eb22f8.jpg\n12.是否更改了最新的降自由度 23f24261fdd1d3d72a213b40667fb37.jpg\nb5c208143d1f2df5346580a43472032.jpg\n13.看看锁中心的变量是否给了1 28af8e58d715f57a8ad9200d392b2ae.jpg\nda03f0e6d6b0617c70641cee152263f.jpg\n14.串口补偿(瞄准yaml + 串口yaml) 使用new_Rmidentify同济降自由度的roll和pitch误差测定函数进行误差测定 串口补偿英雄目前给: -0.09 (调参方法: 调一个值 前后平移目标看两侧装甲板会不会飘 -\u0026gt; 直到两块装甲板不会飘后说明参数基本吻合) 其他没有那么大可以勉强给: 0\n15.相机内参(注意检查相机内参是不是这个车的 两个yaml文件的相机内参是否一致):\n16.弹速: 弹速(英雄 -\u0026gt; 12 (步兵 + 哨兵 + 英雄) -\u0026gt; 21) 弹速微调 -\u0026gt; 问电控 参数具体是0.几\n17.坐标系(识别yaml + 瞄准yaml)\n18.串口线和相机线是否插稳 是否打胶 相机线的螺丝是否拧紧 相机焦距和光圈的螺丝是否会干涉（若螺丝松了 焦距是否糊了） 相机插上微机后是否蓝光还是闪烁蓝光 如果是红光说明掉相机了\n19.是否将跟踪变量更改为1,yaml文件 和参数配置文件的默认配置\n20.降自由度那里 大装甲板的优化是否更改为小装甲板 Cache_-676622a9238366b7.jpg\n改成这个样子: IMG_20260421_225541.jpg\n22.是否有迭代法求子弹飞行时间\n23.更改最新的前哨战的卡尔曼参数\n哨兵decision函数和armorupdate的更新 , 哨兵不打工程, 新的前哨战卡尔曼代码\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%87%AA%E7%9E%84%E8%B5%9B%E5%89%8D%E4%BB%A3%E7%A0%81%E6%A3%80%E6%9F%A5-%E5%A4%87%E5%BF%98%E5%BD%95/","summary":"\u003ch1 id=\"自瞄赛前代码检查备忘录\"\u003e自瞄赛前代码检查+备忘录\u003c/h1\u003e\n\u003cp\u003e赛前检查代码+调参:\n1.硬补偿(瞄准yaml) 调落点\u003c/p\u003e\n\u003cp\u003e2.识别阈值(曝光: 识别yaml + 二值化阈值new_detector.hpp文件中):\n基地识别参数: 160 2300\n3v3比赛参数: 220 2300(当时英雄给了220 1000 不知道220 2300怎么样没试)\n7v7比赛参数: 待定\u003c/p\u003e","title":"自瞄赛前代码检查+备忘录"},{"content":"自瞄使用说明书 长按鼠标右键进入自瞄,松开鼠标右键退出自瞄 右键后按住q键自动开火,松开右键取消自瞄和自动开火,(考虑到英雄弹频过低,且大弹丸触发严格,英雄火控阈值较为严格,如果q键没有打弹说明自瞄认为当前开火时机不够好不适合打弹(也有可能是卡弹了)) 建议如果使用自瞄击打目标请使用q键开火 左键手动开火 请在装甲板数字和灯条未遮挡时使用自瞄，否则视觉无法识别敌方装甲板 如若不确定自瞄是否识别敌方装甲板到请使用q键开火 目前自瞄合适的射击距离在4.5m以内,超过4.5m后命中率会显著下降,但还是可以尝试击打 温馨提示: 如若自瞄瞄歪或者掉自瞄,请果断在该场比赛放弃自瞄,并在赛后马上寻找视觉组紧急处理问题\n","permalink":"https://hydarealman.github.io/wander/posts/1970/01/%E8%87%AA%E7%9E%84%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E%E4%B9%A6/","summary":"\u003ch1 id=\"自瞄使用说明书\"\u003e自瞄使用说明书\u003c/h1\u003e\n\u003cp\u003e长按鼠标右键进入自瞄,松开鼠标右键退出自瞄\n右键后按住q键自动开火,松开右键取消自瞄和自动开火,(考虑到英雄弹频过低,且大弹丸触发严格,英雄火控阈值较为严格,如果q键没有打弹说明自瞄认为当前开火时机不够好不适合打弹(也有可能是卡弹了))\n建议如果使用自瞄击打目标请使用q键开火\n左键手动开火\n请在装甲板数字和灯条未遮挡时使用自瞄，否则视觉无法识别敌方装甲板 如若不确定自瞄是否识别敌方装甲板到请使用q键开火\n目前自瞄合适的射击距离在4.5m以内,超过4.5m后命中率会显著下降,但还是可以尝试击打\n温馨提示: 如若自瞄瞄歪或者掉自瞄,请果断在该场比赛放弃自瞄,并在赛后马上寻找视觉组紧急处理问题\u003c/p\u003e","title":"自瞄使用说明书"},{"content":"","permalink":"https://hydarealman.github.io/wander/commits/","summary":"","title":"提交历史"}]