そして、勾配について教えてください!



最終結果の画面



この記事では、Photoshopのようにグラデーションを描く個人用自転車を発明した方法について説明します。 すぐに警告しますが、アルゴリズムは非常に遅く、最適化されていません。 記事の第2部で行う一般的な勾配アルゴリズムの最適化と検討



なんで?



どういうわけか、私は可能な限りPhotoshopに似たグラデーションのソフトウェアレンダリングを実装したいと考えました。 私には具体的な目標がなかったので、夕方には興味深い仕事をしました。 言語としてJavaが選択されました。 重要なアイデアは、他の人のアルゴリズムを覗くことなく、自分でこのアルゴリズムを書きたいということでした。





何が起こったのか


drawGradient()メソッドは次のように機能する必要があります。2つのポイントの座標と色を設定し、その後、画像全体にグラデーションを描画します。 このようなもの:





この図では、ポイントAの座標(55; 20)と色0xff2e2e2eがあり、ポイントBの座標(175; 180)と色0xffb5b5b5があります。 原点が左上隅にあり、Y軸が下向きであることを忘れないでください。



理解し始める


参考として、最後のスクリーンショットのPhotoshopからグラデーションを取りました。 ご覧のとおり、グラデーションは3つの部分で構成されています。



赤い部分は点Aの色で塗りつぶされ、緑の部分は点Bで塗りつぶされ、残りの領域の各ピクセルの色は、そこから線cおよびdまでの距離に応じて計算されます。



任意のピクセルから線cおよびdまでの距離を決定するアルゴリズムが必要であることは明らかだと思います。 また、「赤」領域にあるピクセル、「緑」領域にあるピクセル、および残りの領域にあるピクセルを判別する方法も必要です。



学校の幾何学コースを思い出して、ランダムなピクセルEについて次の図を描きます。





この図では、AFはピクセルEからラインaまでの距離です。 そして、それに応じて、FBはラインbまでの距離です。 ピクセルの色を決定するのはこれらの距離です。 そして、ピクセルがどの領域に属するかを決定する問題が解決されます。 ここではすべてが非常に簡単です。 AF + FB> ABの場合、ピクセルは赤または緑のゾーンにあります。 どちらを決定するには、AFとFBを比較します。 AF> FBの場合、ピクセルは緑のゾーンにあり、そうでない場合は赤にあります。 ここに数学があります。



だから、私たちの仕事はAFとBFを見つけることです。 AFに焦点を当てます。ピタゴラスの定理により、次のことがわかります。





したがって、ポイントAとEの座標がわかっているため、同じピタゴラスの定理から長さAEの2乗を学習できます。次のようになります。





EFのみを見つけることが残っています。 それは少し難しいですが、それは大丈夫です。 セグメントEFは三角形の高さであり、AB側に下げられているので、高さを見つけるための式が役立ちます。 次のようになります。





pは最も誤解を招くものの1つです。 これは境界ではなく、半周です。 これについて学校で何度か失敗したことを覚えています。 次のように考慮されます。





座標に基づいて、AEと同じ方法でABとEBを計算します。



そのため、AF計算アルゴリズムが一目でわかります。

1. AE、EB、ABを計算します

2. pを計算します

3. EFを計算する

4. AFを計算します



BFのアルゴリズムは似ていますが、説明しません。



逃げる時間です!


BufferedImageのラッパーを表すクラスを作成することにしました。それをEditableImageと呼びましょう。 そして、このクラスでは、私の推定によると、次のメソッドが必要でした。

EditableImage(int width, int height); // void clear(int color); //       void drawGradient(int x1, int y1, int color1, int x2, int y2, int color2); //    BufferedImage getImage(); //    
      
      





以降、私のコードのすべての色はintとして設定され、次の形式になります。

 0xAARRGGBB AA -   (   32    ) RR -    GG -    BB -   
      
      





クラスのアイデアは、グラデーション以外のチップを後で実装したい場合に簡単に行えるという点で便利です。



グラデーションに関係のない補助部品から始めましょう

Gradient.java-プログラムエントリポイント
 package ru.idgdima.gradient; import javax.swing.*; public class Gradient { public static final int IMG_WIDTH = 640; public static final int IMG_HEIGHT = 480; private static GradientPanel panel; //,      public static void main(String[] args) { // ,          //   JFrame frame = new JFrame("Test"); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setResizable(false); //     ,    , //     ,     //     panel = new GradientPanel(IMG_WIDTH, IMG_HEIGHT); frame.add(panel); frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } }
      
      







GradientPanel.java-グラデーションが表示されるパネル
 package ru.idgdima.gradient; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; public class GradientPanel extends JPanel { private BufferedImage image; public GradientPanel(int width, int height) { //       super(); setPreferredSize(new Dimension(width, height)); // ,    ,  //        BufferedImage   //    EditableImage gradientImage = new EditableImage(width, height); gradientImage.clear(0xff000000); gradientImage.drawGradient(55, 20, 0xff2e2e2e, 175, 180, 0xffb5b5b5); image = gradientImage.getImage(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); //     .     // : g.drawImage(image, 0, 0, null); } }
      
      







EditableImage.java-最も興味深いクラス、グラデーションのメソッドはまだ空です
 package ru.idgdima.gradient; import java.awt.image.BufferedImage; public class EditableImage { private int width; private int height; private int[] rgb; //        BufferedImage image; //       getImage public EditableImage(int width, int height) { this.width = width; this.height = height; rgb = new int[width * height]; //    //       getImage image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); } /** *        * @param color   */ public void clear(int color) { for (int i = 0; i < rgb.length; i++) { rgb[i] = color; } } /** *     * @return */ public BufferedImage getImage() { //    rgb  image image.setRGB(0, 0, width, height, rgb, 0, width); return image; } public void drawGradient(int x1, int y1, int color1, int x2, int y2, int color2) { //  } }
      
      









このプログラムは、この段階ですでにコンパイルできますが、黒い絵が表示されます。 勾配のメソッドを書く時が来ました!



私はこのようになった:

drawGradient
 public void drawGradient(int x1, int y1, int color1, int x2, int y2, int color2) { float dx = x1 - x2; //  float dy = y1 - y2; float AB = (float) Math.sqrt(dx * dx + dy * dy); //    //    for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { dx = x1 - x; dy = y1 - y; float AE2 = dx * dx + dy * dy; float AE = (float) Math.sqrt(AE2); dx = x2 - x; dy = y2 - y; float EB2 = dx * dx + dy * dy; float EB = (float) Math.sqrt(EB2); float p = (AB + AE + EB) / 2f; float EF = 2 / AB * (float)Math.sqrt(Math.abs(p * (p - AB) * (p - AE) * (p - EB))) float EF2 = EF * EF; float AF = (float) Math.sqrt(Math.abs(AE2 - EF2)); float BF = (float) Math.sqrt(Math.abs(EB2 - EF2)); if (AF + BF - 0.1f > AB) { //        rgb[y * width + x] = AF < BF ? color1 : color2; } else { //   float progress = AF / AB; //  interpolate    rgb[y * width + x] = interpolate(color1, color2, progress); } } } } /** * @param num -  * @return 0,  num < 0; 255,  num > 255;    num */ private static int clip(int num) { return num <= 0 ? 0 : (num >= 255 ? 255 : num); }
      
      









私はモジュール関数-Math.abs()を使用していることに注意してください。負の数が平方根関数-Math.sqrt()に分類される可能性が少なくともわずかでもあります。 そうでなければ、アーティファクトがあります。



この行で- 0.1f



を削除すると、ひどい混乱になります。 計算エラーのため、小さい数を減算する必要があります。

 if (AF + BF - 0.1f > AB) {
      
      







内挿法を理解することだけが残っており、問題は帽子の中にあります。 初期色、最終色、および進行状況を取得する必要があります。これは0から1までで、各色の割合を決定します。 たとえば、progress = 0の場合は初期色が返され、progress = 1が最終色であり、progress = 0.5が初期と最終の間の平均色です。 タスクは明確で、メソッドは次のように記述されています。

補間する
 private int interpolate(int color1, int color2, float progress) { //     int a1 = (color1 & 0xff000000) >>> 24; int r1 = (color1 & 0x00ff0000) >>> 16; int g1 = (color1 & 0x0000ff00) >>> 8; int b1 = color1 & 0x000000ff; int a2 = (color2 & 0xff000000) >>> 24; int r2 = (color2 & 0x00ff0000) >>> 16; int g2 = (color2 & 0x0000ff00) >>> 8; int b2 = color2 & 0x000000ff; //   float progress2 = (1 - progress); int newA = clip((int) (a1 * progress2 + a2 * progress)); int newR = clip((int) (r1 * progress2 + r2 * progress)); int newG = clip((int) (g1 * progress2 + g2 * progress)); int newB = clip((int) (b1 * progress2 + b2 * progress)); //     return (newA << 24) + (newR << 16) + (newG << 8) + newB; }
      
      









結果を見てみましょう!






すでに悪くはありませんが、私たちは線形補間を使用しており、Photoshopでは間違いなく他のものが使用されています。



補間について


写真を注意深く見てください。 左のグラデーションはアルゴリズムによって描画され、右はPhotoshopによって描画されます。 各行には赤い点があります。 そして、線が濃いほど、左のポイント:



ご覧のとおり、ラインはレールのように直線です。 修正する必要があります。 ここで私は賢明なものを思い付かず、インターネットをスパイすることにしました。 ハブに関する記事を見つけました。この記事では、いくつかのタイプの補間について説明しています。コードもhabrahabr.ru/post/142592です。



さて、コサイン補間を実装します! まず、EditableImageクラスに2つの定数を追加します。

 public static final int INTERPOLATION_LINEAR = 0; public static final int INTERPOLATION_COS = 1;
      
      





次に、これらの定数の1つがそれに供給されるように、補間メソッドを少し書き換えます。

非表示のテキスト
 private int interpolate(int color1, int color2, float progress, int interpolation) { //     int a1 = (color1 & 0xff000000) >>> 24; int r1 = (color1 & 0x00ff0000) >>> 16; int g1 = (color1 & 0x0000ff00) >>> 8; int b1 = color1 & 0x000000ff; int a2 = (color2 & 0xff000000) >>> 24; int r2 = (color2 & 0x00ff0000) >>> 16; int g2 = (color2 & 0x0000ff00) >>> 8; int b2 = color2 & 0x000000ff; //   float f; if (interpolation == INTERPOLATION_LINEAR) { f = progress; } else if (interpolation == INTERPOLATION_COS) { float ft = progress * 3.1415927f; f = (1 - (float) Math.cos(ft)) * 0.5f; } else { throw new IllegalArgumentException(); } int newA = clip((int) (a1 * (1 - f) + a2 * f)); int newR = clip((int) (r1 * (1 - f) + r2 * f)); int newG = clip((int) (g1 * (1 - f) + g2 * f)); int newB = clip((int) (b1 * (1 - f) + b2 * f)); //     return (newA << 24) + (newR << 16) + (newG << 8) + newB; }
      
      







次に、 int interpolation



をdrawGradientメソッドのパラメーターのリストに追加し、この変数をinterpolateメソッドの呼び出し行に追加します。

 rgb[y * width + x] = interpolate(color1, color2, progress , interpolation);
      
      





最後に、GradientPanelクラスで、drawGradientの呼び出しを書き換えて、INTERPOLATION_COSを追加します



この補間方法で何が起こったのかを次に示します。



うーん、見た目は良いのですが、Photoshopの線は明らかにその曲線ではありません。 どうする? それはあまりにも直接的で、曲がりすぎています...そして、これらの両極端の中間を作るとどうなりますか?



素晴らしいアイデア、定数INTERPOLATION_COS_LINEAR = 2



追加します



そして、補間メソッドのコードで、次の場合は別の別のものを追加します。

 } else if (interpolation == INTERPOLATION_COS_LINEAR) { float ft = progress * 3.1415927f; f = (progress + (1 - (float) Math.cos(ft)) * 0.5f) / 2f; }
      
      





そして見よ、それはPhotoshopからのグラデーションのほぼ完全なコピーであることが判明しました!

自分で見てください:



左。



そして、上のスクリーンショットの2つの写真を1つにまとめました。 補間はほぼ同じであり、丸め中のエラーの違いが原因であることがわかります。





ほら、Photoshopのようにグラデーションを作成しましたが、かなり遅くなりました。

ソースとjar



これが最初の部分を終えるところです。 2番目の部分では、できる限りクールにアルゴリズムを最適化しようとします。 そして、グラディエントを描画するための既製の有名な高速アルゴリズムを実装します。それから、速度を自分のものと比較します。



ところで、グラデーションを描くための高速アルゴリズムを念頭に置いている場合は、コメントに残してください。



All Articles