You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
597 lines
17 KiB
C++
597 lines
17 KiB
C++
/*=========================================================================
|
|
|
|
Program: Visualization Toolkit
|
|
Module: vtkOggTheoraWriter.cxx
|
|
|
|
Copyright (c) Michael Wild, Ken Martin, Will Schroeder, Bill Lorensen
|
|
All rights reserved.
|
|
See Copyright.txt or http://www.kitware.com/Copyright.htm for details.
|
|
|
|
This software is distributed WITHOUT ANY WARRANTY; without even
|
|
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
PURPOSE. See the above copyright notice for more information.
|
|
|
|
=========================================================================*/
|
|
|
|
#include "vtkOggTheoraWriter.h"
|
|
|
|
#include "vtkImageData.h"
|
|
#include "vtkObjectFactory.h"
|
|
#include "vtkErrorCode.h"
|
|
|
|
#include "vtk_oggtheora.h"
|
|
|
|
#include <ctime>
|
|
|
|
//---------------------------------------------------------------------------
|
|
class vtkOggTheoraWriterInternal
|
|
{
|
|
public:
|
|
vtkOggTheoraWriterInternal(vtkOggTheoraWriter *creator);
|
|
~vtkOggTheoraWriterInternal();
|
|
|
|
int Start();
|
|
int Write(vtkImageData *id);
|
|
void End();
|
|
|
|
int Dim[2];
|
|
int FrameRate;
|
|
|
|
private:
|
|
|
|
// Helper function to convert an RGB image into the Y'CbCr color space and
|
|
// into the data structure required by theora (i.e. 4:4:4 or 4:2:0
|
|
// subsampling will be used). Refer to http://www.theora.org/doc/Theora.pdf
|
|
// sections 4.3 and 4.3.2. Actually, the equations are inverted. However, I'm
|
|
// not sure whether VTK uses gamma-corrected RGB or not. I assume they are
|
|
// not, which is what we need here. Assume that the width and height are even
|
|
// numbers.
|
|
void RGB2YCbCr(vtkImageData *id, th_ycbcr_buffer ycbcr);
|
|
// Write the ogg/theora header information
|
|
int WriteHeader();
|
|
// Encode a single frame
|
|
int EncodeFrame(th_ycbcr_buffer ycbcr, int lastFrame);
|
|
|
|
vtkOggTheoraWriter *Writer;
|
|
|
|
size_t Off[2]; // offsets of the picture within the frame
|
|
th_enc_ctx *thEncContext; // the theora context (has to be freed)
|
|
th_ycbcr_buffer thImage; // the Y'CbCr image buffer
|
|
ogg_stream_state oggState; // the ogg stream state (has to be cleared)
|
|
FILE* outFile; // the output file stream
|
|
bool haveImageData; // indicater whether a frame has to be encoded
|
|
// (for the leap-frogging)
|
|
|
|
int openedFile;
|
|
int closedFile;
|
|
};
|
|
|
|
//---------------------------------------------------------------------------
|
|
vtkOggTheoraWriterInternal::vtkOggTheoraWriterInternal(vtkOggTheoraWriter *creator)
|
|
{
|
|
this->Writer = creator;
|
|
this->Dim[0] = 0;
|
|
this->Dim[1] = 0;
|
|
|
|
this->Off[0] = 0;
|
|
this->Off[1] = 0;
|
|
this->thEncContext = NULL;
|
|
this->outFile = NULL;
|
|
this->thImage[0].data = NULL;
|
|
this->thImage[1].data = NULL;
|
|
this->thImage[2].data = NULL;
|
|
|
|
this->openedFile = 0;
|
|
this->closedFile = 1;
|
|
this->haveImageData = false;
|
|
|
|
this->FrameRate = 25;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
vtkOggTheoraWriterInternal::~vtkOggTheoraWriterInternal()
|
|
{
|
|
if (!this->closedFile)
|
|
{
|
|
this->End();
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
int vtkOggTheoraWriterInternal::Start()
|
|
{
|
|
this->closedFile = 0;
|
|
|
|
// ogg information
|
|
srand(time(NULL));
|
|
if (ogg_stream_init(&this->oggState,rand())!=0)
|
|
{
|
|
vtkGenericWarningMacro("Could not initialize ogg stream state.");
|
|
return 0;
|
|
}
|
|
|
|
// fill in theora information
|
|
th_info thInfo;
|
|
th_info_init(&thInfo);
|
|
// frame_width and frame_height must be multiples of 16
|
|
thInfo.frame_width = (this->Dim[0]+15)&~0xF;
|
|
thInfo.frame_height = (this->Dim[1]+15)&~0xF;
|
|
thInfo.pic_width = this->Dim[0];
|
|
thInfo.pic_height = this->Dim[1];
|
|
// force even offsets of the picture within the frame
|
|
this->Off[0] = (thInfo.frame_width-this->Dim[0])>>1&~1;
|
|
this->Off[1] = (thInfo.frame_height-this->Dim[1])>>1&~1;
|
|
thInfo.pic_x = static_cast<ogg_uint32_t>(this->Off[0]);
|
|
thInfo.pic_y = static_cast<ogg_uint32_t>(this->Off[1]);
|
|
thInfo.colorspace = TH_CS_ITU_REC_470BG;
|
|
if (this->Writer->GetSubsampling())
|
|
{
|
|
// 4:2:0 subsampling is the only implemented option in libtheora-1.0
|
|
thInfo.pixel_fmt = TH_PF_420;
|
|
}
|
|
else
|
|
{
|
|
thInfo.pixel_fmt = TH_PF_444;
|
|
}
|
|
thInfo.target_bitrate = 0; // variable bitrate recording (default)
|
|
// allow a variable quality/size tradeoff
|
|
//! \todo still have to find appropriate quality parameters though...
|
|
//! valid values are in [0,63].
|
|
switch (this->Writer->GetQuality())
|
|
{
|
|
case 0:
|
|
thInfo.quality = 42;
|
|
break;
|
|
case 1:
|
|
thInfo.quality = 52;
|
|
break;
|
|
default:
|
|
thInfo.quality = 63;
|
|
break;
|
|
}
|
|
thInfo.keyframe_granule_shift = 6; // default value
|
|
// the frame rate (as a fraction)
|
|
thInfo.fps_numerator = this->FrameRate;
|
|
thInfo.fps_denominator = 1;
|
|
// pixel ascpect ratio
|
|
thInfo.aspect_numerator = 1;
|
|
thInfo.aspect_denominator = 1;
|
|
|
|
// create the theora encoder context
|
|
this->thEncContext = th_encode_alloc(&thInfo);
|
|
if (!this->thEncContext)
|
|
{
|
|
vtkGenericWarningMacro(<< "Could not allocate the theora context.");
|
|
return 0;
|
|
}
|
|
|
|
// create the theora buffer (do not cheat with the frame padding,
|
|
// allocate the whole thing!)
|
|
for (size_t i=0; i<3; ++i)
|
|
{
|
|
this->thImage[i].width = thInfo.frame_width;
|
|
this->thImage[i].height = thInfo.frame_height;
|
|
if (this->Writer->GetSubsampling() && i>0)
|
|
{
|
|
// Chroma planes are subsampled by a factor of 2
|
|
this->thImage[i].width /= 2;
|
|
this->thImage[i].height /= 2;
|
|
}
|
|
// the stride is in bytes
|
|
this->thImage[i].stride = this->thImage[i].width*sizeof(unsigned char);
|
|
// make sure there's nothing left laying around...
|
|
delete[] this->thImage[i].data;
|
|
// allocate the image plane
|
|
size_t siz = this->thImage[i].width * this->thImage[i].height;
|
|
this->thImage[i].data = new unsigned char[siz];
|
|
}
|
|
|
|
// thInfo is no longer needed
|
|
th_info_clear(&thInfo);
|
|
|
|
// Finally, open the file and start it off.
|
|
this->outFile = fopen(this->Writer->GetFileName(),"wb");
|
|
if (!this->outFile)
|
|
{
|
|
vtkGenericWarningMacro(<< "Could not open " << this->Writer->GetFileName() << "." );
|
|
return 0;
|
|
}
|
|
this->openedFile = 1;
|
|
|
|
return this->WriteHeader();
|
|
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// ripped from libtheora-1.0/examples/encoder_example.c
|
|
int vtkOggTheoraWriterInternal::WriteHeader()
|
|
{
|
|
th_comment thComment;
|
|
ogg_packet oggPacket;
|
|
ogg_page oggPage;
|
|
|
|
th_comment_init(&thComment);
|
|
|
|
// first packet will get its own page automatically
|
|
if (th_encode_flushheader(this->thEncContext,&thComment,&oggPacket)<=0)
|
|
{
|
|
vtkGenericWarningMacro("Internal Theora library error.");
|
|
return 0;
|
|
}
|
|
ogg_stream_packetin(&this->oggState,&oggPacket);
|
|
if (ogg_stream_pageout(&this->oggState,&oggPage)!=1)
|
|
{
|
|
vtkGenericWarningMacro("Internal Theora library error.");
|
|
return 0;
|
|
}
|
|
fwrite(oggPage.header,1,oggPage.header_len,this->outFile);
|
|
fwrite(oggPage.body,1,oggPage.body_len,this->outFile);
|
|
// remaining theora headers
|
|
int ret;
|
|
while (true)
|
|
{
|
|
ret=th_encode_flushheader(this->thEncContext,&thComment,&oggPacket);
|
|
if (ret<0)
|
|
{
|
|
vtkGenericWarningMacro("Internal Theora library error.");
|
|
return 0;
|
|
}
|
|
else if (!ret)
|
|
break;
|
|
ogg_stream_packetin(&this->oggState,&oggPacket);
|
|
}
|
|
// Flush the rest of our headers. This ensures
|
|
// the actual data in each stream will start
|
|
// on a new page, as per spec.
|
|
while (true)
|
|
{
|
|
ret = ogg_stream_flush(&this->oggState,&oggPage);
|
|
if (ret<0)
|
|
{
|
|
vtkGenericWarningMacro("Internal Theora library error.");
|
|
return 0;
|
|
}
|
|
if (ret==0)break;
|
|
fwrite(oggPage.header,1,oggPage.header_len,this->outFile);
|
|
fwrite(oggPage.body,1,oggPage.body_len,this->outFile);
|
|
}
|
|
|
|
th_comment_clear(&thComment);
|
|
|
|
return 1;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
int vtkOggTheoraWriterInternal::Write(vtkImageData *id)
|
|
{
|
|
// encode the frame from the last call.
|
|
// have to do leap-frogging, because otherwise we can't
|
|
// write the EOS page with the last frame in End().
|
|
int ret = 1;
|
|
if (this->haveImageData)
|
|
{
|
|
ret = this->EncodeFrame(this->thImage,0);
|
|
this->haveImageData = false;
|
|
}
|
|
|
|
this->Writer->GetInputAlgorithm(0, 0)->UpdateWholeExtent();
|
|
|
|
// convert current RGB int YCbCr color space
|
|
this->RGB2YCbCr(id,this->thImage);
|
|
this->haveImageData = true;
|
|
|
|
return ret;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// ripped from libtheora-1.0/examples/encoder_example.c
|
|
int vtkOggTheoraWriterInternal::EncodeFrame(th_ycbcr_buffer, int lastFrame)
|
|
{
|
|
if (th_encode_ycbcr_in(this->thEncContext,this->thImage)<0)
|
|
{
|
|
vtkGenericWarningMacro("Error encoding frame.");
|
|
return 0;
|
|
}
|
|
// retrieve and push packets, writing pages as required
|
|
ogg_packet oggPacket;
|
|
ogg_page oggPage;
|
|
int ret;
|
|
while ((ret=th_encode_packetout(this->thEncContext,lastFrame,&oggPacket)))
|
|
{
|
|
if(ret<0)
|
|
{
|
|
vtkGenericWarningMacro("Error retrieving packet from codec.");
|
|
return 0;
|
|
}
|
|
if (ogg_stream_packetin(&this->oggState,&oggPacket)<0)
|
|
{
|
|
vtkGenericWarningMacro("Error inserting packet into stream.");
|
|
return 0;
|
|
}
|
|
while (ogg_stream_pageout(&this->oggState,&oggPage))
|
|
{
|
|
fwrite(oggPage.header,1,oggPage.header_len,this->outFile);
|
|
fwrite(oggPage.body,1,oggPage.body_len,this->outFile);
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
void vtkOggTheoraWriterInternal::End()
|
|
{
|
|
// flush remaining frame
|
|
if (this->haveImageData)
|
|
if (!this->EncodeFrame(this->thImage,1))
|
|
vtkGenericWarningMacro("Failed to finish writing movie");
|
|
this->haveImageData = false;
|
|
|
|
// clean up
|
|
for (size_t i = 0; i < 3; ++i)
|
|
{
|
|
delete[] this->thImage[i].data;
|
|
this->thImage[i].data = NULL;
|
|
}
|
|
|
|
if (this->thEncContext)
|
|
{
|
|
th_encode_free(this->thEncContext);
|
|
this->thEncContext = NULL;
|
|
}
|
|
|
|
ogg_stream_clear(&this->oggState);
|
|
|
|
if (this->openedFile)
|
|
{
|
|
fclose(this->outFile);
|
|
this->openedFile = 0;
|
|
}
|
|
this->closedFile = 1;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
void vtkOggTheoraWriterInternal::RGB2YCbCr(vtkImageData *id,
|
|
th_ycbcr_buffer ycbcr)
|
|
{
|
|
// convenience
|
|
typedef unsigned char uchar;
|
|
|
|
//
|
|
// constant coefficiens
|
|
//
|
|
|
|
static const uchar OffY = 16, OffCr = 128, OffCb = 128;
|
|
// divide by 255, because the formulas use normalized RGB, i.e in [0,1]
|
|
static const double ExcurY = 219.0/255, ExcurCr = 224.0/255, ExcurCb = 224.0/255,
|
|
Kr = 0.299, Kb = 0.114;
|
|
// derived constants
|
|
static const double Kg = 1 - Kr - Kb,
|
|
Krm1 = Kr - 1,
|
|
Kbm1 = Kb - 1;
|
|
// stride between rows in the YCbCr image planes, since
|
|
// pixels in a row are contiguous, but rows need not be
|
|
static const int strideRGB = this->Dim[0]*3,
|
|
strideY = ycbcr[0].stride/sizeof(uchar), // th_image_plane strides are in bytes
|
|
strideCb = ycbcr[1].stride/sizeof(uchar),
|
|
strideCr = ycbcr[2].stride/sizeof(uchar);
|
|
//
|
|
// computation
|
|
//
|
|
|
|
// the first pixel in the RGB image
|
|
uchar *rgbStart = (uchar*)id->GetScalarPointer();
|
|
// pointers to iterate through the RGB image an the Y, Cb and Cr planes
|
|
uchar *rgb, *Y, *Cb=0, *Cr=0;
|
|
// indicators whether we have to handle chroma planes.
|
|
bool isXCPlane = false,
|
|
isYCPlane = true; // y-flipping
|
|
// loop over rows
|
|
size_t x, y, yC;
|
|
for (y = 0; y < static_cast<size_t>(this->Dim[1]); ++y)
|
|
{
|
|
if (this->Writer->GetSubsampling())
|
|
{
|
|
// reset x indicator and flip y indicator
|
|
isXCPlane = false;
|
|
isYCPlane = !isYCPlane;
|
|
}
|
|
// compute pointers to the first pixel in row y,
|
|
// flipping y coordinate
|
|
rgb = rgbStart + (this->Dim[1]-y-1) * strideRGB;
|
|
Y = ycbcr[0].data + (y+this->Off[1]) * strideY + this->Off[0];
|
|
if (!this->Writer->GetSubsampling())
|
|
{
|
|
Cb = ycbcr[1].data + (y+this->Off[1]) * strideY + this->Off[0];
|
|
Cr = ycbcr[2].data + (y+this->Off[1]) * strideY + this->Off[0];
|
|
}
|
|
else if (isYCPlane)
|
|
{
|
|
// compute y on chroma planes
|
|
yC = (y+this->Off[1])/2;
|
|
// pointers to first pixel in row yC of chroma planes
|
|
Cb = ycbcr[1].data + yC * strideCb + this->Off[0]/2;
|
|
Cr = ycbcr[2].data + yC * strideCr + this->Off[0]/2;
|
|
}
|
|
// loop over columns in row y
|
|
for (x = 0; x < static_cast<size_t>(this->Dim[0]); ++x)
|
|
{
|
|
// do the actual transformation
|
|
*Y = uchar((Kr * rgb[0] + Kg * rgb[1] + Kb *rgb[2]) *ExcurY + OffY);
|
|
if (!this->Writer->GetSubsampling())
|
|
{
|
|
*Cb = uchar((Kr * rgb[0] + Kg * rgb[1] + Kbm1 * rgb[2]) /
|
|
(2 * Kbm1) * ExcurCb + OffCb);
|
|
*Cr = uchar((Krm1 * rgb[0] + Kg * rgb[1] + Kb * rgb[2]) /
|
|
(2 * Krm1) * ExcurCr + OffCr);
|
|
}
|
|
else
|
|
{
|
|
// flip indicator
|
|
isXCPlane = !isXCPlane;
|
|
if (isYCPlane && isXCPlane)
|
|
{
|
|
/* REMARK: actually, interpolation seems to give worse results...
|
|
* just use the associated RGB pixel (a.k.a nearest neighbor).
|
|
*/
|
|
#if 0
|
|
// interpolate surrounding rgb (subsampling)
|
|
// use double in order to not lose too much precision...
|
|
double irgb[3];
|
|
for (size_t i = 0; i < 3; ++i)
|
|
{
|
|
irgb[i] = 0.25 * (rgb[i] + rgb[i+3] +
|
|
rgb[i+strideRGB] + rgb[i+3+strideRGB]);
|
|
}
|
|
*Cb = uchar((Kr * irgb[0] + Kg * irgb[1] + Kbm1 * irgb[2]) /
|
|
(2 * Kbm1) * ExcurCb) + OffCb;
|
|
*Cr = uchar((Krm1 * irgb[0] + Kg * irgb[1] + Kb * irgb[2]) /
|
|
(2 * Krm1) * ExcurCr) + OffCr;
|
|
#else
|
|
*Cb = uchar((Kr * rgb[0] + Kg * rgb[1] + Kbm1 * rgb[2]) /
|
|
(2 * Kbm1) * ExcurCb + OffCb);
|
|
*Cr = uchar((Krm1 * rgb[0] + Kg * rgb[1] + Kb * rgb[2]) /
|
|
(2 * Krm1) * ExcurCr + OffCr);
|
|
#endif
|
|
}
|
|
}
|
|
// advance to next pixel in row y
|
|
rgb += 3;
|
|
++Y;
|
|
if (!this->Writer->GetSubsampling())
|
|
{
|
|
++Cb;
|
|
++Cr;
|
|
}
|
|
else if (isYCPlane && isXCPlane)
|
|
{
|
|
++Cb;
|
|
++Cr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
vtkStandardNewMacro(vtkOggTheoraWriter);
|
|
|
|
//---------------------------------------------------------------------------
|
|
vtkOggTheoraWriter::vtkOggTheoraWriter()
|
|
{
|
|
this->Internals = 0;
|
|
this->Quality = 2;
|
|
this->Rate = 25;
|
|
this->Subsampling = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
vtkOggTheoraWriter::~vtkOggTheoraWriter()
|
|
{
|
|
delete this->Internals;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
void vtkOggTheoraWriter::Start()
|
|
{
|
|
this->Error = 1;
|
|
|
|
if ( this->Internals )
|
|
{
|
|
vtkErrorMacro("Movie already started.");
|
|
this->SetErrorCode(vtkGenericMovieWriter::InitError);
|
|
return;
|
|
}
|
|
if ( this->GetInput() == NULL )
|
|
{
|
|
vtkErrorMacro("Please specify an input.");
|
|
this->SetErrorCode(vtkGenericMovieWriter::NoInputError);
|
|
return;
|
|
}
|
|
if (!this->FileName)
|
|
{
|
|
vtkErrorMacro("Please specify a filename.");
|
|
this->SetErrorCode(vtkErrorCode::NoFileNameError);
|
|
return;
|
|
}
|
|
|
|
this->Internals = new vtkOggTheoraWriterInternal(this);
|
|
|
|
this->Error = 0;
|
|
|
|
this->Initialized = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
void vtkOggTheoraWriter::Write()
|
|
{
|
|
if (this->Error)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !this->Internals )
|
|
{
|
|
vtkErrorMacro("Movie not started.");
|
|
this->Error = 1;
|
|
this->SetErrorCode(vtkGenericMovieWriter::InitError);
|
|
return;
|
|
}
|
|
|
|
// get the data
|
|
vtkImageData* input = this->GetImageDataInput(0);
|
|
this->GetInputAlgorithm(0, 0)->UpdateWholeExtent();
|
|
|
|
int dim[4];
|
|
input->GetDimensions(dim);
|
|
if ( this->Internals->Dim[0] == 0 && this->Internals->Dim[1] == 0 )
|
|
{
|
|
this->Internals->Dim[0] = dim[0];
|
|
this->Internals->Dim[1] = dim[1];
|
|
}
|
|
|
|
if (this->Internals->Dim[0]!= dim[0] || this->Internals->Dim[1]!= dim[1])
|
|
{
|
|
vtkErrorMacro("Image not of the same size.");
|
|
this->Error = 1;
|
|
this->SetErrorCode(vtkGenericMovieWriter::ChangedResolutionError);
|
|
return;
|
|
}
|
|
|
|
if ( !this->Initialized )
|
|
{
|
|
this->Internals->FrameRate = this->Rate;
|
|
if (!this->Internals->Start())
|
|
{
|
|
vtkErrorMacro("Error initializing video stream.");
|
|
this->Error = 1;
|
|
this->SetErrorCode(vtkGenericMovieWriter::InitError);
|
|
return;
|
|
}
|
|
this->Initialized = 1;
|
|
}
|
|
|
|
if (!this->Internals->Write(input))
|
|
{
|
|
vtkErrorMacro("Error storing image.");
|
|
this->Error = 1;
|
|
this->SetErrorCode(vtkErrorCode::UnknownError);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
void vtkOggTheoraWriter::End()
|
|
{
|
|
this->Internals->End();
|
|
|
|
delete this->Internals;
|
|
this->Internals = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
void vtkOggTheoraWriter::PrintSelf(ostream& os, vtkIndent indent)
|
|
{
|
|
this->Superclass::PrintSelf(os, indent);
|
|
os << indent << "Quality: " << this->Quality << endl;
|
|
os << indent << "Rate: " << this->Rate << endl;
|
|
os << indent << "Subsampling: " << this->Subsampling << endl;
|
|
}
|