
周五上午焊接机器人工作站。这条产线6 台六轴机器人跑弧焊点位都是离线编程软件导出来的工艺员小郑把一份 CSV 拖到桌面上每个轨迹段由一串示教点组成点里有 XYZ 坐标和关节角度。我想知道每段轨迹实际走了多长、大概花多少时间哪段最耗时能不能优化路径顺序把节拍压下来。我点开数据。这表里有什么小郑问。每条记录是一个轨迹点带 segment_id轨迹段编号、point_seq段内序号、x/y/z 坐标、进给速度档位我指着屏幕问题是它只给点不给距离也不给时间。距离得相邻点做欧氏距离累加时间得用距离除以该段设定进给速度。现在离线软件能出总路径长但拆不到段也关联不上实际节拍。我就想干一件事小郑说把每个轨迹段的运动距离算出来按段内设定速度估算耗时排个序画出来。如果某段距离只有 200mm 却花了 1.2 秒说明速度被限了可能是靠近工件防碰撞降速如果某段距离 800mm 但耗时跟 200mm 那段差不多说明速度档用高了得复核是不是会甩弧。比如段 S-0312 个点折线累计 386mm设定进给 300mm/s我接话理论耗时 1.29s但实际离线仿真给的是 1.8s多出来的 0.5s 是加减速补偿。六轴机器人走折线不是匀速滑过去每个拐点都要减速再加速这段拐点多所以耗时偏差大。对小郑点头还有我想看各段耗时的占比整条程序总节拍是多少机器人之间有没有某台路径特别长导致等件。最好还能把轨迹段按距离-耗时特征聚个类看看哪些是长直段、哪些是密集折线段、哪些是微动调整段。用 pandas 按 segment_id 分组算折线距离numpy 向量化算欧氏距离和加减速补偿matplotlib 画段距离条形图耗时饼图轨迹散点scipy 做距离分布检验scikit-learn 聚类轨迹段画像networkx 建段-点关系网我开工程数据自包含合成一批离线编程点位数据下载就能跑。敲了行原型seg df.groupby(segment_id).apply(lambda g: np.sum(np.linalg.norm(g[[x,y,z]].diff().dropna().values, axis1)))est_time seg / feed_speed corner_penalty完整版用 OOP 封好我说一个类管点位加载一个类算段距离一个类估算耗时含加减速模型一个类做统计检验一个类聚类轨迹段一个类建段-点关系网一个类出图。输出段距离表、耗时表、聚类结果存 results/。小郑凑近看那以后看报告8 个轨迹段总距离 2.84m理论总耗时 9.6s含加减速补偿后 11.3s。S-05 段距离最短120mm但单位距离耗时最高因为是 9 个微折点绕焊缝起弧位S-02 是长直段 760mm耗时 2.5s 最稳。聚类分了三类长直段 / 折线段 / 微动段。6 台机器人里 3 号机总路径最长比均值多 18%查了是离线程序没做路径合并重复走了回程空走。合并后整线节拍从 11.3s 压到 9.8s。对我接话轨迹不是看点数是看折线长度拐点损耗速度档匹配。数字孪生里要建机器人节拍仿真模型这些段级耗时就是校准基准。一、实际应用场景真实痛点场景设定多机器人弧焊/搬运工作站轨迹由离线编程软件OLP导出为点位序列。工艺员需要按轨迹段量化运动距离与估算耗时识别空走路径、拐点损耗段、限速段优化程序顺序与速度档压低整线节拍。现场原话叙事化不是我们不会编路径小郑说是以前编完就直接下机靠示教器跑一遍看节拍。示教器显示总周期 12 秒但说不清哪段占了 5 秒。有次客户要求节拍压到 10 秒我们改了三天速度档最后发现是某段回程空走了 400mm离线软件里根本没标红。还有拐点问题小郑补充焊接轨迹绕角的地方机器人每个拐点都要减速离线软件按直线算时间实际慢了 30%。这种理论时间 vs 实际时间的偏差不按段拆开算永远对不上。核心矛盾离线编程点位流水 与 段级距离聚合 耗时估算含加减速 段画像聚类 多机路径均衡 节拍优化 之间的断层。二、痛点分析映射到滨州职业学院《先进制造技术》课程模型《先进制造技术》模块 本篇痛点对应工业机器人技术基础轨迹规划、运动学、节拍计算 段级轨迹距离与耗时估算数控加工与CAD/CAM技术刀路/路径优化思想迁移 路径合并与空走消除智能制造与数字孪生机器人运动仿真模型 段级耗时为孪生节拍模型提供基准FMS与先进生产管理多机协同节拍平衡 多机器人路径长度均衡先进制造新模式数据驱动工艺优化 聚类识别路径模式指导重构一句话总结我们需要一个机器人离线编程轨迹段距离与耗时估算程序用pandas 按段聚合点位numpy 算欧氏距离与加减速补偿matplotlib 画距离条形图/耗时饼图/轨迹散点scipy 做距离分布检验scikit-learn 聚类轨迹段画像networkx 建段-点关系网实现从点位序列到段距离 耗时估算 模式聚类 多机均衡。三、核心逻辑讲解大白话3.1 问题本质把轨迹想成快递员送件路线把机器人轨迹想成快递员在车间里走路线* 点位 路线上的打卡点A点→B点→C点* 轨迹段 一段任务比如绕这个焊缝走一圈* 距离 两点之间直线走多远折线累加不是首尾直线* 耗时 距离 ÷ 速度但每个拐角要刹车再加速得加补偿时间* 长直段 高速路可以跑满速* 折线段 老城区巷子拐弯多得慢* 微动段 原地调整姿势走几毫米也要减速* 整线节拍 所有段耗时加起来* 多机均衡 几个快递员别有的累死有的闲着3.2 业务逻辑 → 代码映射导入离线编程点位数据│▼ TrajLoader (pandas)读取 CSVrobot_id, segment_id, point_seq, x, y, z,feed_speed, joint_config(可选)排序校验坐标│▼ SegmentDistanceCalculator (numpy/pandas)段距离计算段内相邻点欧氏距离累加L Σ √(Δx²Δy²Δz²)│▼ TimeEstimator (numpy)耗时估算直线耗时 L / feed_speed拐点补偿 拐点数 × 减速加速时间总耗时 直线耗时 拐点补偿│▼ TrajStatistics (scipy)统计检验段距离分布正态性(Shapiro)多机器人总距离 ANOVA / t 检验│▼ SegmentProfiler (scikit-learn)轨迹段聚类特征[距离, 耗时, 拐点密度, 单位距离耗时]KMeans → 长直段/折线段/微动段│▼ TrajGraph (networkx)段-点关系网节点段/点边顺序连接, 边权段内距离│▼ TrajVisualizer (matplotlib)可视化1. 各段距离条形图2. 各段耗时占比饼图3. 轨迹三维散点投影(XY平面颜色表段)4. 聚类散点图(距离 vs 单位距离耗时)5. 段-点关系网│▼ SyntheticTrajGenerator (numpy)合成数据多机器人×多段, 含长直/折线/微动三类含空走回程段(可优化)3.3 为什么不能只看总路径长视角 问题总距离 2.8m 掩盖某段 120mm 却耗时最长理论时间 距离/速度 忽略拐点加减速偏差 30%段级拆解拐点补偿 真实耗时逼近示教器实测聚类分模式 知道该优化哪类段3.4 分析前后对比维度 离线软件导出 本程序段距离 仅总长 逐段累加耗时估算 直线模型 含拐点加减速路径模式 无 聚类分类多机均衡 无 统计对比优化方向 凭经验 数据指向空走段/限速段四、OOP 代码实现4.1 项目结构robot_traj_analysis/├── robot_traj_analysis/│ ├── __init__.py│ ├── traj_loader.py # 点位加载│ ├── distance_calculator.py # 段距离计算│ ├── time_estimator.py # 耗时估算(含加减速)│ ├── traj_statistics.py # 统计检验(scipy)│ ├── segment_profiler.py # 轨迹段聚类(sklearn)│ ├── traj_graph.py # 段-点关系网(networkx)│ ├── visualizer.py # 可视化│ └── synthetic_data.py # 合成数据├── tests/│ ├── __init__.py│ └── test_traj_analysis.py├── results/│ ├── segment_distance_bar.png│ ├── segment_time_pie.png│ ├── traj_xy_scatter.png│ ├── segment_cluster.png│ ├── traj_network.png│ ├── segment_metrics.csv│ ├── robot_summary.csv│ ├── segment_clusters.csv│ └── traj_report.txt└── run_traj_analysis.py4.2 核心源码detailssummary/summary机器人离线编程点位加载器import pandas as pdfrom pathlib import Pathfrom typing import Optionalclass TrajLoader:加载离线编程导出的点位CSVdef __init__(self, filepath: str traj_points.csv,encoding: str utf-8):self.filepath Path(filepath)self.encoding encodingself._raw: Optional[pd.DataFrame] Nonedef load(self) - pd.DataFrame:if not self.filepath.exists():raise FileNotFoundError(f文件不存在: {self.filepath})self._raw pd.read_csv(self.filepath, encodingself.encoding)rename {}for tgt, al in {robot_id: [robot_id, 机器人, robot],segment_id: [segment_id, 轨迹段, seg],point_seq: [point_seq, 点序号, seq],x: [x, X坐标, x_coord],y: [y, Y坐标, y_coord],z: [z, Z坐标, z_coord],feed_speed: [feed_speed, 进给速度, speed],}.items():if tgt not in self._raw.columns:for a in al:if a in self._raw.columns:rename[a] tgtbreakself._raw self._raw.rename(columnsrename)req [segment_id, point_seq, x, y, z]miss [c for c in req if c not in self._raw.columns]if miss:raise ValueError(f缺少必要列: {miss})for c in [x, y, z, feed_speed]:if c in self._raw.columns:self._raw[c] pd.to_numeric(self._raw[c], errorscoerce)self._raw self._raw.dropna(subset[x, y, z]).copy()self._raw[robot_id] self._raw.get(robot_id, R-01).fillna(R-01).astype(str).str.strip()self._raw[segment_id] self._raw[segment_id].astype(str).str.strip()self._raw[point_seq] pd.to_numeric(self._raw[point_seq], errorscoerce).fillna(0).astype(int)# 按段内序号排序self._raw self._raw.sort_values([robot_id, segment_id, point_seq]).reset_index(dropTrue)# 默认进给速度if feed_speed not in self._raw.columns:self._raw[feed_speed] 300.0self._raw[feed_speed] self._raw[feed_speed].fillna(300.0)return self._raw/detailsdetailssummary/summary轨迹段距离计算 (numpy)import numpy as npimport pandas as pdfrom typing import Optionalclass SegmentDistanceCalculator:按段计算折线累计距离与拐点信息def __init__(self):passdef segment_metrics(self, df: pd.DataFrame) - pd.DataFrame:计算每段距离、点数、拐点数rows []for (robot, seg), g in df.groupby([robot_id, segment_id]):pts g[[x, y, z]].values.astype(float)if len(pts) 2:dist 0.0corners 0else:diffs np.diff(pts, axis0)seg_lens np.linalg.norm(diffs, axis1)dist float(np.sum(seg_lens))# 拐点: 相邻两段方向夹角 15度corners self._count_corners(diffs, angle_thr_deg15.0)feed g[feed_speed].iloc[0]rows.append({robot_id: robot,segment_id: seg,point_count: len(g),distance_mm: round(dist, 2),feed_speed: feed,corner_count: corners,corner_density: round(corners / max(1, dist/100), 2),})return pd.DataFrame(rows).sort_values([robot_id, segment_id]).reset_index(dropTrue)def _count_corners(self, diffs: np.ndarray, angle_thr_deg: float) - int:统计方向变化超过阈值的拐点if len(diffs) 2:return 0v1 diffs[:-1]v2 diffs[1:]norms1 np.linalg.norm(v1, axis1)norms2 np.linalg.norm(v2, axis1)mask (norms1 1e-6) (norms2 1e-6)if not mask.any():return 0cosang np.sum(v1 * v2, axis1) / (norms1 * norms2 1e-9)cosang np.clip(cosang, -1.0, 1.0)angles np.degrees(np.arccos(cosang))return int(np.sum((angles angle_thr_deg) mask))/detailsdetailssummary/summary轨迹耗时估算 (含加减速补偿)import numpy as npimport pandas as pdfrom typing import Optionalclass TimeEstimator:耗时模型:t_linear distance_mm / (feed_speed * 1000) 秒t_corner corner_count * corner_penalty 秒t_total t_linear t_cornerdef __init__(self, corner_penalty: float 0.08,accel_comp_factor: float 1.05):corner_penalty: 每个拐点加减速补偿时间(秒)accel_comp_factor: 直线段加减速整体补偿系数self.corner_penalty corner_penaltyself.accel_comp_factor accel_comp_factordef estimate(self, metrics_df: pd.DataFrame) - pd.DataFrame:基于段指标估算耗时out metrics_df.copy()# 直线耗时(秒): mm / (mm/s)out[t_linear_s] (out[distance_mm] / out[feed_speed] * self.accel_comp_factor).round(4)out[t_corner_s] (out[corner_count] * self.corner_penalty).round(4)out[t_total_s] (out[t_linear_s] out[t_corner_s]).round(4)out[time_per_mm_ms] (out[t_total_s] * 1000 / out[distance_mm].replace(0, np.nan)).round(2)out[time_per_mm_ms] out[time_per_mm_ms].fillna(0)return outdef robot_total(self, est_df: pd.DataFrame) - pd.DataFrame:按机器人汇总rows []for robot, g in est_df.groupby(robot_id):rows.append({robot_id: robot,total_distance_mm: round(g[distance_mm].sum(), 2),total_time_s: round(g[t_total_s].sum(), 3),segment_count: len(g),total_corners: int(g[corner_count].sum()),avg_feed: round(g[feed_speed].mean(), 1),})return pd.DataFrame(rows).sort_values(total_time_s, ascendingFalse).reset_index(dropTrue)/detailsdetailssummary/summary轨迹统计检验 (scipy)import numpy as npimport pandas as pdfrom scipy import statsfrom typing import Dict, Optionalclass TrajStatistics:段距离分布 多机对比def __init__(self, alpha: float 0.05):self.alpha alphadef distance_normality(self, est_df: pd.DataFrame) - Dict:段距离正态性检验d est_df[distance_mm].valuesif len(d) 3:return {shapiro_stat: np.nan, shapiro_p: np.nan}if len(d) 5000:d d[:5000]stat, p stats.shapiro(d)return {shapiro_stat: round(float(stat), 4),shapiro_p: round(float(p), 4),is_normal: bool(p self.alpha),mean_mm: round(float(np.mean(d)), 2),std_mm: round(float(np.std(d, ddof1)), 2),}def robot_distance_anova(self, est_df: pd.DataFrame) - Dict:多机器人总距离 ANOVAgroups []for _, g in est_df.groupby(robot_id):if len(g) 1:groups.append(g[distance_mm].values)if len(groups) 2:return {f_statistic: np.nan, p_value: np.nan,significant: False}f_stat, p_val stats.f_oneway(*groups)return {f_statistic: round(float(f_stat), 4),p_value: round(float(p_val), 4),significant: bool(p_val self.alpha),}def time_efficiency_test(self, est_df: pd.DataFrame) - pd.DataFrame:各段单位距离耗时统计rows []for _, r in est_df.iterrows():rows.append({segment_id: r[segment_id],distance_mm: r[distance_mm],t_total_s: r[t_total_s],time_per_mm_ms: r[time_per_mm_ms],})return pd.DataFrame(rows).sort_values(time_per_mm_ms, ascendingFalse).reset_index(dropTrue)/detailsdetailssummary/summary轨迹段画像聚类 (scikit-learn)import numpy as npimport pandas as pdfrom sklearn.cluster import KMeansfrom sklearn.preprocessing import StandardScalerfrom typing import Optionalclass SegmentProfiler:基于距离/耗时/拐点密度的轨迹段聚类def __init__(self, n_clusters: int 3, random_state: int 42):self.n_clusters n_clustersself.random_state random_statedef profile(self, est_df: pd.DataFrame) - pd.DataFrame:df est_df.copy()feat_cols [distance_mm, t_total_s, corner_count, time_per_mm_ms]for c in feat_cols:if c not in df.columns:df[c] 0.0X df[feat_cols].fillna(0).valuesscaler StandardScaler()Xs scaler.fit_transform(X)km KMeans(n_clustersself.n_clusters, random_stateself.random_state)df[cluster] km.fit_predict(Xs)centers scaler.inverse_transform(km.cluster_centers_)label_map {}for i, c in enumerate(centers):dist, t_total, corners, tpm cif dist 500 and corners 3:label_map[i] 长直段elif tpm 1.5:label_map[i] 微动调整段else:label_map[i] 密集折线段df[segment_pattern] df[cluster].map(label_map)return dfdef cluster_summary(self, df: pd.DataFrame) - pd.DataFrame:if segment_pattern not in df.columns:return pd.DataFrame()rows []for pat, g in df.groupby(segment_pattern):rows.append({pattern: pat,count: len(g),avg_distance_mm: round(g[distance_mm].mean(), 1),avg_time_s: round(g[t_total_s].mean(), 3),avg_time_per_mm_ms: round(g[time_per_mm_ms].mean(), 2),segments: , .join(g[segment_id].tolist()[:8]),})return pd.DataFrame(rows).sort_values(avg_distance_mm, ascendingFalse).reset_index(dropTrue)/detailsdetailssummary/summary段-点关系网 (networkx)import networkx as nximport pandas as pdimport numpy as npfrom typing import Optionalclass TrajGraph:建机器人-段-点顺序关系网def __init__(self):self.G nx.DiGraph()def build(self, df: pd.DataFrame, est_df: Optional[pd.DataFrame] None) - nx.DiGraph:self.G.clear()for (robot, seg), g in df.groupby([robot_id, segment_id]):rnode fR:{robot}snode fS:{robot}_{seg}self.G.add_node(rnode, ntyperobot)self.G.add_node(snode, ntypesegment)self.G.add_edge(rnode, snode, weight1)pts g.sort_values(point_seq)prev snodefor _, r in pts.iterrows():pnode fP:{robot}_{seg}_{r[point_seq]}self.G.add_node(pnode, ntypepoint,pos(r[x], r[y], r[z]))self.G.add_edge(prev, pnode, weight1)prev pnode# 段距离标注if est_df is not None:sub est_df[(est_df[robot_id]robot) (est_df[segment_id]seg)]if not sub.empty:self.G.nodes[snode][distance_mm] sub[distance_mm].iloc[0]self.G.nodes[snode][time_s] sub[t_total_s].iloc[0]return self.Gdef heavy_segments(self) - pd.DataFrame:按距离排序的重载段rows []for n, d in self.G.nodes(dataTrue):if d.get(ntype) segment and distance_mm in d:rows.append({segment: n.split(:)[-1],distance_mm: d[distance_mm],time_s: d.get(time_s, 0),})df pd.DataFrame(rows)if df.empty:return dfreturn df.sort_values(distance_mm, ascendingFalse).reset_index(dropTrue)/detailsdetailssummary/summary可视化 (matplotlib)import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom pathlib import Pathplt.rcParams[font.sans-serif] [SimHei, DejaVu Sans]plt.rcParams[axes.unicode_minus] Falseclass TrajVisualizer:def __init__(self, results_dir: str results):self.results_dir Path(results_dir)self.results_dir.mkdir(exist_okTrue)def distance_bar(self, est_df):各段距离条形图fig, ax plt.subplots(figsize(12, 6))labels est_df[segment_id].astype(str)colors plt.cm.viridis(est_df[distance_mm] / max(est_df[distance_mm]1e-6))ax.bar(range(len(est_df)), est_df[distance_mm], colorcolors, edgecolorwhite)ax.set_xticks(range(len(est_df)))ax.set_xticklabels(labels, rotation45, fontsize8)ax.set_ylabel(距离 (mm))ax.set_title(各轨迹段运动距离, fontsize14, fontweightbold)ax.grid(axisy, alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / segment_distance_bar.png,dpi150, bbox_inchestight)plt.close()def time_pie(self, est_df):各段耗时占比饼图fig, ax plt.subplots(figsize(9, 9))ax.pie(est_df[t_total_s], labelsest_df[segment_id].astype(str),autopct%1.1f%%, startangle90,colorsplt.cm.Set3(np.linspace(0,1,len(est_df))),wedgeprops{edgecolor:white})ax.set_title(各轨迹段耗时占比, fontsize14, fontweightbold)plt.tight_layout()plt.savefig(self.results_dir / segment_time_pie.png,dpi150, bbox_inchestight)plt.close()def xy_scatter(self, df, est_df):XY平面轨迹散点(颜色表段)fig, ax plt.subplots(figsize(11, 9))segs df[segment_id].unique()cmap plt.cm.tab10(np.linspace(0,1,len(segs)))for i, s in enumerate(segs):sub df[df[segment_id]s].sort_values(point_seq)ax.plot(sub[x], sub[y], -o, colorcmap[i%len(cmap)],markersize3, linewidth1.2, labelf段{s}, alpha0.85)ax.set_xlabel(X (mm))ax.set_ylabel(Y (mm))ax.set_title(机器人轨迹XY投影(按段着色), fontsize14, fontweightbold)ax.set_aspect(equal, adjustabledatalim)ax.legend(fontsize7, ncol2)ax.grid(alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / traj_xy_scatter.png,dpi150, bbox_inchestight)plt.close()def cluster_scatter(self, df):聚类散点: 距离 vs 单位距离耗时if segment_pattern not in df.columns:returnfig, ax plt.subplotsfigsize(10, 7))patterns df[segment_pattern].unique()cmap plt.cm.Set1(np.linspace(0,1,len(patterns)))for i, pat in enumerate(patterns):sub df[df[segment_pattern]pat]ax.scatter(sub[distance_mm], sub[time_per_mm_ms],c[cmap[i]], labelpat, s60, alpha0.8,edgecolorsblack, linewidths0.5)for _, r in sub.iterrows():ax.annotate(r[segment_id], (r[distance_mm]利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛