libtiff: 12bit/tile/JPEG圧縮ファイルの読み込み

非常にまれにこのフォーマットに出くわすことがあります。
JPEGファイルが12bitをサポートしているためで、読み込めるソフトもないことからあまり使われません。

以下の説明は筆者の調査と並行して書かれた文章でして、無駄な説明が多いです。注意してください。

まずはjpeglibの設定

jpeglibは12bitの画像をはっきりとサポートしており、jmorecfg.hのところで設定することで8bitモードと12bitモードを切り替えることができます。

jmorecfg.h、14~27行目:

/*
 * Define BITS_IN_JSAMPLE as either
 *   8   for 8-bit sample values (the usual setting)
 *   12  for 12-bit sample values
 * Only 8 and 12 are legal data precisions for lossy JPEG according to the
 * JPEG standard, and the IJG code does not support anything else!
 * We do not support run-time selection of data precision, sorry.
 */

#ifdef _12BIT_MODE
  #define BITS_IN_JSAMPLE 12  /* use 8 or 12 */
#else
  #define BITS_IN_JSAMPLE  8
#endif

こうしておいて、コンパイラオプションに”/D:_12BIT_MODE”を指定すれば、jpeglibの12bit版が出来上がります。
なお、現時点では8bitと12bitの両方には対応できません。

12bit/tile/jpegが読めない

しかし、jpeg/tileのTIFFファイルはJPEG/TILEのTIFF読み込み方法+上記の設定のままでは正しいデータを返してきません。
libtiff側から帰ってくるバッファには12bit分のデータが書き込まれているようなので、たとえば以下のようにして3byte(24bit)を2pixelに変換しても、データはぐちゃぐちゃになっています。

union
{
  BYTE  btBuf[3];                //  24bit
  DWORD  dw2Pix;                  //  32bit
} uPix;

while ( i-- > 0 )
{
  j = uiCols;
  while ( (j -= 3) >= 0 )
  {
    uPix.btBuf[2] = *pIn++;
    uPix.btBuf[1] = *pIn++;
    uPix.btBuf[0] = *pIn++;
    //上位12bit取り出し
    *pOut++ = (WORD)((uPix.dw2Pix & 0x00FFF000) >> 12);
    //下位12bit取り出し
    *pOut++ = (WORD)((uPix.dw2Pix & 0x00000FFF));
  }
  pOut += nOutSkew;
  pIn += nInSkew;
}

libtiffが返してくるデータ

圧縮があろうとなかろうと、タイルのデータを読み込む関数はTIFFReadTile()関数で、TIFF構造体での設定で内部で処理が別れています。
この関数が返してくるデータのサイズは12bit分のタイルサイズで、必要なデータは返しているように見えますが、実は1/4が欠落しています。
タイルサイズが256×256、RGBの場合、TIFFReadTile()がバッファに書き込んだデータをWORD配列にして縦256pix、横256×192(=256×3/4))のビットマップに保存すると、正しい絵が得られます。
つまり、横方向の1/4が欠落していることがわかります。

jpeglibが返してくるデータ

これは、jpeglibが返してくるデータには12bitのデータがぎっちり詰まっているのではなく、2バイト(WORD)に1ピクセルのデータが詰め込まれた配列を返していることを意味しています。
jpeglibは、はっきりと12bit/pixelのデータをサポートしているので、上のデータを正しく読めないのはそのデータを受け取るlibtiff側に問題があるものと思われます。
libtiffがjpeglibからデータを読み込んでいる箇所は、tif_jpeg.cのJPEGDecode()関数内です。

/*
 * Decode a chunk of pixels.
 * "Standard" case: returned data is not downsampled.
 */
/*ARGSUSED*/ static int
JPEGDecode(TIFF* tif, tidata_t buf, tsize_t cc, tsample_t s)
{
  JPEGState *sp = JState(tif);
  tsize_t nrows;

  nrows = cc / sp->bytesperline;
  if (cc % sp->bytesperline)
    TIFFWarning(tif->tif_name, "fractional scanline not read");

  if( nrows > (int) sp->cinfo.d.image_height )
    nrows = sp->cinfo.d.image_height;

  /* data is expected to be read in multiples of a scanline */
  if (nrows)
  {
    do {
      JSAMPROW bufptr = (JSAMPROW)buf;

      if (TIFFjpeg_read_scanlines(sp, &bufptr, 1) != 1)  -------------ここ!
        return (0);
      ++tif->tif_row;
      buf += sp->bytesperline;
      cc -= sp->bytesperline;
    } while (--nrows > 0);
  }
  /* Close down the decompressor if we've finished the strip or tile. */
  return sp->cinfo.d.output_scanline < sp->cinfo.d.output_height
    || TIFFjpeg_finish_decompress(sp);
}

TIFFjpeg_read_scanlines()関数は、libjpegのjpeg_read_scanlines()をコールするラッパです。
調べてみると、jpeglibからかえって来たデータの総量はタイル1ライン当たりtilewidth×sampleperpixel×sizeof(WORD)であることが判明しました。
したがって、jpeglibは1タイルのすべてのデータを返していることになります。

原因はtif構造体

ここで、上のコードをよく見てみると、タイルを1行読み込んだ後、バッファのポインタを1ライン分進めている箇所があります。

/*
 * Decode a chunk of pixels.
 * "Standard" case: returned data is not downsampled.
 */
/*ARGSUSED*/ static int
JPEGDecode(TIFF* tif, tidata_t buf, tsize_t cc, tsample_t s)
{
  JPEGState *sp = JState(tif);
  tsize_t nrows;

  nrows = cc / sp->bytesperline;
  if (cc % sp->bytesperline)
    TIFFWarning(tif->tif_name, "fractional scanline not read");

  if( nrows > (int) sp->cinfo.d.image_height )
    nrows = sp->cinfo.d.image_height;

  /* data is expected to be read in multiples of a scanline */
  if (nrows)
  {
    do {
      JSAMPROW bufptr = (JSAMPROW)buf;

      if (TIFFjpeg_read_scanlines(sp, &bufptr, 1) != 1)
        return (0);
      ++tif->tif_row;
      buf += sp->bytesperline;  -------------ここ!
      cc -= sp->bytesperline;
    } while (--nrows > 0);
  }
  /* Close down the decompressor if we've finished the strip or tile. */
  return sp->cinfo.d.output_scanline < sp->cinfo.d.output_height
    || TIFFjpeg_finish_decompress(sp);
}

上記のとおり、jpeglibは1ピクセル=16bitとしてデータをバッファに書き込んでいますが、
sp->bytesperlineにはtilewidth×sampleperpixel×1.5(=12bit)の値が入っているので、上の箇所でバッファのポインタはその分しか進みません。
ということは次のラインを読み込むときに、前のラインの後ろ1/4を上書きしてしまうことになります。

■■■■■■■■■■■■■■■■■■■■■■■■■■■ ←前のラインのデータ

bufは最初ここを指している。

■■■■■■■■■■■■■■■■■■■■■■■■■■■
□□□□□□□□□□□□□□□□□□□  ↑12bit分しか進まないのでここを指してしまう

正しく進めばここを指すはずだが、、

■■■■■■■■■■■■■■■■■■■■■○○○○○○
○○○○○○○○○○○○○        ↑その結果、前の列の後ろ1/4を上書きして、次のラインを書き込んでしまう。

sp->bytesperlineに値がセットされるのはJPEGPreDecode()(tif_jpeg.cの638~792)内。
ここで、sp->bytesperline = TIFFTileRowWidth()となっています。
TIFFTileRowWidth()は、td->td_bitspersample * td->td_tilewidthを8(=1byte)で割った値を返します。
ここで使われている値はTIFFOpen()で開いたときにセットされる(内部でさらにTIFFReadDirectory()を呼び出していて、そこでセットされる)ため、td->td_bitspersampleには12bitのTIFであれば当然12が入っています。
そこで、td->td_bitspersampleの値を、読み込んだ後で12⇒16にすれば上記の現象を回避できるものと思われますが、、。

libtiffを書き換える

JPEG/TILEのTIFF読み込み方法と同じように、
読み込んだ直後にTIFFSetField()を使ってTIFF構造体をセットしてみます。

if ( *btBitsPerSample == 12 )
{
  TIFFSetField( pTif, TIFFTAG_BITSPERSAMPLE, 16 );
}

こうするとtd->td_bitspersample == 16にはなります。
ところが、こんどはJPEGPreDecode()内の後のところでsp->cinfo.d.data_precision != td_td_bitspersampleがあって、ここを通れません。
ここは、画像データのピクセル深度をチェックしているところで、jpeglibをコンパイルするときに決める値と比較しています。
その結果”Improper JPEG data precision”のエラーが出てしまって止まります。
そこで、強引ですがこの部分をコメントにしてこのチェックをしないようにすると、今度は読み込んだタイルの行の後ろ4分の1がなくなってしまいます。
再びJPEGDecode()を調べてみると、nrowsの値が本来の行数の3/4になっているためで、
タイルデータの3/4を読み込んだところで読み込みを終了してしまうようになっていました。

/*
 * Decode a chunk of pixels.
 * "Standard" case: returned data is not downsampled.
 */
/*ARGSUSED*/ static int
JPEGDecode(TIFF* tif, tidata_t buf, tsize_t cc, tsample_t s)
{
  JPEGState *sp = JState(tif);
  tsize_t nrows;

  nrows = cc / sp->bytesperline;
  if (cc % sp->bytesperline)
    TIFFWarning(tif->tif_name, "fractional scanline not read");

  if( nrows > (int) sp->cinfo.d.image_height )
    nrows = sp->cinfo.d.image_height;

  /* data is expected to be read in multiples of a scanline */
  if (nrows)
  {
    do {
      JSAMPROW bufptr = (JSAMPROW)buf;

      if (TIFFjpeg_read_scanlines(sp, &bufptr, 1) != 1)
        return (0);
      ++tif->tif_row;
      buf += sp->bytesperline;
      cc -= sp->bytesperline;  -------------ここ!
    } while (--nrows > 0);
  }
  /* Close down the decompressor if we've finished the strip or tile. */
  return sp->cinfo.d.output_scanline < sp->cinfo.d.output_height
    || TIFFjpeg_finish_decompress(sp);
}

nrowsJPEGDecode()関数内のnrows = cc / sp->bytesperlineによって計算されています。
ここでccは引数で、TIFFReadEncodedTile()関数から渡されています。
その引数となる値sizeは同じ関数内でsize = tilesizeで代入されています。
さらにtilesizeは同じ関数内でtsize_t tilesize = tif->tif_tilesize;で代入されています。

tif_read.c内、

/*
 * Read a tile of data and decompress the specified
 * amount into the user-supplied buffer.
 */
tsize_t
TIFFReadEncodedTile(TIFF* tif, ttile_t tile, tdata_t buf, tsize_t size)
{
  TIFFDirectory *td = &tif->tif_dir;
  tsize_t tilesize = tif->tif_tilesize;  -------------ここでタイルサイズを代入

  if (!TIFFCheckRead(tif, 1))
    return (-1);
  if (tile >= td->td_nstrips) {
    TIFFError(tif->tif_name, "%ld: Tile out of range, max %ld",
      (long) tile, (u_long) td->td_nstrips);
    return (-1);
  }
  if (size == (tsize_t) -1)
    size = tilesize;  -------------sizeにtilesizeを代入
  else if (size > tilesize)
    size = tilesize;
    ------------ここ!(*tif->tif_decodetile)はJPEGDecode()関数へのポインタ。
    -------------JPEG/TILEのTIFF読み込み方法を参照
  if (TIFFFillTile(tif, tile) && (*tif->tif_decodetile)(tif,  
    (tidata_t) buf, size, (tsample_t)(tile/td->td_stripsperimage))) {
    (*tif->tif_postdecode)(tif, (tidata_t) buf, size);
    return (size);
  } else
    return (-1);
}

というわけで、もとをただせばTIFF構造体のtif->tif_tilesizeが12bitの値のままなのが原因ということになります。

読み込みはTIFFReadEncodedTile()で

tif->tif_tilesizeの値を16bitの値にするために、
TIFFSetField()を使ってtif->tif_tilesizeの値をセットする方法もあるかもしれませんし、
TIFFを開くときにTIFFClientOpen()を使用して設定をこちらで指定しつつファイルを開く方法もあるかもしれませんが、
ここではTIFFReadEncodedTile()を直接クライアントプログラムから呼び出す方法をとります。

TIFFReadEncodedTile()をよく見ると、size=tilesizeが実行されるのは、引数sizeに-1を入れて呼び出した場合で、
TIFFReadTile()から呼び出すとこうなります。
TIFFReadTile()は、読み込み位置などの正当性をチェックした後でTIFFReadEncodedTile()を呼び出しているだけなので、
クライアントプログラムから直接TIFFReadEncodedTile()を呼び出してもさほど不都合はありません。
ここでは以下のようにしてTIFFReadEncodedTile()を呼び出します。

  for ( nRow = 0; nRow < uiImageLength; nRow += uiTileLength )
  {
    uiNRow = ( nRow+uiTileLength > uiImageLength ) ? 
                              uiImageLength-nRow : uiTileLength;

    for ( nCol = 0; nCol < uiImageWidth; nCol += uiTileWidth )
    {
      if ( TIFFReadEncodedTile( pTif, TIFFComputeTile(pTif, nCol, nRow, 0, 0),
          pwTileBuf, uiTileWidth * uiTileLength * btSamplePerPixel * sizeof(WORD) )
                                                     < 0 )
      {
        GlobalUnlock( hMem );
        _TIFFfree( pwTileBuf );
        return false;
      }


ところが、これでも出てくるデータは変わりません。
よく見るとTIFFReadEncodedTile()内にはもうひとつチェックがあって、引数のsizetif->tif_tilesizeを超えている場合、
size=tif->tif_tilesizeが代入されてしまいます。

/*
 * Read a tile of data and decompress the specified
 * amount into the user-supplied buffer.
 */
tsize_t
TIFFReadEncodedTile(TIFF* tif, ttile_t tile, tdata_t buf, tsize_t size)
{
  TIFFDirectory *td = &tif->tif_dir;
  tsize_t tilesize = tif->tif_tilesize;

  if (!TIFFCheckRead(tif, 1))
    return (-1);
  if (tile >= td->td_nstrips) {
    TIFFError(tif->tif_name, "%ld: Tile out of range, max %ld",
      (long) tile, (u_long) td->td_nstrips);
    return (-1);
  }
  if (size == (tsize_t) -1)
    size = tilesize;
  else if (size > tilesize)
    size = tilesize;  -------------ここでもsizeにtilesizeを代入
  if (TIFFFillTile(tif, tile) && (*tif->tif_decodetile)(tif,
    (tidata_t) buf, size, (tsample_t)(tile/td->td_stripsperimage))) {
    (*tif->tif_postdecode)(tif, (tidata_t) buf, size);
    return (size);
  } else
    return (-1);
}

このチェックは意味があまりないと思われるのですが、とりあえずコメントアウトして実行すると、ようやく正しい12bitのデータを読み出すことができます。

tiff-3.8.2では?

このバージョンではJPEG、12bitの形式をサポートしており、コンパイル時に/D:JPEG_LIB_MK1を指定すればそのまま読み込むことができます。
しかし、きっちりサポートしているため、12bitの場合、3バイトに2ピクセルを格納して返してきます。
中身を見てみると、JPEGDecode()内でjpeglibが返してきたピクセルデータ(2バイト1ピクセル)をわざわざつめなおしているようです。
メモリを節約したい場合はこのままをお勧めしますが、取り扱いが面倒なので従来どおり2バイト1ピクセルで受け取りたい場合は以下のようにします。

JPEGPreDecode()

以下の部分を全てコメントアウト

#ifdef JPEG_LIB_MK1
  if (12 != td->td_bitspersample && 8 != td->td_bitspersample) {
      TIFFErrorExt(tif->tif_clientdata, module, "Improper JPEG data precision");
      return (0);
  }
    sp->cinfo.d.data_precision = td->td_bitspersample;
//    sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
#else
  if (sp->cinfo.d.data_precision != td->td_bitspersample) {
      TIFFErrorExt(tif->tif_clientdata, module, "Improper JPEG data precision");
      return (0);
  }
#endif*/
JPEGDecode()内

改造後は以下のようにする。

/*ARGSUSED*/ static int
JPEGDecode(TIFF* tif, tidata_t buf, tsize_t cc, tsample_t s)
{
  JPEGState *sp = JState(tif);
  tsize_t nrows;
  (void) s;

  nrows = cc / sp->bytesperline;
  if (cc % sp->bytesperline)
    TIFFWarningExt(tif->tif_clientdata, tif->tif_name, "fractional scanline not read");

  if( nrows > (int) sp->cinfo.d.image_height )
    nrows = sp->cinfo.d.image_height;

  /* data is expected to be read in multiples of a scanline */
  if (nrows)
  {/*
    JSAMPROW line_work_buf = NULL;*/

    /*
    ** For 6B, only use temporary buffer for 12 bit imagery. 
    ** For Mk1 always use it. 
    *//*
#if !defined(JPEG_LIB_MK1)    
    if( sp->cinfo.d.data_precision == 12 )
#endif
    {
      line_work_buf = (JSAMPROW) 
        _TIFFmalloc(sizeof(short) * sp->cinfo.d.output_width 
              * sp->cinfo.d.num_components );
    }
*/
    do {/*
      if( line_work_buf != NULL )
      {*/
        /* 
        ** In the MK1 case, we aways read into a 16bit buffer, and then
        ** pack down to 12bit or 8bit.  In 6B case we only read into 16
        ** bit buffer for 12bit data, which we need to repack. 
        *//*
        if (TIFFjpeg_read_scanlines(sp, &line_work_buf, 1) != 1)
          return (0);

        if( sp->cinfo.d.data_precision == 12 )
        {
          int value_pairs = (sp->cinfo.d.output_width 
                     * sp->cinfo.d.num_components) / 2;
          int iPair;

          for( iPair = 0; iPair < value_pairs; iPair++ )
          {
            unsigned char *out_ptr = 
              ((unsigned char *) buf) + iPair * 3;
            JSAMPLE *in_ptr = line_work_buf + iPair * 2;

            out_ptr[0] = (in_ptr[0] & 0xff0) >> 4;
            out_ptr[1] = ((in_ptr[0] & 0xf) << 4)
              | ((in_ptr[1] & 0xf00) >> 8);
            out_ptr[2] = ((in_ptr[1] & 0xff) >> 0);
          }
        }
        else if( sp->cinfo.d.data_precision == 8 )
        {
          int value_count = (sp->cinfo.d.output_width 
                     * sp->cinfo.d.num_components);
          int iValue;

          for( iValue = 0; iValue < value_count; iValue++ )
          {
            ((unsigned char *) buf)[iValue] = 
              line_work_buf[iValue] & 0xff;
          }
        }
      }
      else
      {*/
        /*
        ** In the libjpeg6b 8bit case.  We read directly into the 
        ** TIFF buffer.
        */
        JSAMPROW bufptr = (JSAMPROW)buf;
  
        if (TIFFjpeg_read_scanlines(sp, &bufptr, 1) != 1)
          return (0);
//      }

      ++tif->tif_row;
      buf += sp->bytesperline;
      cc -= sp->bytesperline;
    } while (--nrows > 0);
/*
    if( line_work_buf != NULL )
      _TIFFfree( line_work_buf );*/
  }

  /* Close down the decompressor if we've finished the strip or tile. */
  return sp->cinfo.d.output_scanline < sp->cinfo.d.output_height
    || TIFFjpeg_finish_decompress(sp);
}

あとは、TIFFReadEncodedTileのバッファチェックの部分をコメントアウトすればOK。

12bit版と8bit版の両方を共存させたい場合は、まず8bit版でヘッダを調べてビット深度等を読み取って読み込み関数を決定しますが、
このバージョンのライブラリではTIFFOpenでData Precisionが違うといって怒られます。
これは、タグの読み取りのときにTIFFScanlineSize()が呼び出されますが、この関数内でJPEG圧縮時のカラーモードがYCbCrのときに対処するための
機能が追加されているためで、この部分でビット数が調べられているためです。
仕方がないのでこの部分をコメントアウトして以下のように書き換えます。

tsize_t
TIFFScanlineSize(TIFF* tif)
{
  TIFFDirectory *td = &tif->tif_dir;
  tsize_t scanline;
  
  if (td->td_planarconfig == PLANARCONFIG_CONTIG) {
/*    if (td->td_photometric == PHOTOMETRIC_YCBCR
      && !isUpSampled(tif)) {
      uint16 ycbcrsubsampling[2];

      TIFFGetField(tif, TIFFTAG_YCBCRSUBSAMPLING, 
           ycbcrsubsampling + 0,
           ycbcrsubsampling + 1);

      if (ycbcrsubsampling[0] == 0) {
        TIFFErrorExt(tif->tif_clientdata, tif->tif_name,
             "Invalid YCbCr subsampling");
        return 0;
      }

      scanline = TIFFroundup(td->td_imagewidth,
               ycbcrsubsampling[0]);
      scanline = TIFFhowmany8(multiply(tif, scanline,
               td->td_bitspersample,
               "TIFFScanlineSize"));
      return ((tsize_t)
        summarize(tif, scanline,
            multiply(tif, 2,
            scanline / ycbcrsubsampling[0],
            "TIFFVStripSize"),
            "TIFFVStripSize"));
    } else {*/
      scanline = multiply(tif, td->td_imagewidth,
            td->td_samplesperpixel,
            "TIFFScanlineSize");
//    }
  } else
    scanline = td->td_imagewidth;
  return ((tsize_t) TIFFhowmany8(multiply(tif, scanline,
            td->td_bitspersample,
            "TIFFScanlineSize")));
}
アーカイブ