3

Graphics.DrawImageビットマップのストレッチと描画に使用するWinformsアプリケーションがあり、ソースピクセルが宛先にどのようにマップされるかを正確に理解するための支援が必要です。

理想的には、次のような関数を記述したいと思います。

 Point MapPixel(Point p, Size src, Size dst)

これは、ソース画像のピクセル座標を取得し、スケーリングされた宛先のそれに対応する「左上」ピクセルの座標を返します。

わかりやすくするために、2x2ビットマップが4x4にスケーリングされる簡単な例を次に示します。

ズームの例

矢印は、ポイント(1,0)をMapPixelにフィードする方法を示しています。

MapPixel(new Point(1, 0), new Size(2, 2), new Size(4, 4))

(2,0)の結果を与える必要があります。

次のようなロジックを使用して、上記の例でMapPixelを機能させるのは簡単です。

double scaleX = (double)dst.Width / (double)src.Width;
x_dst = (int)Math.Round((double)x_src * scaleX);

dst.Widthただし、この単純な実装では、がの倍数ではない場合、丸めが原因でエラーが発生することに気付きましたsrc.Width。この場合、DrawImageは、画像をフィットさせるために他のピクセルよりも大きく描画するためにいくつかのピクセルを選択する必要があり、そのロジックを複製するのに問題があります。

次のコードは、2x1ビットマップをいくつかの異なる幅にスケーリングすることで問題を示しています。

Bitmap src = new Bitmap(2, 1);
src.SetPixel(0, 0, Color.Red);
src.SetPixel(1, 0, Color.Blue);

Bitmap[] dst = {
  new Bitmap(3, 1),
  new Bitmap(5, 1),
  new Bitmap(7, 1),
  new Bitmap(9, 1)};

// Draw stretched images
foreach (Bitmap b in dst) {
  using (Graphics g = Graphics.FromImage(b)) {
    g.InterpolationMode = InterpolationMode.NearestNeighbor;
    g.PixelOffsetMode = PixelOffsetMode.Half;
    g.DrawImage(src, 0, 0, b.Width, b.Height);
  }
}

元のsrc画像と出力dst画像は次のようになり、MapPixelが青いピクセルをどのようにマッピングする必要があるかを示すいくつかの数字が表示されます。

スケーリングの例

DrawImageがどのピクセルを大きくするかをどのように決定するかを私は一生理解できません。切り上げることもあれば、切り下げることもあるようです。どちらを選択するかは気にしませんが、関数が正しく機能するためには予測可能である必要があります。

上記のMapPixelの例を使用するように変更してみましたが、最も近い奇数MidpointRounding.AwayFromZeroに丸める関数に置き換えました(結果はわずかに改善されますが、それでも完全ではありません)。また、Graphicsクラスにスケーリングを処理させてみました。つまり、設定し、呼び出してから、を使用して座標を変換しようとしました。興味深いことに、TransformPointsメソッドの結果は、DrawImageおよびDrawImageUnscaledの結果とも常に一致するとは限りません。Math.RoundScaleTransformDrawImageUnscaledTransformPoints

また、ヒントを得るためにGDI +を掘り下げてみましたが、まだ有用なものは何も見つかりませんでした。

着地する場所の予測可能性を維持するためだけに、各ピクセルを個別にペイントする必要はありません。

ご参考までに、私が使用している理由はInterpolationMode.NearestNeighborアンチエイリアシングを回避するためであり(個々のピクセルの忠実度を維持する必要があります)、PixelOffsetMode.Halfそこにない場合はDrawImageがビットマップを0.5ピクセルパンするため含まれています。

問題点のさらにいくつかの例には、4pxから13pxにスケーリングする場合のx = 7、および4pxから17pxにスケーリングする場合のx=8が含まれます。

必要に応じて、完全な単体テストコードを投稿できます。これにより、MapPixel関数をドロップインして検証できます。これまでのところ、100%の精度を達成できた唯一の方法は、各ピクセルが一意の色に設定された「ヒント」ソース画像を生成する醜いハックを使用することです。ヒント画像の色を確認し、拡大縮小したヒント画像でその色を探すことで、座標をマッピングします。最適化は可能です(たとえば、ヒント画像は1ピクセルの幅または高さであり、上記の単純なロジックを使用しておおよその答えを推測し、そこから外側に向かって作業します)が、それでも醜いです。

誰かがDrawImageの背後にある配管に光を当てて、MapPixelのより単純な(しかしまだ正確な)実装を考え出すのを手伝ってくれたら幸いです。

4

1 に答える 1

0

有理数 (小学校で教えられるような分数に相当するもの) を使用して整数演算を試してください。丸めの問題を回避します。

これを 1 つの軸で行います (以下の N で表されます)。

元の位置は [0,origN] の範囲内と見なすことができます ターゲット (スケーリングされた) 位置は [0,destN] の範囲内と見なすことができます

つまり、元の位置を次のように合理的に表すことができます。

 origPos                    destPos
---------  for orig, and,  --------- for dest
  origN                      destN

1 つをスケーリングするには、dest 軸で反復し、ソース位置を計算する最後の分周まで整数として格納される可能性がある有理数の同等の分数を使用します::

for current_dest_position in range(destLength):
    required_source_position=floor( (current_dest_position*sourceN)/destN )

srcN と destN は常に合計幅よりも 1 少ない (位置 0 が有効なピクセルであることに関係している) ため、ソースの長さは 16 で、dest の長さは 64 で、srcN は 15 で destN は 63 (range(k)上記の演算子は [0,k-1] を反復処理します)。言語が整数除算を強制する簡単な方法を提供しない場合、フロアが必要です。これは、C/C++ でダック型の値 (javascript、php、lua、python など) を使用するほとんどの言語です。除算を int にキャストするだけです。と:

required_source_position=(int)((current_dest_position*sourceN)/destN);

これは、1 つの軸でプロセスを説明します。他の軸のネストされたループを使用して他の軸に簡単に使用でき、上記の例の N を軸 (X、Y、Z など) に置き換えます。

于 2011-03-29T21:42:07.207 に答える