小ネギソートを作りたい!

Created by @techno_neko

## 自己紹介 - 仕事: 組み込み系 - 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; } ```
## ご静聴ありがとうございました!