简介:面向C#开发者的OpenCvSharp图像处理资源包,重点解决文件夹图像批量读取、窗体显示与多图拼接问题。资源完整涵盖目录遍历、图像矩阵与位图转换、预览控件绑定以及水平垂直拼接等关键实现,附带项目源码、运行库、接口说明和文本教程,适合初步接触视觉开发的编程人员参考学习。压缩包共二百一十三个文件,包含核心的工程源文件、解决方案文件、项目配置,同时有大量动态库文件、接口文档、程序包以及说明文档,便于直接还原开发环境,压缩后大小约一百六十六兆字节,目录结构层次清晰。目前已有七百六十余人下载学习,依据项目实例可快速打通从批量读取到拼接保存的完整链路,并可作为后续图像处理功能扩展的工程基底。
1. 一次要把上百张图拼成一张总览图:OpenCvSharp 与 C# 的落地方案
对于经常处理图片素材的 C# 从业者来说,opencv c# 多图拼接从来不是把两张图粘在一起那么简单。批量读文件夹、按规则排序、统一尺寸、最后拼接成网格图,每一步都有肉眼看不见的坑。我最早在一个图像处理 Demo 里被要求“把某文件夹下所有 PNG 拼成一张大图”,当时第一反应是用 System.Drawing 逐像素拷贝,结果 200 张图跑了一分多钟,内存还飙到 900MB。后来换成 OpenCvSharp,同样的任务压缩到 3 秒内完成。这个库本质是 OpenCV 的 C# 封装,API 风格接近原生 OpenCV,处理图像矩阵的效率远超 GDI+。这份资源解决的正是“文件夹批量读取 + 表单界面显示 + 多图拼接成网格图”这三个连在一起的典型需求,适合正在做图像采集工具、样本标注辅助程序或批处理工具的 C# 开发者。
2. 批量读取文件夹图片:从目录遍历到文件过滤的完整闭环
2.1 为什么用 OpenCvSharp.ImRead 而不是 Bitmap
System.Drawing.Bitmap 在 C# 桌面程序里读图很方便,但在批量场景下有两个硬伤:一是 Bitmap 对象持有 GDI 句柄,处理完必须手动 Dispose,否则文件会被锁定;二是 Bitmap 转像素矩阵做算法处理时,要经过 LockBits 拷贝,代码冗长还容易漏掉释放。OpenCvSharp 的 Cv2.ImRead 直接返回 Mat 对象,图像数据是连续内存块,后续拼接、缩放、阈值处理都在这块内存上操作,不需要频繁做托管和原生层的转换。另外 ImRead 支持 IMREAD_COLOR、IMREAD_GRAYSCALE、IMREAD_UNCHANGED 三种模式,批量读取时统一指定加载方式,能避免混入带透明通道或灰度图导致拼接报错。
using OpenCvSharp; string folderPath = @"D:\image_set\samples"; string[] extensions = new[] { "*.png", "*.jpg", "*.bmp" }; List<string> imageFiles = new List<string>(); foreach (string ext in extensions) { imageFiles.AddRange(Directory.GetFiles(folderPath, ext, SearchOption.TopDirectoryOnly)); } List<Mat> images = new List<Mat>(); foreach (string file in imageFiles) { Mat img = Cv2.ImRead(file, ImreadModes.Color); if (img.Empty()) { Console.WriteLine($"读取失败或文件损坏: {file}"); continue; } images.Add(img); }这段代码里 Directory.GetFiles 的第二个参数是通配符,第三个参数 SearchOption.TopDirectoryOnly 表示只扫当前目录,不递归子目录。我在实际项目里一般会先列出所有匹配文件,再逐个 ImRead,而不是用 Directory.GetAllFiles 一次性拿全路径,这样可以在加载阶段就把损坏文件过滤掉。ImreadModes.Color 强制以三通道 BGR 读入,即使源文件是灰度图或 PNG 带透明通道,读进来后通道数也会统一成 3,方便后续拼接时不做通道判断。
2.2 文件排序:按名称排序的坑与自然排序算法
批量读图后最容易被忽略的是顺序问题。Directory.GetFiles 返回的文件顺序在 NTFS 上通常是按名称排序的,但在网络驱动器或其他文件系统上,顺序是不稳定的。很多人直接用 LINQ 的 OrderBy(f => f) 排序,这样拿到的顺序是按字符串逐字符比较的,结果就是“img10.png”排在“img2.png”前面,拼出来的网格图序列完全错乱。文件名的数字在 1~9 之间时问题不明显,一旦超过 9,字符串排序和自然排序的差异就会暴露。
public static List<string> NaturalSort(List<string> files) { return files .Select(f => new { File = f, Key = System.Text.RegularExpressions.Regex.Replace( Path.GetFileName(f), @"\d+", m => m.Value.PadLeft(6, '0')) }) .OrderBy(x => x.Key) .Select(x => x.File) .ToList(); } List<string> sortedFiles = NaturalSort(imageFiles);这个自然排序的核心思路是把文件名中的数字段统一补零到 6 位,再做普通字符串排序。比如“img2.png”变成“img000002.png”,“img10.png”变成“img000010.png”,字符串比较就能得到和人类直觉一致的顺序。正则表达式 @”\d+” 匹配所有连续数字段,PadLeft(6, '0') 把数字补零对齐。这个做法对“cam_1_20240101.png”这种混合命名也有效,只要数字段长短不超过 6 位,超出的话把 PadLeft 的位数调大即可。
2.3 批量读取时的内存控制
一批图片全部 Load 到 Mat 列表里,内存压力是不能回避的。一张 4000×3000 的彩色图,Mat 占用的内存是 4000×3000×3 字节,约 36MB。100 张图就是 3.6GB,这在 32 位进程里直接导致 OutOfMemoryException。常见做法有两种:一是分批处理,每读入 N 张就拼接一次小网格,然后释放这批 Mat;二是先遍历所有文件获取尺寸信息,计算好网格布局后,再按需读取。多数场景下我会先用前者,因为它代码改动最小。
List<Mat> batch = new List<Mat>(); for (int i = 0; i < sortedFiles.Count; i++) { Mat img = Cv2.ImRead(sortedFiles[i], ImreadModes.Color); if (img.Empty()) continue; batch.Add(img); if (batch.Count == 16) { Mat grid = BuildGrid(batch, 4, 4); // 保存或显示 grid grid.Dispose(); foreach (Mat m in batch) m.Dispose(); batch.Clear(); } }这里每 16 张图组成一个批次,拼接后立即把 Mat 全部 Dispose 掉。Cv2.ImRead 读进来的 Mat 是非托管内存,GC 管不到,必须手动释放。漏掉 Dispose 的后果不是内存缓慢增长,而是进程内存持续高位,最终在拼接大图时突然抛异常。我一般在每个批次末尾统一用 foreach 释放,而不是在循环里处理完单张就释放,因为后者容易误伤还在引用中的 Mat。
3. 表单显示图像:Mat 转 Bitmap 与 PictureBox 渲染的关键链路
3.1 Mat 与 Bitmap 的相互转换
C# 的 WinForms 或 WPF 界面不能直接渲染 Mat,必须先把 Mat 转成 Bitmap。OpenCvSharp 提供了两个方向的扩展方法:Mat.ToBitmap() 和 BitmapConverter.ToMat()。这里最容易踩的坑是像素格式不匹配。OpenCvSharp 默认的 Mat 是 BGR 三通道,而 32 位 Bitmap 是 BGRA 四通道,直接转换时库会自动处理通道映射,但速度会慢一些。如果对性能敏感,可以先把 Mat 转成 ImreadModes.Unchanged 或自行 CvtColor 成 BGRA,再做零拷贝转换。
// 方式一:直接转换,适合显示用 Bitmap bmp = mat.ToBitmap(); // 方式二:先转 BGRA 再转 Bitmap,适合高频刷新场景 Mat bgraMat = new Mat(); Cv2.CvtColor(mat, bgraMat, ColorConversionCodes.BGR2BGRA); Bitmap fastBmp = bgraMat.ToBitmap();ToBitmap 方法内部已经处理了像素格式差异,不需要手动拷贝像素数据。但如果你的 Mat 是灰度图,直接 ToBitmap 会得到 8 位灰度 Bitmap,显示在 PictureBox 上没问题,可如果后续还要把这个 Bitmap 丢给别的控件或保存,最好统一用 CvtColor 转成 BGR 再接 Bitmap,避免下游控件不认识灰度格式。另一个注意点:ToBitmap 返回的 Bitmap 和 Mat 共享内存,Mat 被 Dispose 后 Bitmap 可能渲染出花屏或直接抛异常。所以显示的 Mat 不要急着释放,等 Bitmap 用完或 PictureBox 换图后再释放。
3.2 PictureBox 显示大图:缩放模式与内存抖动
把一张 8000×6000 的大图塞进 PictureBox,默认 SizeMode 是 Normal,控件会直接裁剪超出部分。通常的做法是设置 SizeMode 为 Zoom,让图片等比缩放适应控件区域,但 Zoom 模式的内部实现是用 GDI+ 做缩放,大图缩小时内存开销不小。更可控的做法是先在 Mat 层面做缩小,再转 Bitmap 显示。图片缩小时优先用 Cv2.Resize 配合 INTER_AREA 插值,这个插值算法对缩小图片的质量和速度平衡很好。
Mat displayMat; double scale = Math.Min(pictureBox1.Width / (double)mat.Cols, pictureBox1.Height / (double)mat.Rows); if (scale < 1.0) { Size newSize = new Size((int)(mat.Cols * scale), (int)(mat.Rows * scale)); displayMat = new Mat(); Cv2.Resize(mat, displayMat, newSize, 0, 0, InterpolationFlags.InterArea); } else { displayMat = mat.Clone(); } Bitmap displayBmp = displayMat.ToBitmap(); pictureBox1.Image?.Dispose(); pictureBox1.Image = displayBmp;这里计算缩放比例时用了 Math.Min,保证图片完全显示在 PictureBox 内且保持宽高比。scale 小于 1 才缩放,大于等于 1 时直接 Clone,避免把图放大导致模糊。pictureBox1.Image?.Dispose() 的作用是释放上一帧的 Bitmap,防止快速连续显示多张图时 Bitmap 对象堆积。实际处理中我发现很多内存问题不是 Mat 没释放,而是 PictureBox 的 Image 属性不断更换却没有 Dispose 旧图,最终 GDI 句柄耗尽。
3.3 表单布局:滚动容器与自动适配
单张图显示还好,当需要在界面上同时展示文件夹所有图片的预览时,一个 PictureBox 是不够的。我用的方案是 FlowLayoutPanel 配合自动生成的缩略图 PictureBox。每张图先 Resize 到 200×200 左右的缩略图 Mat,再转 Bitmap 放入 PictureBox,所有 PictureBox 添加到 FlowLayoutPanel 中横向排布。FlowLayoutPanel 开启 AutoScroll 属性后,图片多时会自动出现滚动条,不需要手工处理滚动逻辑。
foreach (string file in sortedFiles) { Mat img = Cv2.ImRead(file, ImreadModes.Color); if (img.Empty()) continue; Mat thumb = new Mat(); Cv2.Resize(img, thumb, new Size(200, 200), 0, 0, InterpolationFlags.InterArea); PictureBox pb = new PictureBox { Width = 200, Height = 200, SizeMode = PictureBoxSizeMode.Zoom, Tag = file }; pb.Image = thumb.ToBitmap(); flowLayoutPanel1.Controls.Add(pb); img.Dispose(); thumb.Dispose(); }缩略图采用固定 200×200 的正方形,SizeMode 用 Zoom 保证图片不变形。Tag 属性里存放原始文件路径,方便后续点击某个缩略图时,加载原始分辨率大图显示。这里有个容易忽略的点:pb.Image 赋值的 Bitmap 是从 thumb 转换来的,thumb 虽然立即 Dispose 了,但 Bitmap 内部已经做了像素拷贝,不依赖原始 Mat,所以显示不会出问题。缩略图 Resize 到 200×200 后,每张预览图占用的内存大约 160KB,200 张预览图也就 32MB,界面不会卡顿。
4. 多图拼接核心:HConcat 和 VConcat 的矩阵拼接组合
4.1 统一尺寸是拼接的前置条件
OpenCvSharp 提供了两个直接的拼接 API:Cv2.HConcat 将多张图按水平方向拼成一行,Cv2.VConcat 按垂直方向拼成一列。但这两个 API 有个硬性要求:参与拼接的所有 Mat 必须有相同的行数(HConcat)或相同的列数(VConcat)。所以“多图拼成网格”的核心步骤是先统一所有图片尺寸,再按行列分组拼接。这里说的统一尺寸不是简单 Resize 到同一分辨率,而是决定采用“拉伸填充”还是“等比例缩放 + 填充背景”。
public static Mat ResizeToFit(Mat src, int targetWidth, int targetHeight) { double scale = Math.Min(targetWidth / (double)src.Cols, targetHeight / (double)src.Rows); int newWidth = (int)(src.Cols * scale); int newHeight = (int)(src.Rows * scale); Mat resized = new Mat(); Cv2.Resize(src, resized, new Size(newWidth, newHeight), 0, 0, InterpolationFlags.InterArea); Mat canvas = new Mat(new Size(targetWidth, targetHeight), MatType.CV_8UC3, Scalar.Black); Rect roi = new Rect((targetWidth - newWidth) / 2, (targetHeight - newHeight) / 2, newWidth, newHeight); resized.CopyTo(canvas[roi]); resized.Dispose(); return canvas; }这段逻辑先按比例缩放图片到能完全放入目标区域的大小,然后创建纯黑画布,把缩放后的图片居中拷贝到画布上。黑边填充避免图片被拉伸变形,适合样本图的视觉一致性要求。如果你希望图片完全填满网格没有黑边,就直接 Resize 到 targetWidth×targetHeight,但那样原图比例失真的问题会很突出。两种方式在批处理场景里都能见到,我是偏向填充方案,因为变形对后续人工检查样本是个干扰项。
4.2 按行列分组拼接:从 Mat 列表到网格大图
统一尺寸后,拼接就变成纯矩阵操作了。假设你要拼一个 rows×cols 的网格图,做法是先把所有图按每 cols 个分成一行,每行内用 HConcat 拼成一行 Mat,然后把所有行 Mat 用 VConcat 拼成最终网格图。
public static Mat BuildGrid(List<Mat> images, int cols, int rows) { if (images.Count > cols * rows) throw new ArgumentException("图片数量超过网格容量"); int cellWidth = images.Max(m => m.Cols); int cellHeight = images.Max(m => m.Rows); List<Mat> resizedImages = images.Select(img => ResizeToFit(img, cellWidth, cellHeight)).ToList(); List<Mat> rowMats = new List<Mat>(); for (int i = 0; i < rows; i++) { List<Mat> rowPics = new List<Mat>(); for (int j = 0; j < cols; j++) { int idx = i * cols + j; if (idx < resizedImages.Count) rowPics.Add(resizedImages[idx]); else rowPics.Add(ResizeToFit(resizedImages[0], cellWidth, cellHeight)); // 补空白 } Mat rowMat = new Mat(); Cv2.HConcat(rowPics, rowMat); rowMats.Add(rowMat); } Mat gridMat = new Mat(); Cv2.VConcat(rowMats, gridMat); foreach (Mat m in resizedImages) m.Dispose(); foreach (Mat m in rowMats) m.Dispose(); return gridMat; }cellWidth 和 cellHeight 从所有图片中取最大值,保证每张图都按相同的画布尺寸统一,这样 HConcat 时所有图的 Mat 尺寸完全一致。rowMats 列表保存每一行拼接后的 Mat,行内图片数不足 cols 时,用第一张图重新缩放到相同尺寸补位,这样 VConcat 时每行的行高相同。最后释放 resizedImages 和 rowMats 的中间 Mat,返回的 gridMat 交由调用方负责 Dispose。
4.3 大量图片分块拼接:避免一次性拼接超大 Mat
如果图片数量达到几百张,直接按上述方式拼成一张网格图,结果 Mat 的尺寸会非常大。比如 100 张 1920×1080 的图按 10×10 网格拼接,最终图是 19200×10800 像素,占用约 600MB 内存,这还没算中间过程。常规做法是把大网格拆成多个小网格,分块拼接、分块保存,最后再用小网格图拼接成完整大图。
public static void BuildLargeGrid(List<string> files, string outputDir, int cols, int rows) { List<Mat> buffer = new List<Mat>(); int batchSize = cols * rows; for (int i = 0; i < files.Count; i += batchSize) { List<string> batchFiles = files.Skip(i).Take(batchSize).ToList(); List<Mat> batchImgs = batchFiles.Select(f => Cv2.ImRead(f, ImreadModes.Color)).ToList(); Mat grid = BuildGrid(batchImgs, cols, rows); string outPath = Path.Combine(outputDir, $"grid_{i / batchSize:D3}.png"); Cv2.ImWrite(outPath, grid); grid.Dispose(); foreach (Mat m in batchImgs) m.Dispose(); } }这个分批逻辑把文件列表按 batchSize 切块,每块拼成一个独立网格图并保存到输出目录。保存用 Cv2.ImWrite,这个方法根据文件扩展名自动判断编码格式,输出 PNG 或 JPG 都可以。分块拼接的额外好处是每张网格图都能单独查看,不需要等到全部处理完才看到结果。之后如果确实需要一张完整大图,可以把这些小网格图再按上述方式二次拼接,内存压力分摊到两个阶段。
5. 避坑笔记:批量读取与拼接路上最常踩的五个坑
5.1 中文路径导致 ImRead 返回空 Mat
现象:文件路径包含中文目录或文件名时,Cv2.ImRead 返回的 Mat 是 Empty,但同一路径用 File.Exists 检查却显示文件存在。原因:OpenCvSharp 底层调用原生 OpenCV 的文件读取函数,Windows 下对中文路径的 Unicode 支持不完整,ImRead 内部用的是窄字符编码。解决:先用 FileStream 把文件读入字节数组,再用 Cv2.ImDecode 从内存解码。
byte[] bytes = File.ReadAllBytes(filePath); Mat img = Cv2.ImDecode(bytes, ImreadModes.Color);ImDecode 的参数和 ImRead 一致,只是数据源从文件路径变成字节数组。这个方法绕过了路径编码问题,在中文 Windows 环境里是稳定方案。我一般涉及文件路径拼接时,也统一用 Path.Combine 而不是字符串直接相加,避免路径分隔符或末级目录带空格的问题。
5.2 图片数量小于网格容量时的索引越界
现象:20 张图要拼 4×5 的网格,但程序在填充最后一个网格空位时抛 IndexOutOfRangeException。原因:代码里直接用 images[i] 去取图,没判断当前索引是否超出 List 范围。解决:访问前判断 idx 是否小于 images.Count,否则创建空白 Mat。
Mat source; if (idx < images.Count) source = images[idx]; else source = images[0]; // 或 new Mat(...) 创建纯色图填充这样即使数量不足,网格布局也保持完整,不会出现某一行短一截导致后续拼接错位。补位用第一张图还是纯色图取决于业务需要,样本预览场景用第一张图复制比较直观,正式出图场景用纯色图更干净。
5.3 Mat 释放顺序错误导致 Bitmap 花屏
现象:PictureBox 显示的图像在一个刷新周期后变成花屏或丢失内容。原因:Mat 被提前 Dispose,而 Bitmap 底层内存和 Mat 的 data 指针指向同一块区域,Mat 释放后 Bitmap 的显示数据已被垃圾回收或覆盖。解决:确保 Mat 的生命周期覆盖 Bitmap 的使用周期,PictureBox 切换图片时,在设置新图后再释放上一帧的 Mat 和 Bitmap。
pictureBox1.Image?.Dispose(); pictureBox1.Image = displayBmp; // 此时再释放 displayMat,而不是在 Resize 后立即释放这条规则的判断标准是:只要 Bitmap 还在被 UI 控件持有引用,它对应的 Mat 就不能释放。稳妥做法是 Bitmap 用 ToBitmap 转换后立即释放 Mat,前提是 ToBitmap 内部做了数据拷贝而不是共享指针。不同版本的 OpenCvSharp 在这块的行为不完全一致,新版默认有拷贝逻辑,但依赖这个行为不是好习惯。
5.4 HConcat 报错:Mat 尺寸不一致
现象:执行 Cv2.HConcat 时抛出 OpenCVException:Sizes of input arguments do not match。原因:参与水平拼接的 Mat 行数不同,或者通道数不同。解决:拼接前统一 Resize 所有图片到相同的 (width, height),并用 Cv2.CvtColor 统一通道数。这一步最好在上一步 ResizeToFit 里就强制完成,利用 OpenCV 的断言机制提前发现异常。
// 验证所有 Mat 尺寸一致 bool allSame = resizedImages.All(m => m.Cols == cellWidth && m.Rows == cellHeight); if (!allSame) throw new InvalidOperationException("图片尺寸未统一,无法拼接");这个检查在图片数量很大时能省下不少排查时间。OpenCvSharp 的报错信息给出的是内部矩阵尺寸,不直接告诉你哪张图出了问题,自检逻辑能让错误信息更可读。
5.5 ImWrite 保存失败且无异常
现象:Cv2.ImWrite 返回 false,代码不抛异常,输出目录里没有生成文件。原因:输出目录不存在、路径过长、或输出路径是只读的。解决:调用 ImWrite 前用 Directory.CreateDirectory 确保目录存在,并用 Path.GetFullPath 检查最终路径是否超过 260 字符限制。
string fullDir = Path.GetFullPath(outputDir); Directory.CreateDirectory(fullDir); bool ok = Cv2.ImWrite(Path.Combine(fullDir, "output.png"), gridMat); if (!ok) Debug.WriteLine("保存失败: " + Path.Combine(fullDir, "output.png"));ImWrite 返回 false 时不会抛异常,程序会继续跑完整个流程,但你最后拿不到输出文件。加上返回值检查和目录预创建,是最基本的防御。路径长度问题在嵌套目录较深的情况下容易碰到,用 Path.GetFullPath 展开后检查 Length 是一个简单的提前拦截手段。
6. 让拼接工具再进一步:自动网格计算与缩略图缓存技巧
多图拼接的流程跑通之后,你会发现两个高频诉求:一是每次手动指定 cols 和 rows 很麻烦,能不能让程序自动计算网格布局;二是改动参数后重跑,几百张图的 Resize 和格式转换全部重来一次,耗时很长。这两个问题都可以在现有代码上做小改造解决。
先看自动网格计算。最稳妥的逻辑是给定图片总数和预期的单张网格图最大边长,反推行列数。比如目标是拼出的网格图宽度不超过 8000 像素,每张图统一为 640×480,那么 cols 最大值是 8000/640 = 12。rows 通过图片总数除以 cols 向上取整得出。如果 rows 和 cols 乘积大于图片总数,网格会有空缺位,正好用上一章的补位逻辑填充。
public static (int cols, int rows) CalcGridSize(int imageCount, int maxGridWidth, int cellWidth) { int cols = Math.Max(1, maxGridWidth / cellWidth); int rows = (int)Math.Ceiling(imageCount / (double)cols); return (cols, rows); }这个计算的边界情况是图片数量很少时,比如 3 张图,maxGridWidth/cellWidth 算出 cols 可能是 12,rows 就是 1,结果这 3 张图被横向拉伸到接近 8000 像素宽,明显不合理。所以实际约束条件里应该加上“cols 最大不超过图片数量”的限制,同时 rows 也不能无限小。常见的做法是预先定义几个候选布局(如 2×2、3×3、4×4、5×5),根据图片数量走到最小的那个行列值。
再看缩略图缓存。批量加载同一批图时,每次 Resize 到缩略图尺寸都要做一次插值计算,几百张图累计耗时可观。改进思路是第一次处理时把缩略图以 JPEG 格式写进缓存目录,文件名用“原始文件名 + _thumb.jpg”保存。第二次读取时先检查缓存文件是否存在,存在就直接 ImRead 缓存文件,跳过 Resize 步骤。因为缩略图本身已经很小,ImRead 的开销几乎可以忽略。
string cacheDir = Path.Combine(folderPath, ".thumb_cache"); Directory.CreateDirectory(cacheDir); foreach (string file in sortedFiles) { string cachePath = Path.Combine(cacheDir, Path.GetFileNameWithoutExtension(file) + "_thumb.jpg"); Mat thumb; if (File.Exists(cachePath)) { thumb = Cv2.ImRead(cachePath, ImreadModes.Color); } else { Mat img = Cv2.ImRead(file, ImreadModes.Color); thumb = new Mat(); Cv2.Resize(img, thumb, new Size(640, 480), 0, 0, InterpolationFlags.InterArea); Cv2.ImWrite(cachePath, thumb); img.Dispose(); } // thumb 加入拼接列表 }缓存目录放在源文件夹下,用隐藏前缀命名避免混淆。JPEG 格式的缩略图有损压缩,如果后续拼接结果要用于精确测量,缓存方案不适用;但用于界面预览和快速拼图完全够用。缓存文件生成后,即使源文件被删除,缩略图依然可用,这对反复调试布局参数很有帮助。从那次做批量处理工具之后,我每次写类似程序前都会先想“这次跑完还要不要重跑”,是就不再重复裁剪,把中间产物落地成文件。这个习惯让我在调整拼接参数时省了大量等待时间,希望这篇笔记也能帮你少走几步弯路。
本文还有配套的精品资源,点击获取