Hand-Drawn Shape Simulation

Boris Kravtsov, PhD
4 min readJul 5, 2022

--

Published on July 5, 2022 | Last Modified on February 11, 2025

Photo by Road Ahead on Unsplash

Shape recognition is a fundamental aspect of human cognition. Even at just 2–3 years old, children can effortlessly recognize objects despite variations in size, position, orientation, or even when the objects are significantly deformed. For instance, they can easily identify heart shapes in both the images above and below.

Fig.1 “Hand-drawn” hearts generated using the hdss Python package

These intentionally distorted forms, known as “hand-drawn shapes,” are invaluable for testing various shape recognition technologies. In the following sections, we’ll demonstrate how to generate these shapes using the hdss software from GitHub, requiring just two simple Python functions.

About the Method

Every shape can be deconstructed into one or more smooth curves. For example, the heart shape below consists of two such curves, highlighted in green and red. Each smooth curve can be defined by a small set of control points along its path, while the rest of the curve is reconstructed through Bézier interpolation. Consequently, a shape can be fully described using one or more text files containing the coordinates of these control points.

Fig.2 Creating a shape from the coordinates of its control points
Fig.3 Plain text files (CSV format) containing control point coordinates

Local Distortion: Any slight alteration to the position of a control point introduces a localized distortion near that point. By randomly adjusting all control points — essentially adding random noise — you can generate numerous variations of the original shape that appear hand-drawn.

Fig.4 Example of local distortion

Global Distortion: Alternatively, you can apply a global distortion by performing a random perspective transformation on all control points simultaneously. After transforming their positions, the shape is reconstructed using Bézier interpolation.

To apply this transformation, define the coordinates of two corresponding quadrilaterals:
1. Src: The positions of four crosses randomly placed within the blue zones.
2. Dst: The corners of the orange square (left image).

Fig.5 Example of global distortion

Tip: While you can apply random local or global distortions independently, combining both methods produces more natural-looking, hand-drawn shapes.

Generating Shapes with Code

"""
https://github.com/boriskravtsov/hand-drawn-shape-simulation
"""

from pathlib import Path

from hdss.src import cfg
from hdss.src.utils import init_directory
from hdss.src.set_params import set_params
from hdss.src.create_shapes import create_shapes
import colors


dir_name = 'data_hearts'
init_directory(dir_name)

set_params(
200, # shape_size = 200 x 200
5, # number_of_shapes
True, # perspective_flag
7, # bezier_noise_param
6) # line_thickness

cfg.flag_fill = False
cfg.fill_color = colors.black
cfg.contour_color = colors.maraschino

shape_name = 'heart'
dir_curves = Path.cwd() / '_CURVES_'
path_curve1 = dir_curves / 'heart_curve1.txt'
path_curve2 = dir_curves / 'heart_curve2.txt'

create_shapes(dir_name, shape_name, path_curve1, path_curve2)

The code provided generates five “hand-drawn” heart shapes at a resolution of 200x200 pixels. These images are saved in a newly created directory named data_hearts, with the following filenames:
heart_0.png, heart_1.png, heart_2.png, heart_3.png, heart_4.png.

The results are shown in Fig. 1 above.

Note: Control point coordinates are expressed as percentages, based on a default shape size of 100x100 pixels. When generating shapes of different sizes, the hdss package automatically recalculates these coordinates.

Additional Examples

Popular geometric shapes, such as stars, can also be created using Bézier splines. The following figures illustrate how control points are used to define these shapes.

Fig.6 Star shapes and their control points

Moreover, the hdss package provides flexible coloring options. Below are examples showcasing various coloring styles applied to the generated shapes.

cfg.flag_fill = True, cfg.fill_color = colors.maraschino, cfg.contour_color = colors.black
cfg.flag_fill = False, cfg.contour_color = colors.magenta
cfg.flag_fill = True, cfg.fill_color = colors.lemon, cfg.contour_color = colors.aqua
cfg.flag_fill = True, cfg.fill_color = colors.teal, cfg.contour_color = colors.teal

--

--

Boris Kravtsov, PhD
Boris Kravtsov, PhD

Written by Boris Kravtsov, PhD

I'm trying to share some of my old thoughts and new perspectives.

No responses yet