Skip to content

Commit 8f0b3fb

Browse files
committed
Add Kittler-Illingworth thresholding
1 parent 07d49bf commit 8f0b3fb

File tree

6 files changed

+181
-1
lines changed

6 files changed

+181
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ for (let i = 0; i < n; i++) {
136136
| 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 |
137137
| timeseries prediction | Holt winters, AR, ARMA, SDAR, VAR, Kalman filter, MLP, RNN |
138138
| change point detection | Cumulative sum, k-nearest neighbor, LOF, COF, SST, KLIEP, LSIF, uLSIF, LSDD, PELT, HMM, Markov switching |
139-
| 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 |
139+
| 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 |
140140
| denoising | NL-means, Hopfield network, RBM, GBRBM |
141141
| edge detection | Roberts cross, Sobel, Prewitt, Laplacian, LoG, Canny, Snakes |
142142
| word embedding | Word2Vec |

js/model_selector.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ const AIMethods = [
548548
{ value: 'automatic_thresholding', title: 'Automatic Thresholding' },
549549
{ value: 'balanced_histogram', title: 'Balanced histogram thresholding' },
550550
{ value: 'otsu', title: 'Otsu' },
551+
{ value: 'kittler_illingworth', title: 'Kittler-Illingworth' },
551552
{ value: 'sezan', title: 'Sezan' },
552553
{ value: 'adaptive_thresholding', title: 'Adaptive Thresholding' },
553554
{ value: 'bernsen', title: 'Bernsen' },

js/view/kittler_illingworth.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import KittlerIllingworthThresholding from '../../lib/model/kittler_illingworth.js'
2+
import Controller from '../controller.js'
3+
import { specialCategory } from '../utils.js'
4+
5+
export default function (platform) {
6+
platform.setting.ml.usage = 'Click "Fit" button.'
7+
platform.setting.ml.reference = {
8+
author: 'J. Kittler, J. Illingworth',
9+
title: 'Minimum error thresholding',
10+
year: 1985,
11+
}
12+
platform.colorSpace = 'gray'
13+
const controller = new Controller(platform)
14+
const fitModel = () => {
15+
const orgStep = platform._step
16+
platform._step = 1
17+
const model = new KittlerIllingworthThresholding()
18+
const y = model.predict(platform.trainInput.flat(2))
19+
threshold.value = model._t
20+
platform.trainResult = y.map(v => specialCategory.density(1 - v))
21+
platform._step = orgStep
22+
}
23+
24+
controller.input.button('Fit').on('click', fitModel)
25+
const threshold = controller.text({ label: ' Estimated threshold ' })
26+
}

lib/model/kittler_illingworth.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Minimum error thresholding / Kittler-Illingworth Thresholding
3+
*/
4+
export default class KittlerIllingworthThresholding {
5+
// Minimum error thresholding
6+
// https://jp.mathworks.com/matlabcentral/fileexchange/45685-kittler-illingworth-thresholding
7+
// http://www.thothchildren.com/chapter/5bc4b10451d930518902af3b
8+
constructor() {}
9+
10+
/**
11+
* Returns thresholded values.
12+
* @param {number[]} x Training data
13+
* @returns {(0 | 1)[]} Predicted values
14+
*/
15+
predict(x) {
16+
this._cand = [...new Set(x)]
17+
this._cand.sort((a, b) => a - b)
18+
const n = this._cand.length
19+
20+
let best_j = Infinity
21+
let best_t = 0
22+
for (let t = 2; t < n - 1; t++) {
23+
let p0 = 0
24+
let p1 = 0
25+
let m0 = 0
26+
let m1 = 0
27+
for (let i = 0; i < n; i++) {
28+
if (x[i] < this._cand[t]) {
29+
p0++
30+
m0 += x[i]
31+
} else {
32+
p1++
33+
m1 += x[i]
34+
}
35+
}
36+
const r0 = p0 / n
37+
const r1 = p1 / n
38+
m0 /= p0
39+
m1 /= p1
40+
41+
let s1 = 0
42+
let s2 = 0
43+
for (let i = 0; i < n; i++) {
44+
if (x[i] < this._cand[t]) {
45+
s1 += (x[i] - m0) ** 2
46+
} else {
47+
s2 += (x[i] - m1) ** 2
48+
}
49+
}
50+
s1 /= p0
51+
s2 /= p1
52+
53+
const j = r0 * Math.log(Math.sqrt(s1) / r0) + r1 * Math.log(Math.sqrt(s2) / r1)
54+
if (j < best_j) {
55+
best_j = j
56+
best_t = t
57+
}
58+
}
59+
this._t = this._cand[best_t]
60+
return x.map(v => (v < this._t ? 0 : 1))
61+
}
62+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { getPage } from '../helper/browser'
2+
3+
describe('segmentation', () => {
4+
/** @type {Awaited<ReturnType<getPage>>} */
5+
let page
6+
beforeEach(async () => {
7+
page = await getPage()
8+
9+
const dataURL = await page.evaluate(() => {
10+
const canvas = document.createElement('canvas')
11+
canvas.width = 100
12+
canvas.height = 100
13+
const context = canvas.getContext('2d')
14+
const imdata = context.createImageData(canvas.width, canvas.height)
15+
for (let i = 0, c = 0; i < canvas.height; i++) {
16+
for (let j = 0; j < canvas.width; j++, c += 4) {
17+
imdata.data[c] = Math.floor(Math.random() * 256)
18+
imdata.data[c + 1] = Math.floor(Math.random() * 256)
19+
imdata.data[c + 2] = Math.floor(Math.random() * 256)
20+
imdata.data[c + 3] = Math.random()
21+
}
22+
}
23+
context.putImageData(imdata, 0, 0)
24+
return canvas.toDataURL()
25+
})
26+
const data = dataURL.replace(/^data:image\/\w+;base64,/, '')
27+
const buf = Buffer.from(data, 'base64')
28+
29+
const dataSelectBox = page.locator('#ml_selector dl:first-child dd:nth-child(2) select')
30+
await dataSelectBox.selectOption('upload')
31+
32+
const uploadFileInput = page.locator('#ml_selector #data_menu input[type=file]')
33+
await uploadFileInput.setInputFiles({
34+
name: 'image_kittler_illingworth.png',
35+
mimeType: 'image/png',
36+
buffer: buf,
37+
})
38+
39+
const taskSelectBox = page.locator('#ml_selector dl:first-child dd:nth-child(5) select')
40+
await taskSelectBox.selectOption('SG')
41+
const modelSelectBox = page.locator('#ml_selector .model_selection #mlDisp')
42+
await modelSelectBox.selectOption('kittler_illingworth')
43+
})
44+
45+
afterEach(async () => {
46+
await page?.close()
47+
})
48+
49+
test('initialize', async () => {
50+
const methodMenu = page.locator('#ml_selector #method_menu')
51+
const buttons = methodMenu.locator('.buttons')
52+
53+
const threshold = buttons.locator('span:last-child')
54+
await expect(threshold.textContent()).resolves.toBe('')
55+
})
56+
57+
test('learn', async () => {
58+
const methodMenu = page.locator('#ml_selector #method_menu')
59+
const buttons = methodMenu.locator('.buttons')
60+
61+
await expect(page.locator('#image-area canvas').count()).resolves.toBe(1)
62+
const threshold = buttons.locator('span:last-child')
63+
await expect(threshold.textContent()).resolves.toBe('')
64+
65+
const fitButton = buttons.locator('input[value=Fit]')
66+
await fitButton.dispatchEvent('click')
67+
68+
await expect(threshold.textContent()).resolves.toMatch(/^[0-9.]+$/)
69+
await expect(page.locator('#image-area canvas').count()).resolves.toBe(2)
70+
})
71+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Matrix from '../../../lib/util/matrix.js'
2+
import KittlerIllingworthThresholding from '../../../lib/model/kittler_illingworth.js'
3+
4+
import { randIndex } from '../../../lib/evaluate/clustering.js'
5+
6+
test('clustering', () => {
7+
const model = new KittlerIllingworthThresholding()
8+
const n = 50
9+
const x = Matrix.concat(Matrix.randn(n, 1, 0, 0.1), Matrix.randn(n, 1, 5, 0.1)).value
10+
11+
const y = model.predict(x)
12+
expect(y).toHaveLength(x.length)
13+
14+
const t = []
15+
for (let i = 0; i < x.length; i++) {
16+
t[i] = Math.floor(i / n)
17+
}
18+
const ri = randIndex(y, t)
19+
expect(ri).toBeGreaterThan(0.9)
20+
})

0 commit comments

Comments
 (0)