Skip to content

Commit

Permalink
Merge commit '1531d19d51cb7e889495a6f1b5f2b1cf0c5c347a'
Browse files Browse the repository at this point in the history
  • Loading branch information
sz3 committed Nov 30, 2020
2 parents 202cfce + 1531d19 commit 5b28aba
Show file tree
Hide file tree
Showing 52 changed files with 5,538 additions and 2,986 deletions.
2 changes: 1 addition & 1 deletion app/src/cpp/libcimbar/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
submodules: 'recursive'

- name: Install dependencies
run: sudo apt-get install libopencv-dev ${{ matrix.config.extra-packages }}
run: sudo apt-get update && sudo apt-get install libopencv-dev ${{ matrix.config.extra-packages }}

- name: Create Build Environment
run: cmake -E make_directory ${{runner.workspace}}/build
Expand Down
2 changes: 1 addition & 1 deletion app/src/cpp/libcimbar/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ dist/
web/cimbar_js.js
web/cimbar_js.wasm
CMakeCache.txt
CMakeLists.txt.user
CMakeLists.txt.*
CTestTestfile.cmake
Makefile
callgrind.out*
Expand Down
26 changes: 13 additions & 13 deletions app/src/cpp/libcimbar/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@
* The default ecc setting is 30/155, which is how we go from 9300 -> 7500 bytes of real data for a 4-color cimbar image.
* Reed Solomon is not an ideal for this use case -- specifically, it corrects byte errors, and cimbar errors tend to involve 1-3 bits at a time. However, since Reed Solomon implementations are ubiquitous, I used it for this prototype.

## Are we 100 kb/s yet?
## Current sustained benchmark

* No. :|
* well, if you include compression, yes! Nailed it! :sunglasses:
* 4-color cimbar with ecc=30:
* 2,980,556 bytes (after compression) in 36s -> 662 kilobits/s

* 43 seconds to transfer a 3.0 MB file == 69 kb/s. This is the current benchmark on my old Android phone.
* specifically, this is using https://github.com/sz3/cfc, running with 4 CPU threads on a Qualcomm Snapdragon 625
* 8-color cimbar with ecc=30:
* 2,980,556 bytes in 31s -> 769 kilobits/s

* details:
* these numbers are use https://github.com/sz3/cfc, running with 4 CPU threads on a Qualcomm Snapdragon 625
* perhaps I will buy a new cell phone to inflate the benchmark numbers.
* for smaller (~1MB) files, I've seen 75 kb/s.
* for artificial benchmark numbers, a decent strategy is to turn down the ecc settings and say a prayer to your prefered deity
* this is neither reliable, nor representative of the usable case, however.
* the sender commandline is `./cimbar_send /path/to/file -s`
* the `shakycam` option allows cfc to quickly discard ghosted frames, and spend more time decoding real data.
* burst rate can be higher (or lower)
* to this end, lower ecc settings *can* provide better burst rates
* 8-color cimbar is considerably more sensitive to lighting conditions. Notably, decoding has some issues with dim screens.

* we have semi-reasonable excuses:
* the android app (cfc) doesn't do anything useful with camera settings. We capture at ~25 fps, and are forced to discard a full 60% of the images due various ghosting and auto-focus issues. A 100% success rate won't happen -- some auto-focus will always be useful, and camera capture rate and the screen display rate are not going to be perfectly aligned, resulting in some number of screen-torn frames. But I'm pretty confident we can do better than 40%!
* editorial comment: the android camera APIs are, to put it charitably, a raging tire fire
* cfc is currently using the legacy "camera" API. The camera2 API has eluded my comprehension.
* cfc is not currently using the GPU effectively. OpenCV on Android can code on the GPU via OpenCL (though you need a custom build), but... it's complicated...
23 changes: 18 additions & 5 deletions app/src/cpp/libcimbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@
### [DETAILS](DETAILS.md) | [PERFORMANCE](PERFORMANCE.md) | [TODO](TODO.md)

## libcimbar: Color Icon Matrix Barcodes
And the quest for 100 kb/s over the air gap...

## What is it?
An experimental barcode format for air-gapped data transfer.

`cimbar` is a proof-of-concept high-density 2D barcode format. Data is stored in a grid of colored tiles -- bits are encoded based on which tile is chosen, and which color is chosen to draw the tile. Reed Solomon error correction is applied on the data, to account for the lossy nature of the video -> digital decoding. Sub-1% error rates are expected, and corrected.
770 kilobits/s through a smartphone camera!

`libcimbar`, the optimized implementation, includes a simple protocol for file encoding based on fountain codes (`wirehair`). Files of up to 33MB can be encoded in a series of cimbar codes, which can be output as images or a live video feed. Once enough distinct image frames have been decoded successfully, the file will be reconstructed successfully. This is true even if the images are received out of order, or if some have been corrupted or are missing.
## Explain?

The encoder outputs an animated barcode to a computer or smartphone screen:
* Encoder web app: https://cimbar.org

While the decoder is a cell phone app that uses the phone camera to read the animated barcode:
* Decoder android app: https://github.com/sz3/cfc

No internet/bluetooth/NFC/etc is used. All data is transmitted through the camera lens. You can try it out yourself, or take my word that it works. :)

## How does it work?

`cimbar` is a high-density 2D barcode format. Data is stored in a grid of colored tiles -- bits are encoded based on which tile is chosen, and which color is chosen to draw the tile. Reed Solomon error correction is applied on the data, to account for the lossy nature of the video -> digital decoding. Sub-1% error rates are expected, and corrected.

`libcimbar`, this optimized implementation, includes a simple protocol for file encoding based on fountain codes (`wirehair`). Files of up to 33MB can be encoded in a series of cimbar codes, which can be output as images or a live video feed. Once enough distinct image frames have been decoded successfully, the file will be reconstructed successfully. This is true even if the images are received out of order, or if some have been corrupted or are missing.

## Platforms

Expand Down Expand Up @@ -46,7 +59,7 @@ make install

By default, libcimbar will try to install build products under `./dist/bin/`.

There is also a beta emscripten+WASM build for the encoder. See [WASM](WASM.md).
To build the emscripten+WASM encoder (what cimbar.org uses), see [WASM](WASM.md).

## Usage

Expand Down
3 changes: 0 additions & 3 deletions app/src/cpp/libcimbar/src/exe/cimbar/cimbar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ int main(int argc, char** argv)
compressionLevel = result["compression"].as<int>();
ecc = result["ecc"].as<unsigned>();

if (fountain)
FountainInit::init();

if (encode)
{
Encoder en(ecc, cimbar::Config::symbol_bits(), colorBits);
Expand Down
2 changes: 0 additions & 2 deletions app/src/cpp/libcimbar/src/exe/cimbar_send/send.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ int main(int argc, char** argv)
fps = defaultFps;
unsigned delay = 1000 / fps;

FountainInit::init();

bool dark = true;
bool use_rotatecam = result.count("rotatecam");
bool use_shakycam = result.count("shakycam");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include "encoder/Encoder.h"

#include "compression/zstd_decompressor.h"
#include "fountain/FountainInit.h"
#include "fountain/fountain_decoder_sink.h"
#include "image_hash/average_hash.h"
#include "serialize/format.h"
Expand All @@ -18,7 +17,6 @@

TEST_CASE( "EncoderRoundTripTest/testFountain.Pad", "[unit]" )
{
FountainInit::init();
MakeTempDirectory tempdir;

std::string inputFile = tempdir.path() / "hello.txt";
Expand Down Expand Up @@ -52,7 +50,6 @@ TEST_CASE( "EncoderRoundTripTest/testFountain.Pad", "[unit]" )

TEST_CASE( "EncoderRoundTripTest/testStreaming", "[unit]" )
{
FountainInit::init();
MakeTempDirectory tempdir;

//input
Expand Down
3 changes: 0 additions & 3 deletions app/src/cpp/libcimbar/src/lib/encoder/test/EncoderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ TEST_CASE( "EncoderTest/testVanilla", "[unit]" )

TEST_CASE( "EncoderTest/testFountain", "[unit]" )
{
FountainInit::init();
MakeTempDirectory tempdir;

std::string inputFile = TestCimbar::getProjectDir() + "/LICENSE";
Expand All @@ -65,7 +64,6 @@ TEST_CASE( "EncoderTest/testFountain", "[unit]" )

TEST_CASE( "EncoderTest/testFountain.Compress", "[unit]" )
{
FountainInit::init();
MakeTempDirectory tempdir;

std::string inputFile = TestCimbar::getProjectDir() + "/LICENSE";
Expand All @@ -84,7 +82,6 @@ TEST_CASE( "EncoderTest/testFountain.Compress", "[unit]" )
TEST_CASE( "EncoderTest/testPiecemealFountainEncoder", "[unit]" )
{
// use the fountain_encoder_stream directly on a char*,int pair
FountainInit::init();
MakeTempDirectory tempdir;
Encoder enc(40, 4, 2);

Expand Down
11 changes: 3 additions & 8 deletions app/src/cpp/libcimbar/src/lib/fountain/FountainDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,12 @@

class FountainDecoder
{
public:
static bool init()
{
return FountainInit::init();
}

public:
FountainDecoder(size_t length, size_t packet_size)
: _codec(wirehair_decoder_create(nullptr, length, packet_size))
, _length(length)
: _length(length)
{
FountainInit::init();
_codec = wirehair_decoder_create(nullptr, length, packet_size);
}

~FountainDecoder()
Expand Down
23 changes: 13 additions & 10 deletions app/src/cpp/libcimbar/src/lib/fountain/FountainEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,36 @@

#include "FountainInit.h"
#include "wirehair/wirehair.h"
#include <array>
#include <cassert>
#include <vector>
#include <iostream>

// will need to split large files

class FountainEncoder
{
public:
static bool init()
protected:
void swap(FountainEncoder& other) throw()
{
return FountainInit::init();
std::swap(_codec, other._codec);
std::swap(_packetSize, other._packetSize);
}

public:
FountainEncoder(const uint8_t* data, size_t length, size_t packet_size)
: _codec(wirehair_encoder_create(nullptr, data, length, packet_size))
, _packetSize(packet_size)
: _packetSize(packet_size)
{
FountainInit::init();
_codec = wirehair_encoder_create(nullptr, data, length, packet_size);
}

~FountainEncoder()
{
wirehair_free(_codec);
}

FountainEncoder& operator=(FountainEncoder temp)
{
temp.swap(*this);
return *this;
}

bool good() const
{
return _codec != nullptr;
Expand Down
2 changes: 1 addition & 1 deletion app/src/cpp/libcimbar/src/lib/fountain/FountainInit.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ namespace FountainInit {
static bool init()
{
static WirehairResult res = wirehair_init();
return res;
return !res;
}
}
17 changes: 15 additions & 2 deletions app/src/cpp/libcimbar/src/lib/fountain/fountain_encoder_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ class fountain_encoder_stream
}

public:

template <typename STREAM>
static fountain_encoder_stream::ptr create(STREAM& stream, unsigned buffer_size, uint8_t encode_id=0)
{
Expand All @@ -41,9 +40,23 @@ class fountain_encoder_stream
return fountain_encoder_stream::ptr( new fountain_encoder_stream(buffs.str(), buffer_size, encode_id & 0x7F) );
}

// this resets the stream!
// but you might need to do if you change other parameters
// ex: different ECC settings => different payload size => different fountain buffer size
bool reset_and_resize_buffer(unsigned buffer_size)
{
if (buffer_size > _data.size())
return false;

_buffer.resize(buffer_size);
_encoder = FountainEncoder((uint8_t*)_data.data(), _data.size(), block_size());
reset();
return true;
}

bool good() const
{
return _encoder.good();
return _encoder.good() and _data.size() > _encoder.packet_size();
}

void reset()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ namespace {

TEST_CASE( "FountainEncodingTest/testEncoder", "[unit]" )
{
FountainEncoder::init();

std::string content;
for (int i = 0; i < 1000; ++i)
content += "0123456789";
Expand All @@ -51,8 +49,6 @@ TEST_CASE( "FountainEncodingTest/testEncoder", "[unit]" )
TEST_CASE( "FountainEncodingTest/testEncodingAndDecoding", "[unit]" )
{
static const unsigned packetSize = 1400;
FountainDecoder::init();
FountainEncoder::init();

unsigned messageSize = 10000;
std::string message;
Expand Down Expand Up @@ -104,7 +100,6 @@ TEST_CASE( "FountainEncodingTest/testConsistency", "[unit]" )
{
const std::vector<std::string> precomputed = blocks();
static const unsigned packetSize = 626;
FountainEncoder::init();

unsigned messageSize = 1000;
std::string message;
Expand Down Expand Up @@ -145,7 +140,6 @@ TEST_CASE( "FountainEncodingTest/testWhichN", "[unit]" )
{
const std::vector<std::string> precomputed = blocks();
static const unsigned packetSize = 624;
FountainEncoder::init();

unsigned messageSize = 6000;
std::string message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ TEST_CASE( "FountainMetadataTest/testFromBytes.Short", "[unit]" )
assertEquals( 0x070809, inmd.file_size() );
}

TEST_CASE( "FountainMetadataTest/testFromBytes.BigFile", "[unit]" )
{
std::string buff = {(char)0x81, 7, 8, 9};
FountainMetadata inmd(buff.data(), buff.size());
assertEquals( 1, inmd.encode_id() );
assertEquals( 0x1070809, inmd.file_size() );
}

TEST_CASE( "FountainMetadataTest/testThings", "[unit]" )
{
FountainMetadata inmd(0_uchar, 0xd0731f);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ namespace {

TEST_CASE( "FountainSinkTest/testDefault", "[unit]" )
{
FountainInit::init();
MakeTempDirectory tempdir;

fountain_decoder_sink<std::ofstream> sink(tempdir.path(), 690);
Expand Down Expand Up @@ -85,7 +84,6 @@ TEST_CASE( "FountainSinkTest/testDefault", "[unit]" )

TEST_CASE( "FountainSinkTest/testMultipart", "[unit]" )
{
FountainInit::init();
MakeTempDirectory tempdir;

fountain_decoder_sink<std::ofstream> sink(tempdir.path(), 690);
Expand Down Expand Up @@ -117,7 +115,6 @@ TEST_CASE( "FountainSinkTest/testSameFrameManyTimes", "[unit]" )
{
// if you give wirehair the same frame (under certain circumstances), you get a seg fault
// sometimes it's fine. The docs say "don't do it", so FountainDecoder acts as the bouncer.
FountainInit::init();
MakeTempDirectory tempdir;

fountain_decoder_sink<std::ofstream> sink(tempdir.path(), 690);
Expand Down
Loading

0 comments on commit 5b28aba

Please sign in to comment.