Aaron

Merge branch 'develop' into 'master'

Simplified code; Gradients working, no color yet;



See merge request !4
Showing 1 changed file with 263 additions and 106 deletions
... ... @@ -80,7 +80,7 @@
###
from PIL import Image
import pandas as pd
from matplotlib import cm
import numpy as np
import argparse
import logging
... ... @@ -143,6 +143,245 @@ def parse_args():
return args
class Mandelbrot():
def __init__(self):
logger.info('Initializing Mandelbrot explorer')
self.iterations = 200
self.gradient = [50, 200]
self.res = {'re': 3000,
'im': 2000}
self.zoom = {'level': 0,
'factor': 2,
'center': [0,0]}
self.bounds = {'re': [1, -2],
'im': [1, -1]}
self.lengths = {'re': 3,
'im': 2}
self.incr = {'re': 0.001,
'im': 0.001}
self.gconf = [
(0, ( 0, 0, 0)),
(1, (200, 255, 200)),
(2, (150, 200, 150)),
(3, (100, 150, 100)),
(4, ( 50, 100, 50)),
]
self.c = np.zeros((self.res['im'], self.res['re']), dtype=np.complex_)
self.z = np.zeros((self.res['im'], self.res['re']), dtype=np.complex_)
self.niters = np.zeros((self.res['im'], self.res['re']))
self.img = np.zeros((self.res['im'], self.res['re'], 3), dtype=np.uint8)
def brot(self, z, c):
return (z * z) + c
def calc_zoom(self):
logger.info('Zooming to: Level %s @ %sx, centered at %s',
self.zoom['level'],
self.zoom['factor'],
self.zoom['center']
)
for z in range(self.zoom['level']):
# The length of line ab, and ad, the left and top edges
# of the new viewport bounding box
ab = self.res['im'] / self.zoom['factor']
ad = self.res['re'] / self.zoom['factor']
# Calculate the new bounds of each axis, centered about self.zoom['center']
re_min = self.bounds['re'][1] + ((self.zoom['center'][0] - (ad / 2)) * self.incr['re'])
im_min = self.bounds['im'][1] + ((self.zoom['center'][1] - (ab / 2)) * self.incr['im'])
re_max = self.bounds['re'][0] - (((self.res['re'] - self.zoom['center'][0]) - (ad / 2)) * self.incr['re'])
im_max = self.bounds['im'][0] - (((self.res['im'] - self.zoom['center'][1]) - (ab / 2)) * self.incr['im'])
self.set_bounds(re_min=re_min, re_max=re_max, im_min=im_min, im_max=im_max)
# Calculate length of each axis
self.calc_lengths()
# Calculate the graph-size of each pixel
self.calc_increments()
# After the first iteration of zoom, the frame is now centered about
# the original zoom center, so we adjust to continue zooming to the
# center of the frame in order to continue zooming on the desired pixel
center = [int(self.res['re'] / 2),
int(self.res['im'] / 2)]
self.set_zoom(center=center)
# Debug logging
logger.debug('re_min: %s + ((%s - (%s / 2)) * %s)',
self.bounds['re'][1],
self.zoom['center'][0],
ad,
self.incr['re']
)
logger.debug('re_max: %s - (((%s - %s) - (%s / 2)) * %s)',
self.bounds['re'][0],
self.res['re'],
self.zoom['center'][0],
ad,
self.incr['re']
)
logger.debug('im_min: %s + ((%s - (%s / 2)) * %s)',
self.bounds['im'][1],
self.zoom['center'][1],
ab,
self.incr['im']
)
logger.debug('im_max: %s - (((%s - %s) - (%s / 2)) * %s)',
self.bounds['im'][0],
self.res['im'],
self.zoom['center'][1],
ab,
self.incr['im']
)
def calc_increments(self):
self.incr['re'] = self.lengths['re'] / self.res['re']
self.incr['im'] = self.lengths['im'] / self.res['im']
logger.debug('incr[re]: %s, incr[im]: %s', self.incr['re'], self.incr['im'])
def calc_lengths(self):
self.lengths['re'] = abs(self.bounds['re'][0] - self.bounds['re'][1])
self.lengths['im'] = abs(self.bounds['im'][0] - self.bounds['im'][1])
logger.debug('len[re]: %s, len[im]: %s', self.lengths['re'], self.lengths['im'])
def set_bounds(self, re_max=None, re_min=None, im_max=None, im_min=None):
if re_max is not None:
self.bounds['re'][0] = re_max
if re_min is not None:
self.bounds['re'][1] = re_min
if im_max is not None:
self.bounds['im'][0] = im_max
if im_min is not None:
self.bounds['im'][1] = im_min
logger.debug('bounds[re]: (%s, %s)', self.bounds['re'][0], self.bounds['re'][1])
logger.debug('bounds[im]: (%s, %s)', self.bounds['im'][0], self.bounds['im'][1])
def set_resolution(self, re_res, im_res=None):
self.res['re'] = int(re_res)
if im_res is None:
self.res['im'] = int((2 * self.res['re']) / 3)
else:
self.res['im'] = int(im_res)
logger.debug('resolution: (%s, %s)', self.res['im'], self.res['re'])
def set_zoom(self, level=None, factor=None, center=None):
if level is not None:
self.zoom['level'] = level
if factor is not None:
self.zoom['factor'] = factor
if center is not None:
self.zoom['center'] = center
logger.debug('zoom: %s', self.zoom)
def calc_c(self):
for yidx, y in enumerate(range(1, self.res['im'] + 1)):
for xidx, x in enumerate(range(1, self.res['re'] + 1)):
# Calculates the value of c for each pixel on the graph
cval = complex(
self.bounds['re'][1] + (self.incr['re'] * x),
self.bounds['im'][1] + (self.incr['im'] * y)
)
self.c[yidx][xidx] = cval
def calc_z(self):
logger.info('Iterating z values')
for i in range(self.iterations):
# All bounded values are contained within 2.0+2.0j, -2.0-2.0j
self.z = np.where(
np.logical_and(
self.z.real <= 2.0,
self.z.imag <= 2.0
),
self.brot(self.z, self.c),
self.z
)
self.niters = self.niters + 1
logger.info('Rendering complete after %s iterations', i+1)
def calc_gradient(self):
logger.info('Generating image data')
rgbs = []
# Generates an array of RGB data equal in size to the final image,
# containing the user-provided gradient configuration
for niters, rgb in self.gconf:
rgbs.append((niters, np.full((self.res['im'], self.res['re'], 3), rgb)))
# Masks the gradient layers into the final image. It does this by
# placing a layer from the RGB config if the iterations match the
# config.
for idx in range(len(rgbs)):
niters, rgb = rgbs[idx]
self.img[...,0] = np.where(
self.z.real >= niters,
rgb[...,0],
self.img[...,0]
)
self.img[...,1] = np.where(
self.z.real >= niters,
rgb[...,1],
self.img[...,1]
)
self.img[...,2] = np.where(
self.z.real >= niters,
rgb[...,2],
self.img[...,2]
)
def write_frame(self, filename):
logger.info('Writing image to %s', args.outfile)
im = Image.fromarray(np.uint8(self.img))
im.save(filename)
if __name__ == '__main__':
start = time.time()
... ... @@ -152,118 +391,36 @@ if __name__ == '__main__':
logging.basicConfig(level=args.loglevel, format='%(asctime)s %(message)s')
logger = logging.getLogger()
logger.info('Initializing Mandelbrot explorer')
m = Mandelbrot()
iters = args.iters
zoom_level = args.zoom_level
zoom_factor = args.zoom_factor
zoom_center = args.zoom_center
Re_res = args.re_res
if args.im_res is None:
Im_res = int((2 * Re_res) / 3)
else:
Im_res = args.im_res
Re_lim = (1, -2)
Im_lim = (1, -1)
Re_len = abs(Re_lim[0] - Re_lim[1])
Im_len = abs(Im_lim[0] - Im_lim[1])
Re_incr = Re_len / Re_res
Im_incr = Im_len / Im_res
logger.info('Zooming to: Level %s @ %sx, centered at %s', zoom_level, zoom_factor, zoom_center)
for z in range(zoom_level):
# The length of line ad, the top of the new bounding box
ad = Re_res / zoom_factor
# Calculate the new bounds of the real axis, centered about zoom_center
Re_min = Re_lim[1] + ((zoom_center[0] - (ad / 2)) * Re_incr)
Re_max = Re_lim[0] - (((Re_res - zoom_center[0]) - (ad / 2)) * Re_incr)
logger.debug('Re_min: %s + ((%s - (%s / 2)) * %s)', Re_lim[1], zoom_center[0], ad, Re_incr)
logger.debug('Re_max: %s - (((%s - %s) - (%s / 2)) * %s)', Re_lim[0], Re_res, zoom_center[0], ad, Re_incr)
# The length of line ab, the left side of new bounding box
ab = Im_res / zoom_factor
# Calculate the new bounds of the imaginary axis, centered about zoom_center
Im_min = Im_lim[1] + ((zoom_center[1] - (ab / 2)) * Im_incr)
Im_max = Im_lim[0] - (((Im_res - zoom_center[1]) - (ab / 2)) * Im_incr)
logger.debug('Im_min: %s + ((%s - (%s / 2)) * %s)', Im_lim[1], zoom_center[1], ab, Im_incr)
logger.debug('Im_max: %s - (((%s - %s) - (%s / 2)) * %s)', Im_lim[0], Im_res, zoom_center[1], ab, Im_incr)
# Update bounds
Re_lim = (Re_max, Re_min)
Im_lim = (Im_max, Im_min)
# Calculate length of each axis
Re_len = abs(Re_lim[0] - Re_lim[1])
Im_len = abs(Im_lim[0] - Im_lim[1])
# Calculate the graph-size of each pixel
Re_incr = Re_len / Re_res
Im_incr = Im_len / Im_res
# After the first iteration of zoom, the frame is now centered about
# the original zoom center, so we adjust to continue zooming to the
# center of the frame in order to continue zooming on the desired pixel
zoom_center = (int(Re_res / 2), int(Im_res / 2))
logger.info('Re bounds: %s', Re_lim)
logger.info('Im bounds: %s', Im_lim)
logger.info('Re increment: %s', Re_incr)
logger.info('Im increment: %s', Im_incr)
logger.info('Initializing c values for frame')
cdata = np.zeros((Im_res, Re_res), dtype=np.complex_)
for yidx, y in enumerate(range(1, Im_res + 1)):
for xidx, x in enumerate(range(1, Re_res + 1)):
# Calculates the value of c for each pixel on the graph
cdata[yidx,xidx] = complex(Re_lim[1] + (Re_incr * x), Im_lim[1] + (Im_incr * y))
brot = lambda z, c: (z * z) + c
pixels = np.full((Im_res, Re_res), complex(0,0))
logger.info('Iterating z values')
for i in range(iters):
prev_pixels = np.copy(pixels)
pixels = np.where(
np.logical_and(
pixels.real < 2.0, pixels.imag < 2.0
),
brot(pixels, cdata),
pixels + complex(i,i)
)
# Iterations
m.iterations = args.iters
# Colors
m.gradient = [50, 200]
if np.array_equal(prev_pixels, pixels):
break
# Image resolution for each axis
m.set_resolution(args.re_res)
logger.info('Rendering complete after %s iterations', i+1)
logger.info('Generating image data')
# Zoom settings
m.set_zoom(level=args.zoom_level,
factor=args.zoom_factor,
center=args.zoom_center
)
ratio = 255 / iters
img_data = np.zeros((Im_res, Re_res, 3), dtype=np.uint8)
m.calc_lengths()
m.calc_increments()
m.calc_zoom()
for yidx, y in enumerate(pixels):
for xidx, x in enumerate(y):
# COMPUTE
m.calc_c()
m.calc_z()
if x.real >= 2.0 or x.imag >= 2.0:
val = int((x.real / iters) * 255)
val = 1.0 if val > 1.0 else val
img_data[yidx][xidx] = [val] * 3
else:
img_data[yidx][xidx] = (255,255,255)
# Generate image data
m.calc_gradient()
logger.info('Writing image to %s', args.outfile)
im = Image.fromarray(img_data)
im.save(args.outfile)
# Write image
m.write_frame(args.outfile)
end = time.time()
logger.info('Frame rendering complete in %s seconds', (end - start))
... ...