AMC-annote.pl

Frédéric Bréal, 10/13/2019 02:33 pm

Download (17.9 kB)

 
1
#! /usr/bin/perl
2
#
3
# Copyright (C) 2009-2019 Alexis Bienvenue <paamc@passoire.fr>
4
#
5
# This file is part of Auto-Multiple-Choice
6
#
7
# Auto-Multiple-Choice is free software: you can redistribute it
8
# and/or modify it under the terms of the GNU General Public License
9
# as published by the Free Software Foundation, either version 2 of
10
# the License, or (at your option) any later version.
11
#
12
# Auto-Multiple-Choice is distributed in the hope that it will be
13
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15
# General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Auto-Multiple-Choice.  If not, see
19
# <http://www.gnu.org/licenses/>.
20
21
use Getopt::Long;
22
23
use Gtk3;
24
use Cairo;
25
26
use List::Util qw(min max sum);
27
28
use AMC::Path;
29
use AMC::Basic;
30
use AMC::Exec;
31
use AMC::Gui::Avancement;
32
use AMC::NamesFile;
33
use AMC::Data;
34
use AMC::DataModule::capture qw/:zone :position/;
35
use AMC::DataModule::layout qw/:flags/;
36
use AMC::Substitute;
37
38
use utf8;
39
40
my $cr_dir      = "";
41
my $rep_projet  = '';
42
my $rep_projets = '';
43
my $fichnotes   = '';
44
my $fich_bareme = '';
45
my $id_file     = '';
46
47
my $seuil    = 0.1;
48
my $seuil_up = 1.0;
49
50
my $data_dir = '';
51
52
my $taille_max  = "1000x1500";
53
my $qualite_jpg = "65";
54
55
my $debug = '';
56
57
my $progress    = 1;
58
my $progress_id = '';
59
60
my $line_width         = 2;
61
my @o_symbols          = ();
62
my $annote_indicatives = '';
63
my $position           = 'marge';
64
my $ecart              = 1;
65
my $ecart_marge        = 1.5;
66
my $pointsize_rel      = 60;
67
68
my $chiffres_significatifs = 4;
69
70
my $verdict                    = 'TOTAL : %S/%M => %s/%m';
71
my $verdict_question_cancelled = '"X"';
72
my $verdict_question           = '';
73
74
my $font_name      = 'FreeSans';
75
my $rtl            = '';
76
my $test_font_size = 100;
77
78
my $fich_noms      = '';
79
my $noms_encodage  = 'utf-8';
80
my $csv_build_name = '';
81
82
my $changes_only = 1;
83
84
# cle : "a_cocher-cochee"
85
my %symboles = (
86
    '0-0' => {qw/type none/},
87
    '0-1' => {qw/type circle color red/},
88
    '1-0' => {qw/type mark color red/},
89
    '1-1' => {qw/type mark color blue/},
90
);
91
92
@ARGV = unpack_args(@ARGV);
93
94
GetOptions(
95
    "cr=s" => \$cr_dir,
96
    "projet=s",  \$rep_projet,
97
    "projets=s", \$rep_projets,
98
    "data=s"                       => \$data_dir,
99
    "id-file=s"                    => \$id_file,
100
    "debug=s"                      => \$debug,
101
    "taille-max=s"                 => \$taille_max,
102
    "qualite=s"                    => \$qualite_jpg,
103
    "progression=s"                => \$progress,
104
    "progression-id=s"             => \$progress_id,
105
    "line-width=s"                 => \$line_width,
106
    "symbols=s"                    => \@o_symbols,
107
    "indicatives=s"                => \$annote_indicatives,
108
    "position=s"                   => \$position,
109
    "pointsize-nl=s"               => \$pointsize_rel,
110
    "ecart=s"                      => \$ecart,
111
    "ecart-marge=s"                => \$ecart_marge,
112
    "ch-sign=s"                    => \$chiffres_significatifs,
113
    "verdict=s"                    => \$verdict,
114
    "verdict-question=s"           => \$verdict_question,
115
    "verdict-question-cancelled=s" => \$verdict_question_cancelled,
116
    "fich-noms=s"                  => \$fich_noms,
117
    "noms-encodage=s"              => \$noms_encodage,
118
    "csv-build-name=s"             => \$csv_build_name,
119
    "font=s"                       => \$font_name,
120
    "rtl!"                         => \$rtl,
121
    "changes-only!"                => \$changes_only,
122
);
123
124
set_debug($debug);
125
126
print( ( "*" x 60 ) . "\n" );
127
print "* WARNING: AMC-annote is now obsolete\n* Please move to AMC-annotate\n";
128
print( ( "*" x 60 ) . "\n" );
129
130
for ( split( /,/, join( ',', @o_symbols ) ) ) {
131
    if (/^([01]-[01]):(none|circle|mark|box)(?:\/([\#a-z0-9]+))?$/) {
132
        $symboles{$1} = { type => $2, color => $3 };
133
    } else {
134
        die "Bad symbol syntax: $_";
135
    }
136
}
137
138
my $commandes = AMC::Exec::new("AMC-annote");
139
$commandes->signalise();
140
141
$cr_dir = $rep_projet . "/cr" if ( !$cr_dir );
142
143
if ( !-d $cr_dir ) {
144
    attention("No CR directory: $cr_dir");
145
    die "No CR directory: $cr_dir";
146
}
147
148
my $noms = '';
149
150
if ($fich_noms) {
151
    $noms = AMC::NamesFile::new(
152
        $fich_noms,
153
        encodage    => $noms_encodage,
154
        identifiant => $csv_build_name
155
    );
156
157
    debug "Keys in names file: " . join( ", ", $noms->heads() );
158
}
159
160
# ---
161
162
sub color_rgb {
163
    my ($s) = @_;
164
    my $col = Gtk3::Gdk::Color::parse($s);
165
    return ( $col->red / 65535, $col->green / 65535, $col->blue / 65535 );
166
}
167
168
my $avance = AMC::Gui::Avancement::new( $progress, id => $progress_id );
169
170
my $data    = AMC::Data->new($data_dir);
171
my $capture = $data->module('capture');
172
my $scoring = $data->module('scoring');
173
my $assoc   = $data->module('association');
174
my $layout  = $data->module('layout');
175
176
$seuil    = $scoring->variable_transaction('darkness_threshold');
177
$seuil_up = $scoring->variable_transaction('darkness_threshold_up');
178
179
#################################
180
181
sub milieu_cercle {
182
    my $zoneid = shift;
183
    return (
184
        $capture->sql_row(
185
            $capture->statement('zoneCenter'),
186
            $zoneid, POSITION_BOX
187
        )
188
    );
189
}
190
191
sub cercle_coors {
192
    my ( $context, $zoneid, $color ) = @_;
193
    my ( $x, $y ) = milieu_cercle($zoneid);
194
    my $t = sqrt( $capture->zone_dist2( $zoneid, $x, $y ) );
195
    $context->set_source_rgb( color_rgb($color) );
196
    $context->new_path;
197
    $context->arc( $x, $y, $t, 0, 360 );
198
    $context->stroke;
199
}
200
201
sub croix_coors {
202
    my ( $context, $zoneid, $color ) = @_;
203
    $context->set_source_rgb( color_rgb($color) );
204
    $context->new_path;
205
    for my $i ( 1, 2 ) {
206
        $context->move_to( $capture->zone_corner( $zoneid, $i ) );
207
        $context->line_to( $capture->zone_corner( $zoneid, $i + 2 ) );
208
    }
209
    $context->stroke;
210
}
211
212
sub boite_coors {
213
    my ( $context, $zoneid, $color ) = @_;
214
    my @pts = "";
215
    $context->set_source_rgb( color_rgb($color) );
216
    $context->new_path;
217
    $context->move_to( $capture->zone_corner( $zoneid, 1 ) );
218
    for my $i ( 2 .. 4 ) {
219
        $context->line_to( $capture->zone_corner( $zoneid, $i ) );
220
    }
221
    $context->close_path;
222
    $context->stroke;
223
}
224
225
my $delta = 1;
226
227
$capture->begin_read_transaction('PAGE');
228
229
my $annotate_source_change = $capture->variable('annotate_source_change');
230
231
my @pages = @{
232
    $capture->dbh->selectall_arrayref( $capture->statement('pages'),
233
        { Slice => {} } )
234
};
235
236
$capture->end_transaction('PAGE');
237
238
$delta             = 1 / ( 1 + $#pages ) if ( $#pages >= 0 );
239
$n_processed_pages = 0;
240
241
my %ok_students = ();
242
243
# a) first case: these numbers are given by --id-file option
244
245
if ($id_file) {
246
247
    open( NUMS, $id_file );
248
    while (<NUMS>) {
249
        chomp;
250
        if (/^[0-9]+(:[0-9]+)?$/) {
251
            $ok_students{$_} = 1;
252
        }
253
    }
254
    close(NUMS);
255
256
}
257
258
my $subst = AMC::Substitute::new(
259
    names   => $noms,
260
    scoring => $scoring,
261
    assoc   => $assoc,
262
    name    => '',
263
    chsign  => $chiffres_significatifs,
264
);
265
266
print "* Annotation\n";
267
268
PAGE: for my $p (@pages) {
269
    my @spc = map { $p->{$_} } (qw/student page copy/);
270
271
    if ( $id_file && !$ok_students{ studentids_string( $spc[0], $spc[2] ) } ) {
272
        next PAGE;
273
    }
274
275
    if ( $changes_only && $p->{timestamp_annotate} > $annotate_source_change ) {
276
        my $f = $p->{annotated};
277
        if ( -f "$cr_dir/corrections/jpg/$f" ) {
278
            print "Skipping page " . pageids_string(@spc) . " (up to date)\n";
279
            debug "Skipping page " . pageids_string(@spc) . " (up to date)";
280
            next PAGE;
281
        }
282
    }
283
284
    debug "Analyzing " . pageids_string(@spc);
285
286
    my $scan = $p->{src};
287
288
    debug "Scan file: $scan";
289
290
    if ($rep_projet) {
291
        $scan = proj2abs(
292
            {
293
                '%PROJET',  $rep_projet,
294
                '%PROJETS', $rep_projets,
295
                '%HOME' => $ENV{HOME},
296
            },
297
            $scan
298
        );
299
    }
300
301
    my $scan_f = $scan;
302
303
    $scan_f =~ s/\[[0-9]+\]$//;
304
305
    if ( -f $scan_f ) {
306
307
        # ONE SCAN FILE
308
309
        # read scan file (converting to PNG)
310
        debug "Reading $scan";
311
        open( CONV, "-|", magick_module("convert"), $scan, "png:-" );
312
        my $surface = Cairo::ImageSurface->create_from_png_stream(
313
            sub {
314
                my ( $cb_data, $length ) = @_;
315
                read CONV, $data, $length;
316
                return ($data);
317
            }
318
        );
319
        close(CONV);
320
321
        my $context = Cairo::Context->create($surface);
322
        $context->set_line_width($line_width);
323
324
        my $lay = Pango::Cairo::create_layout($context);
325
326
        # adjusts text size...
327
        my $l0 = Pango::Cairo::create_layout($context);
328
        $l0->set_font_description(
329
            Pango::FontDescription->from_string(
330
                $font_name . ' ' . $test_font_size
331
            )
332
        );
333
        $l0->set_text('H');
334
        my ( $text_x, $text_y ) = $l0->get_pixel_size();
335
        my $page_width  = $surface->get_width;
336
        my $page_height = $surface->get_height;
337
        debug "Scan height: $page_height";
338
        my $target_y = $page_height / $pointsize_rel;
339
        debug "Target TY: $target_y";
340
        my $font_size = int( $test_font_size * $target_y / $text_y );
341
        debug "Font size: $font_size";
342
343
        $lay->set_font_description(
344
            Pango::FontDescription->from_string(
345
                $font_name . ' ' . $font_size
346
            )
347
        );
348
        $lay->set_text('H');
349
        ( $text_x, $text_y ) = $lay->get_pixel_size();
350
351
        my ( $x_ppem, $y_ppem, $ascender, $descender, $width, $height,
352
            $max_advance );
353
354
        my $idf = pageids_string( @spc, path => 1 );
355
356
        print "Annotating $scan (sheet $idf)...\n";
357
358
        my %question = ();
359
360
        $capture->begin_read_transaction('xSTD');
361
362
        # print global mark and name on the page
363
364
        if ( $p->{page} == 1 || $capture->zones_count( @spc, ZONE_NAME ) ) {
365
            my $text = $subst->substitute( $verdict, @spc[ 0, 2 ] );
366
367
            $lay->set_text($text);
368
            $context->set_source_rgb( color_rgb('red') );
369
            if ($rtl) {
370
                my ( $tx, $ty ) = $lay->get_pixel_size;
371
                $context->move_to( $page_width - $text_x - $tx, $text_y * .7 );
372
            } else {
373
                $context->move_to( $text_x, $text_y * .7 );
374
            }
375
            Pango::Cairo::show_layout( $context, $lay );
376
        }
377
378
        #########################################
379
        # signs around each box
380
381
        my $sth = $capture->statement('pageZones');
382
        $sth->execute( @spc, ZONE_BOX );
383
      BOX: while ( my $b = $sth->fetchrow_hashref ) {
384
385
            my $p_strategy = $scoring->unalias( $p->{student} );
386
            my $q          = $b->{id_a};
387
            my $r          = $b->{id_b};
388
            my $indic      = $scoring->indicative( $p_strategy, $q );
389
390
            next BOX if ( $indic && !$annote_indicatives );
391
392
            # to be ticked?
393
            my $bonne = $scoring->correct_answer( $p_strategy, $q, $r );
394
395
            # ticked on this scan?
396
            my $cochee =
397
              $capture->ticked( $p->{student}, $p->{copy}, $q, $r, $seuil,
398
                $seuil_up );
399
400
            debug "Q=$q R=$r $bonne-$cochee";
401
402
            my $sy = $symboles{"$bonne-$cochee"};
403
404
            if ($debug) {
405
                for my $i ( 1 .. 4 ) {
406
                    debug(
407
                        sprintf( "Corner $i: (%.2f,%.2f)",
408
                            $capture->zone_corner( $b->{zoneid}, $i ) )
409
                    );
410
                }
411
            }
412
413
            if (
414
                !(
415
                    $layout->get_box_flags( $p->{student}, $q, $r,
416
                        BOX_ROLE_ANSWER ) & BOX_FLAGS_DONTANNOTATE
417
                )
418
              )
419
            {
420
                if ( $sy->{type} eq 'circle' ) {
421
                    cercle_coors( $context, $b->{zoneid}, $sy->{color} );
422
                } elsif ( $sy->{type} eq 'mark' ) {
423
                    croix_coors( $context, $b->{zoneid}, $sy->{color} );
424
                } elsif ( $sy->{type} eq 'box' ) {
425
                    boite_coors( $context, $b->{zoneid}, $sy->{color} );
426
                } elsif ( $sy->{type} eq 'none' ) {
427
                } else {
428
                    debug "Unknown symbol type ($bonne-$cochee): $sy->{type}";
429
                }
430
            }
431
432
            # pour avoir la moyenne des coors pour marquer la note de
433
            # la question
434
435
            $question{$q} = {} if ( !$question{$q} );
436
            my @mil = milieu_cercle( $b->{zoneid} );
437
            push @{ $question{$q}->{x} }, $mil[0];
438
            push @{ $question{$q}->{y} }, $mil[1];
439
        }
440
441
        #########################################
442
        # write questions scores
443
444
        if ( $position ne 'none' ) {
445
          QUEST: for my $q ( keys %question ) {
446
                next QUEST if ( $scoring->indicative( $p_strategy, $q ) );
447
                my $x;
448
449
                my $result = $scoring->question_result( @spc[ 0, 2 ], $q );
450
451
                my $text;
452
453
                if ( $result->{why} =~ /c/i ) {
454
                    $text = $verdict_question_cancelled;
455
                } else {
456
                    $text = $verdict_question;
457
                }
458
459
                $text =~ s/\%[S]/$result->{score}/g;
460
                $text =~ s/\%[M]/$result->{max}/g;
461
                $text =~ s/\%[W]/$result->{why}/g;
462
                $text =~ s/\%[s]/$subst->format_note($result->{score})/ge;
463
                $text =~ s/\%[m]/$subst->format_note($result->{max})/ge;
464
465
                my $te = eval($text);
466
                if ($@) {
467
                    debug "Annotation: $text";
468
                    debug "Evaluation error $@";
469
                } else {
470
                    $text = $te;
471
                }
472
473
                $lay->set_text($text);
474
                my ( $tx, $ty ) = $lay->get_pixel_size;
475
476
                # mean of the y coordinate of all boxes
477
                my $y = sum( @{ $question{$q}->{y} } ) /
478
                  ( 1 + $#{ $question{$q}->{y} } ) - $ty / 2;
479
480
                if ( $position eq 'marge' ) {
481
482
                    # scores written in one margin
483
                    if ($rtl) {
484
                        $x = $page_width - $ecart_marge * $text_x - $tx;
485
                    } else {
486
                        $x = $ecart_marge * $text_x;
487
                    }
488
                } elsif ( $position eq 'case' ) {
489
490
                    # scores written at the left of the boxes
491
                    if ($rtl) {
492
                        $x = max( @{ $question{$q}->{x} } ) + $ecart * $text_x;
493
                    } else {
494
                        $x =
495
                          min( @{ $question{$q}->{x} } ) -
496
                          $ecart * $text_x -
497
                          $tx;
498
                    }
499
                } elsif ( $position eq 'marges' ) {
500
501
                    # scores written in one of the margins (left or right),
502
                    # depending on the position of the boxes. This mode is often
503
                    # used when the subject is in a 2-column layout.
504
505
                # fist extract the y coordinates of the boxes in the left column
506
                    my $left = 1;
507
                    my @y    = map { $question{$q}->{y}->[$_] } grep {
508
                        $rtl xor( $question{$q}->{x}->[$_] <= $page_width / 2 )
509
                    } ( 0 .. $#{ $question{$q}->{x} } );
510
                    if ( !@y ) {
511
512
                        # if empty, use the right column
513
                        $left = 0;
514
                        @y    = map { $question{$q}->{y}->[$_] } grep {
515
                            $rtl
516
                              xor( $question{$q}->{x}->[$_] > $page_width / 2 )
517
                        } ( 0 .. $#{ $question{$q}->{x} } );
518
                    }
519
520
                    # set the x-position to the right margin
521
                    if ( $left xor $rtl ) {
522
                        $x = $ecart_marge * $text_x;
523
                    } else {
524
                        $x = $page_width - $ecart_marge * $text_x - $tx;
525
                    }
526
527
                    # set the y-position to the mean of y coordinates of the
528
                    # boxes in the corresponding column
529
                    $y = sum(@y) / ( 1 + $#y ) - $ty / 2;
530
                } else {
531
                    debug "Annotation : position invalide : $position";
532
                    $x = $ecart_marge * $text_x;
533
                }
534
535
                $context->set_source_rgb( color_rgb('red') );
536
                $context->move_to( $x, $y );
537
                Pango::Cairo::show_layout( $context, $lay );
538
            }
539
        }
540
541
        $capture->end_transaction('xSTD');
542
543
        # WRITE TO FILE
544
545
        $context->show_page;
546
547
        my $out_file = "page-$idf.jpg";
548
549
        debug "Saving annotated scan to $cr_dir/corrections/jpg/$out_file";
550
551
        my @args = ();
552
553
        if ($qualite_jpg) {
554
            if ( $qualite_jpg =~ /^[0-9]+$/ ) {
555
                push @args, "-quality", $qualite_jpg;
556
            } else {
557
                debug
558
"WARNING: non-numeric --qualite argument, ignored ($qualite_jpg)";
559
            }
560
        }
561
562
        if ($taille_max) {
563
            if ( $taille_max =~ /^[0-9]*x?[0-9]*$/ ) {
564
                push @args, "-geometry", $taille_max;
565
            } else {
566
                debug
567
"WARNING: malformed --taille-max argument, ignored ($taille_max)";
568
            }
569
        }
570
571
        open( CONV, "|-", magick_module("convert"),
572
            "png:-", @args, "$cr_dir/corrections/jpg/$out_file" );
573
        $surface->write_to_png_stream(
574
            sub {
575
                my ( $cb_data, $data ) = @_;
576
                print CONV $data;
577
            }
578
        );
579
        close(CONV);
580
581
        $capture->begin_transaction('ANNf');
582
        $capture->set_annotated( @spc, $out_file );
583
        $capture->end_transaction('ANNf');
584
585
        $n_processed_pages++;
586
587
    } else {
588
        print "No scan for page " . pageids_string(@spc) . ":$scan_f\n";
589
        debug "No scan: $scan_f";
590
    }
591
592
    $avance->progres($delta);
593
}
594
595
# stores state parameter to know all sheets have been annotated
596
597
$capture->begin_transaction('Aend');
598
$capture->variable( 'annotate_source_change', 0 );
599
$capture->end_transaction('Aend');
600
601
print "VAR: n_processed=$n_processed_pages\n";
602
603
$avance->fin();
604