forked from makiuchi-d/gozxing
-
Notifications
You must be signed in to change notification settings - Fork 1
/
hybrid_binarizer.go
180 lines (166 loc) · 5.64 KB
/
hybrid_binarizer.go
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package gozxing
const (
BLOCK_SIZE_POWER = 3
BLOCK_SIZE = 1 << BLOCK_SIZE_POWER // ...0100...00
BLOCK_SIZE_MASK = BLOCK_SIZE - 1 // ...0011...11
MINIMUM_DIMENSION = BLOCK_SIZE * 5
MIN_DYNAMIC_RANGE = 24
)
type HybridBinarizer struct {
*GlobalHistogramBinarizer
matrix *BitMatrix
}
func NewHybridBinarizer(source LuminanceSource) Binarizer {
return &HybridBinarizer{
NewGlobalHistgramBinarizer(source).(*GlobalHistogramBinarizer),
nil,
}
}
func (this *HybridBinarizer) GetBlackMatrix() (*BitMatrix, error) {
if this.matrix != nil {
return this.matrix, nil
}
source := this.GetLuminanceSource()
width := source.GetWidth()
height := source.GetHeight()
if width >= MINIMUM_DIMENSION && height >= MINIMUM_DIMENSION {
luminances := source.GetMatrix()
subWidth := width >> BLOCK_SIZE_POWER
if (width & BLOCK_SIZE_MASK) != 0 {
subWidth++
}
subHeight := height >> BLOCK_SIZE_POWER
if (height & BLOCK_SIZE_MASK) != 0 {
subHeight++
}
blackPoints := this.calculateBlackPoints(luminances, subWidth, subHeight, width, height)
newMatrix, _ := NewBitMatrix(width, height)
this.calculateThresholdForBlock(luminances, subWidth, subHeight, width, height, blackPoints, newMatrix)
this.matrix = newMatrix
} else {
// If the image is too small, fall back to the global histogram approach.
newMatrix, e := this.GlobalHistogramBinarizer.GetBlackMatrix()
if e != nil {
return nil, e
}
this.matrix = newMatrix
}
return this.matrix, nil
}
func (this *HybridBinarizer) CreateBinarizer(source LuminanceSource) Binarizer {
return NewHybridBinarizer(source)
}
func (this *HybridBinarizer) calculateThresholdForBlock(
luminances []byte, subWidth, subHeight, width, height int, blackPoints [][]int, matrix *BitMatrix) {
maxYOffset := height - BLOCK_SIZE
maxXOffset := width - BLOCK_SIZE
for y := 0; y < subHeight; y++ {
yoffset := y << BLOCK_SIZE_POWER
if yoffset > maxYOffset {
yoffset = maxYOffset
}
top := this.cap(y, 2, subHeight-3)
for x := 0; x < subWidth; x++ {
xoffset := x << BLOCK_SIZE_POWER
if xoffset > maxXOffset {
xoffset = maxXOffset
}
left := this.cap(x, 2, subWidth-3)
sum := 0
for z := -2; z <= 2; z++ {
blackRow := blackPoints[top+z]
sum += blackRow[left-2] + blackRow[left-1] + blackRow[left] + blackRow[left+1] + blackRow[left+2]
}
average := sum / 25
this.thresholdBlock(luminances, xoffset, yoffset, average, width, matrix)
}
}
}
func (this *HybridBinarizer) cap(value, min, max int) int {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func (this *HybridBinarizer) thresholdBlock(luminances []byte, xoffset, yoffset, threshold, stride int, matrix *BitMatrix) {
for y, offset := 0, yoffset*stride+xoffset; y < BLOCK_SIZE; y, offset = y+1, offset+stride {
for x := 0; x < BLOCK_SIZE; x++ {
// Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0.
if int(luminances[offset+x]&0xFF) <= threshold {
matrix.Set(xoffset+x, yoffset+y)
}
}
}
}
func (this *HybridBinarizer) calculateBlackPoints(luminances []byte, subWidth, subHeight, width, height int) [][]int {
maxYOffset := height - BLOCK_SIZE
maxXOffset := width - BLOCK_SIZE
blackPoints := make([][]int, subHeight)
for y := 0; y < subHeight; y++ {
blackPoints[y] = make([]int, subWidth)
yoffset := y << BLOCK_SIZE_POWER
if yoffset > maxYOffset {
yoffset = maxYOffset
}
for x := 0; x < subWidth; x++ {
xoffset := x << BLOCK_SIZE_POWER
if xoffset > maxXOffset {
xoffset = maxXOffset
}
sum := 0
min := 0xFF
max := 0
for yy, offset := 0, yoffset*width+xoffset; yy < BLOCK_SIZE; yy, offset = yy+1, offset+width {
for xx := 0; xx < BLOCK_SIZE; xx++ {
pixel := int(luminances[offset+xx] & 0xFF)
sum += pixel
// still looking for good contrast
if pixel < min {
min = pixel
}
if pixel > max {
max = pixel
}
}
// short-circuit min/max tests once dynamic range is met
if max-min > MIN_DYNAMIC_RANGE {
// finish the rest of the rows quickly
for yy, offset = yy+1, offset+width; yy < BLOCK_SIZE; yy, offset = yy+1, offset+width {
for xx := 0; xx < BLOCK_SIZE; xx++ {
sum += int(luminances[offset+xx] & 0xFF)
}
}
}
}
// The default estimate is the average of the values in the block.
average := sum >> (BLOCK_SIZE_POWER * 2)
if max-min <= MIN_DYNAMIC_RANGE {
// If variation within the block is low, assume this is a block with only light or only
// dark pixels. In that case we do not want to use the average, as it would divide this
// low contrast area into black and white pixels, essentially creating data out of noise.
//
// The default assumption is that the block is light/background. Since no estimate for
// the level of dark pixels exists locally, use half the min for the block.
average = min / 2
if y > 0 && x > 0 {
// Correct the "white background" assumption for blocks that have neighbors by comparing
// the pixels in this block to the previously calculated black points. This is based on
// the fact that dark barcode symbology is always surrounded by some amount of light
// background for which reasonable black point estimates were made. The bp estimated at
// the boundaries is used for the interior.
// The (min < bp) is arbitrary but works better than other heuristics that were tried.
averageNeighborBlackPoint :=
(blackPoints[y-1][x] + (2 * blackPoints[y][x-1]) + blackPoints[y-1][x-1]) / 4
if min < averageNeighborBlackPoint {
average = averageNeighborBlackPoint
}
}
}
blackPoints[y][x] = average
}
}
return blackPoints
}