@ai-on-browser/data-analysis-models
Version:
Data analysis model package without any dependencies
290 lines (253 loc) • 7.06 kB
JavaScript
class Point {
constructor(p, value = null) {
this._p = p
this.value = value
}
get x() {
return this._p[0]
}
get y() {
return this._p[1]
}
distance(p) {
return Math.sqrt((this.x - p.x) ** 2 + (this.y - p.y) ** 2)
}
}
class Circle {
constructor(c, r) {
this._c = c
this._r = r
}
contains(p) {
return (p.x - this._c.x) ** 2 + (p.y - this._c.y) ** 2 < this._r ** 2
}
}
class Triangle {
constructor(p1, p2, p3) {
this.p = [p1, p2, p3]
this.adjoin = [null, null, null]
this._circumcircle = null
}
get p() {
return this._p
}
set p(points) {
this._p = points
this._circumcircle = null
}
get circumcircle() {
if (this._circumcircle) {
return this._circumcircle
}
const [p1, p2, p3] = this.p
const c = 2 * ((p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)) + 1.0e-12
const c21 = p2.x ** 2 - p1.x ** 2 + p2.y ** 2 - p1.y ** 2
const c31 = p3.x ** 2 - p1.x ** 2 + p3.y ** 2 - p1.y ** 2
const cx = ((p3.y - p1.y) * c21 + (p1.y - p2.y) * c31) / c
const cy = ((p1.x - p3.x) * c21 + (p2.x - p1.x) * c31) / c
this._circumcircle = new Circle(new Point([cx, cy]), Math.sqrt((cx - p1.x) ** 2 + (cy - p1.y) ** 2))
return this._circumcircle
}
get area() {
const [p1, p2, p3] = this.p
return Math.abs((p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)) / 2
}
contains(p) {
const outer = (p1, p2, p3) => {
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
}
const o = []
for (let i = 0; i < 3; i++) {
const oi = outer(p, this.p[i], this.p[(i + 1) % 3])
if (oi === 0) {
continue
}
if (o.length > 0 && o[o.length - 1] !== oi < 0) {
return false
}
o.push(oi < 0)
}
return true
}
contains_circle(p) {
return this.circumcircle.contains(p)
}
}
/**
* Delaunay triangulation-based spatial clustering of application with noise
*/
export default class DTSCAN {
// Delaunay Triangulation-Based Spatial Clustering Technique for Enhanced Adjacent Boundary Detection and Segmentation of LiDAR 3D Point Clouds
// https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6767241/
/**
* @param {number} [minPts] Minimum size of neighbors
* @param {number} [threshold] Remove threshold score
*/
constructor(minPts = 5, threshold = 1.0) {
this._minPts = minPts
this._area_threshold = threshold
this._length_threshold = threshold
}
/**
* Returns predicted categories.
* @param {Array<Array<number>>} x Training data
* @returns {number[]} Predicted values
*/
predict(x) {
const n = x.length
if (x[0].length !== 2) {
throw new Error('Only 2d data can apply for current implementation.')
}
const min = [Infinity, Infinity]
const max = [-Infinity, -Infinity]
for (let i = 0; i < n; i++) {
for (let d = 0; d < 2; d++) {
min[d] = Math.min(min[d], x[i][d])
max[d] = Math.max(max[d], x[i][d])
}
}
for (let d = 0; d < 2; d++) {
min[d] -= 1
max[d] += 1
}
const rootPoints = [
new Point([min[0] - (max[1] - min[1]), min[1]]),
new Point([max[0] + (max[1] - min[1]), min[1]]),
new Point([(min[0] + max[0]) / 2, max[1] + (max[0] - min[0]) / 2]),
]
const triangles = [new Triangle(...rootPoints)]
for (let i = 0; i < n; i++) {
const xi = new Point(x[i], i)
let k = 0
for (; k < triangles.length; k++) {
if (triangles[k].contains(xi)) {
break
}
}
const t = triangles.splice(k, 1)[0]
const nt1 = new Triangle(xi, t.p[1], t.p[2])
const nt2 = new Triangle(xi, t.p[2], t.p[0])
const nt3 = new Triangle(xi, t.p[0], t.p[1])
nt1.adjoin = [t.adjoin[0], nt2, nt3]
nt2.adjoin = [t.adjoin[1], nt3, nt1]
nt3.adjoin = [t.adjoin[2], nt1, nt2]
const nt = [nt1, nt2, nt3]
for (let j = 0; j < t.adjoin.length; j++) {
if (!t.adjoin[j]) {
continue
}
const m = t.adjoin[j].adjoin.indexOf(t)
t.adjoin[j].adjoin[m] = nt[j]
}
triangles.push(...nt)
const checkFlip = nt.map(t => [t, 0])
while (checkFlip.length > 0) {
const [cf, j] = checkFlip.pop()
const ad = cf.adjoin[j]
if (!ad) {
continue
}
const m = ad.adjoin.indexOf(cf)
if (!cf.contains_circle(ad.p[m])) {
continue
}
const j1 = (j + 1) % 3
const j2 = (j + 2) % 3
let m1 = (m + 1) % 3
let m2 = (m + 2) % 3
if (ad.p[m1].x !== cf.p[j1].x || ad.p[m1].y !== cf.p[j1].y) {
;[m1, m2] = [m2, m1]
}
const cf_p = cf.p
const cf_a = cf.adjoin
const ad_a = ad.adjoin
cf.p = [cf.p[j], cf.p[j1], ad.p[m]]
cf.adjoin = [ad_a[m2], ad, cf_a[j2]]
if (ad_a[m2]) {
ad_a[m2].adjoin[ad_a[m2].adjoin.indexOf(ad)] = cf
}
ad.p = [cf_p[j], cf_p[j2], ad.p[m]]
ad.adjoin = [ad_a[m1], cf, cf_a[j1]]
if (cf_a[j1]) {
cf_a[j1].adjoin[cf_a[j1].adjoin.indexOf(cf)] = ad
}
checkFlip.push([cf, 0])
checkFlip.push([ad, 0])
}
}
for (let i = triangles.length - 1; i >= 0; i--) {
if (triangles[i].p.some(p => rootPoints.some(rp => p.x === rp.x && p.y === rp.y))) {
triangles.splice(i, 1)
}
}
const areas = []
const lengthes = []
for (const triangle of triangles) {
areas.push(triangle.area)
const [p1, p2, p3] = triangle.p
lengthes.push(p1.distance(p2), p2.distance(p3), p3.distance(p1))
}
const areamean = areas.reduce((s, v) => s + v, 0) / areas.length
const areavar = areas.reduce((s, v) => s + (v - areamean) ** 2, 0) / areas.length
const areastd = Math.sqrt(areavar)
const lengthmean = lengthes.reduce((s, v) => s + v, 0) / lengthes.length
const lengthvar = lengthes.reduce((s, v) => s + (v - lengthmean) ** 2, 0) / lengthes.length
const lengthstd = Math.sqrt(lengthvar)
const neighbors = Array.from(x, () => new Set())
for (const triangle of triangles) {
const areaz = (triangle.area - areamean) / areastd
if (areaz >= this._area_threshold) {
continue
}
const [p1, p2, p3] = triangle.p
const len12z = (p1.distance(p2) - lengthmean) / lengthstd
if (len12z < this._length_threshold) {
neighbors[p1.value].add(p2.value)
neighbors[p2.value].add(p1.value)
}
const len23z = (p2.distance(p3) - lengthmean) / lengthstd
if (len23z < this._length_threshold) {
neighbors[p2.value].add(p3.value)
neighbors[p3.value].add(p2.value)
}
const len13z = (p1.distance(p3) - lengthmean) / lengthstd
if (len13z < this._length_threshold) {
neighbors[p1.value].add(p3.value)
neighbors[p3.value].add(p1.value)
}
}
const p = Array(n).fill(-1)
const visited = Array(n).fill(false)
let c = -1
const stack = []
while (true) {
if (stack.length === 0) {
for (let i = 0; i < n; i++) {
if (!visited[i]) {
if (neighbors[i].size < this._minPts) {
visited[i] = true
continue
}
stack.push(i)
c++
break
}
}
if (stack.length === 0) {
break
}
}
const pi = stack.pop()
if (visited[pi]) {
continue
}
visited[pi] = true
if (neighbors[pi].size < this._minPts) {
continue
}
p[pi] = c
stack.push(...neighbors[pi])
}
return p
}
}