-
Notifications
You must be signed in to change notification settings - Fork 0
/
unrule
executable file
·311 lines (229 loc) · 10.5 KB
/
unrule
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#!/usr/bin/python
#
# vim: filetype=python
# ===========================================================================
# Tool unrule: remove rulers from scanned images
# ---------------------------------------------------------------------------
# Copyright: Oliver Bandel
# Copyleft: GNU GPL v3 or higher/later version
#
# Use this software at your own risk.
# ===========================================================================
import sys
import itertools as it
#from time import perf_counter as pc
import argparse
import numpy as np
from PIL import Image
np.set_printoptions(threshold=sys.maxsize, linewidth=200) # numpy-options
np.set_printoptions(formatter={'int': '{: 7d}'.format, 'float': '{: 6.3f}'.format})
def pdl(data, comment):
"""
print data and length with comment
(mainly for developing reasons)
"""
print("length of {:12s}= {}".format(comment, len(data)))
print("{:10s}{:12s}= {}".format("", comment, data))
def moving_average(array, avlen):
"""
Calculate moving average of 'array', averaged over 'avlen'-many points.
array: the array that will be averaged over
avlen: the number of elements that go into the average (window-size)
"""
if avlen < 1:
avlen = 1
convolutor = list(it.repeat(1, avlen)) # creates a list with avlen ones
return np.convolve(array, convolutor, 'valid')/avlen
def moving_average_with_diff(array, stretch, inner_len):
"""
Calculate moving average and the diff of two mov. averages.
For the two windows: left and right outside windows calculate:
- mean of pixels of both windows
- difference of the seperate means fo these windows
"""
# sanitize parameters
# -------------------
if stretch < 1:
stretch = 1
if inner_len < 1:
inner_len = 1
# mean-diff-conv window
leftconv = (np.repeat(1/stretch, stretch)) # list with stretch ones
inbetween = (np.repeat(0, inner_len)) # inner window is to be ignored
rightconv = (np.repeat(-1/stretch, stretch)) # list with stretch ones
conv_avdiff = np.concatenate((leftconv, inbetween, rightconv)) # whole window
# calculate the diff of the moving averages of left and right outer windows
diff = np.convolve(array, conv_avdiff, 'valid') * (-1) # (-1) because left-right was used for avdiff
# --------------------
# mean-conv window
meanconv = np.repeat(1/(2*stretch), stretch)
conv_mean = np.concatenate((meanconv, inbetween, meanconv)) # whole window
# calculate mean of the left and right outer window
mean = np.convolve(array, conv_mean, 'valid')
return diff, mean
class Antikaro:
"""
This class Antikaro does the main work of removing the rulers.
Antikaro: Anti means against; Karo is german word for rhombus.
Kariert (Karo-like): colloquialism for quadratic lineature of a paper.
"""
def __init__(self, filename, stretch=2, ins=2):
"""
Initialize Antikaro objects.
filename: name of the imagefile (e.g. scanned handwritings)
stretch: size of left and right outer subwindows in pixel
ins: inside window size in pixel
"""
self.filename = filename
self.bwpicarray = readimage_as_grayval_to_array(filename)
self.height, self.width = self.bwpicarray.shape
self.outpicarray = None
# set defaults
self.ins = ins # inner area: number of pixels
self.stretch = stretch # num. of pixels of left as well as right outer window
self._calc_outs() # calculate the whole number of pixels of the windows
self.avdiff_range = (-10,10) # threshold for diff between left and right
self.innernewdiff_range = (-50, -1) # diff between old and new inner
# check parameters compared to size of the image.
if self.height < 2 * self.stretch + self.ins or self.width < 2 * self.stretch + self.ins:
print("Image {} too small for stretch-/ins-settings".format(filename), file=sys.stderr)
raise ValueError
def _calc_outs(self):
"""
Calculate outer size of the whole sliding window (all three subwindows).
calculate sum of the number of pixels of the left and right outer and
the inner windows.
"""
self.outs = 2 * self.stretch + self.ins
def set_stretch(self, stretch):
"""set the size of the left as well as the right outer window"""
self.stretch = stretch
self._calc_outs()
def set_ins(self, ins):
"""set the value of the inner window (number of pixels of the Karo-lines)"""
self.ins = ins
self._calc_outs()
def set_avdiff_range(self, avdiff_range):
"""Set the threshold range for averdiff of left and right outer window."""
self.avdiff_range = avdiff_range
def set_innernewdiff_range(self, innernewdiff_range):
"""Set the threshold range for diff of inner and new."""
self.innernewdiff_range = innernewdiff_range
def save(self, outfilename):
"""
Save the picture, represented by the outpicarray to an image file.
Overwrites existing file with no mercy.
"""
save_nparray_as_pic(self.outpicarray, outfilename)
def _patch_linedata(self, linedata):
"""
Patch the line data of the image, so that the rulers are removed.
Depending on the settings and (so far hard coded) thresholds
replace the karo-line by the average color value of the outer windows.
This function awaits linedata (scanline in jpeglib-speech) and returns
the patched linedata.
in-place modification of the linedata array
Possible enhancement:
read linedata, but patch a copy to avoid mixing
already read and changed data.
"""
# for lazy typing
ins = self.ins # length inner part
stretch = self.stretch # left and right outer window size
# extract thresholds
avdiff_low, avdiff_high = self.avdiff_range
innernewdiff_low, innernewdiff_high = self.innernewdiff_range
innermvav = moving_average(linedata[stretch : -stretch], ins) # inside-win average
averdiff_arr, newval_arr = moving_average_with_diff(linedata, stretch, ins)
#if False: # need to think about this rounding again
# innermvav += 0.5
# averdiff_arr += 0.5
# newval_arr += 0.5
subwin = linedata[stretch : -stretch] # only this area can be corrected
# Threshold testing on which areas to replace
# -------------------------------------------
# find the indexes where changes need to be done
# (hard coded values, I know it's bad - remove if parametrisation makes sense)
# -------------------------------------------
#where = np.where((avdiff_low < averdiff_arr < avdiff_high) and
# (innernewdiff_low < (innermvav - newval_arr) < innernewdiff_high))
where = np.where((avdiff_low < averdiff_arr) &
(averdiff_arr < avdiff_high) &
(innernewdiff_low < innermvav - newval_arr) &
(innermvav - newval_arr < innernewdiff_high))
# set new values for all ins entries, according to the 'where' window
# ----------------------------------------------------------------
for offset in range(ins):
bidxa = (offset + where[0],)
subwin[bidxa] = newval_arr[where] # Set the new values
# Here the main work will be done
# ===============================
def remove_lineature(self):
"""
Remove lineature (rules) from the image.
Birds perspective algorithm of removing the karos to be called from
outside.
"""
bwpicarray = self.bwpicarray
height, width = bwpicarray.shape
outpicarray = bwpicarray.copy()
print("stretch, ins, outs:", self.stretch, self.ins, self.outs)
#print("whole linedata:\n", bwpicarray)
# HORIZONTAL
#t_0 = pc()
for yval in range(0, height - self.outs):
linedata = outpicarray[yval]
self._patch_linedata(linedata) # the actual data patch
outpicarray[yval] = linedata
#t_1 = pc()
#print("# HORIZONTAL: {:8.3f}".format(t_1 - t_0), file=sys.stderr, flush=True)
# VERTIKAL
for xval in range(0, width - self.outs):
linedata = outpicarray[:,xval]
self._patch_linedata(linedata) # the actual data patch
outpicarray[:,xval] = linedata
#t_2 = pc()
#print("# VERTIKAL: {:8.3f}".format(t_2 - t_1), file=sys.stderr, flush=True)
#print("===============================================")
self.outpicarray = outpicarray
return outpicarray
def readimage_to_array(filename):
"""Return numpy-array of image in file 'filename'"""
pic = Image.open(filename)
return np.array(pic)
def readimage_as_grayval_to_array(filename):
"""Return numpy-array of (b/w converted) image from file 'filename'"""
pic = Image.open(filename)
pic_grayval = pic.convert('L')
return np.array(pic_grayval)
def save_nparray_as_pic(nparr, filename):
"""Save the image in nparr as image-file to file 'filename'"""
newimage = Image.fromarray(nparr)
newimage.save(filename)
#####################################################
# initiate the cli-args parser
# ----------------------------
PROGRAMINFO = "This program removes lineature-patterns from an image. (output is black/white image)"
parser = argparse.ArgumentParser(description = PROGRAMINFO)
parser.add_argument("--ins", "-i", default=3, help="set inside-width in pixel")
parser.add_argument("--stretch", "-s", default=3, help="set stretch-width (pixels left and right of ins)")
parser.add_argument('filenames', metavar='infile', type=str, nargs='+', help='Filenames')
args = parser.parse_args()
print("---------------")
print(args)
print("---------------")
print("args.filenames):", args.filenames)
print("Try to remove lineature from these files:", args.filenames)
for filename in args.filenames:
print("working on file:", filename)
try:
#t_0 = pc()
image = Antikaro(filename, int(args.stretch), int(args.ins))
#t_1 = pc()
image.remove_lineature()
outfilename = "unruled_{0}".format(filename)
image.save(outfilename)
print("Resulting file:", outfilename)
except ValueError:
print("Error with file {}".format(filename))
continue