Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # standard library modules
- import argparse
- import os
- import sys
- import random
- import math
- import time
- import textwrap
- import msvcrt
- # custom modules
- try:
- from PIL import Image, ImageDraw
- except ImportError as error:
- error_msg = ('ImportError: {} found in the current working directory or '
- 'any PYTHONPATH location.'.format(error))
- sys.stderr.write('\n'.join(textwrap.wrap(error_msg, 78)) + '\n')
- error_msg = ('Please ensure the module file is in the current working '
- 'directory or a PYTHONPATH location before executing the '
- 'program again')
- sys.stderr.write('\n'.join(textwrap.wrap(error_msg, 78)) + '\n')
- sys.stderr.flush()
- time.sleep(0.25)
- print('\nPress any key to quit this program.')
- msvcrt.getch()
- sys.exit()
- def opt_parse():
- """Command-line argument parser"""
- description = ("""A program or /g/ divertimento that draws a somewhat artistic
- rendition of a source image via the use of shapes and a variety
- of chosen parameters.
- Every time the program runs a directory is created and around
- 50 images in progress are saved there for the sole reason of
- amusement and webm making. The directory name is chosen from
- among parameters values.""")
- usage = "%(prog)s [-h] -s [line|circle|square] -l <LENGTH> -t <THICKNESS> -n <ITERS> -i <INFILE> -o <OUTFILE> -a <NNN-NNN> -c [1|2|3] -e <ED ROC>"
- parser = argparse.ArgumentParser(description=description, usage=usage)
- help = "specifies the shape to use. Default: line"
- parser.add_argument("-s", "--shape", dest="shape", action="store", metavar="[line|circle|square]", default="line",
- help=help)
- help = "specifies line length, circle radius or square side length depending what shape is used. Default: 30"
- parser.add_argument("-l", "--length", dest="length", type=int, metavar="<LENGTH>", default=30,
- help=help)
- help = "specifies line thickness or the square breadth. Default: 1"
- parser.add_argument("-t", "--thickness", dest="thickness", type=int, metavar="<THICKNESS>", default=1,
- help=help)
- help = "specifies the maximun number of iterations to perform. Default: 50000"
- parser.add_argument("-n", "--iters", dest="iterations", type=int, metavar="<ITERS>", default=50000,
- help=help)
- help = "specifies the image filename to serve as source"
- parser.add_argument("-i", "--infile", dest="infile", action="store", metavar="<INFILE>", required=True,
- help=help)
- help = "specifies the image filename to save the final result"
- parser.add_argument("-o", "--outfile", dest="outfile", action="store", metavar="<OUTFILE>", required=True,
- help="help")
- help = "specifies the range of angles to use while drawing lines. Ex: 010-359 signifies a range between 10 to 359 degrees. Default: 000-359"
- parser.add_argument("-a", "--angle_range", dest="angle_range", action="store", metavar="<NNN-NNN>", default="000-359",
- help=help)
- help = "specifies how color distribution is sampled. use 1 for weighted whole image sampling, 2 for whole image sampling and 3 for weighted image sampling only from the area to be drawn. Default: 2"
- parser.add_argument("-c", "--color", dest="color", type=int, metavar="[1|2|3]", default=2,
- help=help)
- help = "specifies the minimum Euclidean distance change of rate to reach before the pregram stops. A value of zero ignores this setting and the program clompletes all iterations. Default: 0.0"
- parser.add_argument("-e", "--ed_roc", dest="ED_RoC", action="store", type=float, metavar="<ED ROC>", default=0.0,
- help=help)
- options = argparse.Namespace()
- options = parser.parse_args(namespace=options)
- return options
- start = time.time()
- # finally parse options
- options = opt_parse()
- # arguments parsing
- shape = options.shape
- infile = options.infile
- iterations = options.iterations
- length = options.length
- thickness = options.thickness
- outfile = options.outfile
- angle_range = (int(options.angle_range[:3]), int(options.angle_range[4:]))
- color_type = options.color
- ED_RoC = options.ED_RoC
- # directory name and creation
- if shape == 'line':
- dirname = "{}-{}-{}-{}-[{}]-{}".format(outfile[:-4],
- shape, length, thickness,
- angle_range,
- color_type)
- else:
- dirname = "{}-{}-{}-{}--{}".format(outfile[:-4],
- shape, length, thickness,
- color_type)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
- # open source image and conver
- sourceImage = Image.open(infile)
- d = sourceImage.convert("RGB")
- # make temp image
- tempImage = Image.new(sourceImage.mode, sourceImage.size, "black")
- def buildBuffer():
- ''' builds data buffers for source and temp images and weighted and
- unweighted color palletes from the original.
- '''
- sourceBuffer = dict()
- tempBuffer = dict()
- colorsBufferWeighted = []
- for x in range(0, sourceImage.size[0]):
- for y in range(0, sourceImage.size[1]):
- sourceBuffer[(x, y)] = sourceImage.getpixel((x, y))
- tempBuffer[(x, y)] = (0, 0, 0)
- colorsBufferWeighted.append(sourceImage.getpixel((x, y)))
- colorsBuffer = list(set(colorsBufferWeighted))
- return sourceBuffer, tempBuffer, colorsBufferWeighted, colorsBuffer
- def templateSquare(length, breadth):
- '''Creates a pixel mask with the required shape.
- '''
- pixels = list()
- d = max(length, breadth)
- tempImage = Image.new('1', (d + 10, d + 10), 0)
- draw = ImageDraw.Draw(tempImage)
- draw.rectangle((0, 0, length, breadth), fill=1, outline=1)
- for x in range(0, tempImage.size[0]):
- for y in range(0, tempImage.size[1]):
- if tempImage.getpixel((x, y)) == 1:
- pixels.append((x, y))
- tempImage.close()
- return pixels
- def pixelSquare(template, length, breadth):
- '''Creates a pixel field from the mask and a random point then makes
- sure to cull any that lies outside the image's dimensions.
- '''
- pixels = set()
- d = max(length, breadth)
- x = random.randint(0 - d, sourceImage.size[0] + d)
- y = random.randint(0 - d, sourceImage.size[1] + d)
- for i, g in template:
- if 0 <= (x + i) < sourceImage.size[0] and 0 <= (y + g) < sourceImage.size[1]:
- pixels.add((x + i, y + g))
- return pixels or {(sourceImage.size[0] - 1, sourceImage.size[1] - 1)}
- def templateCircle(radius):
- '''Creates a pixel mask with the required shape
- '''
- pixels = list()
- d = 2 * radius
- tempImage = Image.new('1', (d + 10, d + 10), 0)
- draw = ImageDraw.Draw(tempImage)
- draw.ellipse((0, 0, d, d), fill=1, outline=1)
- for x in range(0, tempImage.size[0]):
- for y in range(0, tempImage.size[1]):
- if tempImage.getpixel((x, y)) == 1:
- pixels.append((x, y))
- tempImage.close()
- return pixels
- def pixelCircle(template, radius):
- '''Creates a pixel field from the mask and a random point then makes
- sure to cull any that lies outside the image's dimensions.
- '''
- pixels = set()
- x = random.randint(0 - radius, sourceImage.size[0] + radius)
- y = random.randint(0 - radius, sourceImage.size[1] + radius)
- for i, g in template:
- if 0 <= (x + i) < sourceImage.size[0] and 0 <= (y + g) < sourceImage.size[1]:
- pixels.add((x + i, y + g))
- return pixels or {(sourceImage.size[0] - 1, sourceImage.size[1] - 1)}
- def templateLine(length, thickness, angle):
- '''Creates a pixel mask with the required shape
- '''
- x2 = int(length * math.cos(math.radians(angle)))
- y2 = int(length * math.sin(math.radians(angle)))
- pixels = list()
- tempImage = Image.new('1', (length * 2, length * 2), 0)
- draw = ImageDraw.Draw(tempImage)
- draw.line((length, length, x2 + length, y2 + length), fill=1, width=thickness)
- for x in range(0, tempImage.size[0]):
- for y in range(0, tempImage.size[1]):
- if tempImage.getpixel((x, y)) == 1:
- pixels.append((x - length, y - length))
- tempImage.close()
- return pixels
- def pixelLine_fixedAngle(template, length):
- '''Creates a pixel field from the mask and a random point then makes
- sure to cull any that lies outside the image's dimensions.
- '''
- pixels = set()
- x = random.randint(0 - length, sourceImage.size[0] + length)
- y = random.randint(0 - length, sourceImage.size[1] + length)
- for i, g in template:
- if 0 <= (x + i) < sourceImage.size[0] and 0 <= (y + g) < sourceImage.size[1]:
- pixels.add((x + i, y + g))
- return pixels or {(sourceImage.size[0] - 1, sourceImage.size[1] - 1)}
- ############## DOES NOT IMPROVE SHIT ###################
- # def template2Line(length, thickness):
- # """
- # """
- # template = dict()
- #
- # for zeta in range(0, 360):
- # points = list()
- # # Xiaolin Wu's line algorithm
- # x1, y1 = 0, 0
- # # get second Point
- # x2 = int(length * math.cos(math.radians(zeta)))
- # y2 = int(length * math.sin(math.radians(zeta)))
- #
- # dx, dy = x2 - x1, y2 - y1
- # is_steep = abs(dx) < abs(dy)
- #
- # def p(px, py, is_steep):
- # if not is_steep:
- # return (px, py)
- # else:
- # return (py, px)
- #
- # if is_steep:
- # x1, y1, x2, y2, dx, dy = y1, x1, y2, x2, dy, dx
- #
- # if x2 < x1:
- # x1, x2, y1, y2 = x2, x1, y2, y1
- #
- # grad = float(dy) / dx
- # intery = y1 + (1 - (x1 - int(x1))) * grad
- #
- # def draw_endpoint(pt):
- # x, y = pt
- # xend = round(x)
- # yend = y + grad * (xend - x)
- # # xgap = 1 - ((x + 0.5) - int((x + 0.5)))
- # px, py = int(xend), int(yend)
- # points.append(p(px, py, is_steep))
- # points.append(p(px, py + 1, is_steep))
- # return px
- #
- # xstart = draw_endpoint((x1, y1)) + 1
- # xend = draw_endpoint((x2, y2))
- #
- # for x in range(xstart, xend):
- # y = int(intery)
- # points.append(p(x, y, is_steep))
- # points.append(p(x, y + 1, is_steep))
- # intery += grad
- #
- # template[zeta] = list(set(points))
- #
- # return template
- #
- #
- # def pixel2Line(template, length):
- # '''
- # '''
- # pixels = set()
- #
- # x = random.randint(0, sourceImage.size[0] - 1)
- # y = random.randint(0, sourceImage.size[1] - 1)
- #
- # for h in range(thickness):
- # for i, g in template:
- # if 0 <= (x + h + i) < sourceImage.size[0] and 0 <= (y + h + g) < sourceImage.size[1]:
- # pixels.add((x + h + i, y + h + g))
- # return pixels or {(sourceImage.size[0] - 1, sourceImage.size[1] - 1)}
- def pixelLine_one_thickness(length, thickness):
- '''Generates a pixel field from the geometry of a line drawn
- randomly onto the image.
- '''
- pixels = set()
- points = list()
- # get random angle
- zeta = random.randint(angle_range[0], angle_range[1])
- # Bresenham's Line Algorithm
- x1, y1 = 0, 0
- # get second Point
- x2 = int(length * math.cos(math.radians(zeta)))
- y2 = int(length * math.sin(math.radians(zeta)))
- dx, dy = x2 - x1, y2 - y1
- is_steep = abs(dx) < abs(dy)
- # Rotate line
- if is_steep:
- x1, y1, x2, y2 = y1, x1, y2, x2
- if x2 < x1:
- x1, x2, y1, y2 = x2, x1, y2, y1
- # Recalculate differentials
- dx, dy = x2 - x1, y2 - y1
- # Calculate error
- error = int(dx / 2.0)
- ystep = 1 if y1 < y2 else -1
- # Iterate over bounding box generating points between start and end
- y = y1
- points = []
- for x in range(x1, x2 + 1):
- coord = (y, x) if is_steep else (x, y)
- points.append(coord)
- error -= abs(dy)
- if error < 0:
- y += ystep
- error += dx
- x = random.randint(0, sourceImage.size[0] - 1)
- y = random.randint(0, sourceImage.size[1] - 1)
- for h in range(thickness):
- for i, g in points:
- if 0 <= (x + h + i) < sourceImage.size[0] and 0 <= (y + h + g) < sourceImage.size[1]:
- pixels.add((x + h + i, y + h + g))
- return pixels or {(sourceImage.size[0] - 1, sourceImage.size[1] - 1)}
- def pixelLine(length, thickness):
- '''Generates a pixel list from the geometry of a line drawn
- randomly onto the image.
- '''
- pixels = set()
- points = list()
- # get random angle
- zeta = random.randint(angle_range[0], angle_range[1])
- # Xiaolin Wu's line algorithm
- x1, y1 = 0, 0
- # get second Point
- x2 = int(length * math.cos(math.radians(zeta)))
- y2 = int(length * math.sin(math.radians(zeta)))
- dx, dy = x2 - x1, y2 - y1
- is_steep = abs(dx) < abs(dy)
- def p(px, py, is_steep):
- if not is_steep:
- return (px, py)
- else:
- return (py, px)
- if is_steep:
- x1, y1, x2, y2, dx, dy = y1, x1, y2, x2, dy, dx
- if x2 < x1:
- x1, x2, y1, y2 = x2, x1, y2, y1
- grad = float(dy) / dx
- intery = y1 + (1 - (x1 - int(x1))) * grad
- def draw_endpoint(pt):
- x, y = pt
- xend = round(x)
- yend = y + grad * (xend - x)
- # xgap = 1 - ((x + 0.5) - int((x + 0.5)))
- px, py = int(xend), int(yend)
- points.append(p(px, py, is_steep))
- points.append(p(px, py + 1, is_steep))
- return px
- xstart = draw_endpoint((x1, y1)) + 1
- xend = draw_endpoint((x2, y2))
- for x in range(xstart, xend):
- y = int(intery)
- points.append(p(x, y, is_steep))
- points.append(p(x, y + 1, is_steep))
- intery += grad
- x = random.randint(0, sourceImage.size[0] - 1)
- y = random.randint(0, sourceImage.size[1] - 1)
- for h in range(thickness):
- for i, g in points:
- if 0 <= (x + h + i) < sourceImage.size[0] and 0 <= (y + h + g) < sourceImage.size[1]:
- pixels.add((x + h + i, y + h + g))
- return pixels or {(sourceImage.size[0] - 1, sourceImage.size[1] - 1)}
- def colorPixels(pixels):
- '''Builds a color palette from the given pixel field extracting the data
- from the source.
- '''
- colors = []
- for pixel in pixels:
- colors.append(sourceBuffer[pixel])
- return colors
- # relic
- # def lineDraw(pixels, color):
- # '''Draws line'''
- # for pixel in pixels:
- # tempImage.putpixel(pixel, color)
- def BufferDraw(buffer):
- '''Draws buffer to temp image'''
- for item in buffer:
- tempImage.putpixel(item, buffer[item])
- def compare(buffer1=None, buffer2=None, pixels=None, color=None):
- '''Calculates Euclidean distance between two image buffers or the origina
- image buffer and a pixel field representing a shape.'''
- r, g, b = 0, 0, 0
- for pixel in pixels:
- (a, b, c) = sourceBuffer[pixel]
- if color:
- (x, y, z) = color
- else:
- (x, y, z) = tempBuffer[pixel]
- r += (a - x) ** 2
- g += (b - y) ** 2
- b += (c - z) ** 2
- return math.sqrt(r + g + b) / len(pixels)
- sourceBuffer, tempBuffer, colorsBufferWeighted, colorsBuffer = buildBuffer()
- # relic form from when I was experimenting mixing these.
- # may play with it some more later
- colors = {1: [100, 0, 0],
- 2: [0, 100, 0],
- 3: [0, 0, 100]}
- # pixel field template to use if any
- if shape == 'circle':
- template = templateCircle(length)
- if angle_range[0] == angle_range[1] and shape == 'line':
- template = templateLine(length, thickness, random.randint(angle_range[0], angle_range[1]))
- if shape == 'square':
- template = templateSquare(length, thickness)
- # main loop
- counter, pre_ed = 0, 0
- changes, changes_p, changes_t = (0, 0, 0)
- start_l = time.time()
- for i in range(iterations):
- changes_t += 1
- # generates pixel list representing line
- if thickness == 1 and shape == 'line':
- p = pixelLine_one_thickness(length, 1)
- elif angle_range[0] == angle_range[1] and shape == 'line':
- p = pixelLine_fixedAngle(template, length)
- elif shape == 'line':
- p = pixelLine(length, thickness)
- # generates pixel list representing shape
- elif shape == 'circle':
- p = pixelCircle(template, length)
- elif shape == 'square':
- p = pixelSquare(template, length, thickness)
- # picks a random color from the original image
- color = random.choices([random.choice(colorsBufferWeighted),
- random.choice(colorsBuffer),
- random.choice(colorPixels(p))],
- colors[color_type], k=1)[0]
- # calculates Euclidean distances
- a = compare(buffer1=sourceBuffer, buffer2=tempBuffer, pixels=p)
- b = compare(buffer1=sourceBuffer, pixels=p, color=color)
- # if shape improves tenp image then draw shape
- if a > b:
- for pixel in p:
- tempBuffer[pixel] = color
- changes += 1
- changes_p += 1
- # report progress every 10000 iterations
- if i % 10000 == 0 and i != 0:
- end_l = time.time()
- text = "{:d}/{:d} iter ({:d} accepted, {:d} discarded, ratio {:.02f}), {:02.02f}% {:.0f}s."
- print(text.format(i, iterations,
- changes_p, changes_t - changes_p, float(changes_p) / (changes_t - changes_p),
- float(i) / iterations * 100, end_l - start_l))
- changes_p, changes_t = 0, 0
- start_l = time.time()
- # calculate ED every 50000 iterations
- if i % 50000 == 0 and i != 0:
- ed = compare(buffer1=sourceBuffer, buffer2=tempBuffer, pixels=sourceBuffer)
- if abs(pre_ed - ed) < ED_RoC and ED_RoC != 0.0:
- break
- print('Current ED {:.10f} ED RoC {:.10f}'.format(ed, abs(pre_ed - ed)))
- pre_ed = ed
- # save around 50 images in progress to a dir for creating webm
- if i % (iterations / 50) == 0 and i != 0:
- BufferDraw(tempBuffer)
- tempImage.save('{}/{:04d}.jpg'.format(dirname, counter))
- ed = compare(buffer1=sourceBuffer, buffer2=tempBuffer, pixels=sourceBuffer)
- print('Image {:04d} saved, ED {:.10f}'.format(counter, ed))
- counter += 1
- # save final result
- BufferDraw(tempBuffer)
- tempImage.save(outfile)
- ed = compare(buffer1=sourceBuffer, buffer2=tempBuffer, pixels=sourceBuffer)
- print('Image {} saved, ED {:.10f}.'.format(outfile, ed,))
- end = time.time()
- timetaken = end - start
- print('Job done. {} iterations ({} accepted, {} discarded). It took {:.0f} seconds'.format(iterations, changes, iterations - changes, timetaken))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement