Counting Regions in Images

Suppose that you're given an image on white background and you have a task to count the number of regions in that image where a region is loosely defined by these properties:

A) A region is a set of adjacent pixels, AND
B) The absolute color difference between adjacent pixels are within a certain threshold, AND
C) The absolute color difference between any two pixels in the region are within a certain threshold

In other words, a region consists of a chunk of pixels near each other that more or less share the same color. How would you write an algorithm to detect such regions?

One option is to implement the following approach:
1) Define two thresholds: one is between the adjacent pixels (called the "neighbor threshold") and the other one is between any pixel in the region and the center pixel (called the "seed threshold"). The tighter these thresholds the more precised the algorithm (precision-based), and conversely the more relaxed the thresholds the less precised it will be (recall-based).
2) Ignore the white-background
3) Scan the image and for each non-background pixel, assume that the pixel is the beginning (center) of a region
4) From the pixel on #3, do a Breadth-First-Search (BFS) using the thresholds specified in #1. Color the regions using your preferred color, mark the pixels in the region as visited.

Assuming an image of NxN, this algorithm will run in O(2*N^2).

The images below illustrate the input to the program (left) and the output (right) painting the regions in chocolate color:




As you can see it is able to detect simple regions relatively well. Of course the assumption of the white background is crucial here for the correctness of the algorithm. Code's down below, cheers, Marcelo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Collections;

namespace CountRegions
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                Console.WriteLine("Usage: CountRegions.exe <image file>");
                return;
            }

            MyImage myImage = new MyImage(args[0]);

            Console.WriteLine("Processing...");
            myImage.CountRegions();
            Console.WriteLine("Done!");
        }
    }

    class MyImage
    {
        Bitmap image = null;

        public MyImage(string imageFileName)
        {
            image = new Bitmap(imageFileName);
        }

        public void CountRegions()
        {
            bool[,] mark = new bool[image.Width, image.Height];
            int thresholdNeighbor = 5;
            int thresholdSeed = 10;

            for (int c = 0; c < image.Width; c++)
            {
                for (int r = 0; r < image.Height; r++)
                {
                    Color color = image.GetPixel(c, r);
                    if (!(color.R == 255 && color.G == 255 && color.B == 255))
                    {
                        if (!mark[c, r])
                        {
                            RegionMarkdownBFS(c, r, thresholdNeighbor, thresholdSeed, mark);
                        }
                    }
                }
            }

            Random rd = new Random();
            string outputFileName = "output_" + rd.Next().ToString() + ".jpg";
            image.Save(outputFileName);
            Console.WriteLine("File saved to {0}", outputFileName);
        }

        private void RegionMarkdownBFS(int c, int r, int thresholdNeighbor, int thresholdSeed, bool[,] mark)
        {
            Queue queue = new Queue();

            Color pixelSeed = image.GetPixel(c, r);
            mark[c, r] = true;
            image.SetPixel(c, r, Color.Chocolate);

            long key = r * image.Width + c;
            queue.Enqueue(key);
            while (queue.Count > 0)
            {
                key = (long)queue.Dequeue();

                int currentRow = (int)(key / image.Width);
                int currentCol = (int)(key % image.Width);

                Color currentColor = image.GetPixel(currentCol, currentRow);

                //Mark in the image
                image.SetPixel(currentCol, currentRow, Color.Chocolate);

                //Check neighbors
                for (int or = -1; or <= 1; or++)
                {
                    for (int oc = -1; oc <= 1; oc++)
                    {
                        if (currentRow + or >= 0 &&
                            currentRow + or < image.Height &&
                            currentCol + oc >= 0 &&
                            currentCol + oc < image.Width &&
                            !mark[currentCol + oc, currentRow + or])
                        {
                            Color neighborColor = image.GetPixel(currentCol + oc, currentRow + or);
                            if (Math.Abs(neighborColor.R - currentColor.R) <= thresholdNeighbor &&
                                Math.Abs(neighborColor.G - currentColor.G) <= thresholdNeighbor &&
                                Math.Abs(neighborColor.B - currentColor.B) <= thresholdNeighbor)
                            {
                                if (Math.Abs(neighborColor.R - pixelSeed.R) <= thresholdSeed &&
                                    Math.Abs(neighborColor.G - pixelSeed.G) <= thresholdSeed &&
                                    Math.Abs(neighborColor.B - pixelSeed.B) <= thresholdSeed)
                                {
                                    queue.Enqueue((currentRow + or) * image.Width + currentCol + oc);
                                    mark[currentCol + oc, currentRow + or] = true;
                                }
                            }
                        }
                    }
                }
            }
        }

        public void Print()
        {
            for (int r = 0; r < image.Height; r++)
            {
                for (int c = 0; c < image.Width; c++)
                {
                    Color color = image.GetPixel(c, r);
                    Console.Write("({0},{1},{2}) ", color.R, color.G, color.B);
                }
                Console.WriteLine();
            }
        }
    }
}

Comments

Popular posts from this blog

Advent of Code - Day 6, 2024: BFS and FSM

Golang vs. C#: performance characteristics (simple case study)

Advent of Code - Day 7, 2024: Backtracking and Eval