はじめに
画像の色は一般にRGB値で表されますが、人間の感覚に近いのは、「色相」「彩度」「明度」を用いるHSV(またはHSB)表色系です。画像の色の補正にHSVを用いる方法を実装し、RGB値とHSV値との相互の関係を理解しやすくしました。
- 完成版のアプレットを見る
対象読者
色の表示方法に興味を持ち、レタッチソフトなどにあるHSV補正機能の原理を知りたい人。
必要な環境
J2SE 5.0を使っていますが、それより古いバージョンでも大丈夫です。
色の表現方法
コンピュータの無かった時代から、色の表現方法は研究されていて、1905年に発案のA.H.Munsellによるマンセル表色系は、色のデザイナーに広く用いられています。1931年に国際照明委員会(Commission Internationale de l'Eclairage)で決められたCIE系もあり、色彩学の専門家に使われています。
情報処理の分野ではRGB(Red、Green、Blue)系が主流で、これは発光体からの光を合成するブラウン管や液晶画面に適したものです。一方、CMY(Cyan、Magenta、Yellow)系は、反射光を用いるカラー印刷用ですが、CMY系は純粋の黒を出しにくいので、blacK(黒)を含めたCMYK系が多く用いられます。
マンセル表色系と似たHSV(他にHSB、HSI、HLSもある)系は、色の調整用として、画像ソフトに用いられています。HSVは、Hue(色相)、Saturation(彩度)、Value(明度)の略で、Microsoftのアプリケーションで使われています。これと同様のものが、PhotoshopやJavaではHSBと呼ばれ、Value(明度)の代わりにBrightness(明度)が用いられています。Intensity(明度)を用いると、リモートセンシングなどで用いられるHSIになります。明度の代わりにLuminance(輝度)を用いるHLSがありますが、明度と輝度は少し異なります。
本稿では、RGB系とHSV(またはHSB)系の相互変換を紹介しますが、HSB系のBはRGB系にも使われているので、混乱を避けるため、「HSV系」とのみ呼ぶことにします。
RGB系とHSV系の相互変換
RGB系からHSV系への変換
H(色相)は、360°の円周に色を配置します。赤(0°)、黄(60°)、緑(120°)、シアン(180°)、青(240°)、マゼンタ(300°)の順で、360°では再び赤に戻ります。実際には角度でなく、円周を0.0から1.0で表わすのが一般的です。S(彩度)とV(明度)は、R、G、B値の最大をMax
、最小をMin
としたとき、S=(Max - Min)/Max
およびV=Max/255
で表わされます。
HSV系からRGB系への変換
上述の変換を逆にすれば良いのですが、少し複雑なので、参考資料を参照して下さい。
便利な専用Java API
Color
クラスには、RGB値とHSV値(JavaではHSB値と呼びます。以下の説明では、メソッド名以外は、b
の代わりにv
を使いますのでご注意ください)を相互に変換する便利なメソッドがあります。
RGB値からHSV値への変換
RGBtoHSB(r,g,b,hsbvals[])
メソッドを使用します。r
、g
、b
をそれぞれ独立に与えて、このメソッドを実行すると、配列hsbvals[0]
にh
が、hsbvals[1]
にs
が、hsbvals[2]
にv
が格納されます。
HSV値からRGB値への変換
用途により、二つのメソッドを使い分けます。
- HSV値からImageクラスの画像を得たい場合
- HSV値から個別のr、g、b値を得たい場合
HSBtoRGB(h,s,v)
メソッドを使用します。h
、s
、v
を与えると、int
型のrgb
値が得られますので、これを配列rgb[]
に格納した後、createImage
でImage
クラスの画像に変換します。getHSBColor(h,s,b)
メソッドを用いると、Color
クラスのオブジェクトが得られますので、getRed()
などにより、r
、g
、b
値を個別に得ることができます。プログラムの説明
本稿で紹介するプログラムは、以下の処理から成ります。
起動時の動作(init()の内容)
- GUI関係の設定を行う。
- HSVの補正量をゼロに初期設定する。
- 画像ファイルを読み込み、
img_src[0]
とし、RGB値とHSV値を配列に格納する。 - 色見本画像を生成し、
img_src[1]
とし、RGB値とHSV値を配列に格納する。
画面描画時の動作(paint()の内容)
- スクロールバーの上にある表示(ラベル)に、その時のHSV補正量を表示する。
sample_flag
の状態に従って、img_src[0]
またはimg_src[1]
を原画像として、中央左に描画する。- その時のHSV補正量から補正画像のHSV値を求める。
- HSV値から、補正画像
img_dest
を生成する。 img_dest
を中央右側に描画する。- マウスで画像上をクリックしてあれば(
point_flag
がtrue
)、赤丸を表示し、下部に、その場所の色を正方形内に描画し、RGB値とHSV値を表示する。
各フラグの設定
point_flag
は、画像上の点がマウスによって指定(ポイント)されたかどうかを示し、sample_flag
は、色見本が選ばれているかどうかを示すフラグです。マウスクリックにより、下表のように設定されます。
point_flag | sample_flag | |
起動時 | false | false |
画像上をマウスでクリック | true | ―― |
画像外をマウスでクリック | false | ―― |
「画像ファイル←→色見本」 ボタンをクリック | false | false→true true→false |
各変数の名称
原画像を、画像ファイルから読み出したものと、プログラムで生成した色見本の2種類設け、下表のように最初の添え字で区別します。添え字の[0]
と[1]
をtype
と呼び、[画像ファイル←→色見本]ボタンのクリックで切り替えます。
画像名 | RGB値 | HSV値 | |
原画像(画像ファイル) | img_src[0] | src_red[0][SIZE] など | src_hue[0][SIZE] など |
原画像(色見本) | img_src[1] | src_red[1][SIZE] など | src_hue[1] [SIZE]など |
補正画像 | img_dest | dest_rgb など | dest_hue[SIZE] など |
プログラム
import java.applet.Applet; import java.awt.*; import java.awt.image.*; import java.awt.event.*; public class HSV extends Applet implements MouseListener,AdjustmentListener,ActionListener{ // ……中略…… public void init(){ setLayout(new BorderLayout()); point_flag=false; sample_flag=false; //パネルを設定する Panel panel=new Panel(); //2行4列、列間隔、行間隔 panel.setLayout(new GridLayout(2,4,20,5)); add(panel,"North"); //スクロールバーを設定する //最大値+5が必要 scroll_hue=new Scrollbar(Scrollbar.HORIZONTAL,0,5,-50,55); scroll_sat=new Scrollbar(Scrollbar.HORIZONTAL,0,5,-50,55); scroll_val=new Scrollbar(Scrollbar.HORIZONTAL,0,5,-50,55); scroll_hue.addAdjustmentListener(this); scroll_sat.addAdjustmentListener(this); scroll_val.addAdjustmentListener(this); //ラベルを設定する label_hue=new Label( ); label_sat=new Label( ); label_val=new Label( ); //ボタンを設定する button_sample=new Button("画像ファイル←→色見本"); button_reset=new Button("補正量のリセット"); button_sample.addActionListener(this); button_reset.addActionListener(this); //ラベル、スクロールバー、ボタンをパネルに貼り付ける panel.add(label_hue); panel.add(label_sat); panel.add(label_val); panel.add(button_sample); panel.add(scroll_hue); panel.add(scroll_sat); panel.add(scroll_val); panel.add(button_reset); //マウスリスナーを付ける addMouseListener(this); //HSVの補正量をゼロに初期化する dev_hue=0.0; dev_sat=0.0; dev_val=0.0; //画像ファイルを読み込み、RGB値を求め、HSV値に変換して、 //それぞれを格納する img_src[0]=readImageFile("utrecht.jpg"); createRGB(img_src[0],src_red[0],src_green[0],src_blue[0]); changeToHSV(src_red[0],src_green[0],src_blue[0], src_hue[0],src_sat[0],src_val[0]); //色見本画像を作成し、RGB値を求め、HSV値に変換して、 //それぞれを格納する img_src[1]=createSampleImage(); createRGB(img_src[1],src_red[1],src_green[1],src_blue[1]); changeToHSV(src_red[1],src_green[1],src_blue[1], src_hue[1],src_sat[1],src_val[1]); } // ……中略…… //Image画像のRGB値を求めて格納するメソッド private void createRGB(Image img, int[] red, int[] green, int[] blue){ Color color; int[] rgb=new int[SIZE]; //img画像の一次元RGB配列を得る PixelGrabber grabber= new PixelGrabber(img,0,0,WIDTH,HEIGHT,rgb,0,WIDTH); try{ grabber.grabPixels(); }catch(InterruptedException e){} for(int i=0;i<SIZE;i++){ color=new Color(rgb[i]); red[i]=color.getRed(); green[i]=color.getGreen(); blue[i]=color.getBlue(); } } //RGB値をHSV値に変換して格納するメソッド private void changeToHSV(int[] red, int[] green, int[] blue, double[] hue, double[] sat, double[] val){ float[] hsbvals=new float[3]; for(int i=0;i<SIZE;i++){ Color.RGBtoHSB(red[i],green[i],blue[i],hsbvals); hue[i]=(double)hsbvals[0]; sat[i]=(double)hsbvals[1]; val[i]=(double)hsbvals[2]; } } // ……中略…… //HSV値でImage画像を作るメソッド private Image createImageFromHSV(double[] hue, double[] sat, double[] val){ int[] rgb=new int[SIZE]; for(int i=0;i<SIZE;i++) rgb[i]=Color.HSBtoRGB( (float)hue[i],(float)sat[i],(float)val[i]); Image img=createImage( new MemoryImageSource(WIDTH,HEIGHT,rgb,0,WIDTH)); return img; } public void paint(Graphics g){ int type; label_hue.setText("色相補正量="+String.valueOf((float)dev_hue)); label_sat.setText("彩度補正量="+String.valueOf((float)dev_sat)); label_val.setText("明度補正量="+String.valueOf((float)dev_val)); if(sample_flag) type=1; else type=0; g.drawImage(img_src[type],X0,Y0,null); //原画像を左に表示する for(int i=0;i<SIZE;i++){ dest_hue[i]=src_hue[type][i]+dev_hue; dest_sat[i]=src_sat[type][i]+dev_sat; if(dest_sat[i]>1.0) dest_sat[i]=1.0; if(dest_sat[i]<0.0) dest_sat[i]=0.0; dest_val[i]=src_val[type][i]+dev_val; if(dest_val[i]>1.0) dest_val[i]=1.0; if(dest_val[i]<0.0) dest_val[i]=0.0; } Image img_dest=createImageFromHSV(dest_hue,dest_sat,dest_val); g.drawImage(img_dest,X1,Y0,null); //補正画像を右に表示する if(point_flag){ drawMousePoint(point_x,point_y); drawColorSquare(type,point_x,point_y); drawColorData(type,point_x,point_y); } } }
プログラムの使い方
アプレットを実行すると、上方に左から[色相][彩度][明度]のスクロールバーと、その時の補正量が表示されます。初期値は、いずれもスライダーが中心にあり、値は「0.0」になっています。
上方の右上には、[画像ファイル←→色見本]のボタンがあり、起動時には[画像ファイル]になっていて、原画像は画像ファイルから読み込まれます。このボタンをクリックすると、その都度、[画像ファイル]と[色見本]が切り替わります。
上方の右下には、[補正量リセット]ボタンがあり、これをクリックすると、すべての補正量が「0.0」に戻ります。
中央左には原画像が表示され、右にはHSV値が補正された画像が表示されます。マウスで、原画像または補正画像の上をクリックすると、赤丸が表示され、下に、その場所の色を正方形に拡大したものとRGB値およびHSV値が表示されます。両画像以外の場所をクリックすると、赤丸は消えます。
[画像ファイル]は、雨天に撮影したもので、これの補正を試みることができます。[色見本]には、円形(円周方向が色相の変化、半径方向が彩度の変化を表わします)と長方形(縦方向が色相の変化、横方向が明度の変化を表わします)の2種類がありますので、これにより、HSV値の補正の効果を確認することができます。
プログラムの実行結果
図1は実行画面の例で、原画像として「色見本」を使用しています。補正量は、それぞれ0.2にしてあり、実際に使用するには、やや過度ですが、効果が分かりやすくなっています。
原画像の背景色は彩度がゼロのグレイですが、彩度を上げたために、補正画像では強制的にバランスが崩され、少し黄緑色がかっています。バランスの崩れ方は、その時の実行環境によって変わることがあり、たとえば赤色がかったりすることもあります。
左側の円形色見本と、右側の補正画像とを比較すると、補正画像が72°(360°×0.2)だけ回転しています。
画面上で赤丸の付いた場所の色と、RGB値、HSV値が、下方に表示されています。原画像と補正画像を比較すると、H(色相)とS(彩度)は0.2ずつ増えていますが、V(明度)は0.2を加えることができず1.0に抑えられていて、Gは最大値の255に留まっています。
原画像にも、補正画像にも、良く見ると縞状の部分がありますが、これは原理的に発生するもので、補正を過度に行うと特に目立ちます。
まとめ
HSV表色系による色の補正は広く行われていて、レタッチソフトには必須の機能です。本稿では、HSV値を限度いっぱいに0.0から1.0まで変化できるようにして、RGB値とHSV値の関係を理解できるようにしました。ただし、実際にHSV値を大きく変えると、原画像とすっかり変わったものになり、特殊効果を狙う場合以外には、実用的ではありません。
Java APIには、RGB値とHSV値を相互に変換する便利なメソッドが用意されていますので、これを用いるとプログラム作成が容易になります。
参考資料
この記事は、筆者のウエブサイト『Visual C++ 6.0を用いた易しい画像処理(3)―原画像のピクセル毎のRGBデータをHSI表示に変換する―(ただし、現在はVisual C++ 2005 Express Edition 版に改定)』を全面的に改良し、Java言語に書き直して、分かりやすく加筆したものです。
市販ソフトによるHSVの補正方法を解説した書籍で比較的詳しいのは、次の2冊です。
- 『早川廣行のPhotoshop CSプロフェッショナル講座 色補正編』 早川廣行著、毎日コミュニケーションズ刊
- 『Adobe Photoshop CS2 パーフェクトマスター』 KUNIKO著、秀和システム刊