桂沢湖
Home > Document > 正距円筒画像の変形について

公開:2010/11/19

正距円筒画像を立方体表面にマッピングする際の画像変形について

U, V の話

画像をポリゴンにマッピングするとき、空間内のポリゴンの座標 (x, y, z) と 画像上の座標 (u, v) が必要になります。画像上の座標はポリゴンのそれと 区別するために画像の左上を原点とし、右側を u + , 下側を v + で表します。 四角形のポリゴンであれば、ポリゴンの座標と対応させる画像上の座標がそれぞれ 4個必要になります。

FMS では、以下のように指定します。

------------------------------------------------------------------

QUADS // 四角形を指定

TEXTURE takikawa200906_21 // 画像ファイル名

// u v x y z

0.25 0.25 707 0 707 // u, v, x, y, z の順

0.25 63.875 831 0 556

63.875 63.875 815 -162 556

63.875 0.25 694 -138 707

------------------------------------------------------------------

正距円筒画像の特徴

舟形多円錐図法(というらしいです)

正距円筒画像

正距円筒画像は、地球儀に張られている分割画像を平面に敷き詰めて、その隙間を 補完するような形に変形された画像といえます。

画像を眺めた時、赤道付近は実際の見え方に近いですが、上下45度付近 から次第に形が怪しくなって極付近は極端に横に広がった画像になります。

画像の左端を東経0度とすると、中心は東経180度。右端はまた0度と なりますが、画像の上辺では北極点が広がり、下辺は南極点が広がっている といった具合です。

ところが、この画像の左上を基準とする U, V 値が、

球の中心から球の表面の各点を見たときの北極点を基準とする角度とリニアに 対応しているという特徴があります。

たとえば、8192 * 4096 ピクセルの正距円筒画像上の点 ( U, V ) = ( 2048, 512 )には、 球の北極点から右に90度、下に22.5度の地点にあるピクセルが描画されています。

θU = 360 * 2048 / 8192 = 90, θV = 180 * 512 / 4096 = 22.5

キューブへの変換

上の画像は、キューブへ切り出すために変形した画像です。 わかりやすくするために赤い切取線を入れてあります。

この状態から上の三角形を組み合わせて _top 画像にします。 下の三角形が _bottom 画像になり、中央には左から _left、_front、_right が並び、左右の半幅の画像を組み合わせて _back 画像にします。

変形の実際をわかりやすくするために、赤と青の格子模様の画像を変形したものを 以下に示します。

この画像から、立方体各面の球と接する部分は画像が縮小され、 球との距離が大きくなる辺の部分では画像が拡大されていることがわかります。 また、頂点付近では拡大の度合いがより大きくなっています。

変換の考え方

前述の、ピクセル値から角度を求める式から、 正距円筒画像の幅を W, 高さを H としたとき、画像上の点 (U, V)の角度 θU, θV は 以下の式で求められます。

θU = 360 * U / W ... (1)

θV = 180 * V / H ... (2)

また、角度からピクセル値を求める場合は

U = W * θU / 360 ... (3)

V = H * θV / 180 ... (4)

キューブに置き換えるときは、球に接する立方体の各面のすべてのピクセル について、球の中心からの角度を求め、それを上の式で U, V 変換すれば、 そこへもって来るべき正距円筒画像上のピクセルの位置が得られます。

変形後の画像は、下図のように緑の線で同じ形に4分割することが出来ますが、 その画像も赤の線で同じ形に4分割することが出来ます。 角度を求める計算は全体の16分の1になる赤い字の1の領域だけでよいことになります。 また、空白となる三角形部分は計算から除外しても良いこともわかります。

求めた計算結果を1から2へ反転コピーし、1と2から3と4へ反転コピーすることで、 必要な計算結果を得られます。この計算結果を利用して、4分の1ずつ画像全体を処理します。

赤1の領域について、立方体表面から球の原点を見たときの立体的な画像を示します。 画像では、球の45度を示すラインが、立方体の上面に現れることがわかります。

実際の計算

角度を求める計算式を示します。もっとエレガントな方法があるような 気もしますが、上図の上の三角形と下の正方形に分けて考えます。 球の半径を R (正方形の1辺の長さ)とします。

上の三角形上の任意の点 P と球の原点 O の角度を求めます。

tan(θU) = X / Y より、

θU = tan-1 ( X / Y) ... (5)

AP = √( X2 + Y2)

tan(θV) = AP / R より、

θV = tan-1 ( √( X2 + Y2) / R) ... (6)

次に、下の正方形上の任意の点 P と球の原点 O の角度を求めます。

tan(θU) = X / R より、

θU = tan-1 ( X / R) ... (7)

QP = √( X2 + R2)

OQ = R - Y

tan(θV) = QP / OQ より、

θV = tan-1 ( √( X2 + R2) / (R - Y)) ... (8)

Delphi での実装例

以下は、pano2cube.exe で使われているサンプルコードです。 参考になればどうぞ。

-------------------------------------------------------------------------------------------
unit Pano2Cube;

interface

uses
  Windows, Classes, Graphics;

procedure TransformPano2Cube(Bitmap: TBitmap);

implementation

uses
  Math;

type
  TCurrencyUV = record
    iU: Integer;
    dU: Currency;
    iV: Integer;
    dV: Currency;
  end;
  TmxCurrencyUV = array of array of TCurrencyUV;


procedure InitmxUV(R: Integer; mx: TmxCurrencyUV);
var
  R2, R4: Integer;
  U, V: Integer;
  E: Extended;
begin
  R2 := R * 2;
  R4 := R * 4;

  // -1 でクリアする
  for V := 0 to R4 - 1 do
    for U := 0 to R2 - 1 do
    begin
      mx[U, V].iU := -1;
      mx[U, V].dU := -1;
      mx[U, V].iV := -1;
      mx[U, V].dV := -1;
    end;

  // 上面の三角形(1)
  for V := 0 to R - 1  do
    for U := 0 to V do
    begin
      // iU, dU
      if V = 0 then
      begin
        // 三角形の頂点
        mx[U, V].iU := 0;
        mx[U, V].dU := 0;
      end
      else
      begin
        E := (ArcTan(U / V) * 180 / Pi) * R / 45;
        mx[U, V].iU := Trunc(E);
        mx[U, V].dU := E - Trunc(E);
      end;
      // iV, dV
      E := (ArcTan(Sqrt(U * U + V * V) / R) * 180 / Pi) * R / 45;
      mx[U, V].iV := Trunc(E);
      mx[U, V].dV := E - Trunc(E);
    end;

  // 後面の四角形(2)
  for V := R to R2 - 1 do
    for U := 0 to R - 1 do
    begin
      // iU, dU
      E := (ArcTan(U / R) * 180 / Pi) * R / 45;
      mx[U, V].iU := Trunc(E);
      mx[U, V].dU := E - Trunc(E);
      // iV, dV
      E :=  (ArcTan(Sqrt(R * R + U * U) / (R2 - V)) * 180 / Pi) * R / 45;
      mx[U, V].iV := Trunc(E);
      mx[U, V].dV := E - Trunc(E);
    end;

  // コピーする
  for U := R to R2 - 1 do
    for V := 0 to R2 - 1 do
    begin
      // (1)(2)-> (3)(4)
      // 左右反転
      if mx[R2 - 1 - U, V].iU = -1 then
        mx[U, V].iU := -1
      else
      begin
        if mx[R2 - 1 - U, V].dU = 0 then
        begin
          // 小数点以下がゼロの場合
          mx[U, V].iU := R2 - 1 - mx[R2 - 1 - U, V].iU;
          mx[U, V].dU := mx[R2 - 1 - U, V].dU;
        end
        else
        begin
          // Bilinear 線形補間では左上から右下へ処理するので、
          // 補正する
          mx[U, V].iU := R2 - 1 - mx[R2 - 1 - U, V].iU - 1;
          mx[U, V].dU := 1 - mx[R2 - 1 - U, V].dU;
        end;
      end;
      mx[U, V].iV := mx[R2 - 1 - U, V].iV;
      mx[U, V].dV := mx[R2 - 1 - U, V].dV;
    end;
  for U := 0 to R2 - 1 do
    for V := R2 to R4 - 1 do
    begin
      // (2)(4)-> (5)(7)
      // (1)(3)-> (6)(8)
      // 上下反転
      mx[U, V].iU := mx[U, R4 - 1 - V].iU;
      mx[U, V].dU := mx[U, R4 - 1 - V].dU;
      if mx[U, R4 - 1 - V].dV = 0 then
      begin
        // 小数点以下がゼロの場合
        mx[U, V].iV := R4 - 1 - mx[U, R4 - 1 - V].iV;
        mx[U, V].dV := mx[U, R4 - 1 - V].dV;
      end
      else
      begin
        // 線形補間用補正
        mx[U, V].iV := R4 - 1 - mx[U, R4 - 1 - V].iV - 1;
        mx[U, V].dV := 1 - mx[U, R4 - 1 - V].dV;
      end;
    end;
end;

{ ---------------------------------------------------------
  #MovePixels

  元画像の4分の1を立方体画像へ変形させる

  http://www.rainorshine.asia/2008/05/16/post428.html
  による Bilinear 線形補間を利用する


  // windows.pas

  tagRGBQUAD = packed record
    rgbBlue: Byte;
    rgbGreen: Byte;
    rgbRed: Byte;
    rgbReserved: Byte;
  end;
  TRGBQuad = tagRGBQUAD;
  PRGBQuad = ^TRGBQuad;


  // classes.pas

  PPointerList = ^TPointerList;
  TPointerList = array[0..MaxListSize - 1] of Pointer;

  --------------------------------------------------------- }

type
  // for pf32bit
  TQuadLine= array[0..MaxInt div SizeOf(TRGBQuad) - 1] of TRGBQuad;
  PQuadLine = ^TQuadLine;


  // for pf24bit
  TRGBTri = packed record
    rgbBlue: Byte;
    rgbGreen: Byte;
    rgbRed: Byte;
  end;
  PRGBTri = ^TRGBTri;

  TTriLine = array[0..MaxInt div SizeOf(TRGBTri) - 1] of TRGBTri;
  PTriLine = ^TTriLine;

procedure MovePixels(Dest, Source: TBitmap; mx: TmxCurrencyUV);
var
  W, H, U, V: Integer;

  pSourceLines, pDestLines: PPointerList; // Classes.pas
  PixelFormat: TPixelFormat;

  dq, sq00, sq01, sq10, sq11: TRGBQuad;
  dt, st00, st01, st10, st11: TRGBTri;

  sx0, sx1, sy0, sy1: Integer;
  rx0, rx1, ry0, ry1: Currency;

  r0, r1: Currency;
  g0, g1: Currency;
  b0, b1: Currency;


begin
  PixelFormat := Source.PixelFormat;
  W := Source.Width;
  H := Source.Height;

  Dest.Width := W;
  Dest.Height := H;
  Dest.PixelFormat := PixelFormat;

  GetMem(pDestLines, H * SizeOf(Pointer));
  try
    GetMem(pSourceLines, H * SizeOf(Pointer));
    try
      // scanline
      for V := 0 to H - 1  do
      begin
        pDestLines[V] := Dest.ScanLine[V];
        pSourceLines[V] := Source.ScanLine[V];
      end;

      for V := 0 to H - 1 do
      begin

        for U := 0 to W - 1 do
          if mx[U, V].iU <> -1 then
          begin
            sy0 := mx[U, V].iV;            // V データ整数部
            sy1 := Min(sy0 + 1, H - 1);    // 1ピクセル下 の V
            sx0 := mx[U, V].iU;            // U データ整数部
            sx1 := Min(sx0 + 1, W - 1);    // 1ピクセル右の U
            ry1 := mx[U, V].dV;            // 1ピクセル下から取得する割合
            ry0 := 1 - ry1;                // V から取得する割合
            rx1 := mx[U, V].dU;            // 1ピクセル右から取得する割合
            rx0 := 1 - rx1;                // U から取得する割合
            case PixelFormat of
              pf32bit:
                begin
                  {----------------------------------------------------
                  // 整数部だけの処理
                  // PQuadLine(pDestLines^[V])^[U] := PQuadLine(pSourceLines^[mx[U, V].iV])^[mx[U, V].iU];

                      Dest                       Source
                                                   sx0     sx1
                        U                          iU      iU + 1
                      *-------*                  *-------*-------*
                   V  | dq    |     sy0  iV      | sq00  | sq10  |
                      *-------*                  *-------*-------*
                                    sy1  iV + 1  | sq01  | sq11  |
                                                 *-------*-------*
                   ----------------------------------------------------}
                  // dq := PQuadLine(pDestLines^[V])^[U];
                  sq00 := PQuadLine(pSourceLines^[sy0])^[sx0];
                  sq01 := PQuadLine(pSourceLines^[sy1])^[sx0];
                  sq10 := PQuadLine(pSourceLines^[sy0])^[sx1];
                  sq11 := PQuadLine(pSourceLines^[sy1])^[sx1];

                  // R, G, B を右隣と合成
                  r0 := sq00.rgbRed * rx0 + sq10.rgbRed * rx1;
                  r1 := sq01.rgbRed * rx0 + sq11.rgbRed * rx1;

                  g0 := sq00.rgbGreen * rx0 + sq10.rgbGreen * rx1;
                  g1 := sq01.rgbGreen * rx0 + sq11.rgbGreen * rx1;

                  b0 := sq00.rgbBlue * rx0  + sq10.rgbBlue * rx1;
                  b1 := sq01.rgbBlue * rx0  + sq11.rgbBlue * rx1;

                  dq.rgbRed   := Min($FF, Round((r0 * ry0 + r1 * ry1)));
                  dq.rgbGreen := Min($FF, Round((g0 * ry0 + g1 * ry1)));
                  dq.rgbBlue  := Min($FF, Round((b0 * ry0 + b1 * ry1)));

                  PQuadLine(pDestLines^[V])^[U] := dq;

                end;
              pf24bit:
                begin
                  {----------------------------------------------------
                  // 整数部だけの処理
                  // PTriLine(pDestLines^[V])^[U] := PTriLine(pSourceLines^[mx[U, V].iV])^[mx[U, V].iU];

                      Dest                       Source
                                                   sx0     sx1
                        U                          iU      iU + 1
                      *-------*                  *-------*-------*
                   V  | dt    |     sy0  iV      | st00  | st10  |
                      *-------*                  *-------*-------*
                                    sy1  iV + 1  | st01  | st11  |
                                                 *-------*-------*
                   ----------------------------------------------------}
                  // dt := PQuadLine(pDestLines^[V])^[U];
                  st00 := PTriLine(pSourceLines^[sy0])^[sx0];
                  st01 := PTriLine(pSourceLines^[sy1])^[sx0];
                  st10 := PTriLine(pSourceLines^[sy0])^[sx1];
                  st11 := PTriLine(pSourceLines^[sy1])^[sx1];

                  // R, G, B を右隣と合成
                  r0 := st00.rgbRed * rx0 + st10.rgbRed * rx1;
                  r1 := st01.rgbRed * rx0 + st11.rgbRed * rx1;

                  g0 := st00.rgbGreen * rx0 + st10.rgbGreen * rx1;
                  g1 := st01.rgbGreen * rx0 + st11.rgbGreen * rx1;

                  b0 := st00.rgbBlue * rx0  + st10.rgbBlue * rx1;
                  b1 := st01.rgbBlue * rx0  + st11.rgbBlue * rx1;

                  dt.rgbRed   := Min($FF, Round((r0 * ry0 + r1 * ry1)));
                  dt.rgbGreen := Min($FF, Round((g0 * ry0 + g1 * ry1)));
                  dt.rgbBlue  := Min($FF, Round((b0 * ry0 + b1 * ry1)));

                  PTriLine(pDestLines^[V])^[U] := dt;

                end;
            end;
          end;
      end; // for V
    finally
      FreeMem(pSourceLines);
    end;
  finally
    FreeMem(pDestLines);
  end;
end;


procedure TransformPano2Cube(Bitmap: TBitmap);
var
  W, H, R, I: Integer;
  S, D: TBitmap;
  SR, DR: TRect;

  mx: TmxCurrencyUV;

begin
  W := Bitmap.Width div 4;
  H := Bitmap.Height;
  S := TBitmap.Create;
  try
    S.PixelFormat := pf24bit;
    S.Width := W;
    S.Height := H;
    D := TBitmap.Create;
    try
      D.PixelFormat := pf24bit;
      D.Width := W;
      D.Height := H;
      R := W div 2;
      SetLength(mx, R * 2, R * 4);
      try
        InitmxUV(W div 2, mx);

        for I := 0 to 3 do
        begin
          DR := S.Canvas.ClipRect;
          SR := Rect(W * I, 0, W * (I + 1), H);
          S.Canvas.CopyRect(DR, Bitmap.Canvas, SR);
          MovePixels(D, S, mx);
          Bitmap.Canvas.CopyRect(SR, D.Canvas, DR);
        end;
      finally
        Finalize(mx);
      end;
    finally
      D.Free;
    end;
  finally
    S.Free;
  end;
end;

end.
-------------------------------------------------------------------------------------------

        

Top
inserted by FC2 system