File Coverage

blib/lib/Image/Find/Paths.pm
Criterion Covered Total %
statement 310 321 96.5
branch 67 76 88.1
condition 115 126 91.2
subroutine 34 35 97.1
pod 20 22 90.9
total 546 580 94.1


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #-------------------------------------------------------------------------------
3             # Image::Find::Paths - Find paths in an image.
4             # Philip R Brenan at gmail dot com, Appa Apps Ltd Inc., 2018
5             #-------------------------------------------------------------------------------
6             package Image::Find::Paths;
7             our $VERSION = "20180505";
8             require v5.16;
9 1     1   719 use warnings FATAL => qw(all);
  1         8  
  1         41  
10 1     1   6 use strict;
  1         2  
  1         37  
11 1     1   6 use Carp qw(confess);
  1         3  
  1         81  
12 1     1   569 use Data::Dump qw(dump);
  1         8480  
  1         75  
13 1     1   943 use Data::Table::Text qw(:all);
  1         70833  
  1         621  
14             #use Time::HiRes qw(time);
15 1     1   11 use utf8;
  1         3  
  1         7  
16              
17             #my %exec; sub e($) {$exec{$_[0]}++}
18              
19             #1 Methods # Find paths in an image
20              
21             sub new($) #S Find paths in an image represented as a string.
22 4     4 1 10 {my ($string) = @_; # String of blanks; non blanks; new lines defining the image
23 4         34 my @lines = split /\n/, $string;
24 4         13 my $count; # Number of active pixels
25             my %image; # {x}{y} of active pixels
26 4         0 my $x; # Image dimension in x
27 4         14 for my $j(0..$#lines) # Load active pixels
28 49         75 {my $line = $lines[$j];
29 49 100 100     148 $x = length($line) if !defined($x) or length($line) > $x; # Longest line
30 49         79 for my $i(0..length($line)-1) # Parse each line
31 5461 100       10286 {$image{$i}{$j} = 0, $count++ if substr($line, $i, 1) ne q( );
32             }
33             }
34              
35 4         38 my $d = bless{image=>\%image, x=>$x, y=>scalar(@lines), count=>$count, # Create image of paths
36             partitions=>{}, partitionStart=>{}, partitionEnd=>{},
37             partitionPath=>{}};
38              
39 4         19 $d->partition; # Partition the image
40 4         75 $d->start($_), $d->end($_) for 1..$d->numberOfPaths; # Find a start point for each partition
41 4         43 my $h = $d->height; # Clone and add height
42 4         19 $d->shortestPathBetweenEndPoints($h, $_) for 1..$d->numberOfPaths; # Find the longest path in each partition
43 4         789 $d->widthOfPaths;
44 4         1221 $d # Return new image with path details
45             } # new
46              
47             sub clone($) #P Clone an image.
48 1     1 1 3 {my ($i) = @_; # Image
49 1         1 my %partitions; # Clone partitions
50 1         12 for my $p(keys %{$i->partitions})
  1         18  
51 6         35 {for my $x(keys %{$i->partitions->{$p}})
  6         76  
52 32         174 {for my $y(keys %{$i->partitions->{$p}{$x}})
  32         420  
53 94         1602 {$partitions{$p}{$x}{$y} = $i->partitions->{$p}{$x}{$y};
54             }
55             }
56             }
57              
58 1         23 bless {%$i, partitions=>\%partitions}; # Cloned image
59             } # clone
60              
61             sub clonePartition($$) #P Clone a partition of an image.
62 27     27 1 52 {my ($i, $partition) = @_; # Image, partition
63 27         42 my %partition; # Cloned partition
64              
65 27         49 for my $x(keys %{$i->partitions->{$partition}})
  27         420  
66 972         8756 {for my $y(keys %{$i->partitions->{$partition}{$x}})
  972         17297  
67 14124         327529 {$partition{$x}{$y} = $i->partitions->{$partition}{$x}{$y};
68             }
69             }
70              
71 27         402 my $I = bless {%$i}; # Clone image quickly
72 27         59 $I->partitions = {%{$i->partitions}}; # Clone partitions quickly
  27         434  
73 27         674 $I->partitions->{$partition} = \%partition; # Replace cloned partition
74 27         160 $I # Return new image
75             } # clonePartition
76              
77             sub countPixels($) #P Count the pixels in an image.
78 0     0 1 0 {my ($i) = @_; # Image
79              
80 0         0 my $count;
81 0         0 for my $p(keys %{$i->partitions})
  0         0  
82 0         0 {for my $x(keys %{$i->partitions->{$p}})
  0         0  
83 0         0 {for my $y(keys %{$i->partitions->{$p}{$x}})
  0         0  
84 0         0 {++$count
85             }
86             }
87             }
88              
89             $count
90 0         0 } # countPixels
91              
92             sub height($) #P Clone an image adding height to each pixel.
93 4     4 1 13 {my ($i) = @_; # Image
94              
95 4         7 my %contours; # Clone partitions
96 4         7 my $pixels = 0;
97 4         7 for my $p(keys %{$i->partitions}) # Base
  4         64  
98 9         36 {for my $x(keys %{$i->partitions->{$p}})
  9         123  
99 324         453 {for my $y(keys %{$i->partitions->{$p}{$x}})
  324         4314  
100 4708         7994 {$contours{$p}{1}{$x}{$y} = 1;
101 4708         5404 $pixels++;
102             }
103             }
104             }
105              
106 4         25 for my $p(keys %contours) # Contours
107 9         21 {for my $h(1..$pixels)
108 26         47 {my $count;
109 26         38 for my $x(keys %{$contours{$p}{$h}})
  26         829  
110 2231         3942 {for my $y(keys %{$contours{$p}{$h}{$x}})
  2231         11009  
111 20169         49898 {my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
112 20169 50 100     196666 if (exists $contours{$p}{$h }{$x}{$𝕪} and
      100        
      100        
      100        
      100        
      100        
      66        
113             exists $contours{$p}{$h }{$x}{$𝘆} and
114             exists $contours{$p}{$h }{$𝘅}{$y} and
115             exists $contours{$p}{$h }{$𝘅}{$𝘆} and
116             exists $contours{$p}{$h }{$𝘅}{$𝕪} and
117             exists $contours{$p}{$h }{$𝕩}{$y} and
118             exists $contours{$p}{$h }{$𝕩}{$𝘆} and
119             exists $contours{$p}{$h }{$𝕩}{$𝕪})
120 15461         40069 { $contours{$p}{$h+1}{$x}{$y}++;
121 15461         31031 ++$count;
122             }
123             }
124             }
125 26 100       442 last unless defined $count;
126             }
127             }
128              
129 4         8 my %partitions; # Project contours to obtain height partition
130 4         12 for my $p(keys % contours)
131 9         14 {for my $h(sort{$a<=>$b}keys %{$contours{$p}})
  37         64  
  9         38  
132 26         39 {for my $x(keys %{$contours{$p}{$h}})
  26         445  
133 2263         3403 {for my $y(keys %{$contours{$p}{$h}{$x}})
  2263         8132  
134 20169         44576 {$partitions{$p}{$x}{$y} = $h;
135             }
136             }
137             }
138             }
139              
140 4         2204 bless {%$i, partitions=>\%partitions}; # Cloned image
141             } # height
142              
143             sub numberOfPaths($) # Number of paths in the image.
144 30     30 1 248 {my ($i) = @_; # Image
145 30         45 scalar(keys %{$i->partitions})
  30         571  
146             } # numberOfPaths
147              
148             sub partition($) #P Partition the images into disjoint sets of connected points.
149 4     4 1 8 {my ($i) = @_; # Image
150 4         6 for my $x(sort{$a<=>$b} keys %{$i->image}) # Stabilize partition numbers to make testing possible
  1975         2144  
  4         96  
151 322         3168 {for my $y(sort{$a<=>$b} keys %{$i->image->{$x}})
  13162         26781  
  322         7311  
152 4710 100       136318 {$i->mapPartition($x, $y) if $i->image->{$x}{$y} == 0; # Bucket fill anything that touches this pixels
153             }
154             }
155             } # partition
156              
157             sub mapPartition($$$) #P Locate the pixels in the image that are connected to a pixel with a specified value.
158 11     11 1 103 {my ($i, $x, $y) = @_; # Image, x coordinate of first point in partition, y coordinate of first point in partition
159 11         37 my $p = $i->image->{$x}{$y} = $i->numberOfPaths+1; # Next partition
160 11         409 $i->partitions->{$p}{$x}{$y}++; # Add first pixel to this partition
161 11         104 my $pixelsInPartition = 0;
162              
163 11         200 for(1..$i->count) # Worst case - each pixel is a separate line
164 183         487 {my $changed = 0; # Number of pixels added to this partition on this pass
165 183         323 for my $x(keys %{$i->image}) # Each pixel
  183         3295  
166 33681         146477 {for my $y(keys %{$i->image->{$x}})
  33681         558505  
167 524355 100       10012343 {next if $i->image->{$x}{$y} == $p; # Already partitioned
168 264866         5450723 my $I = $i->image;
169 264866         1645802 my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
170 264866 100 100     3830304 if (exists($I->{$𝘅}) && exists($I->{$𝘅}{$y}) && $I->{$𝘅}{$y} == $p or # Add this pixel to the partition if a neigboring pixel exists and is already a part of the paritition
      100        
      66        
      100        
      100        
      100        
      100        
      100        
      66        
      100        
      100        
171             exists($I->{$x}) && exists($I->{$x}{$𝘆}) && $I->{$x}{$𝘆} == $p or
172             exists($I->{$𝕩}) && exists($I->{$𝕩}{$y}) && $I->{$𝕩}{$y} == $p or
173             exists($I->{$x}) && exists($I->{$x}{$𝕪}) && $I->{$x}{$𝕪} == $p)
174 4699         80048 {$i->image->{$x}{$y} = $p;
175 4699         25945 ++$changed;
176 4699         72910 ++$i->partitions->{$p}{$x}{$y}; # Pixels in this partition
177 4699         30870 ++$pixelsInPartition;
178             }
179             }
180             }
181 183 100       4012 last unless $changed; # No more pixels in parition to consider
182             }
183              
184 11 100       47 if ($pixelsInPartition <= 1) # Remove partitions of just one pixel
185 2         3 {for my $x(keys %{$i->image})
  2         29  
186 59         339 {for my $y(keys %{$i->image->{$x}})
  59         733  
187 191 100       3032 {delete $i->image->{$x}{$y} if $i->image->{$x}{$y} == $p;
188 191 100       831 delete $i->image->{$x} unless keys %{$i->image->{$x}}; # Remove containing hash if now empty
  191         2405  
189             }
190             }
191 2         38 delete $i->partitions->{$p}
192             }
193             } # mapPartition
194              
195             sub start($$) #P Find the starting point for a path in a partition.
196 9     9 1 76 {my ($i, $partition) = @_; # Image, partition
197 9         14 my $remove; # Removal sequence
198              
199 9         24 for my $x((sort{$a<=>$b} keys %{$i->partitions->{$partition} })[0]) # Find the first point in a partition
  1924         3529  
  9         136  
200 9         29 {for my $y((sort{$a<=>$b} keys %{$i->partitions->{$partition}{$x}})[0])
  104         200  
  9         159  
201 9         355 {$remove = [$x, $y];
202             }
203             }
204              
205 9         53 $i->partitionStart->{$partition} = # Record start point
206             $i->traverseToOtherEnd($partition, @$remove);
207             } # start
208              
209             sub end($$) #P Find the other end of a path in a partition.
210 9     9 1 76 {my ($i, $partition) = @_; # Image, partition
211             $i->partitionEnd->{$partition} = # Record start point
212 9         15 $i->traverseToOtherEnd($partition, @{$i->partitionStart->{$partition}});
  9         134  
213             } # end
214              
215             sub traverseToOtherEnd($$$$) #P Traverse to the other end of a partition.
216 18     18 1 78 {my ($I, $partition, $X, $Y) = @_; # Image, partition, start x coordinate, start y coordinate
217 18         50 my $i = $I->clonePartition($partition); # Clone the specified partition so that we can remove pixels once they have been processed to spped up the remaining search
218 18         50 my @remove = ([$X, $Y]); # Removal sequence
219 18         52 my %remove = ($X=>{$Y=>1}); # Removal sequence deduplication
220 18         26 my $last; # We know that there are two or more pixels in the paritition
221 18         48 while(@remove)
222 9416         18792 {$last = shift @remove;
223 9416         20673 my ($x, $y) = @$last;
224 9416         181897 delete $i->partitions->{$partition}{$x}{$y};
225 9416         62864 $remove{$x}{$y}++;
226 9416         19934 my @r = $i->searchArea($partition, $x, $y);
227 9416         17954 my @s = grep {my ($x, $y) = @$_; !$remove{$x}{$y}} @r;
  18020         34121  
  18020         50178  
228 9416         18588 for(@r)
229 18020         31590 {my ($x, $y) = @$_; $remove{$x}{$y}++;
  18020         39620  
230             }
231 9416         15417 push @remove, @s;
232 9416         20026 $i->searchArea($partition, $x, $y);
233             }
234             $last # Last point is the other end
235 18         1825 } # traverseToOtherEnd
236              
237             sub searchArea($$$$) #P Return the pixels to search from around a given pixel.
238 18841     18841 1 37695 {my ($i, $partition, $x, $y) = @_; # Image, partition, x coordinate of center of search, y coordinate of center of search.
239 18841         26258 my @s; # Pixels to search from
240 18841         346808 my $P = $i->partitions->{$partition};
241 18841         123300 my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
242 18841 100 100     84156 push @s, [$𝘅, $y] if exists $P->{$𝘅} and exists $P->{$𝘅}{$y};
243 18841 100 66     73457 push @s, [$x, $𝘆] if exists $P->{$x} and exists $P->{$x}{$𝘆};
244 18841 100 66     72891 push @s, [$x, $𝕪] if exists $P->{$x} and exists $P->{$x}{$𝕪};
245 18841 100 100     75805 push @s, [$𝕩, $y] if exists $P->{$𝕩} and exists $P->{$𝕩}{$y};
246             @s # Return all possible pixels
247 18841         57919 } # searchArea
248              
249             sub checkAtLevelOne($$$) #P Confirm that the specified pixel is at level one.
250 18     18 1 102 {my ($i, $partition, $pixel) = @_; # Image, partition, pixel
251 18         35 my ($x, $y) = @$pixel;
252 18         241 my $h = $i->partitions->{$partition}{$x}{$y};
253 18 50       98 defined($h) or confess "No pixel in partition=$partition at x=$x, y=$y";
254 18 50       40 $h == 1 or confess "Pixel in partition=$partition at x=$x, y=$y is $h not one";
255             } # checkAtLevelOne
256              
257             sub searchAreaHighest($$$$$$) #P Return the highest pixels to search from around a given pixel.
258 37917     37917 1 68118 {my ($i, $partition, $seen, $depth, $x, $y) = @_; # Image, partition, pixels already visited, depth of search, x coordinate of center of search, y coordinate of center of search.
259 37917         46109 my @s; # Pixels to search from
260 37917         711989 my $P = $i->partitions->{$partition};
261 37917         234012 my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
262 37917 100 100     310534 push @s, [$𝘅, $y, $P->{$𝘅}{$y}] if exists $P->{$𝘅} and exists $P->{$𝘅}{$y} and !$seen->{$𝘅}{$y} || $seen->{$𝘅}{$y} > $depth;
      100        
      100        
263 37917 100 66     217543 push @s, [$x, $𝘆, $P->{$x}{$𝘆}] if exists $P->{$x} and exists $P->{$x}{$𝘆} and !$seen->{$x}{$𝘆} || $seen->{$x}{$𝘆} > $depth;
      100        
      100        
264 37917 100 66     247632 push @s, [$x, $𝕪, $P->{$x}{$𝕪}] if exists $P->{$x} and exists $P->{$x}{$𝕪} and !$seen->{$x}{$𝕪} || $seen->{$x}{$𝕪} > $depth;
      100        
      100        
265 37917 100 100     208047 push @s, [$𝕩, $y, $P->{$𝕩}{$y}] if exists $P->{$𝕩} and exists $P->{$𝕩}{$y} and !$seen->{$𝕩}{$y} || $seen->{$𝕩}{$y} > $depth;
      100        
      100        
266 37917 100       78760 return @s unless @s > 1; # Nothing further to search or just one pixel - which is then the higest pixel returned
267 34763         88291 my @S = sort {$$b[2] <=> $$a[2]} @s; # Highest pixels first
  35896         98143  
268 34763         53179 my $h = $S[0][2]; # Highest height
269 34763         54407 grep {$$_[2] == $h} @S # Remove lower pixels
  70097         186076  
270             } # searchAreaHighest
271              
272             sub shortestPathBetweenEndPoints($$$) #P Find the shortest path between the start and the end points of a partition.
273 9     9 1 95 {my ($I, $i, $partition) = @_; # Image, image height clone, partition
274              
275 9         144 $i->checkAtLevelOne($partition, $i->partitionStart->{$partition}); # The end points should be at level one because that is the boundary
276 9         132 $i->checkAtLevelOne($partition, $i->partitionEnd ->{$partition});
277              
278 9         12 my ($X, $Y) = @{$i->partitionEnd->{$partition}}; # The end point for this partition
  9         127  
279 9         148 my @path = ($i->partitionStart->{$partition}); # A possible path
280 9         42 my @shortestPath; # Shortest path so far
281 9         16 my @search = [$i->searchArea($partition, @{$path[0]})]; # Initial search area is the pixels around the start pixel
  9         29  
282 9         14 my %seen; # Pixels we have already visited along the possible path
283              
284 9         31 while(@search) # Find the shortest path amongst all the possible paths
285 75843 50       136237 {@path == @search or confess "Search and path depth mismatch"; # These two arrays must stay in sync because their dimensions reflects the progress along the possible path
286 75843         106204 my $search = $search[-1]; # Pixels to search for latest path element
287 75843 100       121595 if (!@$search) # Nothing left to search at this level
288 37917         45548 {pop @search; # Remove search level
289 37917         85665 pop @path; # Pixel to remove from possible path
290             }
291             else
292 37926         46484 {my ($x, $y) = @{pop @$search}; # Next pixel to add to path
  37926         70809  
293 37926 100 100     90933 if ($x == $X and $y == $Y)
294 9 50 33     61 {@shortestPath = @path if !@shortestPath or @path < @shortestPath;
295 9         13 pop @search; # Remove search level
296 9         23 pop @path; # Pixel to remove from possible path
297             }
298             else # Extend the search
299 37917         79275 {push @path, [$x, $y]; # Extend the path
300 37917         52742 my $P = scalar(@path); # Current path length
301             # e(q(shortestPath));
302 37917         91953 my @r = $i->searchAreaHighest($partition, \%seen, $P, $x, $y);
303 37917         74395 for(@r) # Update visitation status
304 37918         69032 {my ($x, $y) = @$_;
305 37918 50 66     161450 $seen{$x}{$y} = $P if !exists $seen{$x}{$y} or $seen{$x}{$y} > $P;
306             # e(q(shortestPath - loop));
307             }
308 37917         84949 push @search, [@r];
309             }
310              
311 37926         50266 if (1) # Set minimum path for surrounding pixels
312 37926         59701 {my $P = scalar(@path) + 1; my $Q = $P + 1;
  37926         53013  
313 37926         71021 my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
314              
315 37926 100 100     129612 $seen{$x}{$𝘆} = $P if !exists $seen{$x}{$𝘆} or $seen{$x}{$𝘆} > $P;
316 37926 100 100     120816 $seen{$x}{$𝕪} = $P if !exists $seen{$x}{$𝕪} or $seen{$x}{$𝕪} > $P;
317 37926 100 100     112049 $seen{$𝘅}{$y} = $P if !exists $seen{$𝘅}{$y} or $seen{$𝘅}{$y} > $P;
318 37926 100 66     148895 $seen{$𝕩}{$y} = $P if !exists $seen{$𝕩}{$y} or $seen{$𝕩}{$y} > $P;
319             }
320             }
321             }
322              
323 9         161 push @shortestPath, $i->partitionEnd->{$partition}; # Add end point.
324 9         170 $I->partitions = $i->partitions; # Save the partition with height information added
325 9         1144 $I->partitionPath->{$partition} = [@shortestPath] # Return the shortest path
326             } # shortestPathBetweenEndPoints
327              
328             sub widthOfPath($$) #P Find the (estimated) width of the path at each point.
329 9     9 1 183 {my ($I, $partition) = @_; # Image, partition
330 9         28 my $i = $I->clonePartition($partition); # Clone the specified partition so that we can remove pixels once they have been processed to spped up the remaining search
331 9         156 my $path = $i->partitionPath->{$partition}; # Path in image
332 9         55 my $maxSteps = @$path;
333 9         44 for my $step(keys @$path)
334 385         11186 {my ($x, $y) = @{$$path[$step]};
  385         1243  
335              
336             my $explore = sub #P Explore away from a point checking that we are still in the partition associated with the path
337 3080     3080   24060 {my ($dx, $dy) = @_; # x direction, y direction
338 3080         6105 for my $step(1..$maxSteps) # Maximum possible width
339             {return $step-1 unless $i->partitions->{$partition} # Keep stepping whilst still in partition
340             {$x+$step*$dx}
341 87943 100       1895839 {$y+$step*$dy};
342             }
343             $maxSteps # We never left the partition
344 385         1762 };
  0         0  
345              
346 385         702 push @{$I->partitionPath->{$partition}[$step]}, 1 + min # Explore in opposite directions along 4 lines and take the minimum as the width
  385         7093  
347             ($explore->(1, 0) + $explore->(-1, 0),
348             $explore->(1, 1) + $explore->(-1, -1),
349             $explore->(0, 1) + $explore->( 0, -1),
350             $explore->(1, -1) + $explore->(-1, +1));
351             }
352             } # widthOfPath
353              
354             sub widthOfPaths($) #P Find the (estimated) width of each path at each point.
355 4     4 1 10 {my ($i) = @_; # Image
356 4         16 $i->widthOfPath($_) for 1..$i->numberOfPaths; # Add path width estimate at each point
357             } # widthOfPaths
358              
359             sub path($$) # Returns an array of arrays [x, y, t] where x, y are the coordinates of each point sequentially along the specified path and t is the estimated thickness of the path at that point. Paths are numbered from 1 to L.
360 1     1 1 3 {my ($i, $partition) = @_; # Image, partition
361 1         15 $i->partitionPath->{$partition} # Return the shortest path
362             } # path
363              
364             sub printHeader($) #P Print a header for the image so we can locate pixels by their coordinates.
365 3     3 1 6 {my ($i) = @_; # Image
366 3         41 my $X = $i->x; my $Y = $i->y;
  3         49  
367 3         15 my $indent = length($Y);
368 3         7 my $space = q( ) x $indent;
369 3         17 my $N = 1 + int($X/10);
370             my $s = join '',
371 3 100       11 map{substr($_, -1) ? q( ) : $_ > 9 ? substr($_, -2, 1) : 0} 0..$X;
  121 100       207  
372 3         24 my $t = substr(("0123456789"x(1 + int($X/10))), 0, $X);
373              
374 3         12 my $f = "Image: X = $X, Y = $Y, Paths = ".$i->numberOfPaths; # Footer layout
375              
376 3         34 ("$space $s\n$space $t\n", "%".$indent."d %s", $f) # Header, line format, footer
377             } # printHeader
378              
379             sub print($) # Print the image: use B, B to show the start and end of each path, otherwise use the estimated thickness of the path at each point to mark the track of each path within each connected partition of the image.
380 3     3 1 7 {my ($i) = @_; # Image
381 3         47 my $X = $i->x; my $Y = $i->y;
  3         54  
382 3         18 my $s = ' ' x $X;
383 3         11 my @s = ($s) x $Y;
384              
385             my $plot = sub # Plot a pixel
386 114     114   155 {my ($x, $y, $symbol) = @_;
387 114         182 substr($s[$y], $x, 1) = $symbol;
388 3         14 };
389              
390 3         10 my ($header, $line, $footer) = $i->printHeader;
391              
392 3         5 for my $partition(keys %{$i->partitionPath}) # Each path
  3         41  
393 8         27 {my ($start, @p) = @{$i->partitionPath->{$partition}}; # Draw path
  8         106  
394 8         65 my @start = @$start; pop @start;
  8         19  
395 8         10 my @end = @{pop @p}; pop @end;
  8         16  
  8         13  
396              
397 8         12 $plot->(@start, q(S));
398 8         16 for(@p)
399 98         163 {my ($x, $y, $h) = @$_;
400 98         138 $plot->($x, $y, $h % 10);
401             }
402 8         12 $plot->(@end, q(E));
403             }
404 3         12 join "\n", $header, (map{sprintf($line, $_, $s[$_])} keys @s), $footer
  31         104  
405             } # print
406              
407             #1 Attributes # Attributes of an image
408              
409             genLValueScalarMethods(q(count)); # Number of points in the image.
410             genLValueScalarMethods(q(image)); # Image data points.
411             genLValueScalarMethods(q(partitions)); # Number of partitions in the image.
412             genLValueScalarMethods(q(partitionEnd)); # End points for each path.
413             genLValueScalarMethods(q(partitionStart)); # Start points for each path.
414             genLValueScalarMethods(q(partitionPath)); # Path for each partition.
415             genLValueScalarMethods(q(x)); # Image dimension in x.
416             genLValueScalarMethods(q(y)); # Image dimension in y.
417              
418             #-------------------------------------------------------------------------------
419             # Export
420             #-------------------------------------------------------------------------------
421              
422 1     1   4691 use Exporter qw(import);
  1         3  
  1         36  
423              
424 1     1   7 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         3  
  1         376  
425              
426             @ISA = qw(Exporter);
427             @EXPORT_OK = qw(
428             );
429             %EXPORT_TAGS = (all=>[@EXPORT, @EXPORT_OK]);
430              
431             # podDocumentation
432              
433             =pod
434              
435             =encoding utf-8
436              
437             =head1 Name
438              
439             Image::Find::Paths - Find paths in an image.
440              
441             =head1 Synopsis
442              
443             Use L to create and analyze a new image, then L to
444             visualize the paths detected, or L to get the coordinates of points
445             along each path in sequential order with an estimate of the thickness of the
446             path at each point.
447              
448             =head1 Description
449              
450             Find paths in an image.
451              
452             The following sections describe the methods in each functional area of this
453             module. For an alphabetic listing of all methods by name see L.
454              
455              
456              
457             =head1 Methods
458              
459             Find paths in an image
460              
461             =head2 new($)
462              
463             Find paths in an image represented as a string.
464              
465             Parameter Description
466             1 $string String of blanks; non blanks; new lines defining the image
467              
468             Example:
469              
470              
471             my $d = new(<
472             11 111
473             11 1 111
474             1111 111 111
475             1 111111 1 111
476             111 1111 111 111
477             11 1111111 1 1
478             11 11111 1 1
479             1 111 1 1 1
480             1111111111 1 111111 1
481             111 1 1 1
482             END
483            
484             is_deeply [$d->count, $d->x, $d->y, $d->numberOfPaths], [96, 80, 10, 6];
485            
486             ok nws($d->print) eq nws(<
487             0 1 2 3 4 5 6 7 8
488             01234567890123456789012345678901234567890123456789012345678901234567890123456789
489            
490             0 E1 E
491             1 11 23
492             2 1111 3
493             3 1 322E S 3
494             4 111 2 E1 2S
495             5 11 221S 1
496             6 11 23 1 E
497             7 1 3 1 S 1
498             8 11111112 1 E1111 1
499             9 S S
500            
501             Image: X = 80, Y = 10, Paths = 6
502             END
503            
504             is_deeply $d->path(5),
505            
506             [[79,4, 1], [78,4, 2], [78,3, 3], [78,2, 3], [78,1, 3], [77,1, 2], [77,0, 1]];
507            
508              
509             This is a static method and so should be invoked as:
510              
511             Image::Find::Paths::new
512              
513              
514             =head2 numberOfPaths($)
515              
516             Number of paths in the image.
517              
518             Parameter Description
519             1 $i Image
520              
521             Example:
522              
523              
524             is_deeply [$d->count, $d->x, $d->y, $d->numberOfPaths], [96, 80, 10, 6];
525            
526              
527             =head2 path($$)
528              
529             Returns an array of arrays [x, y, t] where x, y are the coordinates of each point sequentially along the specified path and t is the estimated thickness of the path at that point. Paths are numbered from 1 to L.
530              
531             Parameter Description
532             1 $i Image
533             2 $partition Partition
534              
535             Example:
536              
537              
538             is_deeply $d->path(5),
539            
540             [[79,4, 1], [78,4, 2], [78,3, 3], [78,2, 3], [78,1, 3], [77,1, 2], [77,0, 1]];
541            
542              
543             =head2 print($)
544              
545             Print the image: use B, B to show the start and end of each path, otherwise use the estimated thickness of the path at each point to mark the track of each path within each connected partition of the image.
546              
547             Parameter Description
548             1 $i Image
549              
550             Example:
551              
552              
553             ok nws($d->print) eq nws(<
554             0 1 2 3 4 5 6 7 8
555             01234567890123456789012345678901234567890123456789012345678901234567890123456789
556            
557             0 E1 E
558             1 11 23
559             2 1111 3
560             3 1 322E S 3
561             4 111 2 E1 2S
562             5 11 221S 1
563             6 11 23 1 E
564             7 1 3 1 S 1
565             8 11111112 1 E1111 1
566             9 S S
567            
568             Image: X = 80, Y = 10, Paths = 6
569             END
570            
571              
572             =head1 Attributes
573              
574             Attributes of an image
575              
576             =head2 count :lvalue
577              
578             Number of points in the image.
579              
580              
581             =head2 image :lvalue
582              
583             Image data points.
584              
585              
586             =head2 partitions :lvalue
587              
588             Number of partitions in the image.
589              
590              
591             =head2 partitionEnd :lvalue
592              
593             End points for each path.
594              
595              
596             =head2 partitionStart :lvalue
597              
598             Start points for each path.
599              
600              
601             =head2 partitionPath :lvalue
602              
603             Path for each partition.
604              
605              
606             =head2 x :lvalue
607              
608             Image dimension in x.
609              
610              
611             =head2 y :lvalue
612              
613             Image dimension in y.
614              
615              
616              
617             =head1 Private Methods
618              
619             =head2 clone($)
620              
621             Clone an image.
622              
623             Parameter Description
624             1 $i Image
625              
626             Example:
627              
628              
629             is_deeply $d, $d->clone;
630            
631              
632             =head2 clonePartition($$)
633              
634             Clone a partition of an image.
635              
636             Parameter Description
637             1 $i Image
638             2 $partition Partition
639              
640             =head2 countPixels($)
641              
642             Count the pixels in an image.
643              
644             Parameter Description
645             1 $i Image
646              
647             =head2 height($)
648              
649             Clone an image adding height to each pixel.
650              
651             Parameter Description
652             1 $i Image
653              
654             =head2 partition($)
655              
656             Partition the images into disjoint sets of connected points.
657              
658             Parameter Description
659             1 $i Image
660              
661             =head2 mapPartition($$$)
662              
663             Locate the pixels in the image that are connected to a pixel with a specified value.
664              
665             Parameter Description
666             1 $i Image
667             2 $x X coordinate of first point in partition
668             3 $y Y coordinate of first point in partition
669              
670             =head2 start($$)
671              
672             Find the starting point for a path in a partition.
673              
674             Parameter Description
675             1 $i Image
676             2 $partition Partition
677              
678             =head2 end($$)
679              
680             Find the other end of a path in a partition.
681              
682             Parameter Description
683             1 $i Image
684             2 $partition Partition
685              
686             =head2 traverseToOtherEnd($$$$)
687              
688             Traverse to the other end of a partition.
689              
690             Parameter Description
691             1 $I Image
692             2 $partition Partition
693             3 $X Start x coordinate
694             4 $Y Start y coordinate
695              
696             =head2 searchArea($$$$)
697              
698             Return the pixels to search from around a given pixel.
699              
700             Parameter Description
701             1 $i Image
702             2 $partition Partition
703             3 $x X coordinate of center of search
704             4 $y Y coordinate of center of search.
705              
706             =head2 checkAtLevelOne($$$)
707              
708             Confirm that the specified pixel is at level one.
709              
710             Parameter Description
711             1 $i Image
712             2 $partition Partition
713             3 $pixel Pixel
714              
715             =head2 searchAreaHighest($$$$$$)
716              
717             Return the highest pixels to search from around a given pixel.
718              
719             Parameter Description
720             1 $i Image
721             2 $partition Partition
722             3 $seen Pixels already visited
723             4 $depth Depth of search
724             5 $x X coordinate of center of search
725             6 $y Y coordinate of center of search.
726              
727             =head2 shortestPathBetweenEndPoints($$$)
728              
729             Find the shortest path between the start and the end points of a partition.
730              
731             Parameter Description
732             1 $I Image
733             2 $i Image height clone
734             3 $partition Partition
735              
736             =head2 widthOfPath($$)
737              
738             Find the (estimated) width of the path at each point.
739              
740             Parameter Description
741             1 $I Image
742             2 $partition Partition
743              
744             =head2 widthOfPaths($)
745              
746             Find the (estimated) width of each path at each point.
747              
748             Parameter Description
749             1 $i Image
750              
751             =head2 printHeader($)
752              
753             Print a header for the image so we can locate pixels by their coordinates.
754              
755             Parameter Description
756             1 $i Image
757              
758              
759             =head1 Index
760              
761              
762             1 L
763              
764             2 L
765              
766             3 L
767              
768             4 L
769              
770             5 L
771              
772             6 L
773              
774             7 L
775              
776             8 L
777              
778             9 L
779              
780             10 L
781              
782             11 L
783              
784             12 L
785              
786             13 L
787              
788             14 L
789              
790             15 L
791              
792             16 L
793              
794             17 L
795              
796             18 L
797              
798             19 L
799              
800             20 L
801              
802             21 L
803              
804             22 L
805              
806             23 L
807              
808             24 L
809              
810             25 L
811              
812             26 L
813              
814             27 L
815              
816             28 L
817              
818             =head1 Installation
819              
820             This module is written in 100% Pure Perl and, thus, it is easy to read, use,
821             modify and install.
822              
823             Standard L process for building and installing modules:
824              
825             perl Build.PL
826             ./Build
827             ./Build test
828             ./Build install
829              
830             =head1 Author
831              
832             L
833              
834             L
835              
836             =head1 Copyright
837              
838             Copyright (c) 2016-2018 Philip R Brenan.
839              
840             This module is free software. It may be used, redistributed and/or modified
841             under the same terms as Perl itself.
842              
843             =cut
844              
845              
846              
847             # Tests and documentation
848              
849             sub test
850 1     1 0 9 {my $p = __PACKAGE__;
851 1         7 binmode($_, ":utf8") for *STDOUT, *STDERR;
852 1 50       47 return if eval "eof(${p}::DATA)";
853 1         43 my $s = eval "join('', <${p}::DATA>)";
854 1 50       5 $@ and die $@;
855 1 100   1 0 6 eval $s;
  1     1   2  
  1     1   30  
  1     2   4  
  1         2  
  1         19  
  1         500  
  1         52124  
  1         9  
  1         63  
  2         6  
  2         8  
  2         52  
  2         48  
  2         3721  
856 1 50       178 $@ and die $@;
857             }
858              
859             test unless caller;
860              
861             1;
862             # podDocumentation
863             __DATA__