﻿

# Multi-scale pyramids

An image can be represented by a multi-scale pyramid.

A multi-scale pyramid is an image represented by a number of images, each smaller then the previous, each representing a different level of detail. As the image in the series are of different sizes, they can be said to be a pyramid of images, and we can talk about building the pyramid from an image, or collapsing the pyramid to make the same image, or a different image is we have procesed the pyramid. If we build a pyramid then immediately collapse it, we should reconstruct the original image. (Collapsing is sometimes called inverting.)

Images at the top of the pyramid are the smallest, perhaps 1x1 pixel. They carry information about low-frequency data. Images at the bottom are the largest, perhaps the same size as the original image. High-frequency data is stored in those lower, larger images.

Each level of the pyramid might be a down-sized version of the input image; this is called a Gaussian pyramid. In a Gaussian pyramid, each layer is made by a lowpass filter: higher frequencies have been removed, like a photograph from a defocused camera. Alternatively, each layer may be a down-sized version of the difference between the upsized previous level and its input; this is called a Laplacian pyramid (also sometimes called a Difference of Gaussians pyramid). Each layer in a Laplacian pyramid is formed by a bandpass filter: it records data about a narrow band of frequencies, with both higher and lower frequencies removed.

Collapsing a Gaussian pyramid is trivial: just copy the bottom level. Provided this is the same size as the input image, it is an exact copy. Collapsing a Laplacian pyramid is more complex, as the layers must be added.

In the literature, multi-scale pyramids:

• have the lowest level the same size as the input image;
• halve in size at each level;

• all levels contribute equally to the reconstructed image.

This implementation relaxes those restrictions. This means that a pyramid may not exactly represent an input image, so a final difference image is also created. When a pyramid is collaped, adding in this final difference may be required to reconstruct the original.

Because the frequencies often double at each level, levels are commonly called octaves (by analogy with music).

I use the word grid to describe the image at a level in the pyramid.

Literature describes two operations required to make each grid: convolution (blurring), followed by sub-sampling. I combine these into one, using IM's -resize operator.

In this implementation of Laplacian pyramids, grids are weighted so they may contribute unequally to the result.

The scripts use TIFF files to store pyramids. If there are (n) octaves, there will be (n+1) images, representing the (n) grids plus the final difference (which may be entirely mid-gray). I refer to the bottom octave of the pramid, with the largest grid, as level zero; it is image number [0] in the TIFF file.

Applications for multi-scale pyramids include:

• searching for sub-images;
• matching panoramas;
• merging/blending;
• noise reduction;
• filling holes;
• copying textures;
• fractal noise, aka Perlin noise;
• changing the shape of the histogram of an octave, eg by matching histograms;
• image compression.

Histograms on this page were make using a process module, mkhisto. See Process modules. Construction, processing and collapsing of pyramids does not need process modules, or even HDRI.

ASIDE: Real-world pyramids have each level at a constant difference in linear size to adjacent levels. Image pyramids have each level at a constant factor to adjacent levels. Beware of stretched analogies.

## Sample input image

 A possible test image: ```%IMG7%magick ^ -size 1x1 xc:White ^ -bordercolor Black -border 100 ^ ( +clone -negate ) ^ +append +repage ^ msp_test.png set SRC=msp_test.png set pyGRAPHIC=1``` The test image used below: ```set SRC=toes.png set pyGRAPHIC=```

Examples below show the round trip: from an input image, build the pyramid, then collapse the pyramid and test how close the result is to the input.

## Make Gaussian pyramid

The script mkGausPyr.bat makes a Gaussian pyramid.

`call %PICTBAT%mkGausPyr %SRC% msp_g1.tiff`

Here is the pyramid:

`%IMG7%magick identify msp_g1.tiff `
```msp_g1.tiff[0] TIFF 267x233 267x233+0+0 16-bit sRGB 625754B 0.001u 0:00.000
msp_g1.tiff[1] TIFF 134x117 134x117+0+0 16-bit sRGB 0.001u 0:00.001
msp_g1.tiff[2] TIFF 67x58 67x58+0+0 16-bit sRGB 0.001u 0:00.001
msp_g1.tiff[3] TIFF 33x29 33x29+0+0 16-bit sRGB 0.001u 0:00.001
msp_g1.tiff[4] TIFF 17x15 17x15+0+0 16-bit sRGB 0.001u 0:00.001
msp_g1.tiff[5] TIFF 8x7 8x7+0+0 16-bit sRGB 0.001u 0:00.001
msp_g1.tiff[6] TIFF 4x4 4x4+0+0 16-bit sRGB 0.001u 0:00.001
msp_g1.tiff[7] TIFF 267x233 267x233+0+0 16-bit Grayscale Gray 0.001u 0:00.001```

There are 8 grids, or levels in the pyramid, numbered 0 to 7. The final image in the file is the image that is the source, minus grid zero enlarged. As grid zero is the same size as the source, it is equal to the source, so the final image is 50% gray.

By setting two variables, we tell the script to create viewable copies of the grids and build an HTML table. This shows the smallest octave at the top, and the largest octave (number 0) at the bottom. The tiff file contains each image shown under the heading "Grid". It also contains the octave 0 difference.

```set mkpDEBUG=1
set mkpHTM=1
set pyPREFIX=mspg_

call %PICTBAT%mkGausPyr %SRC% msp_g2.tiff```

The table shows each grid enlarged to the size of the source image, and a histogram of that enlargement. The final column is the source minus the enlarged grid.

Octave Grid Grid, resized up Histogram of resized grid Difference
6
5
4
3
2
1
0

The script mkGausPyr.bat has created a reconstruction script that will write an output file the name of which is in environment variable mkgpRECON_FILE.

```%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]\n" msp_g2.tiff[%mkgpNUM_OCTAVES%]
echo mkgpRECON_FILE=%mkgpRECON_FILE%

if "%mkgpRECON_FILE%"=="" exit /B 1```
```min=0.500008 max=0.500008
mkgpRECON_FILE=mspg_mkgp_recon.miff ```

From the pyramid file, we can reconstruct an image. We "collapse" the pyramid. Then we compare the reconstruction with the original.

 ```%IMG7%magick ^ -script mspg_mkgp_recon.scr if ERRORLEVEL 1 exit /B 1 %IMG7%magick ^ %mkgpRECON_FILE% msp_recon_g.png if ERRORLEVEL 1 exit /B 1 %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_g.png ^ NULL: >msp_comp1.lis 2^>^&1 ``` `6.98244 (0.000106545)`

## Make Laplacian pyramid

The script mkLapPyr.bat makes a Laplacian pyramid.

We will want to reconstruct this example later, with a modified pyramid, so we will create a file of the variables used.

```set pyWR_VAR=1
set pyPREFIX=mspl1_

call %PICTBAT%mkLapPyr %SRC% msp_l1.tiff

set pyWR_VAR=```

As with the Gaussian, there are 8 grids, or levels in the pyramid, numbered 0 to 7. The final image in the file is the input image minus the sum of the enlarged grids. Here is the pyramid:

`%IMG7%magick identify msp_l1.tiff `
```msp_l1.tiff[0] TIFF 267x233 267x233+0+0 32-bit sRGB 1.19117MiB 0.001u 0:00.000
msp_l1.tiff[1] TIFF 134x117 134x117+0+0 32-bit sRGB 0.001u 0:00.000
msp_l1.tiff[2] TIFF 67x58 67x58+0+0 32-bit sRGB 0.001u 0:00.000
msp_l1.tiff[3] TIFF 33x29 33x29+0+0 32-bit sRGB 0.001u 0:00.000
msp_l1.tiff[4] TIFF 17x15 17x15+0+0 32-bit sRGB 0.001u 0:00.000
msp_l1.tiff[5] TIFF 8x7 8x7+0+0 32-bit sRGB 0.001u 0:00.000
msp_l1.tiff[6] TIFF 4x4 4x4+0+0 32-bit sRGB 0.001u 0:00.000
msp_l1.tiff[7] TIFF 267x233 267x233+0+0 32-bit Grayscale Gray 0.001u 0:00.000```

Laplacian pyramids are cumulative, so the "Difference" column shows the input to that level minus the enlarged grid. Put another way, it shows the source image minus the sum of the enlarged grids at or above that octave.

Octave Grid Grid, resized up Histogram of resized grid Difference
6
5
4
3
2
1
0

The histogram shows that the enlarged grids approach a Laplacian distribution: values cluster around "zero" (50%), with a sharp peak at that value. A Gaussian distribution is similar, but with a rounded peak.

The final difference is zero:

`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l1.tiff[%mklpNUM_OCTAVES%] `
`min=0.5 max=0.5`

From the pyramid file, we can reconstruct an image. We "collapse" the pyramid. Then we compare the reconstruction with the original.

 ```%IMG7%magick ^ -script mspl1_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l1.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l1.png ^ NULL: >msp_comp2.lis 2^>^&1 ``` `67.6802 (0.00103273)`

## Script controls

Both scripts take up to six arguments.

 %1 Input image %2 Output filename for the multi-image pyramid. %3 The minimum block size. Small blocks give high-frequency detail. Default: 1 %4 The maximum block size. Large blocks give low-frequency detail. Default: min(height,width)/2 %5 The block factor. Should be >=1. Default: 2 %6 The power factor. Default: 0

The script starts at block size %3 x %3. This is the smallest block size, which creates the highest frequency noise. Successive blocks are %5 times as large, at increasing sizes so at lower frequencies. It stops when the block size would be greater than or equal to %4.

Block sizes correspond to wavelengths, the inverse of frequencies.

Amplitudes are normalised so the maximum is 1.0. Each is then multiplied by %fnCONST_FAC%, which defaults to 1.0. If %fnCONST_FAC% is set to a value greater then 1.0, the result is likely to be clipped at 100%.

The power factor has no relevance to Gaussian pyramids. For Laplacian pyramids, it determines relative weights of the grids. At the default value of zero, grids have equal weights. For values greater than zero, lower frequencies have more weight. For values less than zero, higher frequencies have more weight. The useful range is about -1 to +1.

The scripts also use the values of some environment variables, if these are set.

 pyPREFIX Prefix for working files (but not output file). Default: py_ pyIM The directory for IM programs. Default: %IMG7% pyMIN_BLK_WH Don't make octaves with fewer blocks across image width or height. Default: no minimum. pyMAX_NUM_OCT Don't make more than this number of octaves. Default: 100 pyWEIGHTING Set to sum to scale weights so sum is 1.0 (or 0.5). Set to max to scale weights so maximum weight is 1.0 (or 0.5). Default: sum pyGRAPHIC If 1, halves the sum or maximum of the weights. Default: no halving. pyWR_VAR If 1, mkvPyr.bat will create text file of variables. Default: doesn't create text file. pyTXT_OUT If 1, echoes some text. Default: no echoing.

## Examples of Laplacian pyramids

For example: set the minimum block size to 10, so grid zero will be about 1/10 the size of the source image.

```set pyPREFIX=mspl2_

call %PICTBAT%mkLapPyr %SRC% msp_l2.tiff 10 80```
`%IMG7%magick identify msp_l2.tiff `
```msp_l2.tiff[0] TIFF 27x23 27x23+0+0 32-bit sRGB 758120B 0.000u 0:00.000
msp_l2.tiff[1] TIFF 13x12 13x12+0+0 32-bit sRGB 0.000u 0:00.000
msp_l2.tiff[2] TIFF 7x6 7x6+0+0 32-bit sRGB 0.000u 0:00.000
msp_l2.tiff[3] TIFF 267x233 267x233+0+0 32-bit sRGB 0.000u 0:00.000```
Octave Grid Grid, resized up Histogram of resized grid Difference
2
1
0
`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l2.tiff[%mklpNUM_OCTAVES%] `
`min=0.233368 max=0.871562`
 ```%IMG7%magick ^ -script mspl2_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l2.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l2.png ^ NULL: >msp_compl2.lis 2^>^&1 ``` `10.5857 (0.000161527)`

Another example: set the minimum block size to 0.2, so blah.

```set pyPREFIX=mspl3_

call %PICTBAT%mkLapPyr %SRC% msp_l3.tiff 0.2 20```
`%IMG7%magick identify msp_l3.tiff `
```msp_l3.tiff[0] TIFF 1335x1165 1335x1165+0+0 32-bit sRGB 24.4533MiB 0.001u 0:00.000
msp_l3.tiff[1] TIFF 668x583 668x583+0+0 32-bit sRGB 0.001u 0:00.000
msp_l3.tiff[2] TIFF 334x291 334x291+0+0 32-bit sRGB 0.001u 0:00.000
msp_l3.tiff[3] TIFF 167x146 167x146+0+0 32-bit sRGB 0.001u 0:00.000
msp_l3.tiff[4] TIFF 83x73 83x73+0+0 32-bit sRGB 0.001u 0:00.000
msp_l3.tiff[5] TIFF 42x36 42x36+0+0 32-bit sRGB 0.001u 0:00.000
msp_l3.tiff[6] TIFF 21x18 21x18+0+0 32-bit sRGB 0.001u 0:00.000
msp_l3.tiff[7] TIFF 267x233 267x233+0+0 32-bit sRGB 0.001u 0:00.000```
Octave Grid Grid, resized up Histogram of resized grid Difference
6
5
4
3
2
1
0
`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l3.tiff[%mklpNUM_OCTAVES%] `
`min=0.484955 max=0.51127`
 ```%IMG7%magick ^ -script mspl3_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l3.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l3.png ^ NULL: >msp_compl3.lis 2^>^&1 ``` `0 (0)`

Another example: set the minimum and maximum block sizes, so we have fewer octaves.

```set pyPREFIX=mspl3a_

call %PICTBAT%mkLapPyr %SRC% msp_l3a.tiff 1 4```
`%IMG7%magick identify msp_l3a.tiff `
```msp_l3a.tiff[0] TIFF 267x233 267x233+0+0 32-bit sRGB 1.12991MiB 0.000u 0:00.000
msp_l3a.tiff[1] TIFF 134x117 134x117+0+0 32-bit sRGB 0.000u 0:00.000
msp_l3a.tiff[2] TIFF 267x233 267x233+0+0 32-bit Grayscale Gray 0.000u 0:00.000```
Octave Grid Grid, resized up Histogram of resized grid Difference
1
0
`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l3a.tiff[%mklpNUM_OCTAVES%] `
`min=0.5 max=0.5`
 ```%IMG7%magick ^ -script mspl3a_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l3a.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l3a.png ^ NULL: >msp_compl3a.lis 2^>^&1 ``` `0 (0)`

Another example: small block factor, so we get more frequencies.

```set pyPREFIX=mspl4_

call %PICTBAT%mkLapPyr %SRC% msp_l4.tiff . . 1.5```
`%IMG7%magick identify msp_l4.tiff `
```msp_l4.tiff[0] TIFF 267x233 267x233+0+0 32-bit sRGB 1.52441MiB 0.001u 0:00.001
msp_l4.tiff[1] TIFF 178x155 178x155+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[2] TIFF 119x104 119x104+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[3] TIFF 79x69 79x69+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[4] TIFF 53x46 53x46+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[5] TIFF 35x31 35x31+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[6] TIFF 23x20 23x20+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[7] TIFF 16x14 16x14+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[8] TIFF 10x9 10x9+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[9] TIFF 7x6 7x6+0+0 32-bit sRGB 0.001u 0:00.001
msp_l4.tiff[10] TIFF 5x4 5x4+0+0 32-bit sRGB 0.001u 0:00.000
msp_l4.tiff[11] TIFF 3x3 3x3+0+0 32-bit sRGB 0.004u 0:00.004
msp_l4.tiff[12] TIFF 267x233 267x233+0+0 32-bit Grayscale Gray 0.004u 0:00.003```
Octave Grid Grid, resized up Histogram of resized grid Difference
11
10
9
8
7
6
5
4
3
2
1
0
`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l4.tiff[%mklpNUM_OCTAVES%] `
`min=0.5 max=0.5`
 ```%IMG7%magick ^ -script mspl4_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l4.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l4.png ^ NULL: >msp_compl4.lis 2^>^&1 ``` `59.0646 (0.000901268)`

Another example: a negative power factor gives more weight to higher frequencies.

We will want to reconstruct this example later, with a modified pyramid, so we will create a file of the variables used.

```set pyWR_VAR=1
set pyPREFIX=mspl5_

call %PICTBAT%mkLapPyr %SRC% msp_l5.tiff . . . -0.5

set pyWR_VAR=```
`%IMG7%magick identify msp_l5.tiff `
```msp_l5.tiff[0] TIFF 267x233 267x233+0+0 32-bit sRGB 1.19117MiB 0.001u 0:00.000
msp_l5.tiff[1] TIFF 134x117 134x117+0+0 32-bit sRGB 0.001u 0:00.001
msp_l5.tiff[2] TIFF 67x58 67x58+0+0 32-bit sRGB 0.001u 0:00.001
msp_l5.tiff[3] TIFF 33x29 33x29+0+0 32-bit sRGB 0.001u 0:00.001
msp_l5.tiff[4] TIFF 17x15 17x15+0+0 32-bit sRGB 0.001u 0:00.000
msp_l5.tiff[5] TIFF 8x7 8x7+0+0 32-bit sRGB 0.001u 0:00.000
msp_l5.tiff[6] TIFF 4x4 4x4+0+0 32-bit sRGB 0.001u 0:00.000
msp_l5.tiff[7] TIFF 267x233 267x233+0+0 32-bit Grayscale Gray 0.001u 0:00.000```
Octave Grid Grid, resized up Histogram of resized grid Difference
6
5
4
3
2
1
0
`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l5.tiff[%mklpNUM_OCTAVES%] `
`min=0.5 max=0.5`
 ```%IMG7%magick ^ -script mspl5_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l5.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l5.png ^ NULL: >msp_compl5.lis 2^>^&1 ``` `1.79162 (2.73384e-05)`

Another example: a positive power factor gives more weight to low frequencies.

```set pyPREFIX=mspl6_

call %PICTBAT%mkLapPyr %SRC% msp_l6.tiff . . . 0.5```
`%IMG7%magick identify msp_l6.tiff `
```msp_l6.tiff[0] TIFF 267x233 267x233+0+0 32-bit sRGB 1.66583MiB 0.001u 0:00.000
msp_l6.tiff[1] TIFF 134x117 134x117+0+0 32-bit sRGB 0.001u 0:00.000
msp_l6.tiff[2] TIFF 67x58 67x58+0+0 32-bit sRGB 0.001u 0:00.000
msp_l6.tiff[3] TIFF 33x29 33x29+0+0 32-bit sRGB 0.001u 0:00.000
msp_l6.tiff[4] TIFF 17x15 17x15+0+0 32-bit sRGB 0.001u 0:00.000
msp_l6.tiff[5] TIFF 8x7 8x7+0+0 32-bit sRGB 0.001u 0:00.000
msp_l6.tiff[6] TIFF 4x4 4x4+0+0 32-bit sRGB 0.001u 0:00.000
msp_l6.tiff[7] TIFF 267x233 267x233+0+0 32-bit sRGB 0.001u 0:00.000```
Octave Grid Grid, resized up Histogram of resized grid Difference
6
5
4
3
2
1
0
`%IMG7%magick identify -format "min=%%[fx:minima] max=%%[fx:maxima]" msp_l6.tiff[%mklpNUM_OCTAVES%] `
`min=0.317592 max=0.785909`
 ```%IMG7%magick ^ -script mspl6_mklp_recon.scr %IMG7%magick ^ %mklpRECON_FILE% msp_recon_l6.png %IMG7%magick compare ^ -metric RMSE ^ %SRC% ^ msp_recon_l6.png ^ NULL: >msp_compl6.lis 2^>^&1 ``` `67.6802 (0.00103273)`

## How it works

The two scripts work in the same way, by first calling mkPyrComm.bat. This does some validation, and calls mkpVar.bat, which does the messy work of calculating from the six input parameters how many levels (octaves) the pyramid will have, and the size and weight of each level.

mkpVar.bat will create blocks up to a specified maximum size. For some applications, we care more about the number of blocks. For example, we may not want any octaves to contain fewer than ten blocks in width or height. For this, set pyMIN_BLK_WH=10. If this value is set too high, no octaves will be created, and mkpVar.bat will fail.

Or we might care more about the number of octaves in the pyramid. For this, set pyMAX_NUM_OCT=6 or whatever. If this variable is not set, the script will use a default value of 100, to prevent infinite loops. If you really want more than 100 octaves, change the script.

Weights are calculated from the variable FR, for "frequency ratio", which is calculated as sqrt( (B*B)/(W*H) ) where B is the block size, W is the required output width and H is the required output height. For larger blocks, representing lower frequencies, the value of FR decreases.

Weights are scaled so that either the maximum weight is 1.0 (set pyWEIGHTING=max), or the sum of the weights is 1.0 (set pyWEIGHTING=sum). (When pyGRAPHIC is 1, the maximum or sum will be 0.5.)

Optionally, mkpVar.bat can write data about a pyramid to a text file named {something}_blk.lis, with each line in the form:

`{variable}={value}`

Other scripts can read this file, to re-create the environment variables. If you want this file to be created, set pyWR_VAR=1.

The mechanism is designed for performance of animations. Storing values in environment variables, or even reading the values from a file, is quicker than recalculating them.

When mkPyrComm.bat finishes, control returns to mkGausPyr.bat or mkLapPyr.bat. That script creates two ImageMagick script files:

• {something}_mk.scr will read the input image and create a list of images, being the pyramid levels and the final (level zero) difference.
• {something}_recon.scr will read the pyramid file and create a list with the single reconstructed image.

mkGausPyr.bat or mkLapPyr.bat then runs magick with {something}_mk.scr, supplying the output file, which typically would have a .tiff extension. Thus the pyramid is created. Optionally, it also creates single-image files and an HTML file that contains a table of those images, including histograms of enlarged grids, such as those on this page.

The basic block for building pyramids is:

```previous_difference - levelled(enlarged(grid)) + 0.5 -> next_difference
shrink(new_difference) -> next_grid```

Conversely, for collapsing pyramids:

`previous + levelled(enlarged(grid)) - 0.5 -> next`

In both directions, clipping is possible (because when 0<=x,y<=100, -50<=x-y+50<=+150). This doesn't happen (in my experience) with ordinary photographs but can happen with graphics. So I provide the pyGRAPHIC option. When this is set to 1, all amplitudes are halved at the levelling stage. (This ensures that 0<=x,y<=50 so 0<=x-y+50<=+100.) This prevents clipping. Unfortunately halving amplitudes can prevent the final difference from being zero, for both photographs and graphics, so the difference must always be included in the image reconstruction. The scripts currently do not detect clipping. Setting pyGRAPHIC to 1 is always safe.

Other circumstances will also cause non-zero final differences:

• grid zero is smaller than the source image (because the minimum block size was greater than one).
• the power factor is greater than zero, so high frequencies have less weight, and can't fully compensate for errors made by (blurred) low frequencies.

Is one difference image always enough? It seems to be, provided pyGRAPHIC=1 so the final difference carries twice the weight of the heaviest grid.

Here are sample _blk.lis, _mk.scr and _recon.scr files from an example above.

mspl5_blk.lis:

```pyWEIGHTING=max
pyGRAPHIC=
pyLINEAR=
NUM_OCTAVES=7
mklpRECON_FILE=mspl5_mklp_recon.miff
IMG_WW=267
IMG_HH=233
BLK_SZ.0=1
N_BLK_W.0=267
N_BLK_H.0=233
FR.0=0.00400928019615047
S1AMP.0=0.321291657536273
M1AMP.0=1
AMP.0=1
sLEVEL.0=
BLK_SZ.1=2
N_BLK_W.1=134
N_BLK_H.1=117
FR.1=0.00801856039230093
S1AMP.1=0.227187509782565
M1AMP.1=0.70710678118655
AMP.1=0.70710678118655
sLEVEL.1=+level 14.6446609406725%,85.3553390593275%
BLK_SZ.2=4
N_BLK_W.2=67
N_BLK_H.2=58
FR.2=0.0160371207846019
S1AMP.2=0.160645828768137
M1AMP.2=0.500000000000001
AMP.2=0.500000000000001
sLEVEL.2=+level 25%,75%
BLK_SZ.3=8
N_BLK_W.3=33
N_BLK_H.3=29
FR.3=0.0320742415692037
S1AMP.3=0.113593754891283
M1AMP.3=0.353553390593275
AMP.3=0.353553390593275
sLEVEL.3=+level 32.3223304703362%,67.6776695296638%
BLK_SZ.4=16
N_BLK_W.4=17
N_BLK_H.4=15
FR.4=0.0641484831384074
S1AMP.4=0.0803229143840684
M1AMP.4=0.250000000000001
AMP.4=0.250000000000001
sLEVEL.4=+level 37.5%,62.5%
BLK_SZ.5=32
N_BLK_W.5=8
N_BLK_H.5=7
FR.5=0.128296966276815
S1AMP.5=0.0567968774456412
M1AMP.5=0.176776695296637
AMP.5=0.176776695296637
sLEVEL.5=+level 41.1611652351681%,58.8388347648319%
BLK_SZ.6=64
N_BLK_W.6=4
N_BLK_H.6=4
FR.6=0.25659393255363
S1AMP.6=0.0401614571920342
M1AMP.6=0.125
AMP.6=0.125
sLEVEL.6=+level 43.75%,56.25%```

mspl5_mklp_mk.scr:

```toes.png
+write mpr:SRC
-channel RGB -resize "4x4!" +channel
+write mspl5_grd_6.miff
+write mpr:GRD6
-channel RGB -resize "267x233!" +channel
+write mspl5_full_6.miff +level 43.75%,56.25%
mpr:SRC
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_6.miff
+write mpr:DIFF6
-channel RGB -resize "8x7!" +channel
+write mspl5_grd_5.miff
+write mpr:GRD5
-channel RGB -resize "267x233!" +channel
+write mspl5_full_5.miff +level 41.1611652351681%,58.8388347648319%
mpr:DIFF6
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_5.miff
+write mpr:DIFF5
-channel RGB -resize "17x15!" +channel
+write mspl5_grd_4.miff
+write mpr:GRD4
-channel RGB -resize "267x233!" +channel
+write mspl5_full_4.miff +level 37.5%,62.5%
mpr:DIFF5
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_4.miff
+write mpr:DIFF4
-channel RGB -resize "33x29!" +channel
+write mspl5_grd_3.miff
+write mpr:GRD3
-channel RGB -resize "267x233!" +channel
+write mspl5_full_3.miff +level 32.3223304703362%,67.6776695296638%
mpr:DIFF4
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_3.miff
+write mpr:DIFF3
-channel RGB -resize "67x58!" +channel
+write mspl5_grd_2.miff
+write mpr:GRD2
-channel RGB -resize "267x233!" +channel
+write mspl5_full_2.miff +level 25%,75%
mpr:DIFF3
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_2.miff
+write mpr:DIFF2
-channel RGB -resize "134x117!" +channel
+write mspl5_grd_1.miff
+write mpr:GRD1
-channel RGB -resize "267x233!" +channel
+write mspl5_full_1.miff +level 14.6446609406725%,85.3553390593275%
mpr:DIFF2
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_1.miff
+write mpr:DIFF1
+write mspl5_grd_0.miff
+write mpr:GRD0
+write mspl5_full_0.miff
mpr:DIFF1
-channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
+write mspl5_diff_0.miff
+write mpr:DIFF0
+delete
mpr:GRD0
mpr:GRD1
mpr:GRD2
mpr:GRD3
mpr:GRD4
mpr:GRD5
mpr:GRD6
mpr:DIFF0
-define quantum:format=floating-point
+depth
-depth 32
-define tiff:alpha=associated
-write msp_l5.tiff
-exit```

mspl5_mklp_recon.scr:

```( msp_l5.tiff[6]
-resize "267x233!"
+level 43.75%,56.25% )
( msp_l5.tiff[5]
-resize "267x233!"
+level 41.1611652351681%,58.8388347648319% )
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
( msp_l5.tiff[4]
-resize "267x233!"
+level 37.5%,62.5% )
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
( msp_l5.tiff[3]
-resize "267x233!"
+level 32.3223304703362%,67.6776695296638% )
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
( msp_l5.tiff[2]
-resize "267x233!"
+level 25%,75% )
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
( msp_l5.tiff[1]
-resize "267x233!"
+level 14.6446609406725%,85.3553390593275% )
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
( msp_l5.tiff[0]
)
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
msp_l5.tiff[7]
-compose Mathematics -define compose:args=0,1,1,-0.5 -composite
-write mspl5_mklp_recon.miff
-exit```

## Representation

In the literature, grids (the layers of a pyramid) are sometimes combined into a single image, like this:

 Example Gaussian pyramid ```%IMG7%magick ^ msp_g1.tiff ^ +delete ^ ( -clone 0 +write mpr:GRD0 +delete ) ^ -delete 0 ^ -append +repage ^ mpr:GRD0 ^ +swap ^ +append +repage ^ msp_repres.png``` Example Laplacian pyramid ```%IMG7%magick ^ msp_l1.tiff ^ +delete ^ ( -clone 0 +write mpr:GRD0 +delete ) ^ -delete 0 ^ -append +repage ^ mpr:GRD0 ^ +swap ^ +append +repage ^ msp_repres_l.png```

## Experiments

Scripts in this section operate on pyramid files. They apply some processing to level 0 then collapse the pyramid to make an image. They repeat this for every level, so there will be one image per level, each with the processing applied to just that level. Then they create HTML code that shows each resulting image. That HTML code is included in this web page, so we can see the images.

The scripts need:

• The pyramid file, typically TIFF format.
• The block list file, called {prefix}blk.lis, which contains values of variables, in particular NUM_OCTAVES.
• The reconstruction script file, {prefix}recon.scr.

By default, mkLapPyr.bat doesn't make a block list file, but will do so if set pyWR_VAR=1.

### Boost narrow-band frequencies

For a Laplacian pyramid, we can selectively boost or cut the amplitude at selected frequencies. This is like the graphic equaliser on an audio system that modifies the loudness of treble, bass, or a selected frequency range. To do this properly we would make an alternative _recon.scr script. As a crude proof-of-concept test, we simply apply a sigmoidal-contrast operation to each of the grids in turn, reconstructing the image each time. The script mspBoostCut.bat does this, creating HTML code that is included below. In this crude test, we don't change the amplitude of the final difference.

"Boost" increases the mid-tone contrast, which usually increases saturation.

```set pyPREFIX=mspl5_

call %PICTBAT%mspBoostCut msp_l5.tiff boost 10 msp_boost.htm
call %PICTBAT%mspBoostCut msp_l5.tiff cut 10 msp_cut.htm```

For comparison, here is toes.png:

Each image shows the effect of applying the stated process to one grid of a Laplacian pyramid, and reconstructing an image from that altered pyramid. In each set, we start at octave 0, which is the largest grid and contains the highest frequencies.

mspBoostCut: boost 10

mspBoostCut: cut 10

Boost more:

```set pyPREFIX=mspl5_

call %PICTBAT%mspBoostCut msp_l5.tiff boost 25 msp_boost2.htm
call %PICTBAT%mspBoostCut msp_l5.tiff boost 75 msp_boost3.htm```

mspBoostCut: boost 25

mspBoostCut: boost 75

The idea could be extended so to allow cutting some arbitrary frequencies while boosting others.

Pyramids msp_l1.tiff and msp_l5.tiff were built with power factors 0.0 and -0.5 to give an equal or greater weight to higher frequencies in the pyramid. Collapsing either pyramid in the ordinary way will restore the original images.

However, we can use the msp_l5.tiff reconstruction script on the msp_l1.tiff pyramid. We can do this very crudely by making a copy of the script, changing the name of the pyramid file from msp_l5 to msp_l1.

`sed -e 's/msp_l5/msp_l1/g' mspl5_mklp_recon.scr >msp_1_sharper.scr`

Then we collapse the msp_l1 pyramid with a script that was created for msp_l5.

 ```%IMG7%magick -script msp_1_sharper.scr %IMG7%magick %mklpRECON_FILE% msp_1_sharper.png```

Because low-frequency weight has reduced, so has colour saturation. We restore colours by replacing the a* and b* channels of an L*a*b* version with those from toes.png.

 ```%IMG7%magick ^ msp_1_sharper.png ^ toes.png ^ -colorspace Lab ^ -separate ^ -delete 1-3 ^ -set colorspace Lab ^ -combine ^ -colorspace sRGB ^ msp_1_shlab.png```

The effect is very heavy. We will create a lighter effect.

```set pyWR_VAR=1
set pyPREFIX=mspl5a_

call %PICTBAT%mkLapPyr %SRC% msp_l5a.tiff . . . -0.1

set pyWR_VAR=

sed -e 's/msp_l5a/msp_l1/g' mspl5a_mklp_recon.scr >msp_1_sharper2.scr```
 ```%IMG7%magick -script msp_1_sharper2.scr %IMG7%magick %mklpRECON_FILE% msp_1_sharper2.png %IMG7%magick ^ msp_1_sharper2.png ^ toes.png ^ -colorspace Lab ^ -separate ^ -delete 1-3 ^ -set colorspace Lab ^ -combine ^ -colorspace sRGB ^ msp_1_shlab2.png```

### General grid processing

Above, we applied a sigmoidal-contrast operation to each of the grids. We can generalise this to apply any IM operation, implemented as mspProcGrid.bat.

For the following effects, we use the pyramid built with a negative power factor, which gives more weight to higher frequencies.

```set pyPREFIX=mspl5_

for /F "tokens=*" %%L in (%pyPREFIX%blk.lis) do set %%L

call %PICTBAT%mspProcGrid msp_l5.tiff "-level 30%%%%,70%%%%" msp_lev1.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-level 45%%%%,55%%%%" msp_lev2.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-auto-level -auto-gamma" msp_alag.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-equalize" msp_equal.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-set option:modulate:colorspace HCL -modulate 100,200,100" msp_sat.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-negate" msp_neg.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "+dither -colors 4" msp_cols.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "+dither -posterize 3" msp_post.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-paint 3" msp_paint.htm
call %PICTBAT%mspProcGrid msp_l5.tiff "-colorspace Gray" msp_gray.htm```

For comparison, here is toes.png:

mspProcGrid: "-level 30%,70%"

mspProcGrid: "-level 45%,55%"

We can get extreme sharpening without ringing.

mspProcGrid: "-auto-level -auto-gamma"

mspProcGrid: "-equalize"

mspProcGrid: "-set option:modulate:colorspace HCL -modulate 100,200,100"

mspProcGrid: "-negate"

mspProcGrid: "+dither -colors 4"

mspProcGrid: "+dither -posterize 3"

mspProcGrid: "-paint 3"

mspProcGrid: "-colorspace Gray"

Some effects work better when used on pyramids built with equal weight to all frequencies.

```set pyPREFIX=mspl1_

for /F "tokens=*" %%L in (%pyPREFIX%blk.lis) do set %%L

call %PICTBAT%mspProcGrid msp_l1.tiff "-level 30%%%%,70%%%%" msp_lev1q.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-level 45%%%%,55%%%%" msp_lev2q.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-auto-level -auto-gamma" msp_alagq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-equalize" msp_equalq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-set option:modulate:colorspace HCL -modulate 100,200,100" msp_satq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-negate" msp_negq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "+dither -colors 4" msp_colsq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "+dither -posterize 3" msp_postq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-paint 3" msp_paintq.htm
call %PICTBAT%mspProcGrid msp_l1.tiff "-colorspace Gray" msp_grayq.htm```

mspProcGrid: "-level 30%,70%"

mspProcGrid: "-level 45%,55%"

mspProcGrid: "-auto-level -auto-gamma"

mspProcGrid: "-equalize"

mspProcGrid: "-set option:modulate:colorspace HCL -modulate 100,200,100"

mspProcGrid: "-negate"

mspProcGrid: "+dither -colors 4"

mspProcGrid: "+dither -posterize 3"

mspProcGrid: "-paint 3"

mspProcGrid: "-colorspace Gray"

## Applications

Laplacian pyramids are useful when blending (see Blending pyramids) and filling holes, aka inpainting (see Laplacian pyramids with transparency). They can also be used to quantify levels of detail in an image; see Detail by pyramids.

Examples on this page store pyramids in TIFF files (and write images to other files, for display on the web page). There may be no need to store the pyramid in any file; the pyramid may be built and collapsed within a single magick command. Examples of this are shown on Fractal noise.

Pyramids can be animated. In particular, the image reconstructed from a pyramid ("collapsing" the pyramid) may use slightly transformed values from the grids, with the amount of transformation changing between frames. Examples of this are shown on Fractal noise animations.

## Cleanup

```set mkpDEBUG=
set mkpHTM=
set pyGRAPHIC=
set pyPREFIX=
set pyIM=```

## Scripts

For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.

### mkGausPyr.bat

```rem From image %1, make Gaussian pyramid %2.
@rem %2 should be capable of containing multiple images, eg type TIFF.
@rem %3 %4 %5 %6 parameters to mkPyrComm
@rem
@rem Also uses:
@rem   mkpDEBUG if 1, creates other image files (full and diff).
@rem   mkpHTM if 1, creates HTM file
@rem
@rem Updated:
@rem   5-September-2022 for IM v7. Remove %IML%.
@rem

@rem For some purposes,
@rem resizing down to the grid might be good enough if we use "-scale?

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 mkgp

if "%pyWEIGHTING%"=="" set pyWEIGHTING=max

call %PICTBAT%mkPyrComm %1 %2 %3 %4 %5 %6
if ERRORLEVEL 1 exit /B 1

echo %INFILE% +write mpr:SRC +delete >%MK_SCR%

:: Could be optimised.
:: If DEBUG, we need to enlarge and find DIFF.
:: If octave 0 isn't full-size, we need DIFF_0
:: If neither of these, we don't need to enlarge and do maths,
:: and could create final DIFF as solid gray 50%.

for /L %%N in (%LAST_OCT%,-1,0) do (

set GRD_FILE=%pyPREFIX%grd_%%N%fnNG_EXT%

rem echo N_BLK_W.%%N=!N_BLK_W.%%N! N_BLK_H.%%N=!N_BLK_H.%%N! AMP.%%N=!AMP.%%N! M1AMP.%%N=!M1AMP.%%N!

set DO_RES=1
if !N_BLK_W.%%N!==%WW% if !N_BLK_H.%%N!==%HH% set DO_RES=0

set WRDIFF=1
if not %%N==0 if not "%mkpDEBUG%"=="1" set WRDIFF=0

( echo mpr:SRC
if !DO_RES!==1 echo -resize "!N_BLK_W.%%N!x!N_BLK_H.%%N!^!"
if "%mkpDEBUG%"=="1" echo +write !GRD_FILE!
echo +write mpr:GRD%%N
if !DO_RES!==1 echo -resize "%WW%x%HH%^!"
if "%mkpDEBUG%"=="1" echo +write %pyPREFIX%full_%%N%fnNG_EXT%
echo mpr:SRC
echo -compose Mathematics -define compose:args=0,1,-1,0.5 -composite
if "%mkpDEBUG%"=="1" echo +write %pyPREFIX%diff_%%N%fnNG_EXT%
echo +write mpr:DIFF%%N
echo +delete
)>>%MK_SCR%

if "%mkpHTM%"=="1" (
echo ^<tr^>
echo ^<td^>%%N^</td^>
echo ^<td^>^<img src="!GRD_FILE!%EXTEXT%" /^>^</td^>
echo ^<td^>^<img src="%pyPREFIX%full_%%N%fnNG_EXT%%EXTEXT%" /^>^</td^>
echo ^<td^>^<img src="%pyPREFIX%full_%%N_h_glc.png" /^>^</td^>
echo ^<td^>^<img src="%pyPREFIX%diff_%%N%fnNG_EXT%%EXTEXT%" /^>^</td^>
echo ^</tr^>
)>>%HTM_TAB%
)

:: List the MPRs, ready to write to pyramid file.
(
for /L %%N in (0,1,%LAST_OCT%) do @(
@echo mpr:GRD%%N
)
echo mpr:DIFF0
echo +depth
echo -write %PYR_FILE%
echo -exit
)>>%MK_SCR%

:: Add final difference, and delinear, to reconstruction.
set DO_RES=1
if !N_BLK_W.0!==%WW% if !N_BLK_H.0!==%HH% set DO_RES=0

(
echo %PYR_FILE%[0]
if !DO_RES!==1 echo -resize "%WW%x%HH%^!"
echo %PYR_FILE%[%NUM_OCTAVES%]
echo -compose Mathematics -define compose:args=0,1,1,-0.5 -composite %sDELIN%
echo -write %RECON_FILE%
echo -exit
)>%RECON_SCR%

if "%mkpHTM%"=="1" echo ^</table^> >>%HTM_TAB%

if "%pyIM%"=="" (
set pyIM=%IMG7%
)
if "%pyIM%"=="" (
echo %0: pyIM not set
exit /B 1
)

%pyIM%magick ^
-script %MK_SCR%

if "%mkpHTM%"=="1" (
for /L %%N in (0,1,%LAST_OCT%) do @(
%IM7DEV%magick ^
%pyPREFIX%full_%%N%fnNG_EXT% ^
-process 'mkhisto capnumbuckets 512 norm' ^
%pyPREFIX%full_%%N_h%fnNG_EXT%

call %PICTBAT%graphLineCol %pyPREFIX%full_%%N_h%fnNG_EXT% . . 0
)
)

call echoRestore

@endlocal & set mkgpOUTFILE=%PYR_FILE%& set mkgpNUM_OCTAVES=%NUM_OCTAVES%& set mkgpRECON_FILE=%RECON_FILE%```

### mkLapPyr.bat

```rem From image %1, make Laplacian pyramid %2.
@rem %2 should be capable of containing multiple images, eg type TIFF.
@rem %3 %4 %5 %6 parameters to mkPyrComm
@rem
@rem Also uses:
@rem   mkpDEBUG if 1, creates other image files (full and diff).
@rem   mkpHTM if 1, creates HTM file
@rem   mkpKEEP_ALPHA_RES if 1, keeps alpha in grids at best resolution.
@rem   pyPREFIX prefix for working files.
@rem
@rem Updated:
@rem   5-September-2022 for IM v7.
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 mklp

if "%pyWEIGHTING%"=="" set pyWEIGHTING=max

call %PICTBAT%mkPyrComm %1 %2 %3 %4 %5 %6
if ERRORLEVEL 1 exit /B 1

set sASSOC=-define tiff:alpha=associated

set KEEP_ALPH=1

for /L %%N in (%LAST_OCT%,-1,0) do (

rem echo N_BLK_W.%%N=!N_BLK_W.%%N! N_BLK_H.%%N=!N_BLK_H.%%N! AMP.%%N=!AMP.%%N! M1AMP.%%N=!M1AMP.%%N!

rem  if !M1AMP.%%N!==1 (
rem    set sLEVEL=
rem  ) else (
rem    for /F "usebackq" %%L in (`%IMG7%magick identify ^
rem      -format "LO=%%[fx:50*(1-!M1AMP.%%N!)]\nHI=%%[fx:50*(1+!M1AMP.%%N!)]" ^
rem      xc:`) do set %%L
rem
rem    set sLEVEL=+level !LO!%%,!HI!%%
rem  )

set DO_RES=1
if !N_BLK_W.%%N!==%WW% if !N_BLK_H.%%N!==%HH% set DO_RES=0

set WRDIFF=1
if not %%N==0 if not "%mkpDEBUG%"=="1" set WRDIFF=0

(
if %%N==%LAST_OCT% if "%mkpKEEP_ALPHA_RES%"=="1" echo ^( +clone -alpha extract +write mpr:ALPH +delete ^)
if !DO_RES!==1 echo -channel RGB -resize "!N_BLK_W.%%N!x!N_BLK_H.%%N!^!" +channel
if "%mkpDEBUG%"=="1" echo +write %pyPREFIX%grd_%%N%fnNG_EXT%
echo +write mpr:GRD%%N
if !DO_RES!==1 echo -channel RGB -resize "%WW%x%HH%^!" +channel
if "%mkpDEBUG%"=="1" echo +write %pyPREFIX%full_%%N%fnNG_EXT% !sLEVEL.%%N!
echo mpr:!PREV_DIFF!
echo -channel RGB -compose Mathematics -define compose:args=0,1,-1,0.5 -composite +channel
if "%mkpKEEP_ALPHA_RES%"=="1" echo mpr:ALPH -alpha off -compose CopyOpacity -composite
set PREV_DIFF=DIFF%%N
if "!WRDIFF!"=="1" echo +write %pyPREFIX%diff_%%N%fnNG_EXT%
echo +write mpr:!PREV_DIFF!
)>>%MK_SCR%

( if %%N==%LAST_OCT% if "%mkpKEEP_ALPHA_RES%"=="1" echo %PYR_FILE%[%NUM_OCTAVES%] -alpha extract +write mpr:ALPH +delete
echo ^( %PYR_FILE%[%%N]
if !DO_RES!==1 echo -resize "%WW%x%HH%^!"
if "%mkpKEEP_ALPHA_RES%"=="1" echo mpr:ALPH -alpha off -compose CopyOpacity -composite
echo !sLEVEL.%%N! ^)

if !DONE_FIRST!==0 (
set DONE_FIRST=1
) else (
echo -compose Mathematics -define compose:args=0,1,1,-0.5 -composite
)
)>>%RECON_SCR%

if "%mkpHTM%"=="1" (
echo ^<tr^>
echo ^<td^>%%N^</td^>
echo ^<td^>^<img src="%pyPREFIX%grd_%%N%fnNG_EXT%%EXTEXT%" /^>^</td^>
echo ^<td^>^<img src="%pyPREFIX%full_%%N%fnNG_EXT%%EXTEXT%" /^>^</td^>
echo ^<td^>^<img src="%pyPREFIX%full_%%N_h_glc.png" /^>^</td^>
echo ^<td^>^<img src="%pyPREFIX%diff_%%N%fnNG_EXT%%EXTEXT%" /^>^</td^>
echo ^</tr^>
)>>%HTM_TAB%
)

:: List the MPRs, ready to write to pyramid file.
( echo +delete
for /L %%N in (0,1,%LAST_OCT%) do @(
@echo mpr:GRD%%N
)
echo mpr:DIFF0

echo -define quantum:format=floating-point
echo +depth
echo -depth 32
echo %sASSOC%
echo -write %PYR_FILE%
echo -exit

)>>%MK_SCR%

:: Add final difference, and delinear, to reconstruction.
(
echo %PYR_FILE%[%NUM_OCTAVES%]
rem if "%mkpKEEP_ALPHA_RES%"=="1" echo ^( +clone -alpha extract +write mpr:ALPH +delete ^)
echo -compose Mathematics -define compose:args=0,1,1,-0.5 -composite %sDELIN%
rem if "%mkpKEEP_ALPHA_RES%"=="1" echo mpr:ALPH -alpha off -compose CopyOpacity -composite
echo -write %RECON_FILE%
echo -exit
)>>%RECON_SCR%

if "%mkpHTM%"=="1" echo ^</table^> >>%HTM_TAB%

if "%pyIM%"=="" (
set pyIM=%IMG7%
)
if "%pyIM%"=="" (
echo %0: pyIM not set
exit /B 1
)

%pyIM%magick ^
-script %MK_SCR%

if "%mkpHTM%"=="1" (
for /L %%N in (0,1,%LAST_OCT%) do @(
%IM7DEV%magick ^
%pyPREFIX%full_%%N%fnNG_EXT% ^
-process 'mkhisto capnumbuckets 512 norm' ^
%pyPREFIX%full_%%N_h%fnNG_EXT%

call %PICTBAT%graphLineCol %pyPREFIX%full_%%N_h%fnNG_EXT% . . 0 %pyPREFIX%full_%%N_h_glc.png
)
)

call echoRestore

@endlocal & set mklpOUTFILE=%PYR_FILE%& set mklpNUM_OCTAVES=%NUM_OCTAVES%& set mklpWW=%WW%& set mklpHH=%HH%& set mklpRECON_FILE=%RECON_FILE%
```

### mkPyrComm.bat

```rem Common processing for making Laplacian or Gaussian pyramids.
rem %1 is input image
rem %2 is output filename
rem %3 to %6 are block parameters
@rem
@rem Updated:
@rem   5-September-2022 for IM v7.
@rem

if "%pyIM%"=="" set pyIM=%IMG7%

if "%pyPREFIX%"=="" set pyPREFIX=mkp_
if "%pyPYR_EXT%"=="" set pyPYR_EXT=.tiff
if "%fnNG_EXT%"=="" set fnNG_EXT=.miff

if "%fnNG_EXT%"==".miff" (
set EXTEXT=jpg
) else (
set EXTEXT=
)

if not exist %INFILE% (
echo %0: Can't find INFILE [%INFILE%]
exit /B 1
)

set PYR_FILE=%2
if "%PYR_FILE%"=="." set PYR_FILE=
if "%PYR_FILE%"=="" set PYR_FILE=%pyPREFIX%%sioCODE%%pyPYR_EXT%

:: The sioCODE was set by the caller.

set TMP_BAT=%pyPREFIX%%sioCODE%.bat
set MK_SCR=%pyPREFIX%%sioCODE%_mk.scr
set HTM_TAB=%pyPREFIX%%sioCODE%.htm
set RECON_SCR=%pyPREFIX%%sioCODE%_recon.scr
set BLK_LIS=%pyPREFIX%blk.lis
set RECON_FILE=%pyPREFIX%%sioCODE%_recon.miff

for /F "usebackq" %%L in (`%IMG7%magick identify ^
-format "WW=%%w\nHH=%%h" ^
%INFILE%`) do set %%L

set NUM_OCTAVES=

call %PICTBAT%mkpVar %WW% %HH% %3 %4 %5 %6
if ERRORLEVEL 1 (
echo %0: mkpVar failed
exit /B 1
)

rem if not exist %BLK_LIS% (
rem   echo %0: Can't find BLK_LIS [%BLK_LIS%]
rem   exit /B 1
rem )

del %RECON_SCR% 2>nul

rem
rem for /F "tokens=*" %%L in (%BLK_LIS%) do set %%L

if "%NUM_OCTAVES%"=="" (
echo %0: NUM_OCTAVES has no value
exit /B 1
)

if "%WW%"=="%IMG_WW%" if "%HH%"=="%IMG_HH%" set BAD_DIMS=0
echo %0: Dimensions don't match: "%WW%" neq "%IMG_WW%" or "%HH%" neq "%IMG_HH%"
exit /B 1
)

( echo %INFILE%
echo +write mpr:SRC %sLIN%
) >%MK_SCR%

if "%mkpHTM%"=="1" (
echo ^<table^>
echo ^<th^>Octave^</th^>^<th^>Grid^</th^>^<th^>Grid, resized up^</th^>^<th^>Histogram of resized grid^</th^>^<th^>Difference^</th^>
)>%HTM_TAB%

set /A LAST_OCT=%NUM_OCTAVES%-1

set DONE_FIRST=0

set PREV_DIFF=SRC

exit /B 0```

### mkpVar.bat

```rem Makes many variable assignments, for making pyramids.
rem Optionally also creates file of variable assignments.

@rem %1 width in pixels. [600]
@rem %2 height in pixels. [400]
@rem %3 minimum block size in pixels. [1]
@rem %4 maximum block size in pixels. [min(w,h)/2]
@rem %5 block factor: Block size is increased by this factor from min to max. >=1 [2]
@rem %6 power factor for amplitude proportional to frequency. [Default 0.0]
@rem   0 for all frequencies have same amplitude;
@rem   > 0 (eg 1, 1.5) for highest frequencies have lowest amplitude.

@rem returns fnNUM_OCTAVES number of octaves.

@rem Also uses:
@rem
@rem   pyIM location of ImageMagick programs. [%IMG7%]
@rem   pyPREFIX prefix for working files (but not output file). [py_]
@rem   pyGRAPHIC if 1, normalises amplitude so sum or maximum is 0.5.
@rem     [Otherwise normalises so sum or maximum is 1.0.]
@rem   pyWEIGHTING scaling for weights:
@rem     sum: scale so sum is 1.0 or 0.5 (default)
@rem     max: scale so maximum is 1.0 or 0.5
@rem   pyLINEAR if 1, operates in linear colorspace
@rem   pyWR_VAR if 1, writes {variable}={value} lines to %BLK_LIS%
@rem   fnNG_EXT extension for noise grid files. [.miff]
@rem   pyTXT_OUT if 1, writes some text data.
@rem   sioCODE of the caller (eg mklp, mkgp)
@rem
@rem First release: 3 October 2015.
@rem Updated:
@rem   5-September-2022 for IM v7.
@rem
@rem

@rem TODO: power defined by clut image file, so we can emphasise middle frequencies.

@rem No compulsory parameters
@rem if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

rem @setlocal enabledelayedexpansion

rem call %PICTBAT%setInOut %1 mkpv

set WW=%1
if "%WW%"=="." set WW=
if "%WW%"=="" set WW=600

set HH=%2
if "%HH%"=="." set HH=
if "%HH%"=="" set HH=400

set BLK_MIN=%3
if "%BLK_MIN%"=="." set BLK_MIN=
if "%BLK_MIN%"=="" set BLK_MIN=1

set BLK_MAX=%4
if "%BLK_MAX%"=="." set BLK_MAX=
if "%BLK_MAX%"=="" (
set BLK_MAX=%HH%
if !BLK_MAX! GTR %WW% set BLK_MAX=%WW%
set /A BLK_MAX=!BLK_MAX!/2
)

set BLK_FAC=%5
if "%BLK_FAC%"=="." set BLK_FAC=
if "%BLK_FAC%"=="" set BLK_FAC=2

set POW_FAC=%6
if "%POW_FAC%"=="." set POW_FAC=
if "%POW_FAC%"=="" set POW_FAC=0

if "%pyPREFIX%"=="" set pyPREFIX=py_
if "%fnNG_EXT%"=="" set fnNG_EXT=.miff

if "%BLK_LIS%"=="" set BLK_LIS=%pyPREFIX%blk.lis
del %BLK_LIS% 2>nul

if "%pyIM%"=="" set pyIM=%IMG7%

if "%pyWEIGHTING%"=="" set pyWEIGHTING=sum

if not "%pyWEIGHTING%"=="sum" if not "%pyWEIGHTING%"=="max" (
echo %0: pyWEIGHTING [%pyWEIGHTING%] should be sum or max.
exit /B 1
)

if "%pyLINEAR%"=="1" (
set sLIN=-set colorspace sRGB -colorspace RGB
set sDELIN=-set colorspace RGB -colorspace sRGB
) else (
set sLIN=
set sDELIN=
)

set PREC=-precision 15

set CONST_FAC=%fnCONST_FAC%
if "%CONST_FAC%"=="" set CONST_FAC=1

:: ------------------ Pre-loop ------------------
if "%pyMAX_NUM_OCT%"=="" (
set /A LOOP_MAX=99
) else (
set /A LOOP_MAX=%pyMAX_NUM_OCT%-1
)

set BLK_SZ=%BLK_MIN%
set NUM_OCTAVES=0
set MAX_AMP=0
set SIG_AMP=0
set BUST_BLK_WH=0

:preloop

set BLK_SZ.%NUM_OCTAVES%=%BLK_SZ%

for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "FR.%NUM_OCTAVES%=%%[fx:sqrt(%BLK_SZ%*%BLK_SZ%/%WW%/%HH%)]" ^
xc:`) do set %%L

for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "N_BLK_W.%NUM_OCTAVES%=%%[fx:int^(%WW%/!BLK_SZ.%NUM_OCTAVES%!+0.5^)]\nN_BLK_H.%NUM_OCTAVES%=%%[fx:int^(%HH%/!BLK_SZ.%NUM_OCTAVES%!+0.5^)]" ^
xc:`) do set %%L

if "!N_BLK_W.%NUM_OCTAVES%!"=="0" set N_BLK_W=1
if "!N_BLK_H.%NUM_OCTAVES%!"=="0" set N_BLK_H=1

if not "%pyMIN_BLK_WH%"=="" (
if /I !N_BLK_W.%NUM_OCTAVES%! LSS %pyMIN_BLK_WH% set BUST_BLK_WH=1
if /I !N_BLK_H.%NUM_OCTAVES%! LSS %pyMIN_BLK_WH% set BUST_BLK_WH=1
)

for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "AMP.%NUM_OCTAVES%=%%[fx:pow(!FR.%NUM_OCTAVES%!,%POW_FAC%)]\nBLK_SZ=%%[fx:%BLK_SZ%*%BLK_FAC%]" ^
xc:`) do set %%L

for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "SIG_AMP=%%[fx:%SIG_AMP%+!AMP.%NUM_OCTAVES%!]\nFIN=%%[fx:%BLK_SZ%>=%BLK_MAX%?1:%NUM_OCTAVES%>=%LOOP_MAX%?1:!BUST_BLK_WH!]\nMAX_AMP=%%[fx:%MAX_AMP%>!AMP.%NUM_OCTAVES%!?%MAX_AMP%:!AMP.%NUM_OCTAVES%!]" ^
xc:`) do set %%L

if "%pyTXT_OUT%"=="1" echo BLK_SZ.%NUM_OCTAVES%=!BLK_SZ.%NUM_OCTAVES%! FR.%NUM_OCTAVES%=!FR.%NUM_OCTAVES%!  AMP.%NUM_OCTAVES%=!AMP.%NUM_OCTAVES%!

set /A NUM_OCTAVES+=1

if %FIN%==0 goto preloop
:: ------------------ End Pre-loop ------------------

if "%BUST_BLK_WH%"=="1" set /A NUM_OCTAVES-=1

if %NUM_OCTAVES%==0 (
echo %0: No octaves created
exit /B 1
)

rem if %NUM_OCTAVES% GEQ %LOOP_MAX% (
rem   echo %0: Large number of octaves
rem   exit /B 1
rem )

set MAX_FR=%FR%

if "%pyTXT_OUT%"=="1" echo NUM_OCTAVES=%NUM_OCTAVES%  MAX_FR=%MAX_FR%  SIG_AMP=%SIG_AMP% MAX_AMP=%MAX_AMP%

if "%pyGRAPHIC%"=="1" for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "MAX_AMP=%%[fx:%MAX_AMP%*2]\nSIG_AMP=%%[fx:%SIG_AMP%*2]" ^
xc:`) do set %%L

if "%pyWR_VAR%"=="1" (
echo %0: Writing to var file [%BLK_LIS%]
)

if "%pyWR_VAR%"=="1" (
echo pyWEIGHTING=%pyWEIGHTING%
echo pyGRAPHIC=%pyGRAPHIC%
echo pyLINEAR=%pyLINEAR%
echo NUM_OCTAVES=%NUM_OCTAVES%
echo %sioCODE%RECON_FILE=%RECON_FILE%
echo IMG_WW=%WW%
echo IMG_HH=%HH%
)>>%BLK_LIS%

if "%pyWR_VAR%"=="1" type %BLK_LIS%

set IMG_WW=%WW%
set IMG_HH=%HH%

:: ------------------ Main loop ------------------
set /A Nm1=%NUM_OCTAVES%-1

set ERROR=0

if "%SIG_AMP%"=="0" (
echo %0: Error: SIG_AMP is zero.
exit /B 1
)

for /L %%N in (0,1,%Nm1%) do (

set OCT_NUM=%%N

rem  for /F "usebackq" %%L in (`%IMG7%magick identify ^
rem    %PREC% ^
rem    -format "N_BLK_W.%%N=%%[fx:int^(%WW%/!BLK_SZ.%%N!+0.5^)]\nN_BLK_H.%%N=%%[fx:int^(%HH%/!BLK_SZ.%%N!+0.5^)]" ^
rem    xc:`) do set %%L

for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "S1AMP.%%N=%%[fx:%CONST_FAC%/%SIG_AMP%*!AMP.%%N!]\nM1AMP.%%N=%%[fx:%CONST_FAC%/%MAX_AMP%*!AMP.%%N!]" ^
xc:`) do set %%L

if "%pyTXT_OUT%"=="1" echo BLK_SZ.%%N=!BLK_SZ.%%N!  N_BLK_W.%%N=!N_BLK_W.%%N! N_BLK_H.%%N=!N_BLK_H.%%N!  FR.%%N=!FR.%%N!  S1AMP.%%N=!S1AMP.%%N! M1AMP.%%N=!M1AMP.%%N!

if "%pyWEIGHTING%"=="sum" (
set AMP.%%N=!S1AMP.%%N!
) else (
set AMP.%%N=!M1AMP.%%N!
)

if !AMP.%%N!==1 (
set sLEVEL.%%N=
) else (
for /F "usebackq" %%L in (`%IMG7%magick identify ^
%PREC% ^
-format "LO=%%[fx:50*^(1-!AMP.%%N!^)]\nHI=%%[fx:50*^(1+!AMP.%%N!^)]" ^
xc:`) do set %%L

set sLEVEL.%%N=+level !LO!%%,!HI!%%
)

if "%pyWR_VAR%"=="1" (
echo BLK_SZ.!OCT_NUM!=!BLK_SZ.%%N!
echo N_BLK_W.!OCT_NUM!=!N_BLK_W.%%N!
echo N_BLK_H.!OCT_NUM!=!N_BLK_H.%%N!
echo FR.!OCT_NUM!=!FR.%%N!
echo S1AMP.!OCT_NUM!=!S1AMP.%%N!
echo M1AMP.!OCT_NUM!=!M1AMP.%%N!
echo AMP.!OCT_NUM!=!AMP.%%N!
echo sLEVEL.!OCT_NUM!=!sLEVEL.%%N!
)>>%BLK_LIS%

rem  for /F "usebackq" %%L in (`%IMG7%magick identify ^
rem    %PREC% ^
rem    -format "BLK_SZ=%%[fx:!BLK_SZ!*%BLK_FAC%]" ^
rem    xc:`) do set %%L
)

rem @endlocal & set fnWW=%WW%& set fnHH=%HH%& set fnNUM_OCTAVES=%NUM_OCTAVES%& set fnMAX_FR=%MAX_FR%& set fnPOW_FAC=%POW_FAC%```

### mspBoostCut.bat

```rem Boosts or cuts amplitudes of grids in a Laplacian pyramid.
rem %1 is the name of the multi-image pyramid file.
rem %2 is "boost" or "cut"
rem %3 is the -sigmoidal-contrast contrast boost or cut to be applied.
rem %4 is the name of the htm file to be generated.
@rem
@rem  Assumes pyPREFIX is correct,
@rem  so we can read the block list file (which must exist),
@rem  and run the reconstruction script.
@rem
@rem Updated:
@rem   15 May 2016 use %IML% for @script.
@rem   5-September-2022 for IM v7.
@rem

@if "%4"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 mbc

set PYR_FILE=%1
set BOOST_CUT=%2
set CONT=%3
set HTM_FILE=%4
set HTM_NAME=%~n4
set SAVED_PYR=%~n1_mbpsav%~x1

if /I "%BOOST_CUT%"=="boost" (
set SIG_SIGN=-
) else if /I "%BOOST_CUT%"=="cut" (
set SIG_SIGN=+
) else (
echo %0: BOOST_CUT [%BOOST_CUT%] is neither boost nor cut
exit /B 1
)

echo SAVED_PYR=%SAVED_PYR%

if not exist %PYR_FILE% (
echo %0: Can't find PYR_FILE [%PYR_FILE%]
exit /B 1
)

set RECON_SCR=%pyPREFIX%mklp_recon.scr
set BLK_LIS=%pyPREFIX%blk.lis

if not exist %BLK_LIS% (
echo %0: Can't find BLK_LIS [%BLK_LIS%]
exit /B 1
)

set NUM_OCTAVES=

for /F "tokens=*" %%L in (%BLK_LIS%) do set %%L

if "%NUM_OCTAVES%"=="" (
echo %0: in %BLK_LIS%, NUM_OCTAVES has no value
exit /B 1
)

if not exist %RECON_SCR% (
echo %0: Can't find RECON_SCR [%RECON_SCR%]
exit /B 1
)

if "%mklpRECON_FILE%"=="" (
echo %0: mklpRECON_FILE is blank.
exit /B 1
)

copy /Y %PYR_FILE% %SAVED_PYR%

( echo ^<p^>%~n0: %BOOST_CUT% %CONT%^</p^>
) >%HTM_FILE%

:: Don't process final difference.

for /L %%N in (0,1,%NUM_OCTAVES%) do (

set sOUT=%pyPREFIX%mbc_%HTM_NAME%_%%N.png

%IMG7%magick ^
%PYR_FILE% ^
^( -clone %%N ^
%SIG_SIGN%sigmoidal-contrast %CONT%,50%% ^
^) ^
-swap -1,%%N ^
+delete ^
%PYR_FILE%

%IMG7%magick ^
-script %RECON_SCR%

%IMG7%magick %mklpRECON_FILE% !sOUT!

( echo ^<img src="!sOUT!jpg" /^>
) >>%HTM_FILE%

)

copy /Y %SAVED_PYR% %PYR_FILE%

type %HTM_FILE%

call echoRestore

@endlocal & set mklpOUTFILE=%PYR_FILE%```

### mspProcGrid.bat

```rem Applies an IM process to grids in a Laplacian pyramid.
rem %1 is the name of the multi-image pyramid file.
rem %2 is the IM process to be applied to each grid.
rem %3 is the name of the htm file to be generated.
@rem
@rem  Assumes pyPREFIX is correct,
@rem  so we can read the block list file,
@rem  and run the reconstruction script.
@rem
@rem Updated:
@rem   5-September-2022 for IM v7.
@rem

@if "%3"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 mpg

set PYR_FILE=%1
set IM_PROC=%~2
set HTM_FILE=%3
set HTM_NAME=%~n3
set SAVED_PYR=%~n1_mbpsav%~x1

echo %~n0: IM_PROC="%IM_PROC%"

if not exist %PYR_FILE% (
echo %0: Can't find PYR_FILE [%PYR_FILE%]
exit /B 1
)

set RECON_SCR=%pyPREFIX%mklp_recon.scr
set BLK_LIS=%pyPREFIX%blk.lis

if not exist %BLK_LIS% (
echo %0: Can't find BLK_LIS [%BLK_LIS%]
exit /B 1
)

set NUM_OCTAVES=

for /F "tokens=*" %%L in (%BLK_LIS%) do set %%L

if "%NUM_OCTAVES%"=="" (
echo %0: in %BLK_LIS%, NUM_OCTAVES has no value
exit /B 1
)

if not exist %RECON_SCR% (
echo %0: Can't find RECON_SCR [%RECON_SCR%]
exit /B 1
)

if "%mklpRECON_FILE%"=="" (
echo %0: mklpRECON_FILE is blank.
exit /B 1
)
copy /Y %PYR_FILE% %SAVED_PYR%

( echo ^<p^>%~n0: "%IM_PROC%"^</p^>
) >%HTM_FILE%

:: Don't process final difference.

for /L %%N in (0,1,%NUM_OCTAVES%) do (

set sOUT=%pyPREFIX%mbc_%HTM_NAME%_%%N.png

%IMG7%magick ^
%PYR_FILE% ^
^( -clone %%N ^
%IM_PROC% ^
^) ^
-swap -1,%%N ^
+delete ^
%PYR_FILE%

%IMG7%magick ^
-script %RECON_SCR%

%IMG7%magick %mklpRECON_FILE% !sOUT!

( echo ^<img src="!sOUT!jpg" /^>
) >>%HTM_FILE%

)

copy /Y %SAVED_PYR% %PYR_FILE%

call echoRestore

@endlocal & set mklpOUTFILE=%PYR_FILE%```

All images on this page were created by the commands shown, using:

`%IMG7%magick -version`
```Version: ImageMagick 7.1.1-20 Q16-HDRI x86 98bb1d4:20231008 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
Features: Cipher DPC HDRI OpenCL OpenMP(2.0)
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193532217)```
`%IM7DEV%magick -version`
```Version: ImageMagick 7.1.1-27 (Beta) Q32-HDRI x86_64 83eefaf2a:20240107 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
Features: Cipher DPC HDRI Modules OpenCL OpenMP(4.5)
Delegates (built-in): bzlib cairo fftw fontconfig freetype heic jbig jng jpeg lcms ltdl lzma pangocairo png raqm raw rsvg tiff webp wmf x xml zip zlib zstd
Compiler: gcc (11.3)```

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF to JPG.

Source file for this web page is mspyr.h1. To re-create this web page, run "procH1 mspyr".

This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.