☰
OpenCvSharp图像滤镜实践:饱和度、明度、对比度、锐化、阴影、高光、色温实现详解
2026/10/1 1:47:14 网站建设 项目流程

简介:OpenCvSharp 图像滤镜效果演示工程,面向 C# 开发者与图像处理入门者,提供基于 OpenCV 封装库的完整 Visual Studio 解决方案。工程以窗体程序为载体,集成了饱和度、明度、对比度、锐化、阴影、高光、色温等常见滤镜调整,既能直接观察图像变化,也能帮助理解各视觉属性背后的算法与参数调节思路。压缩包共 72 个文件,容量约 35.6MB,包含 cs 源码、dll 运行库、jpg 测试素材、sln/csproj 工程配置、exe 可执行文件以及调试信息等;目录区分 Debug/Release 与 x64/x86 构建配置,便于在 Visual Studio 中打开后直接编译和运行。工程还附带 Windows 窗体界面和多种测试图片,读者可对照源码分析每种滤镜的处理流程,也能修改参数观察不同强度效果,以加深对颜色空间、对比度拉伸、边缘增强和色温模拟等方法的理解。目前已有 386 人学习下载,适合希望快速上手 OpenCvSharp 滤镜开发的初中级开发者参考。

1. 这个 OpenCvSharp 滤镜 Demo 到底在解决什么问题

如果你手头有一份名为“饱和度、明度、对比度、锐化、阴影、高光、色温实现滤镜效果”的 OpenCvSharp Demo,第一反应通常是解压、打开工程、按 F5 看效果。但真在 C# 里做过图像处理的人都知道,这七个词背后是两套完全不同的数学:前四个落在色彩空间转换和像素运算上,后三个要靠亮度曲线和掩膜来分离区域。这篇笔记就是把这类 Demo 背后的实现拆开讲清楚:每个滤镜用什么函数、参数怎么设、为什么有些参数一调大就翻车,以及怎么把这套逻辑搬进自己的项目而不是停留在 Demo 演示。适合已经能跑 OpenCvSharp 基础示例、想把这七类滤镜效果当工具用的 C# 开发者。

2. 从 rar 到可跑工程:OpenCvSharp 环境、图像输入与 Bitmap 显示

拿到一个压缩包,第一步不是改算法,而是先把环境跑通。这类滤镜 Demo 最常见的组织方式是:一个 WinForm 或 WPF 工程,界面上放几个 TrackBar 对应饱和度、明度、对比度、锐化、阴影、高光、色温,中间放 PictureBox 显示结果图,核心逻辑集中在几个静态方法里。你直接 F5 大概率能跑,但要把它变成自己能改的项目,得先弄清楚三件事:NuGet 包里哪些 dll 必须存在、图片是怎么读成 Mat 的、以及 Mat 怎么转成 Bitmap 显示在界面上。

2.1 NuGet 包怎么选:OpenCvSharp4 与运行时包的坑

新建一个 .NET 项目后,先用命令行或 NuGet 管理器装上这两个包:

dotnet add package OpenCvSharp4 dotnet add package OpenCvSharp4.runtime.win

OpenCvSharp4 是托管程序集,提供 Cv2、Mat 这些你在代码里直接用的类型;OpenCvSharp4.runtime.win 是 Windows 原生库,里面装着 opencv_world 系列的 dll。如果只装前者,编译能过,一运行就报 DllNotFoundException,这是这类 Demo 最常见的启动失败原因。

还有一点要注意:如果你要把程序发给没有安装 Visual C++ 运行库的机器,直接换用 OpenCvSharp4.runtime.win.selfcontained 这个包,它把运行库一起打进去,省掉目标机器上的环境配置。我自己在写工具类程序时一般直接上 selfcontained,省得每次换电脑都跟运行库较劲。另外,压缩包里的 Demo 如果自带 dll,先确认它的版本和你 NuGet 引用的版本一致,混用会出现 OpenCV 版本不匹配的崩溃,报错信息还特别隐晦,只说找不到某个符号。

2.2 加载图片的职责分开:ImRead、Mat 与 PictureBox 显示

图片读取的职责很单一:用 Cv2.ImRead 把文件读成一个 Mat,后面所有滤镜都只操作这个 Mat。核心代码是这个样子:

string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "imgs", "sample.jpg"); Mat src = Cv2.ImRead(path, ImReadModes.Color); if (src.Empty()) { // 图片没读进来,多半是路径问题,先检查 imgs 目录有没有被复制到输出目录 return; } // 用一个静态方法链处理,每一层都返回新 Mat,方便单独调试 Mat result = FilterPipeline.Apply(src, options);

这里的 ImReadModes.Color 表示读成三通道彩色图,注意 OpenCV 读进来的通道顺序是 BGR 而不是 RGB。这意味着你后续写任何通道操作时,channels[0] 是蓝色、channels[2] 是红色。很多人把饱和度滤镜做成 YCrCb 之后又转回 BGR,发现颜色不对,多半是这一步把通道顺序想反了。

Mat 本身不直接支持 WinForm 的 PictureBox 显示,PictureBox.Image 需要的是 Bitmap 或 Image 类型。所以滤镜处理完的 Mat 还需要转一次。这也是不少 Demo 代码里最容易出问题的地方,下一节把转换写完整。

2.3 把 Mat 转成 Bitmap 显示,别被 Stride 坑到

一个常见的错误写法是把 Mat.Data 指针直接拷给 Bitmap 的 Scan0,再用 Buffer.BlockCopy 一次性复制 Mat.Step() * Mat.Height 字节。这样在绝大多数图片上会花屏,原因是 Bitmap 每行字节数会按 4 字节对齐,而 Mat 的 Step 是紧凑排布,两者往往不相等。

private Bitmap MatToBitmap(Mat mat) { var bmp = new Bitmap(mat.Width, mat.Height, PixelFormat.Format24bppRgb); var rect = new Rectangle(0, 0, mat.Width, mat.Height); var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); int srcStride = (int)mat.Step(); // Mat 每行实际字节数 int dstStride = bmpData.Stride; // Bitmap 每行按 4 字节对齐后的字节数 unsafe { byte* srcPtr = (byte*)mat.Data; byte* dstPtr = (byte*)bmpData.Scan0; for (int y = 0; y < mat.Height; y++) { // 逐行复制,目标行长度对齐后可能比源行长,多出来的字节保留 0 即可 Buffer.MemoryCopy( srcPtr + y * srcStride, dstPtr + y * dstStride, dstStride, Math.Min(srcStride, dstStride)); } } bmp.UnlockBits(bmpData); return bmp; }

这段代码的核心是逐行复制而不是整块复制。dstStride 可能比 srcStride 大,比如宽度为 10 的 24 位图,Mat 每行 30 字节,Bitmap 每行会被对齐成 32 字节,如果按 30 字节整体搬,后面的行就会错位,画面上出现一条条斜向的色带。还有个细节:这函数用到了 unsafe,项目文件里要打开 AllowUnsafeBlocks,否则编译直接失败。

3. 色彩类滤镜的落地写法:饱和度、明度、对比度、色温

色彩类滤镜全部围绕“像素值映射”来展开。它们的差异在于映射作用的通道不同:饱和度动的是色度分量,明度动的是亮度分量,对比度动的是所有通道的相对关系,色温动的则是 RGB 三通道的增益比例。把这四个写明白,剩下阴影高光和锐化反而更好理解。

3.1 YCrCb 饱和度:以 128 为基准的缩放,别在 BGR 上直接搞

饱和度不能直接对 BGR 三通道乘同一个系数,那样得到的是亮度变化而不是颜色浓淡。常见做法是把图从 BGR 转到 YCrCb,Y 代表亮度,Cr 和 Cb 代表色度,然后对 Cr、Cb 做围绕 128 的缩放。

public Mat AdjustSaturation(Mat src, double factor) { // 转到 YCrCb:Y 是亮度,Cr/Cb 是色度,适合单独调饱和度 Mat ycc = new Mat(); Cv2.CvtColor(src, ycc, ColorConversionCodes.BGR2YCrCb); Mat[] channels = Cv2.Split(ycc); Mat cb = channels[1]; Mat cr = channels[2]; Mat cbAdjusted = new Mat(); Mat crAdjusted = new Mat(); // 等价于 (value - 128) * factor + 128,128 是色度中性点 cb.ConvertTo(cbAdjusted, MatType.CV_8U, factor, 128 * (1 - factor)); cr.ConvertTo(crAdjusted, MatType.CV_8U, factor, 128 * (1 - factor)); channels[1] = cbAdjusted; channels[2] = crAdjusted; Cv2.Merge(channels, ycc); Mat dst = new Mat(); Cv2.CvtColor(ycc, dst, ColorConversionCodes.YCrCb2BGR); return dst; }

参数 factor 取 1.0 时图像不变,1.2 到 1.5 之间适合日常提鲜,超过 2.0 后很多颜色会被截断到 0 或 255,画面上出现一块块色斑。这个滤镜效果的关键在于 ConvertTo 里 beta 的写法:128 * (1 - factor)保证 128 这个灰度中间值原地不动,只有偏离中性色的像素才被放大。如果不写 beta 直接乘 factor,整张图的色度会整体偏移,肤色立刻偏紫。

3.2 明度三种做法:加法、乘法与 gamma 曲线

明度调整有三种常见做法,对应不同需求。最直接的是加法偏移,每个通道加上同一个 offset,画面整体变亮或变暗,但暗部会被硬生生提到灰色,失去层次。第二种是乘法,相当于给三通道乘一个系数,亮部容易先溢出。第三种是 gamma 曲线,它对暗部提升更柔和,高光也不容易死白,是修图软件里更常用的方案。

public Mat AdjustBrightnessGamma(Mat src, double gamma) { // gamma > 1:整体提亮;gamma < 1:整体压暗 Mat lut = new Mat(1, 256, MatType.CV_8U); for (int i = 0; i < 256; i++) { double normalized = i / 255.0; double value = Math.Pow(normalized, 1.0 / gamma) * 255.0; lut.Set<byte>(0, i, (byte)Math.Clamp(value, 0, 255)); } Mat dst = new Mat(); Cv2.LUT(src, lut, dst); // LUT 查表,256 个输入直接映射到输出 return dst; }

这段代码用 LUT 查表代替逐像素运算,256 个灰度值各算一次映射,整张图再套用,性能远好于在 C# 里双层循环。gamma 取 1.0 不变,1.2 左右提亮暗部,0.8 左右压暗高光。注意 LUT 直接作用在 BGR 三个通道上,所以它只能改变明暗感觉,不会改变色彩比例。如果你需要严格保持色相不变,应该先转 YCrCb,只对 Y 通道做 LUT,再把通道合并回去。

3.3 对比度用 ConvertTo 的 alpha/beta:顺手把 beta 也讲清楚

很多人在 OpenCvSharp 里用 ConvertTo 调对比度时,会把 beta 当成亮度偏移来加,这是理解上的偏差。ConvertTo 的语义是dst = src * alpha + beta,alpha 负责缩放,beta 负责整体加减。对比度调整的正确姿势是以 128 为锚点:

public Mat AdjustContrast(Mat src, double contrast) { // contrast = 1.0 不变;0.5 变灰;1.5 变硬 double alpha = contrast; double beta = 128 * (1 - contrast); Mat dst = new Mat(); src.ConvertTo(dst, MatType.CV_8U, alpha, beta); return dst; }

这里的 beta 不是简单加一个亮度值,而是为了保持 128 灰不变。当 contrast 大于 1 时,大于 128 的像素更亮,小于 128 的像素更暗,拉开层次;当 contrast 小于 1 时,所有像素向 128 靠拢,画面发灰。如果只设 alpha 不设 beta,你会发现拉高对比度的同时整张图也在变亮,暗部被抬起来,高光直接溢出,这正是不懂 beta 语义的人最容易踩的坑。对比度系数我一般控制在 0.8 到 1.5 之间,超过 1.8 后天空和皮肤会出现明显的断层。

3.4 色温:通道增益与掩膜版两种方案

色温调整的本质是白平衡偏移。暖色调就是增加红色通道、减少蓝色通道,冷色调反过来。在 BGR 通道上直接做通道增益最直观:

public Mat AdjustColorTemperature(Mat src, int temperature) { // temperature > 0 偏暖,< 0 偏冷,范围建议 -50 到 50 double warmR = 1.0 + temperature / 500.0; double warmB = 1.0 - temperature / 500.0; Mat[] bgr = Cv2.Split(src); Mat rAdjusted = new Mat(); Mat bAdjusted = new Mat(); bgr[2].ConvertTo(rAdjusted, MatType.CV_8U, warmR, 0); // 红色是 channels[2] bgr[0].ConvertTo(bAdjusted, MatType.CV_8U, warmB, 0); // 蓝色是 channels[0] bgr[2] = rAdjusted; bgr[0] = bAdjusted; Mat dst = new Mat(); Cv2.Merge(bgr, dst); return dst; }

temperature 取 50 时,红色乘 1.1,蓝色乘 0.9,画面明显偏暖黄;取 -50 时反向偏冷蓝。这个做法的缺点是对纯白区域也会染色,如果只希望改变高光区域的颜色,就需要先提取亮度掩膜,对掩膜区域做通道增益。还要提醒一句:BGR 通道顺序在这里再次体现,红色永远是 channels[2],写反了会出现绿色和紫色互换的诡异效果,到时候怎么调都调不回来。

4. 阴影、高光与锐化:用亮度曲线分离明暗,再用 Unsharp Mask 提细节

色彩类滤镜处理完,剩下的三个效果逻辑不同。阴影和高光不是简单调亮度,而是只针对画面中的暗部或亮部区域做局部调整;锐化则是把高频信息从原图里抽出来再叠加回去。这三者放一起是因为它们都涉及“区域选择”这个概念:阴影高光选的是亮度区间,锐化选的是空间频率。

4.1 阴影与高光:用亮度查表代替硬掩膜

正常做法是把图转到 YCrCb,只对 Y 通道生成一条“按亮度加权”的调整曲线。这样可以同时做到:暗部提亮、亮部少动,或者高光压暗、暗部少动,并且过渡自然,不会出现生硬的明暗边界。

public Mat AdjustShadowsHighlights(Mat src, int shadowAmount, int highlightAmount) { Mat ycc = new Mat(); Cv2.CvtColor(src, ycc, ColorConversionCodes.BGR2YCrCb); Mat[] channels = Cv2.Split(ycc); // shadowAmount > 0 提亮暗部;highlightAmount < 0 压暗高光 int shadowThreshold = 90; // 亮度低于 90 视为阴影区域 int highlightThreshold = 180; // 亮度高于 180 视为高光区域 byte[] lutBytes = new byte[256]; for (int v = 0; v < 256; v++) { double shadowWeight = v <= shadowThreshold ? 1.0 : Math.Max(0, 1.0 - (v - shadowThreshold) / (255.0 - shadowThreshold)); double highlightWeight = v >= highlightThreshold ? 1.0 : Math.Max(0, 1.0 - (highlightThreshold - v) / (double)highlightThreshold); double delta = shadowWeight * shadowAmount + highlightWeight * highlightAmount; lutBytes[v] = (byte)Math.Clamp(v + delta, 0, 255); } Mat lut = new Mat(1, 256, MatType.CV_8U, lutBytes); Mat yAdjusted = new Mat(); Cv2.LUT(channels[0], lut, yAdjusted); // 只对亮度通道查表 channels[0] = yAdjusted; Cv2.Merge(channels, ycc); Mat dst = new Mat(); Cv2.CvtColor(ycc, dst, ColorConversionCodes.YCrCb2BGR); return dst; }

shadowThreshold 和 highlightThreshold 是这条曲线的两个切入点。调低 shadowThreshold,提亮只作用于更暗的区域;调高 highlightThreshold,压暗只作用于更亮的区域。shadowAmount 取 20 左右时暗部细节明显浮现,再大就会让本该是黑的地方变成灰雾。highlightAmount 我一般取负值,比如 -25,用来把过曝的天空拉回层次。这段代码比用 InRange 生成硬掩膜再 CopyTo 的做法干净,不会出现人物边缘一圈光晕的翻车效果。

4.2 Unsharp Mask 锐化:高斯模糊与 AddWeighted 的配合

锐化的经典实现是 Unsharp Mask:先对原图做高斯模糊得到低频图,再用原图减去低频图提取边缘高频,最后把高频按权重加回原图。这个流程用两行代码就能完成:

public Mat Sharpen(Mat src, double amount = 0.8, double sigma = 1.0) { if (amount <= 0) return src.Clone(); Mat blurred = new Mat(); // Size(0, 0) 表示核大小由 sigma 自动计算,sigma 越大,提取的边缘越粗糙 Cv2.GaussianBlur(src, blurred, new Size(0, 0), sigma); Mat sharpened = new Mat(); // 原图权重 1 + amount,模糊图权重 -amount,等价于 原图 + amount * (原图 - 模糊图) Cv2.AddWeighted(src, 1.0 + amount, blurred, -amount, 0, sharpened); return sharpened; }

amount 控制锐化强度,0.3 到 0.8 适合人像,1.0 以上适合纹理明显的物体,超过 1.5 后边缘会出现白边。sigma 控制边缘提取的尺度,值越小提取的细节越细,噪点也越容易被放大;值越大,锐化作用在更大范围的轮廓上,颗粒感会少很多。这里 AddWeighted 的 gamma 参数是 0,不需要额外偏移。如果你用 Laplacian 然后从原图减去它,也能得到类似效果,但对噪声极敏感,稍微有点噪点的图会锐化出一片雪花。我一般只用高斯版本的 Unsharp Mask。

4.3 滤镜叠加顺序:为什么先调色彩再锐化

七个滤镜同时应用时,顺序比参数更重要。我常用的顺序是:明度 → 对比度 → 饱和度 → 色温 → 阴影高光 → 锐化。原因是锐化会放大图像里的高频噪声,如果把它放在饱和度前面,饱和度调整时色度缩放会把这种噪声再放大一遍,最终画面上会出现彩色颗粒。阴影高光放在色彩调整之后,是因为它只作用于亮度通道,此时的色彩已经稳定,不会把色偏带到明暗调整的权重计算里。

这个顺序不是玄学,而是每条处理链都在改变后续步骤的输入分布。你可以把锐化放到第一步试试,再用同样参数跑一遍,对比人物皮肤区域,会发现第二步方案干净得多。

5. 这 7 个滤镜最容易踩的坑:现象、原因与解决

这一章把我在类似 Demo 和小工具项目里见过、踩过的问题集中写出来。每一条都按现象、原因、解决三步写,方便你对照排错。

5.1 现象:调完饱和度或色温后,肤色和草地颜色完全错乱

原因:BGR 与通道顺序搞反。OpenCV 的 ImRead 读进来是 BGR,Split 后 channels[0] 是蓝、channels[2] 是红。如果把它当成 RGB 去增益,红色和蓝色互换,绿色草地变紫红色,人物皮肤发青。另一个原因是在 YCrCb 空间里,把 channels[0] 当成色度去缩放,亮度却被当成色度调,画面会出现诡异的明暗和颜色混杂。 解决:写通道操作前先打印一遍三个通道的均值确认顺序。调色温时只动 channels[0] 和 channels[2],调饱和度时只动 channels[1] 和 channels[2]。把这个确认流程作为一个固定习惯,能省掉大量调参时间。

5.2 现象:明度或饱和度调大后,高光死白,暗部出现彩色噪点

原因:像素值溢出后被截断。ConvertTo 或者 ConvertTo 里 factor 过大,导致很多像素被钳制到 0 或 255,丢失层次。饱和度溢出时色度分量被切断,形成一块块不连续的色斑。 解决:把调整范围限制到合理区间,饱和度不超过 1.8,明度加法不超过 40。要提升暗部时优先用 gamma 或阴影曲线,不要直接用高 offset 把暗部强行拉起来,否则暗部噪声会跟着一起放大。如果必须大幅提亮,先对 Y 通道做一次轻度降噪再提亮。

5.3 现象:锐化后物体边缘出现一圈白边或黑边

原因:amount 过大,或者用了核太小的 Laplacian。Unsharp Mask 提取的边缘被过度放大后,会在明暗交界处形成过冲,表现为白边;在原图减掉过大高频时则形成黑边。 解决:把 amount 降到 1.0 以下,同时把 sigma 提高到 1.5 到 2.0。sigma 越大,提取的是更宏观的边缘,过冲感越弱。如果你必须用很大的 amount,就在锐化前对原图做一次很小的高斯模糊,把孤立噪点先压掉,再做 Unsharp Mask。

5.4 现象:滤镜结果正常,但 PictureBox 显示斜纹状花屏

原因:Mat 转 Bitmap 时没处理 Stride 对齐。整块 Buffer.BlockCopy 复制 Step * Height 的数据,遇到宽度不是 4 倍数的图,每行错位,出现斜向条纹。 解决:用前面 2.3 节那种逐行复制的方式。复制时以 Math.Min(srcStride, dstStride) 为长度,防止越界。另外,如果程序在高 DPI 环境下显示,还需要把 PictureBox 的 SizeMode 设置成 Zoom,否则视觉上也会变形。

5.5 现象:一运行就抛 DllNotFoundException 或 BadImageFormatException

原因:OpenCvSharp4.runtime.win 没有被正确复制到输出目录,或者程序集目标平台与原生 dll 架构不匹配。x64 的程序引用了 x86 的原生库,或者在 AnyCPU 模式下运行时载入失败。 解决:确认 NuGet 包引用了 OpenCvSharp4.runtime.win,并在输出目录里能看到 OpenCvSharp 的 dll 和 opencv_world 的 dll。项目平台建议直接选 x64,不要在 AnyCPU 下赌运气。程序要发给别人用时,换 selfcontained 版运行时包,少一半环境问题。

6. 把 7 个滤镜串成一条可复用管线:参数表与验证方法

Demo 里的滤镜通常各自独立,但真实项目里用户期望的是几个滑块同时生效。这就要把七种效果按顺序串成一条管线,并处理好空值和越界。一个实用的封装是把所有参数放进一个 Options 对象,Apply 方法内部按固定顺序调用。

public class FilterOptions { public double Brightness { get; set; } = 0; // -50 ~ 50,加法提亮 public double Contrast { get; set; } = 1.0; // 0.5 ~ 1.8 public double Saturation { get; set; } = 1.0; // 0.5 ~ 1.8 public int Temperature { get; set; } = 0; // -50 ~ 50 public double Shadow { get; set; } = 0; // 0 ~ 30 public double Highlight { get; set; } = 0; // -30 ~ 0 public double Sharpen { get; set; } = 0; // 0 ~ 1.5 } public static Mat Apply(Mat src, FilterOptions o) { Mat tmp = src.Clone(); // 1. 明度:加法,-50 到 50 对应每像素偏移 tmp.ConvertTo(tmp, MatType.CV_8U, 1.0, o.Brightness); // 2. 对比度:保持 128 中性点 double alpha = o.Contrast; double beta = 128 * (1 - o.Contrast); tmp.ConvertTo(tmp, MatType.CV_8U, alpha, beta); // 3. 饱和度:YCrCb 空间对色度缩放 tmp = SaturationFilter.Adjust(tmp, o.Saturation); // 4. 色温:BGR 的 R/B 通道增益 tmp = TemperatureFilter.Adjust(tmp, o.Temperature); // 5. 阴影高光:亮度查表曲线 tmp = ShadowHighlightFilter.Adjust(tmp, o.Shadow, o.Highlight); // 6. 锐化:Unsharp Mask 放最后,避免放大前序噪声 tmp = SharpenFilter.Adjust(tmp, o.Sharpen); return tmp; }

这个管线里的每一步都应该单独可测。如果最终效果不对,用二分法定位:先只调亮度和对比度,再加饱和度,逐层开启。下面是这套参数的经验取值表,后面对照着调就能少走弯路:

滤镜推荐范围越界表现
明度(加法)-30 ~ 30大于 50 亮部死白
对比度0.8 ~ 1.5大于 1.8 出现色阶断层
饱和度0.8 ~ 1.5大于 1.8 出现色斑
色温-40 ~ 40大于 50 高光整体染色
阴影0 ~ 25超过 30 暗部灰雾
高光-25 ~ 0低于 -30 画面发闷
锐化0.3 ~ 1.0超过 1.5 边缘白边

验证阶段只看预览图不够,我习惯用 OpenCvSharp 拉一下直方图和均值:调对比度后,检查灰度直方图有没有大量像素堆在 0 和 255 两端;调阴影后,看暗部均值是否真的抬升了,而不是整张图变亮。量化验证的好处是能发现预览图上不易察觉的过曝。

Mat gray = new Mat(); Cv2.CvtColor(result, gray, ColorConversionCodes.BGR2GRAY); double mean = Cv2.Mean(gray).Val0; Cv2.MinMaxLoc(gray, out double min, out double max); // mean 落在 90~160 说明明度合适;min/max 接近 0/255 说明对比度到位

我自己最早调阴影高光时也翻过车,直接对全图提亮,结果人物边缘一圈光晕,后来才明白要先分离亮度再做加权曲线。这类 Demo 真正值钱的地方不是某个滤镜写得多玄,而是让你直观看到每个参数越界之后画面会怎么坏掉。先把这条管线按上面的顺序跑通,再往里面加自定义曲线或局部掩膜,你的滤镜效果会比单纯调滑块稳定得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询