Created
April 10, 2017 16:33
-
-
Save indivisible/32725d3b1f302905f109fa12739df7a4 to your computer and use it in GitHub Desktop.
Creates dense montages.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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]) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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