-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathutils.ts
138 lines (124 loc) · 3.71 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { useEffect, useState } from 'react'
const parseLine = (line: string) =>
line
.split(' ')
.filter(Boolean)
// For some wrong recognition like `B5` (stands for BD 55),
// just use the first character
.map(byte => byte[0])
export function getMostCommonLength<T>(lines: T[][]) {
const lengths: Record<number, number> = {}
lines.forEach(line => {
lengths[line.length] = lengths[line.length] || 0
lengths[line.length]++
})
return parseInt(
Object.entries(lengths).sort((a, b) => b[1] - a[1])[0]?.[0] || '0',
10
)
}
export function processMatrix(res: string) {
const lines = res
.split('\n')
.map(parseLine)
.filter(bytes => bytes.length)
const mostCommonLength = getMostCommonLength(lines)
const validLines = lines.filter(line => line.length === mostCommonLength)
const chars = new Set<string>()
validLines.forEach(bytes => {
bytes.forEach(byte => {
chars.add(byte)
})
})
return { lines: validLines, chars }
}
/**
* TODO: Add more filter methods
* @param matrixBytes The bytes appeared in the matrix
*/
export const processTargets = (res: string, matrixBytes: Set<string>) =>
res
.split('\n')
.map(parseLine)
.filter(
bytes =>
bytes.length >= 2 &&
bytes.length <= 5 &&
bytes.every(byte => matrixBytes.has(byte))
)
export function useStorage(storageKey: string, initialState?: any) {
const storedValue = window.localStorage.getItem(storageKey) || initialState
const [state, setState] = useState(storedValue)
useEffect(() => {
window.localStorage.setItem(storageKey, state)
}, [state])
return [state, setState] as const
}
/**
* Turn the photo into a "black text on white background" image.
* Not very smart, but at least it is adaptive.
*/
export function threshold(
context: CanvasRenderingContext2D,
screenshot: boolean = false
) {
const { width, height } = context.canvas
const resolution = width * height
const imageData = context.getImageData(0, 0, width, height)
const { data } = imageData
let cutAt = 128
if (!screenshot) {
// 1. Compute the histogram
const histo = Array(256).fill(0)
for (let i = 0; i < data.length; i += 4) {
// Store the sampled color to the R channel
// The RGB to grayscale threshold conversion doesn't need to be very accurate.
data[i] = Math.round(
data[i] * 0.7 + data[i + 1] * 0.2 + data[i + 2] * 0.1
)
histo[data[i]]++
}
// 2. Cut off the top 1% bright and top 1% dark region
const capThreshold = 0.01
let minCap = 0
let minAccu = 0
for (let i = 0; i < 256; i++) {
minAccu += histo[i] || 0
if (minAccu > resolution * capThreshold) {
minCap = i
break
}
}
let maxCap = 0
let maxAccu = 0
for (let i = 255; i >= 0; i--) {
maxAccu += histo[i] || 0
if (maxAccu > resolution * capThreshold) {
maxCap = i
break
}
}
// 3. Among [minCap, maxCap], search between intensity (brightness) 65% to 90%
// and find the place that has the lowest intensity.
// This might not work well if the histogram is not "U" shaped,
// and can be optimized by maybe applying Otsu's method specifically at this area.
let minHistValue = Infinity
const range = maxCap - minCap
const start = minCap + Math.floor(range * 0.65)
const end = minCap + range * 0.9
for (let i = start; i <= end; i++) {
if (histo[i] < minHistValue) {
minHistValue = histo[i]
cutAt = i
}
}
}
// 4. Threshold the image.
for (let i = 0; i < data.length; i += 4) {
const v = data[i] > cutAt ? 0 : 255
data[i] = v
data[i + 1] = v
data[i + 2] = v
}
context.putImageData(imageData, 0, 0)
}