File Coverage

blib/lib/Image/Find/Paths.pm
Criterion Covered Total %
statement 188 188 100.0
branch 37 42 88.1
condition 38 42 90.4
subroutine 25 25 100.0
pod 12 13 92.3
total 300 310 96.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #-------------------------------------------------------------------------------
3             # Image::Find::Paths - Locate 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 = "20180429";
8             require v5.16;
9 1     1   601 use warnings FATAL => qw(all);
  1         7  
  1         36  
10 1     1   6 use strict;
  1         2  
  1         30  
11 1     1   6 use Carp qw(confess);
  1         1  
  1         77  
12 1     1   573 use Data::Dump qw(dump);
  1         7602  
  1         63  
13 1     1   976 use Data::Table::Text qw(:all);
  1         62573  
  1         503  
14 1     1   10 use utf8;
  1         3  
  1         7  
15              
16             #1 Attributes # Attributes of an image
17              
18             genLValueScalarMethods(q(count)); # Number of points in the image.
19             genLValueScalarMethods(q(image)); # Image data points.
20             genLValueScalarMethods(q(partitions)); # Number of partitions in the image.
21             genLValueScalarMethods(q(partitionEnd)); # End points for each path.
22             genLValueScalarMethods(q(partitionStart)); # Start points for each path.
23             genLValueScalarMethods(q(partitionPath)); # Path for each partition.
24             genLValueScalarMethods(q(x)); # Image dimension in x.
25             genLValueScalarMethods(q(y)); # Image dimension in y.
26              
27             #1 Methods # Locate paths in an image
28              
29             sub new($) #S Find paths in an image represented as a string.
30 1     1 1 4 {my ($string) = @_; # String of blanks; non blanks; new lines defining the image
31 1         9 my @lines = split /\n/, $string;
32 1         5 my $count; # Number of active pixels
33             my %image; # {x}{y} of active pixels
34 1         0 my $x; # Image dimension in x
35 1         4 for my $j(0..$#lines) # Load active pixels
36 10         16 {my $line = $lines[$j];
37 10 100 66     35 $x = length($line) if !defined($x) or length($line) > $x; # Longest line
38 10         18 for my $i(0..length($line)-1) # Parse each line
39 800 100       1454 {$image{$i}{$j} = 0, $count++ if substr($line, $i, 1) ne q( );
40             }
41             }
42            
43 1         12 my $d = bless{image=>\%image, x=>$x, y=>scalar(@lines), count=>$count, # Create image of paths
44             partitions=>{}, partitionStart=>{}, partitionEnd=>{},
45             partitionPath=>{}};
46              
47 1         6 $d->partition; # Partition the image
48 1         12 $d->start($_), $d->end($_) for 1..$d->numberOfPaths; # Find a start point for each partition
49 1         11 $d->shortestPathBetweenEndPoints($_) for 1..$d->numberOfPaths; # Find the longest path in each partition
50              
51 1         39 $d # Return new image with path details
52             }
53            
54             sub clone($) # Clone an image.
55 19     19 1 37 {my ($i) = @_; # Image
56            
57 19         28 my %partitions; # Clone partitions
58 19         28 for my $p(keys %{$i->partitions})
  19         328  
59 114         754 {for my $x(keys %{$i->partitions->{$p}})
  114         1821  
60 513         3195 {for my $y(keys %{$i->partitions->{$p}{$x}})
  513         8249  
61 1349         27629 {$partitions{$p}{$x}{$y} = $i->partitions->{$p}{$x}{$y};
62             }
63             }
64             }
65            
66 19         264 bless {%$i, partitions=>\%partitions}; # Cloned image
67             }
68            
69             sub numberOfPaths($) # Number of paths in the image.
70 11     11 1 23 {my ($i) = @_; # Image
71 11         19 scalar(keys %{$i->partitions})
  11         189  
72             }
73            
74             sub partition($) #P Partition == set of connected points.
75 1     1 1 3 {my ($i) = @_; # Image
76 1         2 for my $x(sort{$a<=>$b} keys %{$i->image}) # Stabilize partition numbers to make testing possible
  88         131  
  1         26  
77 25         162 {for my $y(sort{$a<=>$b} keys %{$i->image->{$x}})
  76         232  
  25         423  
78 73 100       1397 {$i->mapPartition($x, $y) if $i->image->{$x}{$y} == 0; # Bucket fill anything that touches this pixels
79             }
80             }
81             }
82              
83             sub mapPartition($$$) #P Locate the pixels in the image that are connected to a pixel with a specified value
84 8     8 1 83 {my ($i, $x, $y) = @_; # Image, x coordinate of first point in partition, y coordinate of first point in partition
85 8         26 my $p = $i->image->{$x}{$y} = $i->numberOfPaths+1; # Next partition
86 8         325 $i->partitions->{$p}{$x}{$y}++; # Add first pixel to this partition
87 8         76 my $pixelsInPartition = 0;
88              
89 8         155 for(1..$i->count) # Worst case - each pixel is a separate line
90 39         107 {my $changed = 0; # Number of pixels added to this partition on this pass
91 39         58 for my $x(keys %{$i->image}) # Each pixel
  39         685  
92 960         2211 {for my $y(keys %{$i->image->{$x}})
  960         16249  
93 2832 100       54091 {next if $i->image->{$x}{$y} == $p; # Already partitioned
94 2454         49409 my $I = $i->image;
95 2454         15127 my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
96 2454 100 100     30673 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        
97             exists($I->{$x}) && exists($I->{$x}{$𝘆}) && $I->{$x}{$𝘆} == $p or
98             exists($I->{$𝕩}) && exists($I->{$𝕩}{$y}) && $I->{$𝕩}{$y} == $p or
99             exists($I->{$x}) && exists($I->{$x}{$𝕪}) && $I->{$x}{$𝕪} == $p)
100 65         1101 {$i->image->{$x}{$y} = $p;
101 65         354 ++$changed;
102 65         1048 ++$i->partitions->{$p}{$x}{$y}; # Pixels in this partition
103 65         442 ++$pixelsInPartition;
104             }
105             }
106             }
107 39 100       149 last unless $changed; # No more pixels in parition to consider
108             }
109              
110 8 100       32 if ($pixelsInPartition <= 1) # Remove partitions of just one pixel
111 2         4 {for my $x(keys %{$i->image})
  2         36  
112 49         361 {for my $y(keys %{$i->image->{$x}})
  49         799  
113 145 100       3023 {delete $i->image->{$x}{$y} if $i->image->{$x}{$y} == $p;
114 145 100       829 delete $i->image->{$x} unless keys %{$i->image->{$x}}; # Remove containing hash if now empty
  145         2286  
115             }
116             }
117 2         46 delete $i->partitions->{$p}
118             }
119             }
120              
121             sub traverseToOtherEnd($$$$) #P Traverse to the other end of a partition.
122 12     12 1 59 {my ($I, $partition, $x, $y) = @_; # Image, partition, start x coordinate, start y coordinate
123 12         30 my $i = $I->clone; # Clone the image so that we can remove pixels once they have been processed to spped up the remaining search
124 12         43 my @remove = ([$x, $y]); # Removal sequence
125 12         21 my $last; # We know that there are two or more pixels in the paritition
126 12         48 while(@remove)
127 144         259 {$last = shift @remove;
128 144         277 my ($x, $y) = @$last;
129 144         2512 my $P = $i->partitions->{$partition};
130 144         805 delete $P->{$x}{$y}; # Remove the pixel currently being examined
131 144         287 push @remove, $i->searchArea($partition, $x, $y);
132             }
133             $last # Last point is the other end
134 12         339 }
135              
136             sub start($$) #P Find the starting point for a path in a partition.
137 6     6 1 57 {my ($i, $partition) = @_; # Image, partition
138 6         8 my $remove; # Removal sequence
139              
140 6         10 for my $x((sort{$a<=>$b} keys %{$i->partitions->{$partition} })[0]) # Find the first point in a partition
  48         110  
  6         106  
141 6         26 {for my $y((sort{$a<=>$b} keys %{$i->partitions->{$partition}{$x}})[0])
  12         57  
  6         132  
142 6         36 {$remove = [$x, $y];
143             }
144             }
145              
146 6         18 $i->partitionStart->{$partition} = # Record start point
147             $i->traverseToOtherEnd($partition, @$remove);
148             }
149              
150             sub end($$) #P Find the other end of a path in a partition.
151 6     6 1 49 {my ($i, $partition) = @_; # Image, partition
152             $i->partitionEnd->{$partition} = # Record start point
153 6         10 $i->traverseToOtherEnd($partition, @{$i->partitionStart->{$partition}});
  6         96  
154             }
155              
156             sub searchArea($$$$) #P Return the pixels to search from around a given pixel.
157 217     217 1 429 {my ($i, $partition, $x, $y) = @_; # Image, partition, x coordinate of center of search, y coordinate of center of search.
158 217         297 my @s; # Pixels to search from
159 217         3705 my $P = $i->partitions->{$partition};
160 217         1294 my ($𝘅, $𝕩, $𝘆, $𝕪) = ($x+1, $x-1, $y+1, $y-1);
161 217 100       590 push @s, [$𝘅, $y] if exists $P->{$𝘅}{$y};
162 217 100       494 push @s, [$x, $𝘆] if exists $P->{$x}{$𝘆};
163 217 100       495 push @s, [$x, $𝕪] if exists $P->{$x}{$𝕪};
164 217 100       489 push @s, [$𝕩, $y] if exists $P->{$𝕩}{$y};
165             @s # Return all possible pixels
166 217         559 }
167              
168             sub shortestPathBetweenEndPoints($$) #P Find the shortest path between the start and the end points of a partition.
169 6     6 1 106 {my ($I, $partition) = @_; # Image, partition
170 6         16 my $i = $I->clone;
171 6         11 my ($X, $Y) = @{$i->partitionEnd->{$partition}}; # The end point for this partition
  6         104  
172 6         129 my @path = ($i->partitionStart->{$partition}); # A possible path
173 6         36 my @shortestPath; # Shortest path so far
174 6         9 my @search = [$i->searchArea($partition, @{$path[0]})]; # Initial search area is the pixels around the start pixel
  6         17  
175 6         13 my %visited; # Pixels we have already visited along the possible path
176            
177 6         18 while(@search) # Find the shortest path amongst all the possible paths
178 140 50       281 {@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
179 140         216 my $search = $search[-1]; # Pixels to search for latest path element
180 140 100       240 if (!@$search) # Nothing left to search at this level
181 66         85 {pop @search; # Remove search level
182 66         82 my ($x, $y) = @{pop @path}; # Pixel to remove from possible path
  66         111  
183 66         152 delete $visited{$x}{$y}; # Pixel no longer visited on this possible path
184             }
185             else
186 74         133 {my ($x, $y) = @{pop @$search}; # Next pixel to add to path
  74         162  
187 74 50       176 next if $visited{$x}{$y}; # Pixel has already been vsisited on this path so skip it
188 74 100 100     177 if ($x == $X and $y == $Y)
189 7 100 66     39 {@shortestPath = @path if !@shortestPath or @path < @shortestPath;
190 7         11 my ($x, $y) = @{pop @path}; # Pixel to remove from possible path
  7         17  
191 7         14 pop @search; # any other adjacent pixels will not produce a shorter path
192 7         24 delete $visited{$x}{$y}; # Pixel no longer visited on this possible path
193             }
194             else # Extend the search
195 67         155 {push @path, [$x, $y]; # Extend the path
196 67         134 $visited{$x}{$y}++;
197             push @search, # Extend the search area to pixels not already visited on this path
198 67         150 [grep {my ($x, $y) = @$_; !$visited{$x}{$y}}
  134         265  
  134         456  
199             $i->searchArea($partition, $x, $y)]
200             }
201             }
202             }
203              
204 6         109 push @shortestPath, $i->partitionEnd->{$partition}; # Add end point.
205 6         129 $I->partitionPath->{$partition} = [@shortestPath] # Return the shortest path
206             }
207              
208             sub path($$) # Path for a specified partition.
209 1     1 1 3 {my ($i, $partition) = @_; # Image, partition
210 1         18 $i->partitionPath->{$partition} # Return the shortest path
211             }
212            
213             sub print($) # Print the image.
214 1     1 1 4 {my ($i) = @_; # Image
215 1         36 my $X = $i->x; my $Y = $i->y;
  1         24  
216 1         8 my $s = ' ' x $X;
217 1         6 my @s = ($s) x $Y;
218              
219             my $plot = sub
220 61     61   118 {my ($x, $y, $symbol) = @_;
221 61         135 substr($s[$y], $x, 1) = $symbol;
222 1         5 };
223              
224 1         3 for my $partition(keys %{$i->partitionPath}) # Each path
  1         18  
225 6         16 {my ($start, @p) = @{$i->partitionPath->{$partition}}; # Draw path
  6         107  
226 6         40 my $end = pop @p;
227 6         13 $plot->(@$start, q(S));
228 6         15 $plot->(@$_, q(+)) for @p;
229 6         12 $plot->(@$end, q(E));
230             }
231              
232 1         26 join "\n", @s
233             }
234            
235             #-------------------------------------------------------------------------------
236             # Export
237             #-------------------------------------------------------------------------------
238              
239 1     1   2173 use Exporter qw(import);
  1         3  
  1         33  
240              
241 1     1   5 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         2  
  1         301  
242              
243             @ISA = qw(Exporter);
244             @EXPORT_OK = qw(
245             );
246             %EXPORT_TAGS = (all=>[@EXPORT, @EXPORT_OK]);
247              
248             # podDocumentation
249              
250             =pod
251              
252             =encoding utf-8
253              
254             =head1 Name
255              
256             Image::Find::Paths - Locate paths in an image.
257              
258             =head1 Synopsis
259              
260             =head1 Description
261              
262             Locate paths in an image.
263              
264             The following sections describe the methods in each functional area of this
265             module. For an alphabetic listing of all methods by name see L.
266              
267              
268              
269             =head1 Attributes
270              
271             Attributes of an image
272              
273             =head2 count :lvalue
274              
275             Number of points in image
276              
277              
278             =head2 image :lvalue
279              
280             Image data points
281              
282              
283             =head2 partitions :lvalue
284              
285             Number of parts in the image
286              
287              
288             =head2 partitionEnd :lvalue
289              
290             End point for a partition
291              
292              
293             =head2 partitionStart :lvalue
294              
295             Start point for a partition
296              
297              
298             =head2 partitionPath :lvalue
299              
300             Start path for each partition
301              
302              
303             =head2 x :lvalue
304              
305             Image dimension in x
306              
307              
308             =head2 y :lvalue
309              
310             Image dimension in y
311              
312              
313             =head1 Methods
314              
315             Locate paths in an image
316              
317             =head2 new($)
318              
319             Find paths in an image represented as a string.
320              
321             Parameter Description
322             1 $string String of blanks; non blanks; new lines defining the image
323              
324             Example:
325              
326              
327             my $d = new(<
328             11 1
329             11 1 1
330             1111 111 1
331             1 11 1 1
332             111 111 111 1
333             11 11 1 1 1
334             11 111 1 1
335             1 1 1 1 1
336             1111111111 1 111111 1
337             111 1 1 1
338             END
339            
340             ok $d->x == 80;
341            
342             ok $d->y == 10;
343            
344             ok nws($d->print) eq nws(<
345             E+ E
346             ++ +
347             ++++ E++ +
348             + + S +
349             +++ ++ E+ S
350             ++ S+ +
351             ++ +++ + E
352             + + + S +
353             +++++++++ + E++++ +
354             S S
355             END
356            
357             ok $d->numberOfPaths == 6;
358            
359             is_deeply $d->path(5), [[79, 4], [79, 3], [79, 2], [79, 1], [79, 0]];
360            
361              
362             This is a static method and so should be invoked as:
363              
364             Image::Find::Paths::new
365              
366              
367             =head2 clone($)
368              
369             Clone an image.
370              
371             Parameter Description
372             1 $i Image
373              
374             Example:
375              
376              
377             is_deeply $d, $d->clone;
378            
379              
380             =head2 numberOfPaths($)
381              
382             Number of paths in the image.
383              
384             Parameter Description
385             1 $i Image
386              
387             Example:
388              
389              
390             ok $d->numberOfPaths == 6;
391            
392              
393             =head2 path($$)
394              
395             Path for a specified partition.
396              
397             Parameter Description
398             1 $i Image
399             2 $partition Partition
400              
401             Example:
402              
403              
404             is_deeply $d->path(5), [[79, 4], [79, 3], [79, 2], [79, 1], [79, 0]];
405            
406              
407             =head2 print($)
408              
409             Print the image.
410              
411             Parameter Description
412             1 $i Image
413              
414             Example:
415              
416              
417             ok nws($d->print) eq nws(<
418             E+ E
419             ++ +
420             ++++ E++ +
421             + + S +
422             +++ ++ E+ S
423             ++ S+ +
424             ++ +++ + E
425             + + + S +
426             +++++++++ + E++++ +
427             S S
428             END
429            
430              
431              
432             =head1 Private Methods
433              
434             =head2 partition($)
435              
436             Partition == set of connected points.
437              
438             Parameter Description
439             1 $i Image
440              
441             =head2 mapPartition($$$)
442              
443             Locate the pixels in the image that are connected to a pixel with a specified value
444              
445             Parameter Description
446             1 $i Image
447             2 $x X coordinate of first point in partition
448             3 $y Y coordinate of first point in partition
449              
450             =head2 traverseToOtherEnd($$$$)
451              
452             Traverse to the other end of a partition.
453              
454             Parameter Description
455             1 $I Image
456             2 $partition Partition
457             3 $x Start x coordinate
458             4 $y Start y coordinate
459              
460             =head2 start($$)
461              
462             Find the starting point for a path in a partition.
463              
464             Parameter Description
465             1 $i Image
466             2 $partition Partition
467              
468             =head2 end($$)
469              
470             Find the other end of a path in a partition.
471              
472             Parameter Description
473             1 $i Image
474             2 $partition Partition
475              
476             =head2 searchArea($$$$)
477              
478             Return the pixels to search from around a given pixel.
479              
480             Parameter Description
481             1 $i Image
482             2 $partition Partition
483             3 $x X coordinate of center of search
484             4 $y Y coordinate of center of search.
485              
486             =head2 shortestPathBetweenEndPoints($$)
487              
488             Find the shortest path between the start and the end points of a partition.
489              
490             Parameter Description
491             1 $I Image
492             2 $partition Partition
493              
494              
495             =head1 Index
496              
497              
498             1 L
499              
500             2 L
501              
502             3 L
503              
504             4 L
505              
506             5 L
507              
508             6 L
509              
510             7 L
511              
512             8 L
513              
514             9 L
515              
516             10 L
517              
518             11 L
519              
520             12 L
521              
522             13 L
523              
524             14 L
525              
526             15 L
527              
528             16 L
529              
530             17 L
531              
532             18 L
533              
534             19 L
535              
536             20 L
537              
538             =head1 Installation
539              
540             This module is written in 100% Pure Perl and, thus, it is easy to read, use,
541             modify and install.
542              
543             Standard L process for building and installing modules:
544              
545             perl Build.PL
546             ./Build
547             ./Build test
548             ./Build install
549              
550             =head1 Author
551              
552             L
553              
554             L
555              
556             =head1 Copyright
557              
558             Copyright (c) 2016-2018 Philip R Brenan.
559              
560             This module is free software. It may be used, redistributed and/or modified
561             under the same terms as Perl itself.
562              
563             =cut
564              
565              
566              
567             # Tests and documentation
568              
569             sub test
570 1     1 0 11 {my $p = __PACKAGE__;
571 1         9 binmode($_, ":utf8") for *STDOUT, *STDERR;
572 1 50       62 return if eval "eof(${p}::DATA)";
573 1         52 my $s = eval "join('', <${p}::DATA>)";
574 1 50       9 $@ and die $@;
575 1     1   7 eval $s;
  1     1   2  
  1     1   37  
  1         6  
  1         2  
  1         35  
  1         659  
  1         75864  
  1         9  
  1         75  
576 1 50       1694 $@ and die $@;
577             }
578              
579             test unless caller;
580              
581             1;
582             # podDocumentation
583             __DATA__