使用颜色进行命中测试






3.86/5 (3投票s)
提供一个支持使用颜色进行命中测试的颜色命中测试用户控件。

引言
在访问一个包含美国时区彩色地图的网页时,我被要求提供我所在的时区;但不是通过点击地图。而是通过一个包含美国时区名称的组合框来提供。我想,在 WinForms 应用程序中拥有一个可点击的地图可能会很有用。本文描述了由此产生的用户控件;控件的调用方式;以及我在此过程中学到的一些经验。
背景
执行命中测试是一项计算复杂的任务。最简单的形式是,问题是某个点是否在某个多边形内。请注意,我在下面的图表中添加了一个圆形。我们可以构建轴对齐边界矩形来包围多边形。

这些边界矩形使我们能够快速确定一个点(A 或 D)是否不在边界矩形内。但是,一个点在边界矩形内并不能充分说明它在多边形内。点(E 和 H)在边界矩形内,但不在多边形内。需要更复杂的计算。读者可以参考两个“点在多边形内”的参考资料,了解计算样本。
使用多边形还有另一个缺点。仔细查看放大后的时区地图。

边界边缘的像素定义了该边界。使用这种方法定义边界需要数百,甚至数千个边缘。当然,我们可以稍微作弊,使用起点和终点坐标对来描述边界线。但即便如此,我们仍然需要大量的边缘来定义多边形。最后,如果曲线被包含在边界中,事情就会变得棘手。
假设我们可以用颜色填充每个多边形。

现在我们只需要确定在感兴趣的点上是否有颜色。如果有颜色,我们就位于多边形内;如果没有,我们就位于多边形外。这种命中测试称为颜色命中测试。当感兴趣的多边形足够静态以进行着色时,它就有了应用。对于时区地图,我们可以按如下方式为区域着色。

如果鼠标点击发生在品红色区域,则时区为东部;发生在亮绿色区域,则时区为中部;以此类推。
颜色命中测试用户控件
这个 ColorHitTest
用户控件封装了两个项目:一个它将显示的图像和一个节点列表,每个节点包含与图像中出现的颜色相关的数据。 ColorHitTest
用户控件包含以下方法、属性和事件。
名称 | 描述 |
构造函数 | |
ColorHitTest |
public ColorHitTest ( )
创建 ColorHitTest 用户控件的实例 |
方法 | |
Add |
public void Add ( Color color,
string value )
将颜色/值对添加到已保存的颜色/值对中 |
Clear |
public void Clear ( )
删除所有颜色/值对 |
Contains (重载) |
public bool Contains ( Color color )
返回一个 public bool Contains ( string value )
返回一个 public bool Contains ( string value )
bool case_sensitive )
返回一个 |
Get (重载) |
public bool Get ( Color color )
返回与已保存颜色/值对中指定颜色关联的值 public bool Get ( string value )
从已保存的颜色/值对中返回与指定区分大小写值关联的颜色 public bool Get ( string value )
bool case_sensitive )
从已保存的颜色/值对中返回与指定值(可能不区分大小写)关联的颜色 |
Image (重载) |
public void Image ( Bitmap bitmap,
int width,
int height )
用指定的 public void Image ( Bitmap bitmap,
int width,
int height,
bool make_transparent )
用指定的 |
移除 (重载) |
public bool Remove ( Color color )
删除包含指定颜色的已保存颜色/值对 public bool Remove ( string value )
删除包含指定区分大小写值的已保存颜色/值对 public bool Remove ( string value )
bool case_sensitive )
从已保存的颜色/值对中删除包含指定值(可能不区分大小写)的已保存颜色/值对 |
属性 | |
Count |
public int Count
返回已保存颜色/值对的数量 |
事件 | |
OnColorHit | 当用户单击图像中的某个位置时,将 ColorHitEventArgs 返回给其订阅者public ColorHitEventArgs ( Color color,
string value )
|
委托和事件定义如下
// ******************************* delegate ColorHitHandler
public delegate void ColorHitHandler ( object sender,
ColorHitEventArgs e );
// ***************************************** event OnColorHit
public event ColorHitHandler OnColorHit;
使用控件
使用 ColorHitTest
用户控件执行命中测试需要五个步骤
- 准备一个要进行颜色命中测试的位图图像
我从左边的 GIF 开始,花了大约十个小时的沮丧后,完成了右边的 BMP。在此过程中我学到了很多,并在下面描述了一些经验教训。
- 调用
ColorHitTest
构造函数private ColorHitest. ColorHitest time_zones_CHT; time_zones_CHT = new ColorHitest. ColorHitest ( )
当调用
ColorHitTest
构造函数时,用于存储颜色/值对的数据结构会被初始化。 - 初始化颜色/值对,例如
time_zones_CHT.Add ( Color .Magenta , "Eastern" ); time_zones_CHT.Add ( Color .Lime , "Central" ); : time_zones_CHT.Add ( Color .White , "Boundaries" );
每个颜色/值对被放入一个
ColorNode
中,然后将ColorNode
添加到ColorNodes
列表中。放在列表中的ColorNodes
数量没有实际限制。 - 加载用户控件要显示的图像
time_zones_CHT.Image ( display_bitmap, desired_width, desired_height );
在演示中,包含美国时区的位图大小为 425 宽 x 267 高。我想显示一个更小的图像。为了执行缩放,我添加了
ScaleBitmap
方法,该方法是从文章 Bitmap Manipulation Class With Support For Format Conversion, Bitmap Retrieval from a URL, Overlays, etc.(来自 CodeProject)中提取的。缩放显示的位图非常重要;否则,MouseUp
(用于触发onColorHit
事件)的位置将相对于原始位图报告。这会导致颜色被误解,并返回不正确的关联值。 - 订阅命中事件
time_zones_CHT.OnColorHit += new CH.ColorHitHandler ( revise_GUI );
演示 GUI 会根据图像上的颜色/值对进行更新。因此,调用事件处理程序
revise_GUI
。revise_GUI
的签名是private void revise_GUI ( object sender, ColorHitEventArgs e )
ColorHitEventArgs
包含MouseUp
事件发生位置的颜色和值。
为像我一样“图形能力差”的程序员准备的经验教训
本节包含我在为控件准备图形时学到的一些经验。这绝不是对程序员绘图的完整讨论。我只希望我有限的经验能帮助我的读者。
- 在与各种图形编辑器和输出文件类型进行了大量斗争之后,我发现 Microsoft Paint 和位图最符合我的需求。本项目中出现的时区图形是使用 Microsoft Paint 创建的。图像最终保存为位图(.bmp)。我花费了无果的时间处理 JPEG 图像。我发现每次加载 JPEG 图像时,其边缘都会变得模糊,并且我用来填充区域的单一颜色不再是单一颜色。我现在更深入地理解了“有损”的含义。
- 一旦获得所需效果,请立即释放鼠标左键(意外单击鼠标右键会撤销自上次释放鼠标左键以来完成的所有工作)。
- 绘制直线时,几乎总是按住 Shift 键。这会将直线强制以 45° 的倍数角度绘制。
- 绘制到形状的内部。这可以避免大多数意外的鼠标漂移。
- 在像素级别绘图时,始终放大图像,直到可以轻松选择单个像素。在 Paint 中,视图?缩放?自定义…。我使用了 600%
- 仅当您能与边界保持五个像素以上的距离时才使用画笔。除非您要更改大区域的颜色,否则根本不要使用画笔。在 Microsoft Paint 中使用画笔的好处是它能预览单击鼠标左键时颜色将如何变化。
- 了解如何执行撤销上一个操作(通常是 CTRL-Z)。如果您不小心进入了不想更改的区域,这将非常有用。
- 在完成复杂形状的颜色替换后,仍然有几个像素未能更改为我想要的颜色。为了最轻松地修复这些问题,我选择一种对比色,然后用新颜色填充该图形。未覆盖的像素可以轻松识别和更改。将错误的像素更改为新颜色,当一切看起来都不错时,将该区域重新填充为所需的颜色。
- 我发现使用直线工具更改像素颜色很有用。
-
我想要的背景颜色是白色(RGB 255, 255, 255)。不幸的是,背景中有一些脏白色(RGB 255, 251, 240)的污点。为了消除这些污点,我首先使用拾色器工具选择污点颜色。然后我使用颜色填充工具将整个背景填充为污点颜色(确保拾取那些无法填充的小区域。(因为所有水都是连通的,所以我修改了缅因州东北部的区域,使其背景与其余背景连通。)然后我将颜色更改为黑色(RGB 0, 0, 0),并再次使用颜色填充工具填充整个背景。现在出现了一些不是精确污点颜色的小区域。我用黑色填充了这些区域。最后,我用白色填充了背景。结果就是演示中使用的图形。
很重要的一点是,用于填充背景的填充颜色不能是与背景相邻的颜色。当我执行背景污点颜色修复时,夏威夷不是黑色的,所以可以使用黑色。如果夏威夷是黑色的,我将选择一种不是黑色的颜色来执行背景污点颜色修复。
- 我犯了一个错误,没有正确连接分隔时区的线条。为了使油漆填充操作按预期进行,有必要确保两条线段共享一个像素。
- 在选择颜色时,请从“已知”颜色中选择。这是一个有限的调色板,但它可以轻松识别颜色名称和 ARGB 值。遵循此规则时,我发现 Microsoft Paint 并不太友好。颜色菜单项显示一个颜色对话框,其中不包含已知颜色,并且当鼠标悬停在颜色上时甚至不显示颜色名称。
从上面回忆
time_zones_CHT.Add ( Color .Lime , "Central" );
指定
Color.Lime
比指定Color.FromArgb
( 255, 0, 255, 0) 更容易。但是,Microsoft Paint 的颜色对话框有点死板。虽然它显示了一组不错的颜色,但它们不是已知颜色,并且当鼠标悬停在颜色上时没有工具提示来指示颜色名称(如果有)。我在 Microsoft Paint 界面上挣扎了一段时间,然后决定构建已知颜色调色板工具。虽然该工具不能将选定的颜色直接拖放到 Microsoft Paint 的颜色对话框中,但它显示了将 ARGB 值传输到 Microsoft Paint 所需的所有信息。当“准备就绪”时,该工具将通过 CodeProject 提供。
参考文献
- 最优雅的二维“点在多边形内”或 Polygon. contains(p:Point) 算法 Stack Overflow
- 点在多边形策略,Haines, Eric,“Point in Polygon Strategies”,Graphics Gems IV,ed. Paul Heckbert,Academic Press,p. 24-46,1994
- 使用 C# 的多色下拉列表,The Code Project
- 支持格式转换、从 URL 检索位图、叠加等功能的位图处理类,The Code Project