From 654aa1ef3240e04562f3f8b18b8a1810799d63dd Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Mon, 5 Apr 2021 07:36:26 +0200 Subject: [PATCH] Release 1.0.0 Initial release --- .gitignore | 3 +- classes/Collage.py | 74 ++++++++++++++++++++++++++++++++ classes/Color.py | 31 ++++++++++++++ classes/Eyedropper.py | 24 ----------- classes/Samples.py | 81 ++++++++++++++++++++++++++++-------- create_collage.sh | 4 -- main.py | 23 ++++++++-- {mem => memory}/.placeholder | 0 8 files changed, 190 insertions(+), 50 deletions(-) create mode 100644 classes/Collage.py create mode 100644 classes/Color.py delete mode 100644 classes/Eyedropper.py delete mode 100644 create_collage.sh rename {mem => memory}/.placeholder (100%) diff --git a/.gitignore b/.gitignore index ce6e703..0abe166 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ __pycache__ +__sample__.json input/* samples/* -mem/* +memory/* !*/.placeholder \ No newline at end of file diff --git a/classes/Collage.py b/classes/Collage.py new file mode 100644 index 0000000..beceb9e --- /dev/null +++ b/classes/Collage.py @@ -0,0 +1,74 @@ +from PIL import Image +from bisect import bisect_left + +class Schematic(): + def __init__(self,template,samples): + self.template = template + self.samples = samples + + self.schematic = {} + self.create_schematic() + + def query_sample(self,value): + samples = [*self.samples] + pos = bisect_left(samples,value) + + if(pos < 1 or pos > len(samples) - 1): + pos = 0 + + return samples[pos] + + def create_schematic(self): + for x in range(1,self.template.size[0]): + self.schematic[x] = {} + for y in range(1,self.template.size[1]): + r,g,b = self.template.getpixel((x,y)) + eyedropper = "%02x%02x%02x" % (r,g,b) + + self.schematic[x][y] = self.query_sample(eyedropper) + print(f"Generated schematic for pixel at index [{x},{y}] ",end="\r",flush="True") + print("") + +class Collage(): + def __init__(self,input_file,samples): + self.template = Image.open(input_file) + self.samples = samples.samples + self.samples_posix = samples.samples_posix + + self.size = (20,20) + + self.collage = self.create_canvas() + self.create_collage() + + def create_canvas(self): + canvas_width = self.size[0] * self.template.size[0] + canvas_height = self.size[1] * self.template.size[1] + + return Image.new("RGB",(canvas_width,canvas_height)) + + def create_collage(self): + schematic = Schematic(self.template,self.samples).schematic + + offset_x = 0 + offset_y = 0 + + for x in range(1,self.template.size[0]): + offset_x = 0 + for y in range(1,self.template.size[1]): + key = schematic[x][y] + resolve_posix = self.samples[key] + + sample = Image.open(self.samples_posix[resolve_posix]) + sample = sample.resize(self.size) + + self.collage = self.collage.copy() + self.collage.paste(sample,(offset_x,offset_y)) + + offset_x += self.size[0] + + print(f"Pasted best matched sample for index [{x},{y}] ",end="\r",flush="True") + offset_y += self.size[1] + print("") + + def put(self,dest): + self.collage.save(dest,"JPEG") diff --git a/classes/Color.py b/classes/Color.py new file mode 100644 index 0000000..8c6736f --- /dev/null +++ b/classes/Color.py @@ -0,0 +1,31 @@ +from PIL import Image + +# Calculate the average color of a sample +class AverageColor(): + def __init__(self,image): + self.image = Image.open(image) + self.image = self.image.resize((50,50)) # Downscale image to improve performance + + def rgb(self): + width,height = self.image.size + rgb = [0,0,0] + + def average(value): + return round(value / lines) + + # Get RGB of each pixel with by raster scanning + lines = 0 + for x in range(0,width): + for y in range(0,height): + r,g,b = self.image.getpixel((x,y)) + + rgb[0] += r + rgb[1] += g + rgb[2] += b + lines += 1 + + return tuple(map(average,rgb)) + + # Format RGB output as HEX (without #) + def hex(self): + return "%02x%02x%02x" % self.rgb() \ No newline at end of file diff --git a/classes/Eyedropper.py b/classes/Eyedropper.py deleted file mode 100644 index ae45157..0000000 --- a/classes/Eyedropper.py +++ /dev/null @@ -1,24 +0,0 @@ -from PIL import Image - -def compute_average_image_color(img): - width, height = img.size - - r_total = 0 - g_total = 0 - b_total = 0 - - count = 0 - for x in range(0, width): - for y in range(0, height): - r, g, b = img.getpixel((x,y)) - r_total += r - g_total += g - b_total += b - count += 1 - - return (r_total/count, g_total/count, b_total/count) - -img = Image.open('image.png') -#img = img.resize((50,50)) # Small optimization -average_color = compute_average_image_color(img) -print(average_color) \ No newline at end of file diff --git a/classes/Samples.py b/classes/Samples.py index 81c6bed..0f3715a 100644 --- a/classes/Samples.py +++ b/classes/Samples.py @@ -1,26 +1,73 @@ +import zlib +import json +from collections import OrderedDict +from .Color import AverageColor from pathlib import Path -class Samples: - def __init__(self,samples_path): - samples_files = Path(samples_path).glob("*.jpg") - self.samples = [x for x in samples_files if x.is_file()] - +# Generate a unique identifier for the current sample set +class SamplesFingerprint(): + def __init__(self): self.hash = self.create_hash() - self.memory = f"mem/{self.hash}" - - # Create hash from sample names and path + self.memory = f"memory/{self.hash}.json" + self.created = False + + if(not self.hash_exists()): + self.save_hash() + + # Checksum by hashing next sample with sum of previous samples def create_hash(self): - samples_hash = "" - for i in self.samples: - samples_hash = hash(i) + samples_hash = "I like coffee" # Initialize with padding + for i in self.samples_posix: + seed = str(samples_hash) + str(i) + samples_hash = zlib.crc32(bytes(seed,encoding="utf8")) return samples_hash - # Test if the current sample set has been saved + # Test if sample set has been saved def hash_exists(self): - if(Path(self.memory).is_file()): - return True - return False + if(self.created or not Path(self.memory).is_file()): + return False + + self.created = True + return self.created - # Save hash to memory location on disk + # Save identifier of sample set to disk def save_hash(self): - Path(self.memory,"w").touch() \ No newline at end of file + Path(self.memory).touch() + +# Load samples from sample set memory file or from samples/ +class Samples(SamplesFingerprint): + def __init__(self,samples_path,force = False): + samples_files = Path(samples_path).glob("*.jpg") + self.samples_posix = [x for x in samples_files if x.is_file()] # List of PosixPaths + self.samples = {} # HEX from color calc algorithm + + super(Samples,self).__init__() + + try: + self.load_sample_set() + except: + self.map_color() + self.save_sample_set() + + # Get the pixel value for each sample using a desired algorithm + def map_color(self): + for i,sample in enumerate(self.samples_posix): + color = AverageColor(sample).hex() # Get the average color of a sample as HEX + + self.samples[color] = i + print(f"Loaded {i + 1}/{len(self.samples_posix)} samples",end="\r",flush="True") + print("") + + # Save calculated sample set data + def save_sample_set(self): + self.samples = dict(sorted(self.samples.items())) # Sort the dict by chrominance + + with open(self.memory,"w") as f: + json.dump(self.samples,f) + print(f"Saved sample set with fingerprint: {self.hash}") + + # Load pre-calulated sample set data + def load_sample_set(self): + with open(self.memory) as f: + self.samples = json.load(f) + print(f"Loaded {len(self.samples)} samples from set {self.hash}") \ No newline at end of file diff --git a/create_collage.sh b/create_collage.sh deleted file mode 100644 index 134e981..0000000 --- a/create_collage.sh +++ /dev/null @@ -1,4 +0,0 @@ -#! /bin/bash -export PYTHONHASHSEED=0 - -python3 main.py \ No newline at end of file diff --git a/main.py b/main.py index 64595d2..8307af5 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,22 @@ -#from classes import Eyedropper from classes.Samples import Samples +from classes.Collage import Collage +from pathlib import Path -# Load samples from folder -samples = Samples("samples") +force = False +input_path = "input/" -print(samples.hash) \ No newline at end of file +# Load all JPGs from the "samples/" folder +samples = Samples("samples",force) + +input_file = str("input/coffee-resized.jpg") + +collage = Collage(input_file,samples) +collage.put(input_file + "_collage.jpg") + +# input_files = Path(input_path).glob("*.jpg") +# input_files_posix = [x for x in input_files if x.is_file()] # List of PosixPaths + +# for input_file in input_files_posix: +# input_file = str(input_file) +# collage = Collage(input_file,samples) +# collage.put(input_file + "_collage.jpg") \ No newline at end of file diff --git a/mem/.placeholder b/memory/.placeholder similarity index 100% rename from mem/.placeholder rename to memory/.placeholder