qr code detector, marker detector

This commit is contained in:
Michal Kolář
2024-03-04 18:05:35 +01:00
committed by Stanislav Fifik
parent a37605b5b4
commit 6bc3bada6b
33 changed files with 10444 additions and 1 deletions

186
src/nodes/MarkerDetector.ts Normal file
View 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
View 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,
}
}
}

View File

@@ -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';