アルちゃんのprocessing部~その2~Pixel単位で画面の更新
その1
https://note.com/alchan/n/n10ed3c60a534
この記事は予測と推測とだろう運転で成り立っております。あんまし試しておりませんことを謝罪します。
表示されている画面に対してpixel単位でアクセスする手段を考えましょう。これ系統の手法は様々ですが、例えばC#ではGetPixelなりLockBits。processingではgetPixel(古い),get,およびpixelsということになるでしょう。
get(),set()系は座標を指定すれば色を返すため、単純ですが処理が遅く、そうでない方は画面全体を一括して扱う為、処理は早いですがそれなりに複雑です。
画面全体をpixel単位で処理する場合、pixelの総数(width*height)*色情報に相当するデータ分だけメモリが占有されると考えます。
色情報というのは一通りではなく、例えば白黒の2値画像なら1pixelに掛かるデータ量は1bitです。画像サイズが100*100ならば、その画像は10000bitの1250byteくらいのデータサイズです。
256階調のグレースケール画像は1pixelを表すのに8bit、すなわち1byteのデータ量を必要とします。画像サイズが100*100ならば、その画像は10000byteくらいのデータサイズです。
いわゆるフルカラーとなると、RGBそれぞれに8bit、1byte。あわせて24bit、3byteが必要になります。そこに透明度情報をつけて、ARGBあるいはRGBAとして32bitで扱うのは極めて基本的な色の単位です。
32bitはメモリの単位、1区画1アドレスに相当し、いわゆるint型が使用するデータ量でもあったりするためです。また、セガサターンの単位でもあります。
processingのcolorデータ型も、そのような意味合いから32bitで色情報を扱います。というよりもこのcolorデータ型はintそのものです。int型を受ける関数にcolorを突っ込んでもそのまま使用できるはずです。多分。色の並びはARGB(ただし作成する時はcolor(RGBA))。
https://processing.org/reference/color_datatype.html
コードで見てみましょう。
まずは遅いといわれるget(x,y)を用いて画面上の色情報を取得する場合。
color[][]color_field = new color[height][width];
for(int i = 0; i<height;i++)
{
for(int j = 0; j<width; j++)
{
color c = get(j,i);//j=x=col,i=y=row
color_field[i][j]=c;
}
}
次にpixels[]を用いた手法を2つ。色情報を取得するだけならupdatePixels()は不要です。updatePixels()が必要なのはpixels[]の変更を適用する場合のみです。
loadPixels();
for(int i = 0; i<pixels.length;i++)
{
int x = i%width;
int y = i/height;
color c = pixels[i];
color_field[y][x]=c;
}
updatePixels();
loadPixels();
for(int i = 0; i<height;i++)
{
for(int j = 0; j<width; j++)
{
color c = pixels[i*width+j];
color_field[i][j]=c;
}
}
updatePixels();
color[][]になんらかの処理をしたら戻します。すでにpixels[]がロード済みなら loadPixels()は不要です。
color[][]color_field = new color[height][width];
loadPixels();
for(int i = 0; i<pg.height; i++){
for(int j = 0; j<pg.width; j++){
pixels[(i*width) + j] = color_field[i][j]
}
}
updatePixels();
個別の色情報は以下のように取得します。
color c = color_field[i][j];
int a = (int)alpha(c);
int r = (int)red(c);
int g = (int)green(c);
int b = (int)blue(c);
また、シフト演算やビット演算を使って以下のようにも取得できます。
colorデータ型は32bitで、色の並びはARGBでした。右シフトはbit列を右に動かし、はみ出た分は消去されます。即ちbit列のうち、不要な右っ側のbitを削除する操作です。
アンド演算は左項と右項の両方でbitが1なものを1、そうでないものを0とします。0xFFは16進数FF、10進数だと255です。これは32bit二進数だと
00000000000000000000000011111111
です。つまり&0xFFは右端の8bitはそのまま残し、それ以外は全部消す演算です。言い換えると不要な左っ側のbitを削除する操作です。
不要な右側を消すために必要なだけ右に詰め、不要な左側のbitを消したら必要な色情報が256階調で取得できます。
color argb = color_field[i][j];
int a = argb >> 24 & 0xFF;
int r = argb >> 16 & 0xFF;
int g = argb >> 8 & 0xFF;
int b = argb & 0xFF;
以下のようにすれば、データの表示に際してグレースケールの256階調では不足な時、1677万階調まで増幅できるはずです。多分。
//val なんらかのデータの値
int level = 16777216;//256^3
//int level = 65536; //256^2
//int level = 255; //256^1
val = (int)map(val,val_min,val_max,0,level);
int r = val >> 16 & 0xFF;
int g = val >> 8 & 0xFF;
int b = val & 0xFF;
color c = color(r,g,b);
ブレンドモード
ペイントソフトなどのレイヤーの合成モードは以下のような感じでしょう。
PImage やPGraphicsはloadPixels()が効きます。即ちpixel単位で操作できます。
PImage は画像を扱うクラスです。画像とはjpg,gif,png等でまとめられているデータです。PImageはそれらの読み込みができます。また、読み込んだ画像に対するマスク処理、フィルター処理、ブレンド処理などは、既にある程度備わっています。
データ的には読み込んだ画像データcolor[width*height]と、それを処理する関数の集まりです。
https://processing.org/reference/PImage.html
PGraphicsはPImageの派生クラスです。
PGraphicsは出力デバイスの抽象化であると考えられます。以下は全部推測です。
出力デバイスとはディスプレイやプリンタ、液晶タブレットなんかであり、そうしたデバイスとデータでやり取りする場合、やり取りするデータの規格はあらかじめ決めておく必要があります(例えばARGBなのかRGBAなのか、データは先頭から並べるのか末端から並べるのか等)。そうした規格はOS屋さんだとか印刷業界、ディスプレイ業界なんかが決めるでしょう。力の強い業界が取り決めて、その他がそれに従います。
いずれにせよ、出力用のデータをパソコン用のメモリ上にためておく必要はあるでしょう。このような一時的にためておく置き場所をバッファと言ったりしますが、PGraphicsの一つの側面は間違いなくそれです。すなわち画像用のデータであるという側面でloadPixels()が可能であり、その意味ではPImageと同類です。逆にPGraphicsがPImageを継承してしまっているがゆえに、PImageのblend()やmask()がPGraphicsで使用できてしまうことにモヤっとする人もいるでしょう。使う分には便利です。
PGraphicsがPImageと明確に異なるのは、バッファに描き込む手段をもっていることです。例えば
DrawLine,DrawEllipse,DrawRect,DrawBezier,DrawImage,DrawText...
これらはまず規格(GDI,DirectX,OpenGL)があり、OSがAPIとして規格の実行方法を公開し(すなわち、このOSではDrawLine関数を実行すると出力先に線が引けますという機能を公開し)、ビデオカード屋さんが物理的にそれを実現し(例えばおそらく、ブレセンハムのアルゴリズムなどはハード的に実装されていると思われる。バッファの物理的な場所も多分ここ)ます。
processingはJavaであり、JavaはOSごとに合わせに行くタイプであるため、例えばWindows上のprocessingでline()を使用したなら、最終的に実行されるのはWinAPIのGDI規格を抽象化したGraphicsクラスのDrawLine()であると思われます。思われますが、違うかもしれません。
とにもかくにもprocessingできゃっきゃと遊びたいユーザーが考えるのは、processing上でペイントソフトのレイヤー機能みたいなのを実現したいとおもったら、PGraphicsをリスト化するのが一番楽かなということです。
以下、ブレンドモードを強引に自前でやった場合。PGraphicsはPImageを継承している為、これらの基本的な処理が既にある程度備わっています。
void BlendLayer(PImage img1, PImage img2, IBlendMode method)
{
img1.loadPixels();
img2.loadPixels();
for(int i = 0; i<img1.height; i++){
for(int j = 0; j<img1.width; j++){
color blend_color = img2.pixels[(i*img2.width) + j];
color base_color = img1.pixels[(i*img1.width) + j];
int r1 = (int)red(blend_color);
int g1 = (int)green(blend_color);
int b1 = (int)blue(blend_color);
int a1 = (int)alpha(blend_color);
int r2 = (int)red(base_color);
int g2 = (int)green(base_color);
int b2 = (int)blue(base_color);
int a2 = (int)alpha(base_color);
int r = (int)method.Blend(r1,r2);
int g = (int)method.Blend(g1,g2);
int b = (int)method.Blend(b1,b2);
int a = (int)method.Blend(a1,a2);
img1.pixels[(i*img1.width) + j] = color(r,g,b,a);
}//for i
}//for j
img1.updatePixels();
img2.updatePixels();
}
void BlendLayer(PGraphics pg1, PGraphics pg2, IBlendMode method)
{
pg1.beginDraw();
pg2.beginDraw();
pg1.loadPixels();
pg2.loadPixels();
for(int i = 0; i<pg1.height; i++){
for(int j = 0; j<pg1.width; j++){
color blend_color = pg2.pixels[(i*pg2.width) + j];
color base_color = pg1.pixels[(i*pg1.width) + j];
int r1 = (int)red(blend_color);
int g1 = (int)green(blend_color);
int b1 = (int)blue(blend_color);
int a1 = (int)alpha(blend_color);
int r2 = (int)red(base_color);
int g2 = (int)green(base_color);
int b2 = (int)blue(base_color);
int a2 = (int)alpha(base_color);
int r = (int)method.Blend(r1,r2);
int g = (int)method.Blend(g1,g2);
int b = (int)method.Blend(b1,b2);
int a = (int)method.Blend(a1,a2);
pg1.pixels[(i*pg1.width) + j] = color(r,g,b,a);
}//for i
}//for j
pg1.updatePixels();
pg2.updatePixels();
pg1.endDraw();
pg2.endDraw();
}
引数に関数を受けるのはJavaだととても面倒くさいです。インターフェースを作成し、それを実装したクラスがメソッド一つに相当します。C#だとデリゲートがあり、pythonやjavascriptならなんもいらんでしょう。
interface IBlendMode
{
float Blend(float base, float mask);
}
関数を引数に受ける場合はシグネチャ(引数の型と戻り値の型)だけ気を付けます。シグネチャさえ合致していれば、その関数の中身が何であれ文法的には意味が通る(つまり関数を換装できる)という考え方です。
以下、gimpのページからそのままパクったもの
https://docs.gimp.org/2.10/da/gimp-concepts-layer-modes.html
//標準 Normal Layer Modes
class BlendNormal implements IBlendMode
{
float Blend(float base, float mask)
{
return mask;
}
}
//比較(明) Lighten Layer Modes
class BlendLighten implements IBlendMode
{
float Blend(float base, float mask)
{
return max(mask,base);
}
}
//スクリーン
class BlendScreen implements IBlendMode
{
float Blend(float base, float mask)
{
return 255-((255-mask)*(255-base)/255);
}
}
//覆い焼き Dodge
class BlendDodge implements IBlendMode
{
float Blend(float base, float mask)
{
return (256*base)/((255-mask)+1);
}
}
//加算
class BlendAdd implements IBlendMode
{
float Blend(float base, float mask)
{
return min((mask+base),255);
}
}
//比較(暗) Darken Layer Modes
class BlendDarken implements IBlendMode
{
float Blend(float base, float mask)
{
return min(mask,base);
}
}
//乗算 Multiply
class BlendMult implements IBlendMode
{
float Blend(float base, float mask)
{
return (base*mask)/255;
}
}
//焼き込み Burn
class BlendBurn implements IBlendMode
{
float Blend(float base, float mask)
{
return 255-(256*(255-base)/(mask+1));
}
}
//オーバーレイ
class BlendOverlay implements IBlendMode
{
float Blend(float base, float mask)
{
return base/255*(base+(2*mask)/255*(255-base));
}
}
//ソフトライト Soft light
class BlendSoft implements IBlendMode
{
float Blend(float base, float mask)
{
float r = 255-((255-mask)*(255-base)/255);
return ((255-base)*mask+r)/255*base;
}
}
//ハードライト Hard
class BlendHard implements IBlendMode
{
float Blend(float base, float mask)
{
if(mask>128)
{
return 255-(255-2*(mask-128)*(255-base))/256;
}
else
{
return 2*mask*base/256;
}
}
}
//差の絶対値
class BlendDifference implements IBlendMode
{
float Blend(float base, float mask)
{
return Math.abs(base-mask);
}
}
//減算 Subtract
class BlendSubtract implements IBlendMode
{
float Blend(float base, float mask)
{
return max((base-mask),0);
}
}
//微粒取り出し Grain extract
class BlendGrainExtract implements IBlendMode
{
float Blend(float base, float mask)
{
return base-mask+128;
}
}
//微粒結合 Grain merge
class BlendGrainMerge implements IBlendMode
{
float Blend(float base, float mask)
{
return base+mask-128;
}
}
//除算 Divide
class BlendDivide implements IBlendMode
{
float Blend(float base, float mask)
{
return (256*base)/(mask+1);
}
}
以下はいわゆるマスキング処理をする場合。マスクする側のレイヤーは、透明にしたいエリアを特定色で指定し、マスクされる側のレイヤーはそのエリアが透明色に吹き飛ばされます。この処理も、ある程度までならPGraphics及びPImageに備わっています。
void MaskingLayer(PGraphics pg, PGraphics mask, color target_color)
{
pg.loadPixels();
mask.loadPixels();
for(int i = 0; i<pg.height; i++){
for(int j = 0; j<pg.width; j++){
color mask_color = mask.pixels[(i*mask.width) + j];
color pg_color = pg.pixels[(i*mask.width) + j];
int ta = (int)alpha(target_color);
int tr = (int)red(target_color);
int tg = (int)green(target_color);
int tb = (int)blue(target_color);
int ma = (int)alpha(mask_color);
int mr = (int)red(mask_color);
int mg = (int)green(mask_color);
int mb = (int)blue(mask_color);
int pga = (int)alpha(pg_color);
int pgr = (int)red(pg_color);
int pgg = (int)green(pg_color);
int pgb = (int)blue(pg_color);
//マスクとして機能する色に一致する
if(mr==tr&&mg==tg&&mb==tb)
{
//一致する部分をアルファ0にして削除
pg.pixels[(i*pg.width) + j] = color(pgr,pgg,pgb,0);
}
else
{
//残す部分
pg.pixels[(i*pg.width) + j] = color(pgr,pgg,pgb,pga);
}
}//for i
}//for j
pg.updatePixels();
mask.updatePixels();
}
フィルター処理
この辺から段々遊べるようになってきます。詳しくは下部に、歳の割には頭の賢いYoutuberへの動画リンクを張っておきます。
float[][] Convolution(float[][] source, double[][] kernel)
{
int source_h = source.length;
int source_w = source[0].length;
int kernel_h = kernel.length;
int kernel_w = kernel[0].length;
//中心から4辺への距離
int l_edge = kernel_w / 2;//left
int r_edge = kernel_w / 2;//right
int t_edge = kernel_h / 2;//top
int b_edge = kernel_h / 2;//base
float[][] out_buf = new float[source_h][source_w];
//copy
for(int i = 0; i<source_h; i++)
{
for(int j = 0; j<source_w; j++)
{
out_buf[i][j]=source[i][j];
}
}
////式
for (int src_y = 0; src_y < source_h; src_y++)
{
//フチ
if (src_y < t_edge) { continue; }
if (source_h - b_edge - 1 < src_y) { continue; }
for (int src_x = 0; src_x < source_w; src_x++)
{
//フチ
if (src_x < l_edge) { continue; }
if (source_w - r_edge - 1 < src_x) { continue; }
//畳み込み工程
float sum = 0;
for (int kernel_y = 0; kernel_y < kernel_h; kernel_y++)
{
for (int kernel_x = 0; kernel_x < kernel_w; kernel_x++)
{
//(x,y)を中心にすえ
//bmp(hy,hx)とkernel(i,j)を演算する
int hx = src_x + kernel_x - l_edge;
int hy = src_y + kernel_y - t_edge;
float val = source[hy][hx] * (float)kernel[kernel_y][kernel_x];
sum += val;
}//kernel_x
}//kernel_y
//kernelが正規化されていないなら除算が発生する
//out_buf[src_y, src_x] = sum / kernel_sum;
out_buf[src_y][src_x] = sum;
}//src_x
}//src_y
return out_buf;
}
color[][] Convolution(color[][] source, double[][] kernel)
{
int source_h = source.length;
int source_w = source[0].length;
int kernel_h = kernel.length;
int kernel_w = kernel[0].length;
//中心から4辺への距離
int l_edge = kernel_w / 2;//left
int r_edge = kernel_w / 2;//right
int t_edge = kernel_h / 2;//top
int b_edge = kernel_h / 2;//base
color[][] out_buf = new color[source_h][source_w];
//copy
for(int i = 0; i<source_h; i++)
{
for(int j = 0; j<source_w; j++)
{
out_buf[i][j]=source[i][j];
}
}
////式
for (int src_y = 0; src_y < source_h; src_y++)
{
//フチ
if (src_y < t_edge) { continue; }
if (source_h - b_edge - 1 < src_y) { continue; }
for (int src_x = 0; src_x < source_w; src_x++)
{
//フチ
if (src_x < l_edge) { continue; }
if (source_w - r_edge - 1 < src_x) { continue; }
//畳み込み工程
double sum_r = 0;
double sum_g = 0;
double sum_b = 0;
double sum_a = 0;
for (int kernel_y = 0; kernel_y < kernel_h; kernel_y++)
{
for (int kernel_x = 0; kernel_x < kernel_w; kernel_x++)
{
//(x,y)を中心にすえ
//bmp(hy,hx)とkernel(i,j)を演算する
int hx = src_x + kernel_x - l_edge;
int hy = src_y + kernel_y - t_edge;
float r = red(source[hy][hx]);
float g = green(source[hy][hx]);
float b = blue(source[hy][hx]);
float a = alpha(source[hy][hx]);
sum_r += r * kernel[kernel_y][kernel_x];
sum_g += g * kernel[kernel_y][kernel_x];
sum_b += b * kernel[kernel_y][kernel_x];
sum_a += a * kernel[kernel_y][kernel_x];
}//kernel_x
}//kernel_y
//kernelが正規化されていないなら除算が発生する
//out_buf[src_y, src_x] = sum / kernel_sum;
out_buf[src_y][src_x] = color((int)sum_r,(int)sum_g,(int)sum_b,(int)sum_a);
}//src_x
}//src_y
return out_buf;
}
各種カーネル(追加中)
//平滑化
double[][] Smoothing(int m, int n)
{
double[][] kernel = new double[m][n];
for(int i = 0; i<m; i++)
{
for(int j = 0; j<n; j++)
{
kernel[i][j]=1/((double)m*(double)n);
}
}
return kernel;
}
//加重平均化フィルタ(ガウシアン)
double[][] WeightedAveraging3()
{
double[][] kernel = new double[][] {
{ 1d/16, 2d/16, 1d/16},
{ 2d/16, 4d/16, 2d/16},
{ 1d/16, 2d/16, 1d/16}};
return kernel;
}
double[][] WeightedAveraging5()
{
double[][] kernel = new double[][] {
{ 1d/256, 4d/256, 6d/256, 4d/256, 1d/256,},
{ 4d/256, 16d/256, 24d/256, 16d/256, 4d/256,},
{ 6d/256, 24d/256, 36d/256, 24d/256, 6d/256,},
{ 4d/256, 16d/256, 24d/256, 16d/256, 4d/256,},
{ 1d/256, 4d/256, 6d/256, 4d/256, 1d/256,} };
return kernel;
}
double[][] Emboss()
{
double[][] kernel = new double[][]{
{ -2d,-1d, 0 },
{ -1d, 1d, 1d},
{ 0 , 1d, 2d}};
return kernel;
}
double[][] Sharpness()
{
double[][] kernel = new double[][]{
{ 0.0d,-0.3d, 0.0d}, //鮮鋭化カーネル
{-0.3d, 2.2d,-0.3d},
{ 0.0d,-0.3d, 0.0d}};
return kernel;
}
//→(X)方向
double[][] SobelX()
{
double[][] kernel = new double[][] {
{ -1, 0, 1 },
{ -2, 0, 2 },
{ -1, 0, 1 } };
return kernel;
}
//↓(Y)方向
double[][] SobelY()
{
double[][] kernel = new double[][] {
{ -1,-2,-1 },
{ 0, 0, 0 },
{ 1, 2, 1 } };
return kernel;
}
//プリューウィットフィルタ
double[][] PrewittQ()
{
double[][] kernel = new double[][]{
{ -1d/6, 0, 1d/6 },
{ -1d/6, 0, 1d/6 },
{ -1d/6, 0, 1d/6 } };
return kernel;
}
以下、歳の割には頭の賢いYoutuber。