白旗製作所

スキャン画像のモアレ除去のための、ぼかしとアンシャープマスク処理

CDのジャケット等の印刷物をスキャナでスキャンすると、
以下の画像の様にモアレが発生します。
(本画像はCQ出版「定本 トランジスタ回路の設計」表紙画像のスキャン画像の一部です。
ダウンロードして拡大するとモアレがよりよく見えます。)
moire50.png


これは、印刷する際に、あるdpiで印刷していて、
それを異なるdpiで読み取るために、ズレが周期的に発生して
干渉縞ができるためです。

ふつう、スキャナにはこれを除去するモアレ除去という
機能が付いているのですが、安物の機種にはついていなかったり、
ついていてもONOFFのみなど、調整ができなかったりします。
これを調整できるようにしようかと思い、javaでプログラムを作成しました。


AdobeのHPを見ると、『画像に「ぼかし(ガウス)」を加えてから
「アンシャープマスク」で引き締める 2 つのフィルタを適用する
方法が効果的です。』とあります。
スキャン画像のモアレの解消方法
https://helpx.adobe.com/jp/x-productkb/multi/216043.html


おそらく普通のスキャナにも、こういった処理がついていて、
パラメータが固定値になっているのではないでしょうか。

というわけで、Javaで「ぼかし」と「アンシャープマスク」の
2つを実装してみます。


こういった処理にはJava 2D APIを使用するのが良いようです。
コンボリューションを用いた画像の平滑化、鮮鋭化とエッジ検出
http://codezine.jp/article/detail/129


ここでいう「平滑化」が「ぼかし」で「鮮鋭化」が
「アンシャープマスク」ですね。オペレータを作って
Java 2D APIで畳み込みをすればこれらの処理が実現できます。

この例では3×3のオペレータの例しか載っていませんが、
実際は記述されている通り7×7や15×15くらいのサイズが必要ですので、
オペレータを作成する処理が別途必要です。

ここでは、この記事に書いてあるように、ガウス分布(正規分布)を
使って、範囲を分散σ^2で調整できるようなオペレータを作成します。
また、オペレータのサイズも自由に変更できるようにします。

サイズとσ値は調整が必要です。とりあえず、サイズ15、σ2で
ぼかしとアンシャープマスクをかけると、記事冒頭の画像が以下のようになります。
moireremoved50.png

ややボケた感じが残っていますが、最初に目立っていた周期的な
モアレノイズはかなり低減できていると思います。
あとはパラメータ調整と、別のシャープ化処理とかを実装すれば、
そこそこ実用的になるのではと思います。


以下、今回作成したサンプルコードです。
一部に、私のオリジナルのライブラリ関数が使われてるので注意してください。


/**
* 正規分布をつかってオペレータを作る
*/
public static Matrix gausianOperator( int size, double sigma )
{
//サイズが奇数でなければエラー
if( size%2 == 0 ) return null;

//オペレータをつくる
Matrix operator = new Matrix(size, size);

double distance;
int center = size/2;
//各マスに正規分布の値を入れる
for(int r=0; r<size; r++){
for(int c=0; c<size; c++){
distance = Math.hypot(r-center, c-center);//中央からの距離を計算
operator.set(r, c, Numeric.normalDistribution(distance, 0, sigma)); //正規分布の値をセット
}
}

//全部の値の合計が1になるように調整
operator = operator.multiply(1/operator.sum());

return operator;
}
/**
* BufferedImageの画像を畳み込みする
*/
public static BufferedImage convolution( BufferedImage image, Matrix operator,int type)
{
//オペレータをfloat[]に変換する準備をする
Vector temp = new Vector(0);
int size = operator.length();
for(int i=0; i<size; i++) temp = temp.add(operator.getRow(i));

//畳み込む
Kernel kernel=new Kernel(size,size,Cast.doubleToFloat(temp.get()));
ConvolveOp convolveOp;
if(type==1)
convolveOp=new ConvolveOp(kernel,ConvolveOp.EDGE_ZERO_FILL,null);
else
convolveOp=new ConvolveOp(kernel,ConvolveOp.EDGE_NO_OP,null);
BufferedImage output=convolveOp.filter(image,null);
return output;
}

/**
* 画像を平滑化する<br>
*/
public static BufferedImage smooth( BufferedImage image, int size, double sigma )
{
//オペレータを作成する
Matrix operator = gausianOperator( size, sigma );

//オペレータを使って畳み込みする
return convolution(image, operator, 0);
}

/**
* 画像を鮮鋭化する<br>
*/
public static BufferedImage sharpen( BufferedImage image, int size, double sigma )
{
//オペレータを作成する
Matrix operator = gausianOperator( size, sigma );

//オペレータをアンシャープマスクにする
operator = operator.multiply(-1);
operator.set(size/2, size/2, (2-operator.get(size/2, size/2)*(-1)) );

//オペレータを使って畳み込みする
return convolution(image, operator, 0);
}

public static void main(String[] args)
{
BufferedImage target = ImageUtility.read("test.csv");
BufferedImage smoothed = ImageProcessing.smooth(target, 15, 2);
BufferedImage shaped = ImageProcessing.sharpen(smoothed,15, 2);

ImageUtility.save(shaped , "test2.csv");
}


スポンサーサイト
  1. 2015/08/23(日) 23:07:03|
  2. Java
  3. | トラックバック:0
  4. | コメント:0

Javaでマルチスレッド

JavaでGUIを触っていて、重めの処理をしたときにGUIが固まってしまい、
処理がおわるまで反応しなくなったことがありました。

これを避けるには、処理を別スレッドにすれば良いということで、
マルチスレッド化をしましたので、それについて少しメモしておきます。

現在のスレッドと別のスレッドを用意して処理させるには、基本的に以下を書けばよいです。

【GUIのクラス内の処理】
  Test test = new Test();
  Thread thread = new Thread(test);
  thread.start();

【重い処理を記述するクラス】
public Test implements Runnable{
  ...
  public void run(){
  hogehoge(); //何か重い処理
  }
  
  private void hogehoge(){
    ...
  }
}

何をやっているのかというと、GUIクラスでは
Thread thread = new Thread(test)
の箇所で、Testクラスのインスタンスtestを使って、threadという新たなスレッドを作り、
thread.start();
で、test.run()をスレッドthread内で動かしています。

Testクラスでは、run()の中に、別スレッドで行わせたい処理hogehoge()を書いておき、
Runnableというインターフェースをimplements しておきます。


こうすることで、重い処理であるhogehoge()を別スレッドで呼び出すことができるため、
GUIが固まることを回避できます。


上記ではGUIクラスから別スレッドを呼び出しましたが、
当然ながら、GUIクラスを別スレッドとして生成してもよいでしょう。


また、わざわざ別クラスを用意するほどでもなかったりする場合、
以下のようにRunnable()クラスのインスタンスを作って、直接処理を記述することも可能です。

Thread thread = new Thread(new Runnable(){
  public void run(){ hogehoge(); }
});
thread.start();



  1. 2015/03/08(日) 22:18:48|
  2. Java
  3. | トラックバック:0
  4. | コメント:0

JavaでJPEGのEXIF情報を読み取り

JPEG写真のEXIF情報をまとめて持ってくるため、
Javaで取得をしてみました。

EXIF情報取得には、METADATA EXTRACTORという
便利なライブラリがあるのでこれを使わせてもらいます。

EXIF情報は、情報によって格納されているディレクトリが異なるらしいので、
まずJPEGのメタデータを読み込んだあと、必要なディレクトリを読み込んで、情報を取り出します。
EXIF情報には何があるか、情報のタグ名、タグIDがどのようになっているかは、このサンプルが参考になります。

METADATA EXTRACTORを使ってJavaでEXIF情報読み込む例はこことかここにあるのですが、
情報が古く、最新のライブラリの仕様は変わっていてコードがそのままでは使えないため、
ライブラリの仕様に合わせてプログラムを変更します。
またいつ仕様も変更になるかわからないので、githubのGettingStartedを参考にするとよいでしょう。

試しに、以下のデータを読みだした例を置いておきます。
・カメラメーカー
・カメラモデル
・絞り値
・露出時間
・ISO感度
・露出
・焦点距離
・撮影日時

------------------------------------------
import java.io.File;
import java.util.Date;

import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;

public class Main {
public static void main(String[] args) {

// メタデータを読み込み
File jpegFile = new File(fileName);
Metadata metadata = ImageMetadataReader.readMetadata(jpegFile);

// EXIFのディレクトリ(情報種類)を読み込み
ExifIFD0Directory ifdDirectory = metadata.getDirectory(ExifIFD0Directory.class);
ExifSubIFDDirectory subIfdDirectory = metadata.getDirectory(ExifSubIFDDirectory.class);

// タグの値を読み込み
String maker = ifdDirectory.getString(ExifIFD0Directory.TAG_MAKE);
String cameraModel = ifdDirectory.getString(ExifIFD0Directory.TAG_MODEL);
String fNumber = subIfdDirectory.getString(ExifSubIFDDirectory.TAG_FNUMBER);
String exposureTime = subIfdDirectory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME);
String ISO = subIfdDirectory.getString(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT);
String exposureBias = subIfdDirectory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_BIAS);
String foculLength = subIfdDirectory.getString(ExifSubIFDDirectory.TAG_FOCAL_LENGTH);
Date date = subIfdDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);

System.out.println(maker);

}
}
  1. 2014/11/30(日) 09:16:26|
  2. Java
  3. | トラックバック:0
  4. | コメント:0

BufferedImageが遅い

Javaでの画像ファイルの読み書きにBufferedImageというクラスを用いているのですが
このクラスで画像のピクセルデータを読み出すのがとても遅いことに気づきました。

ちょっとピクセルごとに処理しようとすると、とてもじゃないけど待ってられないくらい遅くなります。
この問題は、こことかにあるように、BufferedImageの中身であるDataBufferクラスを取り出して
処理すれば早くなるはずです。また、データをint[]やbyte[]の配列にまで取り出してやっても早くなると思います。

そこで、画像のネガポジ反転処理(1ピクセルごとに、赤青緑データを255から引いた値にする)を例に、
以下の条件でどの程度早く出来るかを試してみました。
ただし、処理後の出力はBufferedImageクラスとします。

①1ピクセルごとにBufferedImage.getRGB(), setRGB()を実行して処理
②最初にBufferedImage.getRGBでデータをint[]型に変換してから処理
③BufferedImageをDataBufferByteに変換してから処理
④BufferedImageをbyte[]に変換してから処理 ※1
※3:ソースコードは最後に書いておきます。

1600×900のサイズのpngデータを用い、それぞれ100回ループした時の平均時間を計測しました。
結果、
①171.29ms
②158.88ms
③72.35ms
④85.04ms
となり、DataBuffereByteに変換して処理するのが最も高速となりました。

本来、byte[]に変換する④が高速になるはずなのですが、
再度BufferedImageに変換し直す処理がネックになっているようで、今回は多少遅くなってしまいました。
例えば出力がBufferedImageで無くても良い処理(画像マッチング等)などでは、
④の方が処理が早いのではないかと思います。※2

まあBufferedImageは読み書きに便利ですし、ARGBやABGRと言った並び順、alpha値の有無等の
画像ごとのデータ・タイプの差異を吸収してくれるため、遅くても仕方ない部分はあるかと思いますが、
ピクセル処理をする場合には少なくともDataBufferに変換してやったほうが、速く処理できるという結果となりました。
まあそれでもあまり速いとは言い難いですが、そこを求めるならそもそもjava使うなよということで…

※1
④のbyte配列だと処理速度は速いはずなのですが、データ範囲が0~255ではなく-128~127
(unsigned→signed)になってしまうため、少し工夫をしないと処理結果が他の関数と異なる場合があります。
まあめんどくさいので、多少処理速度が落ちてもint[]の配列にするのが楽で良いでしょうか。

※2
実際に同じ処理を、BufferedImageで返さないで10回ほどループさせた結果、
③1183ms
④1072ms
となり、byte配列にしたほうが処理速度は高速になりました。


※3
最後に時間計測に用いたソースを添付します。
ここここここを参考にさせていただきました。

①1ピクセルごとにBufferedImage.getRGB(), setRGB()を実行して処理

public static void negaposi_bufferedImageEach(BufferedImage image)
{
 int height = image.getHeight();
 int width = image.getWidth();
 int color = 0;
 int red, blue, green, alpha;
 //反転する
 for(int h=0; h<height; h++){
  for(int w=0; w<width; w++){
   color = image.getRGB(w, h);//色の読込
   //反転
   alpha = ImageUtility.alpha(color);
   red = 255-ImageUtility.red(color);
   blue = 255-ImageUtility.blue(color);
   green = 255-ImageUtility.green(color);
   image.setRGB(w, h, ImageUtility.argb(alpha, red, green, blue));
  }
 }
}


②最初にBufferedImage.getRGBでデータをint[]型に変換してから処理

public static void negaposi_bufferedImagePicelArray(BufferedImage image)
{
 int height = image.getHeight();
 int width = image.getWidth();
 int color = 0;
 int red, blue, green, alpha;
 //ピクセルデータをgetRGBを使ってint配列で取得
 int[] pixel = image.getRGB(0, 0, width, height, null, 0, width);
 //反転する
 for(int h=0; h<height; h++){
  for(int w=0; w<width; w++){
   color = pixel[h*width+w];//色の読込
   //反転
   alpha = ImageUtility.alpha(color);
   red = 255-ImageUtility.red(color);
   blue = 255-ImageUtility.blue(color);
   green = 255-ImageUtility.green(color);
   pixel[h*width+w] = ImageUtility.argb(alpha, red, green, blue);
  }
 }
 image.setRGB(0, 0, width, height, pixel, 0, width);
}


③BufferedImageをDataBufferByteに変換してから処理

public static void negaposi_dataBufferByte(BufferedImage image)
{
 int height = image.getHeight();
 int width = image.getWidth();
 int[] ints = new int[width * height];
 int red, blue, green, alpha, count=0;
 //ピクセルデータをDataBufferByteで取得
 DataBufferByte buf = ImageUtility.convertARGB(image);
 DataBuffer iBuf = new DataBufferInt(ints, width*height);
 //反転する
 for(int i=0; i<buf.getSize(); i+=4, count++){
  alpha = buf.getElem(i);
  red = 255-buf.getElem(i+1);
  green = 255-buf.getElem(i+2);
  blue = 255-buf.getElem(i+3);
  iBuf.setElem(count, ImageUtility.argb(alpha, red, green, blue));
 }
 //BufferedImageに戻す
 int[] bandMasks = new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff };
 WritableRaster writableRaster = Raster.createPackedRaster(iBuf, width, height, width, bandMasks, null);
 image.setData(writableRaster);
}

※以下のBufferedImageをDataBufferByteに変換するコードは省略してますが、
 実際はBufferedImageのtypeごとに別個の処理をすることになります。
public static DataBufferByte convertARGB(BufferedImage image)
{
 int alpha=1, count=0, color=0;
 DataBufferByte rBuf=null;
 DataBuffer iBuf = image.getRaster().getDataBuffer();
 switch(image.getType()){
 case BufferedImage.TYPE_INT_ARGB:
  rBuf = new DataBufferByte(iBuf.getSize()*4);
  for(int i=0; i<iBuf.getSize(); i++){
   color = iBuf.getElem(i);
   rBuf.setElem( i*4+0, color>>24&0xff );
   rBuf.setElem( i*4+1, color>>16&0xff );
   rBuf.setElem( i*4+2, color>>8&0xff );
   rBuf.setElem( i*4+3, color&0xff );
  }
  break;
 }
 return rBuf;
}

④BufferedImageをbyte[]に変換してから処理

public static void negaposi_Array(BufferedImage image)
{
 int height = image.getHeight();
 int width = image.getWidth();
 int red, blue, green, alpha, count=0;
 //ピクセルデータをByte配列で取得
 byte[] bufa = ImageUtility.convertByteARGB(image);
 int[] iBuf = new int[width*height];
 //反転する
 for(int i=0; i<bufa.length; i+=4, count++){
  alpha = bufa[i];
  red = 255-bufa[i+1];
  green = 255-bufa[i+2];
  blue = 255-bufa[i+3];
  iBuf[count] = ImageUtility.argb(alpha, red, green, blue);
 }
 //BufferedImageに戻す
 int[] bandMasks = new int[] { 0x00ff0000, 0x0000ff00, 0x000000ff };
 WritableRaster writableRaster = Raster.createPackedRaster(new DataBufferInt(iBuf, width*height), width, height, width,  bandMasks, null);
 image.setData(writableRaster);
}

※以下の処理も③同様省略してます。
public static byte[] convertByteARGB(BufferedImage image)
{
 int alpha=1, count=0, color=0;
 byte[] rBuf=null;
 DataBuffer iBuf = image.getRaster().getDataBuffer();
 switch(image.getType()){
 case BufferedImage.TYPE_INT_ARGB:
  rBuf = new byte[iBuf.getSize()*4];
  for(int i=0; i<iBuf.getSize(); i++){
   color = iBuf.getElem(i);
   rBuf[ i*4+0] = (byte)(color>>24&0xff );
   rBuf[ i*4+1] = (byte)(color>>16&0xff );
   rBuf[ i*4+2] = (byte)(color>>8&0xff );
   rBuf[ i*4+3] = (byte)(color&0xff );
  }
  break;
 }
 return rBuf;
}





  1. 2014/10/22(水) 00:52:52|
  2. Java
  3. | トラックバック:0
  4. | コメント:0

javaで、型の違う配列での四則演算がしたい

javaで、型の違う配列での四則演算をやりたくていろいろ調べたんですが、やっぱそのままじゃ無理そう。

なぜこんなことをやろうと思ったかといえば、
このブログの最初の記事でmatrixクラスとかを作っていたんですが、
int[][]型のmatrixでも、double[][]型のmatrixでも、1つの関数とかで
同じ挙動を実現したいと思ったのが発端。

たとえば以下のようなメソッドを考えます。

public static double[][] plus(double[][] matrix, double[][] target)
{
double[][] result = new double[matrix.length][matrix[0].length];

for(int i=0; i<matrix.length; i++ )
for(int j=0; j<matrix[0].length; j++ )
result[i][j] = matrix[i][j]+target[i][j];
return result;
}



この処理を、以下のようにint[][]配列でも使いたいのです。
int[][] ho = {1, 2};
int[][] ge = {3, 4};
double[][] hoge = plus(ho,ge);

かといって、関数のオーバーロードを型の種類ごとに用意するのは
保守性が悪くなりますし、コード量も多くなりますので、
なるべくどの型でも使える関数みたいなものを用意したいです。

ただ、どの型でも使える関数というのが、どうもjavaでは容易ではないようです。
配列であってもキャストで(int[][])double[][]と書ければ良いんですがね。

候補として考えたのは以下の3つ。
①Objecti[][]型を使う
②Generic型を使う
③型変換メソッドを作る

他にもラムダ式を用いるとかがあると思いますが
java 1.5上での開発のためここでは考えません。

①、②の方法ではObject型やジェネリック型自体に四則演算がないので、
そのままでは使えません。配列要素の入れ替えとかなら出来ると思います。

結局、ちょっとオーバーヘッドが大きいけど、③の型変換メソッドを作って
入れるのが一番手っ取り早いし実現性も高いかなと思います。


#そもそもこんなmatrixのメソッドはdouble以外で使わなければいいんですが、
 使いたくなるシチュエーションがあるんですよね。。。。





  1. 2014/10/20(月) 01:13:52|
  2. Java
  3. | トラックバック:0
  4. | コメント:0
次のページ