Adding and removing them.
A watermark is a visible distortion to an image often denoting authorship or ownership. As it is visible, a person wishing to "steal" the image for use elsewhere can easily remove the mark, but it may discourage casual theft.
There are many possible ways of watermarking an image. A simple way is by compositing the watermark over the image.
We use "snibgo" in light antialiased letters with a dark shadow at 25% opacity, on a transparent background. This image has no opaque pixels.
%IMG7%magick ^ size 200x100 ^ background None fill #123 label:snibgo ^ ( +clone channel RGB negate +channel repage 22 ) ^ layers merge ^ channel A evaluate Multiply 0.25 +channel ^ trim +repage ^ wm_mark.png 
Most of the pixels in this image are either fully transparent or 25% opaque, but there is antialiasing betwen the regions. We can count the number of different values of alpha:
%IMG7%magick ^ wm_mark.png ^ alpha extract uniquecolors ^ format %%w\n ^ info:
372
We simply composite the watermark over a source image:
%IMG7%magick ^ toes.png ^ wm_mark.png ^ gravity Center ^ compose Over composite ^ wm_toes_wm.png 
This watermarking process above is lossless; it hasn't removed information from the source image, so if we know the watermark and the the watermarked image, we can recover the source.
(This isn't strictly true. We are working in Q16 integer. If both images have 16 bits of information/channel/pixel, we can't combine them in a 16bit format while retaining all the information. Higher opacities of the watermark lose more information from the input, and we can't get it back. But an opacity up to 99.99% gives a visually successful dewatermark.)
Thus, if we have an opaque watermarked image and the nonopaque watermark, we can reconstruct the opaque original with the process that is the inverse of "compose Over composite". (See the Inverse composites page, equation D = (R  S.Sa) / (1Sa).) This removes the watermark from the watermarked image.
%IMG7%magick ^ wm_toes_wm.png ^ ( wm_mark.png ^ ( +clone alpha extract +write mpr:WEX ) ^ alpha off ^ compose Multiply composite ^ ) ^ gravity center ^ compose MinusSrc composite ^ ( mpr:WEX negate ) ^ alpha off ^ compose DivideSrc composite ^ wm_dewm.png 
How close is the dewatermarked image to the original image?
%IMG7%magick compare metric RMSE toes.png wm_dewm.png NULL:
0.126276 (1.92685e06)
%IMG7%magick compare metric PAE toes.png wm_dewm.png NULL:
1 (1.5259e05)
This is very close. The peak error is one in 2^16 as we are using Q16. We are using integer IM so the watermarking "compose Over composite" has been rounded to the nearest integer, and the same for each dewatermarking operation.
Suppose someone took my watermarked image and used this process to remove the watermark. Could I show that they had done so? We find the difference between the original source and the dewatermarked version, autolevelled:
%IMG7%magick ^ toes.png ^ wm_dewm.png ^ compose Difference composite ^ +write wm_dewm_r.png ^ autolevel ^ wm_dewm_r_a.png 
So, although the dewatermarked image looks like the original source, we can compare it to the original and detect the remnants of a watermark.
A dishonest dewatermarker could conceal their action by adding a little noise to the dewatermarked image. A more tamperproof watermark might be noisy (so the watermarking image could not be easily reconstructed), or be lossy so that mathematical reconstruction is not possible, or both. But a dewatermarker could simply remove pixels and use a holefilling technique.
If we have the opaque input image, and the result from the image with a semitransparent watermark composited over it, can we derive the watermark?
Image: toes.png 

Watermarked image: wm_toes_wm.png 
As shown in Inverse composites: Opaque inputs, if we don't know the colours or opacity of a watermark, any one of a virtually infinite number of watermarks might have been used.
However, if we guess the opacity of each pixel, we can calculate its colour. The script findWater.bat assumes that where a pixel is unchanged, the watermark there is fully transparent. Otherwise, it takes a usersupplied value for opacity (0.0 = transparent, 1.0 = opaque). The script returns the derived watermark. It also composites this over the input image, compares that to the userspecified watermarked image, and returns the RMSE score (0.0 is perfect match, 1.0 is totally different).
So we can run the script with a number of values for opacity.
call %PICTBAT%findWater ^ toes.png wm_toes_wm.png 0.0 ^ wm_deriv_00.png echo %FW_COMP% 0.02156181224797551 

call %PICTBAT%findWater ^ toes.png wm_toes_wm.png 0.1 ^ wm_deriv_01.png echo %FW_COMP% 0.009716789188342283 

call %PICTBAT%findWater ^ toes.png wm_toes_wm.png 0.25 ^ wm_deriv_025.png echo %FW_COMP% 3.290983852175886e07 

call %PICTBAT%findWater ^ toes.png wm_toes_wm.png 0.5 ^ wm_deriv_05.png echo %FW_COMP% 3.290983852175886e07 

call %PICTBAT%findWater ^ toes.png wm_toes_wm.png 0.75 ^ wm_deriv_075.png echo %FW_COMP% 8.621969907565209e07 

call %PICTBAT%findWater ^ toes.png wm_toes_wm.png 1.0 ^ wm_deriv_1.png echo %FW_COMP% 0 
The best score, 0, occurs when we guess the watermark opacity is 1.0, ie fully opaque. But we can see edges of the toes showing through. We can also see a ghostly version of the image for watermarks at 0.75 and 0.5 opacity.
The worst score occurs when we guess the watermark is entirely transparent, which is the same as having no watermark.
We get a good score from the opacity that was actually used to make the watermark: 0.25.
The solutions found above have watermarks that have exactly two opacities: entirely transparent, and some other opacity. However, the ground truth watermark has not only full transparency and 25% opacity but also antialiasing between the two. There is no way to determine the antialiasing because there is no unique solution for the opacity at any pixel. However, we can guess at the antialiasing, which supplies the value for the opacity at every pixel, so we can determine the colour at every pixel.
The script findWaterOpac.bat models the antialiasing by creating an image that is black where there is no watermark, and white where there is a watermark. Then it applies a small blur, and levels from 50% to 100%. This gives a graduation that occurs entirely within the watermark. The resulting image is just the opacity of the watermark.
call %PICTBAT%findWaterOpac ^ toes.png wm_toes_wm.png 0.25 ^ wm_opac_025.png ^ "blur 0x0.4" 
From this opacity map, the script findWater2.bat calculates the watermap colours.
call %PICTBAT%findWater2 ^ toes.png wm_toes_wm.png wm_opac_025.png ^ wm_aa_025.png echo %FW_COMP% 3.16411e07 
How many different values are in the alpha channel?
%IMG7%magick ^ wm_aa_025.png ^ alpha extract uniquecolors ^ format %%w\n ^ info:
23
When an image contains transparency, it can be watermarked in exactly the same way.
We will watermark ic_tr_src.png. It contains full transparency,


Apply the watermark: %IMG7%magick ^ ic_tr_src.png ^ wm_mark.png ^ gravity Center ^ compose Over composite ^ wm_tr_wm.png 
Provided we know the watermark and it contains no opaque pixels, we can dewatermark it to reconstruct the original. We use algebra from Inverse composites, in particular these equations ...
Da = (Ra  Sa) / (1Sa) D = (R*Ra  S*Sa) / (Ra  Sa)
... where all the variables are in the range 0.0 to 1.0. This calculates Da and D, the alpha and colour channels, which we combine with "compose CopyOpacity composite", to reconstruct the original.
Dewatermark: %IMG7%magick ^ ( wm_tr_wm.png +write mpr:R alpha extract ^ +write mpr:Ra +delete ^ ) ^ ( wm_mark.png +write mpr:S alpha extract ^ +write mpr:Sa ^ ) ^ mpr:S ^ alpha off ^ gravity Center ^ compose Multiply composite ^ ( mpr:R mpr:Ra alpha off compose Multiply composite ) ^ +swap ^ alpha off ^ compose MinusSrc composite ^ ( mpr:Ra ^ mpr:Sa ^ alpha off ^ compose MinusSrc composite ^ +write mpr:RamSa ^ ) ^ compose DivideSrc composite ^ ( mpr:RamSa ^ ( mpr:Sa negate ) ^ compose DivideSrc composite ^ ) ^ alpha Off ^ compose CopyOpacity composite ^ wm_tr_dewm.png 
How close is this dewatermarked image to the original image?
%IMG7%magick compare metric RMSE ic_tr_src.png wm_tr_dewm.png NULL:
0.0954275 (1.45613e06)
%IMG7%magick compare metric PAE ic_tr_src.png wm_tr_dewm.png NULL:
1.71052 (2.61009e05)
Again, this is very close. We have more operations, so rounding has lost more precision.
Above, we calculate a watermark from an unwatermarked image and a watermarked image. This generally has an infinite number of possible solutions. However, if we have two unwatermarked images and the corresponding two watermarked images, we can calculate the unique solution.
Suppose we have two known opaque images ("dest") without a watermark, and a version of each those two images with a watermark ("result"). These four images are the same size. We assume the same unknown watermark, which may have transparency, was used for both, with "compose Over composite". The watermark is in the same position in both images.
For a given pixel, and a given channel, let the input ("dest") colour values be D1 and D2, and the result watermarked image colour values be R1 and R2. Let the watermark ("source") colour value be S, and the watermark alpha be Sa.
Then Inverse composites: Opaque inputs gives equations for S and Sa:
S = (R1D1)/Sa + D1 = (R2D2)/Sa + D2 Sa = (R1D1) / (SD1) = (R2D2) / (SD2) (=Sa)
Solve the first equation for Sa:
(R1D1) + D1*Sa = (R2D2) + D2*Sa Sa*(D1D2) = (R2D2)(R1D1) = R2  R1 + D1  D2 Sa = (R2  R1 + D1  D2) / (D1D2)
Solve the second equation for S:
(R1D1) * (SD2) = (R2D2) * (SD1) S*R1  S*D1  D2*R1 + D1*D2 = S*R2  S*D2  D1*R2 + D1*D2 S*R1  S*D1  D2*R1 = S*R2  S*D2  D1*R2 S*R1  S*D1  S*R2 + S*D2 = D2*R1  D1*R2 S*(R1  D1  R2 + D2) = D2*R1  D1*R2 S = (D2*R1  D1*R2) / (R1  D1  R2 + D2)
These two equations are used by the script findWater2img.bat to calculate the watermark: S gives the pixel colour values, and Sa gives the pixel alphas. If any values S or Sa are outside the range [0..QuantumRange] then no watermark can be calculated.
For an example, we make two different images for watermarking. They should be the same size, but do not need to be related.
%IMG7%magick ^ toes.png ^ evaluate Pow 1.4 ^ wm_pow1.png 
%IMG7%magick ^ toes.png ^ evaluate Pow 0.6 ^ wm_pow2.png 
We apply a watermark to each image:
%IMG7%magick ^ wm_pow1.png ^ wm_mark.png ^ gravity Center ^ compose Over ^ composite ^ wm_pow1_w.png 
%IMG7%magick ^ wm_pow2.png ^ wm_mark.png ^ gravity Center ^ compose Over ^ composite ^ wm_pow2_w.png 
From these four images, we reconstruct the watermark with the script findWater2img.bat:
call %PICTBAT%findWater2img ^ wm_pow1.png wm_pow2.png ^ wm_pow1_w.png wm_pow2_w.png ^ wm_pow_calcwm.png 
We can invert the order of images 1 and 2, to get the same calculated watermark:
call %PICTBAT%findWater2img ^ wm_pow2.png wm_pow1.png ^ wm_pow2_w.png wm_pow1_w.png ^ wm_pow_calcwm21.png 

Verify that it is the same: %IMG7%magick compare ^ metric RMSE ^ wm_pow_calcwm21.png wm_pow_calcwm21.png ^ NULL: 0 (0) 
[No image] 
How close is this to the actual watermark used?
%IMG7%magick compare metric RMSE wm_mark.png wm_pow_calcwm.png trim NULL:
0.75482 (1.15178e05)
The roundtrip is accurate (1/65536 is 1.526e5), and the calculated watermark is very close to the actual watermark.
But we wouldn't normally have access to the actual watermark. We can also check the result by applying the calculated watermark to the original two inputs, and comparing the results to the watermarked inputs.
%IMG7%magick ^ wm_pow1.png ^ wm_pow_calcwm.png ^ gravity Center ^ compose Over composite ^ wm_pow1_w.png ^ metric RMSE ^ format %%[distortion]\n ^ compare ^ info:
3.11911e06
%IMG7%magick ^ wm_pow2.png ^ wm_pow_calcwm.png ^ gravity Center ^ compose Over composite ^ wm_pow2_w.png ^ metric RMSE ^ format %%[distortion]\n ^ compare ^ info:
3.01222e06
The calculated watermark accurately recreates the watermarked images.
Some special cases are of interest.
In this case, R1==R2, and the equations simplify to:
Sa = (R2  R1 + D1  D2) / (D1D2) = (D1D2) / (D1D2) = 1 hence fully opaque. S = (D2*R1  D1*R2) / (R1  D1  R2 + D2) = R1*(D2D1) / (D2D1) = R1, so the watermark colour is either result.
However, when the values in the input images are also equal, then D1==D1, so S and Sa are indeterminate.
For example, we create two images (mirror images of each other), and watermarked versions of them:
%IMG7%magick ^ size 150x301 ^ gradient:#f84#4f8 ^ rotate 90 ^ +write wm_grad_mark1.png ^ +write mpr:GRAD ^ gravity Center ^ wm_mark.png ^ compose Over composite ^ write wm_grad_mark1_w.png ^ +delete ^ mpr:GRAD flop ^ +write wm_grad_mark2.png ^ wm_mark.png ^ compose Over composite ^ wm_grad_mark2_w.png 
From these four images, we reconstruct the watermark with the script findWater2img.bat:
call %PICTBAT%findWater2img ^ wm_grad_mark1.png wm_grad_mark2.png ^ wm_grad_mark1_w.png wm_grad_mark2_w.png ^ wm_grad_calcwm.png echo Return code is: %ERRORLEVEL% Return code is: 0 
Pixels in the central column of the calculated watermark are indeterminate, with values outside [0..QuantumRange], and are transparent black in the written PNG file wm_grad_calcwm.png. A further process could identify those pixels and use a holefilling (aka inpainting) method to correct them.
Special case when R1==D1:
Sa = (R2  D2) / (D1D2) S = (D2*D1  D1*R2) / ( R2 + D2) = D1*(D2R2)/(D2R2) = D1 = R1
If, in addition, R2==D2:
Sa = 0 / (D1D2) = 0
In other words, where the watermark doesn't change pixels, the watermark must be transparent.
Special case when values of one input image are 0 (black) and values of the other are 1 (white), so D1 is 0 and D2 is 1:
S = R1 / (R1  R2 + 1) Sa = R1  R2 + 1 = R1 / S
When the inputs are entirely black and white, this simplifies the IM command. See the script findWaterBW.bat. For example, we make watermarked black and white images:
%IMG7%magick ^ size 300x150 ^ xc:Black ^ gravity Center ^ wm_mark.png ^ compose Over composite ^ write wm_bk_mark.png ^ +delete ^ xc:White ^ wm_mark.png ^ compose Over composite ^ wm_wh_mark.png 
From these two images, we reconstruct the watermark with the script findWaterBW.bat:
call %PICTBAT%findWaterBW ^ wm_bk_mark.png wm_wh_mark.png ^ wm_pow_calcbw.png 
How close is this to the actual watermark?
%IMG7%magick compare metric RMSE wm_mark.png wm_pow_calcbw.png trim NULL:
0.521761 (7.96156e06)
The roundtrip is accurate, and the calculated watermark is very close to the actual watermark.
For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.
rem Given an opaque input image %1, rem and samesized image %2 which is %1 composited with a watermark, rem where watermark pixels are either fully transparent or opacity %3 (0.0 to 1.0), rem finds the watermark image, and writes it to %4. rem rem Also tests the watermark by compositing over %1, compares this to %2, rem and returns result in FW_COMP, rem where 0.0 is perfect match, 1.0 is totally different. @if "%2"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion rem @call echoOffSave call %PICTBAT%setInOut %1 fw set DST=%1 set RES=%2 set ALPHA=%3 if "%ALPHA%"=="." set ALPHA= if "%ALPHA%"=="" set ALPHA=0.5 if not "%4"=="" if not "%4"=="." set OUTFILE=%4 set TMPDIR=\temp\ set OPAC=%TMPDIR%fw_opac.png rem Set OPAC to the opacity of the watermark image. %IMG7%magick ^ %RES% %DST% ^ compose ChangeMask composite ^ alpha extract ^ channel RGB evaluate Multiply %ALPHA% +channel ^ %OPAC% rem Implement S = (R  D(1Sa)) / Sa rem Calculate S, the assumed watermark. %IMG7%magick ^ %RES% ^ ( %DST% evaluate Multiply %%[fx:1%ALPHA%] ) ^ compose MinusSrc composite ^ evaluate Divide %ALPHA% ^ %OPAC% ^ compose CopyOpacity composite ^ %OUTFILE% rem Test the calculated watermark. for /F "usebackq" %%L in (`%IMG7%magick ^ %DST% ^ %OUTFILE% ^ compose Over composite ^ %RES% ^ metric RMSE ^ precision 16 ^ format "FW_COMP=%%[distortion]" ^ compare ^ info:`) do set %%L echo %0: FW_COMP=%FW_COMP% call echoRestore @endlocal & set fwOUTFILE=%OUTFILE%& set FW_COMP=%FW_COMP%
rem Given an opaque input image %1, rem and samesized image %2 which is %1 composited with a watermark, rem where watermark pixels are either fully transparent or opacity %3 (0.0 to 1.0), rem finds opacity for the watermark image, and writes it to %4. rem %5 is optional processing, eg "blur 0x1 level 50,100%" @if "%2"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion rem @call echoOffSave call %PICTBAT%setInOut %1 fwo set RES=%2 set ALPHA=%3 if "%ALPHA%"=="." set ALPHA= if "%ALPHA%"=="" set ALPHA=0.5 if not "%4"=="" if not "%4"=="." set OUTFILE=%4 set PROC=%~5 if "%PROC%"=="." set PROC= echo %0: PROC is [%PROC%] %IMG7%magick ^ %RES% %INFILE% ^ compose ChangeMask composite ^ alpha extract ^ %PROC% ^ channel RGB evaluate Multiply %ALPHA% +channel ^ %OUTFILE% call echoRestore @endlocal & set fwoOUTFILE=%OUTFILE%
rem Given an opaque input image %1, rem and samesized image %2 which is %1 composited with a watermark, rem where %3 is samesized image of opacity for watermark (0=transparent), rem finds the watermark image, and writes it to %4. rem rem Also tests the watermark by compositing over %1, compares this to %2, rem and returns result in FW_COMP, rem where 0.0 is perfect match, 1.0 is totally different. @rem @rem Updated: @rem 6August2022 for IM v7 @rem @if "%3"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion rem @call echoOffSave call %PICTBAT%setInOut %1 fw set DST=%1 set RES=%2 set ALPHA_IMG=%3 if not "%4"=="" if not "%4"=="." set OUTFILE=%4 rem Implement S = (R  D(1Sa)) / Sa rem Calculate S, the assumed watermark. %IMG7%magick ^ %RES% ^ ( %DST% ^ ( %ALPHA_IMG% negate ) ^ compose Multiply composite ^ ) ^ compose MinusSrc composite ^ %ALPHA_IMG% ^ compose DivideSrc composite ^ %ALPHA_IMG% ^ alpha off ^ compose CopyOpacity composite ^ %OUTFILE% rem Test the calculated watermark. for /F "usebackq" %%L in (`%IMG7%magick ^ %DST% ^ %OUTFILE% ^ compose Over composite ^ %RES% ^ metric RMSE ^ format "FW_COMP=%%[distortion]" ^ compare ^ info:`) do set %%L echo %0: FW_COMP=%FW_COMP% call echoRestore @endlocal & set fwOUTFILE=%OUTFILE%& set FW_COMP=%FW_COMP%
rem Given %1 and %2 are samesized opaque input images without watermarks (D1 and D2), rem %3 and %4 are those images composited with watermark (R1 and R2), rem writes watermark to %5. rem S = (D1*R2 + D2*R1) / (R1  D1  R2 + D2) set IS_OK=0 %IMG7%magick ^ define "compose:clamp=off" ^ +depth ^ ^( %1 set colorspace sRGB +write mpr:D1 ^) ^ ^( %2 set colorspace sRGB +write mpr:D2 ^) ^ ^( %3 set colorspace sRGB +write mpr:R1 ^) ^ ^( %4 set colorspace sRGB +write mpr:R2 ^) ^ delete 01 ^ ^( mpr:D2 mpr:R1 compose Multiply composite ^) ^ ^( mpr:D1 mpr:R2 compose Multiply composite ^) ^ +depth ^ compose MinusSrc composite ^ ^( mpr:R1 ^ mpr:D1 compose MinusSrc composite ^ mpr:R2 compose MinusSrc composite ^ mpr:D2 compose Add composite ^ ^) ^ compose DivideSrc composite ^ ^( ^ ^( mpr:D1 ^ mpr:D2 ^ compose MinusSrc composite ^ +write mpr:D1mD2 ^ ^) ^ mpr:R2 compose Plus composite ^ mpr:R1 compose MinusSrc composite ^ mpr:D1mD2 compose DivideSrc composite ^ ^) ^ alpha off compose CopyOpacity composite ^ +depth ^ verbose info: for /F "usebackq" %%L in (`%IMG7%magick ^ define "compose:clamp=off" ^ +depth ^ ^( %1 set colorspace sRGB +write mpr:D1 ^) ^ ^( %2 set colorspace sRGB +write mpr:D2 ^) ^ ^( %3 set colorspace sRGB +write mpr:R1 ^) ^ ^( %4 set colorspace sRGB +write mpr:R2 ^) ^ delete 01 ^ ^( mpr:D2 mpr:R1 compose Multiply composite ^) ^ ^( mpr:D1 mpr:R2 compose Multiply composite ^) ^ +depth ^ compose MinusSrc composite ^ ^( mpr:R1 ^ mpr:D1 compose MinusSrc composite ^ mpr:R2 compose MinusSrc composite ^ mpr:D2 compose Add composite ^ ^) ^ compose DivideSrc composite ^ ^( ^ ^( mpr:D1 ^ mpr:D2 ^ compose MinusSrc composite ^ +write mpr:D1mD2 ^ ^) ^ mpr:R2 compose Plus composite ^ mpr:R1 compose MinusSrc composite ^ mpr:D1mD2 compose DivideSrc composite ^ ^) ^ alpha off compose CopyOpacity composite ^ +depth ^ format "IS_OK=%%[fx:minima>=0.0 && maxima<=1.0?1:0]\n" ^ +write info: ^ %~5 `) do set %%L if %IS_OK%==0 ( echo %0: failed rem exit /B 1 ) exit /B 0
rem From %1 black watermarked image rem %2 white watermarked image rem writes watermark to %3. %IMG7%magick ^ define compose:clamp=off ^ ( %1 +write mpr:R1 ^ %2 ^ compose MinusSrc composite ^ evaluate Add 100%% +write mpr:Sa ^ ) ^ ( mpr:R1 mpr:Sa ^ compose DivideSrc composite ^ ) ^ +swap ^ alpha off ^ compose CopyAlpha composite ^ +depth ^ %~3
All images on this page were created by the commands shown, using:
%IMG7%magick version
Version: ImageMagick 7.1.042 Q16HDRI x64 396d87c:20220709 https://imagemagick.org Copyright: (C) 1999 ImageMagick Studio LLC License: https://imagemagick.org/script/license.php Features: Cipher DPC HDRI OpenCL Delegates (builtin): 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 (193231332)
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 watermark.h1. To recreate this web page, run "procH1 watermark".
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 republish this page, but only for noncommercial use.
Anyone is permitted to link to this page, including for commercial use.
Page version v1.0 26February2017.
Page created 06Aug2022 23:14:43.
Copyright © 2022 Alan Gibson.