Three methods are shown.
An image may contain broken lines. It might be a scan from an old book, or the result of processing a photograph, or a hand-drawn sketch. A variety of methods can identify and fix these breaks.
We will consider only white lines on black backrounds. They may be aliased. We assume the image is fully opaque.
We create source images to demonstrate techniques. Each image contains broken lines, with a large gap or a small gap. We will repair the small gaps, leaving the large gaps unchanged. In the first four images, we will join line-ends together.
%IMG7%magick ^ -size 200x200 xc:Black ^ -stroke White -strokewidth 5 ^ -draw "line 0,50 80,50" ^ -draw "line 120,50 199,50" ^ -draw "line 0,150 95,150" ^ -draw "line 105,150 199,150" ^ mbl_src1.png |
|
%IMG7%magick ^ -size 200x200 xc:Black ^ -stroke White -strokewidth 5 ^ -draw "line 0,50 80,50" ^ -draw "line 120,60 199,60" ^ -draw "line 0,150 95,150" ^ -draw "line 105,160 199,160" ^ mbl_src2.png |
|
%IMG7%magick ^ -size 200x200 xc:Black ^ -stroke White -strokewidth 5 ^ -draw "line 0,5 80,50" ^ -draw "line 120,50 199,5" ^ -draw "line 0,105 95,150" ^ -draw "line 105,150 199,105" ^ mbl_src3.png |
|
%IMG7%magick ^ -size 200x200 xc:Black ^ -stroke White -strokewidth 5 ^ -fill None ^ -draw "circle 100,-20 100,50" ^ -draw "circle 100,-20 100,150" ^ -stroke None -fill Black ^ -draw "rectangle 80,45 120,55" ^ -draw "rectangle 95,145 105,155" ^ mbl_src4.png |
The following contains broken T-junctions. We will mend the small gap.
%IMG7%magick ^ -size 200x200 xc:Black ^ -stroke White -strokewidth 5 ^ -draw "line 0,5 80,50" ^ -draw "line 0,105 95,150" ^ -draw "circle 1000,150 105,150" ^ mbl_src5.png |
ASIDE: The methods shown will join lines from line-ends. When two lines are in close proximity, but line-ends are distant, the methods will not join them.
Such cases can be joined by "-morphology Close", though this will also join line-ends.
%IMG7%magick ^ -size 200x200 xc:Black ^ -stroke White -strokewidth 5 ^ -draw "circle -1000,100 90,100" ^ -draw "circle 1000,100 110,100" ^ mbl_src6.png |
|
%IMG7%magick ^ mbl_src6.png ^ -morphology Close disk:10 ^ mbl_src6_close.png |
The methods use skeletonized versions of the inputs, and coordinates of the line-ends of the skeletons.
%IMG7%magick ^ mbl_src1.png ^ -virtual-pixel Edge ^ -morphology Thinning:-1 Skeleton ^ +write mbl_skel1.png ^ -morphology HMT LineEnds ^ +write mbl_ends1.png ^ -transparent Black ^ sparse-color:mbl_ends1.lis sed -e 's/ /\n/g' mbl_ends1.lis 80,48,graya(255,1) 120,48,graya(255,1) 76,50,graya(25.0004%,1) 124,50,graya(74.9996%,1) 80,52,graya(74.9996%,1) 120,52,graya(24.9989%,1) 121,52,graya(25.0004%,1) 120,53,graya(25.0004%,1) 95,148,graya(255,1) 105,148,graya(255,1) 91,150,graya(25.0004%,1) 109,150,graya(74.9996%,1) 95,152,graya(74.9996%,1) 105,152,graya(24.9989%,1) 106,152,graya(25.0004%,1) 105,153,graya(25.0004%,1) |
We can get an approximate line width by dividing the mean lightness of the input image by the mean lightness of the skeleton image.
for /F "usebackq" %%L in (`%IMG7%magick ^ mbl_src1.png ^ -format "MN_MAIN=%%[fx:mean]" ^ info:`) do set %%L for /F "usebackq" %%L in (`%IMG7%magick ^ mbl_skel1.png ^ -format "MN_SKEL=%%[fx:mean]" ^ info:`) do set %%L for /F "usebackq" %%L in (`%IMG7%magick identify ^ -format "LINE_THK=%%[fx:%MN_MAIN%/%MN_SKEL%]\nLINE_THK_INT=%%[fx:int(%MN_MAIN%/%MN_SKEL%+0.5)]" ^ xc:`) do set %%L echo LINE_THK=%LINE_THK% LINE_THK_INT=%LINE_THK_INT%
LINE_THK=6.46157 LINE_THK_INT=6
Skeletonized lines generally contain short stub segments, where one end is a line-end and the other end is either bounded by a junction, or is also a line-end. In this example, each line has split ends, which has doubled the numbers of ends. The script pruneStubs.bat removes these, leaving what was the junction as the new line-end. (For more details, see the Pruning skeleton stubs page.)
%IMG7%magick ^ mbl_src1.png ^ -virtual-pixel Edge ^ -morphology Thinning:-1 Skeleton ^ mbl_skelp1.png call %PICTBAT%pruneStubs ^ mbl_skelp1.png mbl_skelp2.png %LINE_THK_INT% %IMG7%magick ^ mbl_skelp2.png ^ -morphology HMT LineEnds ^ +write mbl_endsp1.png ^ -transparent Black ^ sparse-color:mbl_endsp1.lis sed -e 's/ /\n/g' mbl_endsp1.lis 120,48,graya(255,1) 76,50,graya(25.0004%,1) 79,50,graya(255,1) 124,50,graya(74.9996%,1) 120,52,graya(24.9989%,1) 121,52,graya(25.0004%,1) 120,53,graya(25.0004%,1) 105,148,graya(255,1) 91,150,graya(25.0004%,1) 94,150,graya(255,1) 109,150,graya(74.9996%,1) 105,152,graya(24.9989%,1) 106,152,graya(25.0004%,1) 105,153,graya(25.0004%,1) |
We make a script skelPS.bat that makes a skeleton, prunes stubs, and finds the ends.
call %PICTBAT%skelPS ^ mbl_src1.png mbl_1.png %LINE_THK_INT% 79,50,graya(255,1) 122,50,graya(255,1) 94,150,graya(255,1) 107,150,graya(255,1) |
|
call %PICTBAT%skelPS ^ mbl_src2.png mbl_2.png %LINE_THK_INT% 79,50,graya(255,1) 122,60,graya(255,1) 94,150,graya(255,1) 107,160,graya(255,1) |
|
call %PICTBAT%skelPS ^ mbl_src3.png mbl_3.png %LINE_THK_INT% 78,49,graya(255,1) 122,49,graya(255,1) 93,149,graya(255,1) 107,149,graya(255,1) |
|
call %PICTBAT%skelPS ^ mbl_src4.png mbl_4.png %LINE_THK_INT% 79,49,graya(255,1) 121,49,graya(255,1) 93,150,graya(255,1) 108,150,graya(255,1) |
|
call %PICTBAT%skelPS ^ mbl_src5.png mbl_5.png %LINE_THK_INT% 78,49,graya(255,1) 93,149,graya(255,1) |
Two of the methods join lines-ends that are within a threshold distance of each other. How do we find the pairs of line-ends that are to be joined?
From the above, we have a list of coordinates of line-ends. We can easily calculate the distance between any two line-ends:
distancei,j = hypot (Xi-Xj,Yi-Yj)
... where i,j range from zero to (N-1), where N is the number of line-ends. Then we are interested only in the pairs where the distance is less than or equal to a given threshold. In our worked examples, we will use a proximity threshold of 20 pixels.
set PROX_THRESH=20
The script pointsDist.bat finds those pairs of points, writing the output in a format usable by -draw "@file.txt".
call %PICTBAT%pointsDist mbl_3_ends.csv mbl_3_lines.lis %PROX_THRESH%
line 93,149 107,149
This can be done in a shell script, however it takes O(N2) time. If we have millions of line-ends, this is a problem.
Two methods reduce the problem:
Method 1 might use this algorithm, using the line-ends image:
Although this algorithm is not O(N2), it is O(N*W*H), so is likely to be just as slow in practice.
An improved algorithm is:
This improved algorithm is O(r2*N), where r is the "radius". It can use either the image of the line-ends (to find line-ends that are near each other), or the initial input image (to find line-ends that are near lines).
This implemented as pointsRdx.bat. The script has a large overhead per line-end, but is massively quicker then pointsDist.bat when we have hundreds of line-ends.
The essential difference between the scripts is:
Join line-ends that are in close proximity. Basic method: find distance between every pair of points. Better: first, eliminate points that are not in proximity to any others. Do it in C.
When we know which pairs of line-ends are close enough to be joined, we can join them with a simple thin white line.
call %PICTBAT%skelPS ^ mbl_src1.png mbl_jle1.png %LINE_THK_INT% if ERRORLEVEL 1 goto error call %PICTBAT%pointsDist ^ %spsCSVFILE% mbl_jle1_lines.scr %PROX_THRESH% if ERRORLEVEL 1 goto error %IMG7%magick ^ mbl_src3.png ^ +antialias ^ -stroke White ^ -draw "@%pdOUTFILE%" ^ mbl_jle1r.png |
As we know the average stroke-width of the input image, we can use that as the width of the added line.
%IMG7%magick ^ mbl_src3.png ^ +antialias ^ -stroke White ^ -strokewidth %LINE_THK_INT% ^ -draw "@%pdOUTFILE%" ^ mbl_jle1rw.png |
We put this into a script, proxLineEnds.bat.
call %PICTBAT%proxLineEnds ^ mbl_src1.png mbl_ple_1.png %PROX_THRESH% |
|
call %PICTBAT%proxLineEnds ^ mbl_src2.png mbl_ple_2.png %PROX_THRESH% |
|
call %PICTBAT%proxLineEnds ^ mbl_src3.png mbl_ple_3.png %PROX_THRESH% |
|
call %PICTBAT%proxLineEnds ^ mbl_src4.png mbl_ple_4.png %PROX_THRESH% |
|
call %PICTBAT%proxLineEnds ^ mbl_src5.png mbl_ple_5.png %PROX_THRESH% |
Note that this method, like the others, doesn't notice that a pair of line-ends may be the two ends of the same short line, and hence already joined.
We can "grow" lines. That is, we can extend them. One method is to erase (make transparent) background pixels around line-ends, then fill in these holes. These extensions can be used as a mask, so an extension is used only where another line-end (or another extension) falls within this exension.
We could operate on one line-end at a time, extending it by a certain radius. We would then compare the results pair-wise. Where two results have white pixels in the same location, then we know the intersection is caused by an extension. This would raise a difficult problem: How do we know how far to extend lines? We find that two extensions intersect, but we don't want either line to be extended beyond the intersection.
Intead, we erase (make transparent) all the black pixels that are within a circle centered at the mid-point between line-ends. Then we fill all the holes, in priority order. The radius of the circles should be half the proximity threshold, and the window_radius is set to the estimated line thickness.
set /A RAD=%PROX_THRESH%/2 %IMDEV%convert ^ mbl_src3.png ^ +write mpr:IMG ^ +antialias ^ ( mpr:IMG ^ -fill White -colorize 100 ^ -fill Black ^ -draw "translate 100,49 circle 0,0 0,%RAD%" ^ -draw "translate 100,149 circle 0,0 0,%RAD%" ^ mpr:IMG -compose Lighten -composite ^ -fill Black +opaque White ^ ) ^ -alpha off -compose CopyOpacity -composite ^ +write mbl_3t.png ^ -process 'fillholespri window_radius %LINE_THK_INT% lsr 10%% auto_limit_search off cp window' ^ mbl_3m.png |
We put this into a script, extLineEnds.bat.
call %PICTBAT%extLineEnds ^ mbl_src1.png mbl_ele_1.png %PROX_THRESH% |
|
call %PICTBAT%extLineEnds ^ mbl_src2.png mbl_ele_2.png %PROX_THRESH% |
|
call %PICTBAT%extLineEnds ^ mbl_src3.png mbl_ele_3.png %PROX_THRESH% |
|
call %PICTBAT%extLineEnds ^ mbl_src4.png mbl_ele_4.png %PROX_THRESH% |
|
call %PICTBAT%extLineEnds ^ mbl_src5.png mbl_ele_5.png %PROX_THRESH% |
Instead of erasing a circle, we could erase a square or other shape. This could improve performance when we want to mend large breaks where the broken ends point directly towards each other.
The script extLineEnds.bat does the job, but doesn't recognise when it has failed. In the second example above, it has extended a pair of parallel lines, and they don't intersect.
Similarly, if the two lines approached each other at a small angle, nearly parallel, the intersection would fall outside the circle (or square). So the lines would be correctly extended, but they wouldn't meet. The process could be repeated until they did meet.
Creating holes then filling them has a problem. The cloning might come from any pixels that are within the search radius. Ideally, we would clone only from the relevant lines.
The script could be modified to skeletonize the result and test whether any line-ends were present in the relevant crops of the result. If they were, it could roll-back those crops, so lines that don't intersect are not extended.
[[We can detect when this situation has occurred: find any line-ends in the area defined by the circle (or square) of the result. If there are two or more line-ends, the situation has probably occurred, so repeat the process with these new line-ends.
Of course, if the lines are exactly parallel, there will be no intersection. When the lines are extended to the edge of the image, it will have no line-end there.]]
The methods above join line-ends that are close to each other. We can also handle situations where a line-end is close to another line, but not necessarily an end of that line.
For each line-end:
The flood-fill is needed to prevent a line-end from trivially joining with itself, or the next point along the line, etc. However, when a line ends with a U-shape, we might want the end to join with the pixel on the other branch of the 'U'. If the other branch is connected to the line-end within the crop, this algorithm won't join with it.
For example, for the source image #5, the line-end at (93,149):
set LE_X=93 set LE_Y=149 set /A CRP_WH=2*%PROX_THRESH%+1 set /A CRP_L=%LE_X%-%PROX_THRESH% set /A CRP_T=%LE_Y%-%PROX_THRESH% %IMG7%magick ^ mbl_src5.png ^ -fill Black +opaque White ^ -crop %CRP_WH%x%CRP_WH%+%CRP_L%+%CRP_T% +repage ^ +write mbl_5crp.png ^ -draw "color %PROX_THRESH%,%PROX_THRESH% floodfill" ^ mbl_5crpw.png set ncSEA_COL=black call %PICTBAT%nearCoast2 mbl_5crpw.png 50%%%% 50%%%% if ERRORLEVEL 1 exit /B 1 set ncSEA_COL= echo ncCST_CX=%ncCST_CX% ncCST_CY=%ncCST_CY% echo ncCST_X=%ncCST_X% ncCST_Y=%ncCST_Y% ncCST_X=30 ncCST_Y=20 |
|
for /F "usebackq" %%L in (`%IMG7%magick identify ^ -format "ML_X=%%[fx:int(%CRP_L%+%ncCST_X%+0.5)]\nML_Y=%%[fx:int(%CRP_T%+%ncCST_Y%+0.5)]" ^ xc:`) do set %%L echo ML_X=%ML_X% ML_Y=%ML_Y% ML_X=103 ML_Y=149 %IMG7%magick ^ mbl_src5.png ^ +antialias ^ -stroke White ^ -draw "line %LE_X%,%LE_Y% %ML_X%,%ML_Y%" ^ mbl_tcoord.png |
As before, we can use an appropriate strokewidth:
%IMG7%magick ^ mbl_src5.png ^ +antialias ^ -stroke White ^ -strokewidth %LINE_THK_INT% ^ -draw "line %LE_X%,%LE_Y% %ML_X%,%ML_Y%" ^ mbl_tcoordw.png |
We put this into a script, TjuncLineEnds.bat that processes all the line-ends in this manner. The script is more complex than the worked example, as it has to cope with crops that are near the image edge. (I would like IM to have an option so crops can take virtual-pixels.)
call %PICTBAT%TjuncLineEnds ^ mbl_src1.png mbl_tjle_1.png %PROX_THRESH% |
|
call %PICTBAT%TjuncLineEnds ^ mbl_src2.png mbl_tjle_2.png %PROX_THRESH% |
|
call %PICTBAT%TjuncLineEnds ^ mbl_src3.png mbl_tjle_3.png %PROX_THRESH% |
|
call %PICTBAT%TjuncLineEnds ^ mbl_src4.png mbl_tjle_4.png %PROX_THRESH% |
|
call %PICTBAT%TjuncLineEnds ^ mbl_src5.png mbl_tjle_5.png %PROX_THRESH% |
In the first four examples, the script has added two lines, because the line that is close to a line-end has, itself, a line-end. So a line is added from a line-end to the second line, then another line is added from the second line-end back to the first line. To prevent this from happening, first process the image with a method that joins line-ends together.
If the unbroken line doesn't have a line-end here, the added line segment will be perpendicular to the unbroken line, more or less. We might instead use Hough lines, within just the crop, to identify the intersection point (with the caution that the intersection might be outside the crop).
If we want to close gaps at the edge of the image, we can add a white border, run TjuncLineEnds.bat, then shave off the added border.
FUTURE: TjuncLineEnds.bat is painfully slow. It probably needs a cut-down version of nearCoast.bat.
The script combLineEnds.bat combines some of the above methods. They are run in sequence: the output of one method is used as the input to the next, recalculating line-ends for each method. This simple script uses the same threshold for each method. In practice, different thresholds may be preferred.
call %PICTBAT%combLineEnds ^ mbl_src1.png mbl_cle_1.png %PROX_THRESH% |
|
call %PICTBAT%combLineEnds ^ mbl_src2.png mbl_cle_2.png %PROX_THRESH% |
|
call %PICTBAT%combLineEnds ^ mbl_src3.png mbl_cle_3.png %PROX_THRESH% |
|
call %PICTBAT%combLineEnds ^ mbl_src4.png mbl_cle_4.png %PROX_THRESH% |
|
call %PICTBAT%combLineEnds ^ mbl_src5.png mbl_cle_5.png %PROX_THRESH% |
The "Z" in the second example is caused by extLineEnds.bat creating an overshoot, then proxLineEnds.bat joining them.
We use an image from the Canny edge detection page.
set RW_SRC=ca_lab.png |
|
%IMG7%magick ^ %RW_SRC% ^ -virtual-pixel Edge ^ -morphology Thinning:-1 Skeleton ^ mbl_rw1.png call %PICTBAT%pruneStubs ^ mbl_rw1.png mbl_skelrw.png %LINE_THK_INT% %IMG7%magick ^ mbl_skelrw.png ^ -morphology HMT LineEnds ^ +write mbl_endsrw.png ^ -transparent Black ^ sparse-color:mbl_endsrw.lis sed -e 's/ /\n/g' mbl_endsrw.lis |
There are 260 line-ends.
Calculate the approximate width:
for /F "usebackq" %%L in (`%IMG7%magick ^ %RW_SRC% ^ -format "MN_MAIN=%%[fx:mean]" ^ info:`) do set %%L for /F "usebackq" %%L in (`%IMG7%magick ^ mbl_skelrw.png ^ -format "MN_SKEL=%%[fx:mean]" ^ info:`) do set %%L for /F "usebackq" %%L in (`%IMG7%magick identify ^ -format "LINE_THK=%%[fx:%MN_MAIN%/%MN_SKEL%]\nLINE_THK_INT=%%[fx:int(%MN_MAIN%/%MN_SKEL%+0.5)]" ^ xc:`) do set %%L echo LINE_THK=%LINE_THK% LINE_THK_INT=%LINE_THK_INT%
LINE_THK=1.12627 LINE_THK_INT=1
FIXME: we need width before the prune.
call %PICTBAT%combLineEnds ^ %RW_SRC% mbl_rw_cle.png 10 |
|
A blink comparison shows the added lines %IMG7%magick ^ -delay 50 ^ %RW_SRC% ^ mbl_rw_cle.png ^ mbl_rw_blnk.gif |
For some purposes, we want to simplify the image, and the original lines are unimportant. Then a simple (and fast) command may be appropriate. Unlike the methods shown above, -morphology close will merge parallel lines that are less then 2*r apart. This may be good or bad, depending on the application.
%IMG7%magick ^ %RW_SRC% ^ -morphology close disk:10 ^ mbl_morphcl.png |
|
A blink comparison shows the difference. %IMG7%magick ^ -delay 50 ^ %RW_SRC% ^ mbl_morphcl.png ^ mbl_morphcl_blnk.gif |
The image mbl_morphcl.png can be regarded as a thick partition boundary mask.
We often want to simplify this to a skeleton:
%IMG7%magick ^ mbl_morphcl.png ^ -morphology Thinning:-1 Skeleton ^ mbl_morphclsk.png |
|
A blink comparison shows the difference. %IMG7%magick ^ -delay 50 ^ %RW_SRC% ^ mbl_morphclsk.png ^ mbl_morphclsk_blnk.gif |
From the skeleton, we can make a partition-boundary mask:
call %PICTBAT%pruneStubsRep ^ mbl_morphclsk.png mbl_partbnd.png all |
When a number of line-ends are in close proximity to each other, the methods will join them with new lines from every line-end to every other line-end, which is more than necessary and looks ugly. For example, five points that are close together will generate 12 lines joining them. This could be solved by re-calculating line-ends after every join, but performance would be terrible. Perhaps we could extract crops, add each line to the crop, and re-calculate just for the crop.
A method for mending a broken T-junction by extending the line should be possible.
I would like a method for the minimal joining of lines that are in close proximity, as shown in the Aside above. Of course, we can "-morphology close" and skeletonize, but how do we get an "H" shape?
What size of gap should be closed? This is a parameter the user must supply, and the same number is used in all parts of the image. There is scope for automatically determining this number.
Three methods for mending broken lines have been shown. They can be combined, but not with the same line-end data. Instead, they should be run in sequence: the output of one method can be used as the input to another, recalculating line-ends.
For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.
rem From %1, image of skeletonized white lines on black background, fully opaque, rem makes output %2 version with short stubs removed. rem %3 threshold for stub removal, integer number of pixels, or "all". rem Stubs larger than this will not be removed. @rem @rem Updated: @rem 15-Feb-2017 added limit so exclusion string doesn't break line length limit. @rem 6-August-2022 for IM v7 @rem @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave call %PICTBAT%setInOut %1 ps echo %0: %1 %2 %3 if not "%2"=="" if not "%2"=="." set OUTFILE=%2 set MAX_SIZE=%3 if "%MAX_SIZE%"=="." set MAX_SIZE= if "%MAX_SIZE%"=="" set MAX_SIZE=10 set DO_ALL=0 if /I "%MAX_SIZE%" EQU "all" set DO_ALL=1 set EXT=.miff set ENDS=%BASENAME%_ends_ps%EXT% set SEGS=%BASENAME%_segs_ps%EXT% set ENDSEGS=%BASENAME%_endsegs_ps%EXT% set SEGSLIST=%BASENAME%_graysegs_ps.lis set ENDSLIST=%BASENAME%_ends_ps.lis set FLOODLIST=%BASENAME%_flood_ps.lis for /F "usebackq" %%L in (`%IMG7%magick ^ xc: -format "QNUM=%%q" info:`) do set %%L for /F "usebackq" %%L in (`%IMG7%magick identify ^ -format "CANVAS=%%P%%O" %INFILE%`) do set %%L %IMG7%magick ^ %INFILE% ^ -fill White +opaque Black ^ +repage ^ ( -clone 0 ^ -morphology HMT LineEnds ^ -alpha off ^ +write %ENDS% ^ +delete ^ ) ^ -define morphology:compose=Darken ^ -morphology Thinning "LineJunctions;LineJunctions:3>;LineJunctions:5" -clamp ^ -alpha off ^ %SEGS% if ERRORLEVEL 1 exit /B 1 set /A N_DILATE=2*%MAX_SIZE% set nPruned=0 if %DO_ALL%==1 ( ( for /F "usebackq tokens=1-2 delims=, " %%X in (`%IMG7%magick ^ %ENDS% ^ -transparent Black ^ sparse-color:- ^| sed -e 's/ /\n/g' `) do @( @echo color %%X,%%Y floodfill @set /A nPruned+=1 ) ) >%FLOODLIST% type %FLOODLIST% rem -morphology thicken "3>:-,0,-,1,-,1,0,1,0" rem -morphology thicken "3>:-,0,0,1,-,0,0,1,-" %IMG7%magick ^ %INFILE% ^ -alpha off ^ ^( +clone ^ -define "morphology:compose=Darken" ^ -morphology Thinning "LineJunctions;LineJunctions:3>;LineJunctions:5" -clamp ^ -alpha off ^ ^( +clone ^ -fill Black -draw @%FLOODLIST% ^ ^) ^ -compose Difference -composite ^ -alpha off ^ -negate ^ ^) ^ -compose Darken -composite ^ ^( +clone ^ -morphology Hit-and-Miss "3x3:0,0,0,0,1,0,0,0,0" ^ -alpha off ^ -negate ^ ^) ^ -compose Darken -composite ^ -compose Over ^ -alpha off ^ %OUTFILE% goto end ) %IMG7%magick ^ %SEGS% -negate +write mpr:MASK +delete ^ %ENDS% ^ -write-mask mpr:MASK ^ -morphology Dilate:%N_DILATE% Square ^ +write-mask ^ -alpha off ^ +write %ENDSEGS% ^ -define "connected-components:verbose=true" ^ -connected-components 8 ^ NULL: >%SEGSLIST% if ERRORLEVEL 1 exit /B 1 :: Building the string: 1000 is okay; 2200 breaks line length. set HAS_ANY=0 set CH_COLS= set CH_EXCL= set nFnd=0 set nExcl=0 for /F "skip=1 tokens=1-5 delims=: " %%A in (%SEGSLIST%) do ( set HAS_ANY=1 set /A nFnd+=1 set ID=%%A set BND_BOX=%%B set CENTROID=%%C set AREA=%%D set COL=%%E if !AREA! GTR %MAX_SIZE% if !nExcl! LSS 1000 ( set CH_EXCL=!CH_EXCL!,!ID! set /A nExcl+=1 ) ) echo %0: SEGSLIST=%SEGSLIST% nFnd=%nFnd% nExcl=%nExcl% if %HAS_ANY%==0 ( echo %0: ** No components?? ** exit /B 1 ) :: remove leading comma set CH_EXCL=%CH_EXCL:~1% rem echo CH_EXCL=%CH_EXCL% :: FIXME? not necessarily an error. if "%CH_EXCL%"=="" exit /B 1 %IMG7%magick ^ %ENDSEGS% ^ -define "connected-components:remove=%CH_EXCL%" ^ -define "connected-components:mean-color=true" ^ -connected-components 8 ^ -background Black -layers Flatten ^ -fill White +opaque Black ^ -alpha off ^ -negate ^ %INFILE% ^ +repage ^ -compose Darken -composite ^ ( +clone ^ -morphology Hit-and-Miss "3x3:0,0,0,0,1,0,0,0,0" ^ -alpha off ^ -negate ^ ) ^ -compose Darken -composite ^ -compose Over ^ -repage %CANVAS% ^ -alpha off ^ %OUTFILE% if ERRORLEVEL 1 exit /B 1 set /A nPruned=%nFnd%-%nExcl% :end echo %0 end: outfile=%OUTFILE% call echoRestore endlocal & set psOUTFILE=%OUTFILE%& set psNPruned=%nPruned%
rem From %1, white lines on black background, fully opaque, rem makes output %2 skeletonised with short stubs removed. @rem @rem If the given output, %2, is XYZ.png, @rem creates files: @rem XYZ.png skeletonised image, pruned. @rem XYZ_ends.png just the line-ends @rem XYZ_ends.csv list of the line-ends. @rem @rem Updated: @rem 6-August-2022 for IM v7 @rem @if "%2"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave call %PICTBAT%setInOut %1 sps echo %0: %1 %2 if not "%2"=="" if not "%2"=="." set OUTFILE=%2 for /F %%F in ("%OUTFILE%") do ( set OUTBASE=%%~dpnF set OUTEXT=%%~xF ) set TEMPEXT=.png set TEMP_SKEL=%OUTBASE%_sps_temp%TEMPEXT% set OUT_ENDS_CSV=%OUTBASE%_ends.csv set OUT_ENDS=%OUTBASE%_ends%OUTEXT% %IMG7%magick ^ %INFILE% ^ -threshold 50%% ^ -virtual-pixel Edge ^ -morphology Thinning:-1 Skeleton -clamp ^ -alpha off ^ %TEMP_SKEL% if ERRORLEVEL 1 exit /B 1 :: Calculate approx line width. for /F "usebackq" %%L in (`%IMG7%magick ^ %INFILE% ^ -alpha off ^ -format "MN_MAIN=%%[fx:mean]" ^ info:`) do set %%L for /F "usebackq" %%L in (`%IMG7%magick ^ %TEMP_SKEL% ^ -format "MN_SKEL=%%[fx:mean]" ^ info:`) do set %%L if "%MN_SKEL%"=="0" ( echo %0: skeleton [%TEMP_SKEL%] has zero mean exit /B 1 ) for /F "usebackq" %%L in (`%IMG7%magick identify ^ -format "LINE_THK=%%[fx:%MN_MAIN%/%MN_SKEL%]\nLINE_THK_INT=%%[fx:int(%MN_MAIN%/%MN_SKEL%+0.5)]" ^ xc:`) do set %%L echo %0: LINE_THK=%LINE_THK% LINE_THK_INT=%LINE_THK_INT% call %PICTBAT%pruneStubs %TEMP_SKEL% %OUTFILE% %LINE_THK_INT% if ERRORLEVEL 1 exit /B 1 %IMG7%magick ^ %OUTFILE% ^ -morphology HMT LineEnds ^ -alpha off ^ +write %OUT_ENDS% ^ -transparent Black ^ sparse-color: |sed -e 's/ /\n/g' >%OUT_ENDS_CSV% echo %0 end: outfile=%OUTFILE% call echoRestore endlocal & set spsOUTFILE=%OUTFILE%& set spsCSVFILE=%OUT_ENDS_CSV%& set spsENDS=%OUT_ENDS%& set spsLINE_THK_INT=%LINE_THK_INT%
rem Given %1 is text file containing integer coordinates separated by comma and optional spaces, rem one point per line, rem outputs lines to text file %2 (can be same name as %1) rem %3 is a floating-point poximity threshold, rem finds all pairs of points that are less than or equal to the given threshold. @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave set INFILE=%1 if not exist %INFILE% exit /B 1 set OUTFILE=%2 if "%OUTFILE%"=="." set OUTFILE= if "%OUTFILE%"=="" set OUTFILE=pd.lis set GAP_LIMIT=%3 if "%GAP_LIMIT%"=="." set GAP_LIMIT= if "%GAP_LIMIT%"=="" set GAP_LIMIT=10 rem type %INFILE% set II=0 for /F "tokens=1,2 delims=, " %%X in (%INFILE%) do ( rem echo %%X,%%Y set lineEnd[!II!].x=%%X set lineEnd[!II!].y=%%Y set /A II+=1 ) set /A numPnts=%II% set /A lastEnd=%II%-1 set /A lastEndm1=%II%-2 rem set lineEnd (for /L %%A in (0,1,%lastEnd%m1) do ( set nA=%%A set /A X0=lineEnd[!nA!].x set /A Y0=lineEnd[!nA!].y set /A firstB=%%A+1 for /L %%B in (!firstB!,1,%lastEnd%) do ( set nB=%%B set /A X1=lineEnd[!nB!].x set /A Y1=lineEnd[!nB!].y rem echo !X0!,!Y0!, !X1!,!Y1! for /F "usebackq" %%L in (`%IM%identify ^ -format "DO_IT=%%[fx:hypot(!X1!-!X0!,!Y1!-!Y0!)<=%GAP_LIMIT%?1:0]" ^ xc:`) do set %%L if !DO_IT!==1 echo line !X0!,!Y0! !X1!,!Y1! ) )) >%OUTFILE% type %OUTFILE% call echoRestore endlocal & set pdOUTFILE=%OUTFILE%& set pdNUM_PNTS=%numPnts%
rem Given %1 is white points on black background, rem %2 is CSV text file of those coordinates, rem outputs %3 text file containing pairs of points that are close. rem %4 is proximity threshold. @rem @rem Output file may be empty (returns prdxNUM_PNTS=0). @rem @rem Updated: @rem 6-August-2022 for IM v7 @rem @if "%2"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave set INFILE=%1 if not exist %INFILE% exit /B 1 set CSVFILE=%2 if not exist %CSVFILE% exit /B 1 set OUTFILE=%3 if "%OUTFILE%"=="." set OUTFILE= if "%OUTFILE%"=="" set OUTFILE=prdx.lis set GAP_LIMIT=%4 if "%GAP_LIMIT%"=="." set GAP_LIMIT= if "%GAP_LIMIT%"=="" set GAP_LIMIT=10 set CRP_FILE=prdxCrop.miff set OTHERS_CSV=prdxCrop.csv set OTHERS_CSV2=prdxCrop2.csv set TEMP_IN=prdx.miff %IMG7%magick %INFILE% %TEMP_IN% if ERRORLEVEL 1 exit /B 1 set /A CWH=2*%GAP_LIMIT%+1 set II=0 (for /F "tokens=1,2 delims=, " %%X in (%CSVFILE%) do ( rem echo %%X,%%Y set CW=%CWH% set CH=%CWH% set CX=%GAP_LIMIT% set CY=%GAP_LIMIT% set /A XL=%%X-%GAP_LIMIT% set /A YT=%%Y-%GAP_LIMIT% if !XL! LSS 0 ( set /A CW+=!XL! set /A CX+=!XL! set XL=0 ) if !YT! LSS 0 ( set /A CH+=!YT! set /A CY+=!YT! set YT=0 ) for /F "usebackq" %%L in (`%IMG7%magick ^ %TEMP_IN% ^ -crop !CW!x!CH!+!XL!+!YT! +repage ^ -fill Black ^ -draw "color !CX!,!CY! point" ^ -format "MAX=%%[fx:maxima]" ^ +write info:^ -transparent Black ^ sparse-color:%OTHERS_CSV%`) do set %%L if not !MAX!==0 ( rem echo %%X,%%Y rem %IMG7%magick %CRP_FILE% -transparent Black sparse-color:|sed -e 's/ /\n/g' >%OTHERS_CSV% sed -e 's/ /\n/g' %OTHERS_CSV% >%OTHERS_CSV2% for /F "tokens=1,2 delims=, " %%A in (%OTHERS_CSV2%) do ( set /A X1=!XL!+%%A set /A Y1=!YT!+%%B set DOIT=1 if !Y1! LSS %%Y set DOIT=0 if !Y1! EQU %%Y if !X1! LSS %%X set DOIT=0 if !X1! EQU %%X if !Y1! EQU %%Y set DOIT=0 if !DOIT!==1 ( for /F "usebackq" %%L in (`%IMG7%magick identify ^ -format "DOIT=%%[fx:hypot(!X1!-%%X,!Y1!-%%Y)<=%GAP_LIMIT%?1:0]" ^ xc:`) do set %%L ) if !DOIT!==1 ( echo line %%X,%%Y,!X1!,!Y1! set /A II+=1 ) ) ) ) ) >%OUTFILE% :: Note: When a line is short, its two line-ends are close, so will be joined. echo %0: II=%II% call echoRestore endlocal & set prdxOUTFILE=%OUTFILE%& set prdxNUM_PNTS=%numPnts%
rem From image of white lines on black background, rem writes output %2 rem joining line-ends that are within proximity threshold %3. @rem @rem Also uses: @rem skelLineEnds if 1, skeletonises the output. @rem threshLineEnds if 1, thresholds the output at 50%. @rem @rem Updated: @rem 6-August-2022 for IM v7 @rem @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave call %PICTBAT%setInOut %1 ple if not "%2"=="" if not "%2"=="." set OUTFILE=%2 set PROX_THRESH=%3 if "%PROX_THRESH%"=="" set PROX_THRESH=. call %PICTBAT%skelPS ^ %INFILE% %BASENAME%_%sioCODE%.png if ERRORLEVEL 1 exit /B 1 call %PICTBAT%pointsRdx ^ %spsENDS% ^ %spsCSVFILE% %BASENAME%_lines_%sioCODE%.scr %PROX_THRESH% if ERRORLEVEL 1 exit /B 1 echo %0: prdxOUTFILE=%prdxOUTFILE% if "%skelLineEnds%"=="1" ( set sSKEL=-morphology Thinning:-1 skeleton -clamp ) else ( set sSKEL= ) if "%threshLineEnds%"=="1" ( set sTHRESH=-threshold 50%% ) else ( set sTHRESH= ) %IMG7%magick ^ %INFILE% ^ +antialias ^ -stroke White ^ -strokewidth %spsLINE_THK_INT% ^ -draw "@%prdxOUTFILE%" ^ %sSKEL% %sTHRESH% ^ %OUTFILE% if ERRORLEVEL 1 exit /B 1 call echoRestore @endlocal & set %sioCODE%OUTFILE=%OUTFILE%
rem From image of white lines on black background, fully opaque, rem writes output %2 rem by extending pairs of line-ends that are within proximity threshold %3 of each other. rem %4 is method for holes: rem circles holes are circles of given radius centred on centres between line-ends rem squares holes are squares of given radius centred on centres between line-ends rem lines holes are lines with rounded ends with spsLINE_THK_INT between line-ends rem rem Also uses: rem skelLineEnds if 1, skeletonises the output. @rem @rem Updated: @rem 6-August-2022 for IM v7 @rem @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave call %PICTBAT%setInOut %1 ele if not "%2"=="" if not "%2"=="." set OUTFILE=%2 set PROX_THRESH=%3 if "%PROX_THRESH%"=="." set PROX_THRESH= if "%PROX_THRESH%"=="" set PROX_THRESH=10 set METHOD=%4 if "%METHOD%"=="." set METHOD= if "%METHOD%"=="" set METHOD=circles call %PICTBAT%skelPS ^ %INFILE% %BASENAME%_%sioCODE%.png if ERRORLEVEL 1 exit /B 1 echo %0: done skelPS call %PICTBAT%pointsRdx ^ %spsENDS% ^ %spsCSVFILE% %BASENAME%_lines_%sioCODE%.scr %PROX_THRESH% if ERRORLEVEL 1 exit /B 1 echo %0: done pointsRdx set /A RAD=%PROX_THRESH%/2 set HOLES_SCR=%BASENAME%_holes_%sioCODE%.scr echo %0: spsLINE_THK_INT=%spsLINE_THK_INT% set sLINWID= if /I %METHOD%==circles ( call %PICTBAT%lines2circs %prdxOUTFILE% %HOLES_SCR% %RAD% if ERRORLEVEL 1 exit /B 1 ) else if /I %METHOD%==squares ( call %PICTBAT%lines2sqs %prdxOUTFILE% %HOLES_SCR% %RAD% if ERRORLEVEL 1 exit /B 1 ) else if /I %METHOD%==lines ( rem FIXME: more than the line width? set /A wider=2*%spsLINE_THK_INT% call %PICTBAT%lines2RndedLines %prdxOUTFILE% %HOLES_SCR% if ERRORLEVEL 1 exit /B 1 set sLINWID=-stroke Black -linewidth !wider! ) else ( echo %0: Unknown METHOD=%METHOD% exit /B 1 ) echo %0: HOLES_SCR=%HOLES_SCR% type %HOLES_SCR% if "%skelLineEnds%"=="1" ( set sSKEL=-morphology Thinning:-1 skeleton -clamp ) else ( set sSKEL= ) set /A nLSR=(%RAD%+3)/4 set /A WR=%spsLINE_THK_INT%+2 set /A WR2=3*%WR% if %nLSR% LSS %WR2% set nLSR=%WR2% set /A nLSRp=%nLSR%+2 set FH_PARAMS=window_radius %WR% lsr %nLSR% auto_limit_search off auto_repeat v echo FH_PARAMS=%FH_PARAMS% :: FIXME: wrong check if "%l2cCIRCS%"=="0" ( echo %0: no holes %IMG7%magick ^ %INFILE% ^ %OUTFILE% ) else ( echo %0: with holes sSKEL="%sSKEL%" %IM7DEV%magick ^ %INFILE% ^ +write mpr:IMG ^ +antialias ^ ^( mpr:IMG ^ -fill White -colorize 100 ^ -fill Black ^ %sLINWID% ^ -draw "@%HOLES_SCR%" ^ mpr:IMG -compose Lighten -composite ^ -fill Black +opaque White ^ ^) ^ -antialias ^ -alpha off -compose CopyOpacity -composite ^ -process 'fillholespri %FH_PARAMS% dt 0.01 copy window' ^ -process 'fillholespri %FH_PARAMS% dt 0.1' ^ -process 'fillholespri %FH_PARAMS% dt 0.5' ^ -process 'fillholespri %FH_PARAMS%' ^ -compose Over ^ -alpha off ^ %OUTFILE% if not "%sSKEL%"=="" %IMG7%magick ^ %OUTFILE% ^ %sSKEL% ^ -alpha off ^ %OUTFILE% echo %0: with holes done ) if ERRORLEVEL 1 exit /B 1 call echoRestore endlocal & set %sioCODE%OUTFILE=%OUTFILE%
rem From %1 image of white lines on black background, fully opaque, rem writes output %2 rem by joining line-ends with other lines that are within proximity threshold %3. rem rem Also uses: rem skelLineEnds if 1, skeletonises the output. @rem @rem Updated: @rem 6-August-2022 for IM v7 @rem @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave call %PICTBAT%setInOut %1 tjle set TMP_IN=%BASENAME%_tjle_in.miff set LINE_SCR=%BASENAME%_tjle.scr del %LINE_SCR% 2>nul if not "%2"=="" if not "%2"=="." set OUTFILE=%2 set PROX_THRESH=%3 if "%PROX_THRESH%"=="." set PROX_THRESH= if "%PROX_THRESH%"=="" set PROX_THRESH=10 %IMG7%magick %INFILE% %TMP_IN% call %PICTBAT%skelPS ^ %TMP_IN% %BASENAME%_%sioCODE%.png if ERRORLEVEL 1 exit /B 1 set II=0 set /A CRP_WH=2*%PROX_THRESH%+1 for /F "tokens=1,2 delims=, " %%X in (%spsCSVFILE%) do ( rem echo %0: %%X,%%Y set LE_X=%%X set LE_Y=%%Y set /A CRP_L=!LE_X!-%PROX_THRESH% set /A CRP_T=!LE_Y!-%PROX_THRESH% set nwCX=%PROX_THRESH% set nwCY=%PROX_THRESH% set CRP_W=%CRP_WH% set CRP_H=%CRP_WH% if !CRP_L! LSS 0 ( set /A nwCX+=!CRP_L! set /A CRP_W+=!CRP_L! set CRP_L=0 ) if !CRP_T! LSS 0 ( set /A nwCY+=!CRP_T! set /A CRP_H+=!CRP_T! set CRP_T=0 ) for /F "usebackq tokens=1-3 delims=, " %%A in (`%IM7DEV%magick ^ %TMP_IN% ^ -fill Black +opaque White ^ -crop !CRP_W!x!CRP_H!+!CRP_L!+!CRP_T! +repage ^ -draw "color !nwCX!,!nwCY! floodfill" ^ -process 'nearestwhite cx !nwCX! cy !nwCY!' ^ NULL: 2^>^&1`) do ( if "%%A"=="nearestwhite:" ( set ncCST_X=%%B if not "%%B"=="none" ( set ncCST_Y=%%C set /A ML_X=!CRP_L!+%%B set /A ML_Y=!CRP_T!+%%C ) ) ) rem echo %0: ncCST_X=!ncCST_X! ncCST_Y=!ncCST_Y! if not "!ncCST_X!"=="none" ( rem FIXME: check hypot echo line !LE_X!,!LE_Y! !ML_X!,!ML_Y! echo line !LE_X!,!LE_Y! !ML_X!,!ML_Y! >>!LINE_SCR! ) set /A II+=1 ) if ERRORLEVEL 1 exit /B 1 if "%skelLineEnds%"=="1" ( set sSKEL=-morphology Thinning:-1 skeleton -clamp ) else ( set sSKEL= ) if exist %LINE_SCR% ( rem type %LINE_SCR% %IMG7%magick ^ %INFILE% ^ +antialias ^ -stroke White ^ -strokewidth %spsLINE_THK_INT% ^ -draw "@%LINE_SCR%" ^ %sSKEL% ^ -alpha off ^ %OUTFILE% ) else ( %IMG7%magick ^ %INFILE% ^ %sSKEL% ^ -alpha off ^ %OUTFILE% ) call echoRestore endlocal & set %sioCODE%OUTFILE=%OUTFILE%
rem From image of white lines on black background, rem writes output %2 rem combining methods, each with proximity threshold %3. @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion rem @call echoOffSave call %PICTBAT%setInOut %1 cle if not "%2"=="" if not "%2"=="." set OUTFILE=%2 set PROX_THRESH=%3 if "%PROX_THRESH%"=="" set PROX_THRESH=. call %PICTBAT%extLineEnds %INFILE% %OUTFILE% %PROX_THRESH% if ERRORLEVEL 1 exit /B 1 call %PICTBAT%proxLineEnds %OUTFILE% %OUTFILE% %PROX_THRESH% if ERRORLEVEL 1 exit /B 1 call %PICTBAT%TjuncLineEnds %OUTFILE% %OUTFILE% %PROX_THRESH% if ERRORLEVEL 1 exit /B 1 call echoRestore @endlocal & set %sioCODE%OUTFILE=%OUTFILE%
rem Given %1 is text file with lines (eg from pointsDist.bat) rem writes circles to text file %2 [default l2c.lis] rem of radius %3 [10] rem centred on mid-points of coordinate-pairs from %1. @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave set INFILE=%1 if not exist %INFILE% exit /B 1 set OUTFILE=%2 if "%OUTFILE%"=="." set OUTFILE= if "%OUTFILE%"=="" set OUTFILE=l2c.lis del %OUTFILE% 2>nul set RAD=%3 if "%RAD%"=="." set RAD= if "%RAD%"=="" set RAD=10 set nCIRCS=0 (for /F "tokens=2-5 delims=, " %%A in (%INFILE%) do ( set /A XC=^(%%A+%%C^)/2 set /A YC=^(%%B+%%D^)/2 set /A X2=!XC!+%RAD% echo circle !XC!,!YC! !X2!,!YC! set /A nCIRCS+=1 )) >%OUTFILE% call echoRestore endlocal & set l2cOUTFILE=%OUTFILE%&set l2cCIRCS=%nCIRCS%
rem Given %1 is text file with lines (eg from pointsDist.bat) rem writes squares to text file %2 [default l2c.lis] rem of radius %3 [10] rem centred on mid-points of coordinate-pairs from %1. @if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1 @setlocal enabledelayedexpansion @call echoOffSave set INFILE=%1 if not exist %INFILE% exit /B 1 set OUTFILE=%2 if "%OUTFILE%"=="." set OUTFILE= if "%OUTFILE%"=="" set OUTFILE=l2c.lis set RAD=%3 if "%RAD%"=="." set RAD= if "%RAD%"=="" set RAD=10 (for /F "tokens=2-5 delims=, " %%A in (%INFILE%) do ( set /A X0=^(%%A+%%C^)/2-%RAD% set /A Y0=^(%%B+%%D^)/2-%RAD% set /A X1=^(%%A+%%C^)/2+%RAD% set /A Y1=^(%%B+%%D^)/2+%RAD% echo rectangle !X0!,!Y0! !X1!,!Y1! )) >%OUTFILE% call echoRestore endlocal
All images on this page were created by the commands shown, using:
%IMG7%magick -version
Version: ImageMagick 7.1.0-49 Q16-HDRI x64 7a3f3f1:20220924 https://imagemagick.org Copyright: (C) 1999 ImageMagick Studio LLC License: https://imagemagick.org/script/license.php Features: Cipher DPC HDRI OpenCL 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 (193331630)
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 mendbrkln.h1. To re-create this web page, run "procH1 mendbrkln".
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 17-June-2016.
Page created 29-Sep-2022 06:36:54.
Copyright © 2022 Alan Gibson.