1. 遥感影像批处理的核心痛点与Arcpy的破局思路
搞遥感的人手里没攒过几百上千景影像,那都不好意思说自己做过项目。我最早接触遥感影像处理的时候,一景一景在软件里点裁剪、点掩膜,鼠标点到手腕发酸,一整个下午就处理了二十来景,还容易漏掉或者参数设错。后来开始用Arcpy写脚本,才算真正从重复劳动里解放出来。这篇文章就围绕Arcpy批处理遥感影像的裁剪与掩膜这个主题,把我这些年在实际项目中反复打磨过的思路、代码和踩坑经验完整梳理一遍。
先说清楚这个内容适合谁看。如果你手头有几十景甚至上千景遥感影像,需要按研究区边界统一裁剪,或者按矢量范围做掩膜提取,又不想一景一景手动操作,那这篇就是写给你的。哪怕你之前没怎么写过Python,只要跟着思路走,把参数换成你自己的数据路径,基本就能跑起来。我会把每一步为什么这么做讲透,不只是丢一段代码让你抄。
Arcpy是ArcGIS桌面端自带的Python站点包,安装ArcGIS之后就能直接调用,不需要额外配置环境。它的优势在于和ArcGIS的工具体系完全打通,arcpy.sa模块里的栅格处理函数和工具箱里的工具是一一对应的,你在工具箱里怎么设参数,脚本里就怎么传参数。对于遥感影像的裁剪和掩膜来说,核心用到的是ExtractByMask、Clip、ExtractByRectangle这几个函数,配合ListRasters做批量遍历,再套一个循环,就能实现全自动处理。
为什么强调批处理而不是单次处理?因为遥感影像的体量摆在那里。一景Landsat 8的OLI影像大概1GB左右,一景Sentinel-2的L2A产品解压后接近1GB,一个县级行政区的年度监测项目动辄上百景。单次处理的时间成本不只是操作本身,还有等待软件响应、切换文件、检查结果这些隐性开销。批处理把这些全部压缩成一次参数配置加一次运行,中间不需要人工干预,晚上挂着跑,第二天早上收结果,这才是正经的干活方式。
还有一个容易被忽略的点:一致性。手动处理的时候,不同时间、不同状态下设的参数可能有细微差异,比如输出像元大小、重采样方法、NoData值,这些差异在后期做时序分析或者镶嵌的时候会变成大麻烦。批处理脚本把参数固定下来,每一景走的都是同一套逻辑,结果的一致性有保障。这一点在做多时相对比或者变化检测的时候尤其重要。
2. 环境准备与数据组织:磨刀不误砍柴工
2.1 Arcpy运行环境的确认与常见配置问题
Arcpy的运行环境有两个选择:一是ArcGIS自带的Python环境,通常在C:\Python27\ArcGIS10.x或者ArcGIS Pro对应的C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3下面;二是自己配一个conda环境然后装arcpy,但这个比较折腾,不推荐新手走这条路。最省事的办法就是直接用ArcGIS自带的Python,打开IDLE或者PyCharm把解释器指向那个路径就行。
这里有个坑要提前说:ArcGIS Desktop(就是ArcMap那一套)用的是Python 2.7,ArcGIS Pro用的是Python 3.x,两者的arcpy语法有差异。比如Python 2里print是语句不用加括号,Python 3里必须加括号。如果你在网上找的代码跑不通,先检查一下版本对不对。我下面给的代码以Python 3为准,ArcGIS Pro用户可以直接用,ArcMap用户把print改一下就行。
还有一个常见问题是许可证。arcpy.sa模块需要Spatial Analyst扩展许可,如果你在脚本里调用了ExtractByMask但没检查许可,会直接报错。保险的做法是在脚本开头加一句arcpy.CheckOutExtension("Spatial"),处理完再arcpy.CheckInExtension("Spatial")还回去。虽然桌面端一般会自动借,但显式写出来更稳妥,尤其是在多脚本并行或者长时间运行的时候。
2.2 输入数据的组织方式与命名规范
批处理的前提是数据组织得有规律。我见过太多人把影像全部堆在一个文件夹里,文件名五花八门,有的带日期有的不带,有的用横杠有的用下划线,结果写脚本的时候光写文件名匹配规则就写了一上午。正确的做法是在项目开始之前就把目录结构定好,比如:
Project/ ├── input_rasters/ # 原始影像,按日期或轨道号分文件夹 │ ├── 20230115/ │ ├── 20230220/ │ └── ... ├── mask/ # 掩膜矢量文件 │ └── study_area.shp ├── output/ # 输出结果 │ ├── clipped/ │ └── masked/ └── scripts/ # 脚本存放 └── batch_process.py命名规范上,我建议影像文件名里包含日期和轨道号,比如LC08_123032_20230115.tif,这样在脚本里可以用字符串切片或者正则表达式提取信息,方便后续按条件筛选。如果文件名没有规律,那就得靠文件夹来区分,脚本里用os.walk递归遍历,但这样代码会复杂一些,不如一开始就规范命名来得省事。
掩膜矢量文件要注意坐标系。如果矢量和栅格的坐标系不一致,ExtractByMask会报错或者结果错位。稳妥的做法是在脚本里加一步坐标系检查,或者提前把矢量投影到和栅格一致的坐标系。我一般会在脚本里用arcpy.Describe读取栅格的spatialReference,然后判断矢量的坐标系是否匹配,不匹配就自动投影一份临时文件出来用。
2.3 输出路径与覆盖策略
输出路径不要和输入路径混在一起,否则批量处理的时候容易把自己生成的中间文件又当成输入读进去,造成死循环或者结果混乱。我习惯在输出文件夹里再按处理类型分子文件夹,比如clipped放裁剪结果,masked放掩膜结果,这样后期查找和整理都方便。
覆盖策略上,ExtractByMask和Clip默认如果输出文件已存在会直接覆盖,不会弹窗提示。这在批处理里是好事,但也要小心——如果你不小心把输出路径设成了输入路径,那就直接把原始数据覆盖了。我踩过这个坑,一晚上的处理结果把原始影像全毁了,还好有备份。所以脚本里最好加一个判断,如果输出路径和输入路径相同就报错退出。
3. 裁剪与掩膜的核心原理与函数选型
3.1 裁剪和掩膜到底有什么区别
很多人把裁剪和掩膜混着说,其实在ArcGIS的语境里它们有细微但重要的区别。裁剪(Clip)是按矩形范围或者矢量外接矩形来切割栅格,输出的是矩形区域内的像元,矢量边界之外的像元被保留但可能包含在矩形范围内。掩膜(Mask)则是严格按照矢量边界来提取,边界之外的像元全部设为NoData,边界之内保留原始值。简单说,裁剪是“切矩形”,掩膜是“抠形状”。
在实际项目中怎么选?如果你的研究区是矩形或者接近矩形,用裁剪就够了,速度快,结果规整。如果研究区是不规则形状,比如流域边界、行政区划边界,那就必须用掩膜,否则边界外的像元会混进来影响统计。还有一种情况是两者结合:先用裁剪把大范围影像切成小矩形,再用掩膜抠出精确边界,这样处理效率更高,因为裁剪后的影像体量小,掩膜处理起来更快。
Arcpy里对应的函数是:
- 裁剪:
arcpy.Clip_management()或者arcpy.sa.ExtractByRectangle() - 掩膜:
arcpy.sa.ExtractByMask()
Clip_management是工具箱里的裁剪工具,功能更全,支持设置NoData值、保持裁剪范围等选项。ExtractByMask是Spatial Analyst里的掩膜提取,专门做按形状提取。我一般批量处理用ExtractByMask,因为它对矢量的支持更好,而且输出自动把边界外设为NoData,不需要额外设置。
3.2 ExtractByMask函数的参数详解
ExtractByMask的完整签名是:
ExtractByMask(in_raster, in_mask_data)只有两个必填参数,看起来很简单,但实际用的时候有几个细节要注意。
第一个是in_raster,可以是栅格文件路径,也可以是栅格对象。批量处理的时候一般传文件路径,让arcpy自己去读。如果传的是栅格对象,要注意对象释放的问题,不然处理大量影像的时候内存会爆。
第二个是in_mask_data,可以是矢量文件路径,也可以是栅格。传矢量的时候,arcpy会自动把矢量转成栅格掩膜,转换的分辨率默认和输入栅格一致。这里有个隐藏参数是analysis_extent,如果不设,默认用输入栅格的范围,但有时候矢量的范围比栅格大,就会导致输出范围不对。稳妥的做法是显式设置arcpy.env.extent为输入栅格的范围。
还有一个容易忽略的是arcpy.env.snapRaster。如果掩膜矢量的坐标系和栅格不一致,或者像元对齐有问题,设置snapRaster可以让输出像元和输入栅格严格对齐,避免出现半个像元的偏移。这个在做多景影像镶嵌的时候特别重要,不对齐的话镶嵌出来会有缝隙。
3.3 批量遍历的实现方式对比
批量遍历文件有三种常见方式:arcpy.ListRasters()、os.listdir()、glob.glob()。三者各有适用场景。
arcpy.ListRasters()是arcpy自带的,可以直接按栅格格式筛选,比如arcpy.ListRasters("*", "TIF")只列出TIF文件。它还会自动识别栅格数据集,不管文件扩展名是什么。缺点是只能列出工作空间下的栅格,不支持递归子文件夹,而且对文件名模式的支持比较弱。
os.listdir()是Python标准库的,列出文件夹下所有文件,需要自己判断扩展名。优点是灵活,可以配合os.path.join拼路径,也可以递归。缺点是要自己处理文件名编码问题,中文路径有时候会出问题。
glob.glob()支持通配符匹配,比如glob.glob("*.tif")列出所有TIF文件,glob.glob("**/*.tif", recursive=True)递归列出所有子文件夹下的TIF。优点是模式匹配强大,代码简洁。缺点是在文件数量特别多的时候性能不如前两者。
我一般用glob.glob(),因为写起来最直观,而且支持递归,适合数据分文件夹存放的情况。如果文件数量上万,那就换成os.scandir(),性能更好。
4. 完整批处理脚本的逐段拆解与实操
4.1 脚本整体框架与参数配置区
先看整体框架。一个好的批处理脚本应该把参数配置集中放在开头,后面是函数定义和主循环,这样修改参数的时候不用满脚本找。下面是我常用的框架:
# -*- coding: utf-8 -*- import arcpy import os import glob import time # ============ 参数配置区 ============ INPUT_DIR = r"D:\Project\input_rasters" MASK_VECTOR = r"D:\Project\mask\study_area.shp" OUTPUT_DIR = r"D:\Project\output\masked" RASTER_EXT = "*.tif" OVERWRITE = True # =================================== def check_license(): """检查并借用Spatial Analyst许可""" if arcpy.CheckExtension("Spatial") == "Available": arcpy.CheckOutExtension("Spatial") print("Spatial Analyst许可已借用") else: raise RuntimeError("Spatial Analyst许可不可用") def get_raster_list(input_dir, ext): """获取待处理栅格列表""" pattern = os.path.join(input_dir, "**", ext) rasters = glob.glob(pattern, recursive=True) return rasters def batch_extract_by_mask(raster_list, mask, output_dir): """批量掩膜提取""" total = len(raster_list) success = 0 failed = [] for i, raster in enumerate(raster_list, 1): basename = os.path.basename(raster) name, _ = os.path.splitext(basename) out_path = os.path.join(output_dir, name + "_masked.tif") try: # 设置环境参数 arcpy.env.extent = raster arcpy.env.snapRaster = raster arcpy.env.overwriteOutput = OVERWRITE # 执行掩膜提取 out_raster = arcpy.sa.ExtractByMask(raster, mask) out_raster.save(out_path) success += 1 print("[{}/{}] 成功: {}".format(i, total, basename)) except Exception as e: failed.append((basename, str(e))) print("[{}/{}] 失败: {} - {}".format(i, total, basename, str(e))) return success, failed if __name__ == "__main__": start_time = time.time() check_license() if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) rasters = get_raster_list(INPUT_DIR, RASTER_EXT) print("共找到 {} 景影像".format(len(rasters))) success, failed = batch_extract_by_mask(rasters, MASK_VECTOR, OUTPUT_DIR) elapsed = time.time() - start_time print("处理完成: 成功 {} 景, 失败 {} 景, 耗时 {:.1f} 秒".format( success, len(failed), elapsed)) if failed: print("失败列表:") for name, err in failed: print(" {}: {}".format(name, err)) arcpy.CheckInExtension("Spatial")这个框架看着简单,但每一段都有讲究。参数配置区用全大写变量名,一眼就能找到。check_license函数把许可检查单独拎出来,避免在主逻辑里混着写。get_raster_list用glob递归匹配,支持子文件夹。batch_extract_by_mask是核心处理函数,用try-except包住每一景的处理,单景失败不影响整体,最后统一报告失败列表。
4.2 环境参数设置的三个关键项
arcpy.env下面有几十个环境变量,但批处理掩膜的时候最关键的只有三个:extent、snapRaster、overwriteOutput。
arcpy.env.extent控制输出范围。如果不设,ExtractByMask默认用输入栅格的范围,但有时候掩膜矢量的范围比栅格大,输出就会按矢量范围来,导致结果比预期大一圈。显式设为输入栅格的范围,保证输出和输入范围一致。
arcpy.env.snapRaster控制像元对齐。如果掩膜矢量的坐标系和栅格不一致,arcpy在转换矢量掩膜的时候可能会产生半个像元的偏移,导致输出栅格的像元中心和输入不对齐。设了snapRaster之后,输出像元会严格对齐到snapRaster的像元网格上。这个参数在做多景影像镶嵌或者时序分析的时候是必须的。
arcpy.env.overwriteOutput控制是否覆盖已有文件。批处理的时候一般设为True,避免每次弹窗确认。但要注意,如果输出路径和输入路径相同,这个设置会让你直接覆盖原始数据。所以脚本里最好加一个路径检查:
if os.path.abspath(OUTPUT_DIR) == os.path.abspath(INPUT_DIR): raise ValueError("输出路径不能和输入路径相同")4.3 掩膜提取的完整执行流程
把上面的框架串起来,完整的执行流程是这样的:
第一步,检查许可。arcpy.CheckExtension("Spatial")返回"Available"就借用,返回"Unavailable"就报错退出。这一步不能省,尤其是在ArcGIS Desktop环境下,有时候许可被其他程序占用了,不检查的话跑到一半才报错,浪费时间。
第二步,获取文件列表。用glob.glob递归匹配所有TIF文件,返回一个列表。如果文件数量是零,直接提示退出,不用往下跑。
第三步,创建输出文件夹。用os.makedirs创建,加一个exist_ok=True参数避免文件夹已存在时报错。
第四步,循环处理。每一景影像做四件事:拼输出路径、设环境参数、调ExtractByMask、保存结果。输出文件名在原始文件名后面加_masked后缀,方便区分。
第五步,统计结果。记录成功和失败的数量,失败的原因存到列表里,最后统一打印。这样跑完之后一眼就能看出哪些景出了问题,不用去翻日志。
第六步,归还许可。arcpy.CheckInExtension("Spatial")把许可还回去,养成好习惯。
整个流程跑一遍,100景影像大概需要10到20分钟,具体取决于影像大小和机器性能。比手动处理快了几十倍不止。
4.4 裁剪的批处理实现与参数差异
裁剪的批处理逻辑和掩膜基本一样,区别在于调用的函数和参数。用arcpy.Clip_management的话:
def batch_clip(raster_list, clip_vector, output_dir): for i, raster in enumerate(raster_list, 1): basename = os.path.basename(raster) name, _ = os.path.splitext(basename) out_path = os.path.join(output_dir, name + "_clipped.tif") try: arcpy.Clip_management( in_raster=raster, rectangle="", # 用矢量范围 out_raster=out_path, in_template_dataset=clip_vector, nodata_value="0", clipping_geometry="ClippingGeometry", maintain_clipping_extent="NO_MAINTAIN_EXTENT" ) print("[{}/{}] 成功: {}".format(i, len(raster_list), basename)) except Exception as e: print("[{}/{}] 失败: {} - {}".format(i, len(raster_list), basename, e))Clip_management的参数比ExtractByMask多,但核心是in_template_dataset传矢量,clipping_geometry设为"ClippingGeometry"表示按矢量形状裁剪,maintain_clipping_extent设为"NO_MAINTAIN_EXTENT"表示输出范围按实际裁剪结果来,不保持原范围。
和ExtractByMask的区别在于,Clip_management输出的是矩形范围,矢量边界外的像元保留但值可能被设为NoData(取决于nodata_value设置),而ExtractByMask输出的是严格按矢量形状,边界外全部NoData。实际用的时候,如果只需要矩形范围,用Clip_management更快;如果需要精确形状,用ExtractByMask。
5. 性能优化与大批量处理的实战技巧
5.1 内存管理与栅格对象释放
处理大量影像的时候,内存管理是个大问题。ExtractByMask返回的是一个栅格对象,如果不及时释放,内存会一直涨,跑到几十景之后就可能报内存错误。解决办法是在每次循环结束时显式删除对象:
out_raster = arcpy.sa.ExtractByMask(raster, mask) out_raster.save(out_path) del out_raster # 显式释放如果还是不够,可以用arcpy.Delete_management删除中间文件,或者用gc.collect()强制垃圾回收。不过gc.collect()比较耗时,不建议每次循环都调,可以每处理10景调一次。
还有一个技巧是用with语句管理栅格对象,但arcpy的栅格对象不直接支持上下文管理器,需要自己封装。简单起见,用del就够了。
5.2 并行处理的可能性与限制
Python的多线程因为GIL的存在,对CPU密集型任务效果不好,但arcpy的处理大部分时间花在IO和底层C++计算上,GIL在调用底层函数的时候会释放,所以多线程还是有一定效果的。可以用concurrent.futures.ThreadPoolExecutor开4到8个线程并行处理:
from concurrent.futures import ThreadPoolExecutor def process_one(raster): # 单景处理逻辑 pass with ThreadPoolExecutor(max_workers=4) as executor: executor.map(process_one, raster_list)但要注意,arcpy的许可管理不是线程安全的,多个线程同时调CheckOutExtension可能会出问题。稳妥的做法是在主线程借用许可,子线程只做处理不碰许可。另外,多个线程同时写同一个输出文件夹一般没问题,但如果输出文件名有冲突就会互相覆盖,所以文件名要保证唯一。
如果机器核多内存大,用多进程ProcessPoolExecutor效果更好,但arcpy在多进程下的初始化比较麻烦,每个进程都要重新导入arcpy和借用许可,开销不小。我一般用4线程,再多了收益递减。
5.3 处理进度监控与日志记录
批处理跑起来之后,最怕的就是不知道跑到哪了。加一个进度打印是最基本的:
print("[{}/{}] 处理: {}".format(i, total, basename))但光打印到控制台不够,如果脚本在后台跑,控制台输出看不到。更好的做法是同时写日志文件:
import logging logging.basicConfig( filename=os.path.join(OUTPUT_DIR, "process.log"), level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) logging.info("开始处理: {}".format(basename))日志文件的好处是可以事后查,哪一景失败了、失败原因是什么,一目了然。我习惯在日志里记录每景的处理时间,方便估算总体耗时和发现异常慢的影像。
还有一个技巧是用tqdm库显示进度条,比纯文本打印直观得多:
from tqdm import tqdm for raster in tqdm(raster_list, desc="处理进度"): # 处理逻辑 passtqdm不是标准库,需要pip install tqdm。如果环境不允许装第三方库,那就用纯文本的百分比打印,效果也不差。
6. 常见报错与排查技巧实录
6.1 坐标系不匹配导致的掩膜错位
这是最常见的问题。表现是掩膜结果和预期位置偏移,或者输出全是NoData。原因是矢量掩膜的坐标系和输入栅格不一致,arcpy在转换的时候按默认规则处理,导致位置对不上。
排查方法:用arcpy.Describe读取两者的坐标系,对比spatialReference.name是否一致。
raster_sr = arcpy.Describe(raster).spatialReference mask_sr = arcpy.Describe(mask).spatialReference print("栅格坐标系: {}".format(raster_sr.name)) print("矢量坐标系: {}".format(mask_sr.name))如果不一致,解决办法是用arcpy.Project_management把矢量投影到栅格坐标系,输出一个临时文件,用临时文件做掩膜。不要直接改原始矢量,保留原始数据是个好习惯。
6.2 输出全为NoData的几种原因
掩膜结果全是NoData,除了坐标系问题,还有几种可能:
一是矢量范围完全不覆盖栅格范围。检查两者的extent是否相交。用arcpy.Describe(raster).extent和arcpy.Describe(mask).extent对比。
二是矢量的几何有问题,比如自相交、空几何。用arcpy.CheckGeometry_management检查并修复。
三是arcpy.env.extent设错了,设成了和栅格不相交的范围。检查环境设置。
四是栅格的NoData值设置有问题,导致有效像元被当成NoData。检查栅格的noDataValue属性。
排查的时候按这个顺序来:先看范围是否相交,再看几何是否有效,再看环境设置,最后看NoData值。大部分问题在前两步就能定位。
6.3 大批量处理中途中断的恢复策略
跑了几百景,跑到一半脚本崩了,重新跑一遍太浪费时间。解决办法是在脚本里加断点续跑逻辑:处理之前先检查输出文件是否已存在,存在就跳过。
if os.path.exists(out_path) and not OVERWRITE: print("[{}/{}] 跳过已存在: {}".format(i, total, basename)) continue这样重新跑的时候只处理没处理过的,已经完成的直接跳过。配合日志文件,还能知道上次跑到哪了。
如果输出文件存在但不确定是否完整,可以检查文件大小,小于某个阈值就认为不完整,重新处理。或者用arcpy.Describe读取栅格的行列数,和输入对比,不一致就重新处理。
6.4 常见报错速查表
| 报错信息 | 可能原因 | 解决办法 |
|---|---|---|
ERROR 000732: 输入不存在或不受支持 | 路径错误或文件被占用 | 检查路径拼写,关闭占用文件的程序 |
ERROR 999999: 执行函数时出错 | 通用错误,原因多样 | 查看详细错误信息,通常是坐标系或许可问题 |
RuntimeError: 无法借用许可 | Spatial Analyst许可被占用 | 关闭其他ArcGIS程序,重启脚本 |
MemoryError | 内存不足 | 减少并行数,及时释放栅格对象 |
输出全为NoData | 坐标系不匹配或范围不相交 | 检查坐标系和范围 |
输出像元偏移 | 未设snapRaster | 设置arcpy.env.snapRaster |
文件名乱码 | 中文路径编码问题 | 用英文路径,或设置sys.setdefaultencoding |
这张表是我这些年踩坑攒下来的,大部分报错都能在里面找到对应。遇到没见过的报错,先把错误信息完整读一遍,ArcGIS的报错信息虽然有时候很笼统,但仔细看还是能找到线索。
7. 从单机脚本到工程化处理的进阶思路
7.1 参数外部化与配置文件管理
脚本写多了之后,每次改参数都要打开脚本改代码,很麻烦。更好的做法是把参数抽到配置文件里,用JSON或者YAML管理:
{ "input_dir": "D:/Project/input_rasters", "mask_vector": "D:/Project/mask/study_area.shp", "output_dir": "D:/Project/output/masked", "raster_ext": "*.tif", "overwrite": true, "max_workers": 4 }脚本里读配置文件:
import json with open("config.json", "r", encoding="utf-8") as f: config = json.load(f)这样换项目的时候只需要改配置文件,脚本本身不用动。配置文件还可以用版本控制管理,记录每次处理的参数,方便回溯。
7.2 处理结果的质量检查自动化
批处理跑完之后,结果质量检查也很重要。可以写一个检查脚本,自动统计每景输出的有效像元比例、范围是否正确、像元大小是否一致:
def check_result(out_raster, expected_extent): desc = arcpy.Describe(out_raster) # 检查范围 if desc.extent != expected_extent: return False, "范围不一致" # 检查像元大小 if desc.meanCellWidth != expected_cell_size: return False, "像元大小不一致" # 检查有效像元比例 # ... return True, "OK"检查结果输出成CSV,方便在Excel里筛选和统计。这一步能发现很多肉眼看不出来的问题,比如某几景影像因为云覆盖导致有效像元比例极低,或者某几景的范围偏了一点。
7.3 与其他处理步骤的串联
裁剪和掩膜往往只是整个处理流程中的一步。前面可能有辐射定标、大气校正,后面可能有镶嵌、重采样、时序分析。把每一步都写成独立的脚本,用统一的配置文件管理参数,用文件命名规范串联,就能形成一条完整的处理链。
比如:
01_radiometric_calibration.py 02_atmospheric_correction.py 03_batch_clip.py 04_batch_mask.py 05_mosaic.py 06_resample.py每个脚本的输出是下一个脚本的输入,中间用统一的命名规范衔接。这样整个流程模块化,哪一步出问题就单独重跑那一步,不用从头来。
我在实际项目里用这套流程处理过上千景影像,从原始数据到最终分析结果,全程脚本自动化,中间只需要人工检查关键节点的输出质量。这套方法的核心思路就是:把重复劳动交给脚本,把判断和决策留给人。脚本负责稳定、一致、快速地执行,人负责检查结果、调整参数、处理异常。两者配合,效率和质量都能兼顾。
最后分享一个小技巧:脚本写完之后,先拿3到5景影像做测试,确认结果正确再跑全量。测试的时候把日志级别调到DEBUG,把每一步的中间结果都打印出来,方便定位问题。全量跑的时候把日志级别调到INFO,只记录关键信息,避免日志文件太大。这个习惯帮我省了很多时间,尤其是处理新类型数据的时候,先小批量验证再大批量执行,基本不会出大问题。