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 manuipulating images programtically with dmtools is loading
in an image. This is done using
after manuipulating 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
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')
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.
dmtools.transform.blur() functions does just that. It takes a
sigma which indicates how much to blur the image.
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
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')
After blurring, we can actually rescale the image. This step is also called
“resampling.” This is done with
takes a parameter
k which specifies by what factor to scale the image.
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.
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.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).astype(np.uint8) dmtools.write_png(scaled_image, 'checks_10_point.png') scaled_image = transform.rescale(image, k=10, filter='triangle') scaled_image = transform.clip(scaled_image).astype(np.uint8) 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).astype(np.uint8) dmtools.write_png(scaled_image, '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:
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).astype(np.uint8) dmtools.write_png(clip_image, 'checks_10_clip.png') normalize_image = transform.normalize(scaled_image).astype(np.uint8) dmtools.write_png(normalize_image, 'checks_10_normalize.png') wraparound_image = transform.wraparound(scaled_image).astype(np.uint8) dmtools.write_png(wraparound_image, 'checks_10_wraparound.png')
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
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.
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
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.
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')
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 filter, the colors of other channels are unaffected.