【问题标题】:Get the 2D coordinate outline of a region of an image (such as a country on a map)获取图像区域(如地图上的国家)的二维坐标轮廓
【发布时间】:2016-02-15 15:12:01
【问题描述】:

我将如何为图像的某个区域生成 2D 坐标,例如,如果这张地图上的一个国家被挑出来并且是唯一可见的国家: 但在相同大小的画布上,我将如何获取它的 2D 坐标?

因为我想使用 c# 基于这些坐标创建悬停/单击区域,所以我找不到可以检测例如空白画布中的形状并吐出其轮廓坐标的工具。

我主要认为这是我的措辞/术语问题,因为我觉得整个过程已经是一个“事情”,并且有据可查。

【问题讨论】:

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


    【解决方案1】:

    有很多方法可以完成你的任务,这里很少:

    查看Generating Polygons from Image (Filled Shapes),它几乎与您的相同,但起点略有不同。

    简而言之:

    1. 提取所有与白色像素相邻的非白色像素

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

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

      这将分隔国家/地区

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

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

    还有可能更容易实现的 A* 替代方案:

    1. 提取所有与白色像素相邻的非白色像素

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

    2. 将所有白色和透明色重新着色为单色(黑色)。

      从这里重新着色将意味着墙壁

    3. 申请A*寻路

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

    另一种选择是改编这个Finding holes in 2d point sets

    [注释]

    如果您的图像经过过滤(抗锯齿、缩放等),那么您需要进行颜色比较,并留出一定的误差范围,甚至可以移植到 HSV(取决于颜色失真的程度)。

    【讨论】:

      【解决方案2】:

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

      【讨论】:

        【解决方案3】:

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

        这要容易得多, 更容易。

        由于您显然必须为每个大陆分配一种颜色来识别它们,因此您可以遍历图像的所有像素,将每个像素的颜色与您的大陆颜色中最接近的匹配进行匹配,并填充数组中的每个字节与相应的找到的大陆索引。这样,您将获得一个直接引用您的大陆数组的字节数组。实际上,这意味着您创建一个索引的 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 调用。它获得的参数决定了一个索引需要多少相同的相邻值才能使其在修剪后存活。

        一种类似的检查相邻像素的技术可用于获取轮廓,方法是制作一个像素图,所有边都没有被相同的值包围。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-01-27
          • 2011-12-07
          • 1970-01-01
          • 2020-06-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多