pattern_based_images/generator.py

177 lines
7.5 KiB
Python

#!/usr/bin/env python3
import libpme, math, random, sys
import pip._vendor.progress.bar as libbar # used this in 2bit, love this dirty little hack
# The string will be used in sbuilder to print out the pattern and for the filename
operations = {
"^": lambda x, y: x ^ y,
"**": lambda x, y: x ** y,
"*": lambda x, y: x * y,
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"%?": lambda x, y: x if y == 0 else x % y,
"÷?": lambda x, y: x if y == 0 else x / y, # don't want division by zero errors, and filenames can't have / in them.
"log? base": lambda x, y: x if abs(x) <= 1 or abs(y) <= 1 else math.log(abs(x), abs(y)),
#"~": lambda x, y: ~(getop()(x, y)), # todo
">>": lambda x, y: x >> y,
"<<": lambda x, y: x << y,
"&": lambda x, y: x & y,
"|": lambda x, y: x | y
}
unary_operations = {
"~": lambda s, x, y: ~(s(x, y)) if type(x) == type(0) and type(y) == type(0) else "~" + str(s(x, y))
}
# gensym has a 1/3 chance of returning a lambda that returns x, 1/3 for y, and 1/3 for a number generated *when gensym was run*.
def gensym():
r = random.randint(0, 2)
if r == 0:
return lambda x, y: y
elif r == 1:
return lambda x, y: x
else:
r2 = random.randint(1, 16)
# we can't just write "return lambda x, y: random.randint(1, 16)" or it would generate a different random number for each pixel. That bug took forever to find
# as bryan says, "this is what happens when you arent completely fluent in being multiple closures deep at all times"
return lambda x, y: r2
# iterate through syms and ops, each time returning a left associative expression
# so for ops = ["+", "*"] and syms = [5, x, y]
# it would return something that takes an x and y, and effectively returns ((y + x) * 5)
def builder(syms, ops, i = 0):
if len(ops) == 0:
return lambda x, y: syms[i](x, y) # if there are no more operations left, just evaluate the symbol.
return lambda x, y: operations[ops[0]](round(builder(syms, ops[1:], i + 1)(x, y)), syms[i](x, y)) # if there are operations, pop the first one off and recurse
# used in sbuilder to pretty-print the formula
def get_bitmask_char(bitmask, greyscale):
if greyscale == "modulo":
return "%"
return "&"
# Does the same thing as builder, but makes a string
def sbuilder(syms, bitmask, greyscale, ops, i = 0, recurse = False):
if len(ops) == 0:
return str(syms[i]("x", "y"))
if recurse:
return "(" + sbuilder(syms, bitmask, greyscale, ops[1:], i + 1, True) + ") " + ops[0] + " " + str(syms[i]("x", "y"))
return "(" + sbuilder(syms, bitmask, greyscale, ops, i, True) + ") " + get_bitmask_char(bitmask, greyscale) + " " + str(bitmask) # the first time we're called, just print the real value in parenthesis, with an &/% bitmask at the end.
# do the actual bitmasking
def mask(val, greyscale, bitmask):
val = round(val)
if greyscale == "modulo":
return val % 256 # modulo by the maximum brightness for one pixel (8 bits/channel so 2**8)
if greyscale:
return min(255, max(0, val)) # truncate to one byte
if bitmask == 1: # optimization. We could leave this case out and let it use the default, but it would be slower.
return val & bitmask
return 0 if val & bitmask == 0 else 1 # used for bitmask = 128 (arg == "high") only right now
# actually creates an image given a function and a filename.
def build(the_function, name, greyscale):
img = libpme.PME()
img.height = img.width = 1024
img.color_type = libpme.color_types.GREYSCALE
img.bit_depth = 1
if greyscale:
img.bit_depth = 8
data = b'' # will hold the raw pixel data
bar = libbar.IncrementalBar(max = img.height)
bar.start()
for y in range(img.height):
this_scanline = b''
this_scanline += b'\x00' # to indicate that this scanline contains raw pixel data.
if not greyscale:
for x in range(0, img.width, 8): # eight pixels per byte of output, because bit_depth is 1
this_pixel = 0;
for subx in range(8):
this_x = x + subx
val = the_function(this_x, y)
this_pixel += val
this_pixel <<= 1
this_pixel >>= 1
this_scanline += bytes([this_pixel])
else: # for greyscale each pixel is one byte
for x in range(img.width):
this_scanline += bytes([the_function(x, y)])
# bar.update run all the time causes screen flickering, so only run it every 13 scanlines. greyscale is so slow that we may as well run it every time anyways.
data += this_scanline
if y % 13 == 0 or greyscale:
bar.index = y
bar.update()
bar.index = img.height # finish up
bar.finish()
print()
# save the image
img.write_raw_idat_data(img.compress(data))
img.save(name + ".png")
# calls builder to build the the_function function and passes it the building function, build.
def generate(arg = "default", ops = False, syms = False):
if not ops:
ops = [random.choice([x for x, y in operations.items()]) for k in range(random.randint(2, 6))]
if not syms:
syms = [gensym() for i in range(len(ops) + 1)]
# for i in range(len(syms)):
# if random.randint(0, 4) == 0:
# sym = syms[i] # do not try and inline this
# syms[i] = lambda x, y: unary_operations[random.choice(x for x, y in unary_operations.items())](sym, x, y)
literals = [f("x", "y") for f in syms]
if "y" not in literals or "x" not in literals:
print("DEBUG! " + sbuilder(syms, 1, False, ops) + " does not contain both x and y. Trying another")
generate(arg)
return
# uncomment to use the sample data. I used this to test the builder function.
# ops = [">>", "*", "-", "^"]
# x = lambda x, y: x
# y = lambda x, y: y
# syms = [x, y, y, x, lambda x, y: 11]
# syms.reverse()
# if we're running everything, use the now-generated operations and symbols to generate all four functions and exit
if arg == "all":
for a in ["default", "high", "greyscale", "modulo"]:
generate(a, ops, syms)
return
# set bitmask and greyscale based on arg
bitmask = 1
greyscale = False
if arg == "high":
bitmask = 128
elif arg == "greyscale":
greyscale = True
bitmask = 255
elif arg == "modulo":
greyscale = arg
bitmask = 255
# we could abstract the arguments into a dictionary that maps string argument -> list [bitmask, greyscale], then the if arg == all could just iterate over [x for x, y in args.items()]. Maybe next update
# debug
#print(ops)
#the_function = lambda x, y: (((x^y)-y)*x >> 11) & 1
# print out the pattern, also generate the function
print(sbuilder(syms, bitmask, greyscale, ops));
the_function = lambda x, y: mask(builder(syms, ops)(x, y), greyscale, bitmask)
# debug
#print(the_function(2, 2))
# i = int(sys.argv[1])
# badfiles = open("badfiles", "r").read().split("\n")[:-1]
# the_function = eval("lambda x, y: " + badfiles[i].split("/")[-1].replace(" BAD FILENAME.png", ""))
# pass off control to the thing that actually uses the function to make an image, using the value of sbuilder as the filename.
build(the_function, sbuilder(syms, bitmask, greyscale, ops), greyscale)
if len(sys.argv) > 1:
generate(sys.argv[1].lower())
else:
generate()