ゆるゆるプログラミング

・減色処理(メディアンカット)

フルカラー画像の色数を減らす処理の1つのメディアンカットのJavaのソースプログラムです。変換後の色は、変換前の画像に含まれる色から自動で抽出し、出来るだけ元の画像に見た目が近くなるように変換できます。

これを、Javaソースコードにすると以下のようになります。

MedianCut.java
001:    import java.awt.image.BufferedImage;
002:    import java.io.File;
003:    import javax.imageio.ImageIO;
004:    import java.io.IOException;
005:    
006:    
007:    // 元画像のRGBを格納するクラス(bitの3次元配列)
008:    class ColorCube
009:    {
010:    	private int m_data[];
011:    
012:    	// 初期化
013:    	public ColorCube()
014:    	{
015:    		// intは32bitなので、256x256x256を32で割っている
016:    		m_data = new int[ 256 * 256 * 256 / 32 ];
017:    		// 全データを0にする
018:    		for ( int i = 0; i < m_data.length; ++ i ) m_data[ i ] = 0;
019:    	}
020:    
021:    	
022:    	// RGBの値から配列の添え字を計算
023:    	public int data_position( int r, int g, int b )
024:    	{
025:    		if ( ( 0 > r ) || ( 255 < r ) ) return -1;
026:    		if ( ( 0 > g ) || ( 255 < g ) ) return -1;
027:    		if ( ( 0 > b ) || ( 255 < b ) ) return -1;
028:    
029:    		return ( ( r * 256 + g ) * 256 + b ) / 32;
030:    	}
031:    	
032:    
033:    	// 指定のRGBの位置にbit=1を設定
034:    	public boolean set( int r, int g, int b )
035:    	{
036:    		int dpos;
037:    
038:    		dpos = data_position( r, g, b );
039:    		if ( 0 > dpos ) return false;
040:    
041:    		//
042:    		m_data[ dpos ] = m_data[ dpos ] |
043:    				 ( (int)0x80000000 >> ( b % 32 ) );
044:    
045:    		return true;
046:    	}
047:    	
048:    
049:    	// 指定のRGBの位置のbitが1かを判定
050:    	public boolean get( int r, int g, int b )
051:    	{
052:    		int dpos;
053:    
054:    		dpos = data_position( r, g, b );
055:    		if ( 0 > dpos ) return false;
056:    
057:    		//
058:    		if ( 0 != ( m_data[ dpos ] &
059:    			 ( (int)0x80000000 >> ( b % 32 ) ) ) ) return true;
060:    
061:    		return false;
062:    	}
063:    
064:    
065:    	// 指定のRGBの範囲のbit=1の個数を返す
066:    	public int pixel_number( int rmin, int rmax, int gmin, int gmax,
067:    								 int bmin, int bmax )
068:    	{
069:    		int r, g, b;
070:    		int pixnum;
071:    
072:    		pixnum = 0;
073:    		for ( r = rmin; r <= rmax; ++ r ) {
074:    			for ( g = gmin; g <= gmax; ++ g ) {
075:    				for ( b = bmin; b <= bmax; ++ b ) {
076:    					if ( true == get( r, g, b ) ) ++ pixnum;
077:    				}
078:    			}
079:    		}
080:    
081:    		return pixnum;
082:    	}
083:    
084:    
085:    	// 指定のRGBの範囲をR軸で半分に分けるr値を返す
086:    	public int get_median_r( int rmin, int rmax,
087:    					 int gmin, int gmax, int bmin, int bmax )
088:    	{
089:    		int r, rrange;
090:    		int pixnum[], pnum, pixtotal, pixhalf;
091:    		
092:    		rrange = rmax - rmin + 1;
093:    		if ( 0 > rrange ) return rmin;
094:    
095:    		//
096:    		pixnum = new int[ rrange ];
097:    		for ( int i = 0; i < rrange; ++ i ) pixnum[ i ] = 0;
098:    
099:    		//
100:    		pixtotal = 0;
101:    		for ( r = rmin; r <= rmax; ++ r ) {
102:    			pixnum[ r - rmin ] = pixel_number( r, r, gmin, gmax,
103:    									 bmin, bmax );
104:    			pixtotal = pixtotal + pixnum[ r - rmin ];
105:    		}
106:    
107:    		//
108:    		pixhalf = pixtotal / 2;
109:    		pnum = 0;
110:    		for ( r = rmin; r <= rmax; ++ r ) {
111:    			if ( ( pnum + pixnum[ r - rmin ] ) >= pixhalf ) return r;
112:    			pnum = pnum + pixnum[ r - rmin ];
113:    		}
114:    
115:    		//
116:    		return -1;
117:    	}
118:    
119:    
120:    	// 指定のRGBの範囲をG軸で半分に分けるg値を返す
121:    	public int get_median_g( int rmin, int rmax,
122:    					 int gmin, int gmax, int bmin, int bmax )
123:    	{
124:    		int g, grange;
125:    		int pixnum[], pnum, pixtotal, pixhalf;
126:    		
127:    		grange = gmax - gmin + 1;
128:    		if ( 0 > grange ) return gmin;
129:    
130:    		//
131:    		pixnum = new int[ grange ];
132:    		for ( int i = 0; i < grange; ++ i ) pixnum[ i ] = 0;
133:    
134:    		//
135:    		pixtotal = 0;
136:    		for ( g = gmin; g <= gmax; ++ g ) {
137:    			pixnum[ g - gmin ] = pixel_number( rmin, rmax, g, g,
138:    									 bmin, bmax );
139:    			pixtotal = pixtotal + pixnum[ g - gmin ];
140:    		}
141:    
142:    		//
143:    		pixhalf = pixtotal / 2;
144:    		pnum = 0;
145:    		for ( g = gmin; g <= gmax; ++ g ) {
146:    			if ( ( pnum + pixnum[ g - gmin ] ) >= pixhalf ) return g;
147:    			pnum = pnum + pixnum[ g - gmin ];
148:    		}
149:    
150:    		//
151:    		return -1;
152:    	}
153:    
154:    
155:    	// 指定のRGBの範囲をB軸で半分に分けるb値を返す
156:    	public int get_median_b( int rmin, int rmax,
157:    					 int gmin, int gmax, int bmin, int bmax )
158:    	{
159:    		int b, brange;
160:    		int pixnum[], pnum, pixtotal, pixhalf;
161:    		
162:    		brange = bmax - bmin + 1;
163:    		if ( 0 > brange ) return bmin;
164:    
165:    		//
166:    		pixnum = new int[ brange ];
167:    		for ( int i = 0; i < brange; ++ i ) pixnum[ i ] = 0;
168:    
169:    		//
170:    		pixtotal = 0;
171:    		for ( b = bmin; b <= bmax; ++ b ) {
172:    			pixnum[ b - bmin ] = pixel_number( rmin, rmax, gmin, gmax,
173:    										 b, b );
174:    			pixtotal = pixtotal + pixnum[ b - bmin ];
175:    		}
176:    
177:    		//
178:    		pixhalf = pixtotal / 2;
179:    		pnum = 0;
180:    		for ( b = bmin; b <= bmax; ++ b ) {
181:    			if ( ( pnum + pixnum[ b - bmin ] ) >= pixhalf ) return b;
182:    			pnum = pnum + pixnum[ b - bmin ];
183:    		}
184:    
185:    		//
186:    		return -1;
187:    	}
188:    }
189:    
190:    
191:    // 分割されたRGBの範囲と含まれる色の個数を格納するクラス
192:    class ColorCore
193:    {
194:    	public int rmin = 0, rmax = 0;
195:    	public int gmin = 0, gmax = 0;
196:    	public int bmin = 0, bmax = 0;
197:    	public int pixnumber = 0;
198:    }
199:    
200:    
201:    // メディアンカット本体
202:    public class MedianCut {
203:    	public static void main( String[] args ) {
204:    		boolean result;		// 結果格納フラグ
205:    		int     maxcorecount;	//減色後の色数
206:    
207:    		// ColorCube
208:    		ColorCube cube = new ColorCube();
209:    
210:    		// ColorCore
211:    		int corenumber;
212:    		ColorCore core[];
213:    
214:    		// ファイル名
215:    		String inname, outname;
216:    		// 画像格納クラス
217:    		BufferedImage img = null;
218:    
219:    		// 入力した引数が3つ以上かを調べる
220:    		if ( 3 > args.length ) {
221:    			// 入力した引数が2つ未満の場合、使用方法を表示する
222:    			System.out.println(
223:    				 "MedianCut [入力JPEG名] [出力JPEG名] [色数]" );
224:    			return;
225:    		}
226:    
227:    		// 入力JPEG名をinnameに代入(拡張子".jpg"省略なし)
228:    		inname  = args[ 0 ];
229:    		// 出力JPEG名をoutnameに代入(拡張子".jpg"省略なし)
230:    		outname = args[ 1 ];
231:    		
232:    		// 引数を変換し、コア数に代入
233:    		try {
234:    			maxcorecount = Integer.valueOf( args[ 2 ] );
235:    			if ( 2 > maxcorecount ) {
236:    				System.out.println( "色数を2以上を指定してください" );
237:    				return;
238:    			}
239:    			core = new ColorCore[ maxcorecount ];
240:    		}
241:    		catch( NumberFormatException ne )
242:    		{
243:    			System.out.println( "引数が不正です" );
244:    			return;
245:    		}		
246:    		
247:    		// JPEGの読み込み
248:    		try {
249:    			// inname(入力JPEG)を読み込んでimgにセット
250:    			img = ImageIO.read( new File( inname ) );
251:    		} catch (Exception e) {
252:    			// inname(入力JPEG)の読み込みに失敗したときの処理
253:    			 e.printStackTrace();
254:    			return;
255:    		}
256:    
257:    		// 画像の色の持ち方をチェック
258:    		if ( BufferedImage.TYPE_3BYTE_BGR != img.getType() )
259:    		{
260:    			System.out.println( "対応していないカラーモデルです!("
261:    									 + inname +")" );
262:    			return;
263:    		}
264:    
265:    		// 減色処理
266:    		int x, y;
267:    		int width, height;
268:    		int r, g, b, color;
269:    		int rmin, rmax;
270:    		int gmin, gmax;
271:    		int bmin, bmax;
272:    		int dr, dg, db, dmax;
273:    		int maxcore, maxpix;
274:    		int cutp;
275:    
276:    		// 画像サイズの取得
277:    		width = img.getWidth();
278:    		height= img.getHeight();
279:    
280:    		// ColorCubeにRGB値を格納
281:    		rmin = gmin = bmin = 256;
282:    		rmax = gmax = bmax = -1;
283:    		for ( y = 0; y < height; ++ y ) {
284:    			for ( x = 0; x < width; ++ x ) {
285:    				// (x,y)の色を取得
286:    				color = img.getRGB( x, y );
287:    
288:    				// 色をr,g,bに分解
289:    				r = ( color >> 16 ) & 0xff;
290:    				g = ( color >> 8 ) & 0xff;
291:    				b = color & 0xff;
292:    
293:    				// ColorCubeにrgbを格納
294:    				cube.set( r, g, b );
295:    
296:    				// rの範囲をrmin,rmaxに格納
297:    				if ( r < rmin ) rmin = r;
298:    				if ( r > rmax ) rmax = r;
299:    
300:    				// gの範囲をgmin,gmaxに格納
301:    				if ( g < gmin ) gmin = g;
302:    				if ( g > gmax ) gmax = g;
303:    
304:    				// bの範囲をbmin,bmaxに格納
305:    				if ( b < bmin ) bmin = b;
306:    				if ( b > bmax ) bmax = b;
307:    			}
308:    		}
309:    
310:    		// ColorCubeから減色テーブルを作成する
311:    
312:    		// core[]の初期値を設定
313:    		corenumber = 0;
314:    		core[ corenumber ] = new ColorCore();
315:    		core[ corenumber ].rmin = rmin;
316:    		core[ corenumber ].rmax = rmax;
317:    		core[ corenumber ].gmin = gmin;
318:    		core[ corenumber ].gmax = gmax;
319:    		core[ corenumber ].bmin = bmin;
320:    		core[ corenumber ].bmax = bmax;
321:    		core[ corenumber ].pixnumber = cube.pixel_number( rmin, rmax,
322:    							 gmin, gmax, bmin, bmax );
323:    		++ corenumber;
324:    
325:    		// coreの数がmaxcorecountになるまで実行
326:    		while ( corenumber < maxcorecount ) {
327:    			// core[]の中でが画素数が最大のものを選択
328:    			maxcore = 0;
329:    			maxpix = 0;
330:    			for ( int i = 0; i < corenumber; ++ i ) {
331:    				if ( ( 0 == i ) || ( core[ i ].pixnumber > maxpix ) ) {
332:    					maxcore = i;
333:    					maxpix = core[ i ].pixnumber;
334:    				}
335:    			}
336:    
337:    			rmin = core[ maxcore ].rmin;
338:    			rmax = core[ maxcore ].rmax;
339:    			gmin = core[ maxcore ].gmin;
340:    			gmax = core[ maxcore ].gmax;
341:    			bmin = core[ maxcore ].bmin;
342:    			bmax = core[ maxcore ].bmax;
343:    
344:    			// 選択したcoreのr,g,bの範囲をdr,dg,dbに代入
345:    			dr = rmax - rmin;
346:    			dg = gmax - gmin;
347:    			db = bmax - bmin;
348:    		
349:    			// dr,dg,dbの最大値を求める
350:    			dmax = Math.max( dr, Math.max( dg, db ) );
351:    			if ( 0 == dmax ) break;
352:    
353:    			// dr,dg,dbのうち、最大の成分でcoreを分割
354:    			if ( dmax == dr ) {
355:    				// r成分でcoreを分割
356:    
357:    				// 分割するrをcutpに代入
358:    				cutp = cube.get_median_r( rmin, rmax, gmin, gmax,
359:    										 bmin, bmax );
360:    				if ( rmin > cutp ) break;
361:    				// 分割処理
362:    				core[ corenumber ] = new ColorCore();
363:    				core[ corenumber ].rmin = cutp + 1;
364:    				core[ corenumber ].rmax = core[ maxcore ].rmax;
365:    				core[ corenumber ].gmin = core[ maxcore ].gmin;
366:    				core[ corenumber ].gmax = core[ maxcore ].gmax;
367:    				core[ corenumber ].bmin = core[ maxcore ].bmin;
368:    				core[ corenumber ].bmax = core[ maxcore ].bmax;
369:    				core[ corenumber ].pixnumber
370:    					 = cube.pixel_number( cutp + 1, rmax, gmin, gmax,
371:    										 bmin, bmax );
372:    				++ corenumber;
373:    				//
374:    				core[ maxcore ].rmax = cutp;
375:    				core[ maxcore ].pixnumber
376:    					 = cube.pixel_number( rmin, cutp, gmin, gmax,
377:    									 bmin, bmax );
378:    			}
379:    			else {
380:    				if ( dmax == dg ) {
381:    					// g成分でcoreを分割
382:    
383:    					// 分割するgをcutpに代入
384:    					cutp = cube.get_median_g( rmin, rmax, gmin, gmax,
385:    										 bmin, bmax );
386:    					if ( gmin > cutp ) break;
387:    					// 分割処理
388:    					core[ corenumber ] = new ColorCore();
389:    					core[ corenumber ].rmin = core[ maxcore ].rmin;
390:    					core[ corenumber ].rmax = core[ maxcore ].rmax;
391:    					core[ corenumber ].gmin = cutp + 1;
392:    					core[ corenumber ].gmax = core[ maxcore ].gmax;
393:    					core[ corenumber ].bmin = core[ maxcore ].bmin;
394:    					core[ corenumber ].bmax = core[ maxcore ].bmax;
395:    					core[ corenumber ].pixnumber
396:    						 = cube.pixel_number( rmin, rmax,
397:    									 cutp + 1, gmax, bmin, bmax );
398:    					++ corenumber;
399:    					//
400:    					core[ maxcore ].gmax = cutp;
401:    					core[ maxcore ].pixnumber
402:    						 = cube.pixel_number( rmin, rmax,
403:    									 gmin, cutp, bmin, bmax );
404:    				}
405:    				else {
406:    					// b成分でcoreを分割
407:    
408:    					// 分割するbをcutpに代入
409:    					cutp = cube.get_median_b( rmin, rmax, gmin, gmax,
410:    										 bmin, bmax );
411:    					if ( bmin > cutp ) break;
412:    					// 分割処理
413:    					core[ corenumber ] = new ColorCore();
414:    					core[ corenumber ].rmin = core[ maxcore ].rmin;
415:    					core[ corenumber ].rmax = core[ maxcore ].rmax;
416:    					core[ corenumber ].gmin = core[ maxcore ].gmin;
417:    					core[ corenumber ].gmax = core[ maxcore ].gmax;
418:    					core[ corenumber ].bmin = cutp + 1;
419:    					core[ corenumber ].bmax = core[ maxcore ].bmax;
420:    					core[ corenumber ].pixnumber
421:    						 = cube.pixel_number( rmin, rmax,
422:    									 gmin, gmax, cutp + 1, bmax );
423:    					++ corenumber;
424:    					//
425:    					core[ maxcore ].bmax = cutp;
426:    					core[ maxcore ].pixnumber 
427:    						= cube.pixel_number( rmin, rmax, gmin, gmax,
428:    										 bmin, cutp );
429:    				}
430:    			}
431:    		}
432:    
433:    		// ColorCubeから減色テーブルを作成する
434:    		int index_r[] = new int[ corenumber ];
435:    		int index_g[] = new int[ corenumber ];
436:    		int index_b[] = new int[ corenumber ];
437:    		
438:    		for ( int i = 0; i < corenumber; ++ i ) {
439:    			// 色はコアの中心とする
440:    			index_r[ i ] = ( core[ i ].rmin  + core[ i ].rmax ) / 2;
441:    			index_g[ i ] = ( core[ i ].gmin  + core[ i ].gmax ) / 2;
442:    			index_b[ i ] = ( core[ i ].bmin  + core[ i ].bmax ) / 2;
443:    		}
444:    
445:    		// 元の画像の色を減色する
446:    		int l, minl;
447:    		int rl, gl, bl;
448:    		int searchindex;
449:    		int newcolor;
450:    		
451:    		for ( y = 0; y < height; ++ y ) {
452:    			for ( x = 0; x < width; ++ x ) {
453:    				// (x,y)の色を取得
454:    				color = img.getRGB( x, y );
455:    
456:    				// 色をr,g,bに分解
457:    				r = ( color >> 16 ) & 0xff;
458:    				g = ( color >> 8 ) & 0xff;
459:    				b = color & 0xff;
460:    
461:    				// r,g,bに最も近い色をindexから検索
462:    				rl = r - index_r[ 0 ];
463:    				gl = g - index_g[ 0 ];
464:    				bl = b - index_b[ 0 ];
465:    				minl = rl * rl + gl * gl + bl * b;
466:    				searchindex = 0;
467:    
468:    				for ( int i = 1; i < corenumber; ++ i ) {
469:    					//
470:    					rl = r - index_r[ i ];
471:    					gl = g - index_g[ i ];
472:    					bl = b - index_b[ i ];
473:    					l = rl * rl + gl * gl + bl * bl;
474:    					if ( l < minl ) {
475:    						minl = l;
476:    						searchindex = i;
477:    					}
478:    				}
479:    				
480:    				// 最も近い色をr,g,bに設定
481:    				r = index_r[ searchindex ];
482:    				g = index_g[ searchindex ];
483:    				b = index_b[ searchindex ];
484:    				
485:    				// r,g,bの色を合成
486:    				newcolor = ( r << 16 ) + ( g << 8 ) + b;
487:    
488:    				// 合成した色を(x,y)に設定
489:    				img.setRGB( x, y, newcolor );
490:    			}
491:    		}
492:    
493:    		// 色を変更した画像を保存
494:    		try {
495:    			// imgをoutname(出力JPEG)に保存
496:    			result = ImageIO.write( img, "jpeg", new File( outname ) );
497:    		} catch ( Exception e ) {
498:    			// outname(出力JPEG)の保存に失敗したときの処理
499:    			e.printStackTrace();
500:    			return;
501:    		}
502:    
503:    		// 正常に終了
504:    		System.out.println( "正常に終了しました" );
505:    	}
506:    } 

MedianCutを実行

>java MedianCut gundam-001.jpg gundam-mediancut-004.jpg 4

1つ目の引数で元のJPEGファイル名、2つ目の引数に作成するJPEGファイル名、3つ目に色数を指定します。上記の実行例では、減色後の色数を4にしています。

実行結果

・元の画像(gundam-001)

元画像(ガンダム)

・4色で作成した画像(gundam-mediancut-004.jpg)

メディアンカット(4色)メディアンカット(4色)

・8色で作成した画像(gundam-mediancut-008.jpg)

メディアンカット(8色)メディアンカット(8色)

・16色で作成した画像(gundam-mediancut-016.jpg)

メディアンカット(16色)メディアンカット(16色)

・256色で作成した画像(gundam-mediancut-256.jpg)

メディアンカット(256色)メディアンカット(256色)

色数が多いほど、見た目が元の画像に近づきます。

このプログラムでは、減色結果をJPEG形式で保存しているので、変換後のRGB値は指定した色数より多くなります。PNG形式GIF形式などの可逆圧縮を使った画像形式で保存すると指定した色数になります。また、減色した色をフルカラーの画像に格納するように作っているので変換後の画像もフルカラーのままです。パレット情報は登録していません。

■関連コンテンツ

減色 減色について
光と色の3原色 光と色の3原色の考え方を解説
画像の色 画像の色について解説
画像ファイル形式 画像ファイル形式について解説
while文 繰り返し処理に使用するwhile文について解説
コマンドライン引数 外部からの値を受け取る方法について解説

■新着情報

2017.11.17 N値化 カラー画像をN値化する方法について解説
2017.11.16 最も近い値の取得 指定値に最も近い配列の値を取得する方法を解説
2017.10.02 アルファ値(透過) アルファ値(透過)について

■広告

法人向けのETC専用カード

~約8,000名の受講生と80社以上の導入実績~ 企業向けプログラミング研修ならCodeCamp

日本最大級ショッピングサイト!お買い物なら楽天市場

Topへ