Commit ef9b47b9 authored by David Flynn's avatar David Flynn Committed by David Flynn
Browse files

cli: add support for encoding pointcloud sequences

This commit enables the codec to encapsulate more than one frame in the
coded bitstream.  The number of frames to encode is controlled by the
frameCount option.

Support is added to encode multiple source frames from separate ply
files based on a template input file name.  A printf-like "%d" directive
acts as a placeholder for the current frame number.

Decoding functions in a similar way with a each frame decoded from the
bitstream being written to a separate ply file named according to a
template pattern.

The first frame number used for file I/O may be set using the
firstFrameNum option.
parent 105120bd
......@@ -23,7 +23,7 @@
## Running
This TMC13 codec implementation encodes single frames. A single binary
This TMC13 codec implementation encodes frame sequences. A single binary
contains the encoder and decoder implementation, with selection using
the `--mode` option. Documentation of options is provided via the
`--help` command line option.
......
......@@ -16,9 +16,30 @@ encoding functionality. A value of 1 switches to decoding mode.
I/O parameters
--------------
### `--firstFrameNum=INT-VALUE`
The initial frame number of the input or output sequence.
The software replaces any instance of a '%d' printf format directive
with the current frame number when evaluating the following options:
- uncompressedDataPath
- reconstructedDataPath
- postRecolourPath
- preInvScalePath
NB: When decoding, this option relates only to the output file names.
In order to have the decoder produce identically numbered output ply
files as the encoder input, specify the same value of firstFrameNum for
the decoder.
### `--frameCount=INT-VALUE`
(Encoder only)
The number of frames to be encoded.
### `--uncompressedDataPath=FILE`
(Encoder only)
The input source point cloud to be compressed.
The input source point cloud to be compressed. The first instance of
'%d' in FILE will be expanded with the current frame number.
### `--compressedStreamPath=FILE`
The compressed bitstream file output when encoding or input when decoding.
......@@ -28,6 +49,9 @@ The reconstructed point cloud file. When encoding, the output is the
locally decoded picture. It is expected that the reconstructed output
of the encoder and decoder match exactly.
The first instance of '%d' in FILE will be expanded with the current
frame number.
### `--postRecolourPath=FILE`
(Encoder only)
As part of the encoding process, it may be necessary to re-colour the
......@@ -35,6 +59,9 @@ point cloud if the point geometry is altered. This diagnostic output
file corresponds to the re-coloured point cloud prior to attribute
coding without output geometry scaling.
The first instance of '%d' in FILE will be expanded with the current
frame number.
### `--preInvScalePath=FILE`
(Decoder only)
This diagnostic output corresponds to the decoded point cloud (geometry
......@@ -44,6 +71,9 @@ When compared to the output of `postRecolourPath`, the performance of
attribute coding may be directly measured without being confounded
by any geometry losses.
The first instance of '%d' in FILE will be expanded with the current
frame number.
### `--outputBinaryPly=0|1`
Sets the output format of PLY files (Binary=1, ASCII=0). Reading and
writing binary PLY files is more efficient than the ASCII variant,
......
......@@ -5,16 +5,16 @@ Using the codec
./tmc3 [--help] [-c config.cfg] [--parameter=value]
```
The encoder takes as input a PLY file describing a point cloud with
integer positions and, optionally, per-point integer colour and
reflectance attributes.
The encoder takes as input one or more PLY files describing a point
cloud sequence with integer positions and, optionally, per-point integer
colour and reflectance attributes.
The output of the encoder is a binary bitstream encapsulated using the
G-PCC annex-B format.
Conversely, the decoder takes as input a compressed bitstream file in
G-PCC annex-B format and produces a reconstructed PLY file with position
and any present attribute values.
G-PCC annex-B format and produces one or more reconstructed PLY file
with position and any present attribute values.
The software may be configured using either command line arguments or from
a configuration file specified using the `-c|--config=` option.
......
......@@ -40,6 +40,7 @@
#include <cstdint>
#include <iterator>
#include <utility>
#include <string>
#include <vector>
#if _MSC_VER
......@@ -74,7 +75,6 @@ PCCSystemEndianness()
//---------------------------------------------------------------------------
// Replace any occurence of %d with formatted number. The %d format
// specifier may use the formatting conventions of snprintf().
std::string expandNum(const std::string& src, int num);
//---------------------------------------------------------------------------
......
......@@ -58,6 +58,8 @@ struct DecoderParams {
class PCCTMC3Decoder3 {
public:
class Callbacks;
PCCTMC3Decoder3(const DecoderParams& params) : _params(params) { init(); }
PCCTMC3Decoder3(const PCCTMC3Decoder3&) = default;
......@@ -66,9 +68,7 @@ public:
void init();
int decompress(
const PayloadBuffer* buf,
std::function<void(const PCCPointSet3&)> onOutputCloud);
int decompress(const PayloadBuffer* buf, Callbacks* callback);
//==========================================================================
......@@ -124,6 +124,13 @@ private:
GeometryBrickHeader _gbh;
};
//----------------------------------------------------------------------------
class PCCTMC3Decoder3::Callbacks {
public:
virtual void onOutputCloud(const PCCPointSet3&) = 0;
};
//============================================================================
} // namespace pcc
......@@ -72,9 +72,6 @@ struct EncoderParams {
// todo(df): this should go away
std::map<std::string, int> attributeIdxMap;
// Filename for saving recoloured point cloud.
std::string postRecolorPath;
// Parameters that control partitioning
PartitionParams partition;
......@@ -86,6 +83,8 @@ struct EncoderParams {
class PCCTMC3Encoder3 {
public:
class Callbacks;
PCCTMC3Encoder3();
PCCTMC3Encoder3(const PCCTMC3Encoder3&) = default;
PCCTMC3Encoder3& operator=(const PCCTMC3Encoder3& rhs) = default;
......@@ -94,14 +93,14 @@ public:
int compress(
const PCCPointSet3& inputPointCloud,
EncoderParams* params,
std::function<void(const PayloadBuffer&)> outputFn,
Callbacks*,
PCCPointSet3* reconstructedCloud = nullptr);
void compressPartition(
const PCCPointSet3& inputPointCloud,
const PCCPointSet3& originPartCloud,
EncoderParams* params,
std::function<void(const PayloadBuffer&)> outputFn,
Callbacks*,
PCCPointSet3* reconstructedCloud = nullptr);
static void fixupParameterSets(EncoderParams* params);
......@@ -146,6 +145,14 @@ private:
std::multimap<Vec3<double>, int32_t> quantizedToOrigin;
};
//----------------------------------------------------------------------------
class PCCTMC3Encoder3::Callbacks {
public:
virtual void onOutputBuffer(const PayloadBuffer&) = 0;
virtual void onPostRecolour(const PCCPointSet3&) = 0;
};
//============================================================================
} // namespace pcc
......@@ -61,11 +61,20 @@ struct Parameters {
// when true, configure the encoder as if no attributes are specified
bool disableAttributeCoding;
// Frame number of first file in input sequence.
int firstFrameNum;
// Number of frames to process.
int frameCount;
std::string uncompressedDataPath;
std::string compressedStreamPath;
std::string reconstructedDataPath;
// Filename for saving pre inverse scaled point cloud.
// Filename for saving recoloured point cloud (encoder).
std::string postRecolorPath;
// Filename for saving pre inverse scaled point cloud (decoder).
std::string preInvScalePath;
pcc::EncoderParams encoder;
......@@ -78,6 +87,52 @@ struct Parameters {
int reflectanceScale;
};
//----------------------------------------------------------------------------
class SequenceEncoder : public PCCTMC3Encoder3::Callbacks {
public:
// NB: params must outlive the lifetime of the decoder.
SequenceEncoder(Parameters* params);
int compress(Stopwatch* clock);
protected:
int compressOneFrame(Stopwatch* clock);
void onOutputBuffer(const PayloadBuffer& buf) override;
void onPostRecolour(const PCCPointSet3& cloud) override;
private:
Parameters* params;
PCCTMC3Encoder3 encoder;
std::ofstream bytestreamFile;
int frameNum;
};
//----------------------------------------------------------------------------
class SequenceDecoder : public PCCTMC3Decoder3::Callbacks {
public:
// NB: params must outlive the lifetime of the decoder.
SequenceDecoder(const Parameters* params);
int decompress(Stopwatch* clock);
protected:
void onOutputCloud(const PCCPointSet3& decodedPointCloud) override;
private:
const Parameters* params;
PCCTMC3Decoder3 decoder;
std::ofstream bytestreamFile;
int frameNum;
Stopwatch* clock;
};
//============================================================================
int
......@@ -98,9 +153,9 @@ main(int argc, char* argv[])
int ret = 0;
if (params.isDecoder) {
ret = Decompress(params, clock_user);
ret = SequenceDecoder(&params).decompress(&clock_user);
} else {
ret = Compress(params, clock_user);
ret = SequenceEncoder(&params).compress(&clock_user);
}
clock_wall.stop();
......@@ -258,6 +313,15 @@ ParseParameters(int argc, char* argv[], Parameters& params)
" 1: decode")
// i/o parameters
("firstFrameNum",
params.firstFrameNum, 0,
"Frame number for use with interpolating %d format specifiers"
"in input/output filenames")
("frameCount",
params.frameCount, 1,
"Number of frames to encode")
("reconstructedDataPath",
params.reconstructedDataPath, {},
"The ouput reconstructed pointcloud file path (decoder only)")
......@@ -271,7 +335,7 @@ ParseParameters(int argc, char* argv[], Parameters& params)
"The compressed bitstream path (encoder=output, decoder=input)")
("postRecolorPath",
params.encoder.postRecolorPath, {},
params.postRecolorPath, {},
"Recolored pointcloud file path (encoder only)")
("preInvScalePath",
......@@ -779,101 +843,166 @@ ParseParameters(int argc, char* argv[], Parameters& params)
return true;
}
//============================================================================
SequenceEncoder::SequenceEncoder(Parameters* params) : params(params)
{}
//----------------------------------------------------------------------------
int
Compress(Parameters& params, Stopwatch& clock)
SequenceEncoder::compress(Stopwatch* clock)
{
bytestreamFile.open(params->compressedStreamPath, ios::binary);
if (!bytestreamFile.is_open()) {
return -1;
}
const int lastFrameNum = params->firstFrameNum + params->frameCount;
for (frameNum = params->firstFrameNum; frameNum < lastFrameNum; frameNum++) {
if (compressOneFrame(clock))
return -1;
}
std::cout << "Total bitstream size " << bytestreamFile.tellp() << " B\n";
bytestreamFile.close();
return 0;
}
//----------------------------------------------------------------------------
int
SequenceEncoder::compressOneFrame(Stopwatch* clock)
{
std::string srcName{expandNum(params->uncompressedDataPath, frameNum)};
PCCPointSet3 pointCloud;
if (
!pointCloud.read(params.uncompressedDataPath)
|| pointCloud.getPointCount() == 0) {
if (!pointCloud.read(srcName) || pointCloud.getPointCount() == 0) {
cout << "Error: can't open input file!" << endl;
return -1;
}
// Sanitise the input point cloud
// todo(df): remove the following with generic handling of properties
bool codeColour = params.encoder.attributeIdxMap.count("color");
bool codeColour = params->encoder.attributeIdxMap.count("color");
if (!codeColour)
pointCloud.removeColors();
assert(codeColour == pointCloud.hasColors());
bool codeReflectance = params.encoder.attributeIdxMap.count("reflectance");
bool codeReflectance = params->encoder.attributeIdxMap.count("reflectance");
if (!codeReflectance)
pointCloud.removeReflectances();
assert(codeReflectance == pointCloud.hasReflectances());
ofstream fout(params.compressedStreamPath, ios::binary);
if (!fout.is_open()) {
return -1;
}
clock.start();
clock->start();
if (params.colorTransform == COLOR_TRANSFORM_RGB_TO_YCBCR) {
if (params->colorTransform == COLOR_TRANSFORM_RGB_TO_YCBCR) {
pointCloud.convertRGBToYUV();
}
if (params.reflectanceScale > 1 && pointCloud.hasReflectances()) {
if (params->reflectanceScale > 1 && pointCloud.hasReflectances()) {
const auto pointCount = pointCloud.getPointCount();
for (size_t i = 0; i < pointCount; ++i) {
int val = pointCloud.getReflectance(i) / params.reflectanceScale;
int val = pointCloud.getReflectance(i) / params->reflectanceScale;
pointCloud.setReflectance(i, val);
}
}
PCCTMC3Encoder3 encoder;
// The reconstructed point cloud
std::unique_ptr<PCCPointSet3> reconPointCloud;
if (!params.reconstructedDataPath.empty()) {
if (!params->reconstructedDataPath.empty()) {
reconPointCloud.reset(new PCCPointSet3);
}
auto bytestreamLenFrameStart = bytestreamFile.tellp();
int ret = encoder.compress(
pointCloud, &params.encoder,
[&](const PayloadBuffer& buf) { writeTlv(buf, fout); },
reconPointCloud.get());
pointCloud, &params->encoder, this, reconPointCloud.get());
if (ret) {
cout << "Error: can't compress point cloud!" << endl;
return -1;
}
std::cout << "Total bitstream size " << fout.tellp() << " B" << std::endl;
fout.close();
auto bytestreamLenFrameEnd = bytestreamFile.tellp();
int frameLen = bytestreamLenFrameEnd - bytestreamLenFrameStart;
std::cout << "Total frame size " << frameLen << " B" << std::endl;
clock.stop();
clock->stop();
if (!params.reconstructedDataPath.empty()) {
if (params.colorTransform == COLOR_TRANSFORM_RGB_TO_YCBCR) {
if (!params->reconstructedDataPath.empty()) {
if (params->colorTransform == COLOR_TRANSFORM_RGB_TO_YCBCR) {
reconPointCloud->convertYUVToRGB();
}
if (params.reflectanceScale > 1 && reconPointCloud->hasReflectances()) {
if (params->reflectanceScale > 1 && reconPointCloud->hasReflectances()) {
const auto pointCount = reconPointCloud->getPointCount();
for (size_t i = 0; i < pointCount; ++i) {
int val = reconPointCloud->getReflectance(i) * params.reflectanceScale;
int val =
reconPointCloud->getReflectance(i) * params->reflectanceScale;
reconPointCloud->setReflectance(i, val);
}
}
reconPointCloud->write(
params.reconstructedDataPath, !params.outputBinaryPly);
std::string recName{expandNum(params->reconstructedDataPath, frameNum)};
reconPointCloud->write(recName, !params->outputBinaryPly);
}
return 0;
}
//----------------------------------------------------------------------------
void
SequenceEncoder::onOutputBuffer(const PayloadBuffer& buf)
{
writeTlv(buf, bytestreamFile);
}
//----------------------------------------------------------------------------
void
SequenceEncoder::onPostRecolour(const PCCPointSet3& cloud)
{
if (params->postRecolorPath.empty()) {
return;
}
std::string plyName{expandNum(params->postRecolorPath, frameNum)};
// todo(df): stop the clock
if (params->colorTransform != COLOR_TRANSFORM_RGB_TO_YCBCR) {
cloud.write(plyName);
return;
}
PCCPointSet3 tmpCloud(cloud);
tmpCloud.convertYUVToRGB();
tmpCloud.write(plyName);
}
//============================================================================
SequenceDecoder::SequenceDecoder(const Parameters* params)
: params(params), decoder(params->decoder)
{}
//----------------------------------------------------------------------------
int
Decompress(Parameters& params, Stopwatch& clock)
SequenceDecoder::decompress(Stopwatch* clock)
{
ifstream fin(params.compressedStreamPath, ios::binary);
ifstream fin(params->compressedStreamPath, ios::binary);
if (!fin.is_open()) {
return -1;
}
clock.start();
frameNum = params->firstFrameNum;
this->clock = clock;
PayloadBuffer buf;
PCCTMC3Decoder3 decoder(params.decoder);
clock->start();
while (true) {
PayloadBuffer* buf_ptr = &buf;
......@@ -883,53 +1012,61 @@ Decompress(Parameters& params, Stopwatch& clock)
if (!fin)
buf_ptr = nullptr;
int ret =
decoder.decompress(buf_ptr, [&](const PCCPointSet3& decodedPointCloud) {
if (decoder.decompress(buf_ptr, this)) {
cout << "Error: can't decompress point cloud!" << endl;
return -1;
}
if (!buf_ptr)
break;
}
fin.clear();
fin.seekg(0, ios_base::end);
std::cout << "Total bitstream size " << fin.tellg() << " B" << std::endl;
clock->stop();
return 0;
}
//----------------------------------------------------------------------------
void
SequenceDecoder::onOutputCloud(const PCCPointSet3& decodedPointCloud)
{
// copy the point cloud in order to modify it according to the output options
PCCPointSet3 pointCloud(decodedPointCloud);
if (params.colorTransform == COLOR_TRANSFORM_RGB_TO_YCBCR) {
if (params->colorTransform == COLOR_TRANSFORM_RGB_TO_YCBCR) {
pointCloud.convertYUVToRGB();
}
if (params.reflectanceScale > 1 && pointCloud.hasReflectances()) {
if (params->reflectanceScale > 1 && pointCloud.hasReflectances()) {
const auto pointCount = pointCloud.getPointCount();
for (size_t i = 0; i < pointCount; ++i) {
int val = pointCloud.getReflectance(i) * params.reflectanceScale;
int val = pointCloud.getReflectance(i) * params->reflectanceScale;
pointCloud.setReflectance(i, val);
}
}
// Dump the decoded colour using the pre inverse scaled geometry
if (!params.preInvScalePath.empty()) {
pointCloud.write(params.preInvScalePath, !params.outputBinaryPly);
if (!params->preInvScalePath.empty()) {
std::string filename{expandNum(params->preInvScalePath, frameNum)};
pointCloud.write(params->preInvScalePath, !params->outputBinaryPly);
}
decoder.inverseQuantization(pointCloud);
clock.stop();
clock->stop();
if (!pointCloud.write(
params.reconstructedDataPath, !params.outputBinaryPly)) {
std::string decName{expandNum(params->reconstructedDataPath, frameNum)};
if (!pointCloud.write(decName, !params->outputBinaryPly)) {
cout << "Error: can't open output file!" << endl;
}
clock.start();
});
clock->start();
if (ret) {
cout << "Error: can't decompress point cloud!" << endl;
return -1;
}
if (!buf_ptr)
break;
}
fin.clear();
fin.seekg(0, ios_base::end);
std::cout << "Total bitstream size " << fin.tellg() << " B" << std::endl;
clock.stop();
return 0;
// todo(df): frame number should be derived from the bitstream
frameNum++;
}
......@@ -75,8 +75,7 @@ payloadStartsNewSlice(PayloadType type)
int
PCCTMC3Decoder3::decompress(
const PayloadBuffer* buf,
std::function<void(const PCCPointSet3&)> onOutputCloud)
const PayloadBuffer* buf, PCCTMC3Decoder3::Callbacks* callback)
{
// Starting a new geometry brick/slice/tile, transfer any
// finished points to the output accumulator
......@@ -91,7 +90,7 @@ PCCTMC3Decoder3::decompress(
if (!buf) {
// flush decoder, output pending cloud if any
onOutputCloud(_accumCloud);
callback->onOutputCloud(_accumCloud);
_accumCloud.clear();
return 0;
}
......@@ -107,7 +106,7 @@ PCCTMC3Decoder3::decompress(
// NB: frame counter is reset to avoid outputing a runt point cloud
// on the next slice.
case PayloadType::kFrameBoundaryMarker:
onOutputCloud(_accumCloud);
callback->onOutputCloud(_accumCloud);
_accumCloud.clear();
_currentFrameIdx = -1;
return 0;
......@@ -115,7 +114,7 @@ PCCTMC3Decoder3::decompress(
case PayloadType::kGeometryBrick:
activateParameterSets(parseGbhIds(*buf));
if (frameIdxChanged(parseGbh(*_sps, *_gps, *buf, nullptr))) {
onOutputCloud(_accumCloud);