Skip to content

Commit

Permalink
Merge pull request #31 from sz3/mode-b-and-other-goodness
Browse files Browse the repository at this point in the history
Support new cimbar format "mode B", minor UI change = version 0.6.0f
  • Loading branch information
sz3 authored Feb 26, 2024
2 parents 870ab27 + c34d5ee commit c47755e
Show file tree
Hide file tree
Showing 73 changed files with 976 additions and 339 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ android {
applicationId "org.cimbar.camerafilecopy"
minSdkVersion 21
targetSdkVersion 30
versionCode 11
versionName "0.5.15"
versionCode 13
versionName "0.6.0f"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
Expand Down
32 changes: 11 additions & 21 deletions app/src/cpp/cfc-cpp/MultiThreadedDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
class MultiThreadedDecoder
{
public:
MultiThreadedDecoder(std::string data_path, int color_bits);
MultiThreadedDecoder(std::string data_path, bool legacy_mode);

inline static clock_t bytes = 0;
inline static clock_t perfect = 0;
Expand All @@ -26,11 +26,10 @@ class MultiThreadedDecoder
inline static clock_t extractTicks = 0;

bool add(cv::Mat mat);
bool decode(const cv::Mat& img, bool should_preprocess);

void stop();

int color_bits() const;
bool legacy_mode() const;
unsigned num_threads() const;
unsigned backlog() const;
unsigned files_in_flight() const;
Expand All @@ -43,20 +42,20 @@ class MultiThreadedDecoder
void save(const cv::Mat& img);

protected:
int _colorBits;
bool _legacyMode;
Decoder _dec;
unsigned _numThreads;
turbo::thread_pool _pool;
concurrent_fountain_decoder_sink<cimbar::zstd_decompressor<std::ofstream>> _writer;
std::string _dataPath;
};

inline MultiThreadedDecoder::MultiThreadedDecoder(std::string data_path, int color_bits)
: _colorBits(color_bits)
, _dec(cimbar::Config::ecc_bytes(), _colorBits)
inline MultiThreadedDecoder::MultiThreadedDecoder(std::string data_path, bool legacy_mode)
: _legacyMode(legacy_mode)
, _dec(cimbar::Config::ecc_bytes(), cimbar::Config::color_bits(), legacy_mode? 0 : 1, legacy_mode)
, _numThreads(std::max<int>(((int)std::thread::hardware_concurrency()/2), 1))
, _pool(_numThreads, 1)
, _writer(data_path, cimbar::Config::fountain_chunk_size(cimbar::Config::ecc_bytes(), cimbar::Config::symbol_bits() + _colorBits))
, _writer(data_path, cimbar::Config::fountain_chunk_size(cimbar::Config::ecc_bytes(), cimbar::Config::symbol_bits() + cimbar::Config::color_bits(), legacy_mode))
, _dataPath(data_path)
{
FountainInit::init();
Expand Down Expand Up @@ -97,7 +96,8 @@ inline bool MultiThreadedDecoder::add(cv::Mat mat)
// if extracted image is small, we'll need to run some filters on it
clock_t begin = clock();
bool should_preprocess = (res == Extractor::NEEDS_SHARPEN);
unsigned decodeRes = _dec.decode_fountain(img, _writer, should_preprocess);
int color_correction = _legacyMode? 1 : 2;
unsigned decodeRes = _dec.decode_fountain(img, _writer, should_preprocess, color_correction);
bytes += decodeRes;
++decoded;
decodeTicks += clock() - begin;
Expand All @@ -107,16 +107,6 @@ inline bool MultiThreadedDecoder::add(cv::Mat mat)
} );
}

inline bool MultiThreadedDecoder::decode(const cv::Mat& img, bool should_preprocess)
{
return _pool.try_execute( [&, img, should_preprocess] () {
clock_t begin = clock();
bytes += _dec.decode_fountain(img, _writer, should_preprocess);
++decoded;
decodeTicks += clock() - begin;
} );
}

inline void MultiThreadedDecoder::save(const cv::Mat& mat)
{
std::stringstream fname;
Expand All @@ -131,9 +121,9 @@ inline void MultiThreadedDecoder::stop()
_pool.stop();
}

inline int MultiThreadedDecoder::color_bits() const
inline bool MultiThreadedDecoder::legacy_mode() const
{
return _colorBits;
return _legacyMode;
}

inline unsigned MultiThreadedDecoder::num_threads() const
Expand Down
15 changes: 8 additions & 7 deletions app/src/cpp/cfc-cpp/jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ namespace {

cv::Scalar color = cv::Scalar(255,255,255);
if (in_progress == 1)
color = cv::Scalar(255,100,100);
color = cv::Scalar(255,244,94); // 0,191,255
else if (in_progress == 2)
color = cv::Scalar(0,255,0);
cv::Scalar outline = cv::Scalar(0,0,0);
Expand Down Expand Up @@ -123,8 +123,8 @@ namespace {
void drawDebugInfo(cv::Mat& mat, MultiThreadedDecoder& proc)
{
std::stringstream sstop;
sstop << "cfc using " << proc.num_threads() << " thread(s). " << proc.color_bits() << "..." << proc.backlog() << "? ";
sstop << (MultiThreadedDecoder::bytes / std::max<double>(1, MultiThreadedDecoder::decoded)) << "b v0.5.15";
sstop << "cfc using " << proc.num_threads() << " thread(s). " << proc.legacy_mode() << "..." << proc.backlog() << "? ";
sstop << (MultiThreadedDecoder::bytes / std::max<double>(1, MultiThreadedDecoder::decoded)) << "b v0.6.0f";
std::stringstream ssmid;
ssmid << "#: " << MultiThreadedDecoder::perfect << " / " << MultiThreadedDecoder::decoded << " / " << MultiThreadedDecoder::scanned << " / " << _calls;
std::stringstream ssperf;
Expand Down Expand Up @@ -162,20 +162,21 @@ namespace {

extern "C" {
jstring JNICALL
Java_org_cimbar_camerafilecopy_MainActivity_processImageJNI(JNIEnv *env, jobject instance, jlong matAddr, jstring dataPathObj, jint colorBitsJ)
Java_org_cimbar_camerafilecopy_MainActivity_processImageJNI(JNIEnv *env, jobject instance, jlong matAddr, jstring dataPathObj, jint modeInt)
{
++_calls;

// get params from raw address
Mat &mat = *(Mat *) matAddr;
string dataPath = jstring_to_cppstr(env, dataPathObj);
int colorBits = (int)colorBitsJ;
int modeVal = (int)modeInt;
bool legacyMode = modeVal <= 8; // current scheme: old 4c = 4, old 8c = 8, new = bigger number

std::shared_ptr<MultiThreadedDecoder> proc;
{
std::lock_guard<std::mutex> lock(_mutex);
if (!_proc or _proc->color_bits() != colorBits)
_proc = std::make_shared<MultiThreadedDecoder>(dataPath, colorBits);
if (!_proc or _proc->legacy_mode() != legacyMode)
_proc = std::make_shared<MultiThreadedDecoder>(dataPath, legacyMode);
proc = _proc;
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/cpp/libcimbar/DETAILS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ These properties may appear to be magical as you consider them more, and they do
2. wirehair requires the file contents to be stored in RAM
* this relates to the size limit!

This constraint is less of an obstacle than it may seem -- the fountain codes are essentially being used as a wire format, and the encoder and decoder could agree on a scheme to split up, and then reassemble, larger files. Cimbar does not (yet?) implement this, however!
The size constraint is less of an obstacle than it may seem -- the fountain codes are essentially being used as a wire format, and the encoder and decoder could agree on a scheme to split up, and then reassemble, larger files. Cimbar does not (yet?) implement this, however!

## Implementation: Decoder

Expand Down
36 changes: 24 additions & 12 deletions app/src/cpp/libcimbar/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,48 @@
## Numbers of note

* The barcode is `1024x1024` pixels. The individual tiles are `8x8` in a `9x9` grid (there is an empty row/column of spacing on either side)
* **7500** or 8750 bytes per cimbar image, after error correction
* **7500** bytes per cimbar image, after error correction
* There are 16 possible symbols per tile, encoding 4 bits
* There are 4 or 8 possible colors, encoding an additional 2-3 bits per tile.
* These 6-7 bits per tile work out to a maximum of 9300-10850 bytes per barcode, though in practice this number is reduced by error correction.
* These 6 bits per tile work out to a maximum of 9300 bytes per barcode, though in practice this number is reduced by error correction.
* 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 perfect 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, it is currently in use.

## Current sustained benchmark

* 4-color cimbar with ecc=30:
* `mode B` (8x8 4-color) cimbar with ecc=30/155:
* 4,689,084 bytes (after compression) in 44s -> 852 kilobits/s (~106 KB/s)
* mode B was introduced in 0.6.0, and should work in a wide variety of scenarios

* *legacy* `mode 4C` (8x8 4-color) cimbar with ecc=30/155:
* 4,717,525 bytes (after compression) in 45s -> 838 kilobits/s (~104 KB/s)
* the original configuration. Mostly replaced by mode B.

* 8-color cimbar with ecc=30:
* *deprecated* `mode 8C` (8x8 8-color) cimbar with ecc=30/155:
* 4,717,525 bytes in 40s -> 943 kilobits/s (~118 KB/s)
* removed in 0.6.0. 8-color has always been inconsistent, and needs future research

* *beta* `mode S` (5x5 4-color) cimbar with ecc=40/216 (note: not finalized, and requires a special build)
* safely >1 Mbit/s
* format still a WIP. To be continued...

* details:
* cimbar has built-in compression using zstd. What's being measured here is bits over the wire, e.g. data after compression is applied.
* these numbers are using 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.
* the sender is the cimbar.org wasm implementation. An equivalent command line is `./cimbar_send /path/to/file -s`
* these numbers are using https://github.com/sz3/cfc, running with 4 CPU threads on a venerable Qualcomm Snapdragon 625
* more modern cell CPUs run the decoder more quickly, but it turns out that this does not benefit performance much: the camera is usually the bottleneck.
* the sender is the cimbar.org wasm implementation. An equivalent command line is `./cimbar_send /path/to/file`
* cimbar.org uses the `shakycam` option to allow the receiver to detect/discard "in between" frames as part of the scan step. This allows it to spend more processing time decoding real data.
* burst rate can be higher (or lower)
* to this end, lower ecc settings *can* provide better burst rates
* 4-color cimbar is currently preferred, and will give more consistent transfer speeds.
* 8-color cimbar should be considered a prototype within a prototype. It is considerably more sensitive to lighting conditions and color tints.
* to this end, lower ecc settings *can* provide better burst rates. I've aimed for a balance of performance and reliability.
* cimbar `mode B` is preferred, and should be the most reliable.
* The older `mode 4C` *may* give more consistent transfer speeds in certain scenarios, but is mostly included for backwards-compatibility reasons.

* other notes:
* having better lighting in the frame often leads to better results -- this is why cimbar.org has a (mostly) white background. cfc uses android's auto-exposure, auto-focus, etc (it's a very simple app). Good ambient light -- or a white background -- can lead to more consitent quality frame capture.
* having better lighting in the frame often leads to better results -- this is why cimbar.org has a (mostly) white background. cfc uses android's auto-exposure, auto-focus, etc (it's a demo app). Good ambient light -- or a white background -- can lead to more consitent quality frame capture.
* screen brightness on the sender is good, but ambient light is better.
* because of the lighting/exposure question, landscape *may* be better than portrait.
* cfc currently has a low resolution, so the cimbar frame should take up as much of the display as possible (trust the guide brackets)
* the cimbar frame should take up as much of the display as possible (trust the guide brackets)
* the format is designed to decode at resolutions as low as 700x700, but performance may suffer.
* similarly, it's best to keep the camera angle straight-on -- instead of at an angle -- to decode the whole image successfully. Decodes should still happen at higher angles, but the "smaller" part of the image may have more errors than the ECC can deal with.
* other things to be wary of:
* glare from light sources.
Expand Down
6 changes: 3 additions & 3 deletions app/src/cpp/libcimbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

Behold: an experimental barcode format for air-gapped data transfer.

It can sustain speeds of 943+ kilobits/s (~118 KB/s) using just a computer monitor and a smartphone camera!
It can sustain speeds of 850 kilobits/s (~106 KB/s) using just a computer monitor and a smartphone camera!

<p align="center">
<img src="https://github.com/sz3/cimbar-samples/blob/v0.5/6bit/4cecc30f.png" width="70%" title="A non-animated cimbar code" >
<img src="https://github.com/sz3/cimbar-samples/blob/v0.6/b/4cecc30f.png" width="70%" title="A non-animated mode-B cimbar code" >
</p>

## Explain?
Expand All @@ -31,7 +31,7 @@ No internet/bluetooth/NFC/etc is used. All data is transmitted through the camer

The code is written in C++, and developed/tested on amd64+linux, arm64+android (decoder only), and emscripten+WASM (encoder only). It probably works, or can be made to work, on other platforms.

Crucially, because the encoder compiles to asmjs and wasm, it can run on anything with a modern web browser. There are [releases](https://github.com/sz3/libcimbar/releases/latest) if you wish to run the encoder locally instead of via cimbar.org.
Crucially, because the encoder compiles to asmjs and wasm, it can run on anything with a modern web browser. For offline use, you can either install cimbar.org as a progressive web app, or [download the latest release](https://github.com/sz3/libcimbar/releases/latest) of `cimbar_js.html`, save it locally, and open it in your web browser.

## Library dependencies

Expand Down
13 changes: 8 additions & 5 deletions app/src/cpp/libcimbar/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Performance optimizations aside, there are a number of paths that might be inter
* proper metadata/header information?
* would be nice to be able to determine ecc/#colors/#symbols from the cimbar image itself?
* The bottom right corner is the obvious place to reclaim space to make this possible.
* this is complicated by potential aspect ratio changes for future cimbar modes.
* multi-frame decoding?
* when decoding a static cimbar image, it would be useful to be able to use prior (unsuccessful) decode attempts to inform a future decode, and -- hopefully -- increase the probability of success. Currently, all frames are decoded independently.
* there is already a granular confidence metric that could be reused -- the `distance` that's tracked when decoding symbol tiles...
Expand All @@ -18,18 +19,18 @@ Performance optimizations aside, there are a number of paths that might be inter
* there is surely a more optimal set -- a more rigorous approach should yield lower error rates!
* but, more importantly, it may be possible to go up to 32 symbols, and encode 5 symbol bits per tile?
* optimal symbol size?
* the symbols that make up each cell on the cimbar grid are 8x8 (in a 9x9 grid).
* this is because imagehash was on 8x8 tiles!
* smaller sizes might also work?
* the symbols that make up each cell on the cimbar grid are 8x8 (in a 9x9 grid). this is because imagehash was on 8x8 tiles!
* smaller sizes might also work? I've been looking into 5x5 (in a 6x6 grid) as a starting point. It seems promising.
* the limiting factor is the hamming distance between each image hash "bucket", and the 9Xth percentile decoding errors.
* optimal color set?
* the 4-color (2 bit) pallettes seem reasonable. 8-color, perhaps less so?
* this may be a limitation of the algorithm/approach, however. Notably, since each symbol is drawn with one pallette color, all colors need sufficient contrast against the backdrop (#000 or #FFF, depending). This constrains the color space somewhat, and less distinct colors == more errors.
* in addition to contrast, there is interplay (that I don't currently understand) between the overall brightness of the image and the exposure time needed for high framerate capture. More clean frames == more troughput.
* in addition to contrast, there is interplay between the overall brightness of the image and the exposure time needed for high framerate capture. More clean frames == more troughput.
* the camera framerate in the CFC app is limited by auto-exposure and auto-focus behavior. A newer/better decoder app might be helpful.
* optimal grid size?
* 1024x1024 is a remnant of the early prototyping process. There is nothing inherently special about it (except that it fits on a 1920x1080 screen, which seems good)
* the tile grid itself is 1008x1008 (1008 == 9x112 -- there are 112 tile rows and columns)
* a smaller grid would be less information dense, but more resilient to errors. Probably.
* a smaller grid *could* be more resilient to errors, at the expense of data capacity.
* optimal grid shape?
* it's a square because QR codes are square. That's it. Should it be?
* I'm strongly considering 4:3 for the next revision.
Expand All @@ -41,6 +42,8 @@ Performance optimizations aside, there are a number of paths that might be inter
* proper GPU support (OpenCV + openCL) on android?
* It *might* be useful. [CFC]((https://github.com/sz3/cfc) is the current test bed for this.
* wasm decoder?
* android is going to kick CFC out of the store! (testing requirement)
* so it might be time to write this...
* probably needs to use Web Workers
* in-browser GPGPU support would be interesting (but I'm not counting on it)
* ???
Expand Down
22 changes: 18 additions & 4 deletions app/src/cpp/libcimbar/WASM.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,30 @@

## Releases

wasm and asm.js releases are available [here](https://github.com/sz3/libcimbar/releases/latest). The wasm build is what cimbar.org uses. The asm.js build can be downloaded, extracted, and run in a local web browser.
wasm and asm.js releases are available [here](https://github.com/sz3/libcimbar/releases/latest). The wasm build is what cimbar.org uses. [cimbar_js.html](https://github.com/sz3/libcimbar/releases/latest/cimbar_js.html) can be downloaded and opened/run in a local web browser -- no install required.

## Build

To build opencv.js (and the static libraries we'll need to build against opencv)...
To build, use the `package-wasm.sh` script in a docker container:

```
docker run --mount type=bind,source="$(pwd)",target="/usr/src/app" -it emscripten/emsdk:3.1.39
```
Then, inside the container:
```
bash /usr/src/app/package-wasm.sh
```

## Alternative build for the adventurous

Alternatively, if you have a local emscripten setup, you can try to run the package-wasm.sh commands piecemeal:

To build opencv.js:
```
cd /path/to/opencv
mkdir opencv-build-wasm
cd opencv-build-wasm
python ../platforms/js/build_js.py build_wasm --build_wasm --emscripten_dir=/path/to/emscripten
python3 ../platforms/js/build_js.py build_wasm --build_wasm --emscripten_dir=/path/to/emscripten
```

With opencv.js built:
Expand All @@ -22,7 +36,7 @@ mkdir build-wasm
cd build-wasm
source /path/to/emscripten/emsdk/emsdk_env.sh
emcmake cmake .. -DUSE_WASM=1 -DOPENCV_DIR=/path/to/opencv
make -j7 install
make -j5 install
```

(do `-DUSE_WASM=2` to use asm.js instead of wasm)
Expand Down
9 changes: 6 additions & 3 deletions app/src/cpp/libcimbar/package-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ apt update
apt install python3 -y

cd opencv4/
mkdir opencv-build-wasm && cd opencv-build-wasm
mkdir opencv-build-wasm
cd opencv-build-wasm
python3 ../platforms/js/build_js.py build_wasm --build_wasm --emscripten_dir=/emsdk/upstream/emscripten

cd /usr/src/app
mkdir build-wasm && cd build-wasm
mkdir build-wasm
cd build-wasm
emcmake cmake .. -DUSE_WASM=1 -DOPENCV_DIR=/usr/src/app/opencv4
make -j5 install
(cd ../web/ && tar -czvf cimbar.wasm.tar.gz cimbar_js.* index.html main.js)

cd /usr/src/app
mkdir build-asmjs && cd build-asmjs
mkdir build-asmjs
cd build-asmjs
emcmake cmake .. -DUSE_WASM=2 -DOPENCV_DIR=/usr/src/app/opencv4
make -j5 install
(cd ../web/ && zip cimbar.asmjs.zip cimbar_js.js index.html main.js)
Expand Down
2 changes: 1 addition & 1 deletion app/src/cpp/libcimbar/samples
Submodule samples updated from ca4518 to 7443f6
Loading

0 comments on commit c47755e

Please sign in to comment.