## 自己紹介
- 仕事: 組み込み系
- Twitter: @techno_neko
- 所属: Hokkaido.pm
## しめじソート
- しめじをソートするアニメーション
- ピタゴラなんとか
- アルゴリズムの可視化
## おわび
アニメーションは間に合いませんでした
ゴメンなさい
この画像から、
こういう画像を作る
用意した画像
## 用意した画像
- スーパーで小ネギを買ってくる
- 適当に切る
- きれいに拭いたiPadの上に乗せて撮影
ほんとはライトボックスを使う
※この後、スタッフが美味しく頂きました
## 2値化
- 2値化?
- 2色刷りの画像を作る
- 分かりやすいように白と黒
## HSVを使う
- H:Hue(色相)
- S:Saturation(彩度)
- V:Value(明度)
## 明度を使って2値化
明度がしきい値より小さければ白、
そうでなければ黒
明度を使って2値化
## ラベリング
白の領域を、それぞれ異なる色で塗りつぶす
ラベリング
直立させる
ラベルごとに切り抜く
直立させる
回転しながら、
白の範囲の幅が一番小さくなる角度を探す
重なっている画像の判定
縦と横の比で判別
※場合によっては無理
## 入力画像にも同じ処理
- 切り抜き
- 回転
- さらに切り抜き
並べてみる
ソートして並べてみる
## まとめ
1. Imagerは凄い!
1. Imager::transform2は強力!
1. OpenCVを使うともっと簡単!
## おまけ
``` perl
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 );
}
# 小ネギの画像を2値化
my $img_src = Imager->new( file => $ARGV[0] )
or die Imager->errstr();
$img_src->filter( type => "gaussian", stddev => .5 )
or die $img_src->errstr;
$img_src->write( file => $0 . '_gaussian.jpg' );
my $img = Imager::transform2( {
channels => 1,
constants => { vv => 0.7 },
rpnexpr => 'x y getp1 !pix @pix value vv lt 255 255 255 rgb 0 0 0 rgb ifp'
}, $img_src );
# ラベリング
my $labeling_results = exec_labeling( $img );
# 直立するための角度を計算
foreach my $result ( @{$labeling_results} ) {
$result->{angle} = get_angle( $result->{mask_image}, $result->{area_no} );
}
# 切り抜いて、直立させて、余白を除去
my @result_images = map {
my $result = $_;
my $img_tmp = Imager::transform2( {
constants => {
left => $result->{xmin},
top => $result->{ymin},
area_no => $result->{area_no}
},
rpnexpr => 'x y getp1 !pix @pix red area_no eq left x + top y + getp2 255 255 255 rgb ifp'
}, $result->{mask_image}, $img_src );
my $img_dst = $img_tmp->rotate(
degrees => $result->{angle}, back => 'white' );
# マスク画像を回転して2値化
my $rotated_mask = Imager::transform2( {
channels => 1,
rpnexpr => 'x y getp1 !pix @pix red 0 eq @pix 255 255 255 rgb ifp'
}, $result->{mask_image}->rotate(degrees => $result->{angle}, back => 'black') );
# マスク画像の領域を取得
my $area = detect_filled_area( $rotated_mask, 255 );
my $result_image = $img_dst->crop(
left => $area->{xmin},
top => $area->{ymin},
width => $area->{xmax} - $area->{xmin} + 1,
height => $area->{ymax} - $area->{ymin} + 1
);
$rotated_mask->crop(
left => $area->{xmin},
top => $area->{ymin},
width => $area->{xmax} - $area->{xmin} + 1,
height => $area->{ymax} - $area->{ymin} + 1
)->write( file => sprintf('mask_%03d.png', $result->{area_no}) );
# $result_image->write( file => sprintf('rotate_%03d.png', $result->{area_no}) );
$result_image;
} @{$labeling_results};
# 縦と横の比で重なっている画像を判定
@result_images = grep {
($_->getwidth() / $_->getheight()) < 0.3;
} @result_images;
create_arranged_image( \@result_images )->write( file => $0 . '-1.png' );
my @sorted_images = sort { $a->getheight() <=> $b->getheight() } @result_images;
create_arranged_image( \@sorted_images )->write( file => $0 . '-2.png' );
sub create_arranged_image {
my $images_ref = shift;
my $margin = 4;
my ( $dst_width, $dst_height ) = ( 0, 0 );
foreach my $img ( @{$images_ref} ) {
my $w = $img->getwidth();
my $h = $img->getheight();
$dst_width += $img->getwidth() + ($margin * 2);
if ( $dst_height < $img->getheight() ) {
$dst_height = $img->getheight();
}
}
$dst_height += ( $margin * 2 );
my $img_dst = Imager->new(
xsize => $dst_width, ysize => $dst_height, channels => 4 );
$img_dst->box( color => 'white', filled => 1 );
my $x = $margin;
foreach my $img ( @{$images_ref} ) {
my $y = $img_dst->getheight() - $img->getheight() - $margin;
$img_dst->paste( left => $x, top => $y, src => $img );
$x += ( $margin + $img->getwidth() + $margin );
}
return $img_dst;
}
sub exec_labeling {
my $img = shift;
my @results = ();
my $h = $img->getheight();
my ( $ix, $iy ) = ( 0, 0 );
my $area_no = 1;
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 );
my $img_tmp = $img->crop(
left => $area->{xmin},
top => $area->{ymin},
width => $area->{xmax} - $area->{xmin} + 1,
height => $area->{ymax} - $area->{ymin} + 1
);
my %result = %{$area};
$result{mask_image} = Imager::transform2( {
constants => {
area_no => $area_no
},
rpnexpr => 'x y getp1 !pix @pix red area_no eq @pix 0 0 0 rgb ifp'
}, $img_tmp );
$result{area_no} = $area_no;
push @results, \%result;
$area_no++;
$found = 1;
last;
}
$ix++;
}
if ( not $found ) {
$ix = 0;
$iy++;
}
if ( 255 < $area_no ) {
die 'area_no = ', $area_no, ' too many areas! sorry.';
}
}
return \@results;
}
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 +{
xmin => $xmin,
ymin => $ymin,
xmax => $xmax,
ymax => $ymax
};
}
sub detect_filled_area {
my ( $img, $area_no ) = @_;
my ( $w, $h ) = ( $img->getwidth(), $img->getheight() );
for (my $iy=0; $iy<$h; $iy++) {
my $tmp = $img->getsamples( y => $iy, channels => [0] );
my @pixels = unpack( 'C*', $tmp );
for (my $ix=0; $ix<$w; $ix++) {
if ( $pixels[$ix] == $area_no ) {
return calc_filled_area( $img, $area_no, $ix, $iy );
}
}
}
die 'filled area not found.';
}
sub get_angle {
my ( $img, $area_no ) = @_;
my $cur_angle = .0;
my $cur_value = calc_evaluation_value( $img, $cur_angle, $area_no );
my $step = 32.0;
while ( 0.1 < abs($step) ) {
my $new_angle = $cur_angle + $step;
my $new_value = calc_evaluation_value( $img, $new_angle, $area_no );
# printf( "Current %5.1f: %3d, New %5.1f: %3d\n",
# $cur_angle, $cur_value,
# $new_angle, $new_value );
if ( $new_value < $cur_value ) {
$cur_value = $new_value;
$cur_angle = $new_angle;
}
else {
$step *= -0.5;
}
}
return $cur_angle;
}
sub calc_evaluation_value {
my ( $img, $rot_angle, $area_no ) = @_;
my $img_tmp = $img->rotate(
degrees => $rot_angle, back => 'black' );
my $xmin = int( $img_tmp->getwidth() / 2 );
my $xmax = int( $img_tmp->getwidth() / 2 );
my $iy = $img_tmp->getheight();
while ( 0 < $iy-- ) {
my $tmp = $img_tmp->getsamples( y => $iy, channels => [0] );
my @pixels = unpack( 'C*', $tmp );
if ( grep { $_ == $area_no } @pixels ) {
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;
}
}
return $xmax - $xmin + 1;
}
```