#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Does mosiacs. """ import os import time import colorsys import threading import numpy as np from PIL import Image class Mosiac(): def __init__(self): self.numClusters = 8 self.tolerance = 0.1 self.numThreads = 4 self.imageBank = {} self.clusters = [] self.clusterMeans = [] self.bigImageSize = None self.bigImage = None self.smallImage = None self.imageMatrix = [] self.tileSize = None self.matrixSize = None def openBigImage(self, imagePath, mat=(20,40)): """ Opens and initializes the big image. """ self.matrixSize = mat self.bigImage = Image.open(imagePath).convert("RGB") self.tileSize = (self.bigImage.size[0] // mat[0], self.bigImage.size[1] // mat[1]) self.smallImage = self.bigImage.resize(mat) self.bigImageSize = (self.tileSize[0]*mat[0], self.tileSize[1]*mat[1]) def initImageBank(self, root): """ Calculates the average pixel value of all the images in the directory. This is the thread controller. """ then = time.time() files = os.listdir(os.path.join(root, "images")) thread_files = [] num = len(files) // self.numThreads for n in range(self.numThreads): t_files = [file for file in files[n*num:n*num+num]] thread_files.append(t_files) thread_files[-1] += [file for file in files[-(len(files) % num):]] threads = [] for t_files in thread_files: t = threading.Thread(target=self.initImageBankWorker, args=((root, t_files))) threads.append(t) t.start() for t in threads: t.join() print(f"initImageBank took: {time.time()-then} seconds.") def initImageBankWorker(self, root, files): """ Thread worker. """ for file in files: image = Image.open(os.path.join(root, "images", file)) if image.mode == "P": image = image.convert(image.palette.mode) if image.mode == "RGBA": image = alpha_composite_with_color(image).convert("RGB") if image.mode == "L": image = image.convert("RGB") # image = image.convert("RGBA") image = image.resize(self.tileSize, Image.ANTIALIAS) mean = tuple(np.mean(image, axis=(0,1))) # img = Image.new("RGBA", image.size) # img.putdata(list(map(lambda pixel: (255,255,255,0) if pixel == \ # (255,255,255,255) else pixel, image.getdata()))) self.imageBank[mean] = image def debugImageBank(self): """ Create a bank of test images. """ then = time.time() for i in range(0, 257, 16): for j in range(0, 257, 16): for k in range(0, 257, 16): image = Image.new("RGB", self.tileSize, (i,j,k, 255)) mean = tuple(np.mean(image, axis=(0,1))) self.imageBank[mean] = image print(f"debugImageBank took: {time.time()-then} seconds.") def initClusters(self): """ Divides the images into clusters. """ then = time.time() sort = sorted(self.imageBank.keys(), key=lambda pixel: step(*pixel,8)) num = len(sort) // self.numClusters for n in range(self.numClusters): cluster = {pixel: self.imageBank[pixel] for pixel in \ sort[n*num:n*num+num]} self.clusters.append(cluster) self.clusters[-1].update({pixel: self.imageBank[pixel] for pixel in \ sort[-(len(sort) % num):]}) for cluster in self.clusters: mean = tuple(np.mean(list(cluster.keys()), axis=(0))) self.clusterMeans.append(mean) print(f"initClusters took: {time.time()-then} seconds.") def nearestImage(self, pixel): """ Finds the nearest image within the mosiac image bank to the provided pixel. """ dists = [] for mean in self.clusterMeans: dist = np.linalg.norm(np.array(pixel)-np.array(mean)) dists.append(dist) clusterMean = self.clusterMeans[dists.index(min(dists))] cluster = self.clusters[self.clusterMeans.index(clusterMean)] nodes = np.array(list(cluster.keys())) dist = np.sum((nodes - np.array(pixel))**2, axis=1) tol = int(len(dist) * self.tolerance) tol += int(tol == 0) indexs = dist.argsort()[:tol] # choice = dist.argmin() # closet value choice = np.random.choice(indexs) return cluster[tuple(nodes[choice])] def buildMatrix(self): """ Build the image matrix. """ then = time.time() pixels = list(self.smallImage.getdata()) for pixel in pixels: image = self.nearestImage(pixel) self.imageMatrix.append(image) print(f"buildMatrix took: {time.time()-then} seconds.") def buildMosiac(self, root): """ Builds the final mosiac image. """ then = time.time() image = Image.new("RGB", self.bigImageSize, (255,255,255)) # self.bigImage = self.bigImage.crop((0,0,self.bigImageSize[0],self.bigImageSize[1])) n = 0 for y in range(0, self.bigImageSize[1], self.tileSize[1]): for x in range(0, self.bigImageSize[0], self.tileSize[0]): image.paste(self.imageMatrix[n], box=(x,y)) n += 1 # self.bigImage.save(os.path.join(root, "mosiac.png"), "PNG") image.save(os.path.join(root, "mosiac.jpg"), "JPEG", optimize=True, quality=90) print(f"buildMosiac took: {time.time()-then} seconds.") def step (r,g,b, repetitions=1): lum = ( .241 * r + .691 * g + .068 * b )**0.5 h, s, v = colorsys.rgb_to_hsv(r,g,b) h2 = int(h * repetitions) lum2 = int(lum * repetitions) v2 = int(v * repetitions) return (h2, lum, v2) def alpha_composite(front, back): """Alpha composite two RGBA images. Source: http://stackoverflow.com/a/9166671/284318 Keyword Arguments: front -- PIL RGBA Image object back -- PIL RGBA Image object """ front = np.asarray(front) back = np.asarray(back) result = np.empty(front.shape, dtype='float') alpha = np.index_exp[:, :, 3:] rgb = np.index_exp[:, :, :3] falpha = front[alpha] / 255.0 balpha = back[alpha] / 255.0 result[alpha] = falpha + balpha * (1 - falpha) old_setting = np.seterr(invalid='ignore') result[rgb] = (front[rgb] * falpha + back[rgb] * balpha * (1 - falpha)) / result[alpha] np.seterr(**old_setting) result[alpha] *= 255 np.clip(result, 0, 255) # astype('uint8') maps np.nan and np.inf to 0 result = result.astype('uint8') result = Image.fromarray(result, 'RGBA') return result def alpha_composite_with_color(image, color=(255, 255, 255)): """Alpha composite an RGBA image with a single color image of the specified color and the same size as the original image. Keyword Arguments: image -- PIL RGBA Image object color -- Tuple r, g, b (default 255, 255, 255) """ back = Image.new('RGBA', size=image.size, color=color + (255,)) return alpha_composite(image, back) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( description="Generates a mosiac of images.") parser.add_argument( "root", help="Root directory to work in.") # parser.add_argument( # "-s", # "--size", # default=(200,200), # help="Size each image should be.") args = parser.parse_args() mosiac = Mosiac() mosiac.openBigImage(os.path.join(args.root, "big.jpg")) mosiac.initImageBank(args.root) # mosiac.debugImageBank() mosiac.initClusters() mosiac.buildMatrix() mosiac.buildMosiac(args.root)