前々から、CARL社のディスクカッターDC-210Nで本を切っていたんだけど、最近この機器に必要な40枚(実際は漫画ぐらいの紙の分厚さになると50p=25枚が妥当な印象)ごとに分割する処理が大変になってきて、裁断機がほしくなってきていた。
安全性は気にしている製品がいいし、定番どころでPK-513Lか、その後継のPK-513LNかなぁと思っていたんだけど、ハンドルが畳めないらしく、それはちょっと邪魔だなぁ、いいのないかなぁと思ってやめていた。
ところが、高崎精器というところが出している180AT-Pだと、LEDライトも含めPK-513Lとほぼ同じ性能でハンドルが畳めるというじゃないですか。
ということで180AT-Pを買ってみた。
裁断能力という点では、DC-210Nと比較して40枚が180枚になったというのはやはり大きくて、分厚くない文庫本とか漫画の類なら一発でいけるのは非常に楽。大きい本だとさすがに分けなくちゃいけないけど、手間の量が段違い。作業って、時間も重要な要素だけど、手間っていうのも重要なんだなぁと認識した。
配置しておく場所を作ったので、実はこのブログにあるような立ててしまっておくということはしていないんだけど、ハンドルを折っておけるメリットとして、安心感というのがあるということに気づいた。
というのも、ハンドルが上がった状態だと、ロックはかかっていて実際のところは問題ないのだけど、下方向に力をかけられてしまう。でも、ハンドルが畳んである状態だと、平たいのでそもそも力をかける状況になりにくい。(あそびはあって、押すとちょっとだけ下がるけど)
この安心感は違うなぁと思った。
あとは、LEDが思ったより役に立つということ。裁断機って、こんなに切る場所を定めにくいとは思わなかった。慣れるまでかもしれないけど、期待してなかったけどあってよかった装備。とはいえ、表紙が大きすぎてスキャナに入らない、といった1枚っぺらのようなものだと、これからもディスクカッターを使うけどね。
というわけで、良製品なのに、全然メジャーな感じがしない180AT-Pについて、書いておいてみました。
2012-08-19
ダ・ヴィンチ32UとフリスクケースとUbuntuと
ダ・ヴィンチ32U (Da Vinci 32U) という、ストロベリー・リナックスというところが出しているマイコンボードがある。このボード、「Arduino Leonardo」っぽく動く機械なのだが、1つ1260円という安価で購入できる。その上コンパクトなので、ちょっとしたUSBデバイスを作るのには最適な機械なのだ。
(念のために書いておくと、「Arduino」 とは、ハード的にはAtmel社のマイコン、AVRが乗ったマイコンボードである。しかし、このボードには、専用の簡単なIDE(ほぼエディタ)と、豊富なライブラリ集があり、USBの口から簡単にファームウェアを転送できるので、素でAVRを使うよりずっと簡単に機械を作れる。一つ一つを取ると決してすごくないのだが、全体を合わせたときのお手軽さは、 よくできている。)
で、このダ・ヴィンチ32Uがどのくらいコンパクトかというと、フリスクケースに収まるので、入れてみた。
この赤い基板がダ・ヴィンチ32U。つけたのは、赤外線LEDと抵抗、赤外線リモコン用センサーだけ。
閉じたときの絵:
食べるときみたいにちょっとだけ開けると、読み取りセンサーが使えるようになる。見てわかる通り、フリスクケースに入れてもなお隙間ができるくらい小さいので、物は入れやすい。
フリスクケースは、比較的薄いので、ちょっと力を入れればカッターでも切れる。危ないから、ピンバイスで穴を開けてから広げたほうがいいとは思うけど。
問題は、本来はピンヘッダをつけて、何かに刺して固定する製品のようなので、固定がちょっと難しい。うまい方法が思いつかなかったので、ケースに千枚通しで穴を開けて、右端のGNDピンと、ISPピンに糸を通して、それぞれ縫ってしまった。玉止めなにそれうれしいのという感じになるので、おすすめしない。たぶん、ホットボンドなんかで止めるのがいいんじゃないかなと思う。
なお、ボードに書かれているピン名称は、Leonardoのものと異なるので、読み替えが必要。いくつかのサイトに載っているので、参考にできる。たとえばここ:http://dev.tetrastyle.net/2012/02/leonardo.html
なので、http://arduino.cc/en/Main/Software からLinux/32bit版を落としてきて、tar zxvfで/optに展開したら、Leonardを選択することで「Linuxでは」使えるようになった。
(Ubuntu本体に入っているarduino packageには依存関係があるので、apt-get install arduinoで1.0を普通にインストールしてからやったほうがいいと思う。)
ただし、この方法で書き込んでしまったダ・ヴィンチ32Uは、Leonardのbootloaderが書き込まれてしまうので、strawberry-linux公式の言うとおり、Windowsでは使えなくなってしまう(Leonardとして認識されるが、arduino.ccのドライバもstrawberry-linuxのドライバも受け付けない)。
そのため、Ubuntuであっても、最初からstrawberry-linuxのWindows版ドライバのReadmeに書いてある通り、boards.txtを上書きし、ダ・ヴィンチ32Uの定義で使うのがよい。
うちのようにLeonardのbootloaderをうっかり上書きしてしまったらどうするか。Linuxではまだ書き込みができるので、ドライバに含まれている「da-vinci-32u-master.hex」を、上記のboards.txtに書いてある「davinci.hex」というファイル名で、しかるべき場所にコピーしてあげれば、書き込み(Upload)時に、ファームを更新してくれて、Windowsでも使えるようになる。
そもそも簡単にUSBデバイスとして振舞えるようになったというのは大きく違うので、せっかくだからそのうち遊んでみたい。
http://trac.switch-science.com/wiki/tinker-with-Leonardo
(念のために書いておくと、「Arduino」 とは、ハード的にはAtmel社のマイコン、AVRが乗ったマイコンボードである。しかし、このボードには、専用の簡単なIDE(ほぼエディタ)と、豊富なライブラリ集があり、USBの口から簡単にファームウェアを転送できるので、素でAVRを使うよりずっと簡単に機械を作れる。一つ一つを取ると決してすごくないのだが、全体を合わせたときのお手軽さは、 よくできている。)
で、このダ・ヴィンチ32Uがどのくらいコンパクトかというと、フリスクケースに収まるので、入れてみた。
この赤い基板がダ・ヴィンチ32U。つけたのは、赤外線LEDと抵抗、赤外線リモコン用センサーだけ。
閉じたときの絵:
食べるときみたいにちょっとだけ開けると、読み取りセンサーが使えるようになる。見てわかる通り、フリスクケースに入れてもなお隙間ができるくらい小さいので、物は入れやすい。
フリスクケースは、比較的薄いので、ちょっと力を入れればカッターでも切れる。危ないから、ピンバイスで穴を開けてから広げたほうがいいとは思うけど。
問題は、本来はピンヘッダをつけて、何かに刺して固定する製品のようなので、固定がちょっと難しい。うまい方法が思いつかなかったので、ケースに千枚通しで穴を開けて、右端のGNDピンと、ISPピンに糸を通して、それぞれ縫ってしまった。玉止めなにそれうれしいのという感じになるので、おすすめしない。たぶん、ホットボンドなんかで止めるのがいいんじゃないかなと思う。
なお、ボードに書かれているピン名称は、Leonardoのものと異なるので、読み替えが必要。いくつかのサイトに載っているので、参考にできる。たとえばここ:http://dev.tetrastyle.net/2012/02/leonardo.html
Ubuntuで使うには
Ubuntu 12.04では、arduinoのIDEは1.0が入っている。そのため、Leonardは使えない。 boards.txtを書き換えれば使えるかなーと思ったんだけど、以下のようなエラーが出て書き込みができなかった。Binary sketch size: 4450 bytes (of a 28672 byte maximum) avrdude: stk500_recv(): programmer is not responding
なので、http://arduino.cc/en/Main/Software からLinux/32bit版を落としてきて、tar zxvfで/optに展開したら、Leonardを選択することで「Linuxでは」使えるようになった。
(Ubuntu本体に入っているarduino packageには依存関係があるので、apt-get install arduinoで1.0を普通にインストールしてからやったほうがいいと思う。)
ただし、この方法で書き込んでしまったダ・ヴィンチ32Uは、Leonardのbootloaderが書き込まれてしまうので、strawberry-linux公式の言うとおり、Windowsでは使えなくなってしまう(Leonardとして認識されるが、arduino.ccのドライバもstrawberry-linuxのドライバも受け付けない)。
そのため、Ubuntuであっても、最初からstrawberry-linuxのWindows版ドライバのReadmeに書いてある通り、boards.txtを上書きし、ダ・ヴィンチ32Uの定義で使うのがよい。
# cp da-vinci-32u/boards.txt /opt/arduino-1.0.1/hardware/arduino/boards.txt
うちのようにLeonardのbootloaderをうっかり上書きしてしまったらどうするか。Linuxではまだ書き込みができるので、ドライバに含まれている「da-vinci-32u-master.hex」を、上記のboards.txtに書いてある「davinci.hex」というファイル名で、しかるべき場所にコピーしてあげれば、書き込み(Upload)時に、ファームを更新してくれて、Windowsでも使えるようになる。
# cp da-vinci-32u/da-vinci-32u-master.hex /opt/arduino-1.0.1/hardware/arduino/bootloaders/caterina/davinci.hex
こうしてうっかり書いてしまったダ・ヴィンチ32Uを復活させる手段としてUbuntuを使うのもいいかもね。Arduino Unoとの違い
気をつけなくてはいけないかもしれない点として、Arduino UnoとArduino Leonardが若干違うところがあるようだ。スイッチサイエンスのページにまとめられている:http://trac.switch-science.com/wiki/Guide/ArduinoLeonardoそもそも簡単にUSBデバイスとして振舞えるようになったというのは大きく違うので、せっかくだからそのうち遊んでみたい。
http://trac.switch-science.com/wiki/tinker-with-Leonardo
C#でHelloWorldプログラムを作成する
C#を使い始めたので、ひとつ入門記事でも。Hello, Worldするプログラムを作るよ。
まずはソースから。
いつもの数行のコードだと思った?Hello, Worldプログラム自体を生成するプログラムでした。
プログラムの使い方はこう。第一引数にexeのファイル名を指定すると、Hello, World!を出力するexeファイルを作成する。
.NETの世界では、ローカル変数、スタック、引数という3つの枠があり、それぞれ必要に応じて移動して使う。引数の枠から、値をスタックに移動してきて、なんらかの操作を実行、必要に応じてローカル変数に退避する、という感じかな。
なお、ソースの頭のところは、お決まりの引数処理だったり.NETの仕組みみたいな話なので省略して進める。
あとでnewしたいので、デフォルトコンストラクタのConstructorBuilderをとっておく。
生成するコードは以下。
まずldarg_1を使って、関数の引数の1番目を評価スタックにロードする。なお、0はインスタンス。C#はthisはメソッド定義に書いてないけど、最初の引数がselfだったりthisだったりする言語あるよね。そんな感じ。
そして、callを使って、System.Console.WriteLineを呼び出す。
OpCodes.Callには、MethodInfoを渡してあげる必要があるが、これはリフレクションで取ってきてしまう。
上記の例では、stringという引数を持つWriteLineというメソッドを、System.Consoleのタイプから拾ってくる形である。
なぜCallのときはEmitではなく、EmitCallを使っているかというと、可変長引数を持つメソッド用の型をEmit()が受け取ることができないから。ここでは使ってないので、実はEmitでよい。
最後にreturnたるretを書いて、Sayの実装は終わり。
生成するコードは以下。
なお、お気づきかもしれないが、「3.2. インスタンスの生成」で、オブジェクトはスタックに生成されているので、いちいちローカル変数に入れなくてもいい。文字列も固定なので同様。なので、3.1.~は、以下のように書くこともできる。
あとは、エントリポイントを指定して、アセンブリをファイルにSaveするだけ。
ここまで読んだあなたは、C#を使って、プログラムを生成することができるようになっているはず。
フィールドとか、Genericとか、よく使いそうなものを全然解説していないけど、そこは「詳しくはWebで」という感じで。
Javaでも同じようなことがきっとできると思う。Cはどうだろう?LLVMとかの力を借りればいいのかな。
以下は例。
気をつけるべし。
KEN's .NET IL入門 http://www5b.biglobe.ne.jp/~yone-ken/VBNET/IL/il01_FirstStep.html
ラッパークラスの生成 - 匣の向こう側 - あまりに.NETな http://d.hatena.ne.jp/akiramei/20040810/1345306640
まずはソースから。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Reflection; | |
using System.Reflection.Emit; | |
using System.Linq; | |
using System.Threading; | |
namespace HelloWorld1 | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
if (args.Length != 1) | |
{ | |
System.Console.Error.WriteLine("usage: HelloWorld1 filename.exe"); | |
System.Environment.Exit(1); | |
} | |
string exeName = args[0]; | |
// AssemblyBuilder#save(path)がディレクトリまたぎを受け付けないので拒否 | |
if (System.IO.Path.GetDirectoryName(exeName).Length > 0) | |
{ | |
System.Console.Error.WriteLine("Specifing path is not supported."); | |
System.Environment.Exit(1); | |
} | |
// アプリケーションドメインの取得とアセンブリ・モジュールの作成 | |
AppDomain appDomain = Thread.GetDomain(); | |
AssemblyName assemblyName = new AssemblyName() { Name = exeName }; | |
AssemblyBuilder assemblyBuilder = appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave); | |
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("HelloWorld", exeName, true); | |
// クラスの作成 | |
TypeBuilder helloClassBuilder = moduleBuilder.DefineType("Hello", TypeAttributes.Class); | |
ConstructorBuilder ctorBuilder = helloClassBuilder.DefineDefaultConstructor(MethodAttributes.Public); | |
// インスタンスメソッドvoid Say(string value)の作成 | |
MethodBuilder sayMethodBuilder = helloClassBuilder.DefineMethod("Say", | |
MethodAttributes.Public, | |
typeof(void), | |
new Type[] { typeof(string) }); | |
ILGenerator ilSay = sayMethodBuilder.GetILGenerator(); | |
// System.Console.WriteLine()にSayの第一引数を渡してコール | |
ilSay.Emit(OpCodes.Ldarg_1); | |
ilSay.EmitCall(OpCodes.Call, | |
typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }), | |
null); | |
// Sayを抜ける | |
ilSay.Emit(OpCodes.Ret); | |
// エントリポイントとなる静的メソッドの作成 | |
MethodInfo thisMainMethod = System.Reflection.Assembly.GetExecutingAssembly().EntryPoint; | |
MethodBuilder mainMethodBuilder = helloClassBuilder.DefineMethod( | |
thisMainMethod.Name, | |
thisMainMethod.Attributes, | |
thisMainMethod.ReturnType, | |
(from p in thisMainMethod.GetParameters() select p.ParameterType).ToArray<Type>()); | |
ILGenerator ilMain = mainMethodBuilder.GetILGenerator(); | |
// ローカル変数を宣言してメッセージを格納 | |
LocalBuilder msgLocal = ilMain.DeclareLocal(typeof(string)); | |
ilMain.Emit(OpCodes.Ldstr, "Hello, World"); | |
ilMain.Emit(OpCodes.Stloc, msgLocal); | |
// ローカル変数を宣言してHelloオブジェクトを生成・格納 | |
LocalBuilder helloLocal = ilMain.DeclareLocal(helloClassBuilder); | |
ilMain.Emit(OpCodes.Newobj, ctorBuilder); | |
ilMain.Emit(OpCodes.Stloc, helloLocal); | |
// 作成したオブジェクトに対して、Sayメソッドを実行 | |
ilMain.Emit(OpCodes.Ldloc, helloLocal); | |
ilMain.Emit(OpCodes.Ldloc, msgLocal); | |
ilMain.EmitCall(OpCodes.Call, sayMethodBuilder, null); | |
// メイン終了 | |
ilMain.Emit(OpCodes.Ret); | |
// Helloクラスの作成 | |
helloClassBuilder.CreateType(); | |
// エントリポイントの設定 | |
assemblyBuilder.SetEntryPoint(mainMethodBuilder); | |
// 保存 | |
assemblyBuilder.Save(exeName); | |
} | |
} | |
} |
いつもの数行のコードだと思った?Hello, Worldプログラム自体を生成するプログラムでした。
プログラムの使い方はこう。第一引数にexeのファイル名を指定すると、Hello, World!を出力するexeファイルを作成する。
c:\temp> HelloWorld1.exe Hello.exe c:\temp> Hello.exe Hello, World!
スタックのイメージ
説明の前に、この絵がとても大事なので覚えてほしい。このイメージがあれば世のMSIL説明記事と戦える。.NETの世界では、ローカル変数、スタック、引数という3つの枠があり、それぞれ必要に応じて移動して使う。引数の枠から、値をスタックに移動してきて、なんらかの操作を実行、必要に応じてローカル変数に退避する、という感じかな。
ソースの説明
さて、実際にHello, World生成クラスを作ってみる。static void Main()でWriteLine()するだけで本当は十分なのだが、説明のためにインスタンスを生成してSay()というメソッドを呼び出すようになっている。なお、ソースの頭のところは、お決まりの引数処理だったり.NETの仕組みみたいな話なので省略して進める。
1. クラスの作成
TypeBuilder helloClassBuilder = moduleBuilder.DefineType("Hello", TypeAttributes.Class); ConstructorBuilder ctorBuilder = helloClassBuilder.DefineDefaultConstructor(MethodAttributes.Public);ModuleBuilderのDefineType()というメソッドを使い、クラスを作る。
あとでnewしたいので、デフォルトコンストラクタのConstructorBuilderをとっておく。
2. インスタンスメソッドSay()の作成
文字列をコンソールに出力するSay()を作成する。生成後をC#で書くとこんな感じ。public void Say(string value) { System.Console.WriteLine(value); }
生成するコードは以下。
MethodBuilder sayMethodBuilder = helloClassBuilder.DefineMethod("Say", MethodAttributes.Public, typeof(void), new Type[] { typeof(string) });TypeBuilderのDefineMethod()を使って、メソッドを作る。"Say"という名前で、public、戻り値はvoid、ひとつの引数stringを取るという感じ。
ILGenerator ilSay = sayMethodBuilder.GetILGenerator();MethodBuilderのGetILGenerator()というメソッドを呼び出すと、ILを生成していくためのILGeneratorオブジェクトを得られる。アセンブラコードを書いていくようなイメージ。このあと、Emit()やEmitなんとか()を利用して書き込んでいく。
// System.Console.WriteLine()にSayの第一引数を渡してコール ilSay.Emit(OpCodes.Ldarg_1); ilSay.EmitCall(OpCodes.Call, typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }), null); // Sayを抜ける ilSay.Emit(OpCodes.Ret);
まずldarg_1を使って、関数の引数の1番目を評価スタックにロードする。なお、0はインスタンス。C#はthisはメソッド定義に書いてないけど、最初の引数がselfだったりthisだったりする言語あるよね。そんな感じ。
そして、callを使って、System.Console.WriteLineを呼び出す。
OpCodes.Callには、MethodInfoを渡してあげる必要があるが、これはリフレクションで取ってきてしまう。
上記の例では、stringという引数を持つWriteLineというメソッドを、System.Consoleのタイプから拾ってくる形である。
なぜCallのときはEmitではなく、EmitCallを使っているかというと、可変長引数を持つメソッド用の型をEmit()が受け取ることができないから。ここでは使ってないので、実はEmitでよい。
最後にreturnたるretを書いて、Sayの実装は終わり。
3. Mainの作成
エントリポイントとなるMain()を実装する。生成後のものをC#で書くとこんな感じ。private static void Main(string[] array) { string text = "Hello, World"; Hello hello = new Hello(); hello.Say(text); }
生成するコードは以下。
MethodInfo thisMainMethod = System.Reflection.Assembly.GetExecutingAssembly().EntryPoint; MethodBuilder mainMethodBuilder = helloClassBuilder.DefineMethod( thisMainMethod.Name, thisMainMethod.Attributes, thisMainMethod.ReturnType, (from p in thisMainMethod.GetParameters() select p.ParameterType).ToArray<Type>()); ILGenerator ilMain = mainMethodBuilder.GetILGenerator();Mainメソッドは、この生成プログラム自体の定義からもらってきた。実際にILGeneratorを使ってILを作っていくような場面では、生成側のオブジェクトの定義をリフレクションでもらってくるケースが多いと思う。
3.1. ローカル変数の作成
// ローカル変数を宣言してメッセージを格納 LocalBuilder msgLocal = ilMain.DeclareLocal(typeof(string)); ilMain.Emit(OpCodes.Ldstr, "Hello, World"); ilMain.Emit(OpCodes.Stloc, msgLocal);試しにローカル変数を作ってみた例。DeclareLocal()を呼ぶと、ローカル変数が宣言できるので、評価スタックに値をロードしてから、stlocを使って、そこに保存する。
3.2. インスタンスの生成
// ローカル変数を宣言してHelloオブジェクトを生成・格納 LocalBuilder helloLocal = ilMain.DeclareLocal(helloClassBuilder); ilMain.Emit(OpCodes.Newobj, ctorBuilder); ilMain.Emit(OpCodes.Stloc, helloLocal);インスタンスを作成するときは、newobjにコンストラクタを渡す。ここに限った話じゃないけど、Emit達は、~Builderを受けてくれる。すごくよくできていると思う。
3.3. Sayメソッドを実行
// 作成したオブジェクトに対して、Sayメソッドを実行 ilMain.Emit(OpCodes.Ldloc, helloLocal); ilMain.Emit(OpCodes.Ldloc, msgLocal); ilMain.EmitCall(OpCodes.Call, sayMethodBuilder, null);インスタンス、引数の順にldlocを使ってスタックに詰めて、callでSayメソッドを呼び出す。
なお、お気づきかもしれないが、「3.2. インスタンスの生成」で、オブジェクトはスタックに生成されているので、いちいちローカル変数に入れなくてもいい。文字列も固定なので同様。なので、3.1.~は、以下のように書くこともできる。
ilMain.Emit(OpCodes.Newobj, ctorBuilder); ilMain.Emit(OpCodes.Ldstr, "Hello, World"); ilMain.EmitCall(OpCodes.Call, sayMethodBuilder, null);
ilMain.Emit(OpCodes.Ret);retを呼んで終了。
あとは、エントリポイントを指定して、アセンブリをファイルにSaveするだけ。
ここまで読んだあなたは、C#を使って、プログラムを生成することができるようになっているはず。
フィールドとか、Genericとか、よく使いそうなものを全然解説していないけど、そこは「詳しくはWebで」という感じで。
Javaでも同じようなことがきっとできると思う。Cはどうだろう?LLVMとかの力を借りればいいのかな。
IL生成の罠
間違えると実行時のエラーがわかりにくいので、大変である。以下は例。
ret忘れちゃった
// Sayを抜ける // ilSay.Emit(OpCodes.Ret);
ハンドルされていない例外: System.InvalidProgramException: 共通言語ランタイムが無効なプログラムを検出しました。
場所 Hello.Say(String )
場所 Hello.Main(String[] )
スタックから取るの忘れた
// ローカル変数を宣言してメッセージを格納 LocalBuilder msgLocal = ilMain.DeclareLocal(typeof(string)); ilMain.Emit(OpCodes.Ldstr, "Hello, World"); // ilMain.Emit(OpCodes.Stloc, msgLocal);
ハンドルされていない例外: System.InvalidProgramException: JIT コンパイラで内部的な制限が発生しました。
場所 Hello.Main(String[] )
気をつけるべし。
参考
動的処理 « C#たんっ! http://csharptan.wordpress.com/2011/12/16/%E5%8B%95%E7%9A%84%E5%87%A6%E7%90%86/KEN's .NET IL入門 http://www5b.biglobe.ne.jp/~yone-ken/VBNET/IL/il01_FirstStep.html
ラッパークラスの生成 - 匣の向こう側 - あまりに.NETな http://d.hatena.ne.jp/akiramei/20040810/1345306640
余談
実際のところ、「dynamic」もあるし、System.Reflection.EmitでILを書くことになるケースはほとんどないと思う。ダイナミックに関数を生成したいだけなら、Expression Treeという便利なものもあるし。ただ、type-safeなものをなんとかして作りたいなんてレアなときに有用かもしれない。2012-08-15
ImageMagickで本文ボールド化(改良版)
ImageMagickで「本文ボールド化」っぽいことをするで一応ボールド化できるようになったのだけど、threshold で、重ねる画像(レイヤー)を2値化してしまっているせいで、白黒でスキャンしたときのようなでこぼこが目立つようになってしまった。そこで改良してみた。
改良版はこちら。
改良点は、-thresholdによる2値化をやめ、画像の重ね方を「Multiply」(いわゆるレイヤーの「乗算」)にした。あと、アルファチャネルの作成をやめた。
以前のコマンドは、-thresholdを使わずに+compositeで重ねると、色が濃くなるどころか、かえって薄くなるケースがあったため(重ねた部分が優先されるため)に、黒いところを黒くするために使っていた。しかし、乗算にすれば、どちらかが黒ければ黒くなるので、そうすることにした。(ざっくりたとえば0.2 × 0.2 = 0.04なので、黒くなるという感じ。)
参考:ImageMagick v6 Examples -- Compositing Images
アルファチャンネルの作成も、重ね合わせの処理前にレベル補正をきちんとやっておけば問題なかったのでなくした。逆に、レベル補正しない状態では、アルファチャンネル使うverだと、効果が若干弱くなるものの、ハイライト部分が黒っぽくなることが避けられる感じなので、レベル補正がちゃんとできないようなケースでは、前回と同じく、アルファチャネルにレベルをコピーして乗算したほうがいいかもしれない。
細かいところではほかにも-geometryを-rollにしているが、これは-rollが絵を動かすためのオプションだったので、名前を変えただけ(別に-geometryでも動く)。
この本文ボールド化の効果は、コマンドではわかりづらいので、絵も乗せておく。
(下の絵は、コマンドで処理したのち、画像エディタで切り出し、400%に拡大したもの。)
薄い感じ。
結構よくなる。これでもいいかも。ガンマ補正がかなり有効。
さらに文字を太く。
改良版はこちら。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
convert input.jpg -modulate 100,0 -level 10%,90% '(' +clone -roll +0+1 '(' +clone -roll +1-1 ')' +composite -compose Multiply ')' +composite -compose Multiply -gamma 0.5 -geometry 600x750 output.jpg |
改良点は、-thresholdによる2値化をやめ、画像の重ね方を「Multiply」(いわゆるレイヤーの「乗算」)にした。あと、アルファチャネルの作成をやめた。
以前のコマンドは、-thresholdを使わずに+compositeで重ねると、色が濃くなるどころか、かえって薄くなるケースがあったため(重ねた部分が優先されるため)に、黒いところを黒くするために使っていた。しかし、乗算にすれば、どちらかが黒ければ黒くなるので、そうすることにした。(ざっくりたとえば0.2 × 0.2 = 0.04なので、黒くなるという感じ。)
参考:ImageMagick v6 Examples -- Compositing Images
アルファチャンネルの作成も、重ね合わせの処理前にレベル補正をきちんとやっておけば問題なかったのでなくした。逆に、レベル補正しない状態では、アルファチャンネル使うverだと、効果が若干弱くなるものの、ハイライト部分が黒っぽくなることが避けられる感じなので、レベル補正がちゃんとできないようなケースでは、前回と同じく、アルファチャネルにレベルをコピーして乗算したほうがいいかもしれない。
細かいところではほかにも-geometryを-rollにしているが、これは-rollが絵を動かすためのオプションだったので、名前を変えただけ(別に-geometryでも動く)。
この本文ボールド化の効果は、コマンドではわかりづらいので、絵も乗せておく。
(下の絵は、コマンドで処理したのち、画像エディタで切り出し、400%に拡大したもの。)
リサイズ/グレースケール化のみ
convert input.jpg -modulate 100,0 -geometry 650x700 output.jpg
薄い感じ。
+レベル補正/ガンマ補正
convert input.jpg -modulate 100,0 -level 10%,90% -gamma 0.5 -geometry 650x700 output.jpg
結構よくなる。これでもいいかも。ガンマ補正がかなり有効。
+本文ボールド化
convert input.jpg -modulate 100,0 # 色を飛ばす -level 10%,90% # レベル補正 '(' +clone # 画像をコピー -roll +0+1 # 縦に1pixelずらす '(' +clone # 画像をコピー -roll +1-1 # 縦に1pixel戻して、横に1pixelずらす ')' +composite -compose Multiply # 縦にずらしたものと、横にずらしたものを乗算で重ねる ')' +composite -compose Multiply # 元の画像と、ずらしたものを乗算で重ねる -gamma 0.5 # ガンマ補正 -geometry 600x750 # サイズ変更 output.jpg
さらに文字を太く。
2012-08-14
横倒しスキャンはどれだけ速いのか?
本をスキャンする際、横に倒して読んで後で回転させたほうが速くスキャンできると聞いて、どのくらい速くなるか試してみた。単純に考えると、紙を送る量が少ないので、速そうである。
結論としては、試した範囲では、縦でのスキャンに比べて、1.3倍程度の高速化であった。
(*)実はいくつか2回読んだ。でも、ほとんど変わらなかったので、1回としている。まぁ、1回でも2回でも精度はよくないと思うので、真面目な結果を知りたい方はぜひもっといろいろなパターン/回数でチャレンジを。
サイズは、裁断後のサイズ(実測)である。
また、最初は縦横比そのものになるのかなと思っていたが、縦横比とは若干ずれた比になった。動きを見ていると、紙を送り終わった後に若干の待ちがあるので、縦横比に関わらず1枚あたりに固定的にかかる時間があるのだと思う。
実際、「コミック」の場合で、縦横比に関わらない時間があると想定して計算してみると...
残りの「新書」「コミック(大)」で試しに計算してみると、
(もっといい考察があれば教えてくださいまし。)
なお、横版は、紙送りが終わった後に、ファイル(PDF)に出すのに時間がかかっていたということも付け加えておく。
結論としては、試した範囲では、縦でのスキャンに比べて、1.3倍程度の高速化であった。
計測方法
ScanSnap S1500のスーパーファイン/カラーのモードで、以下の3種類の紙を、25枚ずつ、縦(そのままの形)と横(倒した形)で1回(*)ずつ、スキャンボタンを押してから紙送りが終わるまでの時間を、ストップウォッチで計る。(*)実はいくつか2回読んだ。でも、ほとんど変わらなかったので、1回としている。まぁ、1回でも2回でも精度はよくないと思うので、真面目な結果を知りたい方はぜひもっといろいろなパターン/回数でチャレンジを。
種類 | 幅 | 高さ | 幅・高さ比 |
---|---|---|---|
新書 | 10.8cm | 17.3cm | 1.60 |
コミック(普通) | 12.3cm | 18.2cm | 1.47 |
コミック(大) | 14.3cm | 21.0cm | 1.46 |
サイズは、裁断後のサイズ(実測)である。
計測結果
スキャン所要時間の結果は、以下となった。種類 | 縦 | 横 | 縦(1枚あたり) | 横(1枚あたり) | 縦時間・横時間比 |
---|---|---|---|---|---|
新書 | 48.5s | 34.9s | 1.94s/枚 | 1.40s/枚 | 1.39 |
コミック(普通) | 50.8s | 38.0s | 2.03s/枚 | 1.52s/枚 | 1.34 |
コミック(大) | 56.4s | 41.8s | 2.26s/枚 | 1.67s/枚 | 1.34 |
感想
200ページ(100枚)のコミックだとすると、縦の場合と横の場合で以下のようになる。縦:2.03 秒 × 100 枚 = 203 秒 = 3分23秒 横:1.52 秒 × 100 枚 = 152 秒 = 2分32秒後処理の問題や、紙挿入の取り違えなんかもあるので、この時間差をどう見るかは、人次第だと思う、という感じの結果であった。
また、最初は縦横比そのものになるのかなと思っていたが、縦横比とは若干ずれた比になった。動きを見ていると、紙を送り終わった後に若干の待ちがあるので、縦横比に関わらず1枚あたりに固定的にかかる時間があるのだと思う。
実際、「コミック」の場合で、縦横比に関わらない時間があると想定して計算してみると...
a: 1cmあたりのスキャン時間 b: 1枚あたりで固定にかかる時間 2.03 = 18.2a + b 1.52 = 12.3a + b を解いて 0.51 = 5.9a よって a = 0.086 2.03 = 18.2 × 0.086 + b より b = 0.461cmあたり0.086s、1枚あたり0.46s。
残りの「新書」「コミック(大)」で試しに計算してみると、
新書(横):10.8×0.086 + 0.46 = 1.43s/枚 (実測: 1.40s/枚) 新書(縦):17.3×0.086 + 0.46 = 1.94s/枚 (実測: 1.94s/枚) コミック大(横):14.3×0.086 + 0.46 = 1.69s/枚 (実測: 1.67s/枚) コミック大(縦):21.0×0.086 + 0.46 = 2.27s/枚 (実測: 2.26s/枚)とまぁ実測と近い感じなので、きっとそういう感じなのでしょう。
(もっといい考察があれば教えてくださいまし。)
なお、横版は、紙送りが終わった後に、ファイル(PDF)に出すのに時間がかかっていたということも付け加えておく。
登録:
投稿 (Atom)