お手軽な自炊のために

マイコンクラブ2011年度入学のHRHTです。
昨年11月にiPad miniを購入し、主に移動中のPDFリーダーとして利用しています。
単にPDFを見るだけならばすでに持っていたiPod touchでもそこまで不便ではなかったのですが、大きな画面では見やすさが違います。
さて、タイトルにある「自炊」とは、手持ちにある本をスキャンなどによりデジタルデータに変換することをいいます。
自炊方法には大きく分けて破壊的方法と非破壊的方法があります。
どちらの方法をとるにしろ、ある程度しっかりとした体裁で自炊するには小さくはない機器が必要になります。
スキャンの用途だけにそれらの機器を買うよりは他の面白そうなガジェットを買うほうが個人的には有意義だと思っているので、この自由研究では最低限読める程度の自炊を手軽に行う方法を考えていきます。
結論を言えば見開き1ページずつ写真を撮っていくだけです。
ですが、写真を撮っただけだと周囲に予定な部分が残っていたり、本の部分が曲がっていたりして無駄な部分が多くなってしまいます。
以下ではその無駄な要素を排除するようなプログラムについて考えていきます。

このプログラムがやるべきことは、「画像データから本の領域を切り出し、長方形にする」ことです。
内部の流れを考えると、

  1. 画像を取り込む
  2. 本の領域を(自動または手動で)指定する
  3. 切り出した後の大きさを(計算または明示的に与えることにより)指定する
  4. これらの情報から切り出し後の画像の適当な場所での画素を計算する
  5. 切り出し後の画像を保存する

となります。
1および5は適当に検索すれば出てくるので解説は必要ないと思います。
また、2の自動化に関しては解説が面倒なのと手動での調整が必要な場合が多いため解説しません。(「エッジ検出」等で検索すればそれっぽい情報がでてきます)
以下では、領域の縦の部分は直線と仮定して上と下の曲線を指定し、3と4の処理を行うことを考えます。
ここから自分の書いたC#のソースコードを載せながら説明していきますが適当に書いたものなのでこれよりも良い方法があると思われます。
まず、プログラムで使用するデータを規定していきます。
[code lang=”csharp”]
public struct Position
{
public double X;
public double Y;
public double Length;
public Position(double x, double y)
{
this.X = x;
this.Y = y;
this.Length = Math.Sqrt(X * X + Y * Y);
}
public static Position Lerp(Position t1, Position t2, double t)
{
return new Position(t1.X * (1 – t) + t2.X * t, t1.Y * (1 – t) + t2.Y * t);
}
public static double GetLength(Position t1, Position t2)
{
return Math.Sqrt((t1.X – t2.X) * (t1.X – t2.X) + (t1.Y – t2.Y) * (t1.Y – t2.Y));
}
}
public interface ICurve
{
Position Start { get; }
Position End { get; }
Position GetPosition(double t);
double Length { get; }
}
public class Line : ICurve
{
Position start;
public Position Start
{
get { return start; }
}
Position end;
public Position End
{
get { return end; }
}
double length;
public double Length
{
get { return length; }
}
public Line(Position start,Position end)
{
this.start = start;
this.end = end;
this.length = Position.GetLength(start , end);
}
public Position GetPosition(double t)
{
return Position.Lerp(Start, End, t);
}
public static Position GetCrossPosition(Line l1, Line l2)
{
double k = (l2.End.Y – l2.Start.Y) * (l2.End.X – l1.Start.X) – (l2.End.X – l2.Start.X) * (l2.End.Y – l1.Start.Y);
double d = (l2.End.Y – l2.Start.Y) * (l1.End.X – l1.Start.X) – (l2.End.X – l2.Start.X) * (l1.End.Y – l1.Start.Y);
if (Math.Abs(d) < 0.000001)
{
return new Position(double.PositiveInfinity, double.PositiveInfinity);
}
else
{
return l1.GetPosition(k / d);
}
}
}
public struct Color
{
public byte R;
public byte G;
public byte B;
}
public class ImageData
{
int width;
public int Width
{
get { return width; }
}
int height;
public int Height
{
get { return height; }
}
Color[] data;
public ImageData(int width, int height)
{
this.width = width;
this.height = height;
data = new Color[Width * Height];
}
public Color this[int x, int y]
{
get
{
x = (x < 0) ? 0 : ((x >= Width) ? Width – 1 : x);
y = (y < 0) ? 0 : ((y >= Height) ? Height – 1 : y);
return data[x + y * Width];
}
set
{
x = (x < 0) ? 0 : ((x >= Width) ? Width – 1 : x);
y = (y < 0) ? 0 : ((y >= Height) ? Height – 1 : y);
data[x + y * Width] = value;
}
}
}
[/code]
Position構造体は2次元座標に用います。
ICurveインターフェイスは0<=t<=1をパラメータとした曲線の情報を持ったもので、GetPositionが0<=t<=1における曲線上の位置を表します。 Lineクラスは始点と終点を指定した直線を表し、GetCrossPositionによって2直線の交点を得ることができます。 Color構造体は画素の色情報を表し、それの1次元配列を扱いやすくしたものがImageDataクラスです。 C#ならある程度は.Netのクラスで代用できます。 3の「切り出した後の大きさを計算で得る」コードは以下のようになります。 [code lang="csharp"] public static Position GetTransformedSize(ImageData source, ICurve top, ICurve bottom, out Func<double,double> param) { double l = source.Height / 2 / Math.Tan(CameraAngle); double c = Line.GetCrossPosition(new Line(top.Start, bottom.Start), new Line(top.End, bottom.End)); Position k = new Position(c.X - source.Width / 2.0, c.Y - source.Height / 2.0).Length; double theta = Math.Atan2(k, l); double w = Math.Min(top.Length, bottom.Length); double h = (Position.GetLength(top.GetPosition(0.5), bottom.GetPosition(0.5)) / Math.Sin(theta)); double a = Math.Cos(theta); param = (p) => { return a * p * p / 2 + (1 - a / 2) * p; }; return new Position(w, h); } [/code] まずは次の画像を見てください。 簡易模式図
これは写真を撮る際の状況を簡易的に表したものです。
横の赤の線が実際の本の位置です。写真に撮ったとき、上か下の曲線の長さの短い側を基準とすると斜めの赤い線が写真上における本の位置になります。
まず、空行までのところで本に対するカメラの角度を計算しています。
これにはカメラ、画像の中央(茶色の部分)、カメラの水平面と画像の延長の交点(水色の部分、点c)の三角比を使って求めます。
この点cは画像上では消失点にあたるので、平行な直線の画像上の交点から大体の位置が分かります。
CameraAngleはあらかじめ定数で与えておきます。(だいたいπ/8くらいが良いと思います)
横の長さwは短い方を取ります。横の長さは写真上での本の真ん中あたりの長さと先ほど求めたθを用いて計算します。
CameraAngleも使えばより正しい値が得られますがあまり差がないように思います。
また、カメラに対して遠くにある部分が少し小さくなっているのでparamで補正します。
最後に、これで得たor与えた大きさなどから切り出し後の画像を得るコードは次のようになります。
[code lang=”csharp”]
public ImageData Transform(ImageData source, ICurve top, ICurve bottom, int width, int height, Func<double,double> param)
{
ImageData re = new ImageData(width, height);
for (int i = 0; i < width; i++)
{
Position t = top.GetPosition((double)i / width);
Position b = bottom.GetPosition((double)i / width);
for (int j = 0; j < height; j++)
{
Position p = Position.Lerp(t, b, param((double)j / height));
re[i, j] = data[(int)p.X, (int)p.Y];
}
}
return re;
}
[/code]
切り出し後の座標(X,Y) (0<=X,Y<=1となるように補正)の元画像における場所は上の曲線の位置Xに値する位置を得て、そこから下の曲線まで、paramによって補正したY方向の分だけ移動した場所にしています。 これだと、曲線のパラメータの与え方が適当だとうまくいかないのでうまくいくような曲線を指定してください。 元画像とこのコードに(少し手を加えたものに)よって切り出したものは次のようになります。 元画像
切り出し
元画像で曲がっているところや上のところは少し見にくいですが一応読める程度にはなってます。
実際に自炊を行う場合はほぼ真上から撮ると思うので曲がりにさえ気を付ければ十分読めるものになると思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です