5

I'm looking for an algorithm, I am programming in swift now but pseudocode or any reasonably similar "C family" syntax will do.

Imagine a large list of values, such as pixels in a bitmap. You want to pick each one in a visually random order, one at a time, and never pick the same one twice, and always end up picking them all.

I used it before in a Fractal generator so that it was not just rendering line by line, but built it up slowly in a stochastic way, but that was long ago, in a Java applet, and I no longer have the code.

I do not believe it used any pseudo-random number generator, and the main thing I liked about it is that it did not make the rendering time take longer than the just line by line approach. Any of the shuffling algorithms I looked at would make the rendering take longer with such a large number of values to deal with, unless I'm missing something.

EDIT: I used the shuffling an array approach. I shuffle once when the app loads, and it does not take that long anyway. Here is the code for my "Dealer" class.

import Foundation
import Cocoa
import Quartz

class Dealer: NSObject
{
  //########################################################
  var deck = [(CGFloat,CGFloat)]()
  var count = 0
  //########################################################
  init(_ w:Int, _ h:Int)
  {
    super.init()
    deck.reserveCapacity((w*h)+1)
    for y in 0...h
    {
      for x in 0...w
      {
        deck.append((CGFloat(x),CGFloat(y)))
      }
    }
    self.shuffle()
  }
  //########################################################
  func shuffle()
  {
    var j:Int = 0
    let total:Int = deck.count-1
    for i:Int in 0...total
    {
      j = Int(arc4random_uniform(UInt32(total)))
      deck.swapAt(i, j)
    }
  }
  //########################################################
  func deal() -> (CGFloat,CGFloat)
  {
    let result = deck[count]
    let total:Int = deck.count-1
    if(count<total) { count=count+1 } else { count=0 }
    return(result)
  }
  //########################################################
}

The init is called once, and it calls shuffle, but if you want you can call shuffle again if needed. Each time you need a "card" you call Deal. It loops to the beginning when the "deck" is done.

user1082474
  • 119
  • 8
  • I'm not sure if I understood the question correctly - do you wish to create a pixe-based 'fade' effect where in each iteration a single pixel is removed, looking randomly? – Codor Mar 28 '18 at 15:08
  • I guess picking elements in order is not good, there has to be some random element. So I updated the question. – anatolyg Mar 28 '18 at 15:21
  • Related: [Create Random Number Sequence with No Repeats](https://stackoverflow.com/q/693880/509868) – anatolyg Mar 28 '18 at 18:52
  • @Codor not fading out, but building up. Imagine starting with a black image and creating a new image one pixel at a time, in a way that looks random. – user1082474 Mar 28 '18 at 19:50

3 Answers3

2

if you got enough memory space to store all the pixel positions you can shuffle them:

const int xs=640;            // image resolution
const int ys=480;
color pixel[sz];             // image data
const int sz=xs*ys;          // image size 
int adr[sz],i,j;
for (i=0;i<sz;i++) adr[i]=i; // ordered positions
for (i=0;i<sz;i++)           // shuffle them
 {
 j = random(sz);             // pseudo-randomness with uniform distribution 
 swap(pixel[i],pixel[j]);
 }

this way you got guaranteed that each pixel is used once and most likely all of them are shuffled ...

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • I believe that process will take a long time, and delay when the rendering begins. On a modern screen the resolution is not 640 x 480. The one I'm sitting in front of is 1920 x 1080. – user1082474 Mar 28 '18 at 19:55
  • @user1082474 the generation and shuffle can be done just once or when hard coded then not at all (at least not at runtime) so speed is not an issue the only thing than counts is if you have enough memory for this. If you are using wrong pixel access than the speed is limited due to API used and not because of the algorithm used... – Spektre Mar 28 '18 at 21:18
  • This is the answer I picked. I shuffle once on the start of the app, and it does not take too long. – user1082474 Apr 01 '18 at 11:55
1

You need to implement a pseudo-random number generator with a theoretically known period, which is greater than but very close to the number of elements in your list. Suppose R() is a function that implements such a RNG.

Then:

for i = 1...N
    do
        idx = R()
    while idx > N
    output element(idx)
end
  • If the period of the RNG is greater than N, this algorithm is guaranteed to finish, and never output the same element twice
  • If the period of the RNG is close to N, this algorithm will be fast (i.e. the do-while loop will mostly do 1 iteration).
  • If the RNG has good quality, the visual output will look pleasant; here you have to do experiments and decide what is good enough for you

To find a RNG that has an exactly-known period, you should examine theory on RNGs, which is very extensive (maybe too extensive); Wikipedia has useful links. Start with Linear congruential generators: they are very simple, and there is a chance they will be of good enough quality.

anatolyg
  • 26,506
  • 9
  • 60
  • 134
  • I was hoping for something pre-existing, not research for a thesis. Plus I know it was something I did before, back in the 1990's. – user1082474 Mar 28 '18 at 19:56
  • If your list has a constant width, you can do the "research" manually, and just choose one RNG that has the proper period. For example, for a 1920x1080 image, choose period 2^21 or 2^21-1. The RNG itself is 1-2 lines of code. If you need to support a list of any length, then the code may be tricky, involving factorization, or (for [LFSRs](https://en.wikipedia.org/wiki/Linear-feedback_shift_register)) maybe a list of possible RNGs, from which you choose one. – anatolyg Mar 28 '18 at 20:14
1

Here's a working example based on linear feedback shift registers. Since an n-bit LFSR has a maximal sequence length of 2n−1 steps, this will work best when the number of pixels is one less than a power of 2. For other sizes, the pseudo-random coordinates are discarded until one is obtained that lies within the specified range of coordinates. This is still reasonably efficient; in the worst case (where w×h is a power of 2), there will be an average of two LSFR iterations per coordinate pair.

The following code is in Javascript, but it should be easy enough to port this to Swift or any other language.

Note: For large canvas areas like 1920×1024, it would make more sense to use repeated tiles of a smaller size (e.g., 128×128). The tiling will be imperceptible.

var lsfr_register, lsfr_mask, lsfr_fill_width, lsfr_fill_height, lsfr_state, lsfr_timer;
var lsfr_canvas, lsfr_canvas_context, lsfr_blocks_per_frame, lsfr_frame_rate = 50;

function lsfr_setup(width, height, callback, duration) {
    // Maximal length LSFR feedback terms
    // (sourced from http://users.ece.cmu.edu/~koopman/lfsr/index.html)
    var taps = [ -1, 0x1, 0x3, 0x5, 0x9, 0x12, 0x21, 0x41, 0x8E, 0x108, 0x204, 0x402,
                 0x829, 0x100D, 0x2015, 0x4001, 0x8016, 0x10004, 0x20013, 0x40013,
                 0x80004, 0x100002, 0x200001, 0x400010, 0x80000D, 0x1000004, 0x2000023,
                 0x4000013, 0x8000004, 0x10000002, 0x20000029, 0x40000004, 0x80000057 ];
    nblocks = width * height;
    lsfr_size = nblocks.toString(2).length;
    if (lsfr_size > 32) {
        // Anything longer than about 21 bits would be quite slow anyway
        console.log("Unsupposrted LSFR size ("+lsfr_size+")");
        return;
    }
    lsfr_register = 1;
    lsfr_mask = taps[lsfr_size];
    lsfr_state = nblocks;
    lsfr_fill_width = width;
    lsfr_fill_height = height;
    lsfr_blocks_per_frame = Math.ceil(nblocks / (duration * lsfr_frame_rate));
    lsfr_timer = setInterval(callback, Math.ceil(1000 / lsfr_frame_rate));
}

function lsfr_step() {
    var x, y;
    do {
        // Generate x,y pairs until they are within the bounds of the canvas area
        // Worst-case for an n-bit LSFR is n iterations in one call (2 on average)
        // Best-case (where w*h is one less than a power of 2): 1 call per iteration
        if (lsfr_register & 1) lsfr_register = (lsfr_register >> 1) ^ lsfr_mask;
        else lsfr_register >>= 1;
        y = Math.floor((lsfr_register-1) / lsfr_fill_width);
    } while (y >= lsfr_fill_height);
    x = (lsfr_register-1) % lsfr_fill_width;
    return [x, y];
}

function lsfr_callback() {
    var coords;
    for (var i=0; i<lsfr_blocks_per_frame; i++) {
        // Fetch pseudo-random coordinates and fill the corresponding pixels
        coords = lsfr_step();
        lsfr_canvas_context.fillRect(coords[0],coords[1],1,1);
        if (--lsfr_state <= 0) {
            clearInterval(lsfr_timer);
            break;
        }
    }
}

function start_fade() {
    var w = document.getElementById("w").value * 1;
    var h = document.getElementById("h").value * 1;
    var dur = document.getElementById("dur").value * 1;
    lsfr_canvas = document.getElementById("cv");
    lsfr_canvas.width = w;
    lsfr_canvas.height = h;
    lsfr_canvas_context = lsfr_canvas.getContext("2d");
    lsfr_canvas_context.fillStyle = "#ffff00";
    lsfr_canvas_context.fillRect(0,0,w,h);
    lsfr_canvas_context.fillStyle = "#ff0000";
    lsfr_setup(w, h, lsfr_callback, dur);
}
Size:
<input type="text" size="3" id="w" value="320"/>
×
<input type="text" size="3" id="h" value="240"/>
in
<input type="text" size="3" id="dur" value="3"/>
secs
<button onclick="start_fade(); return 0">Start</button>
<br />
<canvas id="cv" width="320" height="240" style="border:1px solid #ccc"/>
r3mainer
  • 23,981
  • 3
  • 51
  • 88