A modern Rhinoceros

../../_images/modern_rhinoceros.webp

One of my favourite artworks is Albrecht Dürer’s Rhinoceros (image, wikipedia). It’s a woodcut of a rhinoceros, made in 1515, based on a written description and a sketch of a rhinoceros that had been brought to Lisbon from India. Dürer never actually saw the rhinoceros, and his image is an early example of what we’d now call a hallucination. But Dürer’s genius is such that his image is better than the real thing - this is what a Rhinoceros ought to look like.

I also enjoyed Nicholas Rougier’s book “Scientific Visualization: Python + Matplotlib” which reminded me that we can do so much more with matplotlib than just plot graphs. I wanted to expand my repertoire of plotting techniques, so I decided to reimagine Dürer’s Rhinoceros as a matplotlib plot.

The plan was to use as many as possible of matplotlib’s built-in graph types, and to use each type only once. I didn’t quite manage this, but it’s close. As a single woodblock, Dürer’s original is in one colour - modern technology allows removal of this restriction, but choosing colours is very difficult, so this version is in one colour palette: viridis (along with a bit of grey and black).

Code to make the poster

Written with much assistance from the awesome GitHub Copilot. Copilot’s encyclopaedic knowledge of matplotlib, and endless patience answering questions along the lines of “How do I do … ?” made the project enormously easier.

The main script creates the figure, adds some text to the top, and delegates the rest of the job to a grid of subplots.

#!/usr/bin/env python

# Re-draw Durer's Rhinoceros using Matplotlib

import sys
import matplotlib
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Polygon
from matplotlib import font_manager

from PIL import Image
import numpy as np
from utils import smoothLine, viridis, colours

# Load the original
bg_im = Image.open(r"The_Rhinoceros_(NGA_1964.8.697)_enhanced.png")
bg_im = bg_im.convert("RGB")
# Convert to numpy array on 0-1
bg_im = np.array(bg_im) / 255.0

# Figure setup
fig = Figure(
    figsize=(3000 / 100, 2368 / 100),  # Width, Height (inches)
    dpi=300,
    facecolor=colours["background"],
    edgecolor=None,
    linewidth=0.0,
    frameon=True,
    subplotpars=None,
    tight_layout=None,
)
canvas = FigureCanvas(fig)
font = {"family": "sans-serif", "sans-serif": "Arial", "weight": "normal", "size": 18}
matplotlib.rc("font", **font)

# Put image in as background - hidden in final result
axb = fig.add_axes([0, 0, 1, 1])
axb.set_axis_off()
axb.set_xlim(0, 1)
axb.set_ylim(0, 1)
# Add the image
bgi_extent = [0.03, 0.98, 0.03, 0.98]
# axb.imshow(bg_im, extent=bgi_extent, aspect="auto", alpha=0.5)

# Put some text in at the top
header_font = font_manager.FontProperties(
    family="Noto Serif", weight="bold", style="italic"
)
header = (
    "On 20 May 1515, an Indian rhinoceros named Ulysses arrived in Lisbon"
    + " from the Far East - the first rhinoceros seen in Europe since Roman times."
    + " It was a gift to to King Manuel I of Portugal"
    + " from Afonso de \n Albuquerque, governor of Portuguese India. The king regifted"
    + " it to Pope Leo X, but it died in a shipwreck off the coast of Italy."
    + " Although the artist Albrecht Dürer never saw the animal, he created a"
    + " famous\nwoodcut of it, based on a sketch and description."
    + " Dürer's genius was such that his image, while not anatomically accurate, is"
    + " better than reality - it shows what a rhinoceros ought to look like. It was"
    + " the\nstandard representation of the animal for centuries, and remains iconic"
    + " today. In 2003 John Hunter released matplotlib, a python library for"
    + " making 2d plots, which has since become an invaluable tool\nfor scientific"
    + " visualization. The scope and power of matplotib make it the perfect choice"
    + " for reimagining a classic image."
)
axb.text(
    x=0.02,
    y=0.98,
    s=header,
    ha="left",
    va="top",
    fontsize=20,
    fontproperties=header_font,
)

# Add a grid of axes
gspec = matplotlib.gridspec.GridSpec(
    ncols=4,
    nrows=5,
    figure=fig,
    width_ratios=[
        1.5,
        1.5,
        1.5,
        1.5,
    ],
    height_ratios=[1, 1, 1, 1, 1],
    wspace=0.1,
    hspace=0.1,
)
# Set the space the subplots take up
fig.subplots_adjust(left=0.02, right=0.99, bottom=0.02, top=0.9)


# Each subplot has a separate function to draw it

# Top Left
from pTL import pTL

ax_TL = pTL(fig, gspec[0, 0])

# Top Centre Left
from pTCL import pTCL

ax_TCL = pTCL(fig, gspec[0, 1])

# Top Right and Centre Right
from pTCR_TR import pTCR_TR

ax_TCR_TR = pTCR_TR(fig, gspec[0, 2:4])

# 2nd Left
from p2L import p2L

ax_2L = p2L(fig, gspec[1, 0])

# 2nd and 3rd Centre Left
from p2CL_3CL import p2CL_3CL

ax_2CL_3CL = p2CL_3CL(fig, gspec[1:3, 1])

# 2nd Centre Right
from p2CR import p2CR

ax_2CR = p2CR(fig, gspec[1, 2])

# 2nd Right
from p2R import p2R

ax_2R = p2R(fig, gspec[1, 3], bg_im, bgi_extent)

# 3rd Left
from p3L import p3L

ax_3L = p3L(fig, gspec[2, 0])

# 3rd Centre Right
from p3CR import p3CR

ax_3CR = p3CR(fig, gspec[2, 2])

# 3rd and 4th Right
from p3R_4R import p3R_4R

ax_3R_4R = p3R_4R(fig, gspec[2:4, 3], bg_im, bgi_extent)

# 4th and 5th Left
from p4L_5L import p4L_5L

ax_4L_5L = p4L_5L(fig, gspec[3:5, 0])

# 4th Centre Left
from p4CL import p4CL

ax_4CL = p4CL(fig, gspec[3, 1])

# 4th and 5th Centre Right
from p4CR_5CR import p4CR_5CR

ax_4CR_5CR = p4CR_5CR(fig, gspec[3:5, 2])

# 5th Centre Left
from p5CL import p5CL

ax_5CL = p5CL(fig, gspec[4, 1])

# 5th Right
from p5R import p5R

ax_5R = p5R(fig, gspec[4, 3])

# Render the new image
fig.savefig("modern_rhinoceros.webp")

Each subplot has its own drawing function:

Top Left panel

Top Centre-Left panel

Top Centre-Right & Right panel

2nd Left panel

2nd and 3rd Centre-Left panel

2nd Centre-Right panel

2nd Right panel

3rd Left panel

3rd Centre-Right panel

3rd & 4th Right panel

4th & 5th Left panel

4th Centre-Left panel

4th & 5th Centre-Right panel

5th Centre-Left panel

5th Right panel

Or as a list:

And a few utility functions and definitions are split off into their own file