diff --git a/README.md b/README.md index abd5648..e22efe2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Automatically grade MCQ exams using optical mark recognition -Autograder allows you to automatically grade MCQ exams. It is written in pure +`autograder` allows you to automatically grade MCQ exams. It is written in pure rust and runs both in the command line and the modern web browsers using wasm. ## Installation @@ -108,23 +108,27 @@ triggering an update in the view. You can always look into the developer console, which has a rather verbose output to what is happening in the background. Afterwards you can download a zip file containing a CSV file with all the -results and conveniently named reports like this ![example report](assets/sample_report.png). +results and conveniently named reports like this: + +![example report](assets/sample_report.png) These image reports were the main motivation to develop this software: using OMR will always be a bit error-prone, especially when students use the bubble sheet in ways that is not intended. -A green circle means that autograder thinks that the selected bubble is the +A green circle means that `autograder` thinks that the selected bubble is the circled one, which according to the key is correct. A red circle shows the -correct answer, meaning that the selected bubble is elsewhere. +correct answer, meaning that the selected bubble is elsewhere. An orange box +indicates that `autograder` was not sure how to understand the choice and that +manual grading is indicated. ### Using autograder from a mobile device -If you only want to use autograder to grade a handful of bubble sheets, you can +If you only want to use `autograder` to grade a handful of bubble sheets, you can do it like this: First, on a device with a large display, navigate to *Create Magic Link*. Here -you can upload a key and a template and autograder generates a _very_ long link. +you can upload a key and a template and `autograder` generates a _very_ long link. This link encodes all the template and key data and can be shared with anyone -- most importantly yourself for usage on a mobile device. Bookmark that very long link with a descriptive name like "Stat101 Test 1" on your mobile device diff --git a/assets/filled_out_example.png b/assets/filled_out_example.png index 8b08d22..5871e34 100644 Binary files a/assets/filled_out_example.png and b/assets/filled_out_example.png differ diff --git a/assets/sample_report.png b/assets/sample_report.png index b956d32..549695a 100644 Binary files a/assets/sample_report.png and b/assets/sample_report.png differ diff --git a/src/image_helpers.rs b/src/image_helpers.rs index abb05a9..5ced9e7 100644 --- a/src/image_helpers.rs +++ b/src/image_helpers.rs @@ -80,6 +80,8 @@ pub fn fax_to_grayimage(data: &[u8], width: u32, height: u32) -> GrayImage { y += 1; }); + // we don't trust binary images and erode them first + imageproc::morphology::erode_mut(&mut result, imageproc::distance_transform::Norm::L1, 1); result } pub fn binary_image_from_image(img: DynamicImage) -> GrayImage { @@ -164,29 +166,23 @@ pub fn rgb_to_egui_color_image(image: &RgbImage) -> egui::ColorImage { } pub fn create_error_image(error_text: &str) -> GrayImage { - // Create a new 400x300 grayscale image - let mut image = GrayImage::new(800, 300); + let mut image = GrayImage::new(1200, 300); - // Fill with light gray background for pixel in image.pixels_mut() { - *pixel = image::Luma([240u8]); + *pixel = image::Luma([255u8]); } - // Load font from binary data embedded in the executable let font_data = crate::typst_helpers::BIOLINUM_BOLD; let font = ab_glyph::FontArc::try_from_slice(font_data).expect("Error loading font"); - // Configure font scale (size) let scale = ab_glyph::PxScale::from(30.0); - // Calculate text position - let x = 20; // Padding from left - let y = 150; // Vertically centered + let x = 20; + let y = 150; - // Draw the error text imageproc::drawing::draw_text_mut( &mut image, - image::Luma([50u8]), // Dark gray text + image::Luma([0u8]), x, y, scale, diff --git a/src/template_scan.rs b/src/template_scan.rs index 1f389aa..69e2266 100644 --- a/src/template_scan.rs +++ b/src/template_scan.rs @@ -238,7 +238,21 @@ impl<'a> TemplateScan<'a> { pub fn set_transformation(&mut self) { let trafo = self.find_transformation(); - self.transformation = trafo; + + if trafo.is_some() { + self.transformation = trafo; + return; + } + + // if this did not work, then in all cases known to us the image had too + // much white noise, so we erode it + imageproc::morphology::erode_mut( + &mut self.scan.image, + imageproc::distance_transform::Norm::L1, + 1, + ); + + self.transformation = self.find_transformation(); } pub fn find_transformation(&self) -> Option {