Simple Image Fuzziness Techniques

At Bing we do something super cool to protect our users: whenever we detect that an image or video may contain adult content, and if the user's search safety setting is set to "Moderate" , we do a nice fuzzy of the image to prevent potential adult leakage, such as in this example:


You won't find this feature on Google. As a matter of fact we at Bing love playing with images, even trying to guess your age based on your own picture!
We use very sophisticated, state-of-the-art techniques of image processing to come up with the ideal masks, filters and algorithms in order to properly perform the fuzzing task at massive scale. Suppose that you were to write a very-very simple code to perform any kind of fuzzing on a given image - how would you do that? In this post I discuss three very simple techniques to perform such a fuzzing (none of them are what we actually use here in Bing! the ones below is just for fun coding!). I've named the techniques as follows:
  1. Random Pixel Shuffling
  2. Proximity-Bound Random Pixel Shuffling
  3. Neighborhood Amortization
Fancy titles for simple algorithms (that's usually the case in Computer Science). To exemplify these techniques, we'll be playing with the arguably most famous celebrities selfie ever:

 
 
The first algorithm is the Random Pixel Shuffling. As the name suggests, here is what we'll do:
  • Pick a number of random shuffles that you want to try - any number, really
  • Pick two random pixels in the image - any two pixels!
  • Swap them!
Yeah, it doesn't get simpler than that. Not ideal, but a start. Doing that, let's see what we get:
 
 
 
OK... The problem with this technique is that it mixes up pixels from the entire image, which gives a kind of pixelated flavor to it. Let's see if the next one improves it.
The second algorithm is the Proximity-Bound Random Pixel Shuffling. Almost exactly the same logic as the above, but you only swap pixels within the same neighborhood:
  • Pick a number of random shuffles that you want to try - any number, really
  • Pick two random pixels in the image - any two pixels!
  • Are these pixels in the same neighborhood? If so, swap them!
The neighborhood can be defined in different ways - one simple way is to use Euclidian Distance. Also, you can improve the speed of the algorithm by not getting "any two pixels" in the second step, but rather any two pixels "within the same neighborhood" already. Doing so, we get something like this image:
 
 
That's better! There is a little extra fuzziness and yet the cluster of colors remains kind of still visible. Still, the pixelated factor remains there! Now for a very different technique.
The third and final algorithm is the Neighborhood Amortization. This one is very different from the previous two. The idea here is the following: we're going to pick random "seeds", which are pixels. From these seeds, we'll expand thru their neighbors and as we do that we'll "amortize" the color of the neighbors by averaging it out. In a more recipe-like flavor:
  • Pick a random number of seeds
  • For each seed, build a cluster of neighbors around this seed. Do this using a Breadth-First Search, or BFS.
  • Traverse the cluster
  • As you do so, amortize the color of the pixel by averaging it out with the neighbor it was reached from
That way we'll create a smother version of the fuzziness and the pixilation problem goes away, giving room for a more distorted (blurred is a better word here) image. Let's see what happens:
 
 
 
I like this one better, although computationally speaking, it is more expensive than the first two.
 
In reality nowadays with any task, especially machine learning oriented ones, the optimal solution comes from combining multiple techniques together. Combining (chaining) the three techniques that have been described here, yields the following results:
 
 

 
Finally, one more example when we chain the three methods together (NeighborhoodAmortization(Proximity-BoundShuffling(ShufflingAll(Image)))):
 
 
I hope you've enjoyed it - have fun playing with images! Code is below. Au revoir!
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
using System.Collections;
namespace ImageFuzziness
{
    class Program
    {
        static void Main(string[] args)
        {
            Bitmap image = new Bitmap(args[0]);
            Console.WriteLine("Shuffling All...");
            ShuffleAll(image, args[1]);
            Console.WriteLine("Done");
            image = new Bitmap(args[0]);
            Console.WriteLine("Proximity-Bound Shuffle...");
            ProximityBoundShuffle(image, args[2], (image.Width * image.Height) / 100);
            Console.WriteLine("Done");
            image = new Bitmap(args[0]);
            Console.WriteLine("Neighborhood Amortization...");
            NeighborhoodAmortization(image, args[3], (image.Width * image.Height) / 100);
            Console.WriteLine("Done");
        }
        static void ShuffleAll(Bitmap image,
                               string outputFileName)
        {
            if (image == null)
            {
                return;
            }
            Random rd = new Random();
            int numberRandomChanges = rd.Next((image.Height * image.Width) / 4, (image.Height * image.Width) / 3);
            for (int i = 0; i < numberRandomChanges; i++)
            {
                int rd_x1 = rd.Next(0, image.Width);
                int rd_y1 = rd.Next(0, image.Height);
                Color color1 = image.GetPixel(rd_x1, rd_y1);
                int rd_x2 = rd.Next(0, image.Width);
                int rd_y2 = rd.Next(0, image.Height);
                Color color2 = image.GetPixel(rd_x2, rd_y2);
               
                //Switch
                image.SetPixel(rd_x1, rd_y1, color2);
                image.SetPixel(rd_x2, rd_y2, color1);
            }
            image.Save(outputFileName);
        }
        static void ProximityBoundShuffle(Bitmap image,
                                          string outputFileName,
                                          int proximity)
        {
            if (image == null)
            {
                return;
            }
            Random rd = new Random();
            int numberRandomChanges = rd.Next((image.Height * image.Width) / 3, 2 * (image.Height * image.Width) / 3);
            for (int i = 0; i < numberRandomChanges; i++)
            {
                int rd_x1 = rd.Next(0, image.Width);
                int rd_y1 = rd.Next(0, image.Height);
                Color color1 = image.GetPixel(rd_x1, rd_y1);
                int rd_x2 = rd.Next(0, image.Width);
                int rd_y2 = rd.Next(0, image.Height);
                Color color2 = image.GetPixel(rd_x2, rd_y2);
                //Conditional Switch
                if ((rd_x1 - rd_x2) * (rd_x1 - rd_x2) + (rd_y1 - rd_y2) * (rd_y1 - rd_y2) <= proximity * proximity)
                {
                    image.SetPixel(rd_x1, rd_y1, color2);
                    image.SetPixel(rd_x2, rd_y2, color1);
                }
            }
            image.Save(outputFileName);
        }
        static void NeighborhoodAmortization(Bitmap image,
                                             string outputFileName,
                                             int neighborhoodSize)
        {
            if (image == null)
            {
                return;
            }
            Random rd = new Random();
            int numberOfSeeds = 20;
            for (int i = 0; i < numberOfSeeds; i++)
            {
                int seed_x = rd.Next(0, image.Width);
                int seed_y = rd.Next(0, image.Height);
                Queue<QueueItem> queue = new Queue<QueueItem>();
                Hashtable htVisited = new Hashtable();
                int key = seed_x * image.Width + seed_y;
                htVisited.Add(key, true);
                QueueItem queueItem = new QueueItem(Color.Black, seed_x, seed_y, 0);
                queue.Enqueue(queueItem);
                while (queue.Count > 0)
                {
                    queueItem = queue.Dequeue();
                    if (queueItem.distanceFromCenter > 0)
                    {
                        Color currentColor = image.GetPixel(queueItem.x, queueItem.y);
                        Color avgColor = Color.FromArgb(((int)currentColor.R + (int)queueItem.colorFrom.R) / 2,
                                                        ((int)currentColor.G + (int)queueItem.colorFrom.G) / 2,
                                                        ((int)currentColor.B + (int)queueItem.colorFrom.B) / 2);
                        image.SetPixel(queueItem.x, queueItem.y, avgColor);
                    }
                    for (int dx = -1; dx <= 1; dx++)
                    {
                        for (int dy = -1; dy <= 1; dy++)
                        {
                            int neighbor_x = queueItem.x + dx;
                            int neighbor_y = queueItem.y + dy;
                            key = neighbor_x * image.Width + neighbor_y;
                            if (neighbor_x >= 0 &&
                                neighbor_x < image.Width &&
                                neighbor_y >= 0 &&
                                neighbor_y < image.Height &&
                                !htVisited.ContainsKey(key) &&
                                (dx != 0 || dy != 0) &&
                                queueItem.distanceFromCenter + 1 <= neighborhoodSize)
                            {
                                htVisited.Add(key, true);
                                QueueItem neighborItem = new QueueItem(image.GetPixel(queueItem.x, queueItem.y),
                                                                        neighbor_x,
                                                                        neighbor_y,
                                                                        queueItem.distanceFromCenter + 1);
                                queue.Enqueue(neighborItem);
                            }
                        }
                    }
                }
            }
            image.Save(outputFileName);
        }
    }
    public class QueueItem
    {
        public Color colorFrom;
        public int x;
        public int y;
        public int distanceFromCenter;
        public QueueItem(Color colorFrom,
                         int x,
                         int y,
                         int distanceFromCenter)
        {
            this.colorFrom = colorFrom;
            this.x = x;
            this.y = y;
            this.distanceFromCenter = distanceFromCenter;
        }
    }
}







Comments

  1. Very nice post Marcelo! I love image processing algorithms because they usually come with best demos :) A few more algorithms and you'll be competing with the Instagram :)

    I think this is a great way to get people interested in computer science, since it shows how powerful a few lines of code could be and makes people wonder about what could be done with a bit more work and effort.

    Have a marvelous start of the week!

    ReplyDelete
  2. This reminded me of my engineering project on Fractal image compression - my initial compression outputs had some much image loss resulted in something like random pixel shuffling ;)

    Very simple 3 step explanation of each method makes it easier for anyone to understand. !

    ReplyDelete

Post a Comment

Popular posts from this blog

Changing the root of a binary tree

Prompt Engineering and LeetCode

ProjectEuler Problem 719 (some hints, but no spoilers)