From da81da3f6f8d3747a11aa0481708522579cb5ef4 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 20 Jul 2023 19:00:59 +0200 Subject: [PATCH] Bezier --- src/display/editor/signature.js | 316 +++++++++++++++++++++++++++++++- web/app_options.js | 2 +- 2 files changed, 313 insertions(+), 5 deletions(-) diff --git a/src/display/editor/signature.js b/src/display/editor/signature.js index 0d5f994839aa4..bdb4a0ae941be 100644 --- a/src/display/editor/signature.js +++ b/src/display/editor/signature.js @@ -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) { @@ -240,9 +506,51 @@ 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, @@ -250,7 +558,7 @@ class SignatureEditor extends StampEditor { ), 0, 0 - ); + ); */ bitmap = offscreen.transferToImageBitmap(); return bitmap; diff --git a/web/app_options.js b/web/app_options.js index 742dfba5496ac..ffae2f873fbef 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -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: {