2
votes

I hope this is an interesting question for some people.

I want to create some pixels on randomized positions on an image, but the randomizing should be depended on the brightness, so the possibility to create a pixel should be high on a bright part of the image and low on a dark part (but still possible).

Lets take this image for example:

Clouds

I want to create a function SetRandomizedPixel which will get the bitmap and sets a pixel on a randomized position. The possibility that the pixel will be created on position 1 should be high, on position 2 medium and on position 3 low.

Clouds with numbers

I'm working with C# and Bitmap/Graphic, but that should not matter.

The only thing I need is a suggestion how to start because I can't figure out a good way to archive this.

Maybe read the brightness and positions of all image pixels and sort the list by brightness - but how can I do a randomize which prefer the brighter areas then?


UPDATE: Working on the answer

The following code results in images like this:

Randomized image

But if we look closer there are a lot of pixels on the left side:

Randomized image zoomed

That's the code (C#/MVC3):

public ActionResult RandomTest()
{

    const int points = 500;

    Bitmap bitmap = new Bitmap(Server.MapPath("~/Files/random-test.jpg"));
    Random random = new Random();

    int imageWidth = bitmap.Width;
    int imageHeight = bitmap.Height;

    float[][] weightedPixels = ConvertImageToGrayScale(bitmap);
    float totalValue = weightedPixels.Sum(i => i.Sum());

    for (var y = 0; y < imageHeight - 1; y++)
    {
        for (var x = 0; x < imageWidth - 1; x++)
        {
            weightedPixels[y][x] /= totalValue;
        }
    }

    for (var i = 0; i < points; i++)
    {
        double randomNumber = random.NextDouble();
        double currentSum = 0;
        for (var y = 0; y < imageHeight - 1; y++)
        {
            for (var x = 0; x < imageWidth - 1; x++)                    
            {
                currentSum += weightedPixels[y][x];
                if (currentSum >= randomNumber)
                {
                    bitmap.SetPixel(x, y, Color.Red);
                    break; 
                }
            }
        }
    }

    // output
    var stream = new MemoryStream();
    bitmap.Save(stream, ImageFormat.Png);
    return File(stream.ToArray(), "image/png");
}


public float[][] ConvertImageToGrayScale(Bitmap bm)
{
    var b = new Bitmap(bm);
    var data = new List<float[]>();
    for (var i = 0; i < b.Width; i++)
    {
        var row = new List<float>();
        for (int x = 0; x < b.Height; x++)
        {
            var oc = b.GetPixel(i, x);
            var grayScale = (int)((oc.R * 0.3) + (oc.G * 0.59) + (oc.B * 0.11));
            row.Add(grayScale);
        }
        data.Add(row.ToArray());
    }
    return data.ToArray();
}
3

3 Answers

4
votes

You could take a grey scale image of the picture and then sum up the total value of the pixels. Then if you divide each pixel by this total you get a weighting. Then you pick a number between 0 and 1 and start at the first weighted pixel and keep adding up the wieghts until you get to the first pixel who makes the sum greater than your random number. Then color that pixel on the original image however you want.

Edit: Psuedo Code

int imageWidth = ...
int imageHeight = ...
int[][] grayScale = ConvertImageToGrayScale(yourImage);
int totalValue = grayScale.Sum();

float[][] weightedPixels = grayScale.AsType(float[][]);
for (int y in 0...imageHeight-1)
     for (int x in 0...imageWidth-1)
         weightedPixels[y][x] /= totalValue;

float randomNumber = RandomReal();
float currentSum = 0;
for (int y in 0...imageHeight-1)
     for (int x in 0...imageWidth-1)
         currentSum += weightedPixels[y][x];
         if (currentSum >= randomNumber)
             break 2; // Here is your random pixel at (x, y)

Edit: Theory

The theory behind this is that we want to transform the image to a probability distribution based on the brightness of each pixel. This way brighter pixels are more likely to be picked than dimmer ones. The conversion to grayscale aids us in generating the PDF or weightedMatrix by reducing each pixel to a single number instead of an RGB triple. We take the sume of the grayscale matrix to get the total value which is needed to create the weighted matrix. The weighted matrix is intialized with the grayscale matrix, but then each element is divided by the total weight. This means that the sum of the weighted matrix is 1, which is a requirement for it to represent a PDF. Now we can pick a random probability, which is between 0 and 1. Using this we pick a pixel by summing along the weighted matrix until the sum is just greater than our probability. The pixel at which that happens is our randomly picked pixel. This is a standard way of randomly picking an item from a list with probabilities associated with each item.

Edit: Fixing Left-Side Line

The problem the OP brought up has to with boundry conditions from looping over the array in Y,X scan lines. If the code is reworked to use a linear array and then transform the index into an X,Y pair the solid line on the left is removed. If the middle chunk of the main method is changed to the following it works as intended:

float[] weightedPixels = ConvertImageToGrayScale(bitmap).SelectMany(r => r).ToArray();
float totalValue = weightedPixels.Sum();

for ( int i = 0; i < weightedPixels.Length; i++) {
        weightedPixels[i] /= totalValue;
}

for (int pIdx = 0; pIdx < points; pIdx++)
{
    double randomNumber = random.NextDouble();
    double currentSum = 0;
    for (int i = 0; i < weightedPixels.Length; i++)
    {
        currentSum += weightedPixels[i];
        if (currentSum >= randomNumber)
        {
            int y = i / imageWidth;
            int x = i % imageWidth;
            bitmap.SetPixel(x, y, Color.Red);
            break;
        }
    }
}
1
votes

You are looking for weighted random picks.

The answer to this question should get you started on the low-level programming: Weighted random numbers

This is how it looks in Mathematica, using the built-in function RandomChoice:

{w, h} = ImageDimensions@img;
brightness = ImageData@ColorConvert[img, "Grayscale"];

(* number of pixels to change *)
npix = 150;
(* new value for changed pixels *) 
newrgb = {1, 1, 1};   

Image[ ReplacePart[
    ImageData@img, 
    RandomChoice[
        Flatten@brightness -> Tuples[{Range[1, h], Range[1, w]}],
        npix] -> newrgb]]

enter image description here

1
votes

I like the weighted random numbers solutions of both troutinator and Matthias Odisio, but I think that for a large image this might be CPU intensive. Especially if several pixels have to be picked up. I would prefer a Monte Carlo simulation approach. Here is some pseudo-code:

Normalize(I)                //in [0,1] range, with 1=bright
n=0
While n < number_of_pixels_to_pick
  x = random(image_size(1)) //random pixel position
  y = random(image_size(2))
  p = random()              // in [0,1] range
  If p < I(x,y) do
    select pixel
    n=n+1
  Endif
Endwhile

Edit: Against this solution, if the image is very dark, it may loop for longer than the deterministic approach.