Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ for (let i = 0; i < n; i++) {
| smoothing | (Linear weighted / Triangular / Cumulative) Moving average, Exponential average, Moving median, KZ filter, Savitzky Golay filter, Hampel filter, Kalman filter, Particle filter, Lowpass filter, Bessel filter, Butterworth filter, Chebyshev filter, Elliptic filter |
| timeseries prediction | Holt winters, AR, ARMA, SDAR, VAR, Kalman filter, MLP, RNN |
| change point detection | Cumulative sum, k-nearest neighbor, LOF, COF, SST, KLIEP, LSIF, uLSIF, LSDD, PELT, HMM, Markov switching |
| segmentation | P-Tile, Automatic thresholding, Balanced histogram thresholding, Otsu's method, Sezan, Adaptive thresholding, Bernsen, Niblack, Sauvola, Phansalkar, Split and merge, Statistical Region Merging, Mean shift |
| segmentation | P-Tile, Automatic thresholding, Balanced histogram thresholding, Otsu's method, Kittler Illingworth thresholding, Sezan, Adaptive thresholding, Bernsen, Niblack, Sauvola, Phansalkar, Split and merge, Statistical Region Merging, Mean shift |
| denoising | NL-means, Hopfield network, RBM, GBRBM |
| edge detection | Roberts cross, Sobel, Prewitt, Laplacian, LoG, Canny, Snakes |
| word embedding | Word2Vec |
Expand Down
1 change: 1 addition & 0 deletions js/model_selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ const AIMethods = [
{ value: 'automatic_thresholding', title: 'Automatic Thresholding' },
{ value: 'balanced_histogram', title: 'Balanced histogram thresholding' },
{ value: 'otsu', title: 'Otsu' },
{ value: 'kittler_illingworth', title: 'Kittler-Illingworth' },
{ value: 'sezan', title: 'Sezan' },
{ value: 'adaptive_thresholding', title: 'Adaptive Thresholding' },
{ value: 'bernsen', title: 'Bernsen' },
Expand Down
26 changes: 26 additions & 0 deletions js/view/kittler_illingworth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import KittlerIllingworthThresholding from '../../lib/model/kittler_illingworth.js'
import Controller from '../controller.js'
import { specialCategory } from '../utils.js'

export default function (platform) {
platform.setting.ml.usage = 'Click "Fit" button.'
platform.setting.ml.reference = {
author: 'J. Kittler, J. Illingworth',
title: 'Minimum error thresholding',
year: 1985,
}
platform.colorSpace = 'gray'
const controller = new Controller(platform)
const fitModel = () => {
const orgStep = platform._step
platform._step = 1
const model = new KittlerIllingworthThresholding()
const y = model.predict(platform.trainInput.flat(2))
threshold.value = model._t
platform.trainResult = y.map(v => specialCategory.density(1 - v))
platform._step = orgStep
}

controller.input.button('Fit').on('click', fitModel)
const threshold = controller.text({ label: ' Estimated threshold ' })
}
62 changes: 62 additions & 0 deletions lib/model/kittler_illingworth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Minimum error thresholding / Kittler-Illingworth Thresholding
*/
export default class KittlerIllingworthThresholding {
// Minimum error thresholding
// https://jp.mathworks.com/matlabcentral/fileexchange/45685-kittler-illingworth-thresholding
// http://www.thothchildren.com/chapter/5bc4b10451d930518902af3b
constructor() {}

/**
* Returns thresholded values.
* @param {number[]} x Training data
* @returns {(0 | 1)[]} Predicted values
*/
predict(x) {
this._cand = [...new Set(x)]
this._cand.sort((a, b) => a - b)
const n = this._cand.length

let best_j = Infinity
let best_t = 0
for (let t = 2; t < n - 1; t++) {
let p0 = 0
let p1 = 0
let m0 = 0
let m1 = 0
for (let i = 0; i < n; i++) {
if (x[i] < this._cand[t]) {
p0++
m0 += x[i]
} else {
p1++
m1 += x[i]
}
}
const r0 = p0 / n
const r1 = p1 / n
m0 /= p0
m1 /= p1

let s1 = 0
let s2 = 0
for (let i = 0; i < n; i++) {
if (x[i] < this._cand[t]) {
s1 += (x[i] - m0) ** 2
} else {
s2 += (x[i] - m1) ** 2
}
}
s1 /= p0
s2 /= p1

const j = r0 * Math.log(Math.sqrt(s1) / r0) + r1 * Math.log(Math.sqrt(s2) / r1)
if (j < best_j) {
best_j = j
best_t = t
}
}
this._t = this._cand[best_t]
return x.map(v => (v < this._t ? 0 : 1))
}
}
71 changes: 71 additions & 0 deletions tests/gui/view/kittler_illingworth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getPage } from '../helper/browser'

describe('segmentation', () => {
/** @type {Awaited<ReturnType<getPage>>} */
let page
beforeEach(async () => {
page = await getPage()

const dataURL = await page.evaluate(() => {
const canvas = document.createElement('canvas')
canvas.width = 100
canvas.height = 100
const context = canvas.getContext('2d')
const imdata = context.createImageData(canvas.width, canvas.height)
for (let i = 0, c = 0; i < canvas.height; i++) {
for (let j = 0; j < canvas.width; j++, c += 4) {
imdata.data[c] = Math.floor(Math.random() * 256)
imdata.data[c + 1] = Math.floor(Math.random() * 256)
imdata.data[c + 2] = Math.floor(Math.random() * 256)
imdata.data[c + 3] = Math.random()
}
}
context.putImageData(imdata, 0, 0)
return canvas.toDataURL()
})
const data = dataURL.replace(/^data:image\/\w+;base64,/, '')
const buf = Buffer.from(data, 'base64')

const dataSelectBox = page.locator('#ml_selector dl:first-child dd:nth-child(2) select')
await dataSelectBox.selectOption('upload')

const uploadFileInput = page.locator('#ml_selector #data_menu input[type=file]')
await uploadFileInput.setInputFiles({
name: 'image_kittler_illingworth.png',
mimeType: 'image/png',
buffer: buf,
})

const taskSelectBox = page.locator('#ml_selector dl:first-child dd:nth-child(5) select')
await taskSelectBox.selectOption('SG')
const modelSelectBox = page.locator('#ml_selector .model_selection #mlDisp')
await modelSelectBox.selectOption('kittler_illingworth')
})

afterEach(async () => {
await page?.close()
})

test('initialize', async () => {
const methodMenu = page.locator('#ml_selector #method_menu')
const buttons = methodMenu.locator('.buttons')

const threshold = buttons.locator('span:last-child')
await expect(threshold.textContent()).resolves.toBe('')
})

test('learn', async () => {
const methodMenu = page.locator('#ml_selector #method_menu')
const buttons = methodMenu.locator('.buttons')

await expect(page.locator('#image-area canvas').count()).resolves.toBe(1)
const threshold = buttons.locator('span:last-child')
await expect(threshold.textContent()).resolves.toBe('')

const fitButton = buttons.locator('input[value=Fit]')
await fitButton.dispatchEvent('click')

await expect(threshold.textContent()).resolves.toMatch(/^[0-9.]+$/)
await expect(page.locator('#image-area canvas').count()).resolves.toBe(2)
})
})
20 changes: 20 additions & 0 deletions tests/lib/model/kittler_illingworth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Matrix from '../../../lib/util/matrix.js'
import KittlerIllingworthThresholding from '../../../lib/model/kittler_illingworth.js'

import { randIndex } from '../../../lib/evaluate/clustering.js'

test('clustering', () => {
const model = new KittlerIllingworthThresholding()
const n = 50
const x = Matrix.concat(Matrix.randn(n, 1, 0, 0.1), Matrix.randn(n, 1, 5, 0.1)).value

const y = model.predict(x)
expect(y).toHaveLength(x.length)

const t = []
for (let i = 0; i < x.length; i++) {
t[i] = Math.floor(i / n)
}
const ri = randIndex(y, t)
expect(ri).toBeGreaterThan(0.9)
})