snibgo's ImageMagick pages

Colour cycling

Cycling colours with different methods gives results that satisfy certain criteria.

For cyclic animations of images of fractal noise, or images that have been distorted by fractal noise (see Fractal noise animations), I needed an ImageMagick process that would take a noise image and a percentage. At slightly different percentages it would create slightly different noise images, but 100% would give the same result as 0% (making it cyclic).

The problem

For convenience, this page considers just one channel, so the examples are grayscale. The same methods can be applied to three channels independently.

We have a grayscale image with values that are distributed evenly from black to white, and the mean is 50%. All values are equally likely. We want to add a value X% (0 <= X <= 100) to every pixel, to create a new image. How can we do this? There is an obvious method: "-evaluate AddModulus" does the job. This is quick and easy. If we add a small value, such as 1%, most pixels will change a small amount. Unfortunately, some pixels will change a large amount; pixels that were slightly beneath white will become slightly above black. This may be undesirable.

So we would like a number of criteria to be satisfied.

Other criteria may be desirable, for example that the standard deviation should be constant.

I don't think any method satisfies all criteria. In particular, I don't think an animated technique can have no sudden changes while also being reversible.

(For some purposes, strict reversibility is not important. Provided we can find some input that gives the desired output, we don't care if other inputs can also give that same output.)

The methods

Create a simple image that will be animated by colour-cycling, using each method.

del ccy_*.miff

set SRC=ccy_src.png

%IMG7%magick ^
  -seed 1234 ^
  -size 200x200 ^
  ( xc: +noise Random ^
    -channel R -separate +channel ) ^
  gradient: ^
  -append +repage ^
  %SRC%
ccy_src.png

For each method, we can create an animation. A loop is run with %%i as the loop counter. Environment variable LZ is set to this value with six digits and leading zeros. (I always use six digits for frame filenames, although six is overkill here.) magick creates each frame file. When the loop is finished, another magick reads all the frame files and creates an animated GIF, which is shown.

The top half of the animation shows what the method does to a noise image. The bottom half shows what the method does to a gradient image, which helps to explain the process.

Each method also calls the script ccy_stats.bat, which calculates some statistics for that method.

The statistics could be collected for every pair of adjacent frames.

Commands here use Windows cmd arithmetic, which assumes all numbers are integers. For real use, we should use floating-point arithmetic.

We will generate frames from 0 to 99, but skip even-numbered frames to save time and space.

set FOR_ARGS=0,2,99
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  %IMG7%magick ^
    %SRC% ^
    -evaluate AddModulus %%i%% ^
    ccy_am_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_am_*.miff ^
  ccy_am.gif

call %PICTBAT%ccy_stats am
0
zeroAE=0 (0)
zeroRMSE=80000
ch20=64241 (0.980255)
pae1820=2575.29 (0.0392964)
mae1820=
mean20=0.49982
ccy_am.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=2*%%i

  %IMG7%magick ^
    %SRC% ^
    -evaluate AddModulus !PC!%% ^
    ccy_am2_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_am2_*.miff ^
  ccy_am2.gif

call %PICTBAT%ccy_stats am2
0
zeroAE=0 (0)
zeroRMSE=80000
ch20=62930 (0.960251)
pae1820=5116.73 (0.0780762)
mae1820=
mean20=0.498832
ccy_am2.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  %IMG7%magick ^
    %SRC% ^
    -evaluate AddModulus %%i%% ^
    -solarize 50%% ^
    -evaluate Multiply 2 ^
    ccy_amsol_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amsol_*.miff ^
  ccy_amsol.gif

call %PICTBAT%ccy_stats amsol
79800
zeroAE=26738.8 (0.408008)
zeroRMSE=79999
ch20=2639.96 (0.0402832)
pae1820=2568.86 (0.0391982)
mae1820=
mean20=0.499046
ccy_amsol.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=%%i
  set /A NEGPC=100-!PC!

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    ccy_amneg_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amneg_*.miff ^
  ccy_amneg.gif

call %PICTBAT%ccy_stats amneg
0
zeroAE=0 (0)
zeroRMSE=80000
ch20=1327.98 (0.0202637)
pae1820=1297.5 (0.0197986)
mae1820=
mean20=0.659339
ccy_amneg.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=%%i
  set /A NEGPC=100-!PC!

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    -auto-level ^
    -auto-gamma ^
    ccy_amnegaa_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amnegaa_*.miff ^
  ccy_amnegaa.gif

call %PICTBAT%ccy_stats amnegaa
79167
zeroAE=29.3111 (0.000447259)
zeroRMSE=79763
ch20=2559.96 (0.0390625)
pae1820=957.202 (0.014606)
mae1820=
mean20=0.524423
ccy_amnegaa.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=2*%%i
  set /A NEGPC=100-!PC!

  if /I !NEGPC! LEQ 0 (
    set /A NEGPC=-!NEGPC!
    set /A PC=200-!PC!
  )

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    ccy_amneg2_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amneg2_*.miff ^
  ccy_amneg2.gif

call %PICTBAT%ccy_stats amneg2
0
zeroAE=0 (0)
zeroRMSE=80000
ch20=2639.96 (0.0402832)
pae1820=2567.78 (0.0391817)
mae1820=
mean20=0.739364
ccy_amneg2.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=2*%%i
  set /A NEGPC=100-!PC!

  if /I !NEGPC! LEQ 0 (
    set /A NEGPC=-!NEGPC!
    set /A PC=200-!PC!
  )

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    -auto-level ^
    -auto-gamma ^
    ccy_amneg2aa_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amneg2aa_*.miff ^
  ccy_amneg2aa.gif

call %PICTBAT%ccy_stats amneg2aa
79167
zeroAE=29.3111 (0.000447259)
zeroRMSE=79782
ch20=5839.91 (0.0891113)
pae1820=3832.15 (0.0584748)
mae1820=
mean20=0.516788
ccy_amneg2aa.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=2*%%i
  set /A NEGPC=100-!PC!

  if /I !NEGPC! LEQ 0 (
    set /A NEGPC=!NEGPC!+100
    set sNEG=-negate
  ) else (
    set sNEG=
  )

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^
       !sNEG! ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    ccy_amneg3_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amneg3_*.miff ^
  ccy_amneg3.gif

call %PICTBAT%ccy_stats amneg3
0
zeroAE=0 (0)
zeroRMSE=80000
ch20=2639.96 (0.0402832)
pae1820=2567.78 (0.0391817)
mae1820=
mean20=0.739364
ccy_amneg3.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=200-2*%%i
  set /A NEGPC=100-!PC!

  if /I !NEGPC! LSS 0 (
    set /A NEGPC=!NEGPC!+100
    set sNEG=-negate
  ) else (
    set sNEG=
  )

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^
       !sNEG! ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    ccy_amneg3n_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amneg3n_*.miff ^
  ccy_amneg3n.gif

call %PICTBAT%ccy_stats amneg3n
200
zeroAE=3276.75 (0.05)
zeroRMSE=80000
ch20=2639.96 (0.0402832)
pae1820=2569.81 (0.0392128)
mae1820=
mean20=0.260846
ccy_amneg3n.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A PC=2*%%i
  set /A NEGPC=100-!PC!

  if /I !NEGPC! LEQ 0 (
    set /A NEGPC=!NEGPC!+100
    set sNEG=-negate
  ) else (
    set sNEG=
  )

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 -evaluate AddModulus !PC!%% ^
       !sNEG! ^) ^
    ^( -clone 1 -negate ^) ^
    ^( -clone 0 -threshold !NEGPC!%% ^) ^
    -delete 0 ^
    -composite ^
    -auto-level ^
    -auto-gamma ^
    ccy_amneg3aa_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_amneg3aa_*.miff ^
  ccy_amneg3aa.gif

call %PICTBAT%ccy_stats amneg3aa
79167
zeroAE=29.3111 (0.000447259)
zeroRMSE=79782
ch20=5839.91 (0.0891113)
pae1820=3832.15 (0.0584748)
mae1820=
mean20=0.516788
ccy_amneg3aa.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=%%i*36/10-90

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 1,!DEG!,0.5,0.5 ^
    ccy_sin_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sin_*.miff ^
  ccy_sin.gif

call %PICTBAT%ccy_stats sin
79799
zeroAE=29880.3 (0.455943)
zeroRMSE=79999
ch20=4591.93 (0.0700684)
pae1820=2917.94 (0.0445249)
mae1820=
mean20=0.498773
ccy_sin.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=%%i*36/10-90

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 0.5,!DEG!,0.5,0.5 ^
    ccy_sin2_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sin2_*.miff ^
  ccy_sin2.gif

call %PICTBAT%ccy_stats sin2
79598
zeroAE=4942.03 (0.0754106)
zeroRMSE=79999
ch20=4591.93 (0.0700684)
pae1820=2911.31 (0.0444238)
mae1820=
mean20=0.801687
ccy_sin2.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=%%i*36/10-90

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 0.5,!DEG!,0.5,0.5 ^
    -auto-level ^
    -auto-gamma ^
    ccy_sin2aa_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sin2aa_*.miff ^
  ccy_sin2aa.gif

call %PICTBAT%ccy_stats sin2aa
79599
zeroAE=4936.99 (0.0753336)
zeroRMSE=79748
ch20=9759.85 (0.148926)
pae1820=4411.45 (0.0673144)
mae1820=
mean20=0.572583
ccy_sin2aa.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=%%i*36/10-90

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 1,!DEG!,0.5,0.5 ^
    -function arcsin 1 ^
    ccy_sinarc_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sinarc_*.miff ^
  ccy_sinarc.gif

call %PICTBAT%ccy_stats sinarc
79800
zeroAE=26738.8 (0.408008)
zeroRMSE=79999
ch20=2943.96 (0.0449219)
pae1820=2848.1 (0.0434592)
mae1820=
mean20=0.499059
ccy_sinarc.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=%%i*36/10-90

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 0.5,!DEG!,0.5,0.5 ^
    -function arcsin 1 ^
    ccy_sinarc2_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sinarc2_*.miff ^
  ccy_sinarc2.gif

call %PICTBAT%ccy_stats sinarc2
69494
zeroAE=6.98167 (0.000106533)
zeroRMSE=79999
ch20=2943.96 (0.0449219)
pae1820=2846.61 (0.0434365)
mae1820=
mean20=0.739352
ccy_sinarc2.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=270-%%i*36/10

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 0.5,!DEG!,0.5,0.5 ^
    -function arcsin 1 ^
    ccy_sinarc2n_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sinarc2n_*.miff ^
  ccy_sinarc2n.gif

call %PICTBAT%ccy_stats sinarc2n
69494
zeroAE=6.98167 (0.000106533)
zeroRMSE=80000
ch20=2928.5 (0.044686)
pae1820=2849.02 (0.0434732)
mae1820=
mean20=0.260854
ccy_sinarc2n.gif

Combine sinarc2 and sinarc2n,
masked over using least-significant bit.

for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG1=%%i*36/10-90
  set /A DEG2=270-%%i*36/10

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 ^
       -function sinusoid 0.5,!DEG1!,0.5,0.5 ^
       -function arcsin 1 ^
    ^) ^
    ^( -clone 0 ^
       -function sinusoid 0.5,!DEG2!,0.5,0.5 ^
       -function arcsin 1 ^
    ^) ^
    ^( -clone 0 ^
       -evaluate And 1 -fill White +opaque Black ^
    ^) ^
    -delete 0 ^
    -composite ^
    ccy_sinarc2o_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sinarc2o_*.miff ^
  ccy_sinarc2o.gif

call %PICTBAT%ccy_stats sinarc2o
69494
zeroAE=6.98167 (0.000106533)
zeroRMSE=79998
ch20=2943.96 (0.0449219)
pae1820=2857.07 (0.043596)
mae1820=
mean20=0.50119

The changes in the noisy half are smooth,
with no overall change in brightness.

The gradient part has only 200 values,
so the least-significant bit has a beat frequency.

ccy_sinarc2o.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  set /A DEG=%%i*36/10-90

  %IMG7%magick ^
    %SRC% ^
    -function sinusoid 0.5,!DEG!,0.5,0.5 ^
    -function arcsin 1 ^
    -auto-level ^
    -auto-gamma ^
    ccy_sinarc2aa_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_sinarc2aa_*.miff ^
  ccy_sinarc2aa.gif

call %PICTBAT%ccy_stats sinarc2aa
79167
zeroAE=29.3111 (0.000447259)
zeroRMSE=79786
ch20=6495.9 (0.0991211)
pae1820=4221.02 (0.0644086)
mae1820=
mean20=0.516784
ccy_sinarc2aa.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  if %%i LSS 50 (
    set /A PC=2*%%i
  ) else (
    set /A PC=200-2*%%i
  )
  set /A PC2=100-!PC!

  %IMG7%magick ^
    %SRC% ^
    +level !PC!%%,!PC2!%% ^
    ccy_lev1_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_lev1_*.miff ^
  ccy_lev1.gif

call %PICTBAT%ccy_stats lev1
0
zeroAE=0 (0)
zeroRMSE=79998
ch20=2623.96 (0.0400391)
pae1820=1315.05 (0.0200664)
mae1820=
mean20=0.499889
ccy_lev1.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  if %%i LSS 50 (
    set /A PCL=%%i
    set /A PCH=%%i*3
    set /A PCH2=100-%%i
  ) else (
    set /A PCL=100-%%i
    set /A PCH=300-%%i*3
    set /A PCH2=%%i
  )
  set /A PCL2=100-!PCL!*3

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 ^
       +level !PCL!%%,!PCL2!%% ^
    ^) ^
    ^( -clone 0 ^
       +level !PCH!%%,!PCH2!%% ^
    ^) ^
    ^( -clone 0 ^
       -threshold 50%% ^
    ^) ^
    -delete 0 ^
    -composite ^
    ccy_lev2_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_lev2_*.miff ^
  ccy_lev2.gif

call %PICTBAT%ccy_stats lev2
0
zeroAE=0 (0)
zeroRMSE=79999
ch20=1311.98 (0.0200195)
pae1820=656.346 (0.0100152)
mae1820=
mean20=0.500259
ccy_lev2.gif
for /L %%i in (%FOR_ARGS%) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  if %%i LSS 50 (
    set /A PCL=%%i*2
    set /A PCL2=100-%%i*4
    set /A PCH=%%i*4
    set /A PCH2=100-%%i*2
  ) else (
    set /A PCL=200-%%i*2
    set /A PCL2=%%i*4-300
    set /A PCH=400-%%i*4
    set /A PCH2=%%i*2-100
  )

  %IMG7%magick ^
    %SRC% ^
    ^( -clone 0 ^
       +level !PCL!%%,!PCL2!%% ^
    ^) ^
    ^( -clone 0 ^
       +level !PCH!%%,!PCH2!%% ^
    ^) ^
    ^( -clone 0 ^
       -threshold 50%% ^
    ^) ^
    -delete 0 ^
    -composite ^
    -auto-level ^
    ccy_lev3_!LZ!.miff
)
%IMG7%magick ^
  -loop 0 ^
  ccy_lev3_*.miff ^
  ccy_lev3.gif

call %PICTBAT%ccy_stats lev3
0
zeroAE=0 (0)
zeroRMSE=79999
ch20=9119.86 (0.13916)
pae1820=4566.01 (0.0696728)
mae1820=
mean20=0.501216
ccy_lev3.gif

Some animations have a single frame with black in the bottom row when it should be white. This arises because "-threshold N%" turns only values above N% to white, so "-threshold 0%" turns everything white, except black which remains black. A fix is to subtract a small value from N. This is messy is a BAT script, so isn't fixed the simple demonstrations above. However, it is fixed in colCyclStr.bat.

Results

How well do the methods satisfy the criteria?

The null method is not shown above, but is provided in the script colCyclStr.bat; it returns an empty string so there is no colour change.

Method Criteria
No change
at zero
Non-zero
changes all
No sudden
changes
Pixels change
equally
Mean 50%
stays 50%
Reversible
null pass fail pass (no changes at all) pass
am pass pass fail pass pass pass
am2 pass pass fail pass pass pass
amsol fail pass pass pass pass fail
amneg pass pass fail pass fail fail
amnegaa pass pass fail pass pass fail
amneg2 pass pass pass pass fail fail
amneg2aa pass pass pass pass pass fail
amneg3 pass pass pass pass fail fail
amneg3n pass pass pass pass fail fail
amneg3aa pass pass pass pass pass fail
sin fail pass pass fail pass fail
sin2 fail pass pass pass fail fail
sin2aa fail pass pass pass slight fail fail
sinarc fail pass pass pass pass fail
sinarc2 pass pass pass pass fail fail
sinarc2n pass pass pass pass fail fail
sinarc2o pass pass pass pass pass fail
sinarc2aa pass pass pass pass pass fail
lev1 pass pass pass fail pass pass
lev2 pass pass pass fail pass pass
lev2 pass pass pass fail pass pass

Discussion

Where a method fails the "mean 50% stays 50%" test and the image cycles through dark and light, I provide an "aa" version of the same method. This adds "-auto-level -auto-gamma" to the method, which tends to keep the mean at 50% but sometimes gives a visual appearance of accelerating and decelerating the change of colour.

The "n" suffix denotes a reversal of the method without the "n". I have provided just two of these: amneg3n and sinarc2n.

Each method has strengths and weaknesses. Leaving aside the "Reversible" criteria (because I don't think it is important), methods amneg3aa, sinarc2aa and sinarc2o pass all the tests. However, the "aa" versions suffer the acceleration/deceleration appearance, and sinarc2o relies on the least significant bit being noisy. When the LSB is not noisy, the result has artefacts.

Method sinarc2o uses the least significant bit of the input image as a mask to choose one of two transformations (sinarc2 and sinarc2n) that both have "no change at zero" but have opposite mean-shifts. The resulting mean is kept constant, without the problems introduced by -auto-level -auto-gamma.

amsol and sinarc are both useful but fail the "no change at zero" test.

The methods that have "sudden changes" may have little practical use.

The goal is for uniformally distributed noise to be handled well, and to remain uniform. IM's +noise Random creates such noise. Other noise types are not uniform and cycling changes the distribution, causing visible pulsing at each cycle. See script nseCycl.bat (results not shown here). For non-uniform noise, method sinarc2o may be the most acceptable, but a better solution is to use +noise Random, apply a chosen method, then apply a clut that changes the distribution from uniform to Gaussian or whatever is desired (perhaps using the method shown in Process modules: matching histograms).

Practical use

If one of the methods is to be used, the appropriate code can be copied. If a choice of methods is to be provided, the script colCyclStr.bat can be called with a method name and percentage (which need not be an integer). It returns appropriate IM code in the variable ccsImStr, and this value can be used within a magick command.

For example:

call %PICTBAT%colCyclStr amneg 25.6

echo %ccsImStr% 
( -clone 0 -evaluate AddModulus 25.6%% )   ( -clone 1 -negate )   ( -clone 0 -threshold 74.4%% )   -delete 0 -compose Over -composite 

To get a list of method names available from colCyclStr, call the script colCyclList.bat:

call %PICTBAT%colCyclList 
null
am
am2
amsol
amneg
amnegaa
amneg2
amneg2aa
amneg3
amneg3n
amneg3aa
sin
sin2
sin2aa
sinarc
sinarc2
sinarc2n
sinarc2o
sinarc2aa
lev1
lev2
lev3

Colour cycle tests

The script colCyclTst.bat uses the above scripts to make a GIF animation from each method of a given input image. The input image can be colour or grayscale. To keep the GIFs small, I use the built-in "rose:".

call %PICTBAT%colCyclTst rose:
call procH1 cct_gifs

The script also builds HTML code for a web page that shows each GIF: Colour cycle tests.

Grayscale versus percent display

Each animation above shows a 2D (two-dimensional) image, transformed by a method with a different percentage to each frame. Instead, we can make a 1D image, transform it into other 1D images, and concatenate (append) them in the other dimension.

Make a 1D gradient.

set SRC1D=ccy_1d_src.png

%IMG7%magick ^
  -size 1x256 gradient: ^
  %SRC1D%
ccy_1d_src.png

Show the sinarc2aa method.

echo off
for /L %%i in (0,1,255) do (

  set LZ=000000%%i
  set LZ=!LZ:~-6!

  for /F "usebackq" %%L in (`%IMG7%magick identify ^
    -format "PC=%%[fx:100*%%i/256]" ^
    xc:`) do set %%L

  call %PICTBAT%colCyclStr sinarc2aa !PC!

  %IMG7%magick ^
    %SRC1D% ^
    !ccsImStr! ^
    ccy_1dsa_!LZ!.miff
)
echo on

%IMG7%magick ^
  ccy_1dsa_*.miff ^
  +append +repage ^
  -pointsize 30 -gravity South ^
  label:sinarc2aa ^
  -append +repage ^
  ccy_1dsa.png
ccy_1dsa.png

In the output image, the y-axis represents the input lightness and the x-axis represents the percentage. Each row shows the changes a pixel will undergo; for example, a pixel that is initially 25% will follow the changes shown in the row that is 25% of the way up an image. Each column shows the output values a grayscale gradient has for a given percentage.

We can put this is a loop, to make an output for each method.

set HTM=ccy_1dsa.htm
del %HTM%

call %PICTBAT%colCyclList >ccyList.lis

echo off

for /F %%M in (ccyList.lis) do (
  set METH=%%M
  echo method=!METH!

  for /L %%i in (0,1,255) do (

    set LZ=000000%%i
    set LZ=!LZ:~-6!

    for /F "usebackq" %%L in (`%IMG7%magick identify ^
      -format "PC=%%[fx:100*%%i/256]" ^
      xc:`) do set %%L

    call %PICTBAT%colCyclStr !METH! !PC!

    %IMG7%magick ^
      %SRC1D% ^
      !ccsImStr! ^
      ccy_1dsa_!LZ!.miff
  )

  %IMG7%magick ^
    ccy_1dsa_*.miff ^
    +append +repage ^
    -pointsize 30 -gravity South ^
    label:!METH! ^
    -append +repage ^
    ccy_1dsa_!METH!.png

  echo ^<img src="ccy_1dsa_!METH!.png" /^> >>%HTM%
)
echo on
ccy_1dsa_null.png ccy_1dsa_am.png ccy_1dsa_am2.png ccy_1dsa_amsol.png ccy_1dsa_amneg.png ccy_1dsa_amnegaa.png ccy_1dsa_amneg2.png ccy_1dsa_amneg2aa.png ccy_1dsa_amneg3.png ccy_1dsa_amneg3n.png ccy_1dsa_amneg3aa.png ccy_1dsa_sin.png ccy_1dsa_sin2.png ccy_1dsa_sin2aa.png ccy_1dsa_sinarc.png ccy_1dsa_sinarc2.png ccy_1dsa_sinarc2n.png ccy_1dsa_sinarc2o.png ccy_1dsa_sinarc2aa.png ccy_1dsa_lev1.png ccy_1dsa_lev2.png ccy_1dsa_lev3.png

Each result, except amneg and amnegaa, can tile horizontally.

Concatenating methods

The output of one method can be used as the input to the same or a different method. Some methods double frequencies, in that a black-white gradient is converted to a double black-white-black gradient. When two doublers are concatenated, we get a quadrupling of frequencies: black-white-black-white-black.

With concatenation, the two (or more) calls to colCyclStr.bat may have different percentages, perhaps changing at different speeds.

Future

A method could use cluts. For 0 to 50%, interpolate between identity clut (which is a gradient) and given clut. Then interpolate back to identity. We could have two cluts: interpolate from identity to first, then first to second, finally second to identity. Probably wise if caller also supplies the zero-frame clut.

The mask technique used in sinarc2o could be useful with other methods. Instead of the least significant bit, a different bit (or perhaps bit parity, or a noise image) might be used.

What method is like amneg2 but instead of the light band moving down then up, the dark band moves up then down?

A method could use a rolling clut, exactly the same size as there are frames in a cycle. Or, instead of rolling, use SRT with "-virtual-pixel tile".

Cleanup

:end

rem del ccy_*.miff

Scripts

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

ccy_stats.bat

@rem
@rem Updated:
@rem   22-August-2022 Upgraded for IM v7.
@rem

set METH=%1

set LIS=ccy_stats_%METH%.lis

%IMG7%magick compare -metric AE -format "\nzeroAE=" ccy_%METH%_000000.miff %SRC% NULL: >%LIS% 2>&1
%IMG7%magick compare -metric RMSE -format "\nzeroRMSE=" ccy_%METH%_000000.miff %SRC% NULL: >>%LIS% 2>&1
%IMG7%magick compare -metric AE -format "\nch20=" ccy_%METH%_000000.miff ccy_%METH%_000020.miff  NULL: >>%LIS% 2>&1
%IMG7%magick compare -metric PAE -format "\npae1820=" ccy_%METH%_000018.miff ccy_%METH%_000020.miff  NULL: >>%LIS% 2>&1
%IMG7%magick compare -metric MAE -format "\nmae1820=" ccy_%METH%_000018.miff ccy_%METH%_000020.miff  NULL: >>%LIS% 2>&1
%IMG7%magick ccy_%METH%_000020.miff -format "\nmean20=%%[fx:mean]" info: >>%LIS%

type %LIS%

colCyclStr.bat

rem Given %1 is a color cycling method and %2 is a percentage of cycle,
rem returns ccsImStr, a string to implement it.
@rem
@rem Updated:
@rem   22-August-2022 Upgraded for IM v7.
@rem

if "%1%"=="" exit /B 1
if "%2%"=="" exit /B 1

goto %1

:null
set ccsImStr=
@exit /B 0

:am
set ccsImStr=-evaluate AddModulus %2%%%%
@exit /B 0

:am2
for /F "usebackq" %%L in (`%IMG7%magick identify -format "ccsPC=%%[fx:2*(%2)]" xc:`) do set %%L
set ccsImStr=-evaluate AddModulus %ccsPC%%%%%
@exit /B 0

:amsol
set ccsImStr=-evaluate AddModulus %2%%%% -solarize 50%% -evaluate Multiply 2
@exit /B 0

:amneg
set ccsPC=%2
for /F "usebackq" %%L in (`%IMG7%magick identify -format "ccsNEGPC=%%[fx:100-(%2)]" xc:`) do set %%L
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite
@exit /B 0

:amnegaa
set ccsPC=%2
for /F "usebackq" %%L in (`%IMG7%magick identify -format "ccsNEGPC=%%[fx:100-(%2)]" xc:`) do set %%L
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite ^
  -auto-level -auto-gamma
@exit /B 0

:amneg2
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsPC=%%[fx:%2<50?2*(%2):200-2*(%2)]\nccsNEGPC=%%[fx:(%2<50?100-2*(%2):2*(%2)-100)-0.00001]" ^
  xc:`) do set %%L
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite
@exit /B 0

:amneg2aa
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsPC=%%[fx:%2<50?2*(%2):200-2*(%2)]\nccsNEGPC=%%[fx:(%2<50?100-2*(%2):2*(%2)-100)-0.00001]" ^
  xc:`) do set %%L
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite ^
  -auto-level -auto-gamma
@exit /B 0

:amneg3
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsPC=%%[fx:2*(%2)]\nccsNEGPC=%%[fx:%2<50?100-2*(%2):200-2*(%2)]\nccsIsNEG=%%[fx:%2<50?0:1]" ^
  xc:`) do set %%L
if !ccsIsNEG!==1 (set ccsNEG=-negate) else set ccsNEG=
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% !ccsNEG! ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite
@exit /B 0

:amneg3n
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsPC=%%[fx:200-2*(%2)]\nccsNEGPC=%%[fx:(%2>=50?2*(%2)-100:2*(%2))-0.00001]\nccsIsNEG=%%[fx:%2>=50?0:1]" ^
  xc:`) do set %%L
if !ccsIsNEG!==1 (set ccsNEG=-negate) else set ccsNEG=
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% !ccsNEG! ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite
@exit /B 0

:amneg3aa
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsPC=%%[fx:2*(%2)]\nccsNEGPC=%%[fx:%2<50?100-2*(%2):200-2*(%2)]\nccsIsNEG=%%[fx:%2<50?0:1]" ^
  xc:`) do set %%L
if !ccsIsNEG!==1 (set ccsNEG=-negate) else set ccsNEG=
set ccsImStr=^( -clone 0 -evaluate AddModulus !ccsPC!%%%% !ccsNEG! ^) ^
  ^^^( -clone 1 -negate ^^^) ^
  ^^^( -clone 0 -threshold !ccsNEGPC!%%%% ^^^) ^
  -delete 0 -compose Over -composite ^
  -auto-level -auto-gamma
@exit /B 0

:sin
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:(%2)*3.6-90]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 1,!ccsDEG!,0.5,0.5
@exit /B 0

:sin2
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:(%2)*3.6-90]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 0.5,!ccsDEG!,0.5,0.5
@exit /B 0

:sin2aa
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:(%2)*3.6-90]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 0.5,!ccsDEG!,0.5,0.5 -auto-level -auto-gamma
@exit /B 0

:sinarc
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:(%2)*3.6-90]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 1,!ccsDEG!,0.5,0.5 -function arcsin 1
@exit /B 0

:sinarc2
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:(%2)*3.6-90]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 0.5,!ccsDEG!,0.5,0.5 -function arcsin 1
@exit /B 0

:sinarc2n
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:270-(%2)*3.6]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 0.5,!ccsDEG!,0.5,0.5 -function arcsin 1
@exit /B 0

:sinarc2o
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG1=%%[fx:%2*3.6-90]\nccsDEG2=%%[fx:270-(%2)*3.6]" ^
  xc:`) do set %%L
set ccsImStr=^
  ^^^( -clone 0 ^
     -function sinusoid 0.5,!ccsDEG1!,0.5,0.5 ^
     -function arcsin 1 ^
  ^^^) ^
  ^^^( -clone 0 ^
     -function sinusoid 0.5,!ccsDEG2!,0.5,0.5 ^
     -function arcsin 1 ^
  ^^^) ^
  ^^^( -clone 0 ^
     -evaluate And 1 -fill White +opaque Black ^
  ^^^) ^
  -delete 0 ^
  -compose Over -composite
@exit /B 0

:sinarc2aa
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsDEG=%%[fx:(%2)*3.6-90]" ^
  xc:`) do set %%L
set ccsImStr=-function sinusoid 0.5,!ccsDEG!,0.5,0.5 -function arcsin 1 -auto-level -auto-gamma
@exit /B 0

:lev1
for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "ccsPC=%%[fx:%2<50?2*(%2):200-2*(%2)]\nccsPC2=%%[fx:%2<50?100-2*(%2):2*(%2)-100]\n" ^
  xc:`) do set %%L
set ccsImStr=+level !ccsPC!%%,!ccsPC2!%%
@exit /B 0

:lev2
set ccsFORM=^
ccsPCL=%%[fx:%2^<50?(%2):100-(%2)]\n^
ccsPCH=%%[fx:%2^<50?3*(%2):300-3*(%2)]\n^
ccsPCH2=%%[fx:%2^<50?100-(%2):(%2)]\n^
ccsPCL2=%%[fx:%2^<50?100-3*(%2):3*(%2)-200]

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "!ccsFORM!" ^
  xc:`) do set %%L
set ccsImStr=^( -clone 0 ^
     +level !ccsPCL!%%%%,!ccsPCL2!%%%% ^) ^
  ^^^( -clone 0 +level !ccsPCH!%%%%,!ccsPCH2!%%%% ^^^) ^
  ^^^( -clone 0 -threshold 50%%%% ^^^) ^
  -delete 0 -compose Over -composite
@exit /B 0

:lev3
set ccsFORM=^
ccsPCL=%%[fx:%2^<50?2*(%2):200-2*(%2)]\n^
ccsPCL2=%%[fx:%2^<50?100-4*(%2):4*(%2)-300]\n^
ccsPCH=%%[fx:%2^<50?4*(%2):400-4*(%2)]\n^
ccsPCH2=%%[fx:%2^<50?100-2*(%2):2*(%2)-100]

for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "!ccsFORM!" ^
  xc:`) do set %%L
set ccsImStr=^( -clone 0 ^
     +level !ccsPCL!%%%%,!ccsPCL2!%%%% ^) ^
  ^^^( -clone 0 +level !ccsPCH!%%%%,!ccsPCH2!%%%% ^^^) ^
  ^^^( -clone 0 -threshold 50%%%% ^^^) ^
  -delete 0 ^
  -compose Over -composite -auto-level
@exit /B 0

colCyclList.bat

I don't provide the source or binary of chStrs.exe, which here simply strips any colons (":") from the piped stream.

@rem  Write a list of available colour cycle methods to stdout

@findstr "^:" %PICTBAT%colCyclStr.bat |chStrs /p0 /i- /o- /f":"

colCyclTst.bat

rem Given image %1,
rem for each colour cycling method, makes test GIF named cct_!METH!.gif
rem Also creates an HTM file, name %2.
rem %3 optional prefix for GIF files. [cct]
@rem
@rem Updated:
@rem   25-September-2022 for IM v7.
@rem

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

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 pd

set HTM=%2
if "%HTM%"=="" set HTM=cct_gifs.h1

set GIF_PREF=%3
if "%GIF_PREF%"=="" set GIF_PREF=cct


set TMPDIR=%TEMP%\

del %TMPDIR%cct_*.miff 2>nul

set METH_LIST=%TEMP%\cctList.lis
del %HTM% 2>nul

set TXT=%TMPDIR%cct_text.png


for /F "usebackq" %%L in (`%IMG7%magick identify ^
  -format "WW=%%w\nHH=%%h\nWT=%%[fx:max(w*0.9,50)]\nHT=%%[fx:max(h*0.9,20)]" ^
  %INFILE%`) do set %%L


echo ^<h1^>Color cycle tests^</h1^> >%HTM%
echo ^<p^>This page is automatically created by the script ^<tt^>colCyclTst.bat^</tt^>. >>%HTM%
echo See ^<a href="colcycl.htm#cct"^>Colour cycling: tests^</a^>.^</p^> >>%HTM%

set FOR_ARGS=0,2,99

call %PICTBAT%colCyclList >%METH_LIST%

for /F %%M in (%METH_LIST%) do (
  set METH=%%M
  echo method=!METH!

  %IMG7%magick ^
    -size %WT%x%HT% label:!METH! ^
    -trim +repage ^
    -bordercolor White -border 3 ^
    -size %WW%x%HH% xc: ^
    +swap ^
    -gravity South -append +repage ^
    %TXT%

  for /L %%i in (%FOR_ARGS%) do (

    set LZ=000000%%i
    set LZ=!LZ:~-6!

    call %PICTBAT%colCyclStr !METH! %%i

    %IMG7%magick ^
      %INFILE% ^
      !ccsImStr! ^
      %TMPDIR%cct_!LZ!.miff
  )

  %IMG7%magick ^
    -loop 0 ^
    %TXT% ^
    null: ^
    %TMPDIR%cct_*.miff ^
    -gravity North ^
    -layers composite ^
    +repage ^
    -layers optimize ^
    %GIF_PREF%_!METH!.gif

  del %TMPDIR%cct_*.miff

  echo ^<img src="%GIF_PREF%_!METH!.gif" /^> >>%HTM%
)

setlocal DisableDelayedExpansion
echo ^<!--Page-version v1.0 23-Sep-2015--^> >>%HTM%
endlocal

echo Created cct_*.gif and %HTM%

call echoRestore

@endlocal & set pdOUTFILE=%HTM%

nseCycl.bat

This creates a single web page showing all colour cycling methods for all +noise types.

@rem
@rem Updated:
@rem   22-August-2022 Upgraded for IM v7.
@rem

set OUT_HTM=nc_htm.htm

del %OUT_HTM%

for /F "usebackq" %%N in (`%IMG7%magick -list noise`) do (
  echo %%N

  %IMG7%magick ^
    -size 200x200 xc:gray^(50%%^) +noise %%N ^
    nc_%%N.png

  set HTM=htm_%%N

  call %PICTBAT%colCyclTst nc_%%N.png !HTM! nc_%%N

  echo ^<h2^>%%N^</h2^> >>%OUT_HTM%

  type !HTM! >>%OUT_HTM%

)

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

%IMG7%magick -version
Version: ImageMagick 6.9.1--6 Q16 x64 2015-06-20 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Features: Cipher DPC Modules OpenMP 
Delegates (built-in): bzlib cairo freetype jng jp2 jpeg lcms lqr openexr pangocairo png ps rsvg tiff webp xml zlib

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 colcycl.h1. To re-create this web page, run "procH1 colcycl".


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.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 18-September-2015.

Page created 25-Sep-2022 03:42:28.

Copyright © 2022 Alan Gibson.