This is a Python script that uses OpenCV and Jinja2 to turn photos into coloring book pages. The result is a printout page that one can print and color!
This Python script currently has 2 algorithms for generating coloring printouts:
- "Downscale" - This algorithm shrinks the image to a really low resolution. it then draws a numbered grid on a page that describes the brightness of each pixel.
- "Shapes" - This algorithm overlaps a bunch of circles and regular polygons on a page. Each shape is assigned a brightness value after sampling pixels from the input image.
Output is always in PostScript format. These files can be converted to
PDF format by programs such as GhostScript's ps2pdf
if needed.
This is just a fun little Python project I made to turn photos into coloring pages. The idea for this project came from a coloring book series, Querkles by Thomas Pavitte. Be sure to check out his coloring books if you like this project!
This is also an exercise for me to learn about Docker. This script is designed to run in a Docker container. Two directories are mounted so the images and output are stored on the host, not in the container.
This is also an exercise in learning the PostScript programming language. It's a language used by printers to lay out a page of text and vector graphics.
This assumes you already have Docker installed.
- Clone this Git repo.
- Run
launch.sh
. This script does the following:- Pull the latest Docker container for
color-by-numbers
from Docker Hub - Mount the
input/
andoutput/
directories of this Git repo. - Start Bash within the container. Now we are ready to process some images!
- Pull the latest Docker container for
- (Optional) Put an image into the
input/
directory on the host machine. You can ignore this step if you want to use one of the sample images provided in the repo. - Run the python script inside the Docker container. Some examples:
There are many options to
./main.py downscale input/gears.jpg output/gears_downscale.ps ./main.py shapes input/gears.jpg output/gears_shapes.ps
main.py
, use the-h
flag to learn more! - (Optional) Convert the PostScript output to a PDF file. GhostScript is
already installed in the container, so simply run a command like this
inside the Docker container:
# from /app inside the container ps2pdf output/gears_downscale.ps output/gears_downscale.pdf # it's a lot simpler if we enter the output folder: cd output # this creates gears_downscale.pdf automatically ps2pdf gears_downscale.ps
This script is really designed for use inside Docker. However, if you want to run the Python code from the repo directly, follow these instructions.
Note: I have not tested this myself.
- Install Python modules with the usual
pip install -r requirements.txt
- If you want to be able to convert PostScript -> PDFs, install GhostScript
- Input images MUST go in the
input/
directory of this repo. The Python script assumes that there will be directories calledinput/
andoutput/
in the same directory asmain.py
. In the Docker container, those two two directories are created by mounting 2 directories on the host machine. - The output directory MUST be the
output/
directory of this repo for the same reasons.
Here are the coloring rules:
- There are
--num-colors
colors. Let's number them with integers from[0, N)
. - 0 is the darkest color,
N-1
is the brightest color - The artist decides a gradient of any N colors. this could be varying shades of a single color, or many different colors. The only important choice is that the gradient must go from dark to light.
- Start coloring!
This method of coloring follows the rules of Querkles by Thomas Pavitte with some slight modifications:
- My script lets you pick how many colors to use. Querkles always uses 5 colors plus the white background color
- My coloring pages do not use a background color. If you want part of the
image to be white, don't color in the brightest color (
N - 1
) - As a programmer, I started the numbering at 0 instead of 1.
When I designed the Shapes algorithm, I wanted to have a similar nummbering system to Downscale. However, I realized that it would be impossible to read the numbers with so many overlapping shapes. Therefore, I came up with these modified rules:
- Shapes with black outlines are always the darkest color
- Shapes with red outlines are always the second darkest color
- As you go through the color wheel from red -> green -> blue, they correspond to brightness values. These colors are evenly spaced on the color wheel.
For example, for --num-colors 6
(the default), you get shapes with the
following outline color:
Outline Color | Brightness |
---|---|
Black | 0 - Darkest color in gradient |
Red | 1 |
Yellow-green | 2 |
Green | 3 |
Blue | 4 |
Red-violet | 5 - Brightest color in gradient |
This algorithm does the following:
- Read the command line arguments. (see
./main.py downscale --help
) - Read in the input image and convert it to grayscale.
- We want to subdivide the image into a grid of squares. Using the image size,
the page size (
--page-size
), the margin size (--margin
), and the desired size per square(--square-size
), calculate how many pixels wide each grid square is on the input image. - Shrink the image, converting squares of the calculated size into single pixels. The average color is taken for each square.
- Bucket the colors of this downsampled image into the number specified
by the user (
--num-colors
). - Scale down these values from
[0, 255]
to[0, num_colors)
. - Use Jinja2 to template a PostScript file that draws a grid with a number per cell. These numbers correspond to the numbers we assigned in the previous step.
- Write the PostScript file to the output directory.
This algorithm does the following:
- Read the command line arguments
- Read in the image and convert it to grayscale
- Given the page size (
--page-size
), margin size (--margin
) and the size of the image, calculate how many points of the output file are needed per pixel of the input image. - Now let's start covering the page with shapes! Repeat the following for
every iteration from 0 to
--iterations
:- Compute the diameter of the circles for this iteration. Start with about half the print area's (page - margins) shorter side. Then do 1/4, 1/8, etc., halving the diameter at each iteration.
- Calculate how many shapes we need to approximately cover the print
area. I use the formula
img.rows * img.cols / circle_diameter ** 2
I'm using the bounding box rather than the circle itself, it's close enough. - Randomly pick the shape types. Circles and regular polygons from 3-8 sides are all equally likely. Note that even the polygons will be colored from a circular region of the input image. The result of this is an array of PostScript commands for the different shapes.
- Calculate the colors for each shape. This involves the following:
- Randomly select a slice out of the image of size
circle_diameter x circle_diameter
. Keep track of the positions of these slices in the image. - Compute the average color using a circularly-shaped kernel of the same size as the circle.
- Quantize the colors so there are only
--num-colors
values - Assign each value a color.
0
is always assigned black.1
is always assigned red. all the other--num-colors - 2
colors have evenly spaced hues around the color wheel
- Randomly select a slice out of the image of size
- Calculate the centers and radii of the circles in points so we know where and how big the shapes are in PostScript
- Generate lines of PostScript code that look like this:
hue saturation brightness x y r shape_command
- Finally, gather up the lines of PostScript code and use Jinja2 to insert them into a PostScript template.
Examples:
(Note: DeviantArt doesn't show the preview image on the page. See here for pictures of my colored printouts)