Introduction to dmtools

This tutorial will walk through a short introduction to dmtools. The tutorial is divided into sections which each focus on a certain module of dmtools. Note that this documentation is under development.

io (input / output)

The first step in manipulating images programmatically with dmtools is loading in an image. This is done using dmtools.io.read_png(). Similarly, after manipulating the image, you can export it to a PNG file with dmtools.io.write_png(). Here is a short example script with no manipulations. Note that this example script assumes that the script io_ex.py and checks_10.png are in the same directory.

# io_ex.py
import dmtools

image = dmtools.read_png('checks_10.png')
dmtools.write_png(image, 'checks_10_clone.png')
checks_10.png

checks_10.png

checks_10_clone.png

checks_10_clone.png

Both dmtools.io.write_png() and dmtools.io.write_netpbm() write additional metadata to the file automatically. This includes the creation time of the image, the software (“dmtools”) used to create the image, and the source code of the script that created the image. We can also use the dmtools.io.Metadata class to provide additional information. In the example below, we export the checks_10.png file to Netpbm so we can view the metadata in plaintext. The example shows the default metadata and an example of providing custom metadata.

# metadata.py
import dmtools

image = dmtools.read_png('checks_10.png')
dmtools.write_netpbm(image, 1, 'checks_10_default_metadata.pbm')

metadata = dmtools.Metadata(author="Me",
                            title="Checks 10",
                            description="Metadata in dmtools example",
                            copyright="MIT License",
                            software="dmtools",
                            disclaimer="None",
                            warning="None",
                            comment="An insightful comment")

dmtools.write_netpbm(image, 1, 'checks_10_custom_metadata.pbm',
                     metadata=metadata)

checks_10_default_metadata.pbm

P1
10 10
# Creation Time: 12-11-2021 00:04:05
# Software: dmtools
# Source: # metadata.py
# import dmtools
# 
# image = dmtools.read_png('checks_10.png')
# dmtools.write_netpbm(image, 1, 'checks_10_default_metadata.pbm')
# 
# metadata = dmtools.Metadata(author="Me",
#                             title="Checks 10",
#                             description="Metadata in dmtools example",
#                             copyright="MIT License",
#                             software="dmtools",
#                             disclaimer="None",
#                             warning="None",
#                             comment="An insightful comment")
# 
# dmtools.write_netpbm(image, 1, 'checks_10_custom_metadata.pbm',
#                      metadata=metadata)
# 
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1

checks_10_custom_metadata.pbm

P1
10 10
# Title: Checks 10
# Author: Me
# Description: Metadata in dmtools example
# Copyright: MIT License
# Creation Time: 12-11-2021 00:04:05
# Software: dmtools
# Warning: None
# Source: # metadata.py
# import dmtools
# 
# image = dmtools.read_png('checks_10.png')
# dmtools.write_netpbm(image, 1, 'checks_10_default_metadata.pbm')
# 
# metadata = dmtools.Metadata(author="Me",
#                             title="Checks 10",
#                             description="Metadata in dmtools example",
#                             copyright="MIT License",
#                             software="dmtools",
#                             disclaimer="None",
#                             warning="None",
#                             comment="An insightful comment")
# 
# dmtools.write_netpbm(image, 1, 'checks_10_custom_metadata.pbm',
#                      metadata=metadata)
# Comment: An insightful comment
# 
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1
1 0 1 0 1 0 1 0 1 0
0 1 0 1 0 1 0 1 0 1

In addition to writing the source code to an image’s metadata by default, the function dmtools.io.recreate_script_from_png() allows one to recover the script that created a PNG image.

transform

The transform module contains many functions for manipulating images. The full API reference can be found here: dmtools.transform. In this section, we will highlight some of the functionality.

Currently, the transform module is mainly focused on image manipulations related to rescaling an image. Frequently, the first step in image rescaling is blurring the image. This provides a good removal of “noise” from the image. The dmtools.transform.blur() functions does just that. It takes a parameter called sigma which indicates how much to blur the image. Usually, sigma=0.5 is a good default. The example script below reads red_blue_square.png and then blurs the image with two different values of sigma. You can see the resulting images below where the larger sigma results in a blurrier image.

# simple_blur.py
import dmtools
from dmtools import transform

image = dmtools.read_png('red_blue_square.png')
blurred_image = transform.blur(image, sigma=5)
dmtools.write_png(blurred_image, 'red_blue_square_blur_5.png')

blurred_image = transform.blur(image, sigma=10)
dmtools.write_png(blurred_image, 'red_blue_square_blur_10.png')
red_blue_square.png

red_blue_square.png

red_blue_square_blur_5.png

red_blue_square_blur_5.png

red_blue_square_blur_10.png

red_blue_square_blur_10.png

After blurring, we can actually rescale the image. This step is also called “resampling.” This is done with dmtools.transform.rescale(). This takes a parameter k which specifies by what factor to scale the image. Hence, k=2 would double the width and height of the image.

When scaling down an image, we have more than one source pixel for each new pixel and we must decide how to assign a color to that new pixel. Similarly, when scaling up an image, we have many pixels in the new image for which there are no corresponding source pixels. Again, we must decide how to assign these pixels a color based on their proximity to the source pixels. A filter is a combination of a weighting function and support which determine how we choose.

In the dmtools rescale implementation, there are multiple built-in filters. A comprehensive list of them is given in the documentation: dmtools.transform.rescale(). Depending on the use case, one or more of the filters may be applicable. The exciting feature of this implementation is the ability to provide one’s own weighting function and support to define custom filters. The weighting function (blue) and supports (red) of some common filters are given below. The weighting function tells us how much to weight the color of a source pixel as a function of its distance to the new pixel. The support defines the “neighborhood” of pixels. In most cases, that is the furthest a source pixel can be while still contributing some weight.

box_filter.png

Box Filter

triangle_filter.png

Triangle Filter

custom_filter.png

Custom Filter

In the example script below, we load the 10x10 checkerboard image and scale it up using three different filters: “point” or “nearest neighbor”, “triangle”, and a custom filter. You can ignore transform.clip and transform.normalize for now. The resulting images are also shown.

# rescale_ex.py
import dmtools
from dmtools import transform
import numpy as np

image = dmtools.read_png('checks_10.png')
scaled_image = transform.rescale(image, k=10, filter='point')
scaled_image = transform.clip(scaled_image)
dmtools.write_png(scaled_image, 'checks_10_point.png')

scaled_image = transform.rescale(image, k=10, filter='triangle')
scaled_image = transform.clip(scaled_image)
dmtools.write_png(scaled_image, 'checks_10_triangle.png')

def f(x):
    return np.sin(x)

# use a custom weighting function and support
scaled_image = transform.rescale(image, k=10, weighting_function=f, support=5)
scaled_image = transform.normalize(scaled_image)
dmtools.write_png(scaled_image, 'checks_10_custom.png')
checks_10_point.png

checks_10_point.png

checks_10_triangle.png

checks_10_triangle.png

checks_10_custom.png

checks_10_custom.png

You can see how “point” is the best filter for maintaining the pixels of the original image. Here, the “triangle” filter causes the image to be blurred since it is takes the average of surrounding white and black pixels causing the gray space between them. Any reasonable filter will mostly decrease the weight as the distance gets further. Furthermore, they will not have significant negative weights. For that reason, the custom filter used here does all sorts of strange things to the image.

After rescaling the image, we would like to write it to a PNG file. However, the rescaling step results in pixels having non-integer values which can also be outside of the [0, 255] range (especially when using strange filters). The transform module provides three different functions to adjust values back into the [0, 255] range: dmtools.transform.clip(), dmtools.transform.normalize(), dmtools.transform.wraparound(). It is recommended to use .astype(np.uint8) to round. The example script below shows how the choice of which of these you use affects the resulting image.

# clamping_ex.py
import dmtools
from dmtools import transform
import numpy as np

def f(x):
    return np.sin(x)

image = dmtools.read_png('checks_10.png')
scaled_image = transform.rescale(image, k=10, weighting_function=f, support=7)

clip_image = transform.clip(scaled_image)
dmtools.write_png(clip_image, 'checks_10_clip.png')

normalize_image = transform.normalize(scaled_image)
dmtools.write_png(normalize_image, 'checks_10_normalize.png')

wraparound_image = transform.wraparound(scaled_image)
dmtools.write_png(wraparound_image, 'checks_10_wraparound.png')
checks_10_clip.png

checks_10_clip.png

checks_10_normalize.png

checks_10_normalize.png

checks_10_wraparound.png

checks_10_wraparound.png

In checks_10_wraparound.png, we can see harsh contrast between gradients where a gradient progressively gets darker until it switches white. This is arising from dark values above 255 (black) wrapping around to 0 (white) and vice versa. In checks_10_clip.png, these are the darkest and whitest areas in the image since values above or below just get clipped to 0 and 255 respectively. Lastly, checks_10_normalize.png normalizes the minimum and largest value to 0 and 255 causing this image to loose contrast in the center when compared to the clipping algorithm.

Another function provided by the transform module is dmtools.transform.composite(). It allows for layering two images. In the case of fully opaque images, the function is uninteresting as the top image will completely obfuscate the below image. However, when images have lower opacities, this function handles what is called Alpha Compositing. In the example script below, we input two images at half opacity and view the result of overlaying in both possible orientations. Note how the composite function creates the effect of the top image being placed over the bottom image.

# composite.py
import dmtools
from dmtools import transform

blue_square = dmtools.read('blue_square.png')
orange_square = dmtools.read('orange_square.png')

blue_over_orange = transform.composite(blue_square, orange_square)
dmtools.write_png(blue_over_orange, 'blue_over_orange.png')

orange_over_blue = transform.composite(orange_square, blue_square)
dmtools.write_png(orange_over_blue, 'orange_over_blue.png')
blue_square.png

blue_square.png

orange_square.png

orange_square.png

blue_over_orange.png

blue_over_orange.png

orange_over_blue.png

orange_over_blue.png

adjustments

The adjustments module currently contains an equivalent to a curves tool. The full API reference can be found here: dmtools.adjustments. In this section, we will give a more detailed explanation of how the curve tool can be used.

A curves tool is a comprehensive tool for changing the colors of an image. It can be used to achieve a variety of effects. It works by specifying a function for remapping the tones of an image. This function can be applied to the image as a whole or to an individual channel (examples of both are given below). As dmtools works with images normalized to [0,1], the curve function should be a function with a domain and range of [0,1].

Let us walk through the example script below. Aside from the identity function (the straight line from (0,0) to (1,1) in which every tone maps to itself), all of the functions it uses are given below.

clip_25_75.png

Clip to [0.25, 0.75]

clip_40_60.png

Clip to [0.40, 0.60]

parabola.png

Parabola

In this script, we apply a variety of different curves to the pallette.png image. Some curves are applied to all channels of an image (when no channel is given) and some are applied to individual channels. As we are working in the RGB (Red, Green, Blue) colorspace, the red channel is channel 0 and the blue channel is channel 2.

# curve.py
import dmtools
from dmtools import adjustments
import numpy as np

image = dmtools.read('pallette.png')

# apply identity to all channels
tmp = adjustments.apply_curve(image, lambda x: x)
dmtools.write_png(tmp, 'pallette_identity.png')

# apply clip from 0.25 to 0.75 to all channels
tmp = adjustments.apply_curve(image, lambda x: np.clip(2*(x-0.25), 0, 1))
dmtools.write_png(tmp, 'pallette_clip_25_75.png')

# apply clip from 0.4 to 0.6 to all channels
tmp = adjustments.apply_curve(image, lambda x: np.clip(5*(x-0.4), 0, 1))
dmtools.write_png(tmp, 'pallette_clip_40_60.png')

# apply clip from 0.4 to 0.6 to red channels
tmp = adjustments.apply_curve(image, lambda x: np.clip(5*(x-0.4), 0, 1), 0)
dmtools.write_png(tmp, 'pallette_clip_40_60_red.png')

# apply clip from 0.4 to 0.6 to blue channels
tmp = adjustments.apply_curve(image, lambda x: np.clip(5*(x-0.4), 0, 1), 2)
dmtools.write_png(tmp, 'pallette_clip_40_60_blue.png')

# apply parabola to all channels
tmp = adjustments.apply_curve(image, lambda x: 4*np.power(x - 0.5, 2))
dmtools.write_png(tmp, 'pallette_parabola.png')
pallette.png

pallette.png

pallette_identity.png

pallette_identity.png

pallette_clip_25_75.png

pallette_clip_25_75.png

pallette_clip_40_60.png

pallette_clip_40_60.png

pallette_clip_40_60_red.png

pallette_clip_40_60_red.png

pallette_clip_40_60_blue.png

pallette_clip_40_60_blue.png

pallette_parabola.png

pallette_parabola.png

All of the images generated by the script are shown above. You should make a few important observations. The identity function does not alter the image. The clip functions reduce contrast at either end of the tonal range but increase it in the center of the range. The clip to [0.40, 0.60] has a more pronounced effect that the clip to [0.25, 0.75]. Lastly, when the curve is applied to a single channel, the colors of other channels are unaffected.