ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

txt转ASC全解析:点云坐标与高程栅格格式转换实战

txt转ASC全解析:点云坐标与高程栅格格式转换实战 简介针对Kvaser采集设备生成的txt文件与CANoe专用ASC格式不兼容的问题资源以Python脚本形式提供了一套轻量转换方案。脚本覆盖txt逐行解析、CAN报文时间/ID/数据抽取、ASC文件结构生成以及时间戳单位换算等关键环节适用于汽车电子、工业自动化等领域中需要将Kvaser数据导入CANoe进行分析调试的测试工程师与嵌入式开发者。资源本身为单个py文件整体压缩包仅2KB无额外依赖开箱即可查看逻辑或直接执行转换。目前已有965人学习下载说明该小工具在同类场景中具备一定参考价值。通过研读脚本读者可理解两种格式的字段映射规则掌握利用Python批量处理通用数据格式转换的思路也可在此基础上扩展为支持更丰富报文类型或批量化转换的工作脚本。1. 把txt变成ASCStart_Program解决的是格式转换路上的最后一公里做点云、GIS数据交接的工程师大概率遇到过这种尴尬手里明明是完整的txt测点文件每行都摆好了点号、X、Y、Z可下游CloudCompare和ArcGIS只认ASC。格式转换不只是改个后缀那么简单分隔符、文件头、坐标顺序、字符编码任何一个环节出问题数据一进软件就是废的。Start_Program这个名字本身就很直白——它是一个把txt转ASC这一整套流程参数化、自动化的落地工具先判断输入到底是点云坐标还是高程网格再按目标软件能识别的ASC规范写出文件。它适合测绘数据处理、三维扫描后处理、GIS插值这类场景也适合刚接触点云格式的学生快速把整条链路跑通。2. 两种ASC的真相点云坐标与高程网格先分清再转换先讲一句容易翻车的大实话ASC不是一个“单格式”它在不同软件里指代两种完全不同的文件结构。一种是以每一行描述一个空间点的“点云坐标型ASC”另一种是带文件头、按规则网格排列的“ESRI ASCII Grid”。下游软件决定你要用哪一种而不是你想转哪一种。搞混了数据文件后缀是.asc打开却全是乱码或者错位。2.1 点云坐标型ASC每一行是一次测量记录点云坐标型ASC的结构最简单本质就是“按空格分隔的多列数值”。常见写法是每行依次存放x、y、z后面可以追加强度值、RGB颜色或者回波次数列数不固定由软件约定。三维激光扫描仪、无人机摄影测量软件导出的点云在中间交换时经常用这种格式。举一个最典型的例子。设备导出的txt是逗号分隔的$ head -3 raw_points.txt 316907.2398,3437985.5573,45.2194,200 316907.3781,3437985.5721,45.2216,198 316907.5102,3437985.5886,45.2231,201而CloudCompare这类软件期望的ASC文件是空格分隔的$ head -3 output.asc 316907.2398 3437985.5573 45.2194 200 316907.3781 3437985.5721 45.2216 198 316907.5102 3437985.5886 45.2231 201从肉眼上看差别不大但软件解析逻辑是“按空格切分”。如果直接把逗号分隔的txt改名成asc丢进去第一行会被当成一整列解析后面全部错位。我见过同事把几百万行点云这样直接改名结果CloudCompare里点云变成了一条斜线就是因为第一列把整行内容都吃进去了。点云型ASC不存在文件头所以列顺序就是唯一约定。常见顺序是x y z intensity但有些软件默认是x y z intensity r g b。因此转换之前必须先确认目标软件期望几列、顺序是什么。通用做法是把源txt的列号映射到目标列上比如源文件的0、1、2、3列正好对应目标格式的x、y、z和强度值。2.2 ESRI ASCII Grid六个文件头字段定生死另一类ASC是GIS里高频使用的ESRI ASCII Grid一般叫GRID格式后缀固定为.asc它是ArcGIS、QGIS做高程分析和插值分析的输入格式。它和点云型ASC最大的区别是文件前六行必须写清楚栅格元信息然后才是按行排列的数值矩阵。一个合法的ESRI ASCII Grid长这样ncols 300 nrows 400 xllcorner 463500.000000 yllcorner 4461000.000000 cellsize 1.000000 NODATA_value -9999 12.3 12.5 12.8 13.1 ... 12.5 12.7 12.9 13.3 ... ...文件头六个字段各有用处。ncols和nrows决定了矩阵宽高后面的数据区必须严格是nrows行、每行ncols个数值xllcorner和yllcorner是栅格左下角的坐标不是栅格中心点坐标cellsize是单个格网边长单位必须和坐标系统一NODATA_value是无值区填充值通常用-9999。这里有个常见误区有些txt从C语言或者FORTRAN程序里导出来数据是按“从左上角开始逐行往右、再换下一行”的顺序排列的而ESRI ASCII Grid也是这个规则——理论上不需要翻转。但如果你手里的txt本身是从右下角开始排列的直接塞进ASC就会得到一张上下翻转的栅格图后面我会专门讲这个坑。2.3 Start_Program的模式判断先识别再转换既然两种ASC结构完全不一样转换工具就必须先判断输入txt属于哪种场景。Start_Program这类工具通用做法是加一个自动识别逻辑先读前几行如果发现ncols、nrows、cellsize这类关键字就按栅格模式处理如果全是数值且列数在3列以上就按点云模式处理如果首行是中文表头跳过表头后再判断。实际处理时我一般会先跑一个探测命令把文件前5行原样打出来看一遍再决定参数。不会完全依赖自动识别因为自动识别遇到表头有中文、注释符号多的情况也可能翻车。对比项点云坐标型ASCESRI ASCII Grid文件头没有直接是数据必须有ncols/nrows/xllcorner/yllcorner/cellsize/NODATA_value六行行语义一行一个点列数3~6不等一行是栅格的一整行列数固定为ncols坐标表达每行都带x、y、z坐标坐标由xllcorner/yllcorner和cellsize计算得出常用软件CloudCompare、MeshLab、PCLArcGIS、QGIS、Global Mapper常见来源LiDAR点云、摄影测量、三维扫描DEM生成、插值分析、气象站点插值这个决策表是转换脚本写if-else的基础。用栅格参数去解析点云数据或者反过来都会得到一堆“能读但完全不能用”的文件。Start_Program的入口参数里明确分出cloud和grid两种模式道理就在这里结构不对后面全是白干。3. 手动执行Start_Program从单文件到批量转换的关键参数3.1 摸清txt的“脾气”再动手拿到一份txt第一件事不是写转换语句而是把文件前几行打出来确认分隔符、列数、表头和编码。这一步直接决定后面所有参数怎么填。$ head -5 raw_points.txt # X, Y, Z, Intensity ← 表头转换时要跳过 316907.2398,3437985.5573,45.2194,200 316907.3781,3437985.5721,45.2216,198 316907.5102,3437985.5886,45.2231,201 $ file raw_points.txt raw_points.txt: ASCII text, with CRLF line terminators看head输出能确认三件事第一行是不是表头、分隔符是逗号还是Tab、一共几列。用file命令可以看出换行符是CRLF还是LFCRLF在Windows设备导出的文件里很常见Python写文件时如果没有指定换行符会出现输出文本里夹杂\r字符的情况。编码问题在第3行更容易暴露。国内很多测量设备导出txt默认是GBK或GB2312在macOS上直接用文本编辑器打开就是乱码。稳妥流程是用file命令先看一眼拿不准就把编码探测参数留给程序处理。这套流程跑熟之后转换本身通常只需要十几秒大部分时间都花在“确认输入格式”上。3.2 核心转换流程Start_Program的两种调用方式Start_Program这类工具的入口脚本常见做法是用Python封装成一个命令行程序同时支持点云和栅格两种模式。核心逻辑不复杂但有几处细节必须处理到位自动探测编码、自动识别分隔符、写入文件时统一LF换行。下面给出一段可以直接改用的核心实现# start_program.py # txt 转 ASC 转换入口支持点云坐标型和 ESRI ASCII Grid 两种模式 import argparse import re import sys # 分隔符映射space 用于按空白切分 DELIM_MAP { tab: \t, comma: ,, space: r\s, semicolon: ;, } def auto_detect_encoding(path): 自动探测编码优先 utf-8-sig退回 gbk最后再试 utf-8 for enc in (utf-8-sig, gbk, utf-8): try: with open(path, r, encodingenc) as f: f.read(2048) return enc except (UnicodeDecodeError, IOError): continue return utf-8 def convert_cloud(src, dst, delimiterspace, cols0,1,2, precision4, skip_headerFalse, encodingauto): 点云模式按列映射取出目标列写出空格分隔的 ASC 行 if encoding auto: encoding auto_detect_encoding(src) delim DELIM_MAP[delimiter] cols_idx [int(x) for x in cols.split(,)] with open(src, r, encodingencoding) as fin, \ open(dst, w, encodingutf-8, newline\n) as fout: for line_no, line in enumerate(fin): if line_no 0 and skip_header: continue parts re.split(delim, line.strip()) if len(parts) max(cols_idx) 1: continue # 列数不够的行直接跳过 row [] for idx in cols_idx: try: row.append(f{float(parts[idx]):.{precision}f}) except ValueError: row.append(0.0) fout.write( .join(row) \n) def convert_grid(src, dst, ncols, nrows, cellsize, xllcorner, yllcorner, nodata-9999, encodingauto): 栅格模式读全部数值校验数量后写出带文件头的 ASC if encoding auto: encoding auto_detect_encoding(src) values [] with open(src, r, encodingencoding) as fin: for line in fin: if line.startswith(ncols) or line.startswith(NODATA): continue values.extend(re.split(r\s, line.strip())) total nrows * ncols if len(values) total: raise ValueError( f数据量不足需要 {total} 个值实际只有 {len(values)} 个 请检查 --nrows/--ncols 是否配反 ) values values[:total] with open(dst, w, encodingutf-8, newline\n) as fout: fout.write(fncols {ncols}\n) fout.write(fnrows {nrows}\n) fout.write(fxllcorner {xllcorner:.6f}\n) fout.write(fyllcorner {yllcorner:.6f}\n) fout.write(fcellsize {cellsize}\n) fout.write(fNODATA_value {nodata}\n) for r in range(nrows): start r * ncols fout.write( .join(values[start:start ncols]) \n) if __name__ __main__: # 主入口只保留最常用参数完整参数说明见 3.3 节 parser argparse.ArgumentParser(descriptiontxt 转 ASC 转换器) parser.add_argument(--mode, choices[cloud, grid], requiredTrue) parser.add_argument(-i, --input, requiredTrue, help输入 txt 路径) parser.add_argument(-o, --output, requiredTrue, help输出 asc 路径) parser.add_argument(-d, --delimiter, defaultspace, choices[tab, comma, space, semicolon]) parser.add_argument(--cols, default0,1,2, help点云模式下列映射比如 0,1,2,3 表示取前四列) parser.add_argument(--precision, typeint, default4) parser.add_argument(--skip-header, actionstore_true) parser.add_argument(--encoding, defaultauto) # grid 模式参数 parser.add_argument(--ncols, typeint) parser.add_argument(--nrows, typeint) parser.add_argument(--cellsize, typefloat) parser.add_argument(--xllcorner, typefloat) parser.add_argument(--yllcorner, typefloat) parser.add_argument(--nodata, typefloat, default-9999) args parser.parse_args()这段脚本里有几个值得说清楚的逻辑点。编码探测放在最前面用utf-8-sig开头是因为Windows记事本导出的UTF-8文件自带BOM头用普通utf-8去读会把BOM的不可见字符带进第一行数据。点云模式里用re.split而不是字符串split是因为当分隔符是空格时源文件里可能混着连续空格和Tab正则表达式统一按“一个或多个空白”切列错位的概率会明显降低。写文件时统一指定newline\n能避免Windows下输出的文本被写成CRLF换行。这一步看起来小实际踩过CRLF的asc文件在Linux服务器上跑批处理时最后一行经常解析出多余的\r字符导致点云边界计算多出一个离群点。另外栅格模式里如果源txt本身就是从别的ASC转来的文件头要跳过这段代码用startswith判断不然会把头部的ncols这一行当成数值混进数据区。3.3 批量转换和精度控制单文件转换验证通过后批量才是提高效率的地方。Start_Program的批量用法可以包装成一个bash循环把目录下所有txt一次转换for f in ./txt_input/*.txt; do python start_program.py --mode cloud \ -i $f -o ./asc_output/$(basename $f .txt).asc \ -d comma --cols 0,1,2,3 --precision 4 --skip-header done批量转换要特别关注命名规则我这里用basename去掉了源文件名里的.txt后缀换成.asc避免输出文件覆盖或重名。循环体内没加断点判断如果某个文件编码异常程序会中断整个批任务。实际工程项目里我会建议在循环里加一个简单的异常捕获单文件失败打印错误继续跑下一个比全挂更省心。精度控制是体量上的隐患。点云文件动辄几百万行保留6位小数和保留3位小数文件体积差接近一倍。坐标类的数据保留4到5位小数足够高程栅格保留到2到3位即可。--precision参数默认给4是在数值精度和文件体积之间折中的结果。如果源文件里有大量科学计数法表示的值比如1.23456e05这类数字在转换时会被重新格式化为定点小数肉眼看着舒服但要注意指数部分不能被截断否则栅格高程直接差一个数量级这是下一章要细说的坑。4. 避坑实录转出的ASC打不开、错位、镜像怎么办4.1 现象点云ASC在软件里打开了但整体“被压扁”曾有一次把扫描仪导出的txt批量转成ASCCloudCompare里打开点云整体像一张纸一样贴在平面上高度全部消失。检查输出文件才发现第三列的数据全是0.0000。原因出在列映射上。源txt实际是“点号、X、Y、Z、强度”我以为前三列就是X、Y、Z实际是点号占用了第一列转换后程序把点号当成XX当成YY当成Z真正的Z被当成强度丢掉了。点号是整数X和Y是6位小数Z也是6位小数肉眼在前10行根本看不出问题直到统计Z值范围才发现最小值0、最大值0。解决方式是在转换前先打印源文件前5行用实际的列位置去填--cols参数。对着这个例子正确写法是--cols 1,2,3,4跳过第0列的点号。从那以后我的检查习惯里多了一条转完必须算一次X、Y、Z的min/max范围Z值全0或者一个恒定值基本可以断定列映射配错了。4.2 现象栅格ASC加载后图层整体偏移到一片空白区域把高程txt转成ESRI ASCII Grid后在QGIS里加载图层出现在离原数据十万八千里的地方而且和底图完全套不上。这种偏移最常见的原因是xllcorner写错了。ESRI ASCII Grid的文件头要求xllcorner和yllcorner是“栅格左下角单元格的左下角坐标”不是栅格中心点坐标更不是数据点的最小值。我最早实现时图省事直接取了源txt里X、Y的最小值填进去结果整个栅格偏移了半个单元格。单个单元格1米时偏移0.5米不明显单元格到10米以上时错开的距离直接能被肉眼识别。解决方法是先搞清源数据的每一个数值代表的是格网中心点坐标还是格网角点坐标。如果是中心点坐标xllcorner要换算成“最小中心坐标减去cellsize的一半”。再一个容易被忽略的是坐标位数文件头里的xllcorner如果只保留3位小数而数据区坐标保留6位ArcGIS也会认为坐标系不匹配加载后自动做投影推导图层照样对不齐。宁可文件头多写几位小数也不要手动截断。4.3 现象Windows设备导出的txt一读就抛UnicodeDecodeError终端里直接报错UnicodeDecodeError: utf-8 codec cant decode byte整批转换全部中断。这个问题的现场几乎一样国内老款测量设备、GPS静态解算软件导出的txt默认用GBK编码里面还夹杂着中文表头比如“序号,X坐标,Y坐标,Z坐标”。Python默认用UTF-8去读第一行就炸。另一个隐蔽场景是文件开头有BOM头Python能读通但第一行数据的第一列会被拼上不可见字符转换出的坐标值始终对不上原数据。解决方式就是脚本里先跑编码探测把auto_detect_encoding的优先级尽量放宽utf-8-sig、gbk、utf-8依次尝试。注意不要把gbk放在最后因为很多gbk文件本身是兼容ASCII的utf-8试不出来gbk能正确解码。遇到gbk和gb2312混用的情况再提供一个--encoding参数手动强制指定比自动探测更稳。4.4 现象栅格转出来后高程值全部变成整数小数被吞转出来的ASC在ArcGIS里打开原本精度是0.01米的DEM所有高程都变成了整数值比如934而不是934.57。原因是使用了文本类型去拼接数值。转换时如果直接把字符串拼接进输出文件或者浮点数格式化时指定了%.0f小数自然消失更隐蔽的是源txt里数值本身用了科学计数法比如9.345700e02而解析逻辑里float()转换后没有指定小数位默认输出按最短表示方式处理写成了934.57看上去没问题但某些软件再读取时就按整数解析。解决方式是在点数云和栅格时都统一显式格式化浮点数按f{value:.4f}写入字符串按float()先转一次再格式化。这里也提醒一下不要把数据的格式化逻辑分散在多个地方统一在一个函数里后续要调整精度只改一处就够了。5. 转换完不急着交付用校验脚本把ASC拉回来检查5.1 读回校验的最快方式ASC输出之后第一时间别急着发给同事先做个“读回验证”——用Python把刚生成的ASC再读一遍统计和源数据的变化。这是我处理格式转换后必做的一步能拦截掉上面提到的绝大多数问题。# verify_asc.py # 读回点云型 ASC校验点数、边界坐标和 Z 值范围 import numpy as np def verify_cloud(path, expect_rowsNone): arr np.loadtxt(path, comments#) print(文件维度 shape:, arr.shape) print(X 范围:, arr[:, 0].min(), -, arr[:, 0].max()) print(Y 范围:, arr[:, 1].min(), -, arr[:, 1].max()) print(Z 范围:, arr[:, 2].min(), -, arr[:, 2].max()) if expect_rows and arr.shape[0] ! expect_rows: print(警告: 行数与预期不一致) return arr def verify_grid(path, ncolsNone, nrowsNone): with open(path, r, encodingutf-8) as f: lines f.readlines() header {l.split()[0]: l.split()[1] for l in lines[:6]} data_lines [l for l in lines[6:] if l.strip()] row_sizes set(len(l.split()) for l in data_lines) print(文件头:, header) print(数据行数:, len(data_lines), 每行列数:, sorted(row_sizes)) if ncols and row_sizes ! {ncols}: print(警告: 数据区列数与 ncols 不一致)这段代码回答了一个关键问题转出来的文件到底“对不对”。点数云时重点看arr.shape[0]和源文件数据行数是否一致看X、Y范围的量级是否正常比如坐标应该是在某个投影带范围是几十万级别的整数如果出现几百说明单位或者坐标系有误Z范围如果全为0就是第三节里说的列映射问题。验栅格时要重点看每行列数是否一致如果某一行因为文本编码问题被截断这一行的列数会比其他行少row_sizes里就会出现两个不同的值。5.2 交付前的五步检查清单检查项方法合格标准行列数/点数一致用上面verify脚本对比点数差为0栅格行数nrows且每行ncols边界坐标范围正常打印X/Y/Z的min、max量级和源数据一致Z不全为0空值和NODATA合理统计NAN和-9999数量NAN为0NODATA占比符合预期文件编码正常file命令查看输出文件输出为UTF-8无CRLF残留软件实际打开CloudCompare或QGIS抽查首尾100行点位无异常偏离栅格无镜像这个清单是我踩了上面一堆坑之后总结出来的标准动作。最开始我以为转换器写完输入输出一片绿就是结束了直到有一次把验证交给同事做第一眼就在ArcGIS里看到一个镜像的DEM——源数据是从右向左排列的我没做翻转直接按原顺序写进ASC图像就像照了镜子。那一次之后我每次批量转完ASC都会强制自己先跑一遍校验脚本再发出去读回文件、比对行数、统计边界、确认Z值范围。宁可把校验写成无脑脚本也不要在同事的软件里翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表