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

Support new cimbar format "mode B", minor UI change = version 0.6.0f #31

Merged
merged 8 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Copy link
Owner Author

Choose a reason for hiding this comment

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

We don't talk about what happened to version 12...

(ok, it's just that the default was mode B, and I concluded that might be a problem)

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)
Copy link
Owner Author

Choose a reason for hiding this comment

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

These ternarys (ternaries?) are kinda gross, but fine for now?

, _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;
Copy link
Owner Author

Choose a reason for hiding this comment

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

.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Notably, this means that mode 4C will not benefit from the color correction matrix generated by mode B. I went back and forth on this, and probably will remove it once mode B is the default (if not sooner 🤔 ).

Mode B's CCM will pretty much always be better, if it exists.

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
Copy link
Owner Author

Choose a reason for hiding this comment

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

As you can see, blue is also an option. I decided to throw our colorblind friends a bone and not use red vs green.

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
Copy link
Owner Author

Choose a reason for hiding this comment

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

These "modeVal" numbers (4, 8, 68) are CFC only at the moment, and might change in the future. libcimbar currently has no such concept, but if we adopt one there we'll probably mirror it here.


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