1

How would I go about generating the 2D coordinates for an area of an image, so for example if one of the countries on this map was singled out and was the only one visible: image1 but on a canvas the same size, how would I go about getting the 2D coordinates for it?

As I then want to create hover/click areas based on these coordinates using c#, I'm unable to find a tool which can detect for example a shape within a blank canvas and spit out its outline coordinates.

I mainly believe this to be a phrasing/terminology issue on my part, as I feel this whole process is already a "thing", and well documented.

Spektre
  • 49,595
  • 11
  • 110
  • 380
Vereonix
  • 1,341
  • 5
  • 27
  • 54

3 Answers3

2

There are many ways to achieve your task here are few:

Look at Generating Polygons from Image (Filled Shapes) which is Almost duplicate of yours but has a bit different start point.

In a nutshell:

  1. extract all non white pixels which are neighboring white pixel

    Just loop through whole image (except outer border pixels) if processed pixel is not white then look to its 4/8 neighbors of processed pixel. If any of them is different color then add the processed pixel color and coordinates to a list.

  2. sort the point list by color

    This will separate countries

  3. apply closed loop / connectivity analysis

    This is vectorisation/polygonize process. Just join not yet used neighboring pixels from list to form lines ...

There is also A* alternative for this that might be easier to implement:

  1. extract all non white pixels which are neighboring white pixel

    Just loop through whole image (except outer border pixels) if processed pixel is not white then look to its 4/8 neighbors of processed pixel. If none of them is different color then clear current pixel with some unused color (black).

  2. recolor all white and the clear color to single color (black).

    from this the recolor color will mean wall

  3. Apply A* path finding

    find first non wall pixel and apply A* like growth filling. When you done filling then just trace back remembering the order of points in a list as a polygon. Optionally joining straight line pixels to single line ...

Another option is adapt this Finding holes in 2d point sets

[notes]

If your image is filtered (Antialiasing,scaling,etc) then you need to do the color comparisons with some margin for error and may be even port to HSV (depends on the level of color distortion).

Community
  • 1
  • 1
Spektre
  • 49,595
  • 11
  • 110
  • 380
0

You can use opencv's findcontour() function. See documentation here: http://docs.opencv.org/2.4/doc/tutorials/imgproc/shapedescriptors/find_contours/find_contours.html.

David Shaked
  • 3,171
  • 3
  • 20
  • 31
0

I think you're going at this the wrong way. Outlines of continents are madness; they are often made up of several parts with lots of small islands. And, you don't need the coordinates of the continents on the image; looking up if your current coordinates are in a list would take far too long. Instead, you should do the opposite: make an index table of the whole image, on which is indicated for each pixel which continent it belongs to.

And that's much, much easier.

Since you obviously have to assign a colour to each continent to identify them, you can go over all of the image's pixels, match each pixel's colour to the closest match in the colours of your continents, and fill each byte in the array with the corresponding found continent index. This way, you get a byte array that directly references your continents array. Effectively, this means you create an indexed 8-bit image, just as a plain bytes array. (There are methods to actually combine this with the colours array and get an image you can use, mind you. It's not too hard.)

For the actual colour matching, the best practice is to use LockBits on the source image to get direct access to the underlying bytes array. In the code below, the call to GetImageData gets me the bytes and the data stride. Then you can iterate over the bytes per line, and build a colour from each block of data that represents one pixel. If you don't want to bother too much with supporting different pixel sizes (like 24bpp), a quick trick is to just paint the source image on a new 32bpp image of the same dimensions (the call to PaintOn32bpp), so you can always simply iterate per four bytes and take the byte values in the order 3,2,1,0 for ARGB. I ignored transparency here because it just complicates the concept of what is and isn't a colour.

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

The GetImageData function:

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

Now, back to the process; once you have that reference table, all you need are the coordinates of your mouse and you can check the reference map at index (Y*Width + X) to see what area you're in. To do that, you can add a MouseMove listener on an ImageBox, like this:

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

Note that a simple generated map produced by nearest colour matching may have errors; when I did automatic mapping of this world map's colours, the border between blue and red, and some small islands in Central America, ended up identifying as Antarctica's purple colour, and some other rogue pixels appeared around the edges of different continents too.

This can be avoided by clearing (I used 0 as default "none") all indices not bordered by the same index at the top, bottom, left and right. This removes some smaller islands, and creates a slight gap between any neighbouring continents, but for mouse coordinates detection it'll still very nicely match the areas. This is the RefineMap call in my InitContinents function. The argument it gets determines how many identical neighbouring values an index needs to allow it to survive the pruning.

A similar technique with checking neigbouring pixels can be used to get outlines, by making a map of pixels not surrounded at all sides by the same value.

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

Nyerguds
  • 5,360
  • 1
  • 31
  • 63