65.9K
CodeProject 正在变化。 阅读更多。
Home

Image Magic - 使用自定义控件的图像级别

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2010 年 5 月 26 日

CPOL

11分钟阅读

viewsIcon

43990

downloadIcon

2251

可直接插入 WPF 自定义调平控件和逻辑。

引言

调整数字图像的色阶是最基本的图像增强操作之一。Photoshop、The Gimp 和 Paint.Net 都提供了一套类似的色阶调整控件。这些控件已经得到认可,并且无论好坏,都是用户所期望的。不幸的是,使用标准的 WPF 控件无法创建这种类型的 UI。本文提供了用于图像色阶调整的无外观自定义控件以及与它们相关的逻辑。

目标

  • 为用户实验提供图像色阶调整控件和逻辑。
  • 选择显示正在发生的事情而不是易用性。没有图形平滑或值截断。
  • 只关注基本功能。避免美化和实现很少使用的功能。没有吸管、半透明颜色、多通道调整或统计数据。
  • 解释得足够清楚,以避免常见的用户意外。
  • 提供标准灰度设置的替代方案。

背景

图像色阶调整基于直方图。直方图以及通常用于修改它们的类似 Photoshop 的 UI 是一个广泛的主题。要快速介绍或复习,请访问 cambridgeincolour。直方图是一个 256 字节的数组,记录图像中每个强度级别的像素数量。在本文中,像素的总强度定义为 (R+G+B)/3。直方图显示在 2 个直方图图中。输入直方图显示在顶部并保持不变。输出直方图显示在底部,并随着用户更改输入和输出滑块上的值而修改。色阶调整控件由以下部分组成:

  • 输入直方图 - 显示已加载的未修改图像的直方图。直方图绘制图像的 3 个颜色分量及其总强度。
  • 输入滑块 - 一个带有 3 个缩略图的滑块,用于调整图像的黑色、灰色和白色值。输入滑块修改直方图中强度值的分布及其范围。
  • 输出直方图 - 显示图像经过输入和输出滑块修改后的直方图。
  • 输出滑块 - 一个带有 2 个缩略图的滑块,用于压缩图像的强度范围。输出 0-黑和白-255 之间的强度值被压缩掉。换句话说,输入黑和白之间的强度范围被重新映射到输出黑和白之间的范围。

Using the Code

你得到什么

解决方案中提供了四个项目

  1. JustXAML - 没有 C# 代码。仅 XAML 用于显示 customLevels 控件。从这里开始了解控件的工作原理、调整大小行为并确认它是一个无外观控件。由于尚未加载图像,直方图是空的。
  2. LevelsLogic - 一个包含用于操作和重新映射直方图的类的 DLL。这是客户逻辑。
  3. TestCustomLevels - 一个将 CustomLevels 控件与 LevelsLogic 耦合的 EXE。添加了用于加载和保存图像的按钮以及另一个用于显示图像的窗口。
  4. WpfCustomRGBHisto - 一个包含 4 个自定义控件的 DLL。CustomLevels 控件使用了 3 个其他自定义控件。TwoThumb (OutputSlider)、ThreeThumb (InputSlider) 和 Histograph(输入和输出图)。

TestCustomLevels 是一个独立的实用程序,演示了 CustomLevels 控件的使用。代码需要引用 LevelsLogicWpfCustomRgbHisto 的 DLL 命名空间的 Using 语句。

using WpfCustomRGBHisto;
using LevelsLogic;

CustomLevels 控件是实现类似 PhotoShop 图像色阶 UI 所需的所有自定义控件的接口。CustomLevels 更像一个 C# 类而不是一个控件。它的功能被最小化,只提供绝对必需的功能。它负责...

  1. 查找其包含的自定义控件的命名部件并将其对用户隐藏。
  2. 定义 LevelValues 类,用于将所有控件的值反馈给用户。LevelValues 类将自定义控件中的 Double 值转换为 int,因为这是用户所需的全部。
  3. 提供一个 public 事件,用户注册该事件以接收任何控件中色阶值变化的通知。用户只收到大于或等于 1 的整数变化的通知。
  4. 提供一种方法,在加载图像时将所有控件设置为其默认值。

CustomLevels 控件不知道它如何被使用。为了开始支持图像色阶调整,需要 2 个按钮来加载和保存图像。XAML 被扩展,将按钮放置在 Grid 的第二行,如下所示

<Window x:Class="TestCustomLevels.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:lib="clr-namespace:WpfCustomRGBHisto;assembly=WpfCustomRGBHisto"
     Title="TestCustomLevels" Height="500" Width="400" Topmost="True">
    <Grid Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions>
        <lib:CustomLevels x:Name="customLevels" Grid.Row="0" 
		Background="DimGray" FontWeight="Bold" />
        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <Button Margin="2" Click="LoadImage_Click">Load Image</Button>
            <Button Margin="2" Click="SaveImage_Click">Save Image</Button>
        </StackPanel>
    </Grid>
</Window>

检查 TestCustomLevels 的代码隐藏显示已添加了 4 个变量。它们是

byte[] origRgb;         //unmodified rgb data from a file
byte[] modRgb;          //modified levels adjusted rgb data goes here, 
                        //inited when image loaded from file
HistoRemap histoRemap;  //represents image remapping specified by input 
                        //and output sliders
ImageWindow imgwind;    //image display, load and save

从加载的图像中获取的 rgb 数据进入 origRgb 数组。一旦加载,origRgb 中的数据保持不变。它是用户希望调整色阶的 rgb 数据源。实际调整色阶数据的目标是 modRgb 数组。origRgbmodRgb 数组的长度相同。histoRemap 对象是程序与执行色阶调整的 LevelsLogic DLL 之间唯一的连接。它使用 CustomLevels 控件中设置的色阶值,并使用它们来确定如何将原始直方图和 rgb 数据重新映射到指定的色阶。

imgwind 对象负责加载、显示和保存正在调整的图像。当从文件中加载图像时,imgwind 读取并存储图像数据两次,一次用于整个全尺寸图像,一次用于缩小到最大化窗口像素尺寸的图像。只有缩小尺寸的图像数据经过色阶调整和显示。在我的相机上,这会将 origRgbmodRgb 数组的长度减少十倍。数据的减少加速了色阶调整,同时保持了显示图像的质量。在图像保存时,全尺寸图像经过色阶调整并写入文件。

调整图像数据的色阶是一个相当快速且 CPU 高效的过程。令人惊讶的是,测试表明,如果滑块快速重新定位,CPU 使用率会上升到足以导致 WPF 问题的程度。这表现为滑块运动卡顿。有时滑块重新定位甚至会瞬间改变方向。通过在后台线程中执行图像数据的色阶调整来修复了这个问题。

调整直方图

直方图色阶调整是 CustomLevels 控件中显示的图形的基础。所有直方图都位于 Histo 对象中。此对象实际上包含 4 个直方图,每个颜色 R、G 和 B 各一个,一个用于总强度 (R+G+B/3)。HistoRemap 用于将每个直方图重新映射到 CustomLevels 控件指示的色阶。通过将当前色阶值传递给 RemapAll 方法,然后对每个直方图调用 RemapHistoArray 来完成重新映射。然后将生成的输出 Histo 对象发送到 CustomLevels 控件进行显示。下面的代码取自 LevelsChanged 事件处理程序,显示了这是如何完成的

histoRemap = new HistoRemap();              //for level mapping of new slider values
//using current slider values compute a level mapping
histoRemap.RemapAll(levels.InputBlack, levels.InputGray, levels.InputWhite,
	levels.OutputBlack, levels.OutputWhite);

Histo iHisto = customLevels.InputHisto;     //fetch input histos, 
                                            //constant for a given image
Histo oHisto = new Histo();                 //need to compute new output histos
foreach (HistoArray ha in Enum.GetValues(typeof(HistoArray)))   //each Histo has 
                                                     //r,g,b and intensity components
{
    int[] ia = iHisto.GetHistoArray(ha);    //original input histo component
    int[] oa = oHisto.GetHistoArray(ha);    //corresponding new output histo component
    histoRemap.RemapHistoArray(ia, oa);     //use level mapping to transform input 
                                            //histo component to output histo component
}
customLevels.OutputHisto = oHisto;          //update output histograph display in 
                                            //custom control

调整图像

更新输出 Histograph 后,图像经过色阶调整并重新显示。这是通过在 BckgrndRemapImageArray 方法上启动后台工作程序来在后台完成的。该方法传递一个 RemapImageArgs 类,其中包含一个 HistoRemap 对象以及对源和目标 rgb 数据数组的引用。该方法如下所示

private void BckgrndRemapImageArray(object sender, DoWorkEventArgs args)
{
    RemapImageArgs remapArgs = (RemapImageArgs)args.Argument;
    HistoRemap hr = remapArgs.Remap;
    hr.RemapImageArray(remapArgs.OrigRgb, remapArgs.ModRgb);                //do work
    args.Result = remapArgs.ModRgb;     //not used, but this is typically how it's done
} //BckgrndRemapImageArray()

滑块的作用

对于大多数用户来说,了解色阶滑块的最佳方法是玩它们。但是,如果您对图像处理感兴趣,您可能会觉得本节很有趣。它描述了滑块的作用,并将它们与 PhotoShop 滑块进行比较。要应用于图像的色阶更改按以下顺序计算。对图像进行一次遍历以应用所有色阶更改。

  1. 输入黑白值
  2. 输入灰度值
  3. 输出黑白值

输入滑块

输入黑白值用于压缩图像的强度范围。0 和黑值之间的所有图像强度值都设置为黑值。同样,白值和 255 之间的强度值都设置为白值。违反直觉的是,将黑色缩略图移向白色会使图像变暗。将白色缩略图移向黑色会使图像变暗。看似数学上不可能的事情有一个解释。输出滑块未被修改。因此,所有变亮的暗值和变暗的亮值都被重新映射(扩展)到 0 和 255。输出直方图显示端点处的像素数量大幅增加。这种不太可能的界面的原因是因为它能产生更好的图像。强度压缩和随后的扩展增加了输入黑白之间的范围。更多的强度值增加了图像的对比度。这几乎总是人们想要的。

输入灰度值缩略图是最复杂的控件。它用于改变强度分布,而不是范围。将缩略图移向白色/黑色会使图像变亮/变暗。黑白值的更改会导致灰度缩略图重新定位。但是,它的值保持不变。如果灰度值设置为其默认值 127,它将始终重新定位到黑白值范围的中心。灰度值只有在用户直接移动灰度缩略图时才会修改。

强度分布可以通过将其映射到曲线来改变。通常使用 Gamma 曲线,因为它描述了像相机这样的物理设备如何响应光线。Gamma 曲线的输出定义为 b**Gamma,其中 b 是在 0-1.0 之间归一化的输入亮度(只需将 RGB 强度除以 255)。Gamma 的实际值对用户来说是无意义的。类似 Photoshop 的 UI 显示 Gamma 值。稍加挖掘就会发现它们实际上显示的是 1/Gamma 而不是 Gamma。倒数更容易编程,但它反转了灰度值滑块的方向。输入滑块不显示 Gamma 值,而是显示 0-255 之间的值。这是灰度 (127) 通过 Gamma 曲线重新映射到的强度值。实现重新映射所需的 Gamma 的实际值在后台计算。默认灰度值为 127,对应于 Gamma 为 1.0。这是一条斜率为 45 度的直线。换句话说,灰度值为 127 意味着不执行 Gamma 校正。

输出滑块

OutputSlider 用于控制图像的强度范围和对比度。输出缩略图的任何移动,只要偏离其默认值,都会降低对比度!将黑色缩略图移向白色会使图像变亮。将白色缩略图移向黑色会使图像变暗。这与输入黑白缩略图的行为相反。建议用户避免使用 OutputSlider。其默认值 0 和 255 可最大化图像的范围和对比度。

类似于 InputSlider,输出黑白值压缩了强度范围。0 和黑值之间的所有图像强度值都设置为黑值。白值和 255 之间的强度值都设置为白值。此外,输入黑白之间的强度值使用直线重新映射到输出黑白之间指定的范围。使用直线进行输出重新映射会使图像褪色,这使人能够欣赏 Gamma 曲线。

直方图

梳状效应

如果你打开一张刚从相机中取出的照片,你可能会对直方图的形状感到惊讶。你看到的是表现不佳的函数,突然降到零或意外上升,而不是你所期望的良好行为的连续函数。放松,你的相机和软件都运行正常。我称这种行为为梳状效应。它是数字操作的产物。在数字世界中,值是量化的,而不是连续的。当发生图像调整时,例如将强度级别 10 更改为 20,强度级别为 10 的像素将更改为占据级别 20。这会在级别 10 处留下一个空洞或归零,并在级别 20 处添加一个尖峰。令人惊奇的是,这种行为可以提供所需的效果。当相机拍照时,图像数据由专有算法处理。我的相机确实在低端对 rgb 数据进行了大量切割,但它尽力生成一个连续的总强度曲线,尤其是在中间调。

扩展

直方图只是网格内的一些多边形。多边形经过缩放,以便最大值适合网格。当滑块重新定位时,多边形的部分可能会显得缩小。实际发生的是多边形中的最大值(可能在端点处)正在增长。当多边形缩放以显示最大值时,其他未更改的值会显得缩小。Photoshop 通过截断最大的多边形值来避免这种情况。这是在易于查看的显示和实际发生的情况之间做出选择。

历史

  • 2010 年 5 月 24 日:首次发布
© . All rights reserved.