File Coverage

blib/lib/Games/Go/Image2SGF.pm
Criterion Covered Total %
statement 27 170 15.8
branch 0 34 0.0
condition 0 11 0.0
subroutine 9 22 40.9
pod 0 10 0.0
total 36 247 14.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2              
3             package Games::Go::Image2SGF;
4             our $VERSION = '1.03';
5              
6             =cut
7              
8             =head1 NAME
9              
10             Games::Go::Image2SGF -- interpret photographs of go positions.
11              
12             =head1 SYNOPSIS
13              
14             my $board = Games::Go::Image2SGF->new(
15             tl => [50, 50],
16             tr => [1000, 50],
17             bl => [50, 1000],
18             br => [1000, 1000],
19             image => 'go_photograph.jpg'
20             );
21              
22             $board->to_sgf;
23             print $board->{sgf};
24              
25             =head1 DESCRIPTION
26              
27             B is a I module to create a computer-readable
28             I format description of the position on a Go board, given a photograph
29             of the position.
30              
31             =head1 OPTIONS
32              
33             Options are passed to B via its constructor. It will
34             attempt to use sane defaults for arguments you don't supply; you must supply
35             values for the required arguments.
36              
37             =over 4
38              
39             =item tl, tr, bl, br
40              
41             Required. The coordinates of the four corners of the go board's grid. You
42             can obtain these by loading your photograph in an image editor that displays
43             image coordinates and hovering the cursor over each of the grid corners.
44              
45             =item image
46              
47             Required. The filename of the image to interpret. This can be in any format
48             supported by I.
49              
50             =item white, black, board
51              
52             Optional. A fairly-representative colour for the white stones, black stones,
53             and go board itself, presented in decimal RGB triplets -- eg. C<[255,255,255]>
54             for white. You should only set these if the defaults are generating incorrect
55             SGF. Default: Black is C<[0,0,0]>, white is C<[255,255,255]>, board colour
56             is C<[100,100,100]>.
57              
58             =item sample_radius
59              
60             Optional. After inferring the grid from the corner points you give, the
61             module will search in a radius of C pixels to look at the
62             area's colour. As with the C arguments, the default
63             is likely to do the right thing; you should only need to change this if
64             your image is very large or very small. Default: 10 pixels.
65              
66             =back
67              
68             =head1 NOTES
69              
70             You may want to use the methods defined in the module in another order, or
71             in conjunction with other methods of your own -- for example, to track
72             video of a live game instead of still images. Note that methods with a
73             leading C<_> are considered internal, and their semantics may change.
74              
75             =head1 DEPENDENCIES
76              
77             C, C.
78              
79             =head1 SEE ALSO
80              
81             Further examples at L,
82             the L SGF standard, and the collaborative guide
83             to Go at L.
84              
85             =head1 AUTHOR
86              
87             Chris Ball Echris@cpan.orgE
88              
89             =cut
90              
91 1     1   26778 use constant BOARDSIZE => 19;
  1         2  
  1         78  
92 1     1   6 use constant BOARD => 0;
  1         2  
  1         40  
93 1     1   5 use constant WHITE => 1;
  1         13  
  1         39  
94 1     1   5 use constant BLACK => 2;
  1         2  
  1         44  
95 1     1   4 use constant X => 0;
  1         2  
  1         38  
96 1     1   4 use constant Y => 1;
  1         2  
  1         58  
97 1     1   5 use constant EPSILON => 0.0001;
  1         2  
  1         35  
98              
99 1     1   5 use strict;
  1         2  
  1         40  
100 1     1   1382 use Imager;
  1         70371  
  1         9  
101              
102             sub new {
103            
104             # Set up some initial defaults. These are overridden by the user
105             # in their constructor. White/black/board/sample_radius are optional.
106 0     0 0   my $self = bless {
107             white => [255,255,255],
108             black => [0,0,0],
109             board => [100,100,100],
110             sample_radius => 10,
111             }, shift;
112            
113             # Handle arguments.
114 0           my %options = @_;
115 0           while (my($key, $val) = each %options) {
116 0           $self->{$key} = $val;
117             }
118              
119             # Some of our arguments are required, and we should have them at this point.
120 0           foreach (qw/tl tr bl br image/) {
121 0 0         unless (defined ($self->{$_})) {
122 0           die "$_ is a required option; see the POD documentation.\n";
123             }
124             }
125              
126             # The mycolors array will be used by Imager to perform the quantization.
127 0           $self->{mycolors} = [ Imager::Color->new(@{ $self->{white} }),
  0            
128 0           Imager::Color->new(@{ $self->{board} }),
129 0           Imager::Color->new(@{ $self->{black} }) ];
130              
131 0           return $self;
132             }
133              
134             sub read_image {
135 0     0 0   my $self = shift;
136            
137 0           my $img = Imager->new();
138 0 0         $img->open(file => $self->{image}) or die $img->errstr();
139 0           $self->{img} = $img;
140             }
141              
142             sub quantize {
143 0     0 0   my $self = shift;
144              
145             # Quantize the image. We tell Imager to choose the colour in mycolors
146             # that each pixel in the image is nearest to, and set the pixel in the
147             # created image to that colour.
148 0 0         $self->{img} = $self->{img}->to_paletted(
149             make_colors => "none",
150             colors => $self->{mycolors},
151             max_colors => 3
152             ) or die $self->{img}->errstr();
153             }
154              
155             sub find_intersections {
156 0     0 0   my $self = shift;
157              
158 0           $self->invert_coords;
159              
160             # Find the equations for the lines connecting the four sides.
161             # Lines are defined by their slope (m) and yintercept (b) with
162             # the line equation: y = mx + b.
163 0           my $m_left = ($self->{tl}[Y] - $self->{bl}[Y]) /
164             ($self->{tl}[X] - $self->{bl}[X]);
165 0           my $b_left = $self->{bl}[Y] - ($m_left * $self->{bl}[X]);
166              
167 0           my $m_right = ($self->{tr}[Y] - $self->{br}[Y]) /
168             ($self->{tr}[X] - $self->{br}[X]);
169 0           my $b_right = $self->{br}[Y] - ($m_right * $self->{br}[X]);
170              
171 0           my $m_top = ($self->{tr}[Y] - $self->{tl}[Y]) /
172             ($self->{tr}[X] - $self->{tl}[X]);
173 0           my $b_top = $self->{tl}[Y] - ($m_top * $self->{tl}[X]);
174              
175 0           my $m_bottom = ($self->{br}[Y] - $self->{bl}[Y]) /
176             ($self->{br}[X] - $self->{bl}[X]);
177 0           my $b_bottom = $self->{bl}[Y] - ($m_bottom * $self->{bl}[X]);
178              
179             # Find the "vanishing points" for the grid the board forms. These are a
180             # "vertical vanishing point" (vvp) for the intersection of left and right
181             # lines, and a "horizontal vanishing point" (hvp) for top and bottom
182             # intersection. There is the possibility that two lines are perfectly
183             # parallel -- we check this first and create a very small difference if
184             # we would otherwise generate a SIGFPE.
185 0 0         if ($m_top == $m_bottom) {
186 0           $m_top += EPSILON;
187             }
188 0 0         if ($m_left == $m_right) {
189 0           $m_left += EPSILON;
190             }
191              
192 0           my $x_vvp = ($b_right - $b_left) / ($m_left - $m_right);
193 0           my $y_vvp = ($m_left * $x_vvp) + $b_left;
194 0           my $x_hvp = ($b_top - $b_bottom) / ($m_bottom - $m_top);
195 0           my $y_hvp = ($m_bottom * $x_hvp) + $b_bottom;
196              
197             # The "horizon" for any two point perspective grid will be the line
198             # connecting these two vanishing points.
199 0           my $m_horizon = ($y_vvp - $y_hvp) / ($x_vvp - $x_hvp);
200 0           my $b_horizon = $y_vvp - ($m_horizon * $x_vvp);
201              
202             # Now find the equation of a line parallel to the horizon that goes through
203             # the bottom right point, called "fg" (short for foreground). (It's
204             # arbitrary which point this parallel line goes through, really, as long as
205             # it's different from the horizon line itself.)
206 0           my $m_fg = $m_horizon;
207 0           my $b_fg = $self->{br}[Y] - ($m_fg * $self->{br}[X]);
208              
209             # Find intersections of the left and right lines on this foreground (fg)
210 0           my $left_fg_x = ($b_left - $b_fg) / ($m_fg - $m_left);
211 0           my $right_fg_x = ($b_right - $b_fg) / ($m_fg - $m_right);
212            
213             # Find distance between these intersections along the x axis.
214 0           my $left_right_fg_x_dist = abs($right_fg_x - $left_fg_x);
215              
216             # Divide this distance into BOARDSIZE-1 fragments to find the spacing of
217             # BOARDSIZE points along it.
218 0           my $fg_lr_spacing = $left_right_fg_x_dist / (BOARDSIZE - 1);
219              
220             # Find intersections of the top and bottom lines on the foreground
221 0           my $top_fg_x = ($b_top - $b_fg) / ($m_fg - $m_top);
222 0           my $bottom_fg_x = ($b_bottom - $b_fg) / ($m_fg - $m_bottom);
223            
224             # Find distance between these intersections along the x axis.
225 0           my $top_bottom_fg_x_dist = abs($top_fg_x - $bottom_fg_x);
226              
227             # Divide this distance into BOARDSIZE-1 fragments to find spacing.
228 0           my $fg_tb_spacing = $top_bottom_fg_x_dist / (BOARDSIZE - 1);
229              
230             # Go through the foreground left-right x points, establish the vertical
231             # lines as detemined by the slope between them and the vvp. Start
232             # with left point and move towards the right.
233 0 0         if ($left_fg_x < $right_fg_x) {
234 0           for my $i (1 .. BOARDSIZE) {
235 0           my $x_i = $left_fg_x + ($fg_lr_spacing * ($i - 1));
236 0           my $y_i = $m_fg * $x_i + $b_fg;
237 0           $self->{vert_m_hash}[$i] = ($y_vvp - $y_i) / ($x_vvp - $x_i);
238 0           $self->{vert_b_hash}[$i] = $y_i - ($self->{vert_m_hash}[$i] * $x_i);
239             }
240             } else {
241 0           for my $i (1 .. BOARDSIZE) {
242 0           my $x_i = $left_fg_x - ($fg_lr_spacing * ($i - 1));
243 0           my $y_i = $m_fg * $x_i + $b_fg;
244 0           $self->{vert_m_hash}[$i] = ($y_vvp - $y_i) / ($x_vvp - $x_i);
245 0           $self->{vert_b_hash}[$i] = $y_i - ($self->{vert_m_hash}[$i] * $x_i);
246             }
247             }
248              
249             # Similarly, go through the foreground top-bottom x points, establish the
250             # horizontal lines as determined by the slope between them and the hvp.
251             # Want to number things from top to bottom, so will start things from
252             # top foreground x and move towards bottom.
253 0 0         if ($top_fg_x < $bottom_fg_x) {
254 0           for my $i (1 .. BOARDSIZE) {
255 0           my $x_i = $top_fg_x + ($fg_tb_spacing * ($i - 1));
256 0           my $y_i = $m_fg * $x_i + $b_fg;
257 0           $self->{horiz_m_hash}[$i] = ($y_hvp - $y_i) / ($x_hvp - $x_i);
258 0           $self->{horiz_b_hash}[$i] = $y_i - ($self->{horiz_m_hash}[$i] * $x_i);
259             }
260             } else {
261 0           for my $i (1 .. BOARDSIZE) {
262 0           my $x_i = $top_fg_x - ($fg_tb_spacing * ($i - 1));
263 0           my $y_i = $m_fg * $x_i + $b_fg;
264 0           $self->{horiz_m_hash}[$i] = ($y_hvp - $y_i) / ($x_hvp - $x_i);
265 0           $self->{horiz_b_hash}[$i] = $y_i - ($self->{horiz_m_hash}[$i] * $x_i);
266             }
267             }
268              
269 0           for my $i (1 .. BOARDSIZE) {
270 0           for my $j (1 .. BOARDSIZE) {
271 0           my $x_vertex = ($self->{horiz_b_hash}[$i] - $self->{vert_b_hash}[$j]) /
272             ($self->{vert_m_hash}[$j] - $self->{horiz_m_hash}[$i]);
273 0           my $y_vertex = ($self->{horiz_m_hash}[$i] * $x_vertex) +
274             $self->{horiz_b_hash}[$i];
275             # Coordinate system:
276             # intersection [3,5] is third from top, fifth from left
277 0           $self->{intersection}[$i][$j] = [ $x_vertex, -1 * $y_vertex ];
278             }
279             }
280             }
281              
282              
283             sub sample {
284 0     0 0   my ($self, $i, $j, $radius) = @_;
285 0           my $stone = "undecided";
286 0           my $blackcount = 0;
287 0           my $whitecount = 0;
288 0           my $boardcount = 0;
289 0           my $x_vertex = $self->{intersection}[$i][$j][X];
290 0           my $y_vertex = $self->{intersection}[$i][$j][Y];
291 0           my $black = $self->{mycolors}->[0];
292 0           my $board = $self->{mycolors}->[1];
293 0           my $white = $self->{mycolors}->[2];
294            
295 0           for (my $k = ($x_vertex - $radius); $k <= ($x_vertex + $radius); $k++) {
296 0           for (my $l = ($y_vertex - $radius); $l <= ($y_vertex + $radius); $l++) {
297 0 0         if (($x_vertex - $k)**2 + ($y_vertex - $l)**2 <= ($radius**2)) {
298             # If this is true, then the point ($k, $l) is in our circle.
299             # Now we sample at it.
300 0           my $gp = $self->{img}->getpixel('x' => $k, 'y' => $l);
301 0 0         next if $gp == undef;
302 0 0         if (_color_cmp($gp, $black) == 1) { $blackcount++; }
  0            
303 0 0         if (_color_cmp($gp, $board) == 1) { $boardcount++; }
  0            
304 0 0         if (_color_cmp($gp, $white) == 1) { $whitecount++; }
  0            
305             }
306             }
307             }
308              
309             # Finished sampling. Use a simple majority to work out which colour
310             # wins. TODO -- there are better ways of doing this. For example,
311             # if we determine one stone to be white or black, we could afterwards
312             # set its radius _in our quantized image_ back to the board colour;
313             # this "explaining away" would alleviate cases where the grid is
314             # slightly off and we're catching pixels of an already-recorded
315             # stone on the edges.
316 0 0 0       if (($whitecount > $blackcount) and ($whitecount > $boardcount)) {
    0          
317 0           $stone = WHITE;
318             } elsif ($blackcount > $boardcount) {
319 0           $stone = BLACK;
320             } else {
321 0           $stone = BOARD;
322             }
323              
324 0           my @letters = qw/z a b c d e f g h i j k l m n o p q r s/;
325 0 0 0       if ($stone == WHITE or $stone == BLACK) {
326 0           $self->update_sgf($stone, $letters[$i], $letters[$j], $stone);
327             }
328              
329 0           return $stone;
330             }
331              
332             sub invert_coords {
333 0     0 0   my $self = shift;
334            
335             # Because the origin (0,0) in the inputed coordinates is in the
336             # upper left instead of the intuitive-for-geometry bottom left,
337             # we want to call this the "fourth quadrant". That means all the
338             # y values are treated as negative numbers, so we convert:
339 0           for (qw(tl tr bl br)) { $self->{$_}[Y] = -$self->{$_}[Y]; }
  0            
340             }
341              
342             sub start_sgf {
343 0     0 0   my $self = shift;
344 0           my $time = scalar localtime;
345 0           $self->{sgf} .= <
346             (;GM[1]FF[4]SZ[19]
347             GN[Image2SGF conversion of $time.]
348              
349             AP[Image2SGF by Chris Ball.]
350             PL[B]
351             ENDSTARTSGF
352             }
353              
354             sub update_sgf {
355 0     0 0   my $self = shift;
356 0           my ($stone, $x, $y) = @_;
357 0 0         if ($stone == BLACK) {
    0          
358 0           push @{$self->{blackstones}}, "$y$x";
  0            
359             }
360             elsif ($stone == WHITE) {
361 0           push @{$self->{whitestones}}, "$y$x";
  0            
362             }
363             }
364              
365             sub finish_sgf {
366 0     0 0   my $self = shift;
367            
368 0           $self->{sgf} .= "\nAB";
369 0           $self->{sgf} .= "[$_]" foreach (@{$self->{blackstones}});
  0            
370            
371 0           $self->{sgf} .= "\nAW";
372 0           $self->{sgf} .= "[$_]" foreach (@{$self->{whitestones}});
  0            
373            
374 0           $self->{sgf} .= ")\n\n";
375             }
376              
377             sub _color_cmp {
378 0     0     my ($l, $r) = @_;
379 0           my @l = $l->rgba;
380 0           my @r = $r->rgba;
381 0   0       return ($l[0] == $r[0] and $l[1] == $r[1] and $l[2] == $r[2]);
382             }
383              
384             sub _to_coords {
385             # Example: "cd" => "C16".
386 0     0     my ($x, $y) = @_;
387 0   0       return chr(64 + $y + ($y > 9 && 1)) . (20 - $x);
388             }
389              
390             sub _from_coords {
391             # Example: "C16" => "cd".
392 0     0     my $move = shift;
393 0           /(.)(\d+)/;
394 0           return ($2, ord($1) - 65);
395             }
396              
397             sub to_sgf {
398 0     0 0   my $self = shift;
399              
400             # The only user-visible method right now. Runs the conversion functions.
401             # (Which are separate methods so that we can keep track of a live game
402             # efficiently -- if the camera is stationary above the board, we only
403             # have to find the grid location once, and can just repeatedly call
404             # read_image/quantize/sample, reusing the coordinates.)
405 0           $self->find_intersections;
406 0           $self->start_sgf;
407 0           $self->read_image;
408 0           $self->quantize;
409              
410 0           for my $i (1 .. BOARDSIZE) {
411 0           for my $j (1 .. BOARDSIZE) {
412 0           my $stone = $self->sample($i, $j, $self->{sample_radius});
413             }
414             }
415            
416 0           $self->finish_sgf;
417             }
418              
419             1;
420