Skip to content

Instantly share code, notes, and snippets.

@indivisible
Created April 10, 2017 16:33
Show Gist options
  • Save indivisible/32725d3b1f302905f109fa12739df7a4 to your computer and use it in GitHub Desktop.
Save indivisible/32725d3b1f302905f109fa12739df7a4 to your computer and use it in GitHub Desktop.
Creates dense montages.
#!/usr/bin/env python3
import os
import os.path
import brotli
from pathlib import Path
def decompress_file(ref_root, in_stream, out_stream):
data = in_stream.read()
if data.startswith(b'DDS'):
out_stream.write(data)
elif data.startswith(b'*'):
# is it really utf-8? Let's hope they don't use non-ascii chars
rel_path = data[1:].decode('utf-8')
in_path = ref_root / rel_path
with open(in_path, 'rb') as real_stream:
return decompress_file(ref_root, real_stream, out_stream)
else:
#FIXME: what the hell are the 1st 4 bytes?
out_stream.write( brotli.decompress(data[4:]) )
def decompress_tree(ref_root, rel_root, out_root):
ref_root = Path(ref_root).resolve()
in_root = ref_root / rel_root
out_root = Path(out_root)
for in_path in in_root.glob('**/*.dds'):
relative = in_path.relative_to(in_root)
out_path = out_root / relative
#print ('decompress %r -> %r' % (in_path, out_path))
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(in_path, 'rb') as in_stream, open(out_path, 'wb') as out_stream:
try:
decompress_file(ref_root, in_stream, out_stream)
except Exception:
print ('Error processing %s' % in_path)
raise
if __name__ == '__main__':
import sys
args = sys.argv[1:]
if len(args) != 3:
print ('Usage: %s <extracted_ggpk_dir> <Art/2Ditems> <out_dir>' % sys.argv[0])
decompress_tree(args[0], args[1], args[2])
#!/usr/bin/env python3
from functools import lru_cache
from pathlib import Path
import numpy as np
from scipy import misc
from scipy import ndimage
def get_images(*globs):
for glob in globs:
for p in Path('.').glob(glob):
yield p
def blit(dest, src, loc):
pos = [i if i >= 0 else None for i in loc]
neg = [-i if i < 0 else None for i in loc]
target = dest[[slice(i,None) for i in pos]]
src = src[[slice(i, j) for i,j in zip(neg, target.shape)]]
target[[slice(None, i) for i in src.shape]] = src
return dest
def blit_with_alpha(dest, src, loc):
idx = src[:,:,3] == 0
src[idx] = [0,0,0,0]
target_area = dest[loc[0]:loc[0]+src.shape[0], loc[1]:loc[1]+src.shape[1]]
target_area += src
return dest
class ImagePacker(object):
def __init__(self, bg, transpose=True):
assert len(bg.shape) == 3 and bg.shape[2] == 4, 'Background needs to be an RGBA image!'
self.transpose = transpose
if self.transpose:
bg = bg.transpose(1, 0, 2)
self.img = bg
self.img_mask = self.create_mask(self.img, 0)
self.line_cache = np.zeros(self.img_mask.shape[0], dtype=int)
self.__rebuild_line_space_cache(0, self.img_mask.shape[0])
print ('shape: %s' % str(self.img.shape))
@staticmethod
def longest_repeat(ia, value=True):
'''count the most <value> repeats in a row. Only works with bool arrays!'''
n = len(ia)
y = np.array(ia[1:] != ia[:-1])
i = np.append(np.where(y), n - 1)
z = np.diff(np.append(-1, i))
if value:
return np.max(z * ia[i])
else:
return np.max(z * ~ia[i])
def __rebuild_line_space_cache(self, from_, len_):
for i in range(from_, from_+len_):
self.line_cache[i] = self.longest_repeat(self.img_mask[i], False)
@staticmethod
@lru_cache()
def create_grow_kernel(growth):
return np.array([[abs(x)+abs(y) <= growth for y in range(-growth, growth+1)] for x in range(-growth, growth+1)])
#return np.array([[x**2+y**2 <= (growth+0.5)**2 for y in range(-growth, growth+1)] for x in range(-growth, growth+1)])
@classmethod
def grow(cls, img, growth):
assert len(img.shape) == 2, 'Only 1 channel is supported! (shape: %s)' % str(img.shape)
exp_img = np.zeros( (img.shape[0] + growth * 2, img.shape[1] + growth * 2), img.dtype )
#blit(exp_img, img, (growth, growth))
exp_img[growth:-growth, growth:-growth] = img
kernel = cls.create_grow_kernel(growth)
return ndimage.maximum_filter(exp_img, footprint=kernel, mode='constant')
@classmethod
def create_mask(cls, img, grow):
mask = img[:,:,3] != 0
if grow == 0:
return mask
return cls.grow(mask, grow)
def add_image(self, img, grow=15):
#TODO:
# + transpose images for y 1st mode
# + replace blit
# + add line space cache
# - check numpy operator lazyness
# + copy the bg alpha channel
if self.transpose:
img = img.transpose(1, 0, 2)
mask = self.create_mask(img, grow)
mask_h, mask_w = mask.shape
bg_mask = self.img_mask
check_collision = lambda x, y: np.any( bg_mask[y:y+mask_h, x:x+mask_w] * mask )
#check_collision = lambda o_x, o_y: any( any(bg_mask[o_y+y, o_x:o_x+mask_w] * mask[y]) for y in range(mask_h) )
#print ('add_img: shape=%s' % str(img.shape))
needed_space = np.array(list(map(self.longest_repeat, mask)))
check_linecache = lambda y: np.all(self.line_cache[y:y+mask_h] >= needed_space)
for y in range(self.img.shape[0] - mask_h):
if not check_linecache(y):
continue
for x in range(self.img.shape[1] - mask_w):
if not check_collision(x, y):
blit_with_alpha(self.img, img, (y + grow, x + grow))
self.img_mask = self.create_mask(self.img, 0)
self.__rebuild_line_space_cache(y, mask_h)
return True
return False
def save(self, name):
img = self.img
if self.transpose:
img = img.transpose(1, 0, 2)
misc.imsave(name, img)
def pack_them_up(background_path, out_path, images, transpose, distance, save_partial):
img = ImagePacker(misc.imread(background_path, mode='RGBA'))
images = list(images)
didnt_fit = 0
for i, image_path in enumerate(images):
print ('Adding %d / %d...' % (i+1, len(images)))
try:
image = misc.imread(image_path, mode='RGBA')
if not img.add_image(image, distance):
didnt_fit += 1
print ("Warning: %r didn't fit on image" % str(image_path))
except Exception:
print ('Error processing %s' % repr(image_path))
if save_partial:
img.save(out_path)
raise
except KeyboardInterrupt:
if save_partial:
img.save(out_path)
raise
print ("Number of images that didn't make it: %d" % didnt_fit)
img.save(out_path)
def main():
import time
start_t = time.time()
import argparse
help_str = '''
1) Make a base image to populate with stuff. A blank image works, but
you can put whatever you want on it. The important bit is that the
parts you want filled with junk must have an alpha value of 0.
Save as PNG.
2) Put the images you want to sprinkle on it in a directory. These
images should be PNGs too, with a proper alpha channel.
3) Run the script. For example:
python3 packthemup.py -r \\
blank.png \\
out.png \\
2DItems/Amulets/*.png \\
2DItems/Weapons/**/*.png'''
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, usage=help_str)
parser.add_argument('background', type=str)
parser.add_argument('output', type=str)
parser.add_argument('image_globs', type=str, nargs='+', help='Pattern for image paths to use. Supports ** for recursive globbing. For example 2DItems/**/*.png')
parser.add_argument('-T', '--transpose', action='store_false')
parser.add_argument('-r', '--randomize', action='store_true')
parser.add_argument('--no-save-partial', action='store_true', help="Don't save partial image on ctrl-c")
parser.add_argument('--distance', help='minimum number of pixels between images', type=int, default=15)
args = parser.parse_args()
print ('started with: %r > %r %r, transpose=%r, randomize=%r' % (args.background, args.output, args.image_globs, args.transpose, args.randomize))
images = list(get_images(*args.image_globs))
if args.randomize:
import random
random.shuffle(images)
pack_them_up(args.background, args.output, images, args.transpose, args.distance, not args.no_save_partial)
end_t = time.time()
print ('Created montage in %.2fs' % (end_t-start_t))
if __name__ == '__main__':
main()
#import argparse
#args = sys.argv[1:]
#print ('started with args %s' % repr(args))
#pack_them_up(args[0], args[1], get_images(*args[2:]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment