From 7b11358d8b3326ff835a56f8ec7c8640d2ec52bd Mon Sep 17 00:00:00 2001 From: sz Date: Mon, 26 Feb 2024 00:26:51 -0600 Subject: [PATCH 1/3] Use per call switch for color_mode -- rather than per object instance I say "color_mode", but this also includes the coupled legacy behavior (merged symbols and colors). Motivation: per-frame bouncing between color_mode, so we can test success/failure for mode B/4C and pick the one that's working. Details: When running a decoder across multiple frames, we have certain state (loaded hashes for imagehash hamming distance, for example) that is longer lived -- that we don't want to run each frame. This *currently* is stored in the CimbDecoder (also the fountain_decoder_sink, though that's not relevant to this change). To allow us to switch between modes, we need the CimbDecoder to not store color_mode, but rather take it as an argument where required. Misc: There are other ways to accomplish this, but this change seems like the best immediate path forward. The actual technical requirement is: (1) to run in different modes (2) on different threads (3) at the same time. --- src/exe/cimbar/cimbar.cpp | 27 ++++---- src/exe/cimbar_recv/recv.cpp | 6 +- src/lib/cimb_translator/CimbDecoder.cpp | 15 ++--- src/lib/cimb_translator/CimbDecoder.h | 9 ++- src/lib/cimb_translator/CimbReader.cpp | 11 ++-- src/lib/cimb_translator/CimbReader.h | 5 +- .../cimb_translator/test/CimbDecoderTest.cpp | 64 +++++++++++++------ .../cimb_translator/test/CimbReaderTest.cpp | 53 ++++++++++++--- src/lib/encoder/Decoder.h | 40 ++++++------ src/lib/encoder/test/DecoderTest.cpp | 12 ++-- src/lib/encoder/test/EncoderRoundTripTest.cpp | 4 +- 11 files changed, 153 insertions(+), 93 deletions(-) diff --git a/src/exe/cimbar/cimbar.cpp b/src/exe/cimbar/cimbar.cpp index 071469f2..636998f8 100644 --- a/src/exe/cimbar/cimbar.cpp +++ b/src/exe/cimbar/cimbar.cpp @@ -119,7 +119,7 @@ int encode(const FilenameIterable& infiles, const std::string& outpath, int ecc, } template -int decode(const FilenameIterable& infiles, const std::function& decodefun, bool no_deskew, bool undistort, int preprocess, int color_correct) +int decode(const FilenameIterable& infiles, const std::function& decodefun, bool no_deskew, bool undistort, unsigned color_mode, int preprocess, int color_correct) { int err = 0; for (const string& inf : infiles) @@ -152,7 +152,7 @@ int decode(const FilenameIterable& infiles, const std::function -std::function fountain_decode_fun(SINK& sink, Decoder& d) +std::function fountain_decode_fun(SINK& sink, Decoder& d) { - return [&sink, &d] (cv::UMat m, bool pre, int cc) { - return d.decode_fountain(m, sink, pre, cc); + return [&sink, &d] (cv::UMat m, unsigned cm, bool pre, int cc) { + return d.decode_fountain(m, sink, cm, pre, cc); }; } @@ -247,8 +247,7 @@ int main(int argc, char** argv) int preprocess = result["preprocess"].as(); unsigned color_mode = legacy_mode? 0 : 1; - bool coupled = legacy_mode; - Decoder d(ecc, colorBits, color_mode, coupled); + Decoder d(ecc, colorBits); if (no_fountain) { @@ -257,13 +256,13 @@ int main(int argc, char** argv) // simpler encoding, just the basics + ECC. No compression, fountain codes, etc. std::ofstream f(outpath); - std::function decodefun = [&f, &d] (cv::UMat m, bool pre, int cc) { - return d.decode(m, f, pre, cc); + std::function decodefun = [&f, &d] (cv::UMat m, unsigned cm, bool pre, int cc) { + return d.decode(m, f, cm, pre, cc); }; if (useStdin) - return decode(StdinLineReader(), decodefun, no_deskew, undistort, preprocess, color_correct); + return decode(StdinLineReader(), decodefun, no_deskew, undistort, color_mode, preprocess, color_correct); else - return decode(infiles, decodefun, no_deskew, undistort, preprocess, color_correct); + return decode(infiles, decodefun, no_deskew, undistort, color_mode, preprocess, color_correct); } // else, the good stuff @@ -273,16 +272,16 @@ int main(int argc, char** argv) if (compressionLevel <= 0) { fountain_decoder_sink sink(outpath, chunkSize, true); - res = decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct); + res = decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, color_mode, preprocess, color_correct); } else // default case, all bells and whistles { fountain_decoder_sink> sink(outpath, chunkSize, true); if (useStdin) - res = decode(StdinLineReader(), fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct); + res = decode(StdinLineReader(), fountain_decode_fun(sink, d), no_deskew, undistort, color_mode, preprocess, color_correct); else - res = decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, preprocess, color_correct); + res = decode(infiles, fountain_decode_fun(sink, d), no_deskew, undistort, color_mode, preprocess, color_correct); } if (not color_correction_file.empty()) d.save_ccm(color_correction_file); diff --git a/src/exe/cimbar_recv/recv.cpp b/src/exe/cimbar_recv/recv.cpp index 5ad1e273..a3c24cf7 100644 --- a/src/exe/cimbar_recv/recv.cpp +++ b/src/exe/cimbar_recv/recv.cpp @@ -70,6 +70,7 @@ int main(int argc, char** argv) string mode = result["mode"].as(); legacy_mode = (mode == "4c") or (mode == "4C"); } + unsigned color_mode = legacy_mode? 0 : 1; unsigned fps = result["fps"].as(); if (fps == 0) @@ -104,8 +105,7 @@ int main(int argc, char** argv) window.auto_scale_to_window(); Extractor ext; - unsigned color_mode = legacy_mode? 0 : 1; - Decoder dec(-1, -1, color_mode, legacy_mode); + Decoder dec(-1, -1); unsigned chunkSize = cimbar::Config::fountain_chunk_size(ecc, colorBits+cimbar::Config::symbol_bits(), legacy_mode); fountain_decoder_sink> sink(outpath, chunkSize); @@ -147,7 +147,7 @@ int main(int argc, char** argv) shouldPreprocess = true; // decode - int bytes = dec.decode_fountain(img, sink, shouldPreprocess); + int bytes = dec.decode_fountain(img, sink, color_mode, shouldPreprocess); if (bytes > 0) std::cerr << "got some bytes " << bytes << std::endl; } diff --git a/src/lib/cimb_translator/CimbDecoder.cpp b/src/lib/cimb_translator/CimbDecoder.cpp index 7268eda3..ad788a69 100644 --- a/src/lib/cimb_translator/CimbDecoder.cpp +++ b/src/lib/cimb_translator/CimbDecoder.cpp @@ -54,11 +54,10 @@ namespace { } } -CimbDecoder::CimbDecoder(unsigned symbol_bits, unsigned color_bits, unsigned color_mode, bool dark, uchar ahashThreshold) +CimbDecoder::CimbDecoder(unsigned symbol_bits, unsigned color_bits, bool dark, uchar ahashThreshold) : _symbolBits(symbol_bits) , _numSymbols(1 << symbol_bits) , _numColors(1 << color_bits) - , _colorMode(color_mode) , _dark(dark) , _ahashThreshold(ahashThreshold) { @@ -160,12 +159,12 @@ unsigned CimbDecoder::check_color_distance(std::tuple a, std: return color_diff(a, b); } -std::tuple CimbDecoder::get_color(int i) const +std::tuple CimbDecoder::get_color(int i, unsigned color_mode) const { - return cimbar::getColor(i, _numColors, _colorMode); + return cimbar::getColor(i, _numColors, color_mode); } -unsigned CimbDecoder::get_best_color(float r, float g, float b) const +unsigned CimbDecoder::get_best_color(float r, float g, float b, unsigned color_mode) const { // transform color with ccm if (internal_ccm().active()) @@ -188,7 +187,7 @@ unsigned CimbDecoder::get_best_color(float r, float g, float b) const float best_distance = 1000000; for (unsigned i = 0; i < _numColors; ++i) { - std::tuple candidate = get_color(i); + std::tuple candidate = get_color(i, color_mode); unsigned distance = check_color_distance(c, candidate); if (distance < best_distance) { @@ -208,12 +207,12 @@ std::tuple CimbDecoder::avg_color(const Cell& color_cell) con return center.mean_rgb(); } -unsigned CimbDecoder::decode_color(const Cell& color_cell) const +unsigned CimbDecoder::decode_color(const Cell& color_cell, unsigned color_mode) const { if (_numColors <= 1) return 0; auto [r, g, b] = avg_color(color_cell); - return get_best_color(r, g, b); + return get_best_color(r, g, b, color_mode); } bool CimbDecoder::expects_binary_threshold() const diff --git a/src/lib/cimb_translator/CimbDecoder.h b/src/lib/cimb_translator/CimbDecoder.h index 54bb3f2c..5901678a 100644 --- a/src/lib/cimb_translator/CimbDecoder.h +++ b/src/lib/cimb_translator/CimbDecoder.h @@ -13,7 +13,7 @@ class CimbDecoder { public: - CimbDecoder(unsigned symbol_bits, unsigned color_bits, unsigned color_mode=1, bool dark=true, uchar ahashThreshold=0); + CimbDecoder(unsigned symbol_bits, unsigned color_bits, bool dark=true, uchar ahashThreshold=0); const color_correction& get_ccm() const; void update_color_correction(cv::Matx&& ccm); @@ -22,10 +22,10 @@ class CimbDecoder unsigned decode_symbol(const cv::Mat& cell, unsigned& drift_offset, unsigned& best_distance, unsigned cooldown=0xFF) const; unsigned decode_symbol(const bitmatrix& cell, unsigned& drift_offset, unsigned& best_distance, unsigned cooldown=0xFF) const; - std::tuple get_color(int i) const; + std::tuple get_color(int i, unsigned color_mode) const; std::tuple avg_color(const Cell& color_cell) const; - unsigned get_best_color(float r, float g, float b) const; - unsigned decode_color(const Cell& cell) const; + unsigned get_best_color(float r, float g, float b, unsigned color_mode) const; + unsigned decode_color(const Cell& cell, unsigned color_mode) const; bool expects_binary_threshold() const; unsigned symbol_bits() const; @@ -44,7 +44,6 @@ class CimbDecoder unsigned _symbolBits; unsigned _numSymbols; unsigned _numColors; - unsigned _colorMode; bool _dark; uchar _ahashThreshold; }; diff --git a/src/lib/cimb_translator/CimbReader.cpp b/src/lib/cimb_translator/CimbReader.cpp index 34788e36..94518742 100644 --- a/src/lib/cimb_translator/CimbReader.cpp +++ b/src/lib/cimb_translator/CimbReader.cpp @@ -90,7 +90,7 @@ namespace { } } -CimbReader::CimbReader(const cv::Mat& img, CimbDecoder& decoder, bool needs_sharpen, int color_correction) +CimbReader::CimbReader(const cv::Mat& img, CimbDecoder& decoder, unsigned color_mode, bool needs_sharpen, int color_correction) : _image(img) , _fountainColorHeader(0U) , _cellSize(Config::cell_size() + 2) @@ -98,21 +98,22 @@ CimbReader::CimbReader(const cv::Mat& img, CimbDecoder& decoder, bool needs_shar , _decoder(decoder) , _good(_image.cols >= Config::image_size() and _image.rows >= Config::image_size()) , _colorCorrection(color_correction) + , _colorMode(color_mode) { _grayscale = preprocessSymbolGrid(img, needs_sharpen); if (_good and color_correction == 1) simpleColorCorrection(_image, decoder); } -CimbReader::CimbReader(const cv::UMat& img, CimbDecoder& decoder, bool needs_sharpen, int color_correction) - : CimbReader(img.getMat(cv::ACCESS_READ), decoder, needs_sharpen, color_correction) +CimbReader::CimbReader(const cv::UMat& img, CimbDecoder& decoder, unsigned color_mode, bool needs_sharpen, int color_correction) + : CimbReader(img.getMat(cv::ACCESS_READ), decoder, color_mode, needs_sharpen, color_correction) { } unsigned CimbReader::read_color(const PositionData& pos) const { Cell color_cell(_image, pos.x, pos.y, Config::cell_size(), Config::cell_size()); - return _decoder.decode_color(color_cell); + return _decoder.decode_color(color_cell, _colorMode); } unsigned CimbReader::read(PositionData& pos) @@ -219,7 +220,7 @@ void CimbReader::init_ccm(unsigned color_bits, unsigned interleave_blocks, unsig cv::Mat arow = (cv::Mat_(1,3) << std::get<1>(it.second), std::get<2>(it.second), std::get<3>(it.second)); actual.push_back(arow); - cimbar::RGB cc = _decoder.get_color(it.first); + cimbar::RGB cc = _decoder.get_color(it.first, _colorMode); cv::Mat drow = (cv::Mat_(1,3) << std::get<0>(cc), std::get<1>(cc), std::get<2>(cc)); desired.push_back(drow); } diff --git a/src/lib/cimb_translator/CimbReader.h b/src/lib/cimb_translator/CimbReader.h index 8a3478d9..a14c26d6 100644 --- a/src/lib/cimb_translator/CimbReader.h +++ b/src/lib/cimb_translator/CimbReader.h @@ -12,8 +12,8 @@ class CimbReader { public: - CimbReader(const cv::Mat& img, CimbDecoder& decoder, bool needs_sharpen=false, int color_correction=2); - CimbReader(const cv::UMat& img, CimbDecoder& decoder, bool needs_sharpen=false, int color_correction=2); + CimbReader(const cv::Mat& img, CimbDecoder& decoder, unsigned color_mode, bool needs_sharpen=false, int color_correction=2); + CimbReader(const cv::UMat& img, CimbDecoder& decoder, unsigned color_mode, bool needs_sharpen=false, int color_correction=2); unsigned read(PositionData& pos); unsigned read_color(const PositionData& pos) const; @@ -34,4 +34,5 @@ class CimbReader CimbDecoder& _decoder; bool _good; int _colorCorrection; + unsigned _colorMode; }; diff --git a/src/lib/cimb_translator/test/CimbDecoderTest.cpp b/src/lib/cimb_translator/test/CimbDecoderTest.cpp index 7b0b638f..6c8e5f38 100644 --- a/src/lib/cimb_translator/test/CimbDecoderTest.cpp +++ b/src/lib/cimb_translator/test/CimbDecoderTest.cpp @@ -27,7 +27,7 @@ namespace { cv::Rect crop(1+x, 1+y, tile10.cols-2, tile10.rows-2); cv::Mat tile8 = tile10(crop); - bits |= cd.decode_color(tile8) << cd.symbol_bits(); + bits |= cd.decode_color(tile8, 1) << cd.symbol_bits(); return bits; } } @@ -74,32 +74,60 @@ TEST_CASE( "CimbDecoderTest/testPrethresholdDecode", "[unit]" ) } } -TEST_CASE( "CimbDecoderTest/test_get_best_color__dark", "[unit]" ) +TEST_CASE( "CimbDecoderTest/test_get_best_color_mode0", "[unit]" ) { CimbDecoder cd(4, 2); // obvious ones - assertEquals(3, cd.get_best_color(255, 0, 255)); - assertEquals(2, cd.get_best_color(255, 255, 0)); - assertEquals(1, cd.get_best_color(0, 255, 255)); - assertEquals(0, cd.get_best_color(0, 255, 0)); + assertEquals(2, cd.get_best_color(255, 0, 255, 0)); + assertEquals(1, cd.get_best_color(255, 255, 0, 0)); + assertEquals(0, cd.get_best_color(0, 255, 255, 0)); + assertEquals(3, cd.get_best_color(0, 255, 0, 0)); // arbitrary edge cases. We can't really say anything about the value of these colors, but we can at least pick a consistent one - assertEquals(0, cd.get_best_color(0, 0, 0)); - assertEquals(0, cd.get_best_color(70, 70, 70)); + assertEquals(0, cd.get_best_color(0, 0, 0, 0)); + assertEquals(0, cd.get_best_color(70, 70, 70, 0)); // these we can use! - assertEquals(0, cd.get_best_color(20, 200, 20)); - assertEquals(0, cd.get_best_color(50, 155, 50)); + assertEquals(3, cd.get_best_color(20, 200, 20, 0)); + assertEquals(3, cd.get_best_color(50, 155, 50, 0)); - assertEquals(3, cd.get_best_color(200, 30, 200)); - assertEquals(3, cd.get_best_color(155, 50, 155)); + assertEquals(2, cd.get_best_color(200, 30, 200, 0)); + assertEquals(2, cd.get_best_color(155, 50, 155, 0)); - assertEquals(2, cd.get_best_color(200, 155, 20)); - assertEquals(2, cd.get_best_color(155, 155, 50)); + assertEquals(1, cd.get_best_color(200, 155, 20, 0)); + assertEquals(1, cd.get_best_color(155, 155, 50, 0)); - assertEquals(1, cd.get_best_color(50, 155, 200)); - assertEquals(1, cd.get_best_color(50, 155, 155)); + assertEquals(0, cd.get_best_color(50, 155, 200, 0)); + assertEquals(0, cd.get_best_color(50, 155, 155, 0)); +} + +TEST_CASE( "CimbDecoderTest/test_get_best_color_mode1", "[unit]" ) +{ + CimbDecoder cd(4, 2); + + // obvious ones + assertEquals(3, cd.get_best_color(255, 0, 255, 1)); + assertEquals(2, cd.get_best_color(255, 255, 0, 1)); + assertEquals(1, cd.get_best_color(0, 255, 255, 1)); + assertEquals(0, cd.get_best_color(0, 255, 0, 1)); + + // arbitrary edge cases. We can't really say anything about the value of these colors, but we can at least pick a consistent one + assertEquals(0, cd.get_best_color(0, 0, 0, 1)); + assertEquals(0, cd.get_best_color(70, 70, 70, 1)); + + // these we can use! + assertEquals(0, cd.get_best_color(20, 200, 20, 1)); + assertEquals(0, cd.get_best_color(50, 155, 50, 1)); + + assertEquals(3, cd.get_best_color(200, 30, 200, 1)); + assertEquals(3, cd.get_best_color(155, 50, 155, 1)); + + assertEquals(2, cd.get_best_color(200, 155, 20, 1)); + assertEquals(2, cd.get_best_color(155, 155, 50, 1)); + + assertEquals(1, cd.get_best_color(50, 155, 200, 1)); + assertEquals(1, cd.get_best_color(50, 155, 155, 1)); } TEST_CASE( "CimbDecoderTest/testColorDecode", "[unit]" ) @@ -109,7 +137,7 @@ TEST_CASE( "CimbDecoderTest/testColorDecode", "[unit]" ) cv::Mat tile = cimbar::getTile(4, 2, true, 4, 2); cv::resize(tile, tile, cv::Size(10, 10)); - unsigned color = cd.decode_color(Cell(tile)); + unsigned color = cd.decode_color(Cell(tile), 1); assertEquals(2, color); unsigned res = decode(cd, tile); assertEquals(34, res); @@ -128,7 +156,7 @@ TEST_CASE( "CimbDecoderTest/testAllColorDecodes", "[unit]" ) cv::Mat tenxten(10, 10, tile.type(), {0,0,0}); tile.copyTo(tenxten(cv::Rect(cv::Point(1, 1), tile.size()))); - unsigned color = cd.decode_color(Cell(tenxten)); + unsigned color = cd.decode_color(Cell(tenxten), 1); assertEquals(c, color); unsigned res = decode(cd, tenxten); assertEquals(i+16*c, res); diff --git a/src/lib/cimb_translator/test/CimbReaderTest.cpp b/src/lib/cimb_translator/test/CimbReaderTest.cpp index f78e19e1..740f7514 100644 --- a/src/lib/cimb_translator/test/CimbReaderTest.cpp +++ b/src/lib/cimb_translator/test/CimbReaderTest.cpp @@ -34,7 +34,7 @@ TEST_CASE( "CimbReaderTest/testReadOnce", "[unit]" ) cv::Mat sample = TestCimbar::loadSample("6bit/4color_ecc30_fountain_0.png"); CimbDecoder decoder(4, 2); - CimbReader cr(sample, decoder); + CimbReader cr(sample, decoder, 1); assertFalse(cr.done()); @@ -52,12 +52,48 @@ TEST_CASE( "CimbReaderTest/testReadOnce", "[unit]" ) assertFalse(cr.done()); } -TEST_CASE( "CimbReaderTest/testSample", "[unit]" ) +TEST_CASE( "CimbReaderTest/testSample.colormode0", "[unit]" ) { cv::Mat sample = TestCimbar::loadSample("6bit/4color_ecc30_fountain_0.png"); CimbDecoder decoder(4, 2); - CimbReader cr(sample, decoder); + CimbReader cr(sample, decoder, 0); + + // read + int count = 0; + std::map res; + for (int c = 0; c < 22; ++c) + { + PositionData pos; + unsigned bits = cr.read(pos); + res[pos.i] = bits; + + unsigned color_bits = cr.read_color(pos); + res[pos.i] |= color_bits << decoder.symbol_bits(); + ++count; + } + + string expected = "0=0 99=8 11680=3 11681=32 11900=28 11901=25 11904=12 11995=2 11996=8 11998=6 " + "11999=54 12001=29 12004=6 12099=2 12195=57 12196=1 12200=5 12201=0 12298=32 " + "12299=34 12300=30 12399=15"; + assertEquals( expected, turbo::str::join(res) ); + + PositionData pos; + while (!cr.done()) + { + cr.read(pos); + ++count; + } + assertTrue(cr.done()); + assertEquals(12400, count); +} + +TEST_CASE( "CimbReaderTest/testSample.colormode1", "[unit]" ) +{ + cv::Mat sample = TestCimbar::loadSample("6bit/4color_ecc30_fountain_0.png"); + + CimbDecoder decoder(4, 2); + CimbReader cr(sample, decoder, 1); // read int count = 0; @@ -88,12 +124,13 @@ TEST_CASE( "CimbReaderTest/testSample", "[unit]" ) assertEquals(12400, count); } + TEST_CASE( "CimbReaderTest/testSampleMessy", "[unit]" ) { cv::Mat sample = TestCimbar::loadSample("6bit/4_30_f0_627_extract.jpg"); CimbDecoder decoder(4, 2); - CimbReader cr(sample, decoder); + CimbReader cr(sample, decoder, 1); // read int count = 0; @@ -127,7 +164,7 @@ TEST_CASE( "CimbReaderTest/testBad", "[unit]" ) cv::Mat sample = TestCimbar::loadSample("6bit/4_30_f2_246.jpg"); CimbDecoder decoder(4, 2); - CimbReader cr(sample, decoder); + CimbReader cr(sample, decoder, 1); // refuse to do anything assertTrue( cr.done() ); @@ -144,7 +181,7 @@ TEST_CASE( "CimbReaderTest/testCCM", "[unit]" ) TestableCimbDecoder decoder(4, 2); decoder.internal_ccm() = color_correction(); - CimbReader cr(sample, decoder); + CimbReader cr(sample, decoder, 1); // this is the header value for the sample -- we could imitate what the Decoder does // and compute it from the symbols, but that seems like overkill for this test. @@ -178,7 +215,7 @@ TEST_CASE( "CimbReaderTest/testCCM.Disabled", "[unit]" ) TestableCimbDecoder decoder(4, 2); decoder.internal_ccm() = color_correction(); - CimbReader cr(sample, decoder, false, false); + CimbReader cr(sample, decoder, 1, false, false); assertFalse( decoder.get_ccm().active() ); @@ -200,7 +237,7 @@ TEST_CASE( "CimbReaderTest/testCCM.VeryNecessary", "[unit]" ) TestableCimbDecoder decoder(4, 2); decoder.internal_ccm() = color_correction(); - CimbReader cr(sample, decoder); + CimbReader cr(sample, decoder, 1); // this is the header value for the sample -- we could imitate what the Decoder does // and compute it from the symbols, but that seems like overkill for this test. diff --git a/src/lib/encoder/Decoder.h b/src/lib/encoder/Decoder.h index c7cbbb3b..f70cec4a 100644 --- a/src/lib/encoder/Decoder.h +++ b/src/lib/encoder/Decoder.h @@ -16,22 +16,22 @@ class Decoder { public: - Decoder(int ecc_bytes=-1, int color_bits=-1, unsigned color_mode=1, bool coupled=false, bool interleave=true); + Decoder(int ecc_bytes=-1, int color_bits=-1, bool interleave=true); template - unsigned decode(const MAT& img, STREAM& ostream, bool should_preprocess=false, int color_correction=2); + unsigned decode(const MAT& img, STREAM& ostream, unsigned color_mode=1, bool should_preprocess=false, int color_correction=2); template - unsigned decode_fountain(const MAT& img, STREAM& ostream, bool should_preprocess=false, int color_correction=2); + unsigned decode_fountain(const MAT& img, STREAM& ostream, unsigned color_mode=1, bool should_preprocess=false, int color_correction=2); - unsigned decode(std::string filename, std::string output); + unsigned decode(std::string filename, std::string output, unsigned color_mode=1); bool load_ccm(std::string filename); bool save_ccm(std::string filename); protected: template - unsigned do_decode(CimbReader& reader, STREAM& ostream); + unsigned do_decode(CimbReader& reader, STREAM& ostream, bool legacy_mode); template unsigned do_decode_coupled(CimbReader& reader, STREAM& ostream); @@ -41,23 +41,19 @@ class Decoder unsigned _eccBlockSize; unsigned _colorBits; unsigned _bitsPerOp; - bool _coupled; - unsigned _colorMode; unsigned _interleaveBlocks; unsigned _interleavePartitions; CimbDecoder _decoder; }; -inline Decoder::Decoder(int ecc_bytes, int color_bits, unsigned color_mode, bool coupled, bool interleave) +inline Decoder::Decoder(int ecc_bytes, int color_bits, bool interleave) : _eccBytes(ecc_bytes >= 0? ecc_bytes : cimbar::Config::ecc_bytes()) , _eccBlockSize(cimbar::Config::ecc_block_size()) , _colorBits(color_bits >= 0? color_bits : cimbar::Config::color_bits()) , _bitsPerOp(cimbar::Config::symbol_bits() + _colorBits) - , _coupled(coupled) - , _colorMode(color_mode) , _interleaveBlocks(interleave? cimbar::Config::interleave_blocks() : 0) , _interleavePartitions(cimbar::Config::interleave_partitions()) - , _decoder(cimbar::Config::symbol_bits(), _colorBits, color_mode, cimbar::Config::dark(), 0xFF) + , _decoder(cimbar::Config::symbol_bits(), _colorBits, cimbar::Config::dark(), 0xFF) { } @@ -74,9 +70,9 @@ inline Decoder::Decoder(int ecc_bytes, int color_bits, unsigned color_mode, bool * * */ template -inline unsigned Decoder::do_decode(CimbReader& reader, STREAM& ostream) +inline unsigned Decoder::do_decode(CimbReader& reader, STREAM& ostream, bool legacy_mode) { - if (_coupled) + if (legacy_mode) return do_decode_coupled(reader, ostream); std::vector interleaveLookup = Interleave::interleave_reverse(reader.num_reads(), _interleaveBlocks, _interleavePartitions); @@ -109,7 +105,7 @@ inline unsigned Decoder::do_decode(CimbReader& reader, STREAM& ostream) } // do color correction init, now that we (hopefully) have some fountain headers from the symbol decode - reader.init_ccm(_colorBits, _interleaveBlocks, _interleavePartitions, cimbar::Config::fountain_chunks_per_frame(_bitsPerOp, _coupled and _colorMode==0)); + reader.init_ccm(_colorBits, _interleaveBlocks, _interleavePartitions, cimbar::Config::fountain_chunks_per_frame(_bitsPerOp, legacy_mode)); bitbuffer colorBits(cimbar::Config::capacity(_colorBits)); // then decode colors. @@ -161,27 +157,27 @@ inline unsigned Decoder::do_decode_coupled(CimbReader& reader, STREAM& ostream) } template -inline unsigned Decoder::decode(const MAT& img, STREAM& ostream, bool should_preprocess, int color_correction) +inline unsigned Decoder::decode(const MAT& img, STREAM& ostream, unsigned color_mode, bool should_preprocess, int color_correction) { - CimbReader reader(img, _decoder, should_preprocess, color_correction); - return do_decode(reader, ostream); + CimbReader reader(img, _decoder, color_mode, should_preprocess, color_correction); + return do_decode(reader, ostream, color_mode==0); } template -inline unsigned Decoder::decode_fountain(const MAT& img, FOUNTAINSTREAM& ostream, bool should_preprocess, int color_correction) +inline unsigned Decoder::decode_fountain(const MAT& img, FOUNTAINSTREAM& ostream, unsigned color_mode, bool should_preprocess, int color_correction) { - CimbReader reader(img, _decoder, should_preprocess, color_correction); + CimbReader reader(img, _decoder, color_mode, should_preprocess, color_correction); aligned_stream aligner(ostream, ostream.chunk_size(), 0, std::bind(&CimbReader::update_metadata, &reader, std::placeholders::_1, std::placeholders::_2)); - return do_decode(reader, aligner); + return do_decode(reader, aligner, color_mode==0); } -inline unsigned Decoder::decode(std::string filename, std::string output) +inline unsigned Decoder::decode(std::string filename, std::string output, unsigned color_mode) { cv::Mat img = cv::imread(filename); cv::cvtColor(img, img, cv::COLOR_BGR2RGB); std::ofstream f(output); - return decode(img, f, false); + return decode(img, f, color_mode, false); } inline bool Decoder::load_ccm(std::string filename) diff --git a/src/lib/encoder/test/DecoderTest.cpp b/src/lib/encoder/test/DecoderTest.cpp index 350c7314..c24c1d9b 100644 --- a/src/lib/encoder/test/DecoderTest.cpp +++ b/src/lib/encoder/test/DecoderTest.cpp @@ -65,9 +65,9 @@ TEST_CASE( "DecoderTest/testDecode.4c", "[unit]" ) // legacy format MakeTempDirectory tempdir; - Decoder dec(0, 2, 0, true); + Decoder dec(0, 2, true); std::string decodedFile = tempdir.path() / "testDecode.txt"; - unsigned bytesDecoded = dec.decode(TestCimbar::getSample("6bit/4color_ecc30_fountain_0.png"), decodedFile); + unsigned bytesDecoded = dec.decode(TestCimbar::getSample("6bit/4color_ecc30_fountain_0.png"), decodedFile, 0); assertEquals( 9300, bytesDecoded ); assertEquals( "7e1919b1210ccc332fc56e8b35cccd622d980f03c6c3b32338bb00aa4b6a22a2", get_hash(decodedFile) ); @@ -77,9 +77,9 @@ TEST_CASE( "DecoderTest/testDecodeEcc.4c", "[unit]" ) { MakeTempDirectory tempdir; - Decoder dec(30, 2, 0, true); + Decoder dec(30, 2, true); std::string decodedFile = tempdir.path() / "testDecode.txt"; - unsigned bytesDecoded = dec.decode(TestCimbar::getSample("6bit/4color_ecc30_fountain_0.png"), decodedFile); + unsigned bytesDecoded = dec.decode(TestCimbar::getSample("6bit/4color_ecc30_fountain_0.png"), decodedFile, 0); assertEquals( 7500, bytesDecoded ); assertEquals( "382c76644a4dff475c5793c5fe061e35e47be252010d29aeaf8d93ee6a3f7045", get_hash(decodedFile) ); @@ -90,9 +90,9 @@ TEST_CASE( "DecoderTest/testDecode.Sample4c", "[unit]" ) // regression test -- useful for now, but is very brittle MakeTempDirectory tempdir; - Decoder dec(0, 2, 0, true); + Decoder dec(0, 2, true); std::string decodedFile = tempdir.path() / "testDecode.txt"; - unsigned bytesDecoded = dec.decode(TestCimbar::getSample("6bit/4_30_f0_627_extract.jpg"), decodedFile); + unsigned bytesDecoded = dec.decode(TestCimbar::getSample("6bit/4_30_f0_627_extract.jpg"), decodedFile, 0); assertEquals( 9300, bytesDecoded ); if (CV_VERSION_MAJOR == 4) diff --git a/src/lib/encoder/test/EncoderRoundTripTest.cpp b/src/lib/encoder/test/EncoderRoundTripTest.cpp index f0f4d17b..a14e0bbb 100644 --- a/src/lib/encoder/test/EncoderRoundTripTest.cpp +++ b/src/lib/encoder/test/EncoderRoundTripTest.cpp @@ -41,7 +41,7 @@ TEST_CASE( "EncoderRoundTripTest/testFountain.Pad", "[unit]" ) Decoder dec(30); fountain_decoder_sink> fds(tempdir.path(), cimbar::Config::fountain_chunk_size(30, 6, false)); - unsigned bytesDecoded = dec.decode_fountain(encodedImg, fds); + unsigned bytesDecoded = dec.decode_fountain(encodedImg, fds, 1); assertEquals( 7500, bytesDecoded ); std::string decodedContents = File(tempdir.path() / "0.626").read_all(); @@ -71,7 +71,7 @@ TEST_CASE( "EncoderRoundTripTest/testStreaming", "[unit]" ) std::optional frame = enc.encode_next(*fes); assertTrue( frame ); - unsigned bytesDecoded = dec.decode_fountain(*frame, fds); + unsigned bytesDecoded = dec.decode_fountain(*frame, fds, 1); assertEquals( 7500, bytesDecoded ); if (fds.num_done()) From f5607a37619795e957668dbacbb06d68cc99c18b Mon Sep 17 00:00:00 2001 From: sz Date: Thu, 29 Feb 2024 20:28:08 -0600 Subject: [PATCH 2/3] Add test to validate we can survive giving the fountain sink the wrong data ... since we're using that as the detection mechanism for auto-detect. Also, a simplification to Decoder::do_decode() --- src/lib/encoder/Decoder.h | 5 ++- src/lib/encoder/test/EncoderRoundTripTest.cpp | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/lib/encoder/Decoder.h b/src/lib/encoder/Decoder.h index f70cec4a..baa58801 100644 --- a/src/lib/encoder/Decoder.h +++ b/src/lib/encoder/Decoder.h @@ -80,7 +80,6 @@ inline unsigned Decoder::do_decode(CimbReader& reader, STREAM& ostream, bool leg colorPositions.resize(reader.num_reads()); // the number of cells == reader.num_reads(). Can we calculate this from config at compile time? Do we care? unsigned bitsPerSymbol = cimbar::Config::symbol_bits(); - unsigned bytesDecoded = 0; { bitbuffer symbolBits(cimbar::Config::capacity(bitsPerSymbol)); // read symbols first @@ -116,8 +115,8 @@ inline unsigned Decoder::do_decode(CimbReader& reader, STREAM& ostream, bool leg } reed_solomon_stream rss(ostream, _eccBytes, _eccBlockSize); - bytesDecoded += colorBits.flush(rss); // will return the pos after this flush(), which includes the previous flush()... - return bytesDecoded; + // flush() will return the (good) cumulative bytes written to the underlying stream + return colorBits.flush(rss); } template diff --git a/src/lib/encoder/test/EncoderRoundTripTest.cpp b/src/lib/encoder/test/EncoderRoundTripTest.cpp index a14e0bbb..27f07a4c 100644 --- a/src/lib/encoder/test/EncoderRoundTripTest.cpp +++ b/src/lib/encoder/test/EncoderRoundTripTest.cpp @@ -46,6 +46,42 @@ TEST_CASE( "EncoderRoundTripTest/testFountain.Pad", "[unit]" ) std::string decodedContents = File(tempdir.path() / "0.626").read_all(); assertEquals( "hello", decodedContents ); + + assertEquals( 1, fds.num_done() ); +} + +TEST_CASE( "EncoderRoundTripTest/testFountain.SinkMismatch", "[unit]" ) +{ + MakeTempDirectory tempdir; + + std::string inputFile = tempdir.path() / "hello.txt"; + std::string outPrefix = tempdir.path() / "encoder.fountain"; + + { + std::ofstream f(inputFile); + f << "hello"; // 5 bytes! + } + + // will be padded so the fountain encoding is happy. The encoded image looks suspiciously non-random! + Encoder enc(30, 4, 2); + assertEquals( 1, enc.encode_fountain(inputFile, outPrefix) ); + + uint64_t hash = 0xeecc8800efce8c48; + std::string path = fmt::format("{}_0.png", outPrefix); + cv::Mat encodedImg = cv::imread(path); + cv::cvtColor(encodedImg, encodedImg, cv::COLOR_BGR2RGB); + assertEquals( hash, image_hash::average_hash(encodedImg) ); + + // decoder + Decoder dec(30); + // sink with a mismatched fountain_chunk_size + // importantly, the sink expects a *larger* chunk than we'll give it... + fountain_decoder_sink> fds(tempdir.path(), cimbar::Config::fountain_chunk_size(30, 6, true)); + + unsigned bytesDecoded = dec.decode_fountain(encodedImg, fds, 1); + assertEquals( 7500, bytesDecoded ); + + assertEquals( 0, fds.num_done() ); } TEST_CASE( "EncoderRoundTripTest/testStreaming", "[unit]" ) From f1e136f143ec87c8a751cdbc28d3403cd2db07d4 Mon Sep 17 00:00:00 2001 From: sz Date: Thu, 29 Feb 2024 23:35:03 -0600 Subject: [PATCH 3/3] Decoder: when we known there's a mismatch between the fountain chunk size... ... and the fountain *stream* (sink)'s chunk size, we shouldn't send it the decoded bytes. It's not going to be able to do anything useful with them. Instead, we'll have a /dev/null style "null_stream" that just tracks how many bytes were written to it. --- src/lib/encoder/Decoder.h | 19 +++++++++++-- src/lib/encoder/reed_solomon_stream.h | 2 +- src/lib/encoder/test/EncoderRoundTripTest.cpp | 10 ++++--- src/lib/util/null_stream.h | 28 +++++++++++++++++++ 4 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/lib/util/null_stream.h diff --git a/src/lib/encoder/Decoder.h b/src/lib/encoder/Decoder.h index baa58801..c7f229f8 100644 --- a/src/lib/encoder/Decoder.h +++ b/src/lib/encoder/Decoder.h @@ -8,6 +8,7 @@ #include "cimb_translator/Config.h" #include "cimb_translator/Interleave.h" #include "util/File.h" +#include "util/null_stream.h" #include #include @@ -166,8 +167,22 @@ template inline unsigned Decoder::decode_fountain(const MAT& img, FOUNTAINSTREAM& ostream, unsigned color_mode, bool should_preprocess, int color_correction) { CimbReader reader(img, _decoder, color_mode, should_preprocess, color_correction); - aligned_stream aligner(ostream, ostream.chunk_size(), 0, std::bind(&CimbReader::update_metadata, &reader, std::placeholders::_1, std::placeholders::_2)); - return do_decode(reader, aligner, color_mode==0); + bool legacy_mode = color_mode == 0; + unsigned chunk_size = cimbar::Config::fountain_chunk_size(_eccBytes, _bitsPerOp, legacy_mode); + auto update_md_fun = std::bind(&CimbReader::update_metadata, &reader, std::placeholders::_1, std::placeholders::_2); + + // we don't want to feed the fountain stream bad data, so we eat the decode if we have a mismatch + // we still might succeed the decode, in which case (hopefully) the positive bytes we return will + // tell our caller to fix the underlying ostream so the next round will work. + if (ostream.chunk_size() != chunk_size) + { + null_stream devnull; + aligned_stream aligner(devnull, chunk_size, 0, update_md_fun); + return do_decode(reader, aligner, legacy_mode); + } + + aligned_stream aligner(ostream, ostream.chunk_size(), 0, update_md_fun); + return do_decode(reader, aligner, legacy_mode); } inline unsigned Decoder::decode(std::string filename, std::string output, unsigned color_mode) diff --git a/src/lib/encoder/reed_solomon_stream.h b/src/lib/encoder/reed_solomon_stream.h index 2f883f0f..3d7f912a 100644 --- a/src/lib/encoder/reed_solomon_stream.h +++ b/src/lib/encoder/reed_solomon_stream.h @@ -88,7 +88,7 @@ class reed_solomon_stream bool _good; }; -inline std::ifstream& operator<<(std::ifstream& s, const ReedSolomon::BadChunk& chunk) +inline std::ifstream& operator<<(std::ifstream& s, const ReedSolomon::BadChunk&) { return s; } diff --git a/src/lib/encoder/test/EncoderRoundTripTest.cpp b/src/lib/encoder/test/EncoderRoundTripTest.cpp index 27f07a4c..0193e06e 100644 --- a/src/lib/encoder/test/EncoderRoundTripTest.cpp +++ b/src/lib/encoder/test/EncoderRoundTripTest.cpp @@ -64,9 +64,10 @@ TEST_CASE( "EncoderRoundTripTest/testFountain.SinkMismatch", "[unit]" ) // will be padded so the fountain encoding is happy. The encoded image looks suspiciously non-random! Encoder enc(30, 4, 2); + enc.set_legacy_mode(); assertEquals( 1, enc.encode_fountain(inputFile, outPrefix) ); - uint64_t hash = 0xeecc8800efce8c48; + uint64_t hash = 0xaecc8c00efce8c28; std::string path = fmt::format("{}_0.png", outPrefix); cv::Mat encodedImg = cv::imread(path); cv::cvtColor(encodedImg, encodedImg, cv::COLOR_BGR2RGB); @@ -75,10 +76,11 @@ TEST_CASE( "EncoderRoundTripTest/testFountain.SinkMismatch", "[unit]" ) // decoder Decoder dec(30); // sink with a mismatched fountain_chunk_size - // importantly, the sink expects a *larger* chunk than we'll give it... - fountain_decoder_sink> fds(tempdir.path(), cimbar::Config::fountain_chunk_size(30, 6, true)); + // importantly, the sink expects a *smaller* chunk than we'll give it... + // because that's a more interesting test... + fountain_decoder_sink> fds(tempdir.path(), cimbar::Config::fountain_chunk_size(30, 6, false)); - unsigned bytesDecoded = dec.decode_fountain(encodedImg, fds, 1); + unsigned bytesDecoded = dec.decode_fountain(encodedImg, fds, 0); assertEquals( 7500, bytesDecoded ); assertEquals( 0, fds.num_done() ); diff --git a/src/lib/util/null_stream.h b/src/lib/util/null_stream.h new file mode 100644 index 00000000..09560e8f --- /dev/null +++ b/src/lib/util/null_stream.h @@ -0,0 +1,28 @@ +/* This code is subject to the terms of the Mozilla Public License, v.2.0. http://mozilla.org/MPL/2.0/. */ +#pragma once + +class null_stream +{ +public: + null_stream() + {} + + null_stream& write(const char*, unsigned length) + { + _count += length; + return *this; + } + + bool good() const + { + return true; + } + + long tellp() const + { + return _count; + } + +protected: + long _count = 0; +};