- PR -

小さな円を描画するといびつになる(Java2D の Ellipse2D や Component の createImage の使用時)

1
投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-10-21 14:52
unibon です。こんにちわ。

昔から気になっているのですが、
Java で小さな円を描画すると妙にいびつになる(歪む)ことがあるので悩んでいます。
つぎのコードで再現できます。

コード:

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.*;

public class JHelohelo extends JPanel {

public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.white);
g.fillRect(0, 0, getWidth(), getHeight());

// 上段(青、綺麗な形状)
g.setColor(new Color(0, 0, 127));
for (int i = 0; i < 20; i++) {
g.drawOval(i * 25 + 10, 0 + 10, i, i);
}

// 中段(赤、少しいびつ)
g.setColor(new Color(127, 0, 0));
for (int i = 0; i < 20; i++) {
Ellipse2D ellipse = new Ellipse2D.Float(i * 25 + 10, 40 + 10, i, i);
((Graphics2D) g).draw(ellipse);
}

// 下段(緑、とてもいびつ)
int width = getWidth();
int height = 40;
Image image = createImage(width, height);
Graphics gg = image.getGraphics();
gg.setColor(Color.white);
gg.fillRect(0, 0, width, height);
gg.setColor(new Color(0, 127, 0));
for (int i = 0; i < 20; i++) {
gg.drawOval(i * 25 + 10, 0 + 10, i, i);
}
gg.dispose();
g.drawImage(image, 0, 80, width, height, this);
}

public static void main(String[] args) {
final JFrame f = new JFrame();
f.addWindowListener(new WindowListener() {
public void windowOpened(WindowEvent e) {
}
public void windowClosing(WindowEvent e) {
f.dispose();
}
public void windowClosed(WindowEvent e) {
}
public void windowIconified(WindowEvent e) {
}
public void windowDeiconified(WindowEvent e) {
}
public void windowActivated(WindowEvent e) {
}
public void windowDeactivated(WindowEvent e) {
}
});
JPanel p = new JHelohelo();
f.getContentPane().add(p);
f.setSize(new Dimension(600, 200));
f.setVisible(true);
}
}


このコードは3通りの描画の仕方をおこなっています。
上段(青色)は古くからの AWT だけの機能で描画したものであり、綺麗です。
中段(赤色)は Java2D の Ellipse2D を使って描画したものですが、
少しいびつになっています。左右や上下が対称ではありません。
下段(緑色)は、Java2D を使わずに AWT だけを使っていますが、
これはダブルバッファのために java.awt.Component クラスの
createImage メソッドで得られた Image から Graphics を得て
それに対して描画しています。
これだと、とてもいびつになってしまいます。

Java2D の Ellipse2D は、おそらく内部的にパスを生成しているため、
小さいものを描画すると誤差がでているのだろうと推測しますので、
分からないでもありません(でも、できれば綺麗に描画してほしい)。

しかし、createImage で得たバッファに書くといびつになる理由が分かりません。
これはなぜなのでしょうか。
また、綺麗に描画できる回避方法はあるでしょうか。

試した環境は、Windows XP + JDK 1.4.2_01 です。
なお、java の起動時のオプションとして、
-Dsun.java2d.noddraw=true
を指定すると、形状が変わります。これも謎です。
以前はこれを指定すると良くなることもあったかもしれません(あいまいです)が、
1.4.2_01 だと悪くなるみたいでした。



#件名が長すぎたので修正しました。

[ メッセージ編集済み 編集者: unibon 編集日時 2003-10-21 14:56 ]
さくらば
大ベテラン
会議室デビュー日: 2002/11/12
投稿数: 145
投稿日時: 2003-10-21 17:32
こんにちは、さくらばです。

引用:

unibonさんの書き込み (2003-10-21 14:52) より:

昔から気になっているのですが、
Java で小さな円を描画すると妙にいびつになる(歪む)ことがあるので悩んでいます。

... 省略 ...

このコードは3通りの描画の仕方をおこなっています。
上段(青色)は古くからの AWT だけの機能で描画したものであり、綺麗です。
中段(赤色)は Java2D の Ellipse2D を使って描画したものですが、
少しいびつになっています。左右や上下が対称ではありません。
下段(緑色)は、Java2D を使わずに AWT だけを使っていますが、
これはダブルバッファのために java.awt.Component クラスの
createImage メソッドで得られた Image から Graphics を得て
それに対して描画しています。
これだと、とてもいびつになってしまいます。

Java2D の Ellipse2D は、おそらく内部的にパスを生成しているため、
小さいものを描画すると誤差がでているのだろうと推測しますので、
分からないでもありません(でも、できれば綺麗に描画してほしい)。



理由は複数あります。

1. JDK1.1 の描画ルーチンと Java2D の描画ルーチンは根本的に異なる
JDK1.1 では Windows に描画を任せているが、Java2D では自分で描画しています
2. Java2D ではユーザ空間とデバイス空間が切り離されている。
この両者間のマッピングを行うのが AffineTransform です。
ユーザ空間は座標が浮動小数点数で表されますが、デバイス空間は整数なので
丸め誤差などが発生します。
3. Java2D は円を複数のベジェ曲線で表します。
通常は 4 本のベジェで表していますが、小さいとベジェで表しても誤差が大
きくなってしまいます。(Unibon さんが予想されたことです)

drawOval は JDK1.1 の描画ルーチンがコールされるので、Windows が描画を行って
います。それで、きれいに描画できるようです。

Java2D では、例えば左上隅が(0, 0)の半径 5 の円は次のコードと同一になります。
小さい円をよく見ると右下のほうが誤差が大きいように見えますが、それは 3.8807118
の方が 1.119288 より丸め誤差が大きいためだと思います。

コード:
	GeneralPath path = new GeneralPath();
	path.moveTo(5.0f, 2.5f);
	path.curveTo(5.0f, 3.8807118f, 3.8807118f, 5.0f, 2.5f, 5.0f);
	path.curveTo(1.1192881f, 5.0f, 0.0f, 3.8807118f, 0.0f, 2.5f);
	path.curveTo(0.0f, 1.1192881f, 1.1192881f, 0.0f, 2.5f, 0.0f);
	path.curveTo(3.8807118f, 0.0f, 5.0f, 1.1192881f, 5.0f, 2.5f);
	path.closePath();



それではどうすればいいかというと、根本的ではありませんが RenderingHint
でアンチエイリアスをかける方法があります。

コード:
	((Graphics2D)g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
					  RenderingHints.VALUE_ANTIALIAS_ON);



引用:

しかし、createImage で得たバッファに書くといびつになる理由が分かりません。
これはなぜなのでしょうか。
また、綺麗に描画できる回避方法はあるでしょうか。



基本的にはイメージに描画する場合は Java2D が使用されるので、そこでまず
いびつになります。次に drawImage を行うときにやはり誤差が出るからでは
ないでしょうか。こちらは未確認です。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-10-22 17:04
unibon です。こんにちわ。

引用:

さくらばさんの書き込み (2003-10-21 17:32) より:
Java2D では、例えば左上隅が(0, 0)の半径 5 の円は次のコードと同一になります。
小さい円をよく見ると右下のほうが誤差が大きいように見えますが、それは 3.8807118
の方が 1.119288 より丸め誤差が大きいためだと思います。


ありがとうございます。
このような変換規則でパスに変換されているとは知りませんでした。

引用:

さくらばさんの書き込み (2003-10-21 17:32) より:
それではどうすればいいかというと、根本的ではありませんが RenderingHint
でアンチエイリアスをかける方法があります。


個人的な好みになってしまい申し訳ないのですが、
アンチエイリアシングのボヤっとした画像は好きではないので、
(私に限っては)これは使わないことにしたいと思ってます。
#巷ではアンチエイリアシングをむしろ好ましいと見る向きも多いので、
#これで解決するケースも多いと思いますが。

引用:

さくらばさんの書き込み (2003-10-21 17:32) より:
基本的にはイメージに描画する場合は Java2D が使用されるので、そこでまず
いびつになります。次に drawImage を行うときにやはり誤差が出るからでは
ないでしょうか。こちらは未確認です。


その後、気づいたのですが、実行環境によっては、たとえ、
paintComponent の引数の Graphics に対して直に drawOval しても、
いびつになることがあるようです(最初のサンプルコードの上段のケースでも)。
これは、どうやら DirectDraw が使えるかどうかで決まるようです。
createImage で得た Image から得られる Graphics についても同様です。

デバッガで追いかけてみたのですが、
DirectDraw が使えない(あるいは効率が悪そう)と判断すると、なにかにつけすぐに、
内部的に Ellipse2D などのパスを使う描画モード(これがいびつ)になってしまうようです。
#sun.awt.windows.WComponentPeer あたりなどで判断しているようです。
このような描画モードを直接、明示的に指定する方法がないようです。
なお、起動時のオプションとして
-Dsun.java2d.ddoffscreen=true
を指定すると、できるだけ DirectDraw を使うモードになるらしく、
いびつにならなくなります。
しかし、Windows の設定で DirectDraw を使わない設定になっているとダメでした。

ちなみに drawImage に関してはビットマップ転送(いわゆる BitBlt)なので、
これ単体ではいびつにはならないと思っています(が確認はしていません)。

なお、
http://www5.airnet.ne.jp/sakuraba/java/laboratory/JDK1.4/Graphics/VolatileImage/VolatileImage.html
を参考にさせていただき
createImage の代わりに createVolatileImage を使うことでも解決できました。
しかし、残念ながらこれも DirectDraw の設定に左右されるようであり、
どのような環境でも解決するまでには至っていません。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-10-23 23:39
unibon です。こんにちわ。

別のスレッド、
「drawRoundRectの角の丸みの設定について」
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=7038&forum=12
などを拝見して思ったのですが、
どうも、Ellipse2D や RoundRectangle2D の実装がヘボすぎる(小さい図形が苦手)、
と思えてきました。
DirectDraw のモードを気にするよりは、
これらの Shape の実装を改善する方向で検討してみたいと思います。
ボム
ベテラン
会議室デビュー日: 2003/07/25
投稿数: 61
投稿日時: 2003-10-24 11:48
unibonさんと同様の現象を体験し、同じようなスレッドを立ててしまったボムです。

unibonさんや、さくらばさんのご説明を読んで見たのですがイマイチ理解できませんでした。
結局、この現象を回避する策は無いのでしょうか?
Keisuke
大ベテラン
会議室デビュー日: 2003/10/24
投稿数: 105
投稿日時: 2003-10-24 12:53
以下のようにすると改善します。
コード:

Ellipse2D ellipse = new Ellipse2D.Float(i * 25 + 10.4999F, 40 + 10.4999F, i, i);




[ メッセージ編集済み 編集者: Keisuke 編集日時 2003-10-24 12:56 ]
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-10-24 18:05
unibon です。こんにちわ。

引用:

ボムさんの書き込み (2003-10-24 11:48) より:
結局、この現象を回避する策は無いのでしょうか?



さくらばさんがご指摘のように、アンチエイリアスをかける方法で、
どのような環境であっても、見栄えの違和感はほぼ回避できます。
ただし、アンチエイリアスによるボケという副作用はありますが。
#以下、アンチエイリアス以外の選択肢について。

もしも、私の最初のサンプルで、
上段(青)が綺麗で、下段(緑)がいびつな環境ならば(その環境に限定した回避策としては)、
私が以前に書いたやりかたである、
-Dsun.java2d.ddoffscreen=true
というオプションを起動時に指定するか、
あるいは createVolatileImage を使うやりかたのいずれかで回避できるでしょう。
しかし、このやりかたでは、それ以外の環境でも必ずしも回避できるとは限りません。

さらに、もしも、上段(青)の段階ですでにいびつな環境ならば、
Ellipse2D(drawOval メソッドが内部的に使うクラス)や
RoundRectangle2D(drawRoundRect メソッドが内部的に使うクラス)の振る舞いを、
そもそも改善する必要があります。
引用:

Keisukeさんの書き込み (2003-10-24 12:53) より:
以下のようにすると改善します。
コード:
Ellipse2D ellipse = new Ellipse2D.Float(i * 25 + 10.4999F, 40 + 10.4999F, i, i);




は、クラスはそのままで引数を調整して、
間接的に振る舞いを調整することになりますが、
Ellipse2D だとかなり効果があり、すごいワザですね。
ただ RoundRectangle2D ではどのような調整を与えればよいのかは
分かりませんでした。

もっとも確実なのは、
Ellipse2D や RoundRectangle2D と同じで精度の高いものを、
新たに作ってしまうことです。が、これがなかなか難しそうです。
RoundRectangle2D はまだ簡単そうですが、Ellipse2D 相当のものは難しそうです。

作りかけですが RoundRectangle2D もどき:
コード:
        GeneralPath gp = new GeneralPath();
        gp.moveTo(10, 80);
        gp.lineTo(10, 20);
        gp.curveTo(9, 15, 15, 9, 20, 10);
        gp.lineTo(80, 10);
        gp.curveTo(85, 9, 91, 15, 90, 20);
        gp.lineTo(90, 80);
        ((Graphics2D) gg).draw(gp);

さくらば
大ベテラン
会議室デビュー日: 2002/11/12
投稿数: 145
投稿日時: 2003-10-27 16:57
こんにちは、さくらばです。

引用:

unibonさんの書き込み (2003-10-24 18:05) より:

もっとも確実なのは、
Ellipse2D や RoundRectangle2D と同じで精度の高いものを、
新たに作ってしまうことです。が、これがなかなか難しそうです。
RoundRectangle2D はまだ簡単そうですが、Ellipse2D 相当のものは難しそうです。



半径が小さいときは逆に精度を落とす方がいいのではないでしょうか。
簡単にいえば強引に四捨五入してしまうというものです。

問題は小数部が切り捨てられてしまうことにあるので、切り捨てられる前に
四捨五入をしてしまうわけです。

java.awt.geom.EllipseIterator クラス (パッケージ内パブリックなクラス
なので JavaDoc などには出てきませんが、src.zip には入っています)
の currentSegment メソッドで return をする前に coords 配列の値を
四捨五入してしまいます。そして、このクラスだけを JAR にして、
-Xbootclasspath/p: で読み込ませるようにします。

これだけで、ずいぶんきれいになりますよ。

# 著作権とかライセンスの問題があるので、ここでコードを出すことができ
# ないのです。すいません。
1

スキルアップ/キャリアアップ(JOB@IT)