我认为你的做法是错误的。大陆的轮廓是疯狂的;它们通常由带有许多小岛的几个部分组成。而且,您不需要图像上的大陆坐标;查找您当前的坐标是否在列表中将花费太长时间。相反,您应该做相反的事情:为整个图像制作一个索引表,在该索引表上为每个像素指示它所属的大陆。
这要容易得多, 更容易。
由于您显然必须为每个大陆分配一种颜色来识别它们,因此您可以遍历图像的所有像素,将每个像素的颜色与您的大陆颜色中最接近的匹配进行匹配,并填充数组中的每个字节与相应的找到的大陆索引。这样,您将获得一个直接引用您的大陆数组的字节数组。实际上,这意味着您创建一个索引的 8 位图像,就像一个普通的字节数组一样。 (请注意,有一些方法可以将其与颜色数组实际结合并获得可以使用的图像。这并不难。)
对于实际的颜色匹配,最佳做法是在源图像上使用 LockBits 来直接访问底层字节数组。在下面的代码中,对GetImageData 的调用让我得到了字节和数据跨度。然后,您可以遍历每行的字节数,并从代表一个像素的每个数据块中构建一种颜色。如果您不想为支持不同的像素大小(如 24bpp)而烦恼,一个快速的技巧是在相同尺寸的新 32bpp 图像上绘制源图像(调用 PaintOn32bpp),这样您总是可以简单地每四个字节迭代一次,并按 ARGB 的 3、2、1、0 顺序获取字节值。我在这里忽略了透明度,因为它只会使颜色的概念变得复杂。
private void InitContinents(Bitmap map, Int32 nearPixelLimit)
{
// Build hues map from colour palette. Since detection is done
// by hue value, any grey or white values on the image will be ignored.
// This does mean the process only works with actual colours.
// In this function it is assumed that index 0 in the palette is the white background.
Double[] hueMap = new Double[this.continentsPal.Length];
for (Int32 i = 0; i < this.continentsPal.Length; i++)
{
Color col = this.continentsPal[i];
if (col.GetSaturation() < .25)
hueMap[i] = -2;
else
hueMap[i] = col.GetHue();
}
Int32 w = map.Width;
Int32 h = map.Height;
Bitmap newMap = ImageUtils.PaintOn32bpp(map, continentsPal[0]);
// BUILD REDUCED COLOR MAP
Byte[] guideMap = new Byte[w * h];
Int32 stride;
Byte[] imageData = ImageUtils.GetImageData(newMap, out stride);
for (Int32 y = 0; y < h; y++)
{
Int32 sourceOffs = y * stride;
Int32 targetOffs = y * w;
for (Int32 x = 0; x < w; x++)
{
Color c = Color.FromArgb(255, imageData[sourceOffs + 2], imageData[sourceOffs + 1], imageData[sourceOffs + 0]);
Double hue;
// Detecting on hue. Values with < 25% saturation are ignored.
if (c.GetSaturation() < .25)
hue = -2;
else
hue = c.GetHue();
// Get the closest match
Double smallestHueDiff = Int32.MaxValue;
Int32 smallestHueIndex = -1;
for (Int32 i = 0; i < hueMap.Length; i++)
{
Double hueDiff = Math.Abs(hueMap[i] - hue);
if (hueDiff < smallestHueDiff)
{
smallestHueDiff = hueDiff;
smallestHueIndex = i;
}
}
guideMap[targetOffs] = (Byte)(smallestHueIndex < 0 ? 0 : smallestHueIndex);
// Increase read pointer with 4 bytes for next pixel
sourceOffs += 4;
// Increase write pointer with 1 byte for next index
targetOffs++;
}
}
// Remove random edge pixels, and save in global var.
this.continentGuide = RefineMap(guideMap, w, h, nearPixelLimit);
// Build image from the guide map.
this.overlay = ImageUtils.BuildImage(this.continentGuide, w, h, w, PixelFormat.Format8bppIndexed, this.continentsPal, null);
}
GetImageData 函数:
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <returns>The raw bytes of the image</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride)
{
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data = new Byte[stride * sourceImage.Height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
sourceImage.UnlockBits(sourceData);
return data;
}
现在,回到流程;一旦你有了那个参考表,你所需要的就是鼠标的坐标,你可以检查索引处的参考地图 (Y*Width + X) 来查看你所在的区域。为此,你可以添加一个 MouseMove ImageBox 上的侦听器,如下所示:
private void picImage_MouseMove(object sender, MouseEventArgs e)
{
Int32 x = e.X - picImage.Padding.Top;
Int32 y = e.Y - picImage.Padding.Left;
Int32 coord = y * this.picWidth + x;
if (x < 0 || x > this.picWidth || y < 0 || y > this.picHeight || coord > this.continentGuide.Length)
return;
Int32 continent = this.continentGuide[coord];
if (continent == previousContinent)
return;
previousContinent = continent;
if (continent >= this.continents.Length)
return;
this.lblContinent.Text = this.continents[continent];
this.picImage.Image = GetHighlightPic(continent);
}
注意,最近颜色匹配生成的简单生成图可能有错误;当我对这张世界地图的颜色进行自动映射时,蓝色和红色之间的边界,以及中美洲的一些小岛,最终被识别为南极洲的紫色,而其他一些流氓像素也出现在不同大陆的边缘。
这可以通过清除(我使用 0 作为默认值“无”)所有在顶部、底部、左侧和右侧不被相同索引包围的索引来避免。这会移除一些较小的岛屿,并在任何相邻大陆之间产生微小的差距,但对于鼠标坐标检测,它仍然会很好地匹配这些区域。这是我的InitContinents 函数中的RefineMap 调用。它获得的参数决定了一个索引需要多少相同的相邻值才能使其在修剪后存活。
一种类似的检查相邻像素的技术可用于获取轮廓,方法是制作一个像素图,所有边都没有被相同的值包围。