diff --git a/src/core/datamatrix/DataMatrixReader.ts b/src/core/datamatrix/DataMatrixReader.ts index 9f1c5ef7..fe2592b3 100644 --- a/src/core/datamatrix/DataMatrixReader.ts +++ b/src/core/datamatrix/DataMatrixReader.ts @@ -11,6 +11,7 @@ import ResultPoint from '../ResultPoint'; import System from '../util/System'; import Decoder from './decoder/Decoder'; import Detector from './detector/Detector'; +import DetectorWithLPattern from './detector/LPattern/DetectorWithLPattern'; /* @@ -63,7 +64,9 @@ export default class DataMatrixReader implements Reader { decoderResult = this.decoder.decode(bits); points = DataMatrixReader.NO_POINTS; } else { - const detectorResult = new Detector(image.getBlackMatrix()).detect(); + let detectorResult; + detectorResult = new Detector(image.getBlackMatrix()).detect(); + // detectorResult = new DetectorWithLPattern(image.getBlackMatrix()).detect(); decoderResult = this.decoder.decode(detectorResult.getBits()); points = detectorResult.getPoints(); } diff --git a/src/core/datamatrix/detector/LPattern/DetectorWithLPattern.ts b/src/core/datamatrix/detector/LPattern/DetectorWithLPattern.ts new file mode 100644 index 00000000..3fd6d81b --- /dev/null +++ b/src/core/datamatrix/detector/LPattern/DetectorWithLPattern.ts @@ -0,0 +1,226 @@ +import NotFoundException from '../../../../core/NotFoundException'; +import BitMatrix from '../../../common/BitMatrix'; +import DetectorResult from '../../../common/DetectorResult'; +import GridSamplerInstance from '../../../common/GridSamplerInstance'; +import ResultPoint from '../../../ResultPoint'; +import { EdgeTracer } from './EdgeTracer'; +import Point from './Point'; +import { RegressionLine } from './RegressionLine'; + +/** + * Ported from: ZXing CPP + * https://github.com/nu-book/zxing-cpp/blob/2ceda95cba943b3b2ab30af3c90150c97e84d416/core/src/datamatrix/DMDetector.cpp#L442-L1072 + */ +export default class DetectorWithLPattern { + + private image: BitMatrix; + + constructor(image: BitMatrix) { + this.image = image; + } + + /** + *

Detects a Data Matrix Code in an image.

+ * + * @return {@link DetectorResult} encapsulating results of detecting a Data Matrix Code + * @throws NotFoundException if no Data Matrix Code can be found + */ + public detect(): DetectorResult { + return DetectorWithLPattern.detectWithLPattern(this.image, true); + } + + // Print matrix (for debugging) + static printBitMatrix(matrix: BitMatrix):void + { + // let content = ""; + // for (let y = 0; y < matrix.getHeight(); ++y) { + // let row = ""; + // for (let x = 0; x < matrix.getWidth(); ++x) + // row += matrix.get(x, y) ? '+' : '.'; + // content += `${row}\n`; + // } + // content.split('\n').forEach((row) => { + // console.log(row); + // }); + // try { + // const fs = require('fs'); + // fs.writeFileSync('dump.txt', content, (err) => { + // if (err) return console.log(err); + // }); + // } catch(e) { + // console.log(e); + // } + } + + static sampleGridAndMoveHalfAPixel(image: BitMatrix, tl: Point, bl: Point, br: Point, tr: Point, dimensionX: number, dimensionY: number): BitMatrix { + // shrink shape by half a pixel to go from center of white pixel outside of code to the edge between white and black + let moveHalfAPixel = (a: Point, b: Point) => { + let a2b = Point.divideBy(Point.sub(b, a), Point.distance(a, b)); + a = Point.add(a, Point.multiplyBy(a2b, 0.5)); + }; + + // move every point towards tr by half a pixel + // reasoning: for very low res scans, the top and right border tend to be around half a pixel moved inward already. + // for high res scans this half a pixel makes no difference anyway. + moveHalfAPixel(tl, tr); + moveHalfAPixel(bl, tr); + moveHalfAPixel(br, tr); + + for (let p of [tl, bl, br, tr]) { + p = Point.add(p, new Point(0.5, 0.5)); + } + + let border = 0; + + return GridSamplerInstance.getInstance().sampleGrid( + image, + dimensionX, dimensionY, + border, border, + dimensionX - border, border, + dimensionX - border, dimensionY - border, + border, dimensionY - border, + tl.x, tl.y, + tr.x, tr.y, + br.x, br.y, + bl.x, bl.y + ); + } + + static detectWithLPattern(image: BitMatrix, tryRotate: boolean): DetectorResult { + + DetectorWithLPattern.printBitMatrix(image); + + // walk to the left at first + const directions = [new Point(-1, 0), new Point(1, 0), new Point(0, -1), new Point(0, 1)]; + + for (let direction of directions) { + + let startTracer = new EdgeTracer(image, new Point(image.getWidth() / 2, image.getHeight() / 2), direction); + + while (startTracer.step()) { + // go forward until we reach a white/black border + if (!startTracer.isEdgeBehind()) + continue; + + let tl: Point, bl: Point, br: Point, tr: Point; + let lineL = new RegressionLine(); + let lineB = new RegressionLine(); + let lineR = new RegressionLine(); + let lineT = new RegressionLine(); + + let t = startTracer.clone(); + + // follow left leg upwards + t.setDirection(t.right()); + if (!t.traceLine(t.right(), lineL)) + continue; + + tl = t.traceCorner(t.right()); + lineL.reverse(); + + let tlTracer = t.clone(); + + // follow left leg downwards + t = startTracer.clone(); + t.setDirection(t.left()); + if (!t.traceLine(t.left(), lineL)) + continue; + + if (!lineL.isValid()) + t.updateDirectionFromOrigin(tl); + let up = t.back(); + bl = t.traceCorner(t.left()); + + tlTracer.setDirection(t.front()); + + // follow bottom leg right + if (!t.traceLine(t.left(), lineB)) + continue; + + if (!lineL.isValid()) + t.updateDirectionFromOrigin(bl); + let right = t.front(); + br = t.traceCorner(t.left()); + + let lenL = Point.distance(tl, bl); + let lenB = Point.distance(bl, br); + if (lenL < 10 || lenB < 10 || lenB < lenL/4 || lenB > lenL*8) + continue; + + let maxStepSize = (lenB / 5 + 1); // datamatrix dim is at least 10x10 + + // at this point we found a plausible L-shape and are now looking for the b/w pattern at the top and right: + // follow top row right 'half way' (4 gaps), see traceGaps break condition with 'invalid' line + tlTracer.setDirection(right); + if (!tlTracer.traceGaps(tlTracer.right(), lineT, maxStepSize, new RegressionLine())) + continue; + + maxStepSize = Math.max(lineT.length / 4, (lenL / 5 + 1)); + + // follow up until we reach the top line + t.setDirection(up); + if (!t.traceGaps(t.left(), lineR, maxStepSize, lineT)) + continue; + + // continue top row right until we cross the right line + if (!tlTracer.traceGaps(tlTracer.right(), lineT, maxStepSize, lineR)) + continue; + + tr = t.traceCorner(t.left()); + + let lenT = Point.distance(tl, tr); + let lenR = Point.distance(tr, br); + + if (Math.abs(lenT - lenB) / lenB > 0.5 || Math.abs(lenR - lenL) / lenL > 0.5 || + lineT.points.length < 5 || lineR.points.length < 5) + continue; + + // console.log("L: %f, %f ^ %f, %f > %f, %f (%d : %d : %d : %d)\n", bl.x, bl.y, + // tl.x - bl.x, tl.y - bl.y, br.x - bl.x, br.y - bl.y, lenL, lenB, lenT, lenR); + + for (let l of [lineL, lineB, lineT, lineR]) { + l.evaluate(true); + } + + // find the bounding box corners of the code with sub-pixel precision by intersecting the 4 border lines + bl = RegressionLine.intersect(lineB, lineL); + tl = RegressionLine.intersect(lineT, lineL); + tr = RegressionLine.intersect(lineT, lineR); + br = RegressionLine.intersect(lineB, lineR); + + let dimT = lineT.modules(tl, tr); + let dimR = lineR.modules(br, tr); + + // console.log("L: %f, %f ^ %f, %f > %f, %f ^> %f, %f", bl.x, bl.y, tl.x - bl.x, tl.y - bl.y, br.x - bl.x, br.y - bl.y, tr.x, tr.y); + // console.log("dim: %d x %d", dimT, dimR); + + // the dimension is 2x the number of black/white transitions + dimT *= 2; + dimR *= 2; + + if (dimT < 10 || dimT > 144 || dimR < 8 || dimR > 144 ) + continue; + + let bits = this.sampleGridAndMoveHalfAPixel(image, tl, bl, br, tr, Math.round(dimT), Math.round(dimR)); + + // DetectorWithLPattern.printBitMatrix(bits); + + if (bits.getWidth() === 0 && bits.getHeight() === 0) + continue; + + return new DetectorResult(bits, [ + new ResultPoint(tl.x, tl.y), + new ResultPoint(bl.x, bl.y), + new ResultPoint(br.x, br.y), + new ResultPoint(tr.x, tr.y), + ]); + } + + // reached border of image -> try next scan direction + if (!tryRotate) + break; // only test first direction (left) + } + + throw new NotFoundException(); + } +} diff --git a/src/core/datamatrix/detector/LPattern/EdgeTracer.ts b/src/core/datamatrix/detector/LPattern/EdgeTracer.ts new file mode 100644 index 00000000..a6dda55c --- /dev/null +++ b/src/core/datamatrix/detector/LPattern/EdgeTracer.ts @@ -0,0 +1,216 @@ +import BitMatrix from '../../../common/BitMatrix'; +import Point from './Point'; +import { RegressionLine } from './RegressionLine'; + +export enum StepResult { + FOUND, + OPEN_END, + CLOSED_END +} + +export enum Value { + INVALID, + WHITE, + BLACK +} + +export class EdgeTracer { + + private image: BitMatrix; + private p: Point; // current position + private d: Point; // current direction + + static mainDirection(d: Point): Point { + return Math.abs(d.x) > Math.abs(d.y) ? new Point(d.x, 0) : new Point(0, d.y); + } + + public pointIsIn(p: Point): boolean { + const b = 0; + return b <= p.x && p.x < this.image.getWidth() - b + && b <= p.y && p.y < this.image.getHeight() - b; + } + + public isIn() { + return this.pointIsIn(Point.round(this.p)); + } + + public getAt(p: Point): any { + if (!this.pointIsIn(p)) + return Value.INVALID; + const q: Point = Point.round(p); + return this.image.get(q.x, q.y) ? Value.BLACK : Value.WHITE; + } + + public blackAt(p: Point): boolean { + return this.getAt(p) === Value.BLACK; + } + + public whiteAt(p: Point): boolean { + return this.getAt(p) === Value.WHITE; + } + + public isEdge(pos: Point, dir: Point): boolean { + return this.whiteAt(pos) && this.blackAt(Point.add(pos, dir)); + } + + traceStep(dEdge: Point, maxStepSize: number, goodDirection: boolean): StepResult { + dEdge = EdgeTracer.mainDirection(dEdge); + for (let breadth = 1; breadth <= (goodDirection ? 1 : (maxStepSize === 1 ? 2 : 3)); ++breadth) + for (let step = 1; step <= maxStepSize; ++step) + for (let i = 0; i <= 2 * (step / 4 + 1) * breadth; ++i) { + let pEdge = Point.add( + Point.add( + this.p, + Point.multiplyBy(this.d, step) + ), + Point.multiplyBy(dEdge, (i&1 ? (i + 1) / 2 : -i / 2)) + ); + this.log(pEdge); + + if (!this.blackAt(Point.add(pEdge, dEdge))) + continue; + + // found black pixel -> go 'outward' until we hit the b/w border + for (let j = 0; j < Math.max(maxStepSize, 3) && this.pointIsIn(pEdge); ++j) { + if (this.whiteAt(pEdge)) { + this.p = pEdge; + return StepResult.FOUND; + } + pEdge = Point.sub(pEdge, dEdge); + if (this.blackAt(Point.sub(pEdge, this.d))) + pEdge = Point.sub(pEdge, this.d); + this.log(pEdge); + } + // no valid b/w border found within reasonable range + return StepResult.CLOSED_END; + } + return StepResult.OPEN_END; + } + + // TODO: IF DEBUG + public log(p: Point): void { + // if (this._log.getHeight() !== this.image.getHeight() || this._log.getWidth() !== this.image.getWidth()) + // this._log = new BitMatrix(this.image.getWidth(), this.image.getHeight()); + // let q = Point.round(p); + // if (this.pointIsIn(q)) + // this._log.set(q.x, q.y); + // console.log(JSON.stringify(p)); + } + + constructor(img: BitMatrix, p: Point, d: Point) { + this.image = img; + this.p = new Point(p.x, p.y); + this.d = new Point(d.x, d.y); + } + + step(s = 1): boolean { + this.p = Point.add(this.p, Point.multiplyBy(this.d, s)); + this.log(this.p); + return this.pointIsIn(this.p); + } + + setDirection(dir: Point) { + this.d = Point.divideBy(dir, Math.max(Math.abs(dir.x), Math.abs(dir.y))); + } + + updateDirectionFromOrigin(origin: Point): boolean { + let old_d = new Point(this.d.x, this.d.y); + this.setDirection(Point.sub(this.p, origin)); + // it the new direction is pointing "backward", i.e. angle(new, old) > pi/2 -> break + if (Point.mul(this.d, old_d) < 0) { + return false; + } + // console.log(`new dir: ${this.d.x}, ${this.d.y}`); + // make sure d stays in the same quadrant to prevent an infinite loop + if (!Point.equals(EdgeTracer.mainDirection(this.d), EdgeTracer.mainDirection(old_d))) + this.d = Point.add(EdgeTracer.mainDirection(old_d), Point.multiplyBy(EdgeTracer.mainDirection(this.d), 0.99)); + return true; + } + + front(): Point { return this.d; } + back(): Point { return new Point(-this.d.x, -this.d.y); } + right(): Point { return new Point(-this.d.y, this.d.x); } + left(): Point { return new Point(this.d.y, -this.d.x); } + + isEdgeBehind(): boolean { + return this.isEdge(this.p, this.back()); + } + + traceLine(dEdge: Point, line: RegressionLine): boolean { + do { + this.log(this.p); + line.add(this.p); + if (line.points.length % 30 === 10) { + line.evaluate(); + if (!this.updateDirectionFromOrigin(Point.add(Point.sub(this.p, line.project(this.p)), line.points[0]))) + return false; + } + let stepResult = this.traceStep(dEdge, 1, line.isValid()); + if (stepResult !== StepResult.FOUND) + return stepResult === StepResult.OPEN_END; + } while (true); + } + + + traceGaps(dEdge: Point, line: RegressionLine, maxStepSize: number, finishLine: RegressionLine): boolean { + line.setDirectionInward(dEdge); + let gaps = 0; + do { + this.log(this.p); + let next_p = Point.round(this.p); + let diff = line.points.length === 0 ? new Point() : Point.sub(next_p, line.points[line.points.length - 1]); + + if (line.points.length === 0 || !Point.equals(line.points[line.points.length - 1], next_p)) + line.add(next_p); + + if (Math.abs(Point.mul(diff, this.d)) > 1) { + ++gaps; + if (line.length > 5) { + const firstPoint = new Point(line.points[0].x, line.points[0].y); + line.evaluate(true); + if (!this.updateDirectionFromOrigin(Point.add(Point.sub(this.p, line.project(this.p)), firstPoint))) + return false; + } + // the minimum size is 10x10 -> 4 gaps + // TODO: maybe switch to termination condition based on bottom line length + if (!finishLine.isValid() && gaps >= 4) + return true; + } + if (line.isValid()) { + // if we are drifting towards the inside of the code, pull the current position back out onto the line + if (line.signedDistance(this.p) > 2) + this.p = Point.add(line.project(this.p), this.d); + } + + + if (finishLine.isValid()) + maxStepSize = Math.min(maxStepSize, finishLine.signedDistance(this.p)); + + + let stepResult = this.traceStep(dEdge, maxStepSize, line.isValid()); + if (stepResult !== StepResult.FOUND) + return stepResult === StepResult.OPEN_END; + } while (true); + } + + + traceCorner(dir: Point): Point { + this.step(); + let ret = new Point(this.p.x, this.p.y); + this.p = new Point(this.p.x, this.p.y); + let _tmp = new Point(dir.x, dir.y); + dir = this.d; + this.d = _tmp; + this.traceStep(Point.multiplyBy(dir, -1), 2, false); + // console.log(`turn: ${this.p.x} x ${this.p.y} -> ${this.d.x}, ${this.d.y}`); + return ret; + } + + clone(): EdgeTracer { + return new EdgeTracer( + this.image, + new Point(this.p.x, this.p.y), + new Point(this.d.x, this.d.y), + ); + } +} diff --git a/src/core/datamatrix/detector/LPattern/Point.ts b/src/core/datamatrix/detector/LPattern/Point.ts new file mode 100644 index 00000000..c539692b --- /dev/null +++ b/src/core/datamatrix/detector/LPattern/Point.ts @@ -0,0 +1,41 @@ +export default class Point { + x: number; + y: number; + + constructor(x: number = 0, y: number = 0) { + this.x = x; + this.y = y; + } + + static equals(a: Point, b: Point) { + return a.x === b.x && a.y === b.y; + } + + static add(a: Point, b: Point): Point { + return new Point(a.x + b.x, a.y + b.y); + } + + static sub(a: Point, b: Point): Point { + return new Point(a.x - b.x, a.y - b.y); + } + + static multiplyBy(a: Point, s: number): Point { + return new Point(a.x * s, a.y * s); + } + + static divideBy(a: Point, d: number): Point { + return new Point(a.x / d, a.y / d); + } + + static mul(a: Point, b: Point): number { + return a.x * b.x + a.y * b.y; + } + + static distance(a: Point, b: Point): number { + return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); + } + + static round(p: Point): Point { + return new Point(Math.round(p.x), Math.round(p.y)); + } +} diff --git a/src/core/datamatrix/detector/LPattern/RegressionLine.ts b/src/core/datamatrix/detector/LPattern/RegressionLine.ts new file mode 100644 index 00000000..fdbf0545 --- /dev/null +++ b/src/core/datamatrix/detector/LPattern/RegressionLine.ts @@ -0,0 +1,150 @@ +import Point from './Point'; + +export class RegressionLine { + + private _points: Point[] = []; + private _directionInward: Point = new Point(); + + private a: number; + private b: number; + private c: number; + + static intersect(l1: RegressionLine, l2: RegressionLine): Point { + const d = l1.a * l2.b - l1.b * l2.a; + const x = (l1.c * l2.b - l1.b * l2.c) / d; + const y = (l1.a * l2.c - l1.c * l2.a) / d; + return new Point(x, y); + } + + static average(c: number[], filter: (x: number) => boolean): number { + let sum = 0; + let num = 0; + for (let v of c) { + if (filter(v)) { + sum += v; + ++num; + } + } + return sum / num; + } + + public _evaluate(ps: Point[]): void { + const allSummarized: Point = ps.reduce((acc, point) => Point.add(acc, point), new Point()); + const mean: Point = Point.divideBy(allSummarized, ps.length); + + let sumXX = 0, sumYY = 0, sumXY = 0; + ps.forEach((p) => { + sumXX += (p.x - mean.x) * (p.x - mean.x); + sumYY += (p.y - mean.y) * (p.y - mean.y); + sumXY += (p.x - mean.x) * (p.y - mean.y); + }); + + if (sumYY >= sumXX) { + this.a = +sumYY / Math.sqrt(sumYY * sumYY + sumXY * sumXY); + this.b = -sumXY / Math.sqrt(sumYY * sumYY + sumXY * sumXY); + } else { + this.a = +sumXY / Math.sqrt(sumXX * sumXX + sumXY * sumXY); + this.b = -sumXX / Math.sqrt(sumXX * sumXX + sumXY * sumXY); + } + if (Point.mul(this._directionInward, this.normal()) < 0) { + this.a = -this.a; + this.b = -this.b; + } + this.c = Point.mul(this.normal(), mean); + } + + get points(): Point[] { + return this._points; + } + + get length(): number { + return this.points.length >= 2 + ? Point.distance(this._points[0], this._points[this._points.length - 1]) + : 0; + } + + public isValid(): boolean { + return this.a !== undefined; + } + + public normal(): Point { + return new Point(this.a, this.b); + } + + public project(p: Point): Point { + return Point.sub( + p, + Point.multiplyBy(this.normal(), Point.mul(this.normal(), p) - this.c) + ); + } + + public signedDistance(p: Point): number { + return (Point.mul(this.normal(), p) - this.c) / Math.sqrt(this.a * this.a + this.b * this.b); + } + + public reverse() { + this._points = this._points.reverse(); + } + + public add(p: Point) { + this._points.push(p); + } + + public setDirectionInward(d: Point) { + this._directionInward = d; + } + + public evaluate(clean = false) { + let ps = this._points; + this._evaluate(ps); + if (clean) { + let old_points_length; + while (true) { + old_points_length = this._points.length; + this._points = this._points.filter(p => !(this.signedDistance(p) > 1.5)); + if (old_points_length === this._points.length) + break; + + // console.log("removed %zu points", old_points_length - this._points.length); + this._evaluate(this._points); + } + } + } + + public modules(beg: Point, end: Point): number { + + console.assert(this._points.length > 3); + + let gapSizes: number[] = []; + + // calculate the distance between the points projected onto the regression line + for (let i = 1; i < this._points.length; ++i) + gapSizes.push(Point.distance(this.project(this._points[i]), this.project(this._points[i - 1]))); + + // calculate the (average) distance of two adjacent pixels + const unitPixelDist: number = RegressionLine.average(gapSizes, (dist) => { return 0.75 < dist && dist < 1.5; }); + + // calculate the width of 2 modules (first black pixel to first black pixel) + let sum = Point.distance(beg, this.project(this._points[0])) - unitPixelDist; + let i = 0; + for (let dist of gapSizes) { + sum += dist; + if (dist > 1.9 * unitPixelDist) { + gapSizes[i] = sum; + i += 1; + sum = 0; + } + } + gapSizes[i] = sum + Point.distance(end, this.project(this._points[this._points.length - 1])); + i += 1; + gapSizes = gapSizes.slice(0, i); // TODO: Check + + let lineLength: number = Point.distance(beg, end) - unitPixelDist; + let meanGapSize: number = lineLength / gapSizes.length; + // console.log(`Unit pixel dist: ${unitPixelDist}`) + // console.log(`lineLength: ${lineLength}, meanGapSize: ${meanGapSize}, gaps: ${gapSizes.length}`); + meanGapSize = RegressionLine.average(gapSizes, (dist) => { return Math.abs(dist - meanGapSize) < (meanGapSize / 2); }); + // console.log(`lineLength: ${lineLength}, meanGapSize: ${meanGapSize}, gaps: ${gapSizes.length}`); + return lineLength / meanGapSize; + } +}