let showOriginal = false;
let filename = 'target.jpg';
let filename_prefix = 'Photo - ';
let filename_ext = 'jpeg';
let matchingMode = 'HSB';
let num_loaded_images = 0;
let click_highlight = [];
createCanvas(windowWidth, windowHeight);
text('Loading source images...', width / 2, height / 2);
function mouseClicked() {
text('Loading source images...', width / 2, height / 2);
console.log("waitToStart: " + waitToStart);
console.log("Loading "+num_images+" source images...");
console.log("waitToStart: " + waitToStart);
text('Finding best matching images for mosaic...', width / 2, height / 2);
if (click_highlight.length != 0) {
highlightTile(mouseX, mouseY);
function highlightTile(click_x, click_y) {
n = floor(click_x/tileWidth);
m = floor(click_y/tileHeight);
console.log("clicked tile: "+m+", "+n);
console.log("click_highlight: "+click_highlight[m][n]);
switch (click_highlight[m][n]) {
click_highlight[m][n] = 2;
this_tile = targetImage.get(n*tileWidth,m*tileHeight,tileWidth,tileHeight);
image(this_tile, n*tileWidth,m*tileHeight,tileWidth,tileHeight);
click_highlight[m][n] = 3;
source_tile = targetImage.get(n*tileWidth,m*tileHeight,tileWidth,tileHeight);
subTileColors = computeSubTileColors(source_tile);
for (let i=0;i<subTileColors.length;i++) {
for (let j=0; j<subTileColors[i].length; j++) {
this_color = subTileColors[i][j];
sub_x = round(tileWidth/num_subTiles);
sub_y = round(tileHeight/num_subTiles);
rect(n*tileWidth + j*sub_x,m*tileHeight + i*sub_y,sub_x,sub_y);
click_highlight[m][n] = 1;
this_tile = sourceImages[image_map[m][n]];
image(this_tile, n*tileWidth,m*tileHeight,tileWidth,tileHeight);
function loadSourceImages() {
for (count = 0; count < num_images; count++) {
this_img = loadImage(filename_prefix +(count+1) + '.' + filename_ext,
sourceImages.push(loaded_img);
if (debug) { console.log("Loaded image number: " + num_loaded_images); }
if (num_loaded_images == num_images) {
text('Creating mosaic...', width / 2, height / 2);
function colorDistanceRGB(c1,c2) {
distance = sqrt( pow(r1-r2,2) + pow(g1-g2,2) + pow(b1-b2,2) );
function colorDistanceHSB(c1,c2) {
if (h_diff > 180) { h_diff = 360 - h_diff; }
distance = sqrt( pow(h_diff,2) + pow(s1-s2,2) + 3*pow(b1-b2,2) );
function findBestMatchForTile(subImage) {
c_target = computeTileColor(subImage);
if (matchingMode == 'RGB') { current_min_distance = colorDistanceRGB(color(0,0,0), color(255,255,255)); }
else if (matchingMode == 'HSB') { current_min_distance = colorDistanceHSB(color('hsb(1,0%,0%)'), color('hsb(360,100%,100%)')); }
for (let i = 0; i < sourceImages.length; i++) {
for (let m=0; m < num_subTiles; m++) {
for (let n=0; n < num_subTiles; n++) {
if (matchingMode == 'RGB') { color_distance += colorDistanceRGB(c_target, colorValues[i][m][n]); }
else if (matchingMode == 'HSB') { color_distance += colorDistanceHSB(c_target, colorValues[i][m][n]); }
color_distance /= num_subTiles*num_subTiles;
matches.push({index: i, distance: color_distance});
if (distance < current_min_distance) {
current_min_distance = distance;
matches.sort(compareByDistance);
return {index: idx, distance: current_min_distance, next_best: matches };
targetImage = loadImage(filename, loadAllImages);
function loadAllImages() {
let srcW = targetImage.width;
let srcH = targetImage.height;
createCanvas(srcW, srcH);
text('Creating mosaic...', width / 2, height / 2);
setTimeout(startCreating, 1000);
function startCreating() {
console.log("Creating mosaic from " + sourceImages.length + " source images..." );
mosaic = createImage(targetImage.width, targetImage.height);
mosaic.copy(targetImage, 0, 0, targetImage.width, targetImage.height, 0, 0, targetImage.width, targetImage.height);
if (colorValues.length == 0) {
numTiles = floor(mosaic.width/tileWidth);
console.log("... finding best source image matches for "+numTiles+"x"+numTiles+" mosaic tiles.");
tileHeight = floor(tileWidth*(targetImage.height/targetImage.width));
for (let m=0; m<numTiles; m++) {
for (let n=0; n<numTiles; n++) {
subImage = mosaic.get(x, y, tileWidth, tileHeight);
matchData = findBestMatchForTile(subImage);
full_matchData = {index: matchData.index, distance: matchData.distance, next_best: matchData.next_best, row: m, column: n};
tile_map.push(full_matchData);
imageIndex = matchData.index;
image_map[m] = image_map_row;
tile_map.sort(compareByDistance);
if (quality_setting == 1) {
removeDuplicateMatches();
for (let t=0; t<tile_map.length; t++) {
h += tile_map[t].distance;
trail.push(tile_map[t].index);
console.log('Greedy search distance: ' + h);
for (let t=0; t<tile_map.length; t++) { h += tile_map[t].distance; }
console.log('Greedy distance, includes duplicates: ' + h);
console.log('Trail: '+trail);
console.log('Entries: '+trail.length);
for (let t=0; t<tile_map.length; t++) {
let imageIndex = trail[t];
let n = tile_map[t].column;
if ( imageIndex < num_images ) {
image(sourceImages[imageIndex], x, y, tileWidth, tileHeight);
mosaic.copy(sourceImages[imageIndex],0,0,sourceImages[imageIndex].width,sourceImages[imageIndex].height,x,y,tileWidth,tileHeight);
image_map[m][n] = imageIndex;
click_highlight = new Array(numTiles);
for (let m=0; m<numTiles; m++) {
click_highlight[m] = new Array(numTiles).fill(1);
if (showOriginal) { image(targetImage,0,0,400,300); }
mosaic.save('Photomosaic', 'png');
function compareByDistance(a, b) {
if (a.distance > b.distance) {
} else if (a.distance < b.distance) {
function removeDuplicateMatches() {
used_images[0] = tile_map[0].index;
for (let t=1; t<tile_map.length; t++) {
if (debug) {console.log('Examining tile #' + t); }
for (let t=0; t<tile_map.length; t++) { h += tile_map[t].distance; }
console.log('Greedy min distance: ' + h);
for (let t=0; t<tile_map.length; t++) {
let tempMinDistance = 1024.0*1024.0*1024.0;
for (let n=0; n<prev_f.length; n++) {
let nextVal = tile_map[t].distance + prev_f[n];
console.log('tile_map[t].index: ' + tile_map[t].index);
console.log('tile_map[t].distance: ' + tile_map[t].distance);
console.log('prev_f[n]: ' + prev_f[n]);
console.log('prev_f_trail[n] includes: ' + prev_f_trail[n].includes(tile_map[t].index) );
if ( !prev_f_trail[n].includes(tile_map[t].index) ) {
if ( (nextVal + h) < tempMinDistance ) {
if ( (t>l_range) && (t<h_range) ) {console.log('t=' + t + ', Adding node 0, index: '+tile_map[t].index); }
tempMinDistance = nextVal + h;
f_trail[t][0] = prev_f_trail[n];
f_trail[t][0].push( tile_map[t].index );
for (let c=0; c<tile_map[t].next_best.length; c++) {
let nextVal = tile_map[t].next_best[c].distance + prev_f[n];
console.log('tile_map[t].next_best[c].index: ' + tile_map[t].next_best[c].index);
console.log('tile_map[t].next_best[c].distance: ' + tile_map[t].next_best[c].distance);
console.log('prev_f_trail[n] includes: ' + prev_f_trail[n].includes(tile_map[t].next_best[c].index) );
if ( !prev_f_trail[n].includes(tile_map[t].next_best[c].index) ) {
if ( (nextVal + h) < tempMinDistance ) {
if ( (t>l_range) && (t<h_range) ) {
console.log('t: '+t+', Adding node... next_best['+c+']');
console.log('tile_map[t].next_best[c].index: ' + tile_map[t].next_best[c].index);
console.log('tile_map[t].next_best[c].distance: ' + tile_map[t].next_best[c].distance);
console.log('prev_f_trail[n] includes: ' + prev_f_trail[n].includes(tile_map[t].next_best[c].index) );
tempMinDistance = nextVal + h;
new_trail = prev_f_trail[n];
new_trail.push(tile_map[t].next_best[c].index);
f_trail[t].push( new_trail );
h -= tile_map[t].distance;
prev_f_trail = f_trail[t];
if ( (t<h_range) && (t>l_range) ) {
console.log('f: ' + prev_f);
console.log('f_trail: ' + prev_f_trail);
console.log('Final f: ' + prev_f);
console.log('Final f_trail: ' + prev_f_trail);
function findNextBestMatch(tile_idx) {
image_idx = tile_map[tile_idx].index;
if (used_images.includes(image_idx)) {
next_choice = tile_map[tile_idx].next_best.shift();
tile_map[tile_idx].index = next_choice.index;
tile_map[tile_idx].distance = next_choice.distance;
tile_map.sort(compareByDistance);
findNextBestMatch(tile_idx);
console.log('... Tile #' + tile_idx + ' matched to image: ' + image_idx);
console.log('... ... choices remaining: ' + tile_map[tile_idx].next_best.length);
used_images.push(image_idx);
function computeImageColors() {
console.log("... analyzing colors for "+sourceImages.length +" source images, using "+num_subTiles+"x"+num_subTiles+" sub tiles.");
for (let i = 0; i < sourceImages.length; i++) {
sourceImages[i].loadPixels();
if (debug) { console.log("computeImageColors() for image "+(i+1)+"..."); }
let subTileWidth = floor(sourceImages[i].width / num_subTiles);
let subTileHeight = floor(sourceImages[i].height / num_subTiles);
for (let m=0; m < num_subTiles; m++) {
for (let n=0; n < num_subTiles; n++) {
subImage = sourceImages[i].get(x, y, subTileWidth, subTileHeight);
tileRowColors[n] = computeTileColor(subImage);
tileColors[m] = tileRowColors;
colorValues[i] = tileColors;
if (debug) { console.log("colorValues-" + i + ": " + colorValues[i]); }
function computeSubTileColors(img) {
let subTileWidth = floor(img.width / num_subTiles);
let subTileHeight = floor(img.height / num_subTiles);
for (let m=0; m < num_subTiles; m++) {
for (let n=0; n < num_subTiles; n++) {
subImage = img.get(x, y, subTileWidth, subTileHeight);
tileRowColors[n] = computeTileColor(subImage);
tileColors[m] = tileRowColors;
function computeTileColor(tileImage) {
if (debug) { console.log("computeTileColors()..." ); }
for (let j = 0; j < tileImage.pixels.length; j+=4) {
r_avg += tileImage.pixels[j];
g_avg += tileImage.pixels[j+1];
b_avg += tileImage.pixels[j+2];
r_avg /= (tileImage.pixels.length / 4);
g_avg /= (tileImage.pixels.length / 4);
b_avg /= (tileImage.pixels.length / 4);
tileColor = color(floor(r_avg), floor(g_avg), floor(b_avg));