"""Bioprinter: print images with colored bacteria and yeast.
This implements the `bioprint` function, which takes an image and writes a CSV
file that the Labcyte Echo dispenser can use to print the pictures on a plate
using yeast, coli, ...
Written by Zulko at the Edinburgh Genome Foundry.
Original idea and Matlab code by Mike Shen:
https://github.com/mshen5/BioPointillism
"""
from collections import Counter
import csv
import numpy as np
from skimage.io import imread, imsave
from skimage.transform import resize
from skimage.color import rgb2lab
def _rownumber_to_rowname(num):
"""Return the row name corresponding to the row number.
For instance 0-> A, 1->B, 2->C, ... 26->AA, 27->AB, ... etc.
"""
if num < 26:
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[num]
else:
return "".join([_rownumber_to_rowname(num / 26 - 1),
_rownumber_to_rowname(num % 26)])
[docs]def bioprint(image_filename, output_filename, bg_color, pigments_wells,
resolution=(192, 128), transfer_volume=2.5,
pigment_well_capacity=25000, transfer_rate=150,
quantified_image_filename=None):
"""Generate a CSV that can be used in the Echo to print the given picture.
Example
=======
# Let's print the EGF logo !
bioprint(
img_filename = "egf_logo.jpeg",
output_filename = "egf_logo.csv",
bg_color=[255,255,255], # The background of the image is white
pigments_wells= { "A1": [0,0,0], # black
"A2": [250,120, 10] } # orange
)
Parameters
==========
image_filename
The path the image file to be printed. Can be virtually any size
and file format, but make sure the image is well adapted to low
resolution yeast printing. If the picture is higher than wide it will
be automatically rotated 90 degrees so as to maximize its resolution on
the plate (the image aspect ratio is conserved).
output_filename
The name of the CSV file written by the function, that will then
be fed to the Echo.
bg_color
A triplet (R,G,B) of 0-255 integers indicating which color
of the original image represents the background (no pigment)
pigments_wells
A dictionary of well names and the corresponding pigments. For
instance {"A1": [0,10,20], "B1":...}. Only one well per pigment
is currently supported.
resolution
Resolution (width, height) of the printing plate. You must define a
plate with these exact same characteristics using the Echo
software. Default is (192, 128) (twice the resolution of a
1536-well plate). The aspect ratio of the original image is always
automatically conserved.
transfer_volume
How many microliters of liquid should be used for each pixel.
The default, 2.5, works very well and is also the lowest possible
value on our Echo.
pigment_well_capacity
Volume in microliters that one pigments well can dispense. The
function raises an error if the total number of pixels for one
color exceeds the content of one pigment well, i.e. if
transfer_volume * number_pixels > well_capacity
transfer_rate
Average number of droplet transfers per second, used only to
give an estimate of the time required for printing.
quantified_image_filename
If a path is provided, the quantified picture will be saved as an
image file under this name.
"""
pigments_wells, pigments_colors = zip(*pigments_wells.items())
# Constants of the problem
colors = np.vstack([np.array(bg_color),
np.array(pigments_colors)]).astype(float)/255
resolution_w, resolution_h = resolution
resolution_ratio = 1.0 * resolution_w / resolution_h
image = imread(image_filename)[:, :, :3]
height, width, _ = image.shape
# IF THE PICTURE IS HIGHER THAN WIDE, CHANGE THE ORIENTATION
if height > width:
image = image.swapaxes(1, 0)[::-1]
height, width = width, height
# RESIZE THE PICTURE TO THE PROVIDED RESOLUTION (KEEP THE ASPECT RATIO)
image_ratio = 1.0 * width / height
if (height > resolution_h) or (width > resolution_w):
if image_ratio > resolution_ratio:
new_size = (int(resolution_w / image_ratio), resolution_w)
image = resize(image, new_size)
else:
new_size = (resolution_h, int(resolution_h * image_ratio))
image = resize(image, new_size)
# QUANTIFY THE ORIGINAL IMAGE WITH THE PROVIDED PIGMENTS COLORS
# First convert the image and pigments colors the LAB color space
image_lab = rgb2lab(image)
colors_shape = (1, len(colors), 3)
colors_lab = rgb2lab(colors.reshape(colors_shape))[0]
image_color_distances = np.dstack([
((image_lab - color_lab.reshape((1, 1, 3)))**2).sum(axis=2)
for color_lab in colors_lab
])
# now image_color_distances[x,y,i] represents the distance between color
# i and the color of the image pixel at [x,y].
image_quantnumbers = image_color_distances.argmin(axis=2)
# CHECK THAT WE WILL HAVE ENOUGH COLORANT
max_pixels_per_color = pigment_well_capacity / transfer_volume
counter = Counter(image_quantnumbers.flatten())
for color, count in counter.items():
if (color != 0) and (count > max_pixels_per_color):
err_message = ("Too much pixels of color #%d. " % (color) +
"Counted %d, max authorized %d" % (
count, max_pixels_per_color))
raise ValueError(err_message)
# WRITE THE CSV
# TO DO: write the wells in an order that miminizes the Echo's travels.
with open(output_filename, 'wb') as csvfile:
writer = csv.writer(csvfile, delimiter=',')
writer.writerow(['Source Well',
'Destination Well',
'Transfer Volume'])
for i, row in enumerate(image_quantnumbers):
for j, color in enumerate(row):
if color != 0:
writer.writerow([
pigments_wells[color-1], # source well
_rownumber_to_rowname(i) + str(j+1), # target "well"
transfer_volume])
# ESTIMATE THE PRINTING TIME
total_pixels = sum([count for (color, count) in counter.items()
if color > 0])
print("%d pixels will be printed in appr. %.1f minutes" % (
total_pixels, total_pixels / transfer_rate))
# SAVE THE QUANTIFIED VERSION OF THE IMAGE IF A FILENAME IS PROVIDED
if quantified_image_filename is not None:
image_quantified = np.array([colors[y] for y in image_quantnumbers])
imsave(quantified_image_filename, image_quantified)