-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.py
More file actions
299 lines (246 loc) · 11.6 KB
/
api.py
File metadata and controls
299 lines (246 loc) · 11.6 KB
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
from flask import Flask, request, jsonify, send_file
from werkzeug.utils import secure_filename
import os
import json
import cv2
import numpy as np
from omr_detector import OMRDetector
from typing import Dict
app = Flask(__name__)
# Configuration
UPLOAD_FOLDER = 'uploads'
OUTPUT_FOLDER = 'output'
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'}
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(OUTPUT_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# Bengali character to option number mapping
BENGALI_TO_OPTION = {
'ক': 1,
'খ': 2,
'গ': 3,
'ঘ': 4
}
def allowed_file(filename: str) -> bool:
"""Check if file extension is allowed"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def convert_answer_key(bengali_answers: Dict[str, str]) -> Dict[str, int]:
"""Convert Bengali answer key to numeric options"""
numeric_answers = {}
for question_num, bengali_char in bengali_answers.items():
if bengali_char in BENGALI_TO_OPTION:
numeric_answers[question_num] = BENGALI_TO_OPTION[bengali_char]
return numeric_answers
def compare_answers(detected: Dict[str, int], answer_key: Dict[str, int]) -> Dict:
"""Compare detected answers with answer key"""
results = {
'total_questions': len(answer_key),
'attempted': len(detected),
'correct': 0,
'incorrect': 0,
'unattempted': 0,
'details': {}
}
for question_num, correct_option in answer_key.items():
detected_option = detected.get(question_num)
if detected_option is None:
results['unattempted'] += 1
results['details'][question_num] = {
'status': 'unattempted',
'detected': None,
'correct': correct_option
}
elif detected_option == correct_option:
results['correct'] += 1
results['details'][question_num] = {
'status': 'correct',
'detected': detected_option,
'correct': correct_option
}
else:
results['incorrect'] += 1
results['details'][question_num] = {
'status': 'incorrect',
'detected': detected_option,
'correct': correct_option
}
return results
def create_marked_image(detector: OMRDetector, comparison: Dict, output_path: str):
"""Create visualization with correct/incorrect answers marked"""
binary = detector.preprocess_image()
circles = detector.detect_circles(binary)
questions = detector.group_circles_into_grid(circles, options_per_question=4)
output = detector.original.copy()
for question_num, question_options in enumerate(questions, 1):
question_str = str(question_num)
if None in question_options or question_str not in comparison['details']:
continue
detail = comparison['details'][question_str]
status = detail['status']
correct_option = detail['correct']
detected_option = detail.get('detected')
for option_idx, circle in enumerate(question_options):
if circle is None:
continue
x, y, radius = circle
y_full = y + detector.crop_offset_y
option_num = option_idx + 1
is_filled = detector.is_circle_filled(circle)
# Use same smaller radius for all colored circles
mark_radius = int(radius * 0.5)
if status == 'incorrect':
if option_num == detected_option:
# Wrong answer marked - fill with deep red
cv2.circle(output, (x, y_full), mark_radius, (0, 0, 180), -1)
cv2.circle(output, (x, y_full), mark_radius, (0, 0, 130), 2)
if option_num == correct_option:
# Correct option - fill with deep green
cv2.circle(output, (x, y_full), mark_radius, (0, 150, 0), -1)
cv2.circle(output, (x, y_full), mark_radius, (0, 100, 0), 2)
elif status == 'correct' and option_num == correct_option:
# Correct answer marked correctly - fill with green
cv2.circle(output, (x, y_full), mark_radius, (0, 150, 0), -1)
cv2.circle(output, (x, y_full), mark_radius, (0, 100, 0), 2)
elif status == 'unattempted' and option_num == correct_option:
# Unattempted - show correct answer in deep green
cv2.circle(output, (x, y_full), mark_radius, (0, 150, 0), -1)
cv2.circle(output, (x, y_full), mark_radius, (0, 100, 0), 2)
# Draw color legend on top of the image
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale_small = 0.6
font_thickness_small = 1
legend_y = 30
legend_start_x = 50
legend_spacing = 250
# Create a white background rectangle for legend
legend_bg_height = 50
cv2.rectangle(output, (0, 0), (output.shape[1], legend_bg_height), (255, 255, 255), -1)
# Red circle - Wrong answer
cv2.circle(output, (legend_start_x, legend_y), 10, (0, 0, 180), -1)
cv2.circle(output, (legend_start_x, legend_y), 10, (0, 0, 130), 2)
cv2.putText(output, "Wrong Answer", (legend_start_x + 25, legend_y + 5),
font, font_scale_small, (0, 0, 0), font_thickness_small)
# Green circle - Correct answer
cv2.circle(output, (legend_start_x + legend_spacing, legend_y), 10, (0, 150, 0), -1)
cv2.circle(output, (legend_start_x + legend_spacing, legend_y), 10, (0, 100, 0), 2)
cv2.putText(output, "Correct Answer", (legend_start_x + legend_spacing + 25, legend_y + 5),
font, font_scale_small, (0, 0, 0), font_thickness_small)
# Create title bar with only statistics
title_text = f"Correct: {comparison['correct']} | Incorrect: {comparison['incorrect']} | Unattempted: {comparison['unattempted']}"
title_height = 60
title_bg = np.ones((title_height, output.shape[1], 3), dtype=np.uint8) * 255
font_scale = 1.0
font_thickness = 2
(text_width, text_height), baseline = cv2.getTextSize(title_text, font, font_scale, font_thickness)
text_x = (output.shape[1] - text_width) // 2
text_y = (title_height + text_height) // 2
cv2.putText(title_bg, title_text, (text_x, text_y), font, font_scale, (0, 0, 0), font_thickness)
final_output = np.vstack([title_bg, output])
cv2.imwrite(output_path, final_output)
@app.route('/check-omr', methods=['POST'])
def check_omr():
"""API endpoint to check OMR answers"""
try:
if 'image' not in request.files:
return jsonify({'error': 'No image file provided'}), 400
file = request.files['image']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type'}), 400
if 'answer_key' not in request.form:
return jsonify({'error': 'No answer_key provided'}), 400
try:
answer_key_bengali = json.loads(request.form['answer_key'])
except json.JSONDecodeError:
return jsonify({'error': 'Invalid JSON format'}), 400
filename = secure_filename(file.filename)
temp_input = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(temp_input)
# Step 1: Detect header information (serial, roll, class, subject_code, set_code)
header_detector = OMRDetector(temp_input, crop_top_range=(20, 40))
header_info = header_detector.detect_header_info()
# Step 2: Detect and check answers from bottom section
answer_key_numeric = convert_answer_key(answer_key_bengali)
detector = OMRDetector(temp_input, crop_bottom_percent=60)
detected_answers = detector.detect_answers(radius_filter=(18, 36))
comparison = compare_answers(detected_answers, answer_key_numeric)
base_name = os.path.splitext(filename)[0]
output_image_path = os.path.join(OUTPUT_FOLDER, f"{base_name}_marked.jpg")
create_marked_image(detector, comparison, output_image_path)
response_data = {
'success': True,
'header': header_info,
'results': {
'total_questions': comparison['total_questions'],
'correct': comparison['correct'],
'incorrect': comparison['incorrect'],
'unattempted': comparison['unattempted'],
'attempted': comparison['attempted'],
'score_percentage': round((comparison['correct'] / comparison['total_questions']) * 100, 2) if comparison['total_questions'] > 0 else 0
},
'details': comparison['details'],
'output_image': output_image_path
}
if os.path.exists(temp_input):
os.remove(temp_input)
return jsonify(response_data), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/get-marked-image/<filename>', methods=['GET'])
def get_marked_image(filename: str):
"""Retrieve marked image"""
try:
file_path = os.path.join(OUTPUT_FOLDER, filename)
if os.path.exists(file_path):
return send_file(file_path, mimetype='image/jpeg')
return jsonify({'error': 'File not found'}), 404
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/detect-omr', methods=['POST'])
def detect_omr():
"""
API endpoint to detect all OMR information including header data and answers
Returns header info (serial, roll, class, subject_code, set_code) and detected answers
"""
try:
if 'image' not in request.files:
return jsonify({'error': 'No image file provided'}), 400
file = request.files['image']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type'}), 400
filename = secure_filename(file.filename)
temp_input = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(temp_input)
# Step 1: Detect header information (serial, roll, class, subject_code, set_code)
# Use crop_top_range (20%, 40%) for header section
header_detector = OMRDetector(temp_input, crop_top_range=(20, 40))
header_info = header_detector.detect_header_info()
# Step 2: Detect answers from bottom section
answer_detector = OMRDetector(temp_input, crop_bottom_percent=60)
detected_answers = answer_detector.detect_answers(radius_filter=(18, 36))
# Prepare response
response_data = {
'success': True,
'header': header_info,
'answers': detected_answers,
'metadata': {
'total_answers_detected': len(detected_answers),
'filename': filename
}
}
# Clean up temp file
if os.path.exists(temp_input):
os.remove(temp_input)
return jsonify(response_data), 200
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/health', methods=['GET'])
def health():
"""Health check"""
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)