-
Notifications
You must be signed in to change notification settings - Fork 13
/
base.py
273 lines (234 loc) · 10.4 KB
/
base.py
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
from ast import Import
from rodan.jobs.base import RodanTask
try:
from gamera.core import init_gamera, Image, load_image
from gamera import gamera_xml
from .ProjectionSplitting import ProjectionSplitter
from .DirtyLayerRepair import DirtyLayerRepairman
init_gamera()
except ImportError:
pass
class DiagonalNeumeSlicing(RodanTask):
name = 'Diagonal Neume Slicing'
author = 'Noah Baxter'
description = 'A tool for splitting neumes into neume components based on diagonal projection.'
enabled = True
category = 'Image Processing'
interactive = False
settings = {
'title': 'Settings',
'type': 'object',
"job_queue": "Python3",
'required': ['Smoothing', 'Minimum Glyph Size', 'Maximum Recursive Cuts', 'Angle', 'Minimum Slice Spread', 'Low Valley Threshold', 'Minimum Segment Length', 'Slice Prioritization'],
'properties': {
'Smoothing': {
'type': 'integer',
'default': 1,
'minimum': 1,
'maximum': 20,
'description': 'How much convolution to apply to projections. More smoothing results in softer cuts.'
},
'Minimum Glyph Size': {
'type': 'integer',
'default': 0,
'minimum': 0,
'maximum': 9999999,
'description': 'Discard post-splitting glyphs with an x or y dimension less than the Minimum Glyph Size.'
},
'Maximum Recursive Cuts': {
'type': 'integer',
'default': 10,
'minimum': 1,
'maximum': 100,
'description': 'How many subcuts are allowed on a glyph. Note that this does not equate to the number of cuts, as if no ideal cuts can be found, the image is returned unprocessed.'
},
'Angle': {
'type': 'integer',
'default': 45,
'minimum': 0,
'maximum': 90,
'description': 'The angle of rotation for finding projections and cutting the image. Best between 30-70 degrees.'
},
'Minimum Slice Spread': {
'type': 'number',
'default': 0.3,
'minimum': 0.0,
'maximum': 1.0,
'description': 'The minimum spread from valley to peak a slice point must have. Value is a percentage of the maximum projection.'
},
'Low Valley Threshold': {
'type': 'number',
'default': 1.0,
'minimum': 0.0,
'maximum': 1.0,
'description': 'Forces a cut when a valley lies bellow a percentage of the maximum projection.'
},
'Minimum Segment Length': {
'type': 'integer',
'default': 5,
'minimum': 0,
'maximum': 9999999,
'description': 'The minimum number of projection values a segment must have. Lower values will allow slice points to be closer together.'
},
'Slice Prioritization': {
'enum': ['None', 'Horizontal', 'Vertical', 'Multi-Cut'],
'default': 'Vertical',
'description': 'Prioritize cuts in one dimension over cuts in the other, or picks the overall best slice from both. Horizontal tends to give better results as neumes mostly extend horizontally.'
},
},
}
input_port_types = [{
'name': 'GameraXML - Connected Components',
'resource_types': ['application/gamera+xml'],
'minimum': 1,
'maximum': 1,
'is_list': False,
}]
output_port_types = [{
'name': 'GameraXML - Connected Components',
'resource_types': ['application/gamera+xml'],
'minimum': 1,
'maximum': 1,
'is_list': False,
}]
def run_my_task(self, inputs, settings, outputs):
glyphs = gamera_xml.glyphs_from_xml(inputs['GameraXML - Connected Components'][0]['resource_path'])
print (settings)
kwargs = {
'smoothing': settings['Smoothing'],
'extrema_threshold': 0,
'min_glyph_size': settings['Minimum Glyph Size'],
'max_recursive_cuts': settings['Maximum Recursive Cuts'],
'rotation': settings['Angle'],
# will it cut?
'min_slice_spread_rel': settings['Minimum Slice Spread'], # minimum spread for a cut
'low_projection_threshold': settings['Low Valley Threshold'], # FORCE a cut if valley under a certain value
'min_projection_segments': settings['Minimum Segment Length'], # ++ less likely to cut, -- more slice points
# Cut prioritizing
'slice_prioritization': settings['Slice Prioritization'],
'prefer_multi_cuts': True if settings['Slice Prioritization'] == 'Multi-Cut' else False,
'prefer_x': True if settings['Slice Prioritization'] == 'Horizontal' else False,
'prefer_y': True if settings['Slice Prioritization'] == 'Vertical' else False,
# Try rotated AND non-rotated projections
'check_axis': False,
# Debug Options
'print_projection_array': False,
'plot_projection_array': False, # script only
'save_cuts': False,
}
ps = ProjectionSplitter(**kwargs)
output_glyphs = []
for g in glyphs:
output_glyphs += ps.run(g)
outfile_path = outputs['GameraXML - Connected Components'][0]['resource_path']
output_xml = gamera_xml.WriteXMLFile(glyphs=output_glyphs,
with_features=True)
output_xml.write_filename(outfile_path)
return True
def test_my_task(self, testcase):
import cv2
import numpy as np
input_cc_png_path = "/code/Rodan/rodan/test/files/240r_CC-analysis_output.xml"
output_path = testcase.new_available_path()
gt_output_path = "/code/Rodan/rodan/test/files/240r_diagonal-neume-slicing_output.xml"
inputs = {
"GameraXML - Connected Components": [{"resource_path":input_cc_png_path}]
}
outputs = {
"GameraXML - Connected Components": [{"resource_path":output_path}]
}
settings = {'Slice Prioritization': 2, 'Minimum Segment Length': 5, 'Angle': 45, 'Minimum Glyph Size': 0, 'Maximum Recursive Cuts': 10, 'Minimum Slice Spread': 0.3, 'Low Valley Threshold': 1, 'Smoothing': 1}
self.run_my_task(inputs=inputs, outputs=outputs, settings=settings)
# Read the gt and predicted result
with open(output_path, "r") as fp:
predicted = [l.strip() for l in fp.readlines()]
with open(gt_output_path, "r") as fp:
gt = [l.strip() for l in fp.readlines()]
# The number lines should be identical
testcase.assertEqual(len(gt), len(predicted))
# also each line should be identical to its counterpart
for i, (gt_line, pred_line) in enumerate(zip(gt, predicted)):
testcase.assertEqual(gt_line, pred_line, "Line {}".format(i))
class DirtyLayerRepair(RodanTask):
name = 'Dirty Layer Repair'
author = 'Noah Baxter'
description = 'A tool for \'repairing\' broken layers by adding errors from a dirty layer. For example, using a text layer to repair its neume layer.'
enabled = True
category = 'Image Processing'
interactive = False
settings = {
'title': 'Settings',
'type': 'object',
"job_queue": "Python3",
'required': ['Minimum Density', 'Despeckle Size'],
'properties': {
'Minimum Density': {
'type': 'number',
'default': 0.3,
'minimum': 0.0,
'maximum': 1.0,
'description': 'Use only glyphs with a density less than the maximum from the dirty layer. Smaller values bring more dirt from the dirty layer but may eventually bring undesired glyphs.'
},
'Despeckle Size': {
'type': 'integer',
'default': 500,
'minimum': 0,
'maximum': 1000,
'description': 'How much post despeckle to apply to the combined output image.'
}
}
}
input_port_types = [{
'name': 'Base Layer',
'resource_types': ['image/rgb+png', 'image/onebit+png', 'image/greyscale+png'],
'minimum': 1,
'maximum': 1,
'is_list': False
},
{
'name': 'Dirty Layer',
'resource_types': ['image/rgb+png', 'image/onebit+png', 'image/greyscale+png'],
'minimum': 1,
'maximum': 1,
'is_list': False
}]
output_port_types = [{
'name': 'Repaired Base Layer',
'resource_types': ['image/onebit+png'],
'minimum': 1,
'maximum': 1,
'is_list': False
}]
def run_my_task(self, inputs, settings, outputs):
base = load_image(inputs['Base Layer'][0]['resource_path'])
dirty = load_image(inputs['Dirty Layer'][0]['resource_path'])
kwargs = {
'despeckle_size': 500,
'density': 0.3,
}
dlr = DirtyLayerRepairman(**kwargs)
image = dlr.run(base, dirty)
outfile_path = outputs['Repaired Base Layer'][0]['resource_path']
image.save_PNG(outfile_path)
return True
def test_my_task(self, testcase):
import cv2
import numpy as np
input_base_path = "/code/Rodan/rodan/test/files/ms73-068_neume.png"
input_dirty_path = "/code/Rodan/rodan/test/files/ms73-068_text.png"
output_path = testcase.new_available_path()
gt_output_path = "/code/Rodan/rodan/test/files/ms73-068_dirty-layer-repair.png"
inputs = {
"Base Layer": [{"resource_path":input_base_path}],
"Dirty Layer": [{"resource_path":input_dirty_path}]
}
outputs = {
"Repaired Base Layer": [{"resource_path":output_path}]
}
settings = {'Minimum Density': 0.3, 'Despeckle Size': 500}
self.run_my_task(inputs=inputs, outputs=outputs, settings=settings)
# The predicted result and gt result should be identical to each other
# The gt result is from running this job on production
gt_output = cv2.imread(gt_output_path, cv2.IMREAD_UNCHANGED)
pred_output = cv2.imread(output_path, cv2.IMREAD_UNCHANGED)
np.testing.assert_array_equal(gt_output, pred_output)