简介这份资源面向机器人知识表示与语义推理方向的学习者和研究者围绕 KnowRob 知识库提供可直接复用的数据仓库用于解决样本数据、测试数据与评估数据分散、难以跨项目复用的问题。数据与主程序代码分离存放便于在不同实验上下文中重复调用适合具备一定本体建模基础、正在搭建语义地图或任务规划实验的中高级读者。压缩包共 406 个文件约 52.4MB以 353 张 jpg 图像和 35 个 owl 本体文件为主另含 md 说明文档、xml 配置、sh 脚本与 Dockerfile可用于环境搭建与结果校对。owl 文件按动作、日志、地图、对象四类组织动作配方描述任务步骤日志记录机器人或人类执行过程语义地图表达环境结构对象模型则说明语义属性、部件组成或 CAD 模型链接。目前已有 208 人学习下载可帮助读者快速获得可运行的实验数据基础对照本体与图像理解语义标注流程减少自建数据集的时间成本。1. 从一次抓取失败的回放说起knowrob_data 到底是什么仿真里机械臂抓同一只杯子十次有三次抓空日志里全是干净的位姿数字看不出哪一步把坐标系定义搞错了。这类问题靠翻 rosbag 很难定位缺的是一层语义把「杯子属于哪一类」「桌面挂在哪个坐标系下」「这个模型文件对应哪个实例」显式写死。knowrob_data 干的就是这件事它把地图、机器人运行日志、对象模型整理成 Web Ontology Language 描述的 OWL 文件配合 KnowRob 的推理侧让这些关系变成可查询的三元组而不是散落在文件名和注释里的约定。它适合已经在跑 ROS、又想往任务级推理或语义地图方向走的团队也适合做机器人知识表示课程设计的人直接拿来当数据底座。如果只是写纯控制算法、不涉及常识推理和物体类别查询引入这套会明显偏重加载和推理的开销不值当。2. 拆开 OWL 目录TBox 与 ABox 在这份数据里怎么分工拿到仓库的第一反应通常是「一堆 .owl 文件从哪个开始加载」。真正决定成败的不是文件多少而是有没有分清 TBox 和 ABox 的边界。这两类内容混在同一个owl/目录里文件名看不出区别但加载顺序一旦反了后面所有查询都会静默返回空结果连一条报错都不给这是这套数据最容易让人翻车的地方。2.1 TBox 与 ABox 的边界为什么加载顺序不能反TBox 是术语层定义类、属性、类之间的rdfs:subClassOf关系以及属性的 domain 和 range。比如「杯子是容器的一种」「hasPath的值域是字符串」。ABox 是断言层描述具体个体这张地图叫map_lab、这个杯子模型叫cup_model_01、这条日志片段产生了 300 个位姿采样点。二者在本体工程里分得很清楚但落到仓库里往往就是几个命名风格相似的文件。反序加载的后果值得单独说。RDF 本身不做模式校验你把 ABox 先灌进去rdf:type指向的那个类 IRI 还没被声明三元组照样进库rdf_has/3也能查出来看起来一切正常。可一旦用owl_individual_of/2去推导「这个个体是不是容器」推理器需要沿rdfs:subClassOf链向上找链是断的结果就是空。更麻烦的是它不报错你会以为是数据缺了回头去查 rosbag白白耗掉半天。我一般这样判断先用rdf_has(I, rdf:type, C)把断言层捞出来确认个体在再用owl_individual_of(I, knowrob:Container)试父类如果前者有、后者空八成就是 TBox 没加载或者加载顺序错了。这两条查询几乎是排查所有「查不到」问题的起手式。2.2 目录与命名空间先拉一张 imports 依赖图不同分支的文件名不完全一致别照着别人的教程写死路径。进仓库先做两件事看目录骨架抓owl:imports的依赖关系。位置典型内容加载次序注意点owl/顶层本体核心类与属性定义属于 TBox1体量最大必须开缓存owl/objects*.owl对象模型的类与个体ABox 为主2依赖顶层本体的类声明maps/地图几何、栅格或点云附带的本体描述3常引用外部 mesh 路径logs/机器人运行日志导出的实例数据4三元组数量最容易失控models/网格、CAD 等二进制资源按需本体里只存路径引用不存内容依赖关系用一条命令就能抓出来别靠肉眼翻# 在仓库根目录执行抓出所有 owl:imports 指向的目标 grep -rn owl:imports --include*.owl . \ | sed s/.*rdf:resource//; s/.*// \ | sort -ugrep -rn递归扫所有.owl--include限定后缀避免扫到二进制模型文件。中间的sed干两件事删掉rdf:resource之前的所有前缀再删掉结尾的引号和残留只留 IRI。sort -u去重因为同一个顶层本体常被十几个文件重复引用。输出的这些 IRI 就是必须优先加载的集合按它们的层级排个序加载脚本的顺序基本就定了。命名空间这块有个习惯值得养成自己的日志实例数据单独开一个命名空间比如http://example.org/log#不要图省事往本体的命名空间里塞。原因很实际将来换一版本体你要重新对齐的是类名如果个体 IRI 也混在里面重命名会牵一发动全身。见过有人把几万个位姿采样点全写进knowrob.owl#下面后来本体升级命名空间一换整批数据成了孤儿。2.3 用 Protégé 与 rdf_has/3 双向确认类目结构图形化工具和命令行查询要交叉验证。用 Protégé 打开顶层本体看类树长什么样、有没有标成owl:equivalentClass的等价定义、属性有没有声明成owl:ObjectProperty还是owl:DatatypeProperty。这一步的意义在于建立预期你心里得先知道「容器下面应该有几个子类」。然后用 Prolog 侧把实际加载后的三元组捞一遍两边对不上就说明owl:imports没展开或者缓存是上一次的旧版本。这个对照做一次后面遇到怪结果时判断会快很多。还有个细节属性到底声明成对象属性还是数据类型属性直接影响字面量的写法。声明成owl:DatatypeProperty的属性值必须写成带类型的字面量literal(type(xsd:double, 0.35))写成裸字符串在 Prolog 侧能查但做数值比较和区间过滤时会全部落空。这个坑后面第 5 章还会展开。3. 接进推理机register_ros_package 与 load_owl 的完整链路数据在磁盘上不等于数据在推理机里。中间这段链路有三个环节容易断ROS 找不到包、package://IRI 解析不出来、加载没有缓存导致每次重启都要重跑几分钟。按顺序把它们打通后面写查询才有意义。3.1 环境准备工作空间、依赖与 package:// 解析KnowRob 用package://这种伪协议来引用本体文件解析靠的是 ROS 的包查找机制本质上就是ROS_PACKAGE_PATH。所以「文件明明在加载却报找不到」这类问题九成出在这一层跟 OWL 语法一点关系都没有。# 1. 放进工作空间的 src目录名建议与 package.xml 里的 name 保持一致 cd ~/catkin_ws/src git clone 仓库地址 knowrob_data # 2. 安装声明的依赖owl 解析、prolog 绑定一般都在 rosdep 里 cd ~/catkin_ws rosdep install --from-paths src --ignore-src -r -y # 3. 编译并 source不 source 等于没配 catkin_make source devel/setup.bash # 4. 确认 ROS 能定位到这个包 rospack find knowrob_data第 1 步的目录名不是随便起的。ROS 1 的包查找对目录名和name一致性有要求目录叫knowrob-data、name写knowrob_data就会出现rospack find能找到但package://knowrob_data/...解析失败的分裂状态很难查。第 4 步是硬性验收rospack find必须返回一个绝对路径。返回空说明devel/setup.bash没 source 或者 source 的是另一个工作空间返回了路径但加载还是失败那就用rospack find knowrob_data的输出拼出完整文件路径ls一下确认目标.owl真的在那个位置不要相信记忆。3.2 加载顺序先 TBox 后 ABox缓存开关怎么设进到 SWI-Prolog 侧先把包注册进来再按 TBox 到 ABox 的顺序加载。这里的cache(true)是个关键参数它把解析结果落到磁盘缓存第二次启动直接读缓存。不加这个参数顶层本体那种体量的文件每次重启都要重新 parse等你等到怀疑人生。% 先注册依赖包和数据包让 Prolog 侧知道去哪里找文件 ?- register_ros_package(knowrob). ?- register_ros_package(knowrob_data). % 1) 加载 TBox类、属性、类层次 ?- load_owl(package://knowrob_data/owl/knowrob.owl, [cache(true)]). % 2) 加载 ABox对象模型与具体个体 ?- load_owl(package://knowrob_data/owl/objects.owl, [cache(true)]). % 3) 日志导出的实例数据放最后 ?- load_owl(file:///home/robot/logs/run.ttl, [format(turtle)]).load_owl/2的第一个参数是 IRI可以是package://、file://也可以用带http://的远程地址但离线环境下别这么干。第二个参数是选项列表cache(true)开磁盘缓存适合那些内容稳定的顶层本体format(turtle)显式指定序列化格式.owl后缀在 RDF/XML 之外还可能是 Turtle 或 OWL/XML靠后缀猜会出事。关于顺序还有一点要补。顶层本体内部常有多层owl:imports正常情况加载器会递归展开但递归深度和环引用处理在不同版本上表现不一样。稳妥做法是先跑一次不带 cache 的加载观察日志里有没有反复 parse 同一个文件如果出现说明存在 import 环那就先手工把依赖扁平化合并成一个文件再加载别硬扛。3.3 验证加载三条查询确认三元组真的进去了加载过程不报错不等于数据进去了。抓三条查询做体检断言层有没有、类层次有没有、数值类型对不对。% 1) 实例计数某个类下面到底挂了多少个体 ?- aggregate_all(count, rdf_has(I, rdf:type, knowrob:Cup), N). % 2) 类层次确认 subClassOf 链是通的 ?- rdf_has(knowrob:Cup, rdfs:subClassOf, Super). % 3) 字面量类型看路径字段是不是带了 xsd 类型 ?- rdf_has(M, knowrob:path, literal(type(xsd:string, P))).第一条用aggregate_all/3做计数count是聚合方式中间是目标最后统一到N。返回 0 而你又确定数据里有杯子就往 ABox 是不是没加载这个方向查。第二条验证 TBoxrdf_has/3第三个参数用变量Super会把所有直接父类列出来如果只吐出一个owl:Thing说明真正的类定义没进来你加载的很可能是精简版或者加载失败了。第三条是给后面数值查询铺路的literal(type(xsd:string, P))这种写法会强制要求字面量带类型标注裸字符串在这里匹配不上。这三条查完心里就有底了。我一般会把它们存成一个小.pl文件放在prolog/目录下每次换数据版本先跑一遍比凭感觉判断可靠得多。4. 把 rosbag 灌成三元组位姿、地图与对象模型的落地转换仓库里预置的数据是静态的真正的价值在于把你自己机器人的运行日志接进去。这一步的难点不在代码量而在取舍一份 50Hz 录制半小时的 rosbag位姿消息上百万条全量转三元组会让内存和查询速度双双失控。得先想清楚查询要回答什么问题再决定采样率和保留字段。4.1 采什么位姿、时间戳与对象标注的取舍先明确查询目标。如果是「机器人在某个时刻附近看到了哪些物体」那时间戳精度到 0.1 秒足够位姿只留位置和四元数就能支撑距离计算如果是「轨迹在哪一段进入了某个地图区域」位置信息要全姿态反而可以丢。目标不同字段集差得很远。降采样是必须做的。常见做法是按固定时间间隔抽帧间隔取 0.2 到 1.0 秒取决于运动速度慢速移动平台取 0.5 秒快速移动的取 0.1 秒。抽帧之后一条轨迹通常降到几千个采样点查询响应从分钟级掉到秒级这是血泪经验换来的数字。对象标注这块要保持克制。rosbag 里的检测结果往往是「第 N 帧出现了一个 cup置信度 0.87」这种带时间戳的瞬时观测直接建成独立个体并且每条都挂完整属性会让三元组数量翻几倍。我的做法是只保留置信度高于阈值的观测并且把连续的同类观测合并成一段区间用起点和终点时间戳表示实例数量能压下去一个数量级。4.2 转换脚本rosbag 到 Turtle 的分片写法脚本的逻辑很直白读消息、抽帧、构三元组、序列化。麻烦的是类型标注和命名空间隔离这两处写错后面的查询会莫名其妙地空。import rosbag from rdflib import Graph, Namespace, URIRef, Literal, RDF, XSD # 本体命名空间只用来挂类名 KN Namespace(http://knowrob.org/kb/knowrob.owl#) # 日志实例单独开命名空间方便整批清理 LOG Namespace(http://example.org/log#) STEP 0.5 # 抽帧间隔秒50Hz 原始数据全转会有上百万条三元组 TOPICS [/tf, /object_pose] g Graph() with rosbag.Bag(robot_run.bag, r) as bag: last_t None for topic, msg, t in bag.read_messages(topicsTOPICS): ts t.to_sec() # 按时间间隔抽帧第一条永远保留 if last_t is not None and ts - last_t STEP: continue last_t ts node URIRef(LOG[fpose_{int(ts * 1000)}]) g.add((node, RDF.type, KN.PoseSample)) # 时间戳一定要带 xsd:double否则区间查询没法比大小 g.add((node, KN.timestamp, Literal(ts, datatypeXSD.double))) g.add((node, KN.x, Literal(float(msg.pose.position.x), datatypeXSD.double))) g.add((node, KN.y, Literal(float(msg.pose.position.y), datatypeXSD.double))) g.add((node, KN.z, Literal(float(msg.pose.position.z), datatypeXSD.double))) # 分片输出单文件超过几十万三元组就换下一个文件 g.serialize(run.ttl, formatturtle) print(ftriples: {len(g)})STEP是抽帧间隔这是整个脚本里唯一需要根据场景调的参数前面的取值建议直接拿来用。TOPICS限定只读需要的消息类型read_messages支持列表别图省事读全量/tf之外的传感器原始数据读进来也用不上白白占内存。URIRef(LOG[fpose_{int(ts * 1000)}])这行决定了个体的 IRI 命名。用毫秒时间戳拼后缀保证唯一且天然有序后面按 IRI 排序就能得到时序。有人用自增序号跨文件合并时必冲突不建议。Literal(..., datatypeXSD.double)是第二处关键。Python 的float直接传给 rdflib 会被序列化成裸数字看着像数字但在部分推理器里被当成字符串处理做比较时静默失败。显式声明XSD.double之后SPARQL 里可以直接写数值过滤条件。输出用 Turtle 而不是 RDF/XML。同样的数据量Turtle 体积小一半人工 diff 也方便而且load_owl显式指定format(turtle)就能吃进去。4.3 回合本体owl_parse 后的实例挂载与命名规范文件生成完先做语法体检再入库别把解析错误带进推理机。rapper这个工具用来做 Turtle 的语法校验很顺手# -i 指定输入格式-c 只做校验不输出 rapper -i turtle -c run.ttl校验通过后加载% 显式声明格式避免按后缀猜错 ?- load_owl(file:///home/robot/logs/run.ttl, [format(turtle)]). % 加载完立刻验证这批采样点挂到哪个类下面了 ?- rdf_has(P, rdf:type, knowrob:PoseSample).入库之后还有一步容易漏把这些采样点和已有的地图或对象模型连起来。孤立的位姿个体查询价值很低常见的做法是加一条关联三元组把这批日志挂到某次运行记录上运行记录再挂到地图上% 建立日志片段到地图实例的关联 ?- rdf_assert(LOG:run_2024_01, knowrob:hasMap, knowrob:map_lab). % 把采样点归到这次运行下 ?- rdf_assert(LOG:pose_1712000000000, knowrob:partOf, LOG:run_2024_01).rdf_assert/3是运行时插入只存在内存里重启就没了。需要持久化就写回 Turtle 文件再重新加载或者在你的转换脚本里直接把关联三元组一并生成。我倾向前者运行时插入适合做临时验证正式数据还是走文件版本管理清晰。5. 避坑与排查五类让你半夜加班的加载失败下面这五条都是实际撞过的按出现频率排。每条都按「现象 → 原因 → 解决」写方便对照排查。看到类似现象不要急着改代码先确认自己遇到的确实是同一类问题。5.1 加载期的两个崩点现象rospack find能返回路径但load_owl(package://knowrob_data/...)报找不到文件换成绝对路径file://立刻就通了。原因package://的解析走的是 Prolog 侧的包注册表不是 shell 里的ROS_PACKAGE_PATH。工作空间 source 了但 Prolog 进程是在 source 之前启动的环境变量没继承进来两套查找机制就对不上了。解决在 source 过工作空间的同一个终端里启动 Prolog或者显式调register_ros_package(knowrob_data)注册一次。长期方案是把注册写进启动脚本别依赖手工。现象加载顶层本体时内存一路涨到几个 G最后进程被杀日志里能看到同一个文件被反复 parse。原因本体之间存在owl:imports环A 引 B、B 又引回 A。加载器递归展开时没有做已访问去重或者去重只在单次调用内有效跨文件就失效了。解决用第 2 章那条grep命令把依赖图拉出来找出环然后在本地把依赖扁平化合并成一个不带owl:imports的单体文件再加载。别指望在加载器层面绕过去扁平化一次省事很久。5.2 查询期的两个空结果现象rdf_has(I, rdf:type, knowrob:Cup)能查到一堆个体但owl_individual_of(I, knowrob:Container)返回 false。原因只有直接断言没有rdfs:subClassOf的传递闭包。推理器没有把「杯子是容器的子类」这条关系展开父类查询自然落空。有些配置下需要显式物化闭包有些是推理机根本没启动。解决先确认类层次查询有结果第 3 章第二条体检查询有层次但推导不出来就把闭包物化一遍或者换用支持 OWL RL 的推理模式重新加载。这一步不做后面所有涉及父类的查询都不可靠。现象SPARQL 里写FILTER(?x 3.5)过滤位姿坐标结果永远为空去掉过滤条件数据全在。原因字面量没带xsd:double类型标注被当成字符串处理字符串比较下10.0 3.5是 false跟数值大小完全无关。Python 侧float直接传给 rdflib 就会这样。解决转换脚本里所有数值字段显式加datatypeXSD.double。已经入库的数据只能重新生成改查询没用因为信息在写入时就丢了。5.3 数据期的单位与命名现象同一个对象模型在两个文件里都有定义加载后查询返回两套属性数值还互相打架。原因新旧版本共存。仓库更新时旧文件没删或者缓存目录里留着上一次的解析结果cache(true)把旧数据又读回来了。解决清掉缓存目录重跑一次对比加载前后的三元组总数。数字对不上就说明有重复源。长期做法是在加载脚本里对同名个体做一次去重检查用rdf_has统计同一 IRI 的rdf:type数量大于 1 就报警。顺带说一个单位问题。本体里存长度时如果不写单位默认按米处理但有些 CAD 导出的模型是毫米。转换时手工除以 1000 是最容易忘的一步忘了之后表现是「模型尺寸大了一千倍」在可视化里一眼能看出来在纯查询里看不出来。我现在的做法是在属性名里带单位后缀从命名上杜绝歧义。6. 一致性校验用 SPARQL 给数据做一次体检数据能查出来不代表数据是对的。加载完、转换完最后一步应该是主动找错而不是等查询出怪结果再回头翻。SPARQL 适合干这个因为它可以表达「不存在」这类否定条件正好覆盖最常见的几类脏数据。三类校验值得固化成脚本。第一类是个体只挂在owl:Thing下说明rdf:type写错了或者类 IRI 拼错了这种数据在按类查询时永远查不到。第二类是字面量类型缺失前面提过会造成数值过滤静默失效。第三类是引用了不存在的目标比如日志里引用了某个地图实例但那张地图根本没加载。from rdflib import Graph g Graph() g.parse(run.ttl, formatturtle) # 校验 1只声明成 owl:Thing 的个体通常是类型写错了 q1 PREFIX owl: http://www.w3.org/2002/07/owl# SELECT ?s WHERE { ?s a owl:Thing . FILTER NOT EXISTS { ?s a ?c . FILTER (?c ! owl:Thing) } } # 校验 2数值属性上缺 xsd:double 标注的字面量 q2 PREFIX xsd: http://www.w3.org/2001/XMLSchema# SELECT ?s ?p ?o WHERE { ?s ?p ?o . FILTER (isLiteral(?o) datatype(?o) xsd:string regex(str(?p), x$|y$|z$|timestamp$)) } for name, q in ((only-thing, q1), (untyped-number, q2)): rows list(g.query(q)) print(name, len(rows)) for r in rows[:5]: print( , r)q1用FILTER NOT EXISTS做否定匹配找出除owl:Thing外没有任何类型的个体。这个查询在几千个实例上跑不到一秒但能一次抓出所有类型拼写错误。q2结合isLiteral和datatype判断再用regex按属性名后缀筛出应该带数值类型的字段——x$|y$|z$|timestamp$这个正则按自己的命名习惯改属性名不一样就换成对应的。校验结果的判定标准要提前定好否则看到一堆输出不知道算不算问题。我的经验阈值是only-thing的数量应该接近 0超过总实例数的 1% 就说明转换脚本的类映射有问题untyped-number必须是 0出现任何一条都要回头改脚本重新生成因为这类数据一旦入库就没法补救。再补一个跨文件引用的检查用 bash 比 SPARQL 更直接# 抓出本体里所有 package:// 引用逐个确认文件真实存在 grep -rho package://[^\]* --include*.owl --include*.ttl . \ | sort -u \ | while read -r iri; do p$(echo $iri | sed s#package://#src/#) [ -f $p ] || echo MISSING: $iri donegrep -rho的-o只输出匹配片段-h去掉文件名前缀配合sort -u拿到去重后的引用集合。循环里把package://换成工作空间里的相对路径-f判断文件是否存在不存在就打一行MISSING。这条命令救过我一次本体里引用了一个早已被删掉的模型文件加载不报错可视化时才暴露成空模型查了快两个小时。从那以后我每次换数据版本都把这三段校验串成一个脚本强制走一遍跑完再碰查询。多花的这两分钟比事后翻日志便宜太多。希望帮到你。本文还有配套的精品资源点击获取