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

Binarizer performance improvements #115

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ This data is in the same form as the [`ImageData`](https://developer.mozilla.org
- `height` - The height of the image you wish to decode.
- `options` (optional) - Additional options.
- `inversionAttempts` - (`attemptBoth` (default), `dontInvert`, `onlyInvert`, or `invertFirst`) - Should jsQR attempt to invert the image to find QR codes with white modules on black backgrounds instead of the black modules on white background. This option defaults to `attemptBoth` for backwards compatibility but causes a ~50% performance hit, and will probably be default to `dontInvert` in future versions.
- `canOverwriteImage` - (`true` (default) or `false`) - Specifies whether the image data can be overwritten for performance improvements or whether it should be kept untouched. If `true` the image buffer will be used internally to reduce additional memory allocation.

### Return value
If a QR is able to be decoded the library will return an object with the following keys.
Expand Down
60 changes: 47 additions & 13 deletions src/binarizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ function numBetween(value: number, min: number, max: number): number {
class Matrix {
private data: Uint8ClampedArray;
private width: number;
constructor(width: number, height: number) {
constructor(width: number, height: number, buffer?: Uint8ClampedArray) {
this.width = width;
this.data = new Uint8ClampedArray(width * height);
const bufferSize = width * height;
if (buffer && buffer.length !== bufferSize) {
throw new Error("Wrong buffer size");
}
this.data = buffer || new Uint8ClampedArray(bufferSize);
}
public get(x: number, y: number) {
return this.data[y * this.width + x];
Expand All @@ -23,24 +27,40 @@ class Matrix {
}
}

export function binarize(data: Uint8ClampedArray, width: number, height: number, returnInverted: boolean) {
if (data.length !== width * height * 4) {
export function binarize(data: Uint8ClampedArray, width: number, height: number, returnInverted: boolean,
canOverwriteImage: boolean) {
const pixelCount = width * height;
if (data.length !== pixelCount * 4) {
throw new Error("Malformed data passed to binarizer.");
}
// assign the greyscale and binary image within the rgba buffer as the rgba image will not be needed after conversion
let bufferOffset = 0;
// Convert image to greyscale
const greyscalePixels = new Matrix(width, height);
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const r = data[((y * width + x) * 4) + 0];
const g = data[((y * width + x) * 4) + 1];
const b = data[((y * width + x) * 4) + 2];
let greyscaleBuffer: Uint8ClampedArray;
if (canOverwriteImage) {
greyscaleBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);
bufferOffset += pixelCount;
}
const greyscalePixels = new Matrix(width, height, greyscaleBuffer);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelPosition = (y * width + x) * 4;
const r = data[pixelPosition];
const g = data[pixelPosition + 1];
const b = data[pixelPosition + 2];
greyscalePixels.set(x, y, 0.2126 * r + 0.7152 * g + 0.0722 * b);
}
}
const horizontalRegionCount = Math.ceil(width / REGION_SIZE);
const verticalRegionCount = Math.ceil(height / REGION_SIZE);
const blackPointsCount = horizontalRegionCount * verticalRegionCount;

const blackPoints = new Matrix(horizontalRegionCount, verticalRegionCount);
let blackPointsBuffer: Uint8ClampedArray;
if (canOverwriteImage) {
blackPointsBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, blackPointsCount);
bufferOffset += blackPointsCount;
}
const blackPoints = new Matrix(horizontalRegionCount, verticalRegionCount, blackPointsBuffer);
for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) {
for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) {
let sum = 0;
Expand Down Expand Up @@ -87,11 +107,25 @@ export function binarize(data: Uint8ClampedArray, width: number, height: number,
}
}

const binarized = BitMatrix.createEmpty(width, height);
let binarized: BitMatrix;
if (canOverwriteImage) {
const binarizedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);
bufferOffset += pixelCount;
binarized = new BitMatrix(binarizedBuffer, width);
} else {
binarized = BitMatrix.createEmpty(width, height);
}

let inverted: BitMatrix = null;
if (returnInverted) {
inverted = BitMatrix.createEmpty(width, height);
if (canOverwriteImage) {
const invertedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);
inverted = new BitMatrix(invertedBuffer, width);
} else {
inverted = BitMatrix.createEmpty(width, height);
}
}

for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) {
for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) {
const left = numBetween(hortizontalRegion, 2, horizontalRegionCount - 3);
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ function scan(matrix: BitMatrix): QRCode | null {

export interface Options {
inversionAttempts?: "dontInvert" | "onlyInvert" | "attemptBoth" | "invertFirst";
canOverwriteImage?: boolean;
}

const defaultOptions: Options = {
inversionAttempts: "attemptBoth",
canOverwriteImage: true,
};

function jsQR(data: Uint8ClampedArray, width: number, height: number, providedOptions: Options = {}): QRCode | null {
Expand All @@ -71,7 +73,7 @@ function jsQR(data: Uint8ClampedArray, width: number, height: number, providedOp

const shouldInvert = options.inversionAttempts === "attemptBoth" || options.inversionAttempts === "invertFirst";
const tryInvertedFirst = options.inversionAttempts === "onlyInvert" || options.inversionAttempts === "invertFirst";
const {binarized, inverted} = binarize(data, width, height, shouldInvert);
const {binarized, inverted} = binarize(data, width, height, shouldInvert, options.canOverwriteImage);
let result = scan(tryInvertedFirst ? inverted : binarized);
if (!result && (options.inversionAttempts === "attemptBoth" || options.inversionAttempts === "invertFirst")) {
result = scan(tryInvertedFirst ? binarized : inverted);
Expand Down