Cyanotype/VDB/Salt Paper Prints – Making Contact Negatives with Python

Be a Python titan! I’d like to put together some scripting idea for photography!

Last summer here in Graz was rather mixed. But to be honest I kind of like it unfortunately this summer is super hot at 35ºC+. While in Ireland all  seasons are covered in one day or even within 20 minutes! That’s Irish weather for you, very much like Python script you can make things in 20 minutes in all kind  directions.

Let’s begin with Python, why I’m I writing or talking about Python. It’s been around for such a long time. Certainly with the advent of the Raspberry PI, a mini computer which made Python more prevalent. One thing you can say about this scripting language, there’s most likely a module to do pretty much anything you can think of. This leads me onto photography and onto my quest to automate some of my workflows and crazy hair brained ideas. I bet you, there’s something in here that will also inspire you, so let’s get started…

What kind of things you can do with Python ?

If you’d like to contribute to this page, please comment down below.

If you just wish to have the script for creating contact print negatives for Cyanotypes/VDB/Salt paper then jump down the page.

First off ideas…

Imagine taking an image with a digital camera, the image is processed for Cyanotype and printed on a transparency sheet for contact printing a Salt Paper or Cyanotype. However all this is portable using an Raspberry Pi and a portable printer running on battery. Then using the UV box that I designed previously for printing we’d have a complete mobile studio to setup anywhere we like, sounds crazy ?  I think all the pieces to make this happen can be found on this page. But it’s just an idea!

Steps to do this idea…

 

Well are a few of mine…

  • Automating: Cyanotype / VDB negatives from colour images
  • Automating: Film scans when using DSLR
  • Automating: Adding EXIF/GPS data for images and negative scans
  • Preparing your scanned images TIFFs for Instagram / Twitter with a nice border.

 

First we’ll need  a sample image to show the results for the code examples below

Scanned Film Image for Kodak Colorplus 200
Scanned Film Image for Kodak Colorplus 200

Getting Started with Jupyter Notebook

Firstly I’m going to start with a really super tool called Jupyter Notebook  this will help you to play and try out things super easy.

You’ll need an installation of Python and Pip. First go to your command line in Windows/MacOs/Linux to see what’s already installed.

python -V
Should return something like : python 3.8.5 or higher!

MacOs Tip : If you’re trying to use Python on a MacOs. Please follow this guide which solutions quite a lot of issues with the default Python.

If you’ve no Python install or an older version please install Python and proceed next task installing Jupyter Notebook.

pip install jupyterlab pillowNow Start the notebook

jupyter lab

Source: Edit images with Jupyter and Python

Now it’s time start coding!

Resizing and Exporting Images

So for example how about resizing and exporting images in Python script. That can’t be too hard for starter ?

x, y = pic.size
x //= 5
y //= 5
smaller = image.resize((x, y))

PILLOW

Python Imaging Library (abbreviated as PIL) (in newer versions known as Pillow) is a free and open-source additional library for the Python programming language that adds support for opening, manipulating, and saving many different image file formats. It is available for Windows, Mac OS X and Linux.

Pillow offers several standard procedures for image manipulation. These include:

  • per-pixel manipulations,
  • masking and transparency handling,
  • image filtering, such as blurring, contouring, smoothing, or edge finding,
  • image enhancing, such as sharpening, adjusting brightness, contrast or color,
  • adding text to images and much more.
  • also GPS/EXIF information

Source: https://en.wikipedia.org/wiki/Python_Imaging_Library
Library
: https://pillow.readthedocs.io/en/stable/

pip install Pillow

Example: TIFF to JPEG  for exporting scanned images for Instagram or the web

im = Image.open('sample.tiff')
print "Generating jpeg for %s" % name
im.thumbnail(im.size)

im.save('sample.jpeg', "JPEG", quality=100)

Add borders to an image

from PIL import Image, ImageOps
img = Image.open('original-image.png')
img_with_border = ImageOps.expand(img,border=300,fill='black')
img_with_border.save('imaged-with-border.png')

Add text to an image

from PIL import Image, ImageFont, ImageDraw 
img = Image.open("image.jpg")
title_font = ImageFont.truetype('playfair/playfair-font.ttf', 200)
title_text = "Caption for the image"
image_editable = ImageDraw.Draw(img)
image_editable.text((15,15), title_text, (237, 230, 211), font=title_font)

Source

B&W Conversion & Sepia

As Cyanotypes and VDB are effectively monotone processes it goes without saying we’ll need to convert to B&W.

Also with the Pillow module, a simple bit of code will do a simple B&W conversion

image.convert('L') # convert image to

Check out https://www.blog.pythonlibrary.org/2017/10/11/convert-a-photo-to-black-and-white-in-python/

What need is the possibilities to use Red, Yellow, Green and Blue filters but more on that later. Let’s start with a Sepia Example

Sepia Example

To print our negative

def make_linear_ramp(white):
ramp = []
r, g, b = white
for i in range(255):
ramp.extend((int(r*i/255),int(g*i/255),int(b*i/255)))
return ramp
sepia = make_linear_ramp((255, 240, 192))
sepiaImage = pic.convert("L")
sepiaImage.putpalette(sepia)
sepiaImage = sepiaImage.convert("RGB")
sepiaImage.save("sepia.jpg")

Converting an image to a negative and vice versa

Again with using the Pillow library to do this rather simple operation. This should cover both negative to positive and the other way around.

ImageOps.invert(image)

Creating Tinted Images for Cyanotypes Negatives

To further improve negatives for contact printing for Salt Paper and Cyanotypes, a filter can be applied to the B&W negative. See my post for more information

Tint Image with Python
Tint Image with Python

from PIL import Image
from PIL.ImageColor import getcolor, getrgb
from PIL.ImageOps import grayscale

def image_tint(src, tint='#ffffff'):
    #if Image.isStringType(src):  # file path?
    src = Image.open(src)
    if src.mode not in ['RGB', 'RGBA']:
        raise TypeError('Unsupported source image mode: {}'.format(src.mode))
    src.load()

    tr, tg, tb = getrgb(tint)
    tl = getcolor(tint, "L")  # tint color's overall luminosity
    if not tl: tl = 1  # avoid division by zero
    tl = float(tl)  # compute luminosity preserving tint factors
    sr, sg, sb = map(lambda tv: tv/tl, (tr, tg, tb))  # per component
                                                      # adjustments
    # create look-up tables to map luminosity to adjusted tint
    # (using floating-point math only to compute table)
    luts = (tuple(map(lambda lr: int(lr*sr + 0.5), range(256))) +
            tuple(map(lambda lg: int(lg*sg + 0.5), range(256))) +
            tuple(map(lambda lb: int(lb*sb + 0.5), range(256))))
    l = grayscale(src)  # 8-bit luminosity version of whole image
    if Image.getmodebands(src.mode) < 4:
        merge_args = (src.mode, (l, l, l))  # for RGB verion of grayscale
    else:  # include copy of src image's alpha layer
        a = Image.new("L", src.size)
        a.putdata(src.getdata(3))
        merge_args = (src.mode, (l, l, l, a))  # for RGBA verion of grayscale
        luts += tuple(range(256))  # for 1:1 mapping of copied alpha values

    return Image.merge(*merge_args).point(luts)

if __name__ == '__main__':
    import os
    import sys

    input_image_path = 'Image-12.jpg'
    print('tinting "{}"'.format(input_image_path))

    root, ext = os.path.splitext(input_image_path)
    suffix = '_result_py{}'.format(sys.version_info[0])
    result_image_path = root+suffix+ext

    print('creating "{}"'.format(result_image_path))
    result = image_tint(input_image_path, '#FFBF73')
    if os.path.exists(result_image_path):  # delete any previous result file
        os.remove(result_image_path)
    result.save(result_image_path)  # file name's extension determines format

    print('done')
  Souce:Stack Overflow

 

Orignal image from film scan Kodak Colorplus 200
Orignal image from film scan Kodak Colorplus 200

Let’s put altogether to make our Cyanotype negative

Putting a script together to create a preview which gives you an idea how a Cyanotype print might actually look like while the script produces the tinted negative for printing. It’s a kind of all-in-one script which gives three posssible outputs, less, more and no contrast.

 

from PIL import Image,  ImageEnhance, ImageOps
from PIL.ImageColor import getcolor, getrgb
from PIL.ImageOps import grayscale

def image_tint(src, tint='#ffffff'):
    #if Image.isStringType(src):  # file path?
    src = Image.open(src)
    if src.mode not in ['RGB', 'RGBA']:
        raise TypeError('Unsupported source image mode: {}'.format(src.mode))
    src.load()
    
    # Make Negative
    negative=ImageOps.invert(src)

    tr, tg, tb = getrgb(tint)
    tl = getcolor(tint, "L")  # tint color's overall luminosity
    if not tl: tl = 1  # avoid division by zero
    tl = float(tl)  # compute luminosity preserving tint factors
    sr, sg, sb = map(lambda tv: tv/tl, (tr, tg, tb))  # per component
                                                      # adjustments
    # create look-up tables to map luminosity to adjusted tint
    # (using floating-point math only to compute table)
    luts = (tuple(map(lambda lr: int(lr*sr + 0.5), range(256))) +
            tuple(map(lambda lg: int(lg*sg + 0.5), range(256))) +
            tuple(map(lambda lb: int(lb*sb + 0.5), range(256))))
    l = grayscale(negative)  # 8-bit luminosity version of whole image
    if Image.getmodebands(negative.mode) < 4:
        merge_args = (negative.mode, (l, l, l))  # for RGB verion of grayscale
    else:  # include copy of src image's alpha layer
        a = Image.new("L", src.size)
        a.putdata(negative.getdata(3))
        merge_args = (negative.mode, (l, l, l, a))  # for RGBA verion of grayscale
        luts += tuple(range(256))  # for 1:1 mapping of copied alpha values

    return Image.merge(*merge_args).point(luts)

if __name__ == '__main__':
    input_image_path = 'Image-12.jpg'
 
    # Tint Image
    result = image_tint(input_image_path, '#FFBF73')

    # Contrast
    enhancer = ImageEnhance.Contrast(result)
    factor = 1 #gives original image
    im_output = enhancer.enhance(factor)
    im_output.save('negative-image.jpg')
    ImageOps.invert(im_output).save('positive.jpg')

    factor = 1.5 #increase contrast
    im_output = enhancer.enhance(factor)
    im_output.save('more-contrast-negative.jpg')
    ImageOps.invert(im_output).save('more-contrast-positive.jpg')


    factor = 0.5 #decrease constrast
    im_output = enhancer.enhance(factor)
    im_output.save('less-contrast-negative.jpg')
    ImageOps.invert(im_output).save('less-contrast-positive.jpg')

 

Next Level

As this code will run pretty much on any device. Taking a Raspberry PI and DSLR connect them together this would create a kind of automate system for producing contact printing negatives for Cyanotype/VBD/Salt paper.

Using gPhoto and Touchscreen a whole remote printing station could be made along with the UV Box

Probably Circuit Python would be a way to go and using PyCups to do the printing part

 

2 Comments

Leave a Reply