简介:TIF/TIFF文件的合并及拆分工具是一份面向图形设计、扫描归档与印刷从业者的小型Windows实用程序,用于把多幅图像拼合为多页TIFF,或从多页文件中按页提取单页,并兼顾元数据保留与批量处理需求。压缩包共3个文件,以2个htm说明文档和1个exe可执行程序为主,整体约370KB,体量轻便,说明文档可辅助了解界面与功能边界。目前已有2722人学习或下载,具备一定参考热度。读者可获得一个可直接运行的工具及配套使用说明,围绕合并顺序、页面范围选择、压缩方式与色彩模式兼容性等环节完成图像整理;对需要追踪拍摄日期、分辨率等属性的扫描件,也可在操作中关注元数据是否保留,从而减少重复劳动、提升批量管理多页TIFF的效率。
1. 别急着合并:TIF/TIFF 拆并工具先看“页”和“块”
TIF/TIFF 文件的合并及拆分工具,最容易让人误判的一点是:它处理的不是“一张图片”,而是一个可能装了几十页、几百个瓦片、甚至带金字塔层的容器。扫描件归档、显微切片导出、遥感影像裁切,都会遇到同一类活:把散图并成一份多页 TIFF,或者把一份大 TIFF 按页、按区域拆开。
这类工具真正解决的是批量、顺序、位深和压缩参数的一致性,不是简单复制粘贴。适合手里有一堆 TIF 要交付、又不想逐张打开另存的人。先别急着拖进看图软件,翻车往往从“看起来能打开”开始。
2. TIF 容器解剖:多页、分块与位深决定工具怎么选
2.1 多页 TIFF 与“拼图”是两件事
很多人把“合并 TIF”理解成把几张图拼成一张大图,实际在交付场景里至少有三种合并方式,选错方向后面全白做。第一种是多页合并:把目录下按序号排列的单页 TIFF 写进同一个文件,每一页保留自己的图像数据,文件结构上是一串 IFD 链,翻页就能看下一张。扫描件、票据、病历切图、显微切片序列,多数走这条路。它的关键不是像素怎么拼,而是页顺序、页尺寸和位深是否一致。
第二种是空间拼接:每张图代表一块地理范围或视野,需要按左上角坐标、重叠区、分辨率拼成一张大画布。这种合并不能只看文件名,必须拿到偏移量和像素分辨率,否则拼出来就是错位的“拼贴画”。第三种是通道合并:同一视野下的 R、G、B 或不同波段分别存成单页,最后合成一个多通道文件。它要求波段顺序、位深和色彩解释一致,不能把 16 位波段硬压成 8 位再合。
工具选型先问一句:交付物是“一本翻页册”还是“一张大地图”?前者用多页写入,后者走坐标拼接,不要用同一个脚本硬套。常见做法是把合并逻辑拆成 page-stack、mosaic、channel-merge 三个入口,参数表分开维护,排错时才能定位到是哪一类合并出的问题。
2.2 位深、色彩空间、压缩:合并前必须对齐的三组参数
位深是最容易被忽略的一项。8 位灰度、16 位灰度、32 位浮点,在数组里都是二维,但写进 TIFF 后查看器解释完全不同。把 16 位影像直接塞进按 8 位打开的流程,常见结果是高光裁切、整体发灰,或者数值被截断。合并前先读第一页的 dtype,后面每一页都对齐到它;确实要降位深,先做线性拉伸或归一化,不要用默认的强转。
色彩空间决定 photometric 怎么填。灰度用 minisblack,RGB 用 rgb,带 alpha 的图要么去掉 alpha,要么明确写成 rgba,不要混在多页里让软件猜。压缩则决定“无损还是有损”:LZW 和 ZLIB 适合归档、医学和遥感,JPEG 压缩率高但有损,不适合需要量测的像素;预测器 predictor 对整数影像能提升压缩率,但要看位深和压缩类型是否匹配。
分块 tile 是存储层的参数,不是显示层的参数。大图写成 256×256 或 512×512 的瓦片,局部读取会快很多;小图硬上瓦片反而增加索引开销。超过 4GB 的文件要开 BigTIFF,否则写到一半报错,前面的页全废。分辨率标签也别丢,XResolution、YResolution 和 resolutionunit 在交付时经常被验收方检查。
2.3 用一段脚本把 TIFF 的底牌翻开
拿到一份陌生 TIFF,先别写合并逻辑,先看结构。下面这段脚本读取页数、首屏形状、 dtype、压缩方式、瓦片大小和分辨率标签,够判断它属于多页、分块还是普通单页。
from pathlib import Path import tifffile path = Path("/data/tif/sample.tif") with tifffile.TiffFile(path) as tif: print("页数:", len(tif.pages)) print("series 数:", len(tif.series)) for i, page in enumerate(tif.pages[:3]): print(f"第{i}页 shape={page.shape} dtype={page.dtype}") print("压缩:", page.compression) print("photometric:", page.photometric) print("tile:", page.tilewidth, page.tilelength) print("XResolution:", page.tags.get("XResolution")) for s in tif.series: print("series:", s.name, s.shape, s.dtype, "levels=", len(s.levels))逻辑很直接:TiffFile 打开的是容器,pages 是物理页,series 是逻辑图像序列。多页扫描件通常 pages 大于 1、series 等于 1;金字塔影像会在 series 里出现多个 levels;分块 TIFF 的 tilewidth 和 tilelength 不为零。参数上重点看 dtype 和 photometric,它们决定后面合并时能不能直接写;XResolution 这类标签如果为空,交付前要补,不要等验收时才发现。
提示:先用这段脚本批量跑一遍源目录,把页数、shape、dtype 打印到日志,比直接开合并脚本安全得多。
3. 合并路径:散图、多页与分块 TIFF 怎么写进一个文件
3.1 散图合并成多页 TIFF:顺序、命名与内存边界
散图合并最常见的翻车点不是写不进去,而是顺序乱了、尺寸不一致、内存被撑爆。文件名排序要按自然序处理,page_2 和 page_10 用普通字符串排序会变成 page_10 在前。源图尺寸不一致时,要么在合并前统一裁切或缩放,要么明确报错让业务方处理,不要偷偷补边,补出来的黑边在交付时很难解释。
下面这段代码按文件名排序读取,逐页检查尺寸,全部通过后再一次性写入多页 TIFF。它适合页数在几百页以内、单页尺寸不大的归档场景。
from pathlib import Path import numpy as np import tifffile src_dir = Path("/data/tif/pages") files = sorted(src_dir.glob("*.tif")) arrays = [] for f in files: arr = tifffile.imread(f) arrays.append(arr) base_shape = arrays[0].shape[:2] base_dtype = arrays[0].dtype for f, arr in zip(files, arrays): if arr.shape[:2] != base_shape: raise ValueError(f"尺寸不一致: {f} {arr.shape[:2]} != {base_shape}") if arr.dtype != base_dtype: raise ValueError(f"位深不一致: {f} {arr.dtype} != {base_dtype}") tifffile.imwrite( "/data/tif/merged_pages.tif", arrays, photometric="rgb" if arrays[0].ndim == 3 and arrays[0].shape[2] == 3 else "minisblack", compression="zlib", predictor=2, resolution=(300, 300), resolutionunit="INCH", metadata={"source": "page-merge"}, )imwrite收到数组列表时,第一维会展开成多页;photometric 根据通道数选择,三通道用 rgb,单通道用 minisblack;compression 用 zlib 保证无损;predictor=2 对 8 位和 16 位整数有压缩增益;resolution 和 resolutionunit 写交付分辨率。页数上千时不要把所有数组堆在内存里,改成逐页写:先写第一页,后续用追加模式或先落临时分卷,再合并。这个边界不提前想清楚,跑到一半内存爆掉,前面读的页全白费。
3.2 大图拼接:重叠区、分辨率与坐标对齐
空间拼接和多页合并完全不是一回事。每块图必须带左上角偏移,画布大小要能覆盖所有块,重叠区还要做羽化或取平均,否则接缝会像刀切。下面是一个无重叠直贴的最小示例,适合已知偏移、块之间严丝合缝的场景;有重叠时不能直接覆盖,要先算权重。
import numpy as np import tifffile # tiles 形如 [(路径, x偏移, y偏移), ...] tiles = [ ("/data/tif/tile_0_0.tif", 0, 0), ("/data/tif/tile_1_0.tif", 4096, 0), ] canvas_h, canvas_w = 8192, 8192 canvas = np.zeros((canvas_h, canvas_w, 3), dtype=np.uint8) for path, x, y in tiles: arr = tifffile.imread(path) h, w = arr.shape[:2] canvas[y:y+h, x:x+w] = arr tifffile.imwrite( "/data/tif/mosaic.tif", canvas, photometric="rgb", compression="zlib", tile=(512, 512), bigtiff=True, )这段代码只解决“贴上去”,不解决“贴得准”。真实拼接要先确认所有块的分辨率一致,单位是米还是像素,重叠宽度是多少;重叠区常见做法是线性羽化,左右各留 10% 到 20% 的过渡带。画布过大时不要一次开满内存,按输出瓦片逐块写,或者先写 BigTIFF 再分块填充。坐标标签也要随像素一起写,否则图拼对了、位置却丢了。
3.3 写出参数:压缩、分块、BigTIFF 怎么选
合并后的文件能不能顺利交付,很大程度取决于写出参数。下面这张表按场景给一个起点,具体还要看源数据噪声和验收方的读取工具。
| 参数 | 常见取值 | 适用场景 | 注意点 |
|---|---|---|---|
| compression | zlib / lzw | 归档、医学、遥感 | 无损,体积比 JPEG 大 |
| compression | jpeg | 预览图、网页展示 | 有损,不适合量测 |
| predictor | 2 | 8/16 位整数 | 浮点影像不要硬套 |
| tile | 256 或 512 | 大图局部读取 | 小图不必开瓦片 |
| bigtiff | True | 预计超过 4GB | 老工具可能不认 |
| photometric | minisblack / rgb | 灰度 / 彩色 | 必须和通道数一致 |
写入大文件时,我一般先把 bigtiff=True 打开,再按需降级,而不是先写普通 TIFF 再赌它不超限。下面这段是带瓦片和 BigTIFF 的写出方式,适合几千像素以上的大图。
import tifffile tifffile.imwrite( "/data/tif/big_mosaic.tif", data, bigtiff=True, compression="zlib", tile=(512, 512), photometric="minisblack", )bigtiff=True让偏移量用 64 位存储,突破 4GB 限制;tile=(512, 512)把图像切成可独立解码的块,后续按区域读取会快很多;compression="zlib"保持无损。如果验收方明确只收普通 TIFF,再关掉 BigTIFF 并控制输出尺寸;这个顺序比事后补救稳。
4. 拆分路径:按页、按瓦片、按通道拆到可交付
4.1 按页拆分:多页 TIFF 逐页导出与命名
多页 TIFF 拆分的核心是“逐页解码、逐页落盘”,不要一次把全部页读进列表。下面这段代码按物理页遍历,文件名补零到四位,保证资源管理器里排序正确。
from pathlib import Path import tifffile src = Path("/data/tif/merged_pages.tif") out_dir = Path("/data/tif/split_pages") out_dir.mkdir(parents=True, exist_ok=True) with tifffile.TiffFile(src) as tif: for i, page in enumerate(tif.pages): arr = page.asarray() out = out_dir / f"page_{i:04d}.tif" tifffile.imwrite( out, arr, photometric="rgb" if arr.ndim == 3 and arr.shape[2] == 3 else "minisblack", compression="zlib", )page.asarray()只解码当前页,内存占用可控;i:04d让 page_0002 排在 page_0010 前面;photometric 按数组形状判断,避免灰度页被写成彩色。如果原文件带 ICC 配置或分辨率标签,拆分时要把这些标签一起复制,否则单页交付会丢元数据。页数很多时把日志写出来,记录每页的 shape 和 dtype,出问题能快速定位到具体页。
4.2 按瓦片/区域拆分:分块 TIFF 与大图裁切
按区域拆分有两种做法:一种是逻辑裁切,先读整页再切片;另一种是流式读取,按存储瓦片或窗口逐块取。下面这段是逻辑裁切,代码短、好懂,适合单页几千万像素以内、内存扛得住的场景。
from pathlib import Path import tifffile src = "/data/tif/large.tif" out_dir = Path("/data/tif/tiles") out_dir.mkdir(parents=True, exist_ok=True) with tifffile.TiffFile(src) as tif: page = tif.pages[0] full = page.asarray() tile_size = 1024 h, w = full.shape[:2] for y in range(0, h, tile_size): for x in range(0, w, tile_size): tile = full[y:y+tile_size, x:x+tile_size] out = out_dir / f"tile_y{y}_x{x}.tif" tifffile.imwrite(out, tile, compression="zlib")这段代码的边界要讲清楚:page.asarray()已经把整页读进内存,图特别大时会爆。真正流式拆分要按存储瓦片读,或者用支持窗口读取的库按块取。tile_size 和原文件存储瓦片对齐时解码开销最小;不对齐也能切,但每块都要重新解码,速度会慢。命名里带 y、x 偏移,后续拼回去或做坐标映射时不用再猜位置。
4.3 按通道与位深拆分:RGB、多光谱、16 位影像
按通道拆分最怕两件事:把 16 位强转成 8 位,把波段顺序搞反。下面这段按最后一维拆波段,保持原 dtype,不做归一化。
import tifffile arr = tifffile.imread("/data/tif/rgb.tif") if arr.ndim == 3 and arr.shape[2] >= 3: names = ["R", "G", "B"] for idx, name in enumerate(names): band = arr[:, :, idx] tifffile.imwrite( f"/data/tif/band_{name}.tif", band, photometric="minisblack", compression="zlib", )arr[:, :, idx]取出单波段后保持二维,photometric 用 minisblack,写出来就是标准灰度 TIFF。多光谱把 names 换成 band_01、band_02 即可;16 位影像不要加astype(np.uint8),需要显示时另存预览图,原始波段保持原位深。波段顺序如果源文件是 BGR,先确认再命名,别让 R 和 B 互换,这种错误在肉眼看图时不一定明显,做指数计算时直接翻车。
5. 避坑与排查:合并拆分翻车的五个现场
5.1 合并后只看到第一页
现象:脚本显示写入了 80 页,打开文件只显示第一张,翻页按钮是灰的。原因通常不是文件坏了,而是查看器把多页 TIFF 当单页图打开,或者写入时每页被写成了独立 series。解决:先用第 2 章的脚本确认 pages 数量;再用另一个查看器交叉验证;如果 pages 正确但软件只显示第一页,导出成图片序列或 PDF 做交付预览。写入时保持一个 series 下的连续页,不要一页一个 series。
5.2 拆分后颜色发灰、尺寸不对
现象:原图看着正常,拆出来的单页发灰、高光溢出,或者长宽比变了。原因多是位深被默认转换、色彩空间解释错误,或者缩放时用了不合适的插值。解决:拆分前打印 dtype 和 photometric,16 位保持 16 位,RGB 不要按 BGR 解释;不要用默认的看图库做 resize,尺寸不一致时先报错。颜色发灰还要检查是不是把线性数据当 sRGB 显示,交付时附一份说明比事后解释省事。
5.3 内存爆掉或处理到一半卡死
现象:小样本跑得通,全量目录跑到一半进程被杀,或者机器开始频繁换页。原因是一次性把所有页读进列表、整张大图一次解码、并发开太多。解决:逐页读写,合并时先写第一页再追加;大图按瓦片或窗口读;批处理并发控制在内存能承受的范围。血泪经验是先用 10 页小样本压测,再逐步放大,不要拿全量目录直接试。
5.4 地理标签/分辨率丢失
现象:像素没问题,但交付方说坐标不对、分辨率不对,或者图放不到正确位置。原因是用裸数组重写时只写了像素,没复制 XResolution、YResolution、resolutionunit 和地理标签。解决:拆分和合并前先读原 tags,能复制的复制,不能复制的在日志里标出来;分辨率至少写 300 DPI 或源文件原值。地理标签涉及坐标系统时,先确认目标格式支持哪些标签,再决定重写策略。
5.5 压缩后体积反而变大
现象:开了压缩,输出比原文件还大。原因可能是对已压缩数据再做压缩、瓦片切得太小导致索引开销、或者 JPEG 质量参数过高。解决:归档场景优先 zlib/lzw,不要对 JPEG 源反复转码;瓦片从 256 或 512 起步,不要一上来就 64;大文件开 BigTIFF 后比较体积。压缩参数不是越小越好,读性能和体积要一起看。
6. 进阶:把合并拆分做成可自检的批处理流水线
6.1 合并后自检:页数、尺寸、位深、压缩四项
每次合并或拆分后,我都会跑一遍自检,不靠肉眼翻图。下面这个函数检查页数、首屏尺寸、dtype 和压缩方式,不匹配直接抛错,批处理里能第一时间拦住坏文件。
import tifffile def check_tif(path, expected_pages=None, expected_shape=None): with tifffile.TiffFile(path) as tif: pages = len(tif.pages) first = tif.pages[0] print(path, "pages=", pages, "shape=", first.shape, "dtype=", first.dtype, "compression=", first.compression) if expected_pages is not None and pages != expected_pages: raise ValueError("页数不符") if expected_shape is not None and first.shape[:2] != expected_shape: raise ValueError("尺寸不符") check_tif("/data/tif/merged_pages.tif", expected_pages=80)6.2 参数化批处理与失败重试
批处理模板不要把路径写死,源目录、输出目录、压缩方式、tile 大小都从参数读;每个文件单独 try,失败写日志后继续,不要一个坏文件打断整批。我一般会保留失败清单,修完源文件只重跑失败项,省掉全量重来。
6.3 一个技巧:先写 BigTIFF 再按需降级
大图合并先开 BigTIFF,确认页数、尺寸、压缩都正常后,再按验收要求决定是否降级成普通 TIFF。这个顺序相当于给自己留后悔药,避免写到 3.8GB 时才发现超限。从那以后我每次合并大图都强制先走一遍 BigTIFF 自检,再谈体积优化。希望帮到你。
本文还有配套的精品资源,点击获取