多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Rhino Python实现字段批量查询与PointCloud彩色标注

Rhino Python实现字段批量查询与PointCloud彩色标注 搞过测绘数据进 Rhino 的人应该都经历过这种尴尬模型里放了几百上千个点位属性早就挂好了可当需要把某个字段——比如井底高程、管径、检测结果——从对象属性里挖出来再放到视口里跟周边模型对照着看时整个人就卡住了。前一阵处理一个排水管线普查的成果数据我又撞上这个需求。当时就在想与其一个个选中对象看属性面板不如用 Rhino 自带的 Python 环境写一个查询脚本选中对象、输入字段名批量把字段查出来然后用 cloud 标注——也就是生成一个 PointCloud 点云对象用点的颜色把字段值的变化直观标在模型里。这条路走通之后效率提升非常夸张。原本要花半小时甚至更久的属性筛查现在基本就是“选对象 输入字段名 回车”三件事几秒钟出图。这篇文章就把整个思路和脚本拆开讲清楚包括字段查询的几种藏身之处、PointCloud 标注的核心机制、完整可抄的代码以及我在实测阶段踩过的几个坑。适合正在用 Rhino 做管线普查、测量成果整理、构件属性可视化的人参考无论你只会一点 Python 还是完全新手照着做都能跑起来。1. 这个脚本到底在解决什么问题1.1 一个能把你逼疯的原始工作流Rhino 不是不能看对象属性。选中一个对象按 F3 打开属性面板里面有名称、图层、材质还有 UserText 这种自定义字段。但如果数据量变成几百个点这套操作就完全不是人干的点一下对象、记一个数值、再点下一个对象、再记……中途接个电话回来就忘了自己记录到第几个了。更要命的是属性面板只能让你“看到”数值没法让你“看到”空间分布。同样是 500 个检查井的井底高程你就算手工把数值全记下来也很难直观分辨出哪一段管道沉降最大、哪个区域的标高异常。真正需要的不是一张列表而是把字段值映射回三维空间里的每一个点上用颜色或者文字直接标注在模型里。这正是“查询指定字段 cloud 标注”组合拳的价值。1.2 为什么选 Python 而不是手工、Grasshopper 或 C#手工路子已经否掉了那为什么不用 Grasshopper我试过用 Grasshopper 来做曲面、点的数据流确实方便但它更适合“参数化预览”要生成一个独立存在于模型里的 PointCloud 对象还要批量写入颜色、控制缺失值、输出统计信息反而绕。Grasshopper 的电池组件里也能写 Python 脚本但每次修改都要回编辑器重跑不太适合做成一个反复使用的小工具。C# 插件当然也行性能好、可打包但开发门槛高还得维护插件加载环境。对于我这种“项目急、脚本要快”的场景Rhino 内置的 Python 环境是性价比最高的选择不用装任何第三方库不需要编译直接在 RhinoPython 编辑器里复制粘贴就能跑。而且它可以同时调用 rhinoscriptsyntax 这种高层封装和 Rhino.Geometry 这种底层几何 API查属性、取坐标、建点云都顺滑。脚本跑通了之后还能做成工具栏按钮一键复用完全够用。2. 字段查询你的数据到底藏在哪种地方2.1 UserTextRhino 里最常用的“附加字段”在 Rhino 里对象可以携带自定义信息最常见的通道就是 UserText。简单理解它就是一个挂在对象身上的键值对字典Key 是字段名Value 是字符串比如井底高程 5.320。Rhino 的属性面板里能看到这些字段很多模型在建模或导入数据时也会自动把外部数据库的字段带进来。Python 里读 UserText 有两种姿势。一种是用rs.GetUserText(obj_id, key)传对象 ID 和字段名直接返回对应的 Value另一种是不传 key一次性拿回整个键值表然后自己遍历。我实际用下来两种都要准备因为不同项目的数据来源不一样有些字段名带了不可见空格有些大小写不统一直接用 key 精确匹配容易扑空。2.2 块实例属性、图层名、对象名也能当字段除了 UserText还有几个不出名但很实用的“字段”来源。块实例本身可以带属性你在图块属性面板里加的字段读取的方式和普通对象一样都是 GetUserText但它额外有块名这个内置字段适合做按图块类型分类的标注。图层名也很好用很多人做管线普查时会把不同管径的线段分到不同图层直接把图层名作为字段读出来就能快速按管径类别上色。对象名称则是另一种典型用法如果数据是从 GIS 或 Excel 导入的原始 ID 往往被放进对象名称里比如C987、JX-023。查询字段时可以优先读名称不影响你另外挂 UserText 业务字段。我在这套脚本里把这几种来源都做了兼容底层逻辑就是“先按指定字段名查 UserText查不到再尝试名称、图层、块名”。2.3 查询模块的容错设计字段查询看着简单真正写起来最怕的是字段缺失。几百个对象里总有那么一两个没有挂上目标字段如果不做防御脚本跑到一半可能打印一串异常最终生成点云时尺寸对不上。我采用的策略是每查询一个对象都做独立 try/except查不到就返回None但对象本身仍然参与点云标注只是颜色走“分类映射”而不是“数值映射”。这样即使某几个点缺字段整个标注结果也不会崩你还能在视口里一眼看出哪些点是缺失值。这套容错思路也建议你保留下来做任何批量属性处理都会用到。3. PointCloud 标注的核心机制3.1 点云不是一堆点是“点 属性”的容器Cloud 标注的关键对象是 Rhino 的 PointCloud点云。很多人以为点云就是给一个坐标列表用rs.AddPoint批量加点那就错过了重点。PointCloud 远不止一堆点它可以给每一个点索引绑定颜色、法线还能附带隐藏状态和用户数据。这意味着你可以把“查询结果”直接写进点云对象里成为模型的一部分而不需要额外维护一张对应表。用点云做标注还有一个隐藏优势它的渲染性能极好。同样一千个点如果用一千个小球体或者 TextDot 堆在模型里Rhino 转动视角会明显掉帧但如果塞进一个 PointCloud显示上就是轻量级原生对象流畅度完全不在一个量级。这也是我坚持用 cloud 而不是单纯文字标签的原因。3.2 颜色映射把数字字段变成视觉信息数值字段怎么变成颜色我采用的是最常用的“蓝到红渐变”方案把字段值线性归一化到 0 到 1然后映射到 HSL 色相从蓝色240 度平滑过渡到红色0 度。你可以把整个过程理解成热力图最小值是蓝色中间值是紫色青色混合最大值是红色这样任何位置的异常高值或低值都能一眼抓出来。对于非数值字段比如井室类型、检测结论归一化就不适用了。我用了另一个技巧对字符串做确定性哈希得到一个稳定的色相值。同一个分类字符串永远映射到同一个颜色这样“破损”和“完好”类别的颜色在不同批次标注之间不会乱跳方便反复调用对比。3.3 点云位置与原始对象的锚点关系点云里每个点的坐标必须落在原始对象的“锚点”上否则标注就失去了空间意义。锚点的选取规则要看对象类型点对象直接用坐标图块实例取插入点曲线取起点曲面和多重曲面取包围盒中心文字标注取 TextDot 位置。这段逻辑在脚本里单独拆成了一个函数方便后续扩展别的对象类型。我强烈建议你把锚点规则写得跟你的数据习惯对齐。比如你习惯用检查井圆心对应一个点对象那就只用点对象如果你的数据是带高程属性的 Curve那就优先用曲线起点同时把曲线中点作为备用方案。4. 可直接抄的完整脚本4.1 脚本的整体结构整个脚本分三层先是对选中对象做字段查询再是提取每个对象的锚点坐标最后把这些坐标组装成 PointCloud 并写入颜色。我把三层各自封装成函数这样不只是一个单次脚本后续就能拆出模块复用。下面是我目前在用的版本基于 Rhino 6 以上环境Python 语法层面兼容 RhinoPython 的 IronPython。注意代码里TARGET_FIELD 高程可以改你项目的字段名也可以用运行时弹窗输入。# -*- coding: utf-8 -*- # 查询指定字段并生成 cloud点云标注 # 适用环境Rhino 6 / RhinoPython import rhinoscriptsyntax as rs import scriptcontext as sc import Rhino import System.Drawing import colorsys TARGET_FIELD 高程 def query_field(obj_id, field_name): 从对象上查询字段兼容 UserText / 名称 / 图层 / 块名 # 1. 直接按字段名取 UserText try: val rs.GetUserText(obj_id, field_name) if val is not None and str(val).strip() ! : return str(val).strip() except Exception: pass # 2. 大小写不敏感的兜底遍历 try: items rs.GetUserText(obj_id) if items: if isinstance(items, dict): for k, v in items.items(): if str(k).strip().lower() field_name.strip().lower(): return str(v) else: for item in items: item str(item) if in item: k, v item.split(, 1) if k.strip().lower() field_name.strip().lower(): return v.strip() except Exception: pass # 3. 特殊内置字段 if field_name.strip() 名称: return rs.ObjectName(obj_id) if field_name.strip() 图层: return rs.ObjectLayer(obj_id) if rs.IsBlockInstance(obj_id) and field_name.strip() 块名: return rs.BlockInstanceName(obj_id) return None def get_anchor_point(obj_id): 提取对象的锚点坐标 if rs.IsPoint(obj_id): return rs.PointCoordinates(obj_id) if rs.IsBlockInstance(obj_id): return rs.BlockInstanceInsertPoint(obj_id) if rs.IsCurve(obj_id): return rs.CurveStartPoint(obj_id) if rs.IsTextDot(obj_id): return rs.TextDotPoint(obj_id) if rs.IsSurface(obj_id) or rs.IsPolysurface(obj_id): bbox rs.BoundingBox(obj_id) if bbox: return rs.PointAdd(bbox[0], rs.PointScale(rs.PointSubtract(bbox[6], bbox[0]), 0.5)) return None def try_float(val): 字符串安全转浮点失败返回 None if val is None: return None try: return float(str(val).replace(,, ).strip()) except Exception: return None def value_to_color(n, vmin, vmax): 数值字段蓝到红渐变 if vmax vmin: t 0.5 else: t (n - vmin) / (vmax - vmin) t max(0.0, min(1.0, t)) h 240.0 * (1.0 - t) r, g, b colorsys.hsv_to_rgb(h / 360.0, 0.85, 1.0) return System.Drawing.Color.FromArgb(int(r * 255), int(g * 255), int(b * 255)) def category_color(label): 分类字段稳定哈希得到色相 total 0 for ch in str(label): total (total * 31 ord(ch)) 0xFFFF h total % 360 r, g, b colorsys.hsv_to_rgb(h / 360.0, 0.7, 1.0) return System.Drawing.Color.FromArgb(int(r * 255), int(g * 255), int(b * 255)) def main(): field_name rs.GetString(要查询的字段名, TARGET_FIELD) if not field_name: return filter_mask rs.filter.point | rs.filter.instance | rs.filter.curve obj_ids rs.GetObjects(选择要标注的对象, filter_mask) if not obj_ids: return records [] for obj_id in obj_ids: pt get_anchor_point(obj_id) val query_field(obj_id, field_name) if pt: records.append({obj_id: obj_id, pt: pt, val: val}) if not records: rs.MessageBox(没有提取到点位请确认至少选中了点、图块或曲线。, 0) return # 区分数值字段与分类字段 for r in records: r[num] try_float(r[val]) nums [r[num] for r in records if r[num] is not None] vmin, vmax (min(nums), max(nums)) if nums else (None, None) # 构建点云 pc Rhino.Geometry.PointCloud() for r in records: pt r[pt] idx pc.Append(pt.X, pt.Y, pt.Z) if r[num] is not None and vmin is not None: color value_to_color(r[num], vmin, vmax) else: color category_color(r[val] if r[val] is not None else 缺失) pc.SetColor(idx, color) pc_id sc.doc.Objects.AddPointCloud(pc) rs.Redraw() # 少量对象时顺手加 TextDot 文字标注 if len(records) 300: for r in records: label str(r[val]) if r[val] is not None else ? sc.doc.Objects.AddTextDot(label, r[pt]) rs.Redraw() print(字段 [%s] 查询完成共标注 %d 个对象 % (field_name, len(records))) if nums: print(值域范围: %s ~ %s % (vmin, vmax)) if __name__ __main__: main()4.2 运行方式和输出在哪里看运行方式非常简单打开 Rhino 菜单里的 RhinoPython 编辑器把上面代码整体粘进去点 Run 即可。脚本会先弹一个对话框让你确认字段名再提示选择对象确定后立刻生成一个名为 PointCloud 的对象并自动把视图刷新出来。代码末尾的print输出会显示在 RhinoPython 编辑器的“输出”窗口里包含标注数量和数值范围。如果你用的是命令行版 Python则输出在 Rhino 命令行。无论哪种方式最重要的产品都在模型视口里一个彩色点云 少量文字标签。此时选中这个点云对象按 F3 就能看到它就是一个标准 PointCloud可以导出、隔离、后续继续处理。4.3 这段代码里最值得留意的三个设计第一个是pc.Append(pt.X, pt.Y, pt.Z)配合pc.SetColor(idx, color)。为什么要用 Append 而不是一次性传点列表因为我们需要在每个点加入的同时拿到索引然后立刻设置颜色。Append 返回的整数索引就是颜色表的索引号序号一一对应不会出现点与颜色错位的问题。第二个是try_float。字段值从 UserText 读出来几乎一定是个字符串字符串里可能含有逗号分隔符、单位后缀、甚至空格。强制转数字前先做一个清洗把逗号删掉、去空格转不了的返回None。这样“5.320”能参与数值映射“DN300”这类文本会乖乖走分类颜色。第三个是category_color的确定性哈希。hash()函数在 Python 进程之间结果不稳定我刻意绕开了它改用字符码迭代累加的方式生成色相。同一个“破损”标记得到的永远是同一种颜色不会因为重启脚本就变色这在实际反复对比中是刚需。5. 实测阶段踩过的坑5.1 UserText 的返回值形态不同 Rhino 版本真的不一样我在脚本里写了isinstance(items, dict)来判断返回结构这不是多余的。实际调试时发现有些 Rhino 版本里rs.GetUserText(obj_id)会返回字典另一些版本返回的是一组keyvalue字符串。如果你只写一种解析方式换个环境脚本就可能半路失效。处理方式就是“两条腿走路”先精确按 key 取取不到再遍历。遍历遇到 dict 就按键值对走遇到字符串列表就按号切一刀。这个防御逻辑值得保留因为不同单位用的 Rhino 版本和数据结构差异很大我自己就遇到过同一套脚本在不同电脑上表现不一致的情况。5.2 点云颜色显示不出来先怀疑显示模式第一次跑通脚本时我兴致勃勃地点 Run结果新生成的点云在视口里一片灰白完全看不出颜色映射。我一度以为是SetColor没生效排查了半天才发现是显示模式的问题Rhino 的某些显示模式尤其是线框模式不会渲染点云的点颜色只有切到着色模式或者在显示面板里打开“点颜色”选项点云的颜色才会显示出来。所以如果你跑了脚本看到的是一片灰不要马上改代码先按两下 F6 切到着色模式或者右键视口标题切换显示模式试试。这个坑非常隐蔽原理上是显示引擎对 PointCloud 的渲染开关造成的跟数据本身没有关系。5.3 TextDot 一多视口立刻变卡我在脚本里加了“少于 300 个对象时顺便生成 TextDot 文字标注”的逻辑阈值不是随便拍的。实测中500 个 TextDot 同时可见时旋转视角已经能感到迟滞上千个的时候基本变成 PPT 放映放大缩小都难受。与其全量堆文字标签不如只留点云颜色需要看某个具体数值时再局部使用文字标注。如果你确实需要大批量文字标注建议换思路用点云颜色做全局分布认知然后通过查询面板或 CSV 导出做精确数值读取。视觉标注的核心价值是发现异常和分布不是替代数据库表格。5.4 字段值类型混乱会让颜色结果完全失去意义最坑的一次是查询“管径”字段结果里面既有DN300这种带符号的字符串又有600这种纯数字还有一两个单元格是300旧管这种备注。如果直接转 float后两种会报错如果全部分类处理纯数字的连续变化信息就被抹掉了。我的应对是分层处理能转成数字的按数值映射不能转的按分类映射。这样大多数点仍然保持热力梯度少数异常点退化为按类别显示。实践里这种“数值优先、分类兜底”的策略比强行统一格式省心得多。6. 几个实用的扩展方向6.1 把 cloud 标注扩展成真正的文字标签如果你需要打印图纸纯点云颜色很难在纸质图纸上表达数值。我后来做了一个分支版本给每个点生成一个 TextDot内容就是字段值同时把点云保留在模型里作为背景色。TextDot 在 Rhino 视口里始终面向屏幕不会因为视角旋转而歪倒配合点云使用效果很好。要注意 TextDot 的字体大小受缩放影响打印前建议用rs.ObjectName或者图层区分标注层关掉不需要的层再导出。实际操作中我会把点云放一层、TextDot 放另一层出图前开关切换比在代码里做精细排布省事得多。6.2 接外部 CSV 数据做真正的“查询”上面的脚本查的是 Rhino 对象自带字段但实际项目里很多字段存在 CSV 或 Excel 里对象上只有一个 ID 编号。这种场景我会再写一个文本匹配模块先用 Python 内置的 csv 把表格读成字典把 ID 字段作为 key再按对象名称或 UserText 里的 ID 去关联其他字段查出来后再走同样的 pointcloud 标注流程。这样做的好处是模型可以保持轻量不需要把所有业务字段灌进 Rhino坏处是匹配逻辑要处理 ID 前后空格、类型不一致的问题需要你在代码里做一次 strip 和字符串化。6.3 把这个脚本改成工具栏按钮脚本跑通之后每次打开编辑器粘贴运行还是有点繁琐。我在公司内部把它封装成了一个小按钮在 Rhino 工具栏上右键添加按钮命令内容填脚本路径点击就能直接唤起字段选择。这样不熟悉代码的同事也能用只要他愿意在对话框里输入正确字段名。封装时有个小建议把脚本里的TARGET_FIELD作为默认值同时允许运行时覆盖。不同项目字段名不一样固定死反而不好。按钮打开后第一件事就是确认字段不会因为记错默认值而查错。6.4 最后再分享一个实际操作中的小技巧遇到特别大的数据量比如上万个点位不要一次性把所有点 Append 进同一个 PointCloud我建议按分类或者按区域分批次生成多个点云。这样单个点云对象更可控而且可以在 Rhino 的图层面板里单独开关某类标注定位异常更方便。另外每次跑完脚本建议把点云放到一个独立图层命名带时间戳比如“cloud标注_20240613”。这样你迭代多轮查询后模型里不会堆满无法区分的老标注。我吃过一次亏连续跑了七八轮脚本视口里全是颜色点根本分不清哪一轮是哪一轮后来才养成图层隔离的习惯。脚本本身不难关键是你怎么在真实工作流中把它安排得井井有条。
返回列表