Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functionality to split encoding/decoding of colors and symbols #91

Merged
merged 23 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
32b9ffa
Experiment with a different color calculation?
sz3 Sep 4, 2023
4067532
color_mode for encoder?
sz3 Sep 7, 2023
db0812c
WIP to vary fountain chunk size by number of symbol bits...
sz3 Sep 13, 2023
e7fdfe3
First pass at splitting symbol and color channels, encoder edition
sz3 Sep 23, 2023
4f9622f
Simplify loop
sz3 Sep 23, 2023
55dbf68
First pass at decoder for split symbols/colors
sz3 Sep 23, 2023
33b6cef
Reintroduce the legacy decoder function
sz3 Oct 1, 2023
80a668c
Tweaks to Encoder default params
sz3 Oct 6, 2023
6b58b25
Flush symbols/colors separately
sz3 Oct 24, 2023
2e7e3aa
Change calc for required frames we generate for small files
sz3 Nov 20, 2023
2c6319d
Change default encode_id to 109, and return to older color calc?
sz3 Dec 19, 2023
36676d7
Update FountainMetadata to include the block_id
sz3 Dec 20, 2023
be36c02
Add FountainHeader logic to CimbReader
sz3 Dec 20, 2023
fcb214d
Working towards proper color correction
sz3 Dec 31, 2023
2f491ef
Changes for the second-pass CCM+color decode
sz3 Jan 5, 2024
88c30e0
Use Moore-Penrose least squares to compute our CCM
sz3 Jan 6, 2024
8c3b0a7
Make new color correction mode the default
sz3 Jan 7, 2024
a05d414
Disable color decode "skip" logic
sz3 Jan 22, 2024
af823fd
Update tests, and add back some "legacy" (0.5.x = "4c") support for e…
sz3 Jan 29, 2024
9f6600d
Add toggle for old "4c" mode in the cimbar_js and the cli tools
sz3 Jan 29, 2024
55299de
`fountain_chunks_per_frame()` config function needs to return "10" fo…
sz3 Jan 31, 2024
196a893
Cleanup some includes/comments
sz3 Feb 18, 2024
7206b91
Use new samples submodule rev
sz3 Feb 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion samples
68 changes: 48 additions & 20 deletions src/exe/cimbar/cimbar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,11 @@ namespace {
}

template <typename FilenameIterable>
int encode(const FilenameIterable& infiles, const std::string& outpath, int ecc, int color_bits, int compression_level, bool no_fountain)
int encode(const FilenameIterable& infiles, const std::string& outpath, int ecc, int color_bits, int compression_level, bool legacy_mode, bool no_fountain)
{
Encoder en(ecc, cimbar::Config::symbol_bits(), color_bits);
if (legacy_mode)
en.set_legacy_mode();
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently the toggle for the old mode (interleaved symbol/color bits). It probably needs to be refactored into a config option, but for now this works.

for (const string& f : infiles)
{
if (f.empty())
Expand All @@ -117,7 +119,7 @@ int encode(const FilenameIterable& infiles, const std::string& outpath, int ecc,
}

template <typename FilenameIterable>
int decode(const FilenameIterable& infiles, const std::function<int(cv::UMat, bool, bool)>& decode, bool no_deskew, bool undistort, int preprocess, bool color_correct)
int decode(const FilenameIterable& infiles, const std::function<int(cv::UMat, bool, int)>& decodefun, bool no_deskew, bool undistort, int preprocess, int color_correct)
{
int err = 0;
for (const string& inf : infiles)
Expand Down Expand Up @@ -150,17 +152,19 @@ int decode(const FilenameIterable& infiles, const std::function<int(cv::UMat, bo
shouldPreprocess = true;
}

int bytes = decode(img, shouldPreprocess, color_correct);
int bytes = decodefun(img, shouldPreprocess, color_correct);
if (!bytes)
err |= 4;
}
return err;
}

// see also "decodefun" for non-fountain decodes, defined as a lambda inline below.
// this one needs its own function since it's a template (:
template <typename SINK>
std::function<int(cv::UMat,bool,bool)> fountain_decode_fun(SINK& sink, Decoder& d)
std::function<int(cv::UMat,bool,int)> fountain_decode_fun(SINK& sink, Decoder& d)
{
return [&sink, &d] (cv::UMat m, bool pre, bool cc) {
return [&sink, &d] (cv::UMat m, bool pre, int cc) {
return d.decode_fountain(m, sink, pre, cc);
};
}
Expand All @@ -177,8 +181,10 @@ int main(int argc, char** argv)
("o,out", "Output file prefix (encoding) or directory (decoding).", cxxopts::value<string>())
("c,color-bits", "Color bits. [0-3]", cxxopts::value<int>()->default_value(turbo::str::str(colorBits)))
("e,ecc", "ECC level", cxxopts::value<unsigned>()->default_value(turbo::str::str(ecc)))
("m,mode", "Select a cimbar mode. B (the default) is new to 0.6.x. 4C is the 0.5.x config. [B,4C]", cxxopts::value<string>()->default_value("B"))
("z,compression", "Compression level. 0 == no compression.", cxxopts::value<int>()->default_value(turbo::str::str(compressionLevel)))
("color-correct", "Toggle decoding color correction. 1 == on. 0 == off.", cxxopts::value<int>()->default_value("1"))
("color-correct", "Toggle decoding color correction. 2 == full (fountain mode only). 1 == simple. 0 == off.", cxxopts::value<int>()->default_value("2"))
Copy link
Owner Author

@sz3 sz3 Feb 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaning towards removing the old ("simple") color-correct logic. It's nice that it can be run under more conditions, but in its current form it's just not good enough to be useful.

("color-correction-file", "Debug -- save color correction matrix generated during fountain decode, or use it for non-fountain decodes", cxxopts::value<string>())
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used for doing a two-pass manual debug/verification process:

  1. decode sample in fountain mode, saving ccm to file
  2. use ccm file when decoding in non-fountain mode, generating a sample output file (errors and all) that can be used to validate error rates (e.g. with the cimbar grader tool )

("encode", "Run the encoder!", cxxopts::value<bool>())
("no-deskew", "Skip the deskew step -- treat input image as already extracted.", cxxopts::value<bool>())
("no-fountain", "Disable fountain encode/decode. Will also disable compression.", cxxopts::value<bool>())
Expand Down Expand Up @@ -216,48 +222,70 @@ int main(int argc, char** argv)
compressionLevel = result["compression"].as<int>();
ecc = result["ecc"].as<unsigned>();

bool legacy_mode = false;
if (result.count("mode"))
{
string mode = result["mode"].as<string>();
legacy_mode = (mode == "4c") or (mode == "4C");
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-m 4C to switch back to cimbar 0.5.x mode.

}

if (encodeFlag)
{
if (useStdin)
return encode(StdinLineReader(), outpath, ecc, colorBits, compressionLevel, no_fountain);
return encode(StdinLineReader(), outpath, ecc, colorBits, compressionLevel, legacy_mode, no_fountain);
else
return encode(infiles, outpath, ecc, colorBits, compressionLevel, no_fountain);
return encode(infiles, outpath, ecc, colorBits, compressionLevel, legacy_mode, no_fountain);
}

// else, decode
bool no_deskew = result.count("no-deskew");
bool undistort = result.count("undistort");
int color_correct = result["color-correct"].as<int>();
string color_correction_file;
if (result.count("color-correction-file"))
color_correction_file = result["color-correction-file"].as<string>();
int preprocess = result["preprocess"].as<int>();

Decoder d(ecc, colorBits);
unsigned color_mode = legacy_mode? 0 : 1;
bool coupled = legacy_mode;
Decoder d(ecc, colorBits, color_mode, coupled);

if (no_fountain)
{
if (not color_correction_file.empty())
d.load_ccm(color_correction_file);

// simpler encoding, just the basics + ECC. No compression, fountain codes, etc.
std::ofstream f(outpath);
std::function<int(cv::UMat,bool,bool)> fun = [&f, &d] (cv::UMat m, bool pre, bool cc) {
std::function<int(cv::UMat,bool,int)> decodefun = [&f, &d] (cv::UMat m, bool pre, int cc) {
return d.decode(m, f, pre, cc);
};
if (useStdin)
return decode(StdinLineReader(), fun, no_deskew, undistort, preprocess, color_correct);
return decode(StdinLineReader(), decodefun, no_deskew, undistort, preprocess, color_correct);
else
return decode(infiles, fun, no_deskew, undistort, preprocess, color_correct);
return decode(infiles, decodefun, no_deskew, undistort, preprocess, color_correct);
}

// else, the good stuff
unsigned chunkSize = cimbar::Config::fountain_chunk_size(ecc);
int res = -200;

unsigned chunkSize = cimbar::Config::fountain_chunk_size(ecc, colorBits+cimbar::Config::symbol_bits(), legacy_mode);
if (compressionLevel <= 0)
{
fountain_decoder_sink<std::ofstream> sink(outpath, chunkSize, true);
return decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct);
res = decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct);
}
else // default case, all bells and whistles
{
fountain_decoder_sink<cimbar::zstd_decompressor<std::ofstream>> sink(outpath, chunkSize, true);

// else -- default case, all bells and whistles
fountain_decoder_sink<cimbar::zstd_decompressor<std::ofstream>> sink(outpath, chunkSize, true);
if (useStdin)
res = decode(StdinLineReader(), fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct);
else
res = decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct);
}
if (not color_correction_file.empty())
d.save_ccm(color_correction_file);

if (useStdin)
return decode(StdinLineReader(), fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct);
else
return decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct);
return res;
}
18 changes: 14 additions & 4 deletions src/exe/cimbar_recv/recv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ int main(int argc, char** argv)
options.add_options()
("i,in", "Video source.", cxxopts::value<string>())
("o,out", "Output directory (decoding).", cxxopts::value<string>())
("c,colorbits", "Color bits. [0-3]", cxxopts::value<int>()->default_value(turbo::str::str(colorBits)))
("c,colorbits", "Color bits. [0-3]", cxxopts::value<int>()->default_value(turbo::str::str(colorBits)))
("e,ecc", "ECC level", cxxopts::value<unsigned>()->default_value(turbo::str::str(ecc)))
("f,fps", "Target decode FPS", cxxopts::value<unsigned>()->default_value(turbo::str::str(defaultFps)))
("h,help", "Print usage")
("m,mode", "Select a cimbar mode. B (the default) is new to 0.6.x. 4C is the 0.5.x config. [B,4C]", cxxopts::value<string>()->default_value("B"))
("h,help", "Print usage")
;
options.show_positional_help();
options.parse_positional({"in", "out"});
Expand All @@ -62,6 +63,14 @@ int main(int argc, char** argv)

colorBits = std::min(3, result["colorbits"].as<int>());
ecc = result["ecc"].as<unsigned>();

bool legacy_mode = false;
if (result.count("mode"))
{
string mode = result["mode"].as<string>();
legacy_mode = (mode == "4c") or (mode == "4C");
}

unsigned fps = result["fps"].as<unsigned>();
if (fps == 0)
fps = defaultFps;
Expand Down Expand Up @@ -95,9 +104,10 @@ int main(int argc, char** argv)
window.auto_scale_to_window();

Extractor ext;
Decoder dec;
unsigned color_mode = legacy_mode? 0 : 1;
Decoder dec(-1, -1, color_mode, legacy_mode);

unsigned chunkSize = cimbar::Config::fountain_chunk_size(ecc);
unsigned chunkSize = cimbar::Config::fountain_chunk_size(ecc, colorBits+cimbar::Config::symbol_bits(), legacy_mode);
fountain_decoder_sink<cimbar::zstd_decompressor<std::ofstream>> sink(outpath, chunkSize);

cv::Mat mat;
Expand Down
15 changes: 13 additions & 2 deletions src/exe/cimbar_send/send.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ int main(int argc, char** argv)
("c,colorbits", "Color bits. [0-3]", cxxopts::value<int>()->default_value(turbo::str::str(colorBits)))
("e,ecc", "ECC level", cxxopts::value<unsigned>()->default_value(turbo::str::str(ecc)))
("f,fps", "Target FPS", cxxopts::value<unsigned>()->default_value(turbo::str::str(defaultFps)))
("m,mode", "Select a cimbar mode. B (the default) is new to 0.6.x. 4C is the 0.5.x config. [B,4C]", cxxopts::value<string>()->default_value("B"))
("z,compression", "Compression level. 0 == no compression.", cxxopts::value<int>()->default_value(turbo::str::str(compressionLevel)))
("h,help", "Print usage")
;
Expand All @@ -61,6 +62,14 @@ int main(int argc, char** argv)
colorBits = std::min(3, result["colorbits"].as<int>());
compressionLevel = result["compression"].as<int>();
ecc = result["ecc"].as<unsigned>();

bool legacy_mode = false;
if (result.count("mode"))
{
string mode = result["mode"].as<string>();
legacy_mode = (mode == "4c") or (mode == "4C");
}

unsigned fps = result["fps"].as<unsigned>();
if (fps == 0)
fps = defaultFps;
Expand All @@ -73,7 +82,7 @@ int main(int argc, char** argv)
return 70;
}

configure(colorBits, ecc, compressionLevel);
configure(colorBits, ecc, compressionLevel, legacy_mode);

std::chrono::time_point start = std::chrono::high_resolution_clock::now();
while (true)
Expand All @@ -91,7 +100,9 @@ int main(int argc, char** argv)
continue;
}

if (!encode(reinterpret_cast<unsigned char*>(contents.data()), contents.size(), static_cast<int>(i)))
// start encode_id is 109. This is mostly unimportant (it only needs to wrap between [0,127]), but useful
// for the decoder -- because it gives it a better distribution of colors in the first frame header it sees.
if (!encode(reinterpret_cast<unsigned char*>(contents.data()), contents.size(), static_cast<int>(i+109)))
{
std::cerr << "failed to encode file " << infiles[i] << std::endl;
continue;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/bit_file/bitbuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ class bitbuffer
return _buffer;
}

void copy_to_buffer(const char* data, unsigned size)
{
_buffer.resize(size, 0);
std::copy(data, data+size, _buffer.data());
}

writer get_writer(size_t pos=0)
{
return writer(*this, pos);
Expand Down
14 changes: 14 additions & 0 deletions src/lib/bit_file/test/bitbufferTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,17 @@ TEST_CASE( "bitbufferTest/testOverwrite", "[unit]" )
assertEquals( '.', bb.buffer()[0x3f5] );
assertEquals( 't', bb.buffer()[0x3f6] );
}

TEST_CASE( "bitbufferTest/testCopyToBuffer", "[unit]" )
{
bitbuffer bb;

std::string hello = "hello";
bb.copy_to_buffer(hello.data(), 5);

assertEquals( 0x68, bb.read(0, 8) );
assertEquals( 0x65, bb.read(8, 8) );
assertEquals( 0x6c, bb.read(16, 8) );
assertEquals( 0x6c, bb.read(24, 8) );
assertEquals( 0x6f, bb.read(32, 8) );
}
21 changes: 18 additions & 3 deletions src/lib/chromatic_adaptation/color_correction.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,30 @@ class color_correction
return transform().inv() * d * transform();
}

static inline cv::Matx<float, 3, 3> get_moore_penrose_lsm(const cv::Mat& actual, const cv::Mat& desired)
{
// inspired by the python colour-science package. It's not complicated,
// but I didn't know that going in.
// See also:
// https://en.wikipedia.org/wiki/Moore-Penrose_inverse
cv::Mat x, y, z;
cv::transpose(desired, x);
cv::transpose(actual, y);
cv::invert(y, z, cv::DECOMP_SVD);

y = x * z;
return y;
}

public:
color_correction()
: _active(false)
: _active(false)
{
}

color_correction(cv::Matx<float, 3, 3>&& m)
: _m(m)
, _active(true)
: _m(m)
, _active(true)
{
}

Expand Down
55 changes: 53 additions & 2 deletions src/lib/chromatic_adaptation/test/color_correctionTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ TEST_CASE( "color_correctionTest/testTransform", "[unit]" )
std::stringstream ss;
ss << mat;
assertEquals( "[1.0655777, 0.2109226, -0.013239831;\n"
" 0.023168325, 0.98723376, -0.0046780901;\n"
" 0, 0, 1]", ss.str() );
" 0.023168325, 0.98723376, -0.0046780901;\n"
" 0, 0, 1]", ss.str() );
}

std::tuple<float, float, float> c = color_correction(std::move(mat)).transform(180, 98, 255);
Expand All @@ -29,3 +29,54 @@ TEST_CASE( "color_correctionTest/testTransform", "[unit]" )
assertAlmostEquals( 255, std::get<2>(c) );
}

TEST_CASE( "color_correctionTest/testComputeMoorePenrose", "[unit]" )
{
cv::Mat actual = (cv::Mat_<float>(5,3) <<
0, 142.31060606, 0,
0, 148.75, 148.75,
148.75, 148.75, 0,
148.75, 0, 148.75,
255, 255, 255);

cv::Mat desired = (cv::Mat_<float>(5,3) <<
0, 255, 0,
0, 255, 255,
255, 255, 0,
255, 0, 255,
255, 255, 255);

cv::Matx<float, 3, 3> mat = color_correction::get_moore_penrose_lsm(actual, desired);
{
std::stringstream ss;
ss << mat;
assertEquals( "[1.5223049, -0.10023587, -0.19198087;\n"
" -0.20533442, 1.6441474, -0.20533434;\n"
" -0.19198078, -0.10023584, 1.5223049]", ss.str() );
}
}

TEST_CASE( "color_correctionTest/testComputeMoorePenrose.2", "[unit]" )
{
cv::Mat actual = (cv::Mat_<float>(5,3) <<
14.58901515, 115.74431818, 39.88320707,
19.34027778, 124.4375, 115.37152778,
140.70486111, 137.45833333, 65.50694444,
131.59722222, 41.22222222, 104.84027778,
171.625, 163.625, 158.875);

cv::Mat desired = (cv::Mat_<float>(5,3) <<
0, 255, 0,
0, 255, 255,
255, 255, 0,
255, 0, 255,
255, 255, 255);

cv::Matx<float, 3, 3> mat = color_correction::get_moore_penrose_lsm(actual, desired);
{
std::stringstream ss;
ss << mat;
assertEquals( "[2.0261116, -0.21691091, -0.19806443;\n"
" -0.43822661, 2.4562523, -0.41700464;\n"
" -0.55769891, -1.1443435, 3.4819376]", ss.str() );
}
}
Loading
Loading