简介:本资源是一个基于OpenCvSharp的C#图像滤镜开发实战项目,面向图像处理初学者、计算机视觉入门开发者及.NET平台图像应用开发者,解决常见视觉效果编程实现问题。项目完整实现了饱和度、明度、对比度、锐化、阴影、高光与色温七大核心滤镜功能,覆盖摄影调色与UI图像增强等典型应用场景。压缩包共72个文件,包含11个C#源码文件(含主窗体Form1.cs及设计器)、1个Visual Studio解决方案(.sln)与1个项目配置文件(.csproj),辅以9个运行依赖DLL、8张测试图像(jpg)、6个配置文件(config)及5个可执行程序(exe),整体大小35.6MB。已有384人学习下载,提供开箱即用的编译环境与结构清晰的工程组织——源码模块分离、资源与设置独立管理、测试图内置,便于快速理解OpenCvSharp图像通道操作、色彩空间转换与卷积增强逻辑,并可直接扩展为图像编辑器基础框架。 做图像处理这行,手机里永远少不了几张家人的老照片。扫描进电脑后发现,偏色、发灰、暗部死黑,用Photoshop一点点拉曲线不是不行,可几十张图一张张修,耐心很快就耗光了。于是我用OpenCvSharp写了个小Demo,把饱和度、明度、对比度、锐化、阴影、高光、色温这七项参数全部做成可实时调节的滑块,跑起来之后还能批量出图。打开那个.rar工程,你会看到一套非常典型的滤镜处理流程,今天就把里面的实现思路、代码细节和踩过的坑一起梳理出来。
这个Demo适合两类人:一类是刚接触OpenCvSharp的C#开发者,想看看图像滤镜到底怎么落地;另一类是经常做照片批处理、需要把PS里的调色动作转成代码的人。它不是那种只能跑一下的玩具Demo,而是能直接扩展成小工具的基础框架。
1. 这个RAR包里到底装了什么
1.1 为什么我会写这样一个滤镜Demo
我最初的需求很简单:把一批扫描出来的旧照片统一做一次校正。它们在扫描过程中普遍存在饱和度偏低、暗部死黑、高光发闷的问题。虽然Lightroom也能批量处理,但公司的生产环境中偏要用C#做自动化,所以必须在OpenCvSharp这边找解决方案。
试了一圈发现,OpenCvSharp的API很适合做滤镜,因为它把OpenCV的底层能力完整暴露给了C#。转色彩空间、卷积滤波、查找表映射,这些操作全部有现成函数。只是网上能搜到的中文资料大多是零散片段,要么只讲一种滤镜,要么就是把像素级for循环贴出来给你看。所以这个Demo里我把几个常用滤镜集中在一起,做成了带参数的形式,方便后续二次开发。
1.2 Demo的模块划分
解压.rar之后,工程结构大概是这样的:一个主窗体负责加载图片和调节参数,一个滤镜引擎类封装所有的图像处理算法,再有一些示例图片用来测试。
主窗体上放了七组TrackBar,分别对应饱和度、明度、对比度、锐化、阴影、高光、色温。用户拖动滑块,当前帧立即重算并显示在PictureBox上。这个交互看起来简单,但背后的数据流是完整的:
- 原始图片加载成OpenCvSharp的Mat对象,始终保留一份原图。
- 每次参数变化,从原图克隆出一份工作副本。
- 工作副本按顺序经过各滤镜处理。
- 处理后的Mat转成Bitmap显示出来。
这个流程保证了参数调乱之后,只要松开滑块或点击重置,画面还能回到原始状态,不会因为连续处理导致质量损耗。
1.3 滤镜清单与典型使用场景
这七个滤镜不是随便凑的。它们在修图软件里几乎对应着“基础校正”的所有环节。
| 滤镜 | 作用 | 适用场景 |
|---|---|---|
| 饱和度 | 增强或降低颜色鲜艳度 | 风景照、美食照 |
| 明度 | 整体提亮或压暗 | 曝光不准、氛围调整 |
| 对比度 | 强化明暗差异 | 发灰的照片、雾感画面 |
| 锐化 | 增强边缘细节 | 照片变软、扫描件 |
| 阴影 | 提亮暗部细节 | 逆光照片、衣服暗部 |
| 高光 | 压缩过亮区域 | 天空过曝、水面反光 |
| 色温 | 整体偏暖或偏冷 | 白平衡不准、风格化 |
实际使用中,这些滤镜往往要组合起来。比如一张逆光人像,先提阴影找回面部细节,再压一点高光让背景不过曝,然后适当增加饱和度,最后做轻微锐化。这套流程在Demo里只要拖动四个滑块就能复现。
2. 饱和度与明度调整:先解决颜色空间的问题
2.1 RGB直接加减为什么不行
很多新手拿到图片,第一反应是直接对RGB三个通道做加法或乘法。比如“整体提亮”就是每个像素的R、G、B都加50,“增强饱和度”就是让每个通道偏离灰度值更多一点。
这样做确实能出效果,但非常容易出岔子。整体提亮时,纯蓝天空和肤色都在变亮,但纯蓝色通道的数值已经接近顶格,继续加就会出现肉眼可见的颜色断层。增强饱和度时,如果对RGB三个通道分别做不同倍率的乘法,颜色会发生奇怪的偏移,红色可能变成橙色,绿色可能变成荧光绿。
根本原因在于RGB不是为人眼感知而设计的色彩空间。它只是显示器的物理输出模型。对人的视觉来说,“饱和度”和“明度”是相对独立的感知维度,在RGB里却纠缠在一起。所以调整饱和度、明度时,最好先把图像转换到HSV或HSL这类色彩空间。
2.2 HSV/HSL色彩空间的基本逻辑
HSV把颜色拆成三个分量:H是色相,S是饱和度,V是明度。打开任何一张彩色图片,在HSV空间观察三个通道就能发现:
- H通道保存的是“这是什么颜色”,红色和黄色在这里有明显区分。
- S通道保存的是“颜色有多纯”,灰色区域接近0,纯红接近255。
- V通道保存的是“光有多强”,它接近灰度图。
这就把颜色和亮度解耦了。调整明度时只动V通道,不会影响H和S;调整饱和度时只动S通道,颜色本身和明暗都不会发生剧烈偏转。
顺带一提,OpenCV的HSV和普通人理解的HSV在数值范围上有差异。OpenCvSharp中,H的范围是0到180,S和V的范围是0到255。这与图像处理软件里常见H为0到360、S和V为0到100是不同的,初用者最容易在这里被坑。
2.3 OpenCvSharp代码实现饱和度与明度调整
核心流程分四步:BGR转HSV、拆分通道、调整S和V、合并通道转回BGR。
private Mat AdjustSaturationAndBrightness(Mat src, double saturation, double brightness) { Mat hsv = new Mat(); Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV); Mat[] channels = Cv2.Split(hsv); Mat s = channels[1]; Mat v = channels[2]; // 饱和度,用ConvertTo做矩阵乘法,目标类型CV_8U会自动饱和截断 Mat sAdjusted = new Mat(); s.ConvertTo(sAdjusted, MatType.CV_8U, saturation, 0); // 明度,用ConvertTo做偏移,brightness范围建议-100~100 Mat vAdjusted = new Mat(); v.ConvertTo(vAdjusted, MatType.CV_8U, 1.0, brightness); channels[1] = sAdjusted; channels[2] = vAdjusted; Mat result = new Mat(); Cv2.Merge(channels, result); Cv2.CvtColor(result, result, ColorConversionCodes.HSV2BGR); hsv.Dispose(); foreach (Mat ch in channels) ch.Dispose(); return result; }这里有一个关键点:ConvertTo的第一个参数alpha是倍率,第二个参数beta是偏移。目标类型如果是CV_8U,OpenCV会自动把小于0的值截成0,大于255的值截成255。所以即使saturation设为2.5,饱和度过曝也不会溢出,只会让颜色到一个“很浓但仍在显示范围”的状态。
2.4 参数范围和H通道的坑
我试下来,饱和度的合理范围是0.6到1.6。低于0.6画面会明显发灰,高于1.6开始出现色块断层,尤其是肤色区域,容易变成蜡像感。明度的合理范围是-80到80。超过正负100时,暗部或亮部会大量丢失细节。
还有一个极易踩的坑:HSV转回BGR时,如果H通道不小心被修改成大于180或小于0,转出来的颜色会是乱的。所以在Demo中,H通道始终不做任何运算,只原样传递。如果你想让某种颜色整体偏向另一种色相,也不要直接对H通道做加减,而是利用Cv2.Add配合CoreOperations.Wrap这种带环绕的加法,否则H=178红色加30会变成负数,得到预料之外的颜色。我的Demo里没有开放H通道,就是为了避免这个复杂问题。
3. 对比度和锐化:一个管层次,一个管纹理
3.1 对比度的本质与线性变换
对比度在数学上就是让亮的地方更亮、暗的地方更暗。最简单的模型是线性变换:
dst(x, y) = alpha * src(x, y) + beta当alpha大于1时,像素值之间的差距被放大,对比度提升;当alpha小于1时,像素值向中间值靠拢,画面变灰。beta是整体亮度偏移,可以让曲线上下平移。
OpenCvSharp中不需要自己写循环,用ConvertTo一行就能完成。比如把对比度范围设为0到100,映射到alpha为0.8到1.8:
private Mat AdjustContrast(Mat src, double contrast) { double alpha = 0.8 + contrast / 100.0; double beta = 0; Mat result = new Mat(); src.ConvertTo(result, MatType.CV_8UC3, alpha, beta); return result; }CV_8UC3是3通道8位图像类型。这里同样利用ConvertTo的饱和截断特性,超出阈值的像素不会被扭成奇怪的颜色,而是被限制在正常范围。
3.2 锐化为什么要用卷积,为什么不直接用Photoshop的USM
对比度调整的是大范围亮度差异,锐化调整的是局部边缘。经典锐化算法有两种:卷积核和USM。
卷积核方式就是用一个3x3矩阵扫描图像。常见的拉普拉斯锐化核长这样:
0 -1 0 -1 5 -1 0 -1 0这个核的中心权重是5,四周是-1。它检测到“当前像素和周围像素差异较大”时,会把差异放大,所以边缘会变得更陡峭。实际效果是照片看起来“清晰”了,但代价是噪点也被同等放大。
USM(Unsharp Mask)则是先对原图做高斯模糊,再用原图减去模糊结果得到边缘遮罩,最后把遮罩叠回原图。公式上相当于:
result = (1 + amount) * 原图 - amount * 模糊图USM能控制锐化作用的半径和阈值,比直接拉普拉斯核更不容易出现白色光晕。Photoshop里的智能锐化就是这个思路。在OpenCvSharp中实现USM只需要两个函数,代码也不复杂。
3.3 代码实现:Filter2D和AddWeighted的组合
如果只想快速出效果,用拉普拉斯核最直接:
private Mat SharpenKernel(Mat src, double amount) { if (amount <= 0) return src.Clone(); float[] kernelData = { 0f, (float)-amount, 0f, (float)-amount, 1f + 4f * (float)amount, (float)-amount, 0f, (float)-amount, 0f }; Mat kernel = new Mat(3, 3, MatType.CV_32FC1, kernelData); Mat result = new Mat(); Cv2.Filter2D(src, result, src.Type(), kernel); return result; }这段代码中amount相当于锐化强度,范围为0到1。当amount=0.5时,中心权重为3,四周为-0.5,边缘增强效果已经很明显。Filter2D是OpenCV的图像卷积基础API,它可以在任意尺寸的核上工作,所以后续如果要做模糊、浮雕、边缘检测等效果,都可以复用这一段逻辑。
USM版本更适合实际照片:
private Mat SharpenUsm(Mat src, double amount) { if (amount <= 0) return src.Clone(); Mat blurred = new Mat(); Cv2.GaussianBlur(src, blurred, new Size(0, 0), 3); Mat result = new Mat(); Cv2.AddWeighted(src, 1 + amount, blurred, -amount, 0, result); return result; }GaussianBlur的第三个参数new Size(0, 0)表示不指定核大小,只根据sigma计算。sigma设为3时,模糊半径适中,边缘遮罩能保留更多细节。AddWeighted把原图和模糊图做一个带权重的叠加,amount越大边缘越犀利。
我的经验是:风景照锐化amount用0.3到0.5,人像慎用大于0.3的锐化,否则皮肤毛孔和瑕疵会被无差别放大,反而显得脏。
3.4 强度过猛时画面会出现什么
这两个滤镜都容易做过头。
对比度提高到一定程度,直方图两端会堆得非常满,画面变成一片死白加一片死黑,中间层次全丢。锐化过猛就更明显了,物体边缘会出现一圈白边,尤其是天空和山体交界的地方,俗称“光晕”。
出现光晕的时候,先别急着降低amount,试试增加高斯模糊的sigma。sigma越大,锐化作用的范围越广,边缘过渡会更柔和,不容易在单像素边界上形成白线。这个思路同样适用于后面要做的阴影高光处理:很多调色副作用都不是因为“强度”错了,而是因为“作用范围”没有控制好。
4. 阴影与高光:比想象中更依赖LUT
4.1 阴影高光不是简单的亮度偏移
阴影和高光是修图中最微妙的一组参数。许多人误以为提亮阴影就是给暗部像素统一加一个固定亮度值,压缩高光就是给亮部像素统一减一个值。如果真这么做,结果会很糟糕:暗部像蒙了一层灰雾,亮部直接糊成一片。
原因在于人眼对暗部和亮部的敏感度不同。暗部细节藏在微弱的亮度差异里,只做线性偏移会把那些微差一起推出去,造成“灰”。高光区域一旦损失细节,再低的强度也会显得塑料感很强。所以阴影高光必须使用非线性映射。
4.2 用LUT做分段映射
查找表(LUT)是图像处理中非常高效的思路:预先算好一张输入亮度到输出亮度的映射表,然后对每个像素查表替换。因为亮度只有256个可能值,所以LUT可以做到非常平滑。
阴影调整的核心思路:把亮度在0到128之间的区域,按一定系数向中间值方向拉伸。高光调整的核心思路:把亮度在128到255之间的区域,按一定系数向中间值方向压缩。
我这里用的公式很简单,但效果已经能打:
private byte[] BuildShadowHighlightLut(float shadow, float highlight) { float shadowStrength = Math.Max(0, Math.Min(1, shadow / 100f)); float highlightStrength = Math.Max(0, Math.Min(1, highlight / 100f)); byte[] lut = new byte[256]; for (int i = 0; i < 256; i++) { float p = i / 255f; float adjusted = p; if (p < 0.5f) { adjusted = p + (0.5f - p) * shadowStrength * 0.8f; } else if (p > 0.5f) { adjusted = p - (p - 0.5f) * highlightStrength * 0.8f; } adjusted = adjusted * 0.85f + p * 0.15f; lut[i] = (byte)(Math.Max(0f, Math.Min(1f, adjusted)) * 255f + 0.5f); } return lut; }这里的0.8和0.15是经验系数。0.8决定阴影和高光的最大回拉幅度,0.15则让整条曲线不至于完全脱离原始亮度,避免画面对比度被过度压缩。如果你希望阴影和高光的影响范围更聚焦,可以把0.5这个分割点换成带线性过渡的混合区间。
4.3 C#端LUT生成与Cv2.LUT的使用
OpenCvSharp提供了Cv2.LUT,可以直接把LUT应用到图像上。不过要对亮度而不是RGB通道做LUT,最安全的方式是转到HSV,只改V通道:
private Mat AdjustShadowHighlight(Mat src, float shadow, float highlight) { Mat hsv = new Mat(); Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV); Mat[] channels = Cv2.Split(hsv); Mat v = channels[2]; byte[] lutData = BuildShadowHighlightLut(shadow, highlight); Mat lutMat = new Mat(256, 1, MatType.CV_8UC1); Marshal.Copy(lutData, 0, lutMat.DataPointer, 256); Mat vAdjusted = new Mat(); Cv2.LUT(v, lutMat, vAdjusted); channels[2] = vAdjusted; Mat result = new Mat(); Cv2.Merge(channels, result); Cv2.CvtColor(result, result, ColorConversionCodes.HSV2BGR); hsv.Dispose(); lutMat.Dispose(); foreach (Mat ch in channels) ch.Dispose(); return result; }代码里Marshal.Copy会把byte数组拷贝到Mat的内存中。必须确保数组长度和Mat尺寸一致,否则会越界。这里lutMat是256行1列,每个像素一个字节,正好对应0到255的亮度输入。
如果我直接把LUT应用到BGR三个通道上,画面的色彩饱和度和色相都会微调,尤其是红色和蓝色区域的偏移肉眼可辨。转到HSV后只动V通道,是更稳妥的做法。
4.4 怎么避免画面发灰和光晕
阴影高光参数调得太高,最典型的副作用就是画面发灰。这是因为阴影提亮的同时,暗部像素向中间调移动,原本对比鲜明的暗部层次被压缩了。要缓解这个问题,我通常会在阴影高光之后紧跟着做一次对比度补偿,把整体对比度提回来一点:
result = AdjustShadowHighlight(result, shadow, highlight); result = AdjustContrast(result, 10);这里的10是轻微对比度增益,只为了找回因为阴影提亮而损失的通透感。另一个常见问题是高光压缩后,亮部边缘出现一圈暗边。这种光晕主要出现在高光区域和阴影区域交界处,比如天空贴着树梢的位置。解决方法是降低高光强度,而不是改模糊半径。因为高光本就应该针对大面积过曝区域,太激进反而会让边缘过渡变得不自然。
5. 色温调整:用通道增益模拟白平衡偏移
5.1 色温数值与RGB通道的关系
色温这个概念听起来玄学,落实到RGB图像上其实很朴素。色温低,就是画面偏暖偏黄,也就是红色通道和绿色通道的权重相对更高,蓝色通道相对更低。色温高,就是画面偏冷偏蓝,蓝色通道权重相对提升。
所以调整色温不需要转色彩空间,只需要按比例缩放R和B通道的增益。G通道可以保持不变,因为人眼对绿色不敏感,而且绿色通道同时参与提亮和降噪,动它容易让肤色变得很奇怪。常见的白平衡算法里也只会对R和B通道做增益校正。
5.2 通过Split、ConvertTo、Merge三步实现
我的Demo里色温参数范围是-100到100。正数表示偏暖,负数表示偏冷。换算成通道增益时,我用一个线性映射:
private Mat AdjustTemperature(Mat src, double temperature) { if (temperature == 0) return src.Clone(); double redGain = 1.0 + temperature / 100.0 * 0.25; double blueGain = 1.0 - temperature / 100.0 * 0.25; Mat[] channels = Cv2.Split(src); // B, G, R顺序 Mat rAdjusted = new Mat(); Mat bAdjusted = new Mat(); channels[2].ConvertTo(rAdjusted, MatType.CV_8UC1, redGain, 0); channels[0].ConvertTo(bAdjusted, MatType.CV_8UC1, blueGain, 0); Mat result = new Mat(); Cv2.Merge(new Mat[] { bAdjusted, channels[1], rAdjusted }, result); return result; }这里temperature=100时,红色通道增益为1.25,蓝色通道增益为0.75。画面会明显偏暖,适合模拟落日氛围。temperature=-100时,蓝色增益1.25,红色增益0.75,画面偏冷,适合夜景或忧郁风格。
Cv2.Split输出的顺序是B、G、R,因为OpenCV内部默认使用BGR。这个顺序问题几乎每隔一段时间就会在群里看到有人踩坑:把三个通道当成RGB去合并,结果整张图的红色和蓝色对调,问题还很难一眼看出。所以我在代码里特意加了注释。
5.3 多个滤镜叠加时的顺序问题
Demo里滤镜的执行顺序,我一开始是乱序调的,后来发现效果差别很大。经过多轮对比,最终固定为:
- 饱和度与明度
- 色温
- 阴影与高光
- 对比度
- 锐化
这个顺序遵循“先调颜色,再调明暗,最后做纹理增强”的原则。
先调饱和度和明度,相当于把一个偏暗偏灰的图片基础打好。再调色温,解决白平衡问题。阴影高光和对比度都是对亮度结构的调整,放在一起处理。锐化一定是最后一步,因为前面几步可能产生模糊或噪点,锐化可以对最终结果做一次细节加强,同时不会把中间处理的伪影再次放大。
如果你把对比度放在阴影高光之前,阴影提亮之后对比度可能会变得过高,之前压下来的高光又被拉回去。如果你把锐化放在色温前面,色温调整时导致的通道缩放会放大锐化产生的细小噪点,画质会更粗糙。这个顺序不是绝对标准,但很适合作为一个可复现的默认方案。
6. 从Demo到能用的工具:性能优化与常见坑
6.1 千万别在UI线程里做像素级遍历
最初版本里,我为了追求“每个像素都能精确控制”,在C#里直接写了这样的代码:
for (int y = 0; y < src.Rows; y++) { for (int x = 0; x < src.Cols; x++) { Vec3b pixel = src.Get<Vec3b>(y, x); pixel[0] = ... src.Set<Vec3b>(y, x, pixel); } }这段代码在一张1920x1080的图片上跑一次,耗时能到几百毫秒。如果主窗体里拖动滑块,每次变化都触发一次全图遍历,图片会明显卡顿,CPU占用直接拉满。原因很直接:C#的Get<Vec3b>和Set<Vec3b>是逐像素调用的,每次都要做Mat的边界判断和类型转换,开销是内置函数的上百倍。
改进的方向不是优化for循环本身,而是尽量用OpenCV内置的矩阵运算替代像素级访问。
6.2 用内置函数代替手工循环
前文代码里的所有滤镜,没有一个需要手动遍历像素。ConvertTo、Cv2.LUT、Cv2.Filter2D、Cv2.AddWeighted都是基于底层C++实现的,内部有SIMD优化,比C#循环快一个数量级。
如果你真的需要做像素级自定义逻辑,至少要在非UI线程里跑,或者用Parallel.For配合unsafe指针。OpenCvSharp的Mat.Indexer<Vec3b>提供了相对快的像素访问方式,但依然比内置函数慢。我在Demo里对“锐化强度”做了测试:同样的USM效果,用Cv2.GaussianBlur加Cv2.AddWeighted耗时大约15毫秒,用纯C#像素级遍历加自定义卷积差不多要500毫秒,差距超过30倍。
所以第三版Demo里,所有滤镜都只依赖OpenCvSharp内置API。这样不仅代码清爽,性能也在可控范围内。即使是4K原图,整套流程跑下来也能在100毫秒左右完成,足够做实时预览。
6.3 Mat与Bitmap互转以及内存释放
OpenCvSharp在WinForms里显示图片时,需要把Mat转成Bitmap:
using Mat mat = ProcessImage(srcMat); using Bitmap bmp = BitmapConverter.ToBitmap(mat); pictureBox.Image?.Dispose(); pictureBox.Image = (Bitmap)bmp.Clone();这里有个隐藏问题:BitmapConverter.ToBitmap(mat)会创建一个和Mat共享内存的Bitmap。如果这个Bitmap被PictureBox持有,而Mat已经被Dispose,再刷新画面时会出现“内存损坏”导致程序崩溃。稳妥的做法是用Clone()复制一份Bitmap,再让PictureBox持有这个克隆对象。代价是多了一次内存拷贝,但换来了稳定性。
Mat本身也实现了IDisposable。在滤镜方法里,我用了很多临时Mat。每个方法结束时都要调用Dispose。如果创建太多临时Mat而不释放,长时间跑批量处理时,内存占用会持续上涨,最终触发OutOfMemoryException。
缩小Mat的作用域,或者在整个接口外统一处理,是管理内存的关键。你可以在方法内部用using语句包裹Mat变量,但这会让代码缩进变得很深。我在Demo里选择在方法末尾集中Dispose,同时重写了一个小工具方法,参数传入Mat时立刻Clone一份,避免外部内容被修改。
6.4 保存时的图像质量设置
处理完的照片最终要输出,OpenCvSharp的Cv2.ImWrite在保存JPG时,默认质量是92。如果你需要更高画质,要显式设置编码参数:
ImageEncodingParam[] jpegParams = { new ImageEncodingParam(ImwriteFlags.JpegQuality, 95) }; Cv2.ImWrite("output.jpg", result, jpegParams);ImwriteFlags.JpegQuality的取值范围是0到100,95比默认值略有提升,肉眼可能看不出差距,但批量出图后放到大屏上,暗部的色块会干净一些。
保存PNG时没有质量损失,但文件体积会大不少。如果处理的是存档级照片,我建议PNG输出;如果只是做网络分享,JPG质量85到90就够,再高只会增大文件体积,观感提升有限。
另一个容易被忽略的坑是:Cv2.ImWrite在路径包含中文时,Windows下偶发写不进去的情况,因为OpenCV底层对中文路径的支持不完善。我建议实际项目中传入英文路径,或者先把Mat转成Bitmap,再用Bitmap.Save处理中文路径。
回到开头说的那批旧照片,现在用这个Demo处理一遍,整个过程不会超过三分钟。对我来说,这个项目最大的收获不是那七种滤镜,而是理解了滤镜效果背后的几个通用思路:色彩空间决定你能调整什么,查找表决定曲线质量,内置函数决定性能。接下来如果再遇到新的修图需求,比如复古胶片风、电影感青橙调,都可以在这个Demo的基础上加LUT或调色矩阵。我个人还有个习惯:正式处理4K原图前,先缩小到1/4尺寸预览参数,确定效果后再跑全尺寸。这样既能快速试错,又能避免反复等待,你可以试试。
本文还有配套的精品资源,点击获取