User:Geek3/ImageScripting

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search

This page gives an overview of several techniques how to generate images with automated scripts. Many technical images or drawings are well defined in geometric terms and it is much better to create them with an automated computer program than drawing them by hand. Advantages are

  • Possible faster image generation
  • More exact and well defined images
  • It is very simple to create altered versions or animations

Since the rendering of images is in most cases not time critical at all, it is best to use a slow but powerful programming language that will allow for rapid programming such as Python.

Generating SVG vector graphics

[edit]

SVG vector graphics with lxml etree

[edit]

This is a very low-level method. SVG vector graphics are xml documents, therefore using an xml library such as Lxml.etree is already a great enhancement over plain text generation.


Python XML etree example script:

source code (105 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

from math import *

try:
    from lxml import etree
except ImportError:
    print('You need to install the lxml library: https://lxml.de/')
    # documentation at https://docs.python.org/3/library/xml.etree.elementtree.html
    exit(1)

# SVG documentation at https://www.w3.org/TR/SVG/


class SvgDocument:
    '''
    creates a svg document structure using lxml.etree
    '''
    def __init__ (self, name, width=800, height=600, bgcolor='white',
        licence='GFDL-cc', commons=False):
        self.name = name
        self.width = int(width)
        self.height = int(height)
        self.licence = licence
        self.commons = commons
        
        # create document structure
        self.svg = etree.Element('svg',
            nsmap={None:'https://www.w3.org/2000/svg',
            'xlink':'https://www.w3.org/1999/xlink'})
        self.svg.set('version', '1.1')
        self.svg.set('baseProfile', 'full')
        self.svg.set('width', str(int(width)))
        self.svg.set('height', str(int(height)))
        
        # title
        self.title = etree.SubElement(self.svg, 'title').text = self.name
        
        # description
        self.desc = etree.SubElement(self.svg, 'desc')
        self.desc.text = self.name + '\n'
        if commons:
            self.desc.text += "\nabout: https://commons.wikimedia.org/wiki/File:{0}.svg".format(self.name)
        if self.licence == 'GFDL-cc':
            self.desc.text += "rights: Creative Commons Attribution ShareAlike license\n"
        self.desc.text += '  '
        
        # background
        if bgcolor is not None: 
            self.background = etree.SubElement(self.svg, 'rect')
            self.background.set('id', 'background')
            self.background.set('x', '0')
            self.background.set('y', '0')
            self.background.set('width', str(width))
            self.background.set('height', str(height))
            self.background.set('fill', bgcolor)
        
        # image elements
        self.content = etree.SubElement(self.svg, 'g')
        self.content.set('id', 'image')
    
    
    def draw_object(self, name, params, group=None):
        '''
        Draw arbitraty svg object.
        Params must be a dictionary of valid svg parameters.
        '''
        if group is None:
            obj = etree.SubElement(self.content, name)
        else:
            obj = etree.SubElement(group, name)
        for i, j in params.items():
            obj.set(str(i), str(j))
        return obj
    
    
    def write(self, filename=None):
        # write content to file
        if filename is None:
            filename = self.name
        outfile = open(filename + '.svg', 'wb')
        outfile.write(etree.tostring(self.svg, xml_declaration=True,
            pretty_print=True, encoding='utf-8'))
        outfile.close()
        print('image written to', filename + '.svg')


doc = SvgDocument('svg-etree-example')
doc.draw_object('text', {'style':'font-size:24px; text-anchor:middle',
    'transform':'translate(400, 50)'}).text = "SVG image created with lxml.etree"

# draw a star
n = 11
ri = 70
ro = 120
points = []
for i in range(n):
    points.append((ro * sin(i * 2*pi / n), -ro * cos(i * 2*pi / n)))
    points.append((ri * sin((i+0.5) * 2*pi / n), -ri * cos((i+0.5) * 2*pi / n)))
doc.draw_object('path', {'transform':'translate(400, 300)', 'd':'M ' +
        ' L '.join(['{:.4f},{:.4f}'.format(p[0], p[1]) for p in points]) + ' Z',
        'style':'fill:yellow; stroke:black; stroke-width:2'})
        
doc.write()
example image created with lxml etree


SVG vector graphics with python svgwrite

[edit]

Svgwrite [1] offers a very convenient way to create svg graphics from python.

Python svgwrite example script:

source code (39 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

try:
    import svgwrite
except ImportError:
    print('requires svgwrite library: https://pypi.org/project/svgwrite/')
    # documentation at https://svgwrite.readthedocs.io/
    exit(1)

from math import *

# document
size = 800, 600
name = 'svg-svgwrite-example'
doc = svgwrite.Drawing(name + '.svg', profile='full', size=size)
doc.set_desc(name, name + ".svg\nrights: Creative Commons Attribution ShareAlike license")

# background
doc.add(doc.rect(id='background', insert=(0, 0), size=size, fill='white', stroke='none'))

# text
doc.add(doc.text('SVG image created with svgwrite', font_size='24px',
    text_anchor='middle', transform='translate(400, 50)', stroke='none', fill='black'))

# draw a star
n = 11
ri = 70
ro = 120
points = []
for i in range(n):
    points.append((ro * sin(i * 2*pi / n), -ro * cos(i * 2*pi / n)))
    points.append((ri * sin((i+0.5) * 2*pi / n), -ri * cos((i+0.5) * 2*pi / n)))
doc.add(doc.path(transform='translate(400, 300)', d='M ' +
        ' L '.join(['{:.4f},{:.4f}'.format(p[0], p[1]) for p in points]) + ' Z',
        fill='yellow', stroke='black', stroke_width='2'))

doc.save(pretty=True)
example images created with python svgwrite
Poincare-sphere arrows.svg
Poincare sphere
Ehrenfest-paradox-disk.svg
Ehrenfest paradox
Cylindrical magnet.svg
Cylindrical magnet

SVG vector graphics with cairo

[edit]

The Cairo library provides powerful drawing techniques with SVG export. However the control of how the output file will internally look like is more restricted with this method. For instance, text is automatically converted to paths.


Python cairo SVG example script:

source code (56 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

try:
    import cairo as C
except ImportError:
    print('You need to install the cairo library')
    # documentation at https://www.cairographics.org/documentation/
    exit(1)

from math import *

filename = 'svg-cairo-example.svg'
width, height = 800, 600

# create an image
surf = C.SVGSurface(filename, width, height)
ctx = C.Context(surf)

# background
ctx.new_path()
ctx.set_source_rgb(1, 1, 1)
ctx.rectangle(0, 0, width, height)
ctx.fill()

# text
ctx.set_font_size(24)
ctx.set_source_rgb(0, 0, 0)
text = 'SVG image created with cairo'
ctx.move_to (400 - ctx.text_extents(text)[2] / 2., 50)
ctx.show_text(text)

# draw a star
n = 11
mx, my = 400, 300
ri = 70
ro = 120
points = []
for i in range(n):
    points.append((mx + ro * sin(i * 2*pi / n), my - ro * cos(i * 2*pi / n)))
    points.append((mx + ri * sin((i+0.5) * 2*pi / n), my - ri * cos((i+0.5) * 2*pi / n)))

ctx.new_path()
ctx.move_to(*points[0])
for p in points[1:]:
    ctx.line_to(*p)
ctx.close_path()

ctx.set_source_rgb(1, 1, 0)
ctx.fill_preserve()
ctx.set_source_rgb(0, 0, 0)
ctx.set_line_width(2.0)
ctx.stroke()

# save as SVG
surf.finish()


SVG vector graphics with Matplotlib

[edit]

The Matplotlib plotting library can be used to create arbitrary vector drawings not necessarily related to plots. The usage is rather high-level and provides many specific drawing functions.


Python Matplotlib SVG example script:

source code (52 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

'''
example script how to create an svg image with matplotlib
Graphics elements documentation: https://matplotlib.org/api/patches_api.html
'''

from math import *
import matplotlib.pyplot as plt
import matplotlib.patches as patches


# settings
fname = 'svg-matplotlib-example'
width, height = 800, 600 # image size in pixels


# prepare the image canvas
fig = plt.figure(figsize=(width/72., height/72.), dpi=72)
fig.gca().set_position((0, 0, 1, 1))
plt.xlim(0, width)
plt.ylim(0, height)
plt.axis('off')

# text
plt.text(width/2., height * 11./12., 'svg image created with Matplotlib',
    fontsize=24, horizontalalignment='center')

# draw a star
n = 11
mx, my = 400, 300
ri = 70
ro = 120

points = []
for i in range(n):
    ao = i * 2*pi / n
    ai = (i+0.5) * 2*pi / n
    points.append((mx + ro * sin(ao), my + ro * cos(ao)))
    points.append((mx + ri * sin(ai), my + ri * cos(ai)))

fig.gca().add_patch(
    patches.Polygon(points, closed=True, facecolor='yellow', edgecolor='black'))


# save and set unit to pixels
fig.savefig(fname + '.svg', dpi=72)
txt = open(fname + '.svg', 'r').read()
txt = txt.replace('width="%ipt"'%width, 'width="%ipx"'%width, 1)
txt = txt.replace('height="%ipt"'%height, 'height="%ipx"'%height, 1)
open(fname + '.svg', 'w').write(txt)


Drawing raster images

[edit]

Raster images with PIL

[edit]

The Python Image Library (PIL) provides very basic drawing capabilities. PIL is actually better suited for pixel-wise image manipulation.


Python PIL example script:

source code (40 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

try:
    from PIL import Image, ImageDraw
except ImportError:
    print('You need to install the Python Image Library (http://www.pythonware.com/products/pil/)')
    # documentation at http://effbot.org/imagingbook/pil-index.htm
    exit(1)

from math import *

width, height = 800, 600

# create an image
im = Image.new('RGBA', (width, height))
draw = ImageDraw.Draw(im)

# background
draw.rectangle(((0, 0), (width, height)), fill=(255, 255, 255))

# text
text = 'PNG image created with PIL'
draw.text((400 - draw.textsize(text)[0] / 2., 50), text, (0, 0, 0))

# draw a star
n = 11
mx, my = 400, 300
ri = 70
ro = 120
points = []
for i in range(n):
    points.append((mx + ro * sin(i * 2*pi / n), my - ro * cos(i * 2*pi / n)))
    points.append((mx + ri * sin((i+0.5) * 2*pi / n), my - ri * cos((i+0.5) * 2*pi / n)))

draw.polygon(points, outline=(0, 0, 0), fill=(255, 255, 0))

# save as PNG
im.save('png-PIL-example.png')


Raster images with cairo

[edit]

Cairo is a very powerful graphics library and can be accessed from python.


Python cairo raster example script:

source code (55 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

import cairo as C
from math import *

width, height = 800, 600

# create an image
surf = C.ImageSurface(C.FORMAT_RGB24, width, height)
ctx = C.Context(surf)

# background
ctx.new_path()
ctx.set_source_rgb(1, 1, 1)
ctx.rectangle(0, 0, width, height)
ctx.fill()

# text
ctx.set_font_size(24)
ctx.set_source_rgb(0, 0, 0)
text = 'PNG image created with cairo'
ctx.move_to (400 - ctx.text_extents(text)[2] / 2, 50)
ctx.show_text(text)

# draw a star
n = 11
mx, my = 400, 300
ri = 70
ro = 120
points = []
for i in range(n):
    points.append((mx + ro * sin(i * 2*pi / n), my - ro * cos(i * 2*pi / n)))
    points.append((mx + ri * sin((i+0.5) * 2*pi / n), my - ri * cos((i+0.5) * 2*pi / n)))

ctx.new_path()
ctx.move_to(*points[0])
for p in points[1:]:
    ctx.line_to(*p)
ctx.close_path()

ctx.set_source_rgb(1, 1, 0)
ctx.fill_preserve()
ctx.set_source_rgb(0, 0, 0)
ctx.set_line_width(2.0)
ctx.stroke()

# save as PNG
surf.write_to_png('png-cairo-example.png')
cairo raster image example


gif Animations with Matplotlib

[edit]

Matplotlib has not only excellent graph plottling capabilites, but it can also be used to create arbitrary gif animations with relatively low effort.


Python Matplotlib raster example script:

source code (54 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

'''
example script how to create a gif animation with matplotlib
Animation documentation: http://wiki.scipy.org/Cookbook/Matplotlib/Animations
Graphics elements documentation: http://matplotlib.org/api/patches_api.html
'''

from math import *
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from matplotlib import animation


# settings
fname = 'gif-matplotlib-example'
width, height = 800, 600
nframes = 40
fps=20


def animate(nframe):
    # prepare a clean and image-filling canvas for each frame
    plt.clf()
    fig.gca().set_position((0, 0, 1, 1))
    plt.xlim(0, width)
    plt.ylim(0, height)
    plt.axis('off')
    
    # text
    plt.text(width/2, 550, 'gif animation created with Matplotlib',
        fontsize=20, horizontalalignment='center')
    
    # draw a star
    n = 11
    mx, my = 400, 300
    ri = 70
    ro = 120
    a0 = 2*pi/n * float(nframe)/nframes # rotation angle
    
    points = []
    for i in range(n):
        ao = i * 2*pi / n - a0
        ai = (i+0.5) * 2*pi / n - a0
        points.append((mx + ro * sin(ao), my - ro * cos(ao)))
        points.append((mx + ri * sin(ai), my - ri * cos(ai)))
    
    fig.gca().add_patch(
        Polygon(points, closed=True, facecolor='yellow', edgecolor='black'))

fig = plt.figure(figsize=(width/100., height/100.))
anim = animation.FuncAnimation(fig, animate, frames=nframes)
anim.save(fname + '.gif', writer='imagemagick', fps=fps)
Matplotlib animation example


Pixel-wise image generation

[edit]

Pixel-wise image generation with Netpbm

[edit]

Netpbm or PNM is a simplistic format to create ASCII and binary images. It does not require any additional libraries, which maximizes the portability of scripts using it. PNM images are understood by all decent graphics programs and they can be converted to a more common raster image format such as PNG with free graphics software like GIMP or the command line program pnmtopng.


Python PNM example script:

source code (39 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

# use the PNM format (http://en.wikipedia.org/wiki/Netpbm_format)
# no need for external libraries

import colorsys
from math import *

width, height = 800, 600

# create header
image = open('pnm-example.ppm', 'w')
image.write('P3\n') # color image
image.write('{0} {1}\n'.format(width, height))
image.write('255\n')

def color(ix, iy):
    '''return color for specific position'''
    # create a rainbow ring
    rx = (ix + 0.5 - width / 2) / 100
    ry = (iy + 0.5 - height / 2) / 100
    angle = pi + atan2(rx, ry)
    amplitude = (rx**2 + ry**2) * exp(1 - (rx**2 + ry**2))
    return colorsys.hsv_to_rgb(angle / (2*pi), amplitude, 1)


for iy in range(height):
    for ix in range(width):
        c = color(ix, iy)
        image.write('{0} {1} {2}'.format(*[int(max(0, min(256*i, 255))) for i in c]))
        if (ix < width - 1):
            image.write(' ')
        else:
            image.write('\n')
	
image.close()


Pixel-wise image generation with imageio

[edit]

imageio is a Python library that provides an easy interface to read and write a wide range of image data. It can be used so save NumPy array images and to replace the previous SciPy misc.imsave() method, that has been removed from the library. This opens the convenient possibility to create pixe-wise defined images from scratch.

Python imageio example script:

source code (34 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

'''
Creating simple raster images with imageio
documentation: https://imageio.github.io/
'''

import numpy as np
import imageio
import colorsys
from math import *

width, height = 800, 600

def color(ix, iy):
    '''return color for specific position'''
    # create a rainbow ring
    rx = (ix + 0.5 - width / 2) / 100
    ry = (iy + 0.5 - height / 2) / 100
    angle = pi + atan2(rx, ry)
    amplitude = (rx**2 + ry**2) * exp(1 - (rx**2 + ry**2))
    return colorsys.hsv_to_rgb(angle / (2*pi), amplitude, 1)

image = np.zeros((height, width, 3), dtype=int)
for iy in range(height):
    for ix in range(width):
        c = color(ix, iy)
        image[iy, ix] = tuple([int(max(0, min(256 * i, 255))) for i in c])

imageio.imsave('png-imageio-example.png', image)

Pixel-wise images with PIL

[edit]

The Python Image Library (PIL) provides very basic drawing capabilities. PIL is actually better suited for pixel-wise image manipulation.


Python PIL example script:

source code (34 lines)
#!/usr/bin/python3
# -*- coding: utf8 -*-

'''
# Creating simple raster images with PIL
documentation: http://effbot.org/imagingbook/image.htm
'''

from PIL import Image
import numpy as np
import colorsys
from math import *

width, height = 800, 600

def color(ix, iy):
    '''return color for specific position'''
    # create a rainbow ring
    rx = (ix + 0.5 - width / 2) / 100
    ry = (iy + 0.5 - height / 2) / 100
    angle = pi + atan2(rx, ry)
    amplitude = (rx**2 + ry**2) * exp(1 - (rx**2 + ry**2))
    return colorsys.hsv_to_rgb(angle / (2*pi), amplitude, 1)

image_file = Image.new('RGB', (width, height))
for iy in range(height):
    for ix in range(width):
        c = tuple([int(max(0, min(256 * i, 255))) for i in color(ix, iy)])
        image_file.putpixel((ix, iy), c)

image_file.save('PIL-pixelwise-example.png', 'PNG')
example pixel-wise images created with PIL
Airydisks rayleigh sqrt.png
Rayleigh criterion
Hydrogen eigenstate n4 l3 m1.png
Hydrogen orbital


Pixel-wise gif Animations with Matplotlib

[edit]

Matplotlib can also be used for animations that are defined per pixel. We simply need to fill a 2D array of color values and then pass it to Matplotlib via imshow().


Python Matplotlib pixel-wise raster example script:

source code (56 lines)
#!/usr/bin/env python3
# -*- coding: utf8 -*-

from math import *
import colorsys
import scipy
import matplotlib.pyplot as plt
from matplotlib import animation
from PIL import Image

# settings
fname = 'matplotlib-pixelwise-animation-example'
width, height = 200, 200
nframes = 20
fps = 10

mx, my = (width - 1) / 2, (height - 1) / 2

def func(x, y, phase):
    r = sqrt((y - my)**2 + (x - mx)**2)
    phi = atan2(y - my, x - mx) + r / 50 * sin(2*pi * phase)
    a = exp(-((x-mx)**2 + (y-my)**2) / 60**2)
    hsv = (((phi / (2*pi)) % 1 + 1) % 1, a, 1)
    return colorsys.hsv_to_rgb(*hsv)

def draw(nframe):
    # draw an image
    image = scipy.zeros((height, width, 3))
    
    phase = nframe / float(nframes)
    for y in range(height):
        for x in range(width):
            image[y, x, :] = scipy.array(func(x, y, phase))
    return image

def animate(nframe):
    # prepare a clean and image-filling canvas for each frame
    plt.clf()
    fig.gca().set_position((0, 0, 1, 1))
    plt.xlim(-0.5, width + 0.5)
    plt.ylim(-0.5, height + 0.5)
    plt.axis('off')
    
    print('frame', nframe)
    image = draw(nframe)
    
    imgplot = plt.imshow(image)
    imgplot.set_interpolation('nearest')

fig = plt.figure(figsize=(width / 100, height / 100))
anim = animation.FuncAnimation(fig, animate, frames=nframes, repeat=False)
anim.save(fname + '.gif', writer='imagemagick', fps=fps)
Matplotlib pixelwise animation example


Assembling animations

[edit]
gifsicle animation example

Creating animations in the GIF format is easy once you created a series of still-images. All you need is the free program gifsicle. If the still images are available in another format such as PNG or SVG, ImageMagick can do the conversion. The following commands can be executed in the command line shell such as the bash or by using cygwin. The most convenient way is to embed the commands in a python script using os.system(command).

Convert all still frames to GIF:

for i in *.svg;
  do convert "$i" "$(echo $i | sed s/\\\.svg/\\\.gif/)";
done


Combine the GIF frames to an animation:

gifsicle -d5 -l0 *.gif > animation.gif