I continued to experiment with ChatGPT and my mosaic generator project (a.k.a. Pixelator).

I ended up fine tuning the algorithm (with ChatGPT’s help) and incorporated the ability to create mosaics using various palettes.

Searching around, I found an RGB table for all colors in Lego pieces. It occurred to me that I could generalize this, and create tables for different materials.

Searching more, I found the RGB codes for Crayola and for Prisma pencils. Both from Jenny’s awesome website.

These became 2 data structures like this:

{
    "color_list": [
		{ "color": "Red", "hex":"#ED0A3F"},
		{ "color": "Maroon", "hex":"#C32148"},
		{ "color": "Scarlet", "hex":"#FD0E35"},
		{ "color": "Brick Red", "hex":"#C62D42"},
		{ "color": "English Vermilion", "hex":"#CC474B"}
	]
}
{
    "color_list": [
		{ "color": "10 % Cool Grey", "hex": "E6E8E8" },
		{ "color": "10 % French Grey", "hex": "E9E7DD" },
		{ "color": "10 % Warm Grey", "hex": "EAE8EA" },
		{ "color": "20 % Cool Grey", "hex": "D9DDE2" },
		{ "color": "20 % French Grey", "hex": "D4D3C9" }
	]
}

These files I can directly require into my project. I also asked ChatGPT to create a 16 gray scale, using the same data structure. This, we can compute. ChatGPT wrote it promptly:

grayscale_colors.color_list = Array.from({ length: 16 }, (_, i) => {
    const value = Math.floor((i / 15) * 255);
    const hex = value.toString(16).padStart(2, '0');
    return {
        color: `gray_${hex}`,
        hex: `#${hex}${hex}${hex}`
    };
});

I am quite happy with the results, and I will call this MVP “done” for the time being. Below is (from left to right):

  • The original
  • 48x48 Crayola tiles
  • 48x48 Gray scale tiles

Using the module

The main function is:

exports.processImageToGrid = (image, type, palette, gridWidth, gridHeight) => {
	...
}

image is the image, type is the content-type (e.g. image/jpg, etc).

palette can be: lego, crayola, grayscale or prisma. If left undefined, the default is lego.

gridWidth and gridHeight specify the shape of the mosaic.

processImageToGrid returns a Promise. If it resolves successfully, the result is:

{
    inventory: [],      // Distribution of colors
    image: base64buffer,// Processed image data
    instructions: [],   // Instructions for building
    imageRaw: [],       // Raw image data (array of numbers representing RGB)
}

inventory is an array of objects that summarizes all pieces of each color needed.

[
{
    color: 'Dark Purple'
    count: 345
},
{
    color: 'Sky Blue Light'
    count: 12
}
...
]

image is the resulting image, base64 encoded and ready to insert into an <img> tag as an embedded src.

imageRaw is the resulting mosaic as a RGB array.

instructions is an array of summarized sequence to draw/build each row.

It will be something like:

[
  [
    { color: 'Two-tone Silver', hex: '737271', count: 90 },
    { color: 'Flat Silver', hex: '898788', count: 35 },
    { color: 'Glow In Dark Trans', hex: 'BDC6AD', count: 44 },
    { color: 'Pearl Very Light Gray', hex: 'ABADAC', count: 30 },
    { color: 'Modulex Terracotta', hex: '5C5030', count: 63 }
  		...
  ],
  [
    { color: 'Dark Gray', hex: '6D6E5C', count: 1 },
    { color: 'Dark Tan', hex: '958A73', count: 1 },
    { color: 'Glow In Dark Trans', hex: 'BDC6AD', count: 3 },
    { color: 'Dark Tan', hex: '958A73', count: 1 },
    { color: 'Pearl Titanium', hex: '3E3C39', count: 1 },
    { color: 'Dark Tan', hex: '958A73', count: 1 },
    { color: 'Pearl Very Light Gray', hex: 'ABADAC', count: 1 }
	  	...
  ]
]

instructions.length will equal the gridHeight parameter. The sum of all counts on all the objects of a row will equal the gridWidth.

With this function, it is easy to capture a file, pass parameters and then render the result. In my case, I am using a very simple HTML form (omitting details like security, etc):

<form action="/pixelate/upload" method="post" enctype="multipart/form-data">
  <label for="image">Upload an image:</label>
  <input type="file" name="image" accept="image/*" required>
  <br/>
  <label for="cells">Cells</label>
  <input type="text" name="cells" required value="48">
  <br/>  
  <label for="model">Choose a target palette:</label>
  <select name="model" id="palette">
    <option value="grayscale">Grayscale</option>
    <option value="lego">Lego Colors</option>
    <option value="crayola">Crayola Colors</option>
    <option value="prisma">Prisma 150 Colors</option>
  </select>
  <br/>
  <button type="submit">Submit</button>
</form>

The POST route in Express to handle the form submission:

server.post('/upload', (req, res) => {
  const form = new multiparty.Form();
  let options = {};

  form.on('field', (name, value) => {
    options[name] = value;
  });

  let chunks = [];
  form.on('part', (part) => {
        if(!part.filename) {
            return;
        }

        part.on('data', (chunk) => {
            chunks.push(chunk);
        });

        part.on('end', () => {
          options.contentType = part.headers['content-type'];
        });
  });

  form.on('close', () => {
    const cells = Number(options.cells) || 48;
    const buffer = Buffer.concat(chunks);
    pixelate.processImageToGrid(buffer, options.contentType, options.model, cells, cells)
      .then((result) => {
        result.cells = cells;
        res.render('output', result);
      })
      .catch((err) => {
          res.status(500).send("Error processing image.");
          return;
      });
  });

  form.parse(req);
});

I should perhaps remind you, dear reader, that 90% of this code was generated by ChatGPT after precise instructions from me!

My next post will cover the algorithm itself.