Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Udayraj123
GitHub Repository: Udayraj123/OMRChecker
Path: blob/master/src/processors/CropOnMarkers.py
214 views
1
import os
2
3
import cv2
4
import numpy as np
5
6
from src.logger import logger
7
from src.processors.interfaces.ImagePreprocessor import ImagePreprocessor
8
from src.utils.image import ImageUtils
9
from src.utils.interaction import InteractionUtils
10
11
12
class CropOnMarkers(ImagePreprocessor):
13
def __init__(self, *args, **kwargs):
14
super().__init__(*args, **kwargs)
15
config = self.tuning_config
16
marker_ops = self.options
17
self.threshold_circles = []
18
# img_utils = ImageUtils()
19
20
# options with defaults
21
self.marker_path = os.path.join(
22
self.relative_dir, marker_ops.get("relativePath", "omr_marker.jpg")
23
)
24
self.min_matching_threshold = marker_ops.get("min_matching_threshold", 0.3)
25
self.max_matching_variation = marker_ops.get("max_matching_variation", 0.41)
26
self.marker_rescale_range = tuple(
27
int(r) for r in marker_ops.get("marker_rescale_range", (35, 100))
28
)
29
self.marker_rescale_steps = int(marker_ops.get("marker_rescale_steps", 10))
30
self.apply_erode_subtract = marker_ops.get("apply_erode_subtract", True)
31
self.marker = self.load_marker(marker_ops, config)
32
33
def __str__(self):
34
return self.marker_path
35
36
def exclude_files(self):
37
return [self.marker_path]
38
39
def apply_filter(self, image, file_path):
40
config = self.tuning_config
41
image_instance_ops = self.image_instance_ops
42
image_eroded_sub = ImageUtils.normalize_util(
43
image
44
if self.apply_erode_subtract
45
else (image - cv2.erode(image, kernel=np.ones((5, 5)), iterations=5))
46
)
47
# Quads on warped image
48
quads = {}
49
h1, w1 = image_eroded_sub.shape[:2]
50
midh, midw = h1 // 3, w1 // 2
51
origins = [[0, 0], [midw, 0], [0, midh], [midw, midh]]
52
quads[0] = image_eroded_sub[0:midh, 0:midw]
53
quads[1] = image_eroded_sub[0:midh, midw:w1]
54
quads[2] = image_eroded_sub[midh:h1, 0:midw]
55
quads[3] = image_eroded_sub[midh:h1, midw:w1]
56
57
# Draw Quadlines
58
image_eroded_sub[:, midw : midw + 2] = 255
59
image_eroded_sub[midh : midh + 2, :] = 255
60
61
best_scale, all_max_t = self.getBestMatch(image_eroded_sub)
62
if best_scale is None:
63
if config.outputs.show_image_level >= 1:
64
InteractionUtils.show("Quads", image_eroded_sub, config=config)
65
return None
66
67
optimal_marker = ImageUtils.resize_util_h(
68
self.marker, u_height=int(self.marker.shape[0] * best_scale)
69
)
70
_h, w = optimal_marker.shape[:2]
71
centres = []
72
sum_t, max_t = 0, 0
73
quarter_match_log = "Matching Marker: "
74
for k in range(0, 4):
75
res = cv2.matchTemplate(quads[k], optimal_marker, cv2.TM_CCOEFF_NORMED)
76
max_t = res.max()
77
quarter_match_log += f"Quarter{str(k + 1)}: {str(round(max_t, 3))}\t"
78
if (
79
max_t < self.min_matching_threshold
80
or abs(all_max_t - max_t) >= self.max_matching_variation
81
):
82
logger.error(
83
file_path,
84
"\nError: No circle found in Quad",
85
k + 1,
86
"\n\t min_matching_threshold",
87
self.min_matching_threshold,
88
"\t max_matching_variation",
89
self.max_matching_variation,
90
"\t max_t",
91
max_t,
92
"\t all_max_t",
93
all_max_t,
94
)
95
if config.outputs.show_image_level >= 1:
96
InteractionUtils.show(
97
f"No markers: {file_path}",
98
image_eroded_sub,
99
0,
100
config=config,
101
)
102
InteractionUtils.show(
103
f"res_Q{str(k + 1)} ({str(max_t)})",
104
res,
105
1,
106
config=config,
107
)
108
return None
109
110
pt = np.argwhere(res == max_t)[0]
111
pt = [pt[1], pt[0]]
112
pt[0] += origins[k][0]
113
pt[1] += origins[k][1]
114
# print(">>",pt)
115
image = cv2.rectangle(
116
image, tuple(pt), (pt[0] + w, pt[1] + _h), (150, 150, 150), 2
117
)
118
# display:
119
image_eroded_sub = cv2.rectangle(
120
image_eroded_sub,
121
tuple(pt),
122
(pt[0] + w, pt[1] + _h),
123
(50, 50, 50) if self.apply_erode_subtract else (155, 155, 155),
124
4,
125
)
126
centres.append([pt[0] + w / 2, pt[1] + _h / 2])
127
sum_t += max_t
128
129
logger.info(quarter_match_log)
130
logger.info(f"Optimal Scale: {best_scale}")
131
# analysis data
132
self.threshold_circles.append(sum_t / 4)
133
134
image = ImageUtils.four_point_transform(image, np.array(centres))
135
# appendSaveImg(1,image_eroded_sub)
136
# appendSaveImg(1,image_norm)
137
138
image_instance_ops.append_save_img(2, image_eroded_sub)
139
# Debugging image -
140
# res = cv2.matchTemplate(image_eroded_sub,optimal_marker,cv2.TM_CCOEFF_NORMED)
141
# res[ : , midw:midw+2] = 255
142
# res[ midh:midh+2, : ] = 255
143
# show("Markers Matching",res)
144
if config.outputs.show_image_level >= 2 and config.outputs.show_image_level < 4:
145
image_eroded_sub = ImageUtils.resize_util_h(
146
image_eroded_sub, image.shape[0]
147
)
148
image_eroded_sub[:, -5:] = 0
149
h_stack = np.hstack((image_eroded_sub, image))
150
InteractionUtils.show(
151
f"Warped: {file_path}",
152
ImageUtils.resize_util(
153
h_stack, int(config.dimensions.display_width * 1.6)
154
),
155
0,
156
0,
157
[0, 0],
158
config=config,
159
)
160
# iterations : Tuned to 2.
161
# image_eroded_sub = image_norm - cv2.erode(image_norm, kernel=np.ones((5,5)),iterations=2)
162
return image
163
164
def load_marker(self, marker_ops, config):
165
if not os.path.exists(self.marker_path):
166
logger.error(
167
"Marker not found at path provided in template:",
168
self.marker_path,
169
)
170
exit(31)
171
172
marker = cv2.imread(self.marker_path, cv2.IMREAD_GRAYSCALE)
173
174
if "sheetToMarkerWidthRatio" in marker_ops:
175
marker = ImageUtils.resize_util(
176
marker,
177
config.dimensions.processing_width
178
/ int(marker_ops["sheetToMarkerWidthRatio"]),
179
)
180
marker = cv2.GaussianBlur(marker, (5, 5), 0)
181
marker = cv2.normalize(
182
marker, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX
183
)
184
185
if self.apply_erode_subtract:
186
marker -= cv2.erode(marker, kernel=np.ones((5, 5)), iterations=5)
187
188
return marker
189
190
# Resizing the marker within scaleRange at rate of descent_per_step to
191
# find the best match.
192
def getBestMatch(self, image_eroded_sub):
193
config = self.tuning_config
194
descent_per_step = (
195
self.marker_rescale_range[1] - self.marker_rescale_range[0]
196
) // self.marker_rescale_steps
197
_h, _w = self.marker.shape[:2]
198
res, best_scale = None, None
199
all_max_t = 0
200
201
for r0 in np.arange(
202
self.marker_rescale_range[1],
203
self.marker_rescale_range[0],
204
-1 * descent_per_step,
205
): # reverse order
206
s = float(r0 * 1 / 100)
207
if s == 0.0:
208
continue
209
rescaled_marker = ImageUtils.resize_util_h(
210
self.marker, u_height=int(_h * s)
211
)
212
# res is the black image with white dots
213
res = cv2.matchTemplate(
214
image_eroded_sub, rescaled_marker, cv2.TM_CCOEFF_NORMED
215
)
216
217
max_t = res.max()
218
if all_max_t < max_t:
219
# print('Scale: '+str(s)+', Circle Match: '+str(round(max_t*100,2))+'%')
220
best_scale, all_max_t = s, max_t
221
222
if all_max_t < self.min_matching_threshold:
223
logger.warning(
224
"\tTemplate matching too low! Consider rechecking preProcessors applied before this."
225
)
226
if config.outputs.show_image_level >= 1:
227
InteractionUtils.show("res", res, 1, 0, config=config)
228
229
if best_scale is None:
230
logger.warning(
231
"No matchings for given scaleRange:", self.marker_rescale_range
232
)
233
return best_scale, all_max_t
234
235