Imagerで2値化した画像にラベリングする

前回の時点で2値化まで済んでいるので、
次はラベリングを行う。

OpenCVだと、flood_fillで塗りつぶすと、
塗りつぶした範囲が取得できるので、それを使うんだけど、
Imagerの場合はそれがないので、塗りつぶした範囲の計算を行う必要がある。(*1)

ちなみに、Imagerflood_fillはここ。
https://metacpan.org/pod/Imager::Draw#flood_fill

という訳で、さっそく2値化した画像にラベリングして、
塗りつぶした範囲の取得を行ってみる。

use v5.14;
use strict;
use warnings;
use Imager;

if ( (not @ARGV) or (not -e $ARGV[0]) ) {
    say "Usage:
    perl $0 file_path";
    exit( 0 );
}

my $img = Imager->new( file => $ARGV[0] )
    or die Imager->errstr();

my $h = $img->getheight();
my ( $ix, $iy ) = ( 0, 0 );
my $area_no = 8;
while ( $iy < $h ) {
    my $tmp = $img->getsamples( y => $iy, channels => [0] );
    my @pixels = unpack( 'C*', $tmp );

    my $found = 0;
    while ( $ix < scalar(@pixels) ) {
        if ( $pixels[$ix] == 255 ) {
            my $c = Imager::Color->new( $area_no, $area_no, $area_no );
            $img->flood_fill( x => $ix, y => $iy, color => $c );

            my $area = calc_filled_area( $img, $area_no, $ix, $iy );
            print_area_info( $area_no, $area );
            my $img_dst = crop_labeled_area( $img, $area_no, $area, 1 );
            $img_dst->write( file => sprintf("%03d.png", $area_no) );

            $area_no += 8;
            $found = 1;
            last;
        }

        $ix++;
    }

    if ( not $found ) {
        $ix = 0;
        $iy++;
    }

    if ( 255 < $area_no ) {
        die 'area_no = ', $area_no, ' too many areas! sorry.';
    }
}

sub calc_filled_area {
    my ( $img, $area_no, $filled_x, $filled_y ) = @_;

    my ( $xmin, $xmax ) = ( $filled_x, $filled_x );
    my ( $ymin, $ymax ) = ( $filled_y, $filled_y );

    my $h = $img->getheight();
    my $iy = $ymin;
    while ( $iy < $h ) {
        my $tmp = $img->getsamples( y => $iy, channels => [0] );
        my @pixels = unpack( 'C*', $tmp );

        my $found = grep { $_ == $area_no } @pixels;
        if ( $found ) {
            my $st = 0;
            $st++ while $pixels[$st] != $area_no;

            my $en = scalar(@pixels) - 1;
            $en-- while $pixels[$en] != $area_no;

            $xmin = $st if $st < $xmin;
            $xmax = $en if $xmax < $en;
        }

        if ( not $found ) {
            last;
        }
        else {
            $ymax = $iy;
            $iy++;
        }
    }

    return +{
        left   => $xmin,
        top    => $ymin,
        right  => $xmax,
        bottom => $ymax
    };
}

sub print_area_info {
    my ( $area_no, $area ) = @_;
    printf( "area_no = %3d, (x, y) = (%4d, %4d), w = %4d, h = %4d",
        $area_no,
        $area->{left},
        $area->{top},
        $area->{right} - $area->{left} + 1,
        $area->{bottom} - $area->{top} + 1 );
    print "\n";
}

sub crop_labeled_area {
    my ( $img_src, $area_no, $area, $margin ) = @_;

    my $x = $area->{left} - $margin;
    my $y = $area->{top} - $margin;
    my $w = $area->{right} - $area->{left} + 1 + ($margin * 2);
    my $h = $area->{bottom} - $area->{top} + 1 + ($margin * 2);

    my $img_tmp = $img_src->crop(
        left => $x, top => $y, width => $w, height => $h );

    my $img_dst = Imager::transform2( {
        channels => 1,
        constants => { area_no => $area_no },
        rpnexpr => 'x y getp1 !pix @pix red area_no eq 255 255 255 rgb 0 0 0 rgb ifp'
    }, $img_tmp );

    $img_dst or die $Imager::ERRSTR;
    return $img_dst;
}

$img->write( file => $0 . '.png' );

これを実行すると、こんな感じの出力が得られる。

$ perl aaa.pl 20140727-2.png
area_no = 8, (x, y) = ( 559, 85), w = 109, h = 123
area_no = 16, (x, y) = ( 325, 105), w = 50, h = 145
area_no = 24, (x, y) = ( 693, 159), w = 275, h = 78
area_no = 32, (x, y) = ( 114, 208), w = 223, h = 73
area_no = 40, (x, y) = ( 436, 220), w = 143, h = 29
area_no = 48, (x, y) = ( 828, 234), w = 123, h = 65
area_no = 56, (x, y) = ( 31, 267), w = 91, h = 165
area_no = 64, (x, y) = ( 762, 268), w = 51, h = 155
area_no = 72, (x, y) = ( 388, 275), w = 53, h = 251
area_no = 80, (x, y) = ( 471, 275), w = 40, h = 229
area_no = 88, (x, y) = ( 196, 283), w = 110, h = 122
area_no = 96, (x, y) = ( 536, 296), w = 184, h = 87
area_no = 104, (x, y) = ( 217, 382), w = 148, h = 139
area_no = 112, (x, y) = ( 289, 395), w = 333, h = 220
area_no = 120, (x, y) = ( 102, 432), w = 112, h = 52
area_no = 128, (x, y) = ( 559, 434), w = 179, h = 166
area_no = 136, (x, y) = ( 841, 461), w = 63, h = 93
area_no = 144, (x, y) = ( 625, 525), w = 235, h = 62
area_no = 152, (x, y) = ( 185, 613), w = 136, h = 105
area_no = 160, (x, y) = ( 366, 626), w = 177, h = 90

ラベリングした結果画像が分かりやすいように番号を8倍してるけど、
実際は1始まりで、1ずつ増やす。

20140728-1

この他にも、塗りつぶし範囲で切り抜いた画像も出力されるので、
気になる人は前回の2値化画像を入力して実行すると良いと思う。

あと、List::MoreUtilsを使うと、
塗りつぶし範囲の計算がもう少し楽になると思う。

おしまい。

(*1) i_flood_fill_lowっていうのがあって、そういう情報も取得できそう・・・。

Leave a Comment