-
Notifications
You must be signed in to change notification settings - Fork 0
/
generator.py
176 lines (156 loc) · 7.46 KB
/
generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/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()