Skip to content

GravO8/halftone-lines

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Halftone Lines

A python script that levarages the open cv and PIL libraries to generate halftone images, with lines.

(If you like this repo, you may also like this one)

Command line usage

Put the source image in the same directory as halftone_lines_cmd.py and execute the script from the command line, like so:

python3 halftone_lines_cmd.py landscape.jpg

example

You can also learn about the optional arguments, doing so:

python3 halftone_lines_cmd.py -h

Warning: The script takes a long time to execute for large images. Consider resizing the input image so that neither the height nor the width is larger than 1000 pixels.

Optional arguments

Kernel Size

As described in the algorithm section bellow, the image is scanned with a (squared) kernel in a sliding window fashion. You can control the size of this kernel with the kernel optional argument.

kernel

Larger kernels scan larger chunks of the image at a time, resulting in courser output images. Using smaller kernels results in smoother images but takes longer and produces larger outputs.

Side

The side optional argument controls the size of the lines in the output image.

side

Larger side values produce smoother but also larger output images. Smaller side values generate smaller images with interesting textures.

Alpha

When alpha = 1, the maximum width of each line is the side value. The alpha argument can be used to decrease or increase this maximum width using values less than or larger than 1, respectively.

alpha

Angle

The angle optional parameter controls the angle the image is scanned, which is then reflected on the output image like so:

angle

You can specify any angle you want, in degrees. The angle is pushed into the [0, 180[ range (which includes all possible rotation variations) before being applied to the image.

Color

The bg_color and fg_color optional arguments set the background and foreground colors of the output image, respectively.

color

Darker colors are recommended for the foreground color. If you are calling the program using the command line interface, you have to use "" or '' to delimit the rgb value, like so:

python3 halftone_lines_cmd.py landscape.jpg -fg "(255,0,0)"

Algorithm

The algorithm is composed of two parts: the scanning and the drawing. In the scanning part, a sliding window kernel computes the mean grey level intensity of every image region it visits. Then, in the drawing part, lines whose thickness varies depending on the intensities previously measured, are drawn.

1. The Kernel and the Quadrants

The algorithm scans the input image in its four quadrants. This is done by finding the center of the image (variable self.center) and placing four squared kernels there, like so:

   +-------+      +-------+      +-------+
   |       |      |       |      | +-+-+ |    
   |       |      |   o   |      | +-o-+ |        
   |       |      |       |      | +-+-+ |        
   +-------+      +-------+      +-------+
   input img      input img   the four squares   
                  center      around the center

These kernels are defined by the array vertices in the quadrant method of the Scan class. They are constructed from the base kernel defined by the vertices (0,0), (0,self.kernel_s), (self.kernel_s,self.kernel_s) and (self.kernel_s,0), where kernel_s stands for "kernel side".

The arguments h_sign and v_sign (horizontal and vertical sign, respectively) are used to diferentiate four kernels (and quadrants):

(h_sign = +1) and (v_sign = +1) is the fourth quadrant (bottom right)
(h_sign = -1) and (v_sign = +1) is the second quadrant (bottom left)
(h_sign = -1) and (v_sign = -1) is the second quadrant (top left)
(h_sign = +1) and (v_sign = -1) is the first quadrant (top right)

Note: This may seen unintuitive when compared to the mathematical description of the four quadrants. However, we have to consider that unlike in the mathematical coordinate system, where the point (0,0) is the center of the coordinate space, the point (0,0) in a 2D matrix (an image) is the upper left corner. Additionally, increasing y coordinates move down along the matrix, not up.

The kernel's vertices are then rotated by angle degrees using the rotation matrix generated by the rotation_matrix function and moved to the center of the image by adding the center coordinates:

vertices_r = self.r.dot(vertices).T + self.center

2. Sliding Window

Ok, the kernels are in place. How can they be moved to scan the image? This is done with translation vectors, the move_h_r and move_v_r variables, that move the kernels horizontally and vertically, respectively and are defined like so:

move_h_r = self.r.dot( np.array([self.kernel_s,0]) ) # move horizontally rotated
move_v_r = self.r.dot( np.array([0,self.kernel_s]) ) # move vertically rotated

They too are rotated by angle degrees to match the orientation of the kernels.

The current position of the kernel is updated like so:

current = vertices_r + x*h_sign*move_h_r + y*v_sign*move_v_r

Where x and y are integer values that range from 0 to an artibitrarly large number. This means the kernel can move to places outside the original image. Indeed, when the kernel reaches a position outside the image, its sliding movement is stopped.

The image below has a schematic representation of how the sliding window works. Colored squares represent places the kernel visits in the third quadrant. Green squares represent squares where the kernel is used to compute the underlying grey level intensity mean. As long as the kernel overlaps with the image with at least one pixel, its mean will be considered. Red squares are the last position visited by the scanning algorithm in each row. As depicted by the numbers, which correspond to the x values of that particular row, the kernel in this quadrant moves from left to right. In this case, red squares are squares outside the image and where continuing moving left won't ever reach the image.The yellow squares are also outside the image, but on the other hand, continuing moving left from their position can reach the image. The x iteration stops in the red squares. The y iretation stops in the last red square, corresponding to the last row where no pixel in the image is scan because the row is completely outside the image bounding box.

sliding window corner

Note: The algorithm can be optimized by directly updating the StraightLine objects instead of constructing new objects everytime the kernel is moved.

3. Drawing

While scanning, the position of the different image regions visited and their respective mean grey level intensity are stored in a dictionary, indexed by the region's y position (variable self.canvas_r). The output image is created by iterating this dictionary as each of its entries corresponds to a line. The lines are created with the SigmoidPolygon class that, as the name suggests, creates the polygons that are then actually drawn. The points in this line are added with the height method that specifies the thickness (or height) the line should have at a particular position. The smooth thickness variation is implemented with a sigmoid function (see the make_sigmoid method). As the lines are drawn tilted (following the kernel's orientation), they must be rotated to match the original orientation by reverting the angle degrees rotation and finally, they are translatated to the center of the image.

Original image credits

The photo used in this README is called Landscape with Mountains and Small Pond and you can see the original version here. Check out its original photographer here.

License

This program is licensed under the MIT License - see the LICENSE file for details.

Releases

No releases published

Packages

No packages published

Languages