获取图像区域的2D坐标轮廓(例如地图上的国家/地区)

时间:2015-11-13 22:36:17

标签: c# asp.net .net image-processing coordinates

如何为图像区域生成2D坐标,例如,如果此地图上的某个国家/地区被挑选出来并且是唯一可见的国家:image1但是在画布上相同的尺寸,我将如何获得它的2D坐标?

当我想要使用c#基于这些坐标创建悬停/点击区域时,我无法找到可以检测例如空白画布内的形状并吐出其轮廓坐标的工具。

我主要认为这是一个措辞/术语问题,因为我觉得整个过程已经是一个""并且有很好的文档记录。

3 个答案:

答案 0 :(得分:2)

有很多方法可以实现您的任务:

看看Generating Polygons from Image (Filled Shapes)这几乎与你的重复,但起点有点不同。

简而言之:

  1. 提取所有非白色像素,这些像素是相邻的白色像素

    如果处理过的像素不是白色,只需遍历整个图像(外边框像素除外),然后查看处理像素的4/8个邻居。如果它们中的任何一种颜色不同,则将处理后的像素颜色和坐标添加到列表中。

  2. 按颜色对点列表进行排序

    这将分开国家

  3. 应用闭环/连接性分析

    这是矢量化/多边形化过程。只是加入尚未使用列表中的相邻像素来形成线条......

  4. 还有一个替代方案可以更容易实现:

    1. 提取所有非白色像素,这些像素是相邻的白色像素

      如果处理过的像素不是白色,只需遍历整个图像(外边框像素除外),然后查看处理像素的4/8个邻居。如果它们都不是不同的颜色,那么用一些未使用的颜色(黑色)清除当前像素。

    2. 将所有白色和清晰颜色重新着色为单色(黑色)。

      从中获得重新着色的颜色将意味着墙

    3. 应用A*路径寻找

      找到第一个非墙像素并应用A *像生长填充。完成填充后,只需trace back将列表中的点顺序记为多边形。可选择将直线像素连接到单行...

    4. 另一个选择是调整此Finding holes in 2d point sets

      <强> [注释]

      如果您的图像被过滤(抗锯齿,缩放等),那么您需要进行颜色比较,并有一些误差余量,甚至可能是HSV的端口(取决于颜色失真的程度)。

答案 1 :(得分:0)

您可以使用opencv的findcontour()函数。请参阅此处的文档:http://docs.opencv.org/2.4/doc/tutorials/imgproc/shapedescriptors/find_contours/find_contours.html

答案 2 :(得分:0)

我认为你的方式是错误的。大陆的轮廓是疯狂的;它们通常由几个部分组成,有许多小岛屿。并且,您不需要图像上的大陆坐标;查看当前坐标是否在列表中需要花费太长时间。相反,你应该做相反的事情:制作整个图像的索引表,在该表上为每个像素表示它所属的大陆。

而且那很多,很多更容易。

由于你显然必须为每个大陆分配一种颜色来识别它们,你可以遍历所有图像的像素,将每个像素的颜色与大陆颜色中最接近的匹配相匹配,并填充数组中的每个字节与相应的发现大陆指数。这样,您将获得一个直接引用您的大陆数组的字节数组。实际上,这意味着您创建索引的8位图像,就像普通字节数组一样。 (有一些方法可以将它与颜色数组实际组合起来并获得一个你可以使用的图像,请注意。这并不太难。)

对于实际的颜色匹配,最佳做法是在源图像上使用LockBits以直接访问底层字节数组。在下面的代码中,对GetImageData的调用获取了字节和数据步幅。然后,您可以遍历每行的字节数,并从表示一个像素的每个数据块构建颜色。如果您不想在支持不同的像素大小(如24bpp)时烦恼太多,那么快速的技巧就是在相同尺寸的新32bpp图像上绘制源图像(调用PaintOn32bpp),所以你总是可以简单地按每四个字节迭代一次,并按照3GB,1的顺序获取ARGB的字节值。我在这里忽略了透明度,因为它只会使什么是和不是颜色的概念复杂化。

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);
}

现在,回到过程;一旦你有了参考表,你需要的只是鼠标的坐标,你可以在索引(Y *宽度+ 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默认为“无”)来避免所有索引在顶部,底部,左侧和右侧没有相同的索引。这会移除一些较小的岛屿,并在任何相邻的大陆之间产生微小的间隙,但对于鼠标坐标检测,它仍然可以很好地匹配这些区域。这是RefineMap函数中的InitContinents调用。它得到的参数决定了索引需要多少相同的相邻值才能使它在修剪中存活下来。

通过制作未在所有边都被相同值包围的像素的地图,可以使用类似的检查邻近像素的技术来获得轮廓。

Test app showing detected regions, with the hovered continent replaced by an outline

相关问题