Exploring Inversions using Python
Karim K. Kardous

Initial Setup

I start by configuring the Python environment using reticulate, specifying a custom virtual environment and ensuring required packages (matplotlib and pillow) are installed. This setup step ensures Python and R work seamlessly together; granted ‘seamlessly’ might be over-reaching, but at least for this case, fairly commonly used modules, it was the case.

Show the code
library(reticulate)
invisible(
  capture.output({
    Sys.setenv(RETICULATE_PYTHON = "py_venv/bin/python3.9")
    reticulate::use_python("py_venv/bin/python3.9", required = TRUE)
    })
)

invisible(
  capture.output({
  use_python("py_venv/bin/python3.9", required = TRUE)
  py_config()
  py_install(c('matplotlib', 'pillow'), envname = "py_venv", pip = TRUE)
  })
)

With this chunk executed, R is now ready to run Python code from the designated virtual environment with the necessary libraries installed.

Image Configuration and Setup using matplotlib & Pillow

To overcome issues with displaying Pillow images directly in Quarto, I used matplotlib.pyplot to render and show images in the document. This section defines utility functions for drawing images programmatically, including a basic black board, a chessboard, and an inversion-based pattern. Otherwise, you’d likely have to set figure parameters such as dimensions, axes, etc. for every image input, so I create the below function to reduce/eliminate this redundancy.

Show the code
from PIL import Image 
import matplotlib.pyplot as plt
import math

# build a function to ease the plotting, reusable boilerplate basically with option to add additional arguments
def draw_figure(width=512, height=512, draw_func=None, title='Custom Board', figsize=(4, 4), background='white', **kwargs):
    
    board = Image.new('RGB', (width, height), background) 
    # apply function if provided
    if draw_func:
        draw_func(board, width, height, **kwargs)
    
    plt.figure(figsize=figsize)
    plt.imshow(board)
    plt.axis('off')
    plt.title(title)
    plt.show()
    
  
def solid_unicolor_board(board, width, height):
  
  for x in range(width):
    for y in range(height):
      board.putpixel( (x, y), (0, 0, 0))

# define chess board pattern
def chess_pattern(board, width, height, square = 64, **kwargs):
  
  for x in range(width):
    for y in range(height):
      cx = x // square # or you can use math.floor(x / square) if index doesn't jstart at 0 
      cy = y // square
      color = ((cx + cy) % 2) * 255
      board.putpixel((x, y), (color, color, color)) # black and white
      
# define an inversion 
def inversion_pattern(board, width, height, R = 256, square = 64, **kwargs):

  for x in range(width):
    for y in range(height):
      x0 = x - width / 2 + 0.5  # define a center for x and one for y with offsets by half a pixel as technically the center is at .5, .5
      y0 = y - height / 2 + 0.5
      f = R**2 / (x0**2 + y0**2) # inverse factor or the radius squared divided by distance to center; addition to avoid zero division
      x1 = x0 * f
      y1 = y0 * f
      cx = math.floor(x1 / square)
      cy = math.floor(y1 / square)
      color = ((cx + cy) % 2) * 255
      # board.putpixel((x, y), (color, color, 0)) # black and yellow  
      board.putpixel((x, y), (color, color, color)) # black and white  

# outputting results/images
draw_figure(
  draw_func = solid_unicolor_board,
  title = 'Solid Black Board'
  )

Show the code
draw_figure(
  draw_func = chess_pattern, 
  title = 'Chess Board'
  )

Show the code
  
draw_figure(
  draw_func = inversion_pattern, 
  square = 256, 
  title = 'Initial inversion Board'
  )

The closer you get to the center, the more stretched and squished things become visually (that’s if you really zoom it). This also explains why some areas look pixelated - the image doesn’t have enough detail at this size to show everything smoothly so next resolution is improved.

Improving Output Resolution

To reduce the pixelation, especially towards the center of the image, I increase both the image dimensions and the square size; essentially increase resolution.

Show the code
draw_figure(
  draw_func = inversion_pattern, 
  width = 512 * 2, 
  height = 512 * 2, 
  square = 256 * 2, 
  R = 256 * 2, 
  title = 'Improved Resolution (x2)'
  )

Doubling the dimensions already leads to significant visual improvement. The lift in clarity is again specially visible towards the center/origin where distortion is most intense/visible.

Improving Output Resolution (by a factor of 10)

Increasing resolution tenfold didn’t slow down render time that much so I go for it and it does look like we win in the trade.

Show the code
draw_figure(
  draw_func = inversion_pattern, 
  width = 512 * 10, 
  height = 512 * 10, 
  square = 256 * 10, 
  R = 256 * 10, 
  title = 'Final Inversion - Improved Resolution (x10)'
  )

Overall, I thought it was interesting, aesthetically if nothing else, to see how with a few lines of code, fairly intricate and cool-looking images get created. Thanks for reading !