Skip to content

Instantly share code, notes, and snippets.

@nrrb
Last active November 30, 2019 06:47
Show Gist options
  • Save nrrb/997547 to your computer and use it in GitHub Desktop.
Save nrrb/997547 to your computer and use it in GitHub Desktop.
My first Python script. Replicating and enhancing functionality of a Processing (Java) applet.

This makes a poster-size display of an image split up into 4"x6" photo prints, to make it cheaper to get a poster-sized print.

$0.09/print is common for buying 50+ 4"x6" photo prints in the US, so you can print a square foot for $0.54, and a 36"x24" poster can be made for $3.24. The effiency shows at larger scales; a floor to ceiling print, let's say 10 feet high, and 10 feet wide for good measure, would be $54. Granted, this does involve arranging 600 photo prints, but it could be like a devilishly challenging jigsaw puzzle.

Here's where you can get some really high resolution images, suitable for printing at floor to ceiling sizes:

  1. HubbleSOURCE - Images from the NASA Hubble Space Telescope.
  2. Project Apollo Archive - Photos from the Apollo moon mission.
  3. The Cell Image Library - Looking at the very small instead, high resolution pictures of microbiology.

This is the opposite of what the very cool VAST group does.

from TileImageClass import TileableImage
from TiledImageClass import TiledImage
import random
import Image
#import ImageDraw
import time
target_size = 256
tile_size = 64
image_source_folder = "./images/"
target_image_path = "./turntable_small.jpg"
out_filename = "./test.jpg"
def GenerateSourceImageDB(source_folder):
def GetImageFilePaths(imageListDir):
import re
import os
imageList = os.listdir(imageListDir)
imageList = imageList[:]
imagefilter = re.compile("\.(jpg|png)", re.IGNORECASE)
imageList = filter(imagefilter.search, imageList)
for index in range(len(imageList)):
imageList[index] = imageListDir + imageList[index]
return imageList
imageDB = []
image_path_list = GetImageFilePaths(image_source_folder)
for image_path in image_path_list:
a = TileableImage(image_path)
imageDB.append(a)
# print "Processed", len(imageDB), "images."
return imageDB
def ColorTupleToDict(colortuple):
return {'red': colortuple[0], 'green': colortuple[1], 'blue': colortuple[2]}
def FindImageMatch(tileimage_list, target_color):
def ColorDistance(colordict1, colordict2):
deltaRed = colordict1['red'] - colordict2['red']
deltaGreen = colordict1['green'] - colordict2['green']
deltaBlue = colordict1['blue'] - colordict2['blue']
return 3*(deltaRed**2) + 6*(deltaGreen**2) + (deltaBlue**2)
min_color_dist = 10*(255**2)
index_closest = 0
for index in range(len(tileimage_list)):
source_color = tileimage_list[index].average_color
distance = ColorDistance(source_color, target_color)
if(distance < min_color_dist):
min_color_dist = distance
index_closest = index
return tileimage_list[index_closest]
# Generate the database of source images
print "Generating database of source images..."
start_time = time.clock()
source_images = GenerateSourceImageDB(image_source_folder)
total_time = time.clock() - start_time
time_per_image = total_time / len(source_images)
print "Operation time:", total_time, "seconds"
print "Total images gathered:", len(source_images)
print "Took an average of", time_per_image, "seconds per image."
# Pick a target image
target_image = TileableImage(target_image_path)
print 'Using image:', target_image.image_filepath
# For each pixel in the target image, find the source image that is closest to it
target_image_file = Image.open(target_image.image_filepath)
print 'Original dimensions of target image:', target_image_file.size[0], 'x', target_image_file.size[1]
target_image_file.thumbnail((target_size, target_size))
print 'New dimensions of target image:', target_image_file.size[0], 'x', target_image_file.size[1]
target_width = target_image_file.size[0]
target_height = target_image_file.size[1]
tile_table = []
for y in range(target_height):
print "Processing pixel row", y
for x in range(target_width):
target_color = target_image_file.getpixel((x, y))
target_color_dict = ColorTupleToDict(target_color)
closest_image_obj = FindImageMatch(source_images, target_color_dict)
# Make a tiled object from the source image object
tiled_source = TiledImage(closest_image_obj, tile_size, tile_size)
# Save the tiled object along with the pixel coordinates
tile_table.append([tiled_source, x, y])
print "Rendering output image..."
out_image = Image.new("RGB", (tile_size * target_width, tile_size * target_height))
#out_draw = ImageDraw.Draw(out_image, "RGB")
for tile in tile_table:
im = tile[0].RenderedImage()
offset_x = tile[1] * tile_size
offset_y = tile[2] * tile_size
out_image.paste(im, (int(offset_x), int(offset_y)))
print "Saving to", out_filename, "..."
out_image.save(out_filename, "JPEG")
from TileImageClass import TileableImage
class TiledImage:
# Class attributes:
# source_image - TileableImage class object
# target_width - width in pixels of target area
# target_height - height in pixels of target area
# tile_corner_x - list of x coordinates for tiles
# tile_corner_y - list of y coordinates for tiles
# tile_width - list of tile widths
# tile_height - list of tile heights
# num_tiles - number of tiles
def __init__(self, source_image, target_width, target_height):
self.source_image = source_image
self.target_width = target_width
self.target_height = target_height
self._GenerateTileCoordinates()
def _GenerateTileCoordinates(self):
rect_corner_x = 0.0
rect_corner_y = 0.0
rect_width = float(self.target_width)
rect_height = float(self.target_height)
rect_aspect = rect_width / rect_height
source_width = float(self.source_image.dimensions['width'])
source_height = float(self.source_image.dimensions['height'])
tile_aspect = source_width / source_height
width_constrained = False
height_constrained = False
rect_min_size = 5
t_width = 0
t_height = 0
num_tiles = 0
self.tile_corner_x = []
self.tile_corner_y = []
self.tile_width = []
self.tile_height= []
self.num_tiles = 0
while((rect_width >= rect_min_size) and (rect_height >= rect_min_size)):
if(tile_aspect == rect_aspect):
t_width = rect_width
t_height = rect_height
self.tile_corner_x.append(rect_corner_x)
self.tile_corner_y.append(rect_corner_y)
self.tile_width.append(t_width)
self.tile_height.append(t_height)
self.num_tiles += 1
rect_width -= t_width
rect_height -= t_height
if(rect_height != 0):
rect_aspect = rect_width / rect_height
square_aspect = True
break
elif(tile_aspect > rect_aspect):
width_constrained = True
height_constrained = False
square_aspect = False
elif(tile_aspect < rect_aspect):
width_constrained = False
height_constrained = True
square_aspect = False
if(width_constrained and not square_aspect):
t_width = rect_width
t_height = t_width / tile_aspect
self.tile_corner_x.append(rect_corner_x)
self.tile_corner_y.append(rect_corner_y)
self.tile_width.append(t_width)
self.tile_height.append(t_height)
self.num_tiles += 1
rect_corner_y += t_height
rect_height -= t_height
if(rect_height != 0):
rect_aspect = rect_width / rect_height
if(height_constrained and not square_aspect):
t_height = rect_height
t_width = t_height * tile_aspect
self.tile_corner_x.append(rect_corner_x)
self.tile_corner_y.append(rect_corner_y)
self.tile_width.append(t_width)
self.tile_height.append(t_height)
self.num_tiles += 1
rect_corner_x += t_width
rect_width -= t_width
if(rect_height != 0):
rect_aspect = rect_width / rect_height
# sourceImage should be an Image object already
def RenderedImage(self):
import Image
avg_source_color = self.source_image.average_color
avg_source_color = (avg_source_color['red'], avg_source_color['green'], avg_source_color['blue'])
target_image = Image.new("RGB", (self.target_width, self.target_height), avg_source_color)
source_image_data = Image.open(self.source_image.image_filepath)
for index in range(len(self.tile_corner_x)):
x = self.tile_corner_x[index]
y = self.tile_corner_y[index]
w = self.tile_width[index]
h = self.tile_height[index]
im = source_image_data.resize((int(w), int(h)))
target_image.paste(im, (int(x), int(y)))
return target_image
class TileableImage:
# internal attributes:
#
# image_filepath
# file_checksum
# dimensions
# average_color
# date_entry_created
def __init__(self, filepath):
self.LoadFile(filepath)
def LoadFile(self, filepath):
self.image_filepath = filepath
self._LoadFileChecksum()
self._LoadDimensions()
self._LoadAvgColor()
self._SetDateCreated()
def _LoadFileChecksum(self):
if(self.image_filepath == ''):
return
import hashlib
try:
imageFile = open(self.image_filepath, 'rb')
except:
return -1
hash = hashlib.md5()
hash.update(imageFile.read())
self.file_checksum = hash.hexdigest()
return self.file_checksum
def _LoadDimensions(self):
if(self.image_filepath == ''):
return
import Image
opened = False
try:
imageObject = Image.open(self.image_filepath)
opened = True
except IOError:
return -1
if(opened):
imageSize = imageObject.size
self.dimensions = {'width': imageSize[0], 'height': imageSize[1]}
return self.dimensions
def _LoadAvgColor(self):
if(self.image_filepath == ''):
return
import Image
import ImageStat
colorAvg = []
opened = False
try:
imageObject = Image.open(self.image_filepath)
opened = True
except:
return -1
if(opened):
imstats = ImageStat.Stat(imageObject)
if(imageObject.mode == 'RGB' or imageObject.mode == 'RGBA'):
for index in range(3):
bandAvg = imstats.sum[index] / imstats.count[index]
colorAvg.append(bandAvg)
else:
greyAvg = imstats.sum[0] / imstats.count[0]
colorAvg = [greyAvg, greyAvg, greyAvg]
# return colorAvg
self.average_color = {'red': colorAvg[0], 'green': colorAvg[1], 'blue': colorAvg[2]}
return self.average_color
def _SetDateCreated(self):
from time import gmtime, strftime
self.date_entry_created = strftime("%a, %b %d %Y %H:%M:%S %Z", gmtime())
return self.date_entry_created
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment