Skip to content

Commit 783f94a

Browse files
committed
1049972 : Optical Mark Recognition (OMR) engine for detecting and extracting using Syncfusion
1 parent 6be86d2 commit 783f94a

21 files changed

Lines changed: 1824 additions & 0 deletions

OMR/.NET Core/OMR/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
bin/
2+
obj/
3+
.vs/
4+
.idea/
5+
*.user
6+
*.suo
7+
TestResults/
8+
artifacts/
9+
poc-output/
10+
*.nupkg
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project>
2+
<PropertyGroup>
3+
<TargetFramework>net9.0</TargetFramework>
4+
<Nullable>enable</Nullable>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<LangVersion>latest</LangVersion>
7+
<AnalysisLevel>latest</AnalysisLevel>
8+
</PropertyGroup>
9+
</Project>

OMR/.NET Core/OMR/LICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <https://unlicense.org>

OMR/.NET Core/OMR/Omr.sln

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Omr.Pdf", "src\Omr.Pdf\Omr.Pdf.csproj", "{F5EFFC8A-9899-4405-AF55-C8538FBD5024}"
9+
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Omr.Poc", "src\Omr.Poc\Omr.Poc.csproj", "{67082E98-A89E-4539-A229-57A8A21F42C6}"
11+
EndProject
12+
Global
13+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
14+
Debug|Any CPU = Debug|Any CPU
15+
Debug|x64 = Debug|x64
16+
Debug|x86 = Debug|x86
17+
Release|Any CPU = Release|Any CPU
18+
Release|x64 = Release|x64
19+
Release|x86 = Release|x86
20+
EndGlobalSection
21+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
22+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Debug|x64.ActiveCfg = Debug|Any CPU
25+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Debug|x64.Build.0 = Debug|Any CPU
26+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Debug|x86.ActiveCfg = Debug|Any CPU
27+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Debug|x86.Build.0 = Debug|Any CPU
28+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Release|Any CPU.Build.0 = Release|Any CPU
30+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Release|x64.ActiveCfg = Release|Any CPU
31+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Release|x64.Build.0 = Release|Any CPU
32+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Release|x86.ActiveCfg = Release|Any CPU
33+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024}.Release|x86.Build.0 = Release|Any CPU
34+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Debug|x64.ActiveCfg = Debug|Any CPU
37+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Debug|x64.Build.0 = Debug|Any CPU
38+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Debug|x86.ActiveCfg = Debug|Any CPU
39+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Debug|x86.Build.0 = Debug|Any CPU
40+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Release|x64.ActiveCfg = Release|Any CPU
43+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Release|x64.Build.0 = Release|Any CPU
44+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Release|x86.ActiveCfg = Release|Any CPU
45+
{67082E98-A89E-4539-A229-57A8A21F42C6}.Release|x86.Build.0 = Release|Any CPU
46+
EndGlobalSection
47+
GlobalSection(SolutionProperties) = preSolution
48+
HideSolutionNode = FALSE
49+
EndGlobalSection
50+
GlobalSection(NestedProjects) = preSolution
51+
{F5EFFC8A-9899-4405-AF55-C8538FBD5024} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
52+
{67082E98-A89E-4539-A229-57A8A21F42C6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
53+
EndGlobalSection
54+
EndGlobal

OMR/.NET Core/OMR/README.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# OMR Sample
2+
3+
Template-based Optical Mark Recognition (OMR) sample package for **.NET 9**. The library reads filled bubbles on a scanned answer sheet (PNG/JPEG or a scanned PDF page), aligns the page to a JSON template, and returns structured results.
4+
5+
## What you get
6+
7+
| Project / Folder | Use it for |
8+
| --- | --- |
9+
| [lib](lib) | Pre-compiled Release assemblies for `Omr.Engine` |
10+
| [src/Omr.Pdf](src/Omr.Pdf) | Run the engine over a multi-page **scanned** PDF |
11+
| [src/Omr.Poc](src/Omr.Poc) | End-to-end console sample: generate a PDF, decode QR, grade, annotate, write CSV |
12+
| [samples/templates/exam-cs101-v3.json](samples/templates/exam-cs101-v3.json) | Example exam template |
13+
14+
The engine does **not** grade papers, send email, or require WinForms. The POC demonstrates those as replaceable callbacks.
15+
16+
## Requirements
17+
18+
- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
19+
- Network access to restore NuGet packages (SkiaSharp, Syncfusion PDF, ZXing for the POC)
20+
- Optional: `SYNCFUSION_LICENSE` — without a key, Syncfusion may watermark generated PDFs
21+
22+
Linux CI/dev images already use `SkiaSharp.NativeAssets.Linux.NoDependencies`.
23+
24+
## Build
25+
26+
```bash
27+
dotnet build Omr.sln
28+
```
29+
30+
## Run the sample (POC)
31+
32+
The console app generates a synthetic multi-page exam PDF, recognizes each page, applies a sample grading formula, and writes outputs to a folder.
33+
34+
```bash
35+
dotnet run --project src/Omr.Poc -- --pages 4 --out ./poc-output
36+
```
37+
38+
| Argument | Default | Meaning |
39+
| --- | --- | --- |
40+
| `--pages` | `3` | Number of answer sheets to generate and process |
41+
| `--out` | `./poc-output` | Output directory |
42+
43+
**Outputs**
44+
45+
| File | Contents |
46+
| --- | --- |
47+
| `exam-batch.pdf` | Generated input batch |
48+
| `page-NNN.json` | Recognition result for that page |
49+
| `page-NNN.png` | Annotated correction image |
50+
| `annotated.pdf` | All annotated pages |
51+
| `summary.csv` | Student id, grade, answers, review flag, paths |
52+
53+
QR payloads in the sample look like `exam-cs101|v3|student-001`. Ctrl+C cancels a run.
54+
55+
Page variants in the sample: all correct, one wrong answer, one blank, one double-mark (needs review).
56+
57+
## Use the engine on an image
58+
59+
Reference `lib/Omr.Engine.dll` from your app.
60+
61+
```csharp
62+
using Omr.Engine;
63+
using Omr.Engine.Templates;
64+
65+
OmrTemplate template = OmrTemplate.Load("exam-cs101-v3.json");
66+
template.EnsureValid();
67+
68+
var options = new OmrRecognitionOptions
69+
{
70+
FilledThreshold = 0.70f,
71+
BlankThreshold = 0.20f,
72+
AutoRotate = true,
73+
Deskew = true,
74+
MinAlignmentScore = 0.35f
75+
};
76+
77+
using var processor = new OmrProcessor(options);
78+
Omr.Engine.Results.OmrPageResult result = processor.Recognize("scan.png", template);
79+
80+
Console.WriteLine($"{result.PageStatus} align={result.AlignmentScore:0.00}");
81+
82+
foreach (var group in result.Groups)
83+
{
84+
Console.WriteLine($"{group.Id}: {group.Status} [{string.Join(",", group.SelectedOptionIds)}]");
85+
}
86+
87+
File.WriteAllText("page.json", result.ToJson());
88+
```
89+
90+
`Recognize` also accepts a `Stream` or a SkiaSharp `SKBitmap`. Pass `sourcePageIndex` when the image came from a PDF page.
91+
92+
### Recognition options
93+
94+
| Option | Default | Role |
95+
| --- | --- | --- |
96+
| `FilledThreshold` | `0.70` | Fill score at or above this counts as filled |
97+
| `BlankThreshold` | `0.20` | Fill score at or below this counts as empty |
98+
| `AutoRotate` | `true` | Try 0° / 90° / 180° / 270° using anchors |
99+
| `Deskew` | `true` | Small-angle deskew |
100+
| `MinAlignmentScore` | `0.35` | Below this, the page fails (no silent answers) |
101+
| `TreatMultipleAsReview` | `true` | `Multiple` groups set page `NeedsReview` |
102+
| `TreatAmbiguousAsReview` | `true` | `Ambiguous` groups set page `NeedsReview` |
103+
| `MinEffectiveDpi` | `150` | Warn/fail when estimated DPI is too low |
104+
105+
Require `0 ≤ BlankThreshold < FilledThreshold ≤ 1`.
106+
107+
### Result statuses
108+
109+
**Page:** `Succeeded`, `NeedsReview`, `Failed`, `Skipped`
110+
111+
**Group:** `Selected`, `Blank`, `Multiple`, `Ambiguous`, `Unreadable`
112+
113+
The engine never picks a winner when two bubbles are filled or a mark is in the ambiguous band. Light/erased marks show as `Partial` or `ErasureSuspect` on the option, and the group becomes `Ambiguous` (not a silent `Selected`).
114+
115+
## Use the PDF adapter
116+
117+
Reference `Omr.Engine` and `Omr.Pdf`. PDFs must be **scanned sheets** (each page contains an embedded raster). Vector-only pages cannot be rasterized in this build.
118+
119+
```csharp
120+
using Omr.Engine;
121+
using Omr.Engine.Templates;
122+
using Omr.Pdf;
123+
124+
OmrTemplate template = OmrTemplate.Load("exam-cs101-v3.json");
125+
126+
using var engine = new OmrProcessor();
127+
using var pdf = new PdfOmrProcessor(engine);
128+
129+
await foreach (var page in pdf.RecognizeAsync(
130+
"exams.pdf",
131+
templateResolver: new MyTemplateResolver(template),
132+
barcodeDecoder: new MyQrDecoder(), // optional; implement IBarcodeDecoder
133+
options: new PdfOmrOptions
134+
{
135+
RequireQr = true,
136+
FallbackTemplate = template,
137+
StartPageIndex = 0,
138+
FailFast = false
139+
}))
140+
{
141+
// Grade and store using page.Groups — not the engine
142+
}
143+
```
144+
145+
| `PdfOmrOptions` | Default | Role |
146+
| --- | --- | --- |
147+
| `RequireQr` | `true` | Fail the page if QR decode returns nothing |
148+
| `FallbackTemplate` | `null` | Template when the resolver returns null |
149+
| `StartPageIndex` / `EndPageIndexInclusive` | all pages | Page range (0-based) |
150+
| `FailFast` | `false` | Stop the batch on the first failed page |
151+
152+
Implement `ITemplateResolver.Resolve(qrValue)` to map a QR string to a template. The sample resolver (`CatalogTemplateResolver`) understands `templateId|version|studentId`.
153+
154+
Implement `IBarcodeDecoder` yourself (the POC uses ZXing). The engine does not depend on a barcode library.
155+
156+
## Define a template
157+
158+
Coordinates are **normalized** `[0, 1]` with origin at the **top-left**, Y down. Use at least **three** `anchor` regions (four corners work well); make one fiducial larger so 180° rotation can be distinguished.
159+
160+
```json
161+
{
162+
"schemaVersion": "1.0",
163+
"templateId": "exam-cs101",
164+
"templateVersion": "3",
165+
"page": { "width": 8.5, "height": 11.0, "unit": "inch", "orientation": "portrait" },
166+
"regions": [
167+
{ "id": "tl-anchor", "kind": "anchor", "rect": { "x": 0.035, "y": 0.032, "w": 0.055, "h": 0.042 } },
168+
{ "id": "qr", "kind": "barcode", "rect": { "x": 0.70, "y": 0.035, "w": 0.22, "h": 0.12 } }
169+
],
170+
"groups": [
171+
{
172+
"id": "q1",
173+
"groupKind": "answer",
174+
"linkedGroupId": "q1-confidence",
175+
"selectionPolicy": "single",
176+
"marks": [
177+
{ "id": "A", "shape": "oval", "rect": { "x": 0.16, "y": 0.24, "w": 0.045, "h": 0.028 } }
178+
]
179+
}
180+
]
181+
}
182+
```
183+
184+
- `kind`: `anchor` | `barcode` | `mark` (marks usually live under `groups`)
185+
- `selectionPolicy`: `single` or `multiple`
186+
- `groupKind`: `answer` | `confidence` | `other` (hints only; scoring is the same)
187+
- `shape`: `oval` | `circle` | `square` | `rectangle`
188+
189+
Load and check before a batch: `OmrTemplate.Load(path)` then `EnsureValid()` / `Validate()`.
190+
191+
Print registration marks on the physical sheet in the same places as the `anchor` rects.
192+
193+
## Plug in grading and delivery (POC)
194+
195+
The POC keeps grading out of the engine:
196+
197+
- `IExamGrader` / `WeightedConfidenceGrader` — example: correct + High=1.0, Medium=0.5, Low=0.25; blank/multiple/ambiguous score 0 and flag review
198+
- `IResultDelivery` / `FolderDelivery` — writes `summary.csv` (swap this for email or a test mailbox)
199+
- `GradeAnnotator` — draws OK / X / ? on a copy of the page
200+
201+
Copy those types into your app or replace them.
202+
203+
## Scan quality
204+
205+
Use clean 150–300 DPI scans, full page visible, dark filled bubbles, and printed corner anchors. Crops that clip anchors, extreme skew, or faint marks produce `NeedsReview` or `Failed` with a reason instead of a guessed answer.
206+
207+
## Limits
208+
209+
- No handwriting / OCR
210+
- No exam layout generator
211+
- No production email or student directory
212+
- Scanned PDF pages must contain an embedded image; Pdfium page rendering is not included (it crashed on some Linux hosts)
213+
- Console POC only (not a WinForms UI)
214+
215+
## License
216+
217+
This repository is under the [Unlicense](LICENSE). Syncfusion and other NuGet dependencies have their own licenses.

0 commit comments

Comments
 (0)