0

canvasで画像処理

html5のcanvasのピクセル単位の処理を試してみた。ここで動かせる → http://dev.shokai.org/test/canvas/cv/

ソースはここ https://github.com/shokai/js-canvas-test/tree/master/cv


輪郭抽出とかしてみた
html5 canvas edge detect


■canvasでのピクセル処理方法
canvasはgetContext(‘2d’)してからgetImageDataするとImageDataオブジェクトが手に入る。
あとの処理はこんな感じ(画像をグレースケールにする例、cv/main.jsの39行目あたり

var canvas = $('canvas#img');
var ctx = canvas[0].getContext('2d');

var img = ctx.getImageData(0, 0, canvas[0].width, canvas[0].height);
for(var i = 0; i < img.data.length; i+=4){
var r = img.data[i]&0xFF;
var g = img.data[i+1]&0xFF;
var b = img.data[i+2]&0xFF;
// var a = img.data[i+3]&0xFF;
var gray = (r+g+b)/3;
img.data[i] = gray;
img.data[i+1] = gray;
img.data[i+2] = gray;
}
ctx.putImageData(img, 0, 0);
データが1次元配列になっていて、RGBAの順に値が格納されている。なのでImageData.dataの要素数はピクセル数の4倍になる。
x,y座標でデータを取れる関数が無いので、自分で色々試して作ってみたけどどうしても3〜6倍ぐらい遅くなってしまうので1次元配列のまま扱う事にした。
JavaScriptで多値を扱うには配列やオブジェクトを使わなければならないんだけどどうやらそれらの生成コストがかなり高いらしく、1ピクセルずつ処理したらひどく遅くなった。


■クロスドメイン
別ドメインの画像をcanvasに読み込んで、getImageDataはできない。chromeだと “Uncaught Error: SECURITY_ERR: DOM Exception 18” というエラーが開発パネルに出る。
ローカルでの開発時にローカルのhtmlからローカルの画像を読み込んでgetImageDataしてもこのセキュリティエラーが出るので、rubyで簡単なhttpサーバーを作っておいたのでコレ使うといい。
実行するとそのディレクトリでhttpサーバーが起動する。


■実行速度
端末によって実行速度が違う。

下の画像は、今年の2月に買った11インチMacbook Airのchromeで、1000×1000の画像を4段階量子化したところ。
だいたい50ミリ秒で量子化できた。遅いパソコンでも毎秒数回は実行できそう。
iPodTouch 4Gや初代iPadはMacbook Airより30倍ぐらい遅くて2秒弱かかる。Nexus Sが意外なことに更に遅くて25秒ぐらいかかる。Macの500倍遅い。
html5 canvas qunatize

2

scansnapで自炊した本をkindleで読めるように補正する(2)

橋本商会 scansnapで自炊した本をkindleで読めるように補正するをOpenCV使ってC++で書き直したら60倍速くなった。635ページのオライリーの本が約1分半で処理できたし、画質も前のJRuby版より少し読みやすくなった気がする。


こういう事をする。

■やっている事
scansnapで自炊した本をそのままkindleで表示するには色々問題がある。

  • kindleの解像度は800×600だけど、画面内にページ位置等のUIが入るし画面を縦横回転させたり拡大縮小できるので、表示時にkindle上でリサイズされる。この時、線が細くて薄い部分が消滅してしまって、文字がかすれて読めなくなってしまう。
  • scansnapで取り込んだままのPDFファイルでは、コントラストが弱くてとても読みづらい。
  • ページの上下左右の余白が邪魔で、kindleに全画面表示した時に小さくなってしまう

kindle上でリサイズされる事を前提として、文字が消えてしまうような細い部分を太くしてやるしかない。
しょうがないので、コントラストを上げたり、拡大縮小されるのを前提として先にアンチエイリアス?的な処理をしたり、余白を切り取ったり、リサイズしたりするようにした。
上下左右を裁ち落とし→リサイズ→グレースケール化→2値化→黒の周りを#999999で太らせるという処理をしている。


上下左右裁ち落とし→グレースケール化→2値化→黒の周りを#000000で太らせる→リサイズ
という処理に変えた。JRuby+javax.imageioでピクセル単位に処理すると、解像度が大きい画像を処理するのが苦しかったので先にリサイズするようにしていた。
OpenCVは十分速いので、高解像度のまま処理して最後に縮小するように変更した。これでサイズが大きい本でも綺麗に変換できる。


■使う
OpenCV1.00以上と、boostのboost::system, boost::filesystem, boost::program_optionsのインストールが必要。少なくともMacとUbuntuでは動く。
その辺はgithub/shokai/scansnap_adjust_images_kindleに書いた。


gitリポジトリを持ってきてコンパイルする

git clone git://github.com/shokai/scansnap_adjust_images_kindle.git
cd scansnap_adjust_images_kindle/kindlize_images

# for Mac
make -f Makefile.macosx

./configureの作り方がよくわからないのでとりあえずMac用のMakefileだけ置いてある。
Linuxとかでも、opencv、boost::filesystem、boost::progoram_optionsをインストールしてあればコンパイルはできる。(この組み合わせは色々なOSでよく使ってる)


オライリーの判型の本をkindle用に補正する。上下左右の余白を削除して白黒になって文字が太くなる。イラストは見づらくなるかもしれない。
mkdir ~/tmp/mybook_kindle
./kindlize_images --help
./kindlize_images -i ~/tmp/mybook/ -o ~/tmp/mybook_kindle/ -t 190 -w 1200 -h 1600 --cleft 120 --cright 120 --ctop 150 --cbottom 150


はじめにPDFから連番のJPEG画像に書き出して、それからこのツールを使って変換して、最後に連番画像をPDFにまとめるという使い方を想定している。
PDFから連番画像を書き出す方法は、前の記事やgithubのREADME.mdに書いた。
連番画像を1つのPDFへ結合するのは、gitリポジトリの中に一緒にimages2pdf.appというautomatorアプリを入れて置いたのでそれを使うと良い。


■ソースコード
100行切った。boost::filesystemのおかげでディレクトリ内一括処理が書きやすかった。
JRuby使って書くよりもコードが35行短くなったけど、最初に試行錯誤するにはJRuby+javax.imageioでやる方が手軽だったな。

kindlize_images/kindlize_images.cpp at master from shokai's scansnap_adjust_images_kindle – GitHub
#include "cv.h"
#include "highgui.h"
#include <boost/program_options.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>
#include <iostream>
using namespace boost;
using namespace std;
namespace fs = boost::filesystem;

IplImage *adjust_image(IplImage *img, program_options::variables_map argmap){
int cleft = argmap["cleft"].as<int>();
int cright = argmap["cright"].as<int>();
int ctop = argmap["ctop"].as<int>();
int cbottom = argmap["cbottom"].as<int>();

const int w = img->width - cleft - cright;
const int h = img->height - ctop - cbottom;

cvSetImageROI(img, cvRect(cleft, ctop, w, h));

IplImage *img_gray = cvCreateImage(cvSize(w,h), IPL_DEPTH_8U, 1);
cvCvtColor(img, img_gray, CV_BGR2GRAY);

// 2値化
IplImage *img_bin = cvCreateImage(cvSize(w,h), IPL_DEPTH_8U, 1);
cvThreshold(img_gray, img_bin, argmap["threshold"].as<int>(), 255, CV_THRESH_BINARY);

// 文字を太らせる
cvErode(img_bin, img_bin, NULL, 1);

// リサイズ
int width = argmap["width"].as<int>();
int height = argmap["height"].as<int>();
IplImage *img_resized;
double scale = ((double)h)/w;
if(((double)height)/width < scale){ // 縦長
img_resized = cvCreateImage(cvSize((int)(height/scale), height), IPL_DEPTH_8U, 1);
}
else{ // 横長
img_resized = cvCreateImage(cvSize(width, (int)(scale*width)), IPL_DEPTH_8U, 1);
}
cvResize(img_bin, img_resized, CV_INTER_LINEAR);

cvReleaseImage(&img_bin);
cvReleaseImage(&img_gray);
return img_resized;
}

int main(int argc, char* argv[]) {
program_options::options_description opts("options");
opts.add_options()
("help", "ヘルプを表示")
("width,w", program_options::value<int>(), "output width")
("height,h", program_options::value<int>(), "output height")
("threshold,t", program_options::value<int>(), "binarize threshold")
("input,i", program_options::value<string>(), "input directory name")
("output,o", program_options::value<string>(), "output directory name")
("cleft", program_options::value<int>(), "crop left (pixel)")
("cright", program_options::value<int>(), "crop right (pixel)")
("ctop", program_options::value<int>(), "crop top (pixel)")
("cbottom", program_options::value<int>(), "crop bottom (pixel)");
program_options::variables_map argmap;
program_options::store(parse_command_line(argc, argv, opts), argmap);
program_options::notify(argmap);
if (argmap.count("help") || !argmap.count("input") || !argmap.count("output") ||
!argmap.count("threshold")) {
cerr << "[input, output] required" << endl;
cerr << opts << endl;
return 1;
}

string in_dir = argmap["input"].as<string>();
fs::path path = complete(fs::path(in_dir, fs::native));
fs::directory_iterator end;
for (fs::directory_iterator i(path); i!=end; i++){
string img_fullname = in_dir + i->leaf();
cout << img_fullname << endl;
IplImage *img, *img_result;
img = cvLoadImage(img_fullname.c_str());
if(!img){
cerr << "image file load error" << endl;
}
else{
img_result = adjust_image(img, argmap);
string out_filename = argmap["output"].as<string>() + "/" + i->leaf();
cvSaveImage(out_filename.c_str(), img_result);
cvReleaseImage(&img);
cvReleaseImage(&img_result);
}
}
}

2

scansnapで自炊した本をkindleで読めるように補正する

最近新しく発売されたkindle黒が届いたが、scansnapで作ったPDFを入れてみたら文字がかすれて読めなくなって悲しかったので、なんとかするスクリプトをJRubyで作った。
http://github.com/shokai/scansnap_adjust_images_kindleに連番画像の補正プログラムと、連番画像をPDFにまとめるautomatorアプリを置いておいた。
JRuby1.5+Java1.5以上なら動くと思う。

scansnapの設定は橋本商会 scansnapと裁断機を買って本を電子化しまくるに書いたのと同じ。白黒ページはスーパーファインのグレースケールで取り込んでいる。

文庫本を補正した場合こうなる。
日本語の文庫本を読めるようにする事を目的に作ってるので、他の判型だとちょっとダメかも(理由は後述)
R0015336.JPG


詳解OpenCVという本を、補正をかけずに表示したところ。
文字の中でも線の細い部分だけが、kindle上でリサイズした時に消滅するので字が読めない
R0015339.JPG


詳解OpenCVの同じページ補正するとこうなる。
それなりに読めるが、線の密度とか計算せずに全て太らせているのでアルファベットや記号がところどころ融合して読みづらい。
R0015337.JPG


■やっている事
scansnapで自炊した本をそのままkindleで表示するには色々問題がある。

  • kindleの解像度は800×600だけど、画面内にページ位置等のUIが入るし画面を縦横回転させたり拡大縮小できるので、表示時にkindle上でリサイズされる。この時、線が細くて薄い部分が消滅してしまって、文字がかすれて読めなくなってしまう。
  • scansnapで取り込んだままのPDFファイルでは、コントラストが弱くてとても読みづらい。
  • ページの上下左右の余白が邪魔で、kindleに全画面表示した時に小さくなってしまう

kindle上でリサイズされる事を前提として、文字が消えてしまうような細い部分を太くしてやるしかない。
しょうがないので、コントラストを上げたり、拡大縮小されるのを前提として先にアンチエイリアス?的な処理をしたり、余白を切り取ったり、リサイズしたりするようにした。
上下左右を裁ち落とし→リサイズ→グレースケール化→2値化→黒の周りを#999999で太らせるという処理をしている。



■画像を補整する

まずPDFから連番画像に戻す。MacやLinuxなら、pdfimagesというツールを使うといい。
# for Mac
% sudo port install pdfX

# for Ubuntu
% sudo apt-get install xpdf-utils

# extract images
% mkdir -p ~/tmp/mybook
% pdfimages -j ~/Documents/book/mybook.pdf ~/tmp/mybook/
もしくはAcrobat Proを買って「書き出し」でもできる。


補正する。githubから俺の作ったツールを持ってくる。
% git clone git://github.com/shokai/scansnap_adjust_images_kindle.git
% cd scansnap_adjust_images_kindle


で、先ほど作った連番画像のディレクトリを元にして、補正した連番画像を作る
% jruby kindlize_images.rb -help
% jruby kindlize_images.rb -i ~/tmp/mybook/ -o ~/tmp/mybook_kindle/ -w 1200 -h 1600 -cl 150 -cr 150 -ct 120 -cb 180 -t 240
出力ディレクトリは無ければ自動的に作成される。

上はオライリーの本の場合の裁ち落とし。
文庫の場合はこんなパラメータ
jruby kindlize_images.rb -i ~/tmp/mybook/ -o ~/tmp/mybook_kindle/ -w 1200 -h 1600 -cl 30 -cr 30 -ct 80 -cb 115 -t 220


作った連番画像をPDFに戻すには、cloneしたgitリポジトリ内のimages2pdf.appを使えばいい。MacのAutomator.appで作ったアプリ。
Preview.appでもPDFのページを並べ替えたりできるが、連番ページを順番に入れる事はできなかったので作った。



■問題点
文字を太らせる処理は、1 pixelずつグレーで周りを塗っているだけなので、文字のサイズや密度を考慮していない。
処理速度も遅い。300ページの本でも1時間近くかかる。まあ読む速度より遅くはないからいいや。もっとたくさんkindleに入れたくなったらC++でOpenCV使って書き直す。


■ソース
Javaのjavax.imageioをRubyから使うのが簡単な画像処理に便利なので、JRubyを使ってます

kindlize_images.rb
#!/usr/bin/env jruby
# -*- coding: utf-8 -*-
# ディレクトリ内の画像を全てkindleで読みやすいように2値化、アンチエイリアス、リサイズする
# イラストも2値化されるので、小説などの文字ページ専用。
require 'rubygems'
require 'ArgsParser'
require 'java'
import 'java.lang.System'
import 'javax.imageio.ImageIO'
import 'java.awt.image.BufferedImage'
import 'java.awt.image.WritableRaster'
import 'java.awt.image.AffineTransformOp'
import 'java.awt.geom.AffineTransform'
import 'java.awt.Color'
$KCODE = 'u'

parser = ArgsParser.parser
parser.bind(:width, :w, 'width')
parser.bind(:height, :h, 'height')
parser.bind(:input, :i, 'input')
parser.bind(:output, :o, 'output')
parser.bind(:cleft, :cl, 'crop left (pixel)')
parser.bind(:cright, :cr, 'crop right (pixel)')
parser.bind(:ctop, :ct, 'crop top (pixel)')
parser.bind(:cbottom, :cb, 'crop bottom (pixel)')
parser.bind(:threshold, :t, 'threshold of binarize (0-255)')
first, params = parser.parse(ARGV)

if !parser.has_params([:width, :height, :input, :output, :threshold]) or
parser.has_option(:help)
puts parser.help
puts 'e.g. jruby kindlize_images.rb -i /path/to/in_dir/ -o /path/to/out_dir/ -w 1200 -h 1600 -t 240'
puts 'e.g. jruby kindlize_images.rb -i /path/to/in_dir/ -o /path/to/out_dir/ -w 1200 -h 1600 -cleft 50 -cright 50 -ctop 80 -cbottom 100 -t 240'
exit 1
end

p params
WIDTH = params[:width].to_i
HEIGHT = params[:height].to_i
Dir.mkdir(params[:output]) unless File.exists? params[:output]
params[:output] += '/' unless params[:output] =~ /\/$/

Dir.glob(params[:input]+'*').each{|i|
puts i
begin
img = ImageIO.read(java.io.File.new(i))
rescue => e
STDERR.puts 'image load error'
STDERR.puts e
next
end
puts "size : #{img.width}x#{img.height}"

if params[:cleft] or params[:cright] or params[:ctop] or params[:cbottom]
params[:cleft] = 0 unless params[:cleft]
params[:cright] = 0 unless params[:cright]
params[:ctop] = 0 unless params[:ctop]
params[:cbottom] = 0 unless params[:cbottom]
img = img.get_subimage(params[:cleft].to_i, params[:ctop].to_i,
img.width-params[:cleft].to_i-params[:cright].to_i,
img.height-params[:ctop].to_i-params[:cbottom].to_i)
puts "crop : #{img.width}x#{img.height}"
end

# リサイズ
if img.width.to_f/img.height > WIDTH.to_f/HEIGHT # 指定されたWIDTH,HEIGHTよりも横長の画像
scale = WIDTH.to_f/img.width
img_resized = BufferedImage.new(WIDTH, (scale*img.height).to_i, img.type)
else # 縦長
scale = HEIGHT.to_f/img.height
img_resized = BufferedImage.new((scale*img.width).to_i, HEIGHT, img.type)
end
puts "scale : #{scale}"
AffineTransformOp.new(AffineTransform.getScaleInstance(scale, scale), nil).filter(img, img_resized)
puts "resized : #{img_resized.width}x#{img_resized.height}"

# 固定サイズ画像にはめこむ
img_frame = BufferedImage.new(WIDTH, HEIGHT, img.type)
graph = img_frame.graphics
graph.color = Color.new(255,255,255)
graph.fillRect(0, 0, WIDTH, HEIGHT)
if WIDTH > img_resized.width
graph.drawImage(img_resized, (WIDTH-img_resized.width)/2, 0, nil)
else
graph.drawImage(img_resized, 0, (HEIGHT-img_resized.height)/2, nil)
end
puts "set in frame : #{img_frame.width}x#{img_frame.height}"
img = img_frame

# 2値化
for y in 0...img.height do
for x in 0...img.width do
pix = img.get_rgb(x, y)
r = pix >> 16 & 0xFF
g = pix >> 8 & 0xFF
b = pix & 0xFF
gray = (r+g+b)/3
if gray > params[:threshold].to_i
pix = 0xFFFFFF
else
pix = 0x000000
end
img.set_rgb(x, y, pix)
end
end
puts "binarize"

# 膨張
img_dilated = BufferedImage.new(img.width, img.height, img.type)
for y in 1...img.height-1 do
for x in 1...img.width-1 do
if img.get_rgb(x, y)&0xFF == 0
img_dilated.set_rgb(x, y, 0x000000)
elsif img.get_rgb(x-1, y)&0xFF == 0 or img.get_rgb(x+1, y)&0xFF == 0 or
img.get_rgb(x, y-1)&0xFF == 0 or img.get_rgb(x,y+1)&0xFF == 0
img_dilated.set_rgb(x, y, 0x999999)
else
img_dilated.set_rgb(x, y, 0xFFFFFF)
end
end
end
img = img_dilated
puts "dilate"

out_name = i.split(/\//).last
out_type = 'bmp'
begin
ImageIO.write(img, out_type, java.io.File.new(params[:output]+out_name))
puts 'saved! => '+params[:output]+out_name
rescue => e
STDERR.puts 'image save error'
STDERR.puts e
next
end
}

0

ZeroMQでOpenCV cvOpticalFlowのデータを配信する

1VQ9がZeroMQで遊んでたので、俺も橋本商会 cvCalcOpticalFlowBMをZeroMQでpubしてみた。ZeroMQはなんか面倒な事を適当にやってくれるmessaging libraryで、色々な言語のバインディングが出ている。

ZeroMQのpubはセンサーのデータとかを垂れ流しにするのに都合がよさそう。
clientが何台いるかどうかを考えないで良いし、pub/subどちらが先に起動していても適当に接続処理をしてくれる。cookbookを見てるとmulticastやthread間通信にも使ってる。とりあえずセンサーデータ垂れ流しという用途に俺はよく使いそう。


ソースコードはgithubに置いた
他にも単純なカウントアップのpub/sub両方をC++/C/Rubyで書いた(6種)のと、twitterのstream APIをZMQ_PUBで中継するのを作ってみた(解説:zeromqインストール、twitter stream APIを中継 – 橋本詳解)。特にstream APIのHUB的存在は便利。

あと、mongrel2WebSocketやXMLSocketとZeroMQの接続をしてくれるようになるらしくて期待してる。



受信側
opticalflow_sub.rb

#!/usr/bin/env ruby
require 'rubygems'
require 'zmq'

ctx = ZMQ::Context.new
sock= ctx.socket(ZMQ::SUB)
sock.connect('tcp://127.0.0.1:5000')
sock.setsockopt(ZMQ::SUBSCRIBE, 'opticalflow')

loop do
puts sock.recv()
end


送信側。これを適当なパソンコにUSBカメラ刺して動かしておけば、別のマシンから動きが取れる!!
opticalflow_pub.cpp
// http://opencv.jp/sample/optical_flow.html
#include <cv.h>
#include <highgui.h>
#include <cxcore.h>
#include <ctype.h>
#include <stdio.h>
#include <iostream>
#include <boost/format.hpp>
#include <zmq.hpp>

using namespace std;
using namespace boost;

void detect_flow(IplImage *img, IplImage *img_p, IplImage *dst);
zmq::context_t ctx(1);
zmq::socket_t sock(ctx, ZMQ_PUB);

int main(int argc, char* argv[]) {
IplImage *img = NULL;
CvCapture *capture = NULL;
capture = cvCreateCameraCapture(0);
//capture = cvCaptureFromAVI("test.mov");
if(capture == NULL){
cerr << "capture device not found!!" << endl;
return -1;
}

sock.bind("tcp://127.0.0.1:5000");

CvSize size = cvSize(320, 240);
IplImage *img_resized = cvCreateImage(size, IPL_DEPTH_8U, 3);
IplImage *img_gray = cvCreateImage(size, IPL_DEPTH_8U, 1);
IplImage *img_gray_p = cvCreateImage(size, IPL_DEPTH_8U, 1);
IplImage *img_dst = cvCreateImage(size, IPL_DEPTH_8U, 3);

char winNameCapture[] = "Capture";
cvNamedWindow(winNameCapture, CV_WINDOW_AUTOSIZE);

while (1) {
img = cvQueryFrame(capture);
cvResize(img, img_resized);
cvCvtColor(img_resized, img_gray, CV_BGR2GRAY);
cvCopy(img_resized, img_dst);
detect_flow(img_gray, img_gray_p, img_dst);
cvShowImage(winNameCapture, img_dst);
cvCopy(img_gray, img_gray_p);
if (cvWaitKey(10) == 'q') break;
}

cvReleaseCapture(&capture);
cvDestroyWindow(winNameCapture);

return 0;
}

void detect_flow(IplImage *src_img1, IplImage *src_img2, IplImage *dst_img){
int i, j, dx, dy, rows, cols;
int block_size = 24;
int shift_size = 10;
CvMat *velx, *vely;
CvSize block = cvSize(block_size, block_size);
CvSize shift = cvSize(shift_size, shift_size);
CvSize max_range = cvSize(50, 50);

rows = int(ceil (double (src_img1->height) / block_size));
cols = int(ceil (double (src_img1->width) / block_size));
velx = cvCreateMat(rows, cols, CV_32FC1);
vely = cvCreateMat(rows, cols, CV_32FC1);
cvSetZero(velx);
cvSetZero(vely);

cvCalcOpticalFlowBM(src_img1, src_img2, block, shift, max_range, 0, velx, vely);
string result_str = string("");
for (i = 0; i < velx->width; i++) {
for (j = 0; j < vely->height; j++) {
dx = (int)cvGetReal2D(velx, j, i);
dy = (int)cvGetReal2D(vely, j, i);
cvLine(dst_img, cvPoint(i * block_size, j * block_size),
cvPoint(i * block_size + dx, j * block_size + dy), CV_RGB(255, 0, 0), 1, CV_AA, 0);
if(dx != 0 || dy != 0){
result_str += str(format("[%d,%d,%d,%d]") % (i*block_size) % (j*block_size) % dx % dy);
}
}
}
if(result_str.size() > 0){
result_str = str(format("opticalflow %s") % result_str);
cout << result_str << endl;
zmq::message_t msg(result_str.size()+1); // ZeroMQ
memcpy(msg.data(), result_str.c_str(), result_str.size()+1);
sock.send(msg);
}
}


g++ -O opticalflow_pub.cpp -o opticalflow_pub.bin -I/opt/local/include/opencv -lcv -lcvaux -lcxcore -lhighgui  -I/usr/local/include /usr/local/lib/libzmq.a


これで動いた座標とその方向 [x,y,dx,dy] が連続で送られてくる。
opticalflow [48,216,4,-29][72,216,0,-29][96,216,0,-29][264,216,-9,-29]
opticalflow [48,216,4,-29][96,216,0,-29][120,216,0,-29][264,216,-9,-29]
opticalflow [48,216,4,-29][96,168,0,10][96,192,-10,-20][96,216,0,-29][120,192,0,10][120,216,0,-29][144,216,0,-29][168,216,0,-29][192,48,-10,0][192,216,0,-29][216,216,0,-29][264,216,-9,-29]
opticalflow [96,168,0,10][96,192,-10,-10][96,216,0,-29][120,168,0,10][120,192,0,10][120,216,0,-29][144,216,0,-29][168,48,0,10][168,96,0,10][168,216,0,-29][192,72,0,40][192,96,0,-30][192,216,0,-29][264,216,-9,-29]
opticalflow [48,216,4,-29][96,168,0,10][96,216,0,-29][120,168,0,10][120,192,0,10][120,216,0,-29][144,216,0,-29][168,48,10,0][168,96,0,10][168,216,0,-29][192,96,0,-30][192,216,0,-29][264,216,-9,-29]

0

JRubyでglitch iconを作る(2)

プラグイン機構を採用し、ランダムにいろいろ作れるようにしてみた。
普通はjpegとかのバイナリを直接いじるみたいだけどよく分からないのでJRubyでjavax.imageioを使ってやっている。

ランダムに96個作ってみて8×12に敷き詰めてみたのがこれ。
で、定期的にランダムに作ってtwitterのアイコンとしてアップロードしてる
montage96
敷き詰めるのはImageMagickと一緒にインストールされるmontageコマンドでできる montageコマンド – 橋本詳解

ソースコードは全てgithubに置いた


■仕組み
単機能モジュールをランダムに連結する事である程度ランダムな画像を生成できる。
javax.imageio.BufferedImageのインスタンスを受け取り、少し加工して返すというJRubyのmoduleをプラグインとし、それらをGlitchというclassがランダムに呼び出す。


今のところプラグインは23種類ある。単体だとそれほど派手にはならない。
glitchicon montage



■プラグイン
http://github.com/shokai/glitchicon/tree/master/plugins/にある。
pluginが動くルールはこれ。

  • pluginsディレクトリの中に置いておく
  • ファイル名末尾が.rbである
  • JRubyのmoduleである
  • ファイル名の先頭1文字を大文字にしたmodule名である
  • BufferedImageを受け取って、同じサイズのBufferedImageを返す glitch(BufferedImage) というstatic methodを持つ
  • 受け取ったBufferedImageに対しては破壊的に処理しても、そうでなくてもいい

プラグインはBufferedImageの処理に集中できるように、他の事はglitch.rbがやる。


どのプラグインも簡単にできている。30行ぐらい。

drumroll_verticalプラグインの場合(画像では一番左、下から2番目)
#!/usr/bin/env jruby
require 'java'
import 'java.lang.System'
import 'javax.imageio.ImageIO'
import 'java.awt.image.BufferedImage'


module Drumroll_vertical

  def Drumroll_vertical.glitch(img)
    img_result = BufferedImage.new(img.width, img.height, img.type)
    roll = 0
    for x in 0...img.width do
      roll = rand(img.height) if rand > 0.95
      roll = 0 if rand > 0.95
      for y in 0...img.height do
        pix = img.get_rgb(x, y)
        y2 = y+roll
        y2 -= img.height if y2 > img.height-1
        img_result.set_rgb(x, y2, pix)
      end
    end
    return img_result
  end

end
ランダムに縦に区切って、ランダムにスロットのドラムみたいにずらす。


色を反転させるcolor_reverseプラグイン
#!/usr/bin/env jruby
require 'java'
import 'java.lang.System'
import 'javax.imageio.ImageIO'
import 'java.awt.image.BufferedImage'


module Color_reverse

  def Color_reverse.glitch(img)
    for y in 0...img.height do
      for x in 0...img.width do
        pix = img.get_rgb(x, y)
        r = pix >> 16 & 0xFF
        g = pix >> 8 & 0xFF
        b = pix & 0xFF
        r = 256-r
        g = 256-g
        b = 256-b
        pix = ((r << 16)&0xFF0000 | (g << 8)&0xFF00 | b)
        img.set_rgb(x,y, pix)
      end
    end
    return img
  end

end


量子化した後に輪郭抽出するquantize_contour
#!/usr/bin/env jruby
require 'java'
import 'java.lang.System'
import 'javax.imageio.ImageIO'
import 'java.awt.image.BufferedImage'


module Quantize_contour

  def Quantize_contour.glitch(img)

    for y in 0...img.height do
      for x in 0...img.width do
        pix = img.get_rgb(x, y)
        r = pix >> 16 & 0xFF
        g = pix >> 8 & 0xFF
        b = pix & 0xFF
        gray = (r+g+b)/3
        quant = gray & 0xC0
        pix = ((quant << 16)&0xFF0000 | (quant << 8)&0xFF00 | quant)
        img.set_rgb(x, y, pix)
      end
    end

    img_result = BufferedImage.new(img.width, img.height, img.type)
    for y in 1...img.height-1 do
      for x in 1...img.width-1 do
        pix = img.get_rgb(x, y)
        around = (img.get_rgb(x-1,y)+img.get_rgb(x+1,y)+img.get_rgb(x,y-1)+img.get_rgb(x,y+1))/4
        if around < pix
          pix = 0
        else
          pix = 0xFFFFFF
        end
        img_result.set_rgb(x, y, pix)
      end
    end
    return img_result
  end

end


そんなかんじ。画像をいじるけど、元の画像を知っている人なら元画像を思い浮かべられる程度にglitchしたい。