Pixel distribution algorithm

Asked

Viewed 818 times

5

Well, lately I’ve been thinking about algorithms to create random maps/ images and I came up with a question that I’m going to post here for you to clarify.

Suppose I have green and yellow pixels. I’m not working with a matrix, but a one-dimensional vector because of the project.

Someone could quote me an algorithm outline or where to start to distribute the pixels grouped each time I run the program, however, that is not always the same result, that create shapes with these green and yellow pixels?

  • In what language are you programming? What do you mean by grouped? A random sequence is not enough?

  • I’m doing in c++. The idea is to generate a map with earth/sand, IE, it would be interesting to have zones where similar pixels were grouped.

  • I added the tag [tag:games] just because the question applies to automated content generation (maps, in this case).

2 answers

7

As I have already commented, there are a multitude of suggestions that can be made. But unfortunately you have not given feedback regarding your problem, so there is no way to suggest something more appropriate. Patience.

Your current answer (which you have already accepted, by the way) has a possible suggestion, which works well. But, as you yourself noted in the comments, working with data as a single one-dimensional vector also has its difficulties. That is why I suggested that you work with the image in the traditional format (that is, two-dimensional) and only then transform the data to the one-dimensional format you need (who knows why, since you don’t describe it). This approach will give you the opportunity to work with several different algorithms and, mainly, to reuse ready-made code that is out there.

Finally, here is a simple implementation of the suggestion using the region growth algorithm I mentioned earlier. The explanation is documented in the code.

#include <cstdlib>
#include <iostream>
#include <ctime>
#include <algorithm>
#include "image.h"

// ------------------------------
// Remova se não usar OpenCV
// ------------------------------
#include <opencv2\opencv.hpp>
using namespace cv;
// ------------------------------

/**
 * Função de geração de um número inteiro aleatório entre um intervalo dado.
 * @param min Inteiro com o mímimo número esperado.
 * @param max Inteiro com o máximo número esperado.
 * @return Inteiro com o valor sorteado.
 */
int random(int min, int max)
{
    int n = max - min + 1;
    int remainder = RAND_MAX % n;
    int x;
    do
    {
        x = rand();
    } while(x >= RAND_MAX - remainder);
    return min + x % n;
}

/**
 * Gera um mapa de regiões aleatórias.
 * @param colors Vetor de Pixels com as cores possívels
 * para as regiões.
 * @param width Inteiro positivo com o largura da imagem gerada.
 * @param width Inteiro positivo com o altura da imagem gerada.
 * @param iterations Inteiro positivo com o número de interações para
 * o agrupamento de pixels sob a imagem inicialmente aleatória. O default
 * é 100 mil. Se esse valor for 0, nenhuma permutação é realizada e a imagem
 * do mapa resultante contém apenas ruído.
 * @param window Inteiro positivo com o tamanho da janela de comparação para
 * o agrupamento. Essa janela tem que ser um número cuja raíz quadrada resulta
 * em um inteiro ímpar (9, 25, 49, 81, etc) de valor mínimo 9.
 * @return Objeto Image com a imagem gerada.
 */
Image genMap(vector<Pixel> colors, uint width, uint height, uint iterations = 100000, uint window = 9)
{
    // Checa os parâmetros de entrada
    double windowRoot = std::sqrt(window);
    if(windowRoot < 3 || (windowRoot - int(windowRoot)) != 0 || int(windowRoot) % 2 == 0)
        throw std::invalid_argument("tamanho de janela invalido");

    // Usa o horário atual como semente para o gerador de números aleatórios
    std::srand((uint)std::time(0));

    // ++++++++++++++++++++++++++++++++++++++
    // 1 - Cria o mapa totalmente em preto
    // ++++++++++++++++++++++++++++++++++++++
    Image map(width, height);

    // ++++++++++++++++++++++++++++++++++++++
    // 2 - Sorteia aleatoriamente os pixels nas cores recebidas
    // ++++++++++++++++++++++++++++++++++++++
    for(uint x = 0; x < width; x++)
    {
        for(uint y = 0; y < height; y++)
        {
            uint c = random(0, colors.size()-1);
            map.pixel(x, y) = colors[c];
        }
    }

    // ++++++++++++++++++++++++++++++++++++++
    // 3 - Agrupa os pixels aleatoriamente, para formar as regiões  
    // ++++++++++++++++++++++++++++++++++++++
    uint offset = int(std::ceil(windowRoot) / 2);
    for(uint i = 0; i < iterations; i++)
    {
        // Sorteia um pixel qualquer na imagem
        int xCenter = random(0, width-1);
        int yCenter = random(0, height-1);
        Pixel color = map.pixel(xCenter, yCenter);

        // Conta quantos são os pixels vizinhos que são iguais
        // ao pixel "central" sorteado
        int count = 0;
        for(uint x = xCenter - offset; x <= xCenter + offset; x++)
        {
            for(uint y = yCenter - offset; y <= yCenter + offset; y++)
            {
                // Desconsidera pixels fora da área da imagem
                if(x < 0 || x >= width || y < 0 || y >= height)
                    continue;

                // Incrementa se for a mesma cor
                if(map.pixel(x, y) == color)
                    count++;
            }
        }

        // Se mais da metade da janela é da mesma cor do pixel central,
        // transforma todos os vizinhos na cor do pixel central
        if(count > int(window / 2))
        {
            for(uint x = xCenter - offset; x <= xCenter + offset; x++)
            {
                for(uint y = yCenter - offset; y <= yCenter + offset; y++)
                {
                    // Desconsidera pixels fora da área da imagem
                    if(x < 0 || x >= width || y < 0 || y >= height)
                        continue;

                    // Define a cor
                    map.pixel(x, y) = color;
                }
            }
        }
    }

    // ++++++++++++++++++++++++++++++++++++++
    // Pronto! Só devolve a imagem gerada. :)
    // ++++++++++++++++++++++++++++++++++++++
    return map;
}

// ------------------------------
// Remova se não usar OpenCV
// ------------------------------
void showImage(char *title, Mat image)
{
    namedWindow(title, cv::WINDOW_AUTOSIZE);
    imshow(title, image);
}
// ------------------------------

/**
 * Converte a imagem (bidimensional) para um longo vetor de
 * valores (unidimensional).
 * @return Vetor de com todos os valores dos pixels (na ordem
 * R, G, B) agrupados linha após linha.
 */
vector<uchar> imageToVector(const Image &image)
{
    vector<uchar> ret;
    for(uint y = 0; y < image.height(); y++)
    {
        for(uint x = 0; x < image.width(); x++)
        {
            Pixel p = image.pixel(x, y);
            ret.push_back(p.red);
            ret.push_back(p.green);
            ret.push_back(p.blue);
        }
    }
    return ret;
}

/**
 * Converte um longo vetor de valores (unidimensional) para uma
 * imagem (bidimensional).
 * @param data Vetor com todos os valores dos pixels (na ordem
 * R, G, B) agrupados linha após linha.
 * @param width Inteiro positivo com a largura da imagem.
 * @param height Inteiro positivo com a altura da imagem.
 */
Image vectorToImage(vector<uchar> data, uint width, uint height)
{
    if(data.size() != (width * height * 3))
        throw std::invalid_argument("os dados e as dimensoes recebidos não são condizentes com uma imagem em 3 canais");

    Image ret(width, height);
    for(uint i = 0; i < data.size(); i += 3)
    {
        int x = (i / 3) % width;
        int y = (i / 3) / width;
        ret.pixel(x, y) = Pixel(data[i], data[i + 1], data[i + 2]);
    }

    return ret;
}

/**
 * Função principal.
 */
int main()
{
    Pixel amarelo(255, 255, 0);
    Pixel verde(0, 255, 0);

    Image t1 = genMap({ amarelo, verde }, 400, 300, 0);
    Image t2 = genMap({ amarelo, verde }, 400, 300, 100000);
    Image t3 = genMap({ amarelo, verde }, 400, 300, 100000, 25);
    Image t4 = genMap({ amarelo, verde }, 400, 300, 500000, 25);

    cout << "Teste 1: " << endl;
    cout << t1 << endl << endl;

    cout << "Teste 2: " << endl;
    cout << t2 << endl << endl;

    cout << "Teste 3: " << endl;
    cout << t3 << endl << endl;

    cout << "Teste 4: " << endl;
    cout << t4 << endl << endl;

    // ------------------------------
    // Remova se não usar OpenCV
    // ------------------------------
    showImage("Teste 1 - Aleatoria", t1.toMat());
    showImage("Teste 2 - Janela: 9 e Iteracoes: 100 mil", t2.toMat());
    showImage("Teste 3 - Janela: 25 e Iteracoes: 100 mil", t3.toMat());
    showImage("Teste 4 - Janela: 25 e Iteracoes: 500 mil", t4.toMat());
    waitKey(0);
    // ------------------------------

    cout << endl << "Teste de Conversao" << endl;
    Image t = genMap({ amarelo, verde }, 4, 10, 0);

    cout << "Imagem bidimensional original: " << endl;
    cout << t << endl << endl; 

    cout << "Convertida para vetor unidimensional: " << endl;
    vector<uchar> v = imageToVector(t);
    for(uint i = 0; i < v.size(); i++)
        cout << int(v[i]) << " ";
    cout << endl << endl;

    cout << "Convetida de volta para imagem bidimensional: " << endl;
    cout << vectorToImage(v, 4, 10) << endl;

    return 0;
}

It generates the following text output:

Teste 1:
Largura: 400 Altura: 300
Pixels:
[(255,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(255,255,0),(255,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(255,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(0,255,0),(0,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(0,255,0),(255,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0),(255,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
mais 290 linhas...


Teste 2:
Largura: 400 Altura: 300
Pixels:
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(0,255,0),(0,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
mais 290 linhas...


Teste 3:
Largura: 400 Altura: 300
Pixels:
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0),(255,255,0), mais 390 colunas...]
mais 290 linhas...


Teste 4:
Largura: 400 Altura: 300
Pixels:
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
[(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0),(0,255,0), mais 390 colunas...]
mais 290 linhas...



Teste de Conversao
Imagem bidimensional original:
Largura: 4 Altura: 10
Pixels:
[(0,255,0),(0,255,0),(255,255,0),(255,255,0)]
[(255,255,0),(0,255,0),(0,255,0),(0,255,0)]
[(255,255,0),(255,255,0),(0,255,0),(0,255,0)]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0)]
[(255,255,0),(0,255,0),(0,255,0),(255,255,0)]
[(0,255,0),(255,255,0),(0,255,0),(0,255,0)]
[(0,255,0),(255,255,0),(0,255,0),(255,255,0)]
[(0,255,0),(255,255,0),(255,255,0),(255,255,0)]
[(0,255,0),(255,255,0),(0,255,0),(255,255,0)]
[(255,255,0),(255,255,0),(255,255,0),(0,255,0)]


Convertida para vetor unidimensional:
0 255 0 0 255 0 255 255 0 255 255 0 255 255 0 0 255 0 0 255 0 0 255 0 255 255 0 255 255 0 0 255 0 0 255 0 255 255 0 255 255 0 255 255 0 255 255 0 255 255 0 0 255 0 0 255 0 255 255 0 0 255 0 255 255 0 0 255 0 0 255 0 0 255 0 255 255 0 0 255 0 255 255 0 0 255 0 255 255 0 255 255 0 255 255 0 0 255 0 255 255 0 0 255 0 255 255 0 255 255 0 255 255 0 255 255 0 0 255 0

Convetida de volta para imagem bidimensional:
Largura: 4 Altura: 10
Pixels:
[(0,255,0),(0,255,0),(255,255,0),(255,255,0)]
[(255,255,0),(0,255,0),(0,255,0),(0,255,0)]
[(255,255,0),(255,255,0),(0,255,0),(0,255,0)]
[(255,255,0),(255,255,0),(255,255,0),(255,255,0)]
[(255,255,0),(0,255,0),(0,255,0),(255,255,0)]
[(0,255,0),(255,255,0),(0,255,0),(0,255,0)]
[(0,255,0),(255,255,0),(0,255,0),(255,255,0)]
[(0,255,0),(255,255,0),(255,255,0),(255,255,0)]
[(0,255,0),(255,255,0),(0,255,0),(255,255,0)]
[(255,255,0),(255,255,0),(255,255,0),(0,255,0)]

It also generates the following graphical output (dependent on Opencv - do not use if not installed):

inserir a descrição da imagem aqui

Quickly, how this code works:

  1. It creates an image (a two-dimensional vector of pixels, with 3 channels since vc speaks of colors - could be a channel only for grayscale images) and sets the pixels randomly according to the given colors (test image 1).
  2. Then, it groups the pixels by making a kind of iterative region growth: it draws a pixel and checks the neighborhood colors (according to a preset window); if the neighbors have enough of the color of the pixel drawn, it changes all the neighborhood for that color, then making it homogeneous (i.e., a grouping).
  3. Repeat step 2 for a new interaction.

Thus, it should be possible to notice that the number of interactions and the size of the window influence the result. However, many interactions naturally make processing take longer. It is important to find a suitable balance for your need.

I implemented two auxiliary classes called Image and Pixel to allow you to run this code in a generic way (without Opencv, which I only use to display the images because they are cool! ). But this implementation is very crude and simple. If you can, use directly another structure (such as the Mat of Opencv, which already implements a series of algorithms that you could use to manipulate and generate more interesting structures).

If you originally have the data in a one-dimensional vector, convert it to a two-dimensional structure, use this algorithm and then convert it back to one-dimensional.

Finally, two additional information:

  1. If your need involves generating content (a terrain for a game, perhaps?), there are many other approaches that may be useful. In that case, I suggest also take a look at this my other answer on Procedural Content Generation.

  2. If the real need behind the idea of the one-dimensional vector is something related to the serialization of the image, there are better ways of do this. In many cases even native to the implementation of library you will use. In Opencv, for example, you have direct access to the attribute data class Mat if needed, in addition to that exist various suggestions out there how to do.

The complete code (including auxiliary classes Image and Pixel) is on Github.

EDIT: I had also commented on other possibilities, among them the use of Cellular Automata. To see a functional example well cool that can be useful in the generation of maps, I suggest playing with this implementation in Javascript of the famous cellular automaton called Game of Life.

6


Want something like the one below? Click the blue button to run.

function agrupar(antes) {
    var depois = "";
    for (var i = 0; i < antes.length; i++) {
        var a = i === 0 ? "A" : antes.charAt(i - 1);
        var b = antes.charAt(i);
        var c = i === antes.length - 1 ? "A" : antes.charAt(i + 1);
        var m = (a === "V" ? 1 : 0) + (b === "V" ? 1 : 0) + (c === "V" ? 1 : 0);
        depois += m >= 2 ? "V" : "A";
    }
    return depois;
}

var cores = "VA";

var resultado = "";
for (var i = 0; i < 50; i++) {
    resultado += cores.charAt(Math.floor(Math.random() * cores.length));
}

$("#antes").html(resultado);
resultado = agrupar(resultado);
$("#depois1").html(resultado);
resultado = agrupar(resultado);
$("#depois2").html(resultado);
resultado = agrupar(resultado);
$("#depois3").html(resultado);
#antes, #depois1, #depois2, #depois3 {
  font-family: monospace;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table>
    <tr><td>Original ranômico:</td><td id='antes'></td></tr>
    <tr><td>Agrupando 1:</td><td id='depois1'></td></tr>
    <tr><td>Agrupando 2:</td><td id='depois2'></td></tr>
    <tr><td>Agrupando 3:</td><td id='depois3'></td></tr>
</table>

  • Hmm, I just saw that the question was edited by specifying C++ as a programming language. I will leave my answer here if the algorithm interest to OP or third party.

  • Yes, it is something like this and it will help a lot, because I believe that the most important thing in this case is logic rather than syntax. In my case I have another problem, as it is a map/ image and I am treating as unidimencional vector, I need to find a way to balance also the adjacencies in the y axis, because this algorithm solves in x.

  • @DJM_JM If it is one-dimensional, then there would simply be no y-axis, only x-axis. So I don’t understand what you meant. Could you elaborate on what you’re trying to do? What do you mean by treating the map/image (which I believe is two-dimensional) as a one-dimensional vector? You mean you have a large vector with w*h elements where each block of w elements is a line? Or you mean something else? Edit the question to make it clearer.

  • It’s a 2500 unidimencional vector that represents a 50 x 50 map. That’s exactly what you said, I simulate the x and y axes from this "big vector".

  • @DJM_JM In this case, in function agrupar, I should take the element and its 8 nearby neighbors instead of the element and its 2 neighbors. And instead of m >= 2, you would use m >= 4 or m >= 5. The formula to access the element x,y in the vector is vetor[y * largura + x]. Other possible variants would be to give different weights to the neighbors (for example, those that are diagonally adjacent have less weight than the laterally adjacent ones) or to take neighbors that are not immediately adjacent, but also 2 or 3 pixels away.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.