qr code detector, marker detector
This commit is contained in:
186
src/nodes/MarkerDetector.ts
Normal file
186
src/nodes/MarkerDetector.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
ImageVar,
|
||||
NumberVar,
|
||||
StringVar,
|
||||
} from './io/AbstractIOSet';
|
||||
import Node from "./Node";
|
||||
import {canvasPool2D} from "../canvas/CanvasPool";
|
||||
import {mediaSize} from './canvas';
|
||||
import {waitForMedia} from "../utils";
|
||||
import {Detector, Marker} from '../../vendor/ts-aruco/src/aruco'
|
||||
|
||||
enum PivotPoint {
|
||||
LEFT_TOP = "left_top",
|
||||
RIGHT_TOP = "right_top",
|
||||
LEFT_BOTTOM = "left_bottom",
|
||||
RIGHT_BOTTOM = "right_bottom",
|
||||
CENTER = "center",
|
||||
}
|
||||
|
||||
const inputs = {
|
||||
image: {
|
||||
type: 'Image'
|
||||
} as ImageVar,
|
||||
markerId: {
|
||||
type: 'Number',
|
||||
} as NumberVar,
|
||||
pivotPoint: {
|
||||
type: 'String',
|
||||
enum: [
|
||||
PivotPoint.LEFT_TOP,
|
||||
PivotPoint.RIGHT_TOP,
|
||||
PivotPoint.LEFT_BOTTOM,
|
||||
PivotPoint.RIGHT_BOTTOM,
|
||||
PivotPoint.CENTER,
|
||||
],
|
||||
} as StringVar,
|
||||
}
|
||||
|
||||
const outputs = {
|
||||
image: {
|
||||
type: 'Image'
|
||||
} as ImageVar,
|
||||
markerId: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
x: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
y: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
rotation: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
x1: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
y1: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
x2: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
y2: {
|
||||
type: 'Number'
|
||||
} as NumberVar,
|
||||
};
|
||||
|
||||
type CanvasContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
||||
|
||||
export default class MarkerDetector extends Node<typeof inputs, typeof outputs> {
|
||||
detector: Detector;
|
||||
|
||||
constructor(options = {}) {
|
||||
super('MarkerDetector', inputs, outputs);
|
||||
this.detector = new Detector();
|
||||
}
|
||||
|
||||
async _update() {
|
||||
await waitForMedia(this.in.image.value);
|
||||
const {width, height} = mediaSize(this.__in.image.value);
|
||||
|
||||
if (!this.in.image.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalImageCanvas = canvasPool2D.createCanvas();
|
||||
originalImageCanvas.width = width;
|
||||
originalImageCanvas.height = height;
|
||||
originalImageCanvas.acquire();
|
||||
const originalImageCanvasCtx = originalImageCanvas.getContext('2d');
|
||||
|
||||
originalImageCanvasCtx.drawImage(this.in.image.value as CanvasImageSource | OffscreenCanvas, 0, 0);
|
||||
|
||||
let results = this.detector.detect(originalImageCanvasCtx.getImageData(0, 0, width, height));
|
||||
|
||||
if (this.in.markerId.value) {
|
||||
results = results.filter((marker) => marker.id === this.in.markerId.value);
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
this.out.image.value = originalImageCanvas;
|
||||
this.out.markerId.value = undefined;
|
||||
this.out.x.value = undefined;
|
||||
this.out.y.value = undefined;
|
||||
this.out.rotation.value = undefined;
|
||||
this.out.x1.value = undefined;
|
||||
this.out.y1.value = undefined;
|
||||
this.out.x2.value = undefined;
|
||||
this.out.y2.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = results[0];
|
||||
|
||||
const {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
|
||||
} = this.calculateOutput(result, this.in.pivotPoint.value as PivotPoint);
|
||||
|
||||
this.out.image.value = originalImageCanvas;
|
||||
this.out.markerId.value = id;
|
||||
this.out.x.value = x;
|
||||
this.out.y.value = y;
|
||||
this.out.rotation.value = rotation;
|
||||
this.out.x1.value = x1;
|
||||
this.out.y1.value = y1;
|
||||
this.out.x2.value = x2;
|
||||
this.out.y2.value = y2;
|
||||
}
|
||||
|
||||
private calculateOutput(marker: Marker, pivot: PivotPoint = PivotPoint.LEFT_TOP) {
|
||||
const {points} = marker.corners;
|
||||
|
||||
const minX = Math.min(...points.map((p) => p.x));
|
||||
const minY = Math.min(...points.map((p) => p.y));
|
||||
const maxX = Math.max(...points.map((p) => p.x));
|
||||
const maxY = Math.max(...points.map((p) => p.y));
|
||||
const w = maxX - minX;
|
||||
const h = maxY - minY;
|
||||
|
||||
let x: number, y: number;
|
||||
switch (pivot) {
|
||||
case PivotPoint.CENTER:
|
||||
x = minX + w / 2;
|
||||
y = minY + h / 2;
|
||||
break;
|
||||
case PivotPoint.LEFT_TOP:
|
||||
x = minX;
|
||||
y = minY;
|
||||
break;
|
||||
case PivotPoint.RIGHT_TOP:
|
||||
x = maxX;
|
||||
y = minY;
|
||||
break;
|
||||
case PivotPoint.LEFT_BOTTOM:
|
||||
x = minX;
|
||||
y = maxY;
|
||||
break;
|
||||
case PivotPoint.RIGHT_BOTTOM:
|
||||
x = maxX;
|
||||
y = maxY;
|
||||
break;
|
||||
}
|
||||
|
||||
const rotation = Math.atan2(points[1].y - points[0].y, points[1].x - points[0].x) * (180 / Math.PI);
|
||||
|
||||
return {
|
||||
id: marker.id,
|
||||
x,
|
||||
y,
|
||||
rotation,
|
||||
x1: points[0].x,
|
||||
y1: points[0].y,
|
||||
x2: points[1].x,
|
||||
y2: points[1].y,
|
||||
};
|
||||
}
|
||||
}
|
||||
157
src/nodes/QRCodeDetector.ts
Normal file
157
src/nodes/QRCodeDetector.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
ImageVar,
|
||||
NumberVar,
|
||||
} from './io/AbstractIOSet';
|
||||
import Node from "./Node";
|
||||
import {canvasPool2D} from "../canvas/CanvasPool";
|
||||
import {mediaSize} from './canvas';
|
||||
import {OutputProperties, StringVar} from "graphfx/src/nodes/io/AbstractIOSet";
|
||||
import {BrowserQRCodeReader} from "@zxing/browser";
|
||||
import {Result, ResultPoint, NotFoundException} from "@zxing/library";
|
||||
import {waitForMedia} from "graphfx/src/utils";
|
||||
|
||||
const inputs = {
|
||||
image: {
|
||||
type: 'Image'
|
||||
} as ImageVar,
|
||||
filter: {
|
||||
type: 'String',
|
||||
} as StringVar,
|
||||
}
|
||||
|
||||
const outputs = {
|
||||
image: {
|
||||
type: 'Image'
|
||||
} as ImageVar,
|
||||
rotation: {
|
||||
type: 'Number',
|
||||
} as NumberVar,
|
||||
x: {
|
||||
type: 'Number',
|
||||
} as NumberVar,
|
||||
y: {
|
||||
type: 'Number',
|
||||
} as NumberVar,
|
||||
size: {
|
||||
type: 'Number',
|
||||
} as NumberVar,
|
||||
};
|
||||
|
||||
type CanvasContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
||||
|
||||
export default class QRCodeDetector extends Node<typeof inputs, typeof outputs> {
|
||||
reader: BrowserQRCodeReader;
|
||||
|
||||
constructor(options = {}) {
|
||||
super('QRCodeDetector', inputs, outputs);
|
||||
this.reader = new BrowserQRCodeReader();
|
||||
}
|
||||
|
||||
async _update() {
|
||||
await waitForMedia(this.in.image.value);
|
||||
const {width, height} = mediaSize(this.__in.image.value);
|
||||
|
||||
if (!this.in.image.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalImageCanvas = canvasPool2D.createCanvas();
|
||||
originalImageCanvas.width = width;
|
||||
originalImageCanvas.height = height;
|
||||
originalImageCanvas.acquire();
|
||||
const originalCanvasCtx = originalImageCanvas.getContext('2d');
|
||||
|
||||
originalCanvasCtx.drawImage(this.in.image.value as CanvasImageSource | OffscreenCanvas, 0, 0);
|
||||
|
||||
this.out.rotation.value = 0;
|
||||
this.out.size.value = 0;
|
||||
this.out.x.value = 0;
|
||||
this.out.y.value = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const result = this.reader.decodeFromCanvas(originalImageCanvas);
|
||||
const {
|
||||
rotation,
|
||||
size,
|
||||
x,
|
||||
y,
|
||||
} = this.coverQRCode(originalCanvasCtx, result);
|
||||
|
||||
if (!this.in.filter.value || result.getText() === this.in.filter.value) {
|
||||
this.out.rotation.value = rotation;
|
||||
this.out.size.value = size;
|
||||
this.out.x.value = x;
|
||||
this.out.y.value = y;
|
||||
// this.out.image.value = this.in.image.value;
|
||||
this.out.image.value = this.in.image.value;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundException) {
|
||||
break;
|
||||
}
|
||||
this.out.image.value = this.in.image.value;
|
||||
console.log(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private calculateDistance = (point1: ResultPoint, point2: ResultPoint) => {
|
||||
const dx = point1.getX() - point2.getX();
|
||||
const dy = point1.getY() - point2.getY();
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
private calculateRotation = (point1: ResultPoint, point2: ResultPoint) => {
|
||||
const dx = point2.getX() - point1.getX();
|
||||
const dy = point2.getY() - point1.getY();
|
||||
return Math.atan2(dy, dx);
|
||||
};
|
||||
|
||||
private coverQRCode(ctx: CanvasContext, result: Result) {
|
||||
const resultPoints = result.getResultPoints();
|
||||
|
||||
if (resultPoints.length < 3) {
|
||||
console.error('Probably not a QR code')
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore estimatedModuleSize is not a part of the public API but we need that value
|
||||
const qrCodeWidth = this.calculateDistance(resultPoints[0], resultPoints[2]) - resultPoints[0].estimatedModuleSize * 6;
|
||||
// @ts-ignore estimatedModuleSize is not a part of the public API but we need that value
|
||||
const finderPatternWidth = resultPoints[0].estimatedModuleSize * 7;
|
||||
const rotation = this.calculateRotation(resultPoints[0], resultPoints[1]);
|
||||
|
||||
// Save the current context state
|
||||
ctx.save();
|
||||
|
||||
// Translate to the center of the top left finder pattern
|
||||
ctx.translate(resultPoints[0].getX(), resultPoints[0].getY());
|
||||
|
||||
// Rotate the context
|
||||
ctx.rotate(rotation);
|
||||
|
||||
// Draw the rectangle
|
||||
ctx.beginPath();
|
||||
ctx.rect(-finderPatternWidth / 2, -finderPatternWidth / 2, qrCodeWidth + finderPatternWidth, qrCodeWidth + finderPatternWidth);
|
||||
ctx.fillStyle = '#FF0000';
|
||||
ctx.fill();
|
||||
|
||||
// Restore the context state
|
||||
ctx.restore();
|
||||
|
||||
const x = (resultPoints[0].getX() + resultPoints[2].getX()) / 2;
|
||||
const y = (resultPoints[0].getY() + resultPoints[2].getY()) / 2;
|
||||
|
||||
return {
|
||||
// rotation here is in radians and I know it's -90deg, so I need to add 90deg to it
|
||||
rotation: rotation + Math.PI / 2,
|
||||
size: qrCodeWidth,
|
||||
x,
|
||||
y,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,3 +31,5 @@ export {default as BodySegmentation} from './AI/BodySegmentation';
|
||||
export {default as BodyPartSegmentation} from './AI/BodyPartSegmentation';
|
||||
export {default as FaceFeaturePosition} from './AI/FaceFeaturePosition';
|
||||
export {default as Eval} from './Eval';
|
||||
export {default as QRCodeDetector} from './QRCodeDetector';
|
||||
export {default as MarkerDetector} from './MarkerDetector';
|
||||
|
||||
Reference in New Issue
Block a user