画像処理

セカンドライフで綺麗な水面を作る

投稿日:


こんにちは!

セカンドライフ技術系 Advent Calendar 2015」の12月13日(日)担当の なたで です!

木曜日の記事(セカンドライフでジャンプ台を作る)に引き続いて…
今日は、セカンドライフで綺麗な水面を作ります。
けっこう長めなので、理由はともかく作りたいという方は、読み飛ばして、
途中に張ってある画像を保存して、テクスチャに使用していってください。


綺麗な波をつくるには・・・?

綺麗な水面を作るために、水の特徴を考えます。
水といえば波があるということです。

次のように2つの波の衝突を考えます

nami001
1つは、右から左へ流れる大きな波。

nami002
もう1つは、左から右へ流れる小さい波。

nami003
この2つが、衝突しても波と波とが重なり合って、消滅するということはありません。

つまり、波を作るためには、何面もの波を重ねると本物のように作ることができます。

よって、次のように作ると良いことが分かります。
「複数の面を重ねるようにおいて、模様に波(水面)のテクスチャを設定して、それぞれ別の方向へ動かす」

ここでさらに、波のテクスチャについて考えていきます。
単純に、波の模様のテクスチャだけを用意するだけでいいのでしょうか。

一度、波を実際に目で見た時を考えてみます。

nami004
波は、うねうねした形をもっており、うねうねに合わせた法線ベクトルがあります。
光は、波に当たっときに、法線を境に反射をします。
そして、反射した光が目に入った時に、白く輝いて見えることが分かります。

これらから、水面のテクスチャは、法線マップを作成する必要があるのです。
波の模様テクスチャから、法線マップを作ります。


特徴をまとめます

・複数の波を、合成する必要がある。
・それぞれの波を別方向でスクロールさせる。
・法線マップを使用して、リアルな反射を持たせる。


波のテクスチャを用意しよう

波のテクスチャは、シームレスなパーリンノイズが適切です。
シームレスというのは、上下左右につなぎ目のないという意味です。

具体的にシームレスなパーリンノイズを作る方法ですが、
実は検索してもなかなか見つかりませんでした。
一応、下記のようなツールを発見できたものの、登録が必要であるため、ダウンロードをしていません。
Procedural seamless noise texture generator

また、CLIP STUDIOというツールにも、パーリンノイズを作成する機能があるのですが、
こちらは、シームレスなパーリンノイズを作成できません。

そこで、シームレスなパーリンノイズを作成する簡易ツールを作りました。
nami_iconダウンロード

Javaの環境があれば、MacでもWindowsでも動きます。
ここでは、Windowsの環境での説明をしていきます。

【1】
「すべてのプログラム」→「アクセサリ」→「コマンドプロンプト」を実行します。

【2】
次のように、jarファイルがおいてあるディレクトリへ移動します。

cd C:\Users\xxx\Desktop\yyy

【3】
次のように実行してください。(-help と書けば使い方がでます。)

java -jar noise.jar -width 256 -height 256 -persistence 0.6 -lacnarity 2.5 -seed 123456

【4】
noise
これがパーリンノイズ(テクスチャA)です。

次に、このパーリンノイズを「高さ(ハイトマップ)」と見立てて
ノーマルマップを作成していきます。

ノーマルマップの作成ツールは、ハイトマップから自動でノーマルマップを作成できる
NormalMap-Online」を使用させていただきます。

【5】
normalmake
左側の箇所に画像ファイルのアイコンをドラッグ&ドロップして、
画像を読み込ませて、真ん中上部のスライダーで設定をしていきます。

【6】
NormalMap
Downloadを押せば、保存ができます。
これで、ノーマルマップ(テクスチャB)の完成です。

【7】
さらに、色用のテクスチャも用意します。
具体的には、透明度だけを持つ白色テクスチャです。
透明度には、パーリンノイズのテクスチャを使用します。

ovicon64
今回は、高機能な画像補正専用ツールの「オレンジビューア」を使用します。
このツールも、Javaをインストールする必要があります。

nami100
今回は、256×256のパーリンノイズなので、256×256の白色の画像を作成して
ビューアで読み込みを行います。

nami101
「色」→「アルファチャンネル」→「ファイルから読み込む…」で
さきほどの256×256のパーリンノイズを指定します。

nami102
これで、パーリンノイズのアルファチャンネルを持つ白い画像を作成できました。
「ファイルを名前を付けて保存」で、「xxx.png」と拡張子を「png」にして保存してください。
このソフトは、保存するときの拡張子によって自動で、その形式で保存されます。
pngにする理由は、透明度情報を持たせるためです。

noise2

完成した画像がこちらになります。(テクスチャC


テクスチャを設定しよう

立方体を作成して、薄くします。
これを水面にします。

nami200
色は水のような青色にします。

nami201
テクスチャの拡散反射の設定は「テクスチャC」を使います。

nami202
バンプマップの設定は、「テクスチャB」を使います。

nami203
鏡面反射の色成分の設定は「テクスチャA」を使います。

水平スケール、垂直スケールは、面の面積に合わせて、適度な値にしておきましょう。

nami301
この時点で、だいぶ水面のようになります。

nami302
あとは、2枚重ねておきましょう。
また、2枚目に関しては、あとでスクロールの関係上、180度回転させておきましょう。


スクロールさせる

波テクスチャを設定した重ねたオブジェクトの2つ、
それぞれにスクリプトを入れて、テクスチャをスクロールさせます。

スクロールは、かなり遅いほうがそれっぽいです。
スクリプトはこんな感じです。

start() {
	integer	mode	=	ANIM_ON | SMOOTH | LOOP;
	integer	face	=	ALL_SIDES;
	integer	sizex	=	0;
	integer	sizey	=	0;
	float	start	=	0.0;
	float	length	=	0.0;
	float	rate	=	0.005;
	llSetTextureAnim(0, face, 0, 0, 0.0, 0.0, 0.0);
	llSetTextureAnim(mode, face, sizex, sizey, start, length, rate);
}
default{state_entry(){start();}}

これを入れ込んで、実行させるとスクロールが始まります!

これで綺麗な水面の完成です!

テテーン!

これで私の「セカンドライフ技術系 Advent Calendar 2015」の記事は終わりです。
このような発表のきっかけを用意していただきました、sabroさんありがとうございました。

今回の記事を通して、3Dのテクスチャの作成や設定のテクニックに興味を持っていただければ幸いです・・・!

プログラミングのアイデア1

投稿日: 更新日:


適当に思いついたプログラミングアイデアのメモ。

・オートフォーカス
オートフォーカスとはカメラのピントを自動で合わせるもの。
ピントがあっているということは、画像がクッキリしているということ。
クッキリしているということは、高域の周波数成分が大きいということ。
ということで、ピントを段々いろいろと試していって、
もっとも高域の周波数成分が大きくなるところを探す。
そこがピントが合っている部分と考えられる。

・文章をスキャナで取り込んだ画像の傾きの自動補正
画像を大津の判別分析法とかで2値化する。
2値化した画像に対して、毎行(x軸方向)ごとの黒色の値をカウントする。
このx軸方向にカウントした値の列をベクトルとみなす。
ここで、画像が傾いている場合は、このベクトルの各値が滑らかなにつながっていると予想される。
従って、あらゆる傾きに対して、上記のベクトルを調べる。
このベクトルの中で、もっとも高周波数成分が含まれる傾きが、
画像が水平になっていると思われる。

・文字列のあいまい検索
検索したい文字列を X
Xを用いて検索する文章を Y
まずXに含まれるワードを保存しておく。
(漢字とか、カタカナとかでワード判定。あるいは辞書を使う。)
Yに対して、文章を句読点で区切りを分ける。
各文章ごとに、上記のワードを調べる。
全てのワードが含まれていれば、その文章に検索したい情報が含まれると考えられる。
あるいは、Y全体に対して上記のワードがどこに含まれるか調べる。
ワードが集中している箇所が、Xのことが書いてあると考えられる。

Javaでファイルをロードする前に画像の大きさを調べる

投稿日: 更新日:


BMP, PNG, JPEG, GIF画像を読み込む前に画像の横幅と縦幅を調べるサンプル。

ソースコードを「ソースコード HTML化 コンバーター「唐辛子」」で変換してます
半角スペース、全角スペース、&nbsp、preが使用できないのでインデントが使えません。
各自、ソフトでインデントの修正コマンドなどを使用して下さい。すみません><

追記:ImageInputStreamImageReaderを使ったほうが良かったかも。
こちらだとロード前に画像の大きさの他に、画像のカラーモデルもわかるみたい。

入力引数
file … ファイルパス
stream … ファイルのInputStream
length … ファイルサイズ

public class X {

public static void check(File file,InputStream stream,int length) {
if((file == null)||(stream == null)||(length<0)) {
return(null);
}
Loader loader = new Loader(file, stream, length);
int[] imagesize = getImageSize(loader);
System.out.println(imagesize[0] + ” “+ imagesize[1]);
loader.close();
return(null);
}

public static int[] getImageSize(Loader loader) {
//最初の2バイトを調べる
byte[] head = loader.getByte(0, 2);
if(head == null) {
loader.close();
return(null);
}
//受け取りのデータ確保
int[] imagesize = new int[2];
for(int i = 0;i < imagesize.length;i++) imagesize[i] = –1;
//検索
if(!getImageSizeJPEG(head, loader, imagesize)) {
if(!getImageSizePNG(head, loader, imagesize)) {
if(!getImageSizeGIF(head, loader, imagesize)) {
getImageSizeBMP(head, loader, imagesize);
}
}
}
if((imagesize[0] == –1)||(imagesize[1] == –1)) {
return(null);
}
else {
return(imagesize);
}
}

public static boolean getImageSizeBMP(byte[] head,Loader loader,int[] type) {
//bfType
if((head[0] != (byte)0x42)||((head[1] != (byte)0x4D))) {
return(false);
}
byte[] binary;
int length = 0;
//ファイル長のテスト
binary = loader.getByte(0, 4);
if(binary == null) return(true);
length = ((binary[3]&0xff) << 24) | ((binary[2]&0xff) << 16) | ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
if(length != loader.getByteSize()) {
return(true);
}
if(!loader.setOffset(8)) return(true);
//情報ヘッダのサイズ
binary = loader.getByte(0, 4);
if(binary == null) return(true);
length = ((binary[3]&0xff) << 24) | ((binary[2]&0xff) << 16) | ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
//BMPCOREHEADER構造体
if(length == 12) {
binary = loader.getByte(0, 2);
if(binary == null) return(true);
type[0] = ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
binary = loader.getByte(0, 2);
if(binary == null) return(true);
type[1] = ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
}
//BITMAPINFOHEADER構造体
else if(length == 40) {
binary = loader.getByte(0, 4);
if(binary == null) return(true);
type[0] = ((binary[3]&0xff) << 24) | ((binary[2]&0xff) << 16) | ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
binary = loader.getByte(0, 4);
if(binary == null) return(true);
type[1] = ((binary[3]&0xff) << 24) | ((binary[2]&0xff) << 16) | ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
type[1] = Math.abs(type[1]);//負数の場合はデータは上から下へ
}
return(true);
}

public static boolean getImageSizeGIF(byte[] head,Loader loader,int[] type) {
//シグネーチャ
if((head[0] != (byte)0x47)||((head[1] != (byte)0x49))) {
return(false);
}
if(!loader.setOffset(4)) return(true);
byte[] binary;
binary = loader.getByte(0, 2);
if(binary == null) return(true);
type[0] = ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
binary = loader.getByte(0, 2);
if(binary == null) return(true);
type[1] = ((binary[1]&0xff) << 8) | (binary[0] & 0xff);
//このあとブロックが並ぶ
return(true);
}

public static boolean getImageSizePNG(byte[] head,Loader loader,int[] type) {
//PNG ファイルシグネチャ
if((head[0] != (byte)0x89)||((head[1] != (byte)0x50))) {
return(false);
}
if(!loader.setOffset(6)) return(true);
byte[] binary;
int marker;
int length = 0;
//探索回数
for(int i = 0;i < 100;i++) {
//チャンク全体のデータサイズ
binary = loader.getByte(0, 4);
if(binary == null) return(true);
length = ((binary[0]&0xff) << 24) | ((binary[1]&0xff) << 16) | ((binary[2]&0xff) << 8) | (binary[3] & 0xff);
//チャンクの種類
binary = loader.getByte(0, 4);
if(binary == null) return(true);
marker = ((binary[0]&0xff) << 24) | ((binary[1]&0xff) << 16) | ((binary[2]&0xff) << 8) | (binary[3] & 0xff);
//System.out.printf(“%08X %d\n”, marker,length);
switch(marker) {
case 0x49484452: //IHDR (イメージヘッダ)
binary = loader.getByte(0, 4);
if(binary == null) return(true);
type[0] = ((binary[0]&0xff) << 24) | ((binary[1]&0xff) << 16) | ((binary[2]&0xff) << 8) | (binary[3] & 0xff);
binary = loader.getByte(0, 4);
if(binary == null) return(true);
type[1] = ((binary[0]&0xff) << 24) | ((binary[1]&0xff) << 16) | ((binary[2]&0xff) << 8) | (binary[3] & 0xff);
return(true);
case 0x49444154: //SOS(イメージデータ)
return(true);
}
//次のセグメントへ(CRC(4byte)を追加)
if(!loader.setOffset(length + 4)) return(true);
}
return(true);
}

public static boolean getImageSizeJPEG(byte[] head,Loader loader,int[] type) {
//SOI(Start of Image) 0xFFD8
if((head[0] != (byte)0xFF)||((head[1] != (byte)0xD8))) {
return(false);
}
byte[] binary;
int marker;
int length = 0;
//探索回数
for(int i = 0;i < 100;i++) {
//セグメントのマーカー
binary = loader.getByte(0, 2);
if(binary == null) return(true);
marker = ((binary[0] << 8) | binary[1]) &0xffff;
//セグメント全体のデータサイズ
binary = loader.getByte(0, 2);
if(binary == null) return(true);
length = ((binary[0]&0xff) << 8) | (binary[1] & 0xff);
//System.out.printf(“%08X %d\n”, marker,length);
switch(marker) {
case 0xFFC0: //SOF0
case 0xFFC1: //SOF1
case 0xFFC2: //SOF2
case 0xFFC3: //SOF3
case 0xFFC5: //SOF5
case 0xFFC6: //SOF6
case 0xFFC7: //SOF7
case 0xFFC9: //SOF9
case 0xFFCA: //SOF10
case 0xFFCB: //SOF11
case 0xFFCD: //SOF13
case 0xFFCE: //SOF14
case 0xFFCF: //SOF15
if(!loader.setOffset(1)) return(true);
binary = loader.getByte(0, 2);
if(binary == null) return(true);
type[1] = ((binary[0]&0xff) << 8) | (binary[1] & 0xff);
binary = loader.getByte(0, 2);
if(binary == null) return(true);
type[0] = ((binary[0]&0xff) << 8) | (binary[1] & 0xff);
return(true);
case 0xFFDA: //SOS(イメージデータ開始)
return(true);
}
//次のセグメントへ(length(2byte)を除く)
if(!loader.setOffset(length – 2)) return(true);
}
return(true);
}

/**
* 簡易ロード<br>
* シークが動かせないものや、ファイルとして読み込んでいるもの、再ロードできないものなどを<br>
* 色々なデータに関して、同じように扱えるようにする。
*/

private static class Loader {
private File file = null;
private static final int defaultBufferSize = 65536;
private InputStream inputstream = null;
private FileInputStream fileinputstream = null;
private int length = 0;
private int seek = 0;
private byte[] data = null;
public Loader(File file,InputStream stream,int length) {
this.file = file;
this.length = length;
this.inputstream = stream;
//ファイルとして扱える
if(this.inputstream instanceof FileInputStream) {
this.fileinputstream = (FileInputStream) stream;
this.fileinputstream.mark(defaultBufferSize);
return;
}
//シーク位置が動かせる
if(this.inputstream.markSupported()) {
this.inputstream.mark(defaultBufferSize);
return;
}
//一度しかロードできないのでロードしておく。
this.data = getByte();
}
/**
* 終了処理必ず行う。
*/

public void closeStream() {
if(inputstream != null) {
try {
inputstream.close();
} catch (IOException e) {
}
inputstream = null;
fileinputstream = null;
}
}
/**
* 終了処理必ず行う。
*/

public void close() {
closeStream();
data = null;
file = null;
}
/**
* 一部を切り出す。シークがずれます。
* @param offset
* @param length
* @return
*/

public byte[] getByte(int offset,int length) {
if((this.seek + offset + length) >= this.length) {
return(null);
}
this.seek += offset;
byte[] out = new byte[length];
if(data != null) {//既にロード済みなら
System.arraycopy(data, this.seek, out, 0, length);
}
else {//ロードしていないので通常動作
try {
this.inputstream.read(out, offset, length);
} catch (IOException e) {
return(null);
}
}
this.seek += length;
return(out);
}
/**
* seek位置をずらす
* @param seek
* @return
*/

public boolean setOffset(int offset) {
return(this.setSeek(this.seek + offset));
}
/**
* seek位置を設定します
* @param seek
* @return
*/

public boolean setSeek(int seek) {
if(this.length <= seek) {//範囲外
return(false);
}
if(data != null) {//データ取得済み
this.seek = seek;
return(true);
}
if(this.seek <= seek) {//スキップ
try {
this.inputstream.skip(seek – this.seek);
} catch (IOException e) {
return(false);
}
this.seek = seek;
return(true);
}
//最初から読み込み直し
if(this.fileinputstream != null) {//ファイルの場合
closeStream();
try {
this.fileinputstream = new FileInputStream(file);
this.inputstream = this.fileinputstream;
} catch (FileNotFoundException e) {
}
}
else if(this.inputstream.markSupported()) {//シークを移動できるなら
try {
this.inputstream.reset();
} catch (IOException e) {
}
}
else {//それ以外は無理
return(false);
}
this.seek = seek;
try {
this.inputstream.skip(this.seek);
} catch (IOException e) {
return(false);
}
return(true);
}
/**
* ファイルサイズを取得する。
* @return
*/

public int getByteSize() {
return(this.length);
}
/**
* ファイルのバイナリを取得する。
* シーク位置が0になります。
* @return
*/

public byte[] getByte() {
if(data != null) {//既にロード済みなら
return(data);
}
this.setSeek(0);
BufferedInputStream bufferedstream = null;
try {
bufferedstream = new BufferedInputStream(inputstream, defaultBufferSize);
byte[] binary = new byte[length];
bufferedstream.read(binary);
data = binary;
}
catch (FileNotFoundException e) {/*e.printStackTrace();*/}
catch (IOException e) {/*e.printStackTrace();*/}
finally {
try {
if(bufferedstream != null) {
bufferedstream.close();
}
}
catch (Exception e) {
}
}
return(data);
}
}
}

ipmpackにアフィン変換を追加

投稿日: 更新日:


久しぶりにipmpackを更新しました。
まだvectorからはダウンロード出来ません。
placearkの方からダウンロードして下さい。

今回オマケとして、sample\ipmpackの中に、
少しだけコンパイル前のソースコードが入っています。

ipmpack 1.050
(公開したてなので、バージョン据え置きの細かい修正が入るかも。vector公開まで待つといいかも。)

いろいろと更新しました。
主な更新点は次のような感じです。
・アフィン変換機能
・画像の一部コピー機能
・カラーマトリックス変換のオフセット値の設定機能
・命令の引数の省略可能な箇所を増やす
・一部の変な命令名を変更
・HSPの新しいサムネイル機能付きヘルプに対応

詳しいところは、ヘルプやreadmeに任せまして、
目玉のアフィン変換機能の紹介。


上のように、アフィン変換を用いて、拡大回転思いのままです。
回転なら、grotateより品質がよく綺麗に回転できます。
線形補間にも対応しています。

ただ少し処理が重たいです。
ソースコードが「sample\ipmpack\sample_vram.zip」の中に入っているので、
自由にいじって下さい。
ライセンスは前の通りNYSLのような感じです。

ヘルプでサムネイルが表示されます!

「BufferedImage」の TYPE_4BYTE_ABGR から TYPE_INT_ARGB にする方法

投稿日: 更新日:


Java の BufferedImage は getType() を使うことでタイプを知ることが出来ます。
byte型でABGRの順に記録されている場合は、BufferedImage.TYPE_4BYTE_ABGR
int型でARGBが記録されている場合は、BufferedImage.TYPE_INT_ARGB になります。

TYPE_4BYTE_ABGR の画像を、
ColorConvertOp(中ではg.drawImage(image, 0, 0, null);) を利用して変換すると、なぜか色が変わる場合があります。※
PixelGrabber を利用すれば、色は変わらないです。

ですが、直接変換する方法のサンプルを紹介します。
BufferedImage から int型のデータを取得するサンプルにもなると思います。

BufferedImage.TYPE_4BYTE_ABGR の image から
BufferedImage.TYPE_INT_ARGB の newimage へ

int width  = image.getWidth();
    int height = image.getHeight();
    int pixelsize = width * height;
    BufferedImage newimage = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
    int[] pixels = ((DataBufferInt)(newimage.getRaster().getDataBuffer())).getData();
    byte[] binary = ((DataBufferByte)(image.getRaster().getDataBuffer())).getData();
    int r,g,b,a;
    for(int i=0,j=0;i<pixelsize;i++) {
        a = binary[j++]&0xff;
        b = binary[j++]&0xff;
        g = binary[j++]&0xff;
        r = binary[j++]&0xff;
    pixels[i] = (a<<24)|(r<<16)|(g<<8)|b;
    }

以上です。

BufferedImage.TYPE_3BYTE_BGR の場合は

    b = binary[j++]&0xff;
    g = binary[j++]&0xff;
    r = binary[j++]&0xff;
    pixels[i] =0xff000000|(r<<16)|(g<<8)|b;

BufferedImage.TYPE_CUSTOM で取得した DataBuffer のタイプが TYPE_BYTE の場合も
上記のような感じで変換できるようです。
但し、順番が下記のようになるっぽいです。

BufferedImage.TYPE_CUSTOM かつ getColorModel().hasAlpha() で true の場合は

    r = binary[j++]&0xff;
    g = binary[j++]&0xff;
    b = binary[j++]&0xff;
    a = binary[j++]&0xff;
    pixels[i] = (a<<24)|(r<<16)|(g<<8)|b;

BufferedImage.TYPE_CUSTOM かつ getColorModel().hasAlpha() で false の場合は

    r = binary[j++]&0xff;
    g = binary[j++]&0xff;
    b = binary[j++]&0xff;
    pixels[i] = 0xff000000|(r<<16)|(g<<8)|b;

ImageIO.read で読み込んだ時点で、
インデックスカラーとグレースケール以外は、TYPE_INT_ARGB に変換してくれればいいのに。


追記メモ
色が変わると言ったのですが、drawImageの品質を
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
を使って上げればもしかしたら防げるかもしれない。

「BufferedImage」の TYPE_BYTE_GRAY から TYPE_BYTE_INDEXED にする方法

投稿日: 更新日:


Java の BufferedImage は getType() を使うことでタイプを知ることが出来ます。
グレースケールの場合は、BufferedImage.TYPE_BYTE_GRAY
インデックスカラーの場合は、BufferedImage.TYPE_BYTE_INDEXED になります。

グレースケールの画像を、 PixelGrabber 、又は ColorConvertOp を利用して
TYPE_INT_ARGB に変換させようと思うと、なぜか明るくなることがあります。
インデックスカラーの画像を TYPE_INT_ARGB に変換させる場合はそういうことはありません。

直にグレースケールから TYPE_INT_ARGB に変換してもいいのですが、
インデックスカラーへの変換方法について書きます。
インデックスカラーの BufferedImage の生成方法のサンプルにもなると思います。

static BufferedImage repairBufferedImage(BufferedImage image) {
	Raster raster = image.getData();
	int height = raster.getHeight();
	int width = raster.getWidth();
	int datasize = width * height;
	final int GRAYSIZE = 256;
	int[] color = new int[4];
	byte[] pallet_grayscale = new byte[GRAYSIZE];
	for (int i = 0; i < GRAYSIZE; i++) {
		pallet_grayscale[i] = (byte) i;
	}
	IndexColorModel icm = new IndexColorModel(8, GRAYSIZE,
			pallet_grayscale, pallet_grayscale, pallet_grayscale);
	byte[] indexout = new byte[datasize];
	int i = 0;
	for (int y = 0; y < height; y++) {
		for (int x = 0; x < width; x++) {
			raster.getPixel(x, y, color);
			indexout[i] = (byte) color[0];
			i++;
		}
	}
	BufferedImage out = new BufferedImage(width, height,
			BufferedImage.TYPE_BYTE_INDEXED, icm);
	Graphics g = out.createGraphics();
	g.drawImage(Toolkit.getDefaultToolkit().createImage(
			new MemoryImageSource(width, height, icm, indexout, 0, width)),
			0, 0, null);
	g.dispose();
	g = null;
	return (out);
}

以上です。

Javaで3D 5

投稿日: 更新日:


以前の続きでテクスチャのパーリンノイズの補間法の話です。
いい加減補間について調べようといろいろとプログラムして作ってみました。
テクスチャなので、上下と左右が繋がるように補間します。

8x8px → 256x256px と、各補間法で32倍拡大してみます。

最近傍補間(NearestNeighbor)(1×1の格子点を使用)

線形補間(Biliner)(2×2の格子点を使用)

三角関数による補間(Cosine)(2×2の格子点を使用)

5次のエルミート曲線による補間(2×2の格子点を使用)

バイキュービック法(Bicubic)
(4×4の格子点を用いたsinc関数の近似補間関数を用いる)
sinc関数はrect関数をフーリエ変換・逆フーリエ変換したもの。

Lanczos2
(4×4の格子点を用いた近似補間関数を用いない手法)

Lanczos3
(6×6の格子点を用いた近似補間関数を用いない手法)

近似関数を用いたバイキュービック法の方が、近似関数を用いないLanczos2より画質がいいと思う。速度も速いですし。Lanczos3はバイキュービック法とほぼ変わらない。近似関数とほぼ一致。

結論
綺麗に拡大したい。 → バイキュービック補完
早く綺麗に拡大したい → 3次のエルミート曲線による補間
速度重視で拡大したい → 最近傍補間

バイキュービック補間の係数については、koujinz blogさんのブログの「画像の拡大「Bicubic法」」を用いています。

この係数なんですが、Javaの※拡大機能のバイキュービック補間は、速度重視で多分違う係数を用いていると思う。上記の係数を用いた方が綺麗に補完できましたし。それか、固定小数点で計算していて誤差が出るとか?

※g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);の方法

.NET TIPSの「画像を高品質に拡大/縮小するには?」を見ると
.NETのバイキュービック補間でも「Bicubic」と「HighQualityBicubic」の2種類があるから、
きっと同じバイキュービック補間でも、係数が変えて速度変えるとかあるのかな。

というか「HighQualityBilinear」ってまじで線形補完なのか。どうやってるんだろう。

三角関数による補間とエルミート曲線による補間の元ネタは、t-potの管理人さんが執筆した「DirectX 9 シェーダプログラミングブック」のパーリンノイズの章です。一番後ろの方です。