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
|
|
|
|
|
|
|
|