Skip to content

Commit

Permalink
Bezier
Browse files Browse the repository at this point in the history
  • Loading branch information
calixteman committed Jul 25, 2023
1 parent 547bd57 commit da81da3
Show file tree
Hide file tree
Showing 2 changed files with 313 additions and 5 deletions.
316 changes: 312 additions & 4 deletions src/display/editor/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,273 @@ class SignatureEditor extends StampEditor {
}

get MAX_RATIO() {
return 0.1;
return 0.75;
}

static #neighborIndexToId(i0, j0, i, j) {
/*
3 2 1
4 X 0
5 6 7
0: 0,1 => 7
1: -1,1 => 6
2: -1,0 => 3
3: -1,-1 => 0
4: 0,-1 => 1
5: 1,-1 => 2
6: 1,0 => 5
7: 1,1 => 8
*/
const id = i - i0 + 3 * (j - j0) + 4;
switch (id) {
case 0:
return 3;
case 1:
return 4;
case 2:
return 5;
case 3:
return 2;
case 5:
return 6;
case 6:
return 1;
case 7:
return 0;
case 8:
return 7;
default:
return -1;
}
}

static #neighborIdToIndex(i, j, id) {
/*
3 2 1
4 X 0
5 6 7
*/
switch (id) {
case 0:
return [i, j + 1];
case 1:
return [i - 1, j + 1];
case 2:
return [i - 1, j];
case 3:
return [i - 1, j - 1];
case 4:
return [i, j - 1];
case 5:
return [i + 1, j - 1];
case 6:
return [i + 1, j];
case 7:
return [i + 1, j + 1];
default:
return null;
}
}

static #clockwiseNonZero(buf, width, i0, j0, i, j, offset) {
const id = this.#neighborIndexToId(i0, j0, i, j);
for (let k = 0; k < 8; k++) {
const kk = (-k + id - offset + 16) % 8;
const ij = this.#neighborIdToIndex(i0, j0, kk);
if (buf[ij[0] * width + ij[1]] !== 0) {
return ij;
}
}
return null;
}

static #counterclockwiseNonZero(buf, width, i0, j0, i, j, offset) {
const id = this.#neighborIndexToId(i0, j0, i, j);
for (let k = 0; k < 8; k++) {
const kk = (k + id + offset + 16) % 8;
const ij = this.#neighborIdToIndex(i0, j0, kk);
if (buf[ij[0] * width + ij[1]] !== 0) {
return ij;
}
}
return null;
}

static #findCountours(buf, width, height, threshold) {
// Based on the Suzuki's algorithm:
// https://www.nevis.columbia.edu/~vgenty/public/suzuki_et_al.pdf

const N = buf.length;
const types = new Int32Array(N);
for (let i = 0; i < N; i++) {
types[i] = buf[i] <= threshold ? 1 : 0;
}

for (let i = 1; i < height - 1; i++) {
types[i * width] = types[i * width + width - 1] = 0;
}
for (let i = 0; i < width; i++) {
types[i] = types[width * height - 1 - i] = 0;
}

let nbd = 1;
let lnbd;
const contours = [];

for (let i = 1; i < height - 1; i++) {
lnbd = 1;
for (let j = 1; j < width - 1; j++) {
const ij = i * width + j;
const pix = types[ij];
if (pix === 0) {
continue;
}

let i2 = i;
let j2 = j;

if (pix === 1 && types[ij - 1] === 0) {
// Outer border.
nbd += 1;
j2 -= 1;
} else if (pix >= 1 && types[ij + 1] === 0) {
// Hole border.
nbd += 1;
j2 += 1;
if (pix > 1) {
lnbd = pix;
}
} else {
if (pix !== 1) {
lnbd = Math.abs(pix);
}
continue;
}

const points = [[j, i]];
const isHole = j2 === j + 1;
const contour = {
isHole,
points,
id: nbd,
parent: 0,
};
contours.push(contour);

let contour0;
for (const c of contours) {
if (c.id === lnbd) {
contour0 = c;
break;
}
}

if (!contour0) {
contour.parent = isHole ? lnbd : 0;
} else if (contour0.isHole) {
contour.parent = isHole ? contour0.parent : lnbd;
} else {
contour.parent = isHole ? lnbd : contour0.parent;
}

const i1j1 = this.#clockwiseNonZero(types, width, i, j, i2, j2, 0);
if (i1j1 === null) {
types[ij] = -nbd;
if (types[ij] !== 1) {
lnbd = Math.abs(types[ij]);
}
continue;
}
const [i1, j1] = i1j1;
i2 = i1;
j2 = j1;
let i3 = i;
let j3 = j;

while (true) {
const [i4, j4] = this.#counterclockwiseNonZero(
types,
width,
i3,
j3,
i2,
j2,
1
);
points.push([j4, i4]);
const ij3 = i3 * width + j3;
if (types[ij3 + 1] === 0) {
types[ij3] = -nbd;
} else if (types[ij3] === 1) {
types[ij3] = nbd;
}

if (i4 === i && j4 === j && i3 === i1 && j3 === j1) {
if (types[ij] !== 1) {
lnbd = Math.abs(types[ij]);
}
break;
} else {
i2 = i3;
j2 = j3;
i3 = i4;
j3 = j4;
}
}
}
}
return contours;
}

static #douglasPeucker(points) {
// Based on the Douglas-Peucker algorithm:
// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
let dmax = 0;
let index = 0;
const end = points.length;
if (points.length < 3) {
return points;
}

const first = points[0];
const last = points[end - 2];
const [ax, ay] = first;
const [bx, by] = last;
const abx = bx - ax;
const aby = by - ay;
const dist = Math.hypot(abx, aby);

// Guessing the epsilon value.
// See "A novel framework for making dominant point detection methods
// non-parametric".
const m = aby / abx;
const invS = 1 / dist;
const phi = Math.atan(m);
const cosPhi = Math.cos(phi);
const sinPHi = Math.sin(phi);
const tmax = invS * (Math.abs(cosPhi) + Math.abs(sinPHi));
const poly = 1 - tmax + tmax * tmax;
const partialPhi = Math.max(
Math.atan(invS * Math.abs(sinPHi + cosPhi) * poly),
Math.atan(invS * Math.abs(sinPHi - cosPhi) * poly)
);
const epsilon = (dist * partialPhi) ** 2;

for (let i = 1; i < end - 1; i++) {
const [x, y] = points[i];
const d = Math.abs(abx * (ay - y) - aby * (ax - x)) / dist;
if (d > dmax) {
index = i;
dmax = d;
}
}
if (dmax > epsilon) {
const recResults1 = this.#douglasPeucker(points.slice(0, index + 1));
const recResults2 = this.#douglasPeucker(points.slice(index));
return recResults1.slice(0, -1).concat(recResults2);
}
return [first, last];
}

static #bilateralFilter(buf, width, height, sigmaS, sigmaR, kernelSize) {
Expand Down Expand Up @@ -240,17 +506,59 @@ class SignatureEditor extends StampEditor {
16
);
const threshold = this.#guessThreshold(histogram);
const uint32Thresholded = this.#threshold(uint8Filtered, threshold);
// const thresholded = this.#threshold(uint8Filtered, threshold);
const contourList = this.#findCountours(
uint8Filtered,
newWidth,
newHeight,
threshold
);
console.log(contourList);

ctx.putImageData(
ctx.filter = "none";
//ctx.fillStyle = "white";
//ctx.fillRect(0, 0, newWidth, newHeight);
ctx.clearRect(0, 0, newWidth, newHeight);
ctx.fillStyle = "black";
ctx.beginPath();

for (const contour of contourList) {
let { points } = contour;
points = this.#douglasPeucker(points);
ctx.moveTo(...points[0]);

for (let i = 2; i < points.length; i++) {
const [x0, y0] = points[i - 2];
const [x1, y1] = points[i - 1];
const [x2, y2] = points[i];
const prevX = (x0 + x1) / 2;
const prevY = (y0 + y1) / 2;
const x3 = (x1 + x2) / 2;
const y3 = (y1 + y2) / 2;
ctx.bezierCurveTo(
prevX + (2 * (x1 - prevX)) / 3,
prevY + (2 * (y1 - prevY)) / 3,
x3 + (2 * (x1 - x3)) / 3,
y3 + (2 * (y1 - y3)) / 3,
x3,
y3
);
}
}

ctx.fill();

// const uint32Thresholded = this.#threshold(uint8Filtered, threshold);

/* ctx.putImageData(
new ImageData(
new Uint8ClampedArray(uint32Thresholded.buffer),
newWidth,
newHeight
),
0,
0
);
); */
bitmap = offscreen.transferToImageBitmap();

return bitmap;
Expand Down
2 changes: 1 addition & 1 deletion web/app_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const defaultOptions = {
// suitable for Firefox, it's why it's disabled by default.
// TODO: remove it when unnecessary.
/** @type {boolean} */
value: false,
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableStampEditor: {
Expand Down

0 comments on commit da81da3

Please sign in to comment.