diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b6579851..c692bd8f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,24 +1,52 @@
+exclude: "__snapshots__/.*$"
+default_install_hook_types: [pre-commit, pre-push]
repos:
-- repo: https://github.com/pre-commit/pre-commit-hooks
+ - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- - id: check-yaml
- - id: end-of-file-fixer
+ - id: check-yaml
+ stages: [commit]
+ - id: end-of-file-fixer
exclude_types: ["csv", "json"]
- - id: trailing-whitespace
-- repo: https://github.com/pycqa/isort
+ stages: [commit]
+ - id: trailing-whitespace
+ stages: [commit]
+ - repo: https://github.com/pycqa/isort
rev: 5.5.4
hooks:
- - id: isort
+ - id: isort
args: ["--profile", "black"]
-- repo: https://github.com/psf/black
+ stages: [commit]
+ - repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- - id: black
-- repo: https://github.com/pycqa/flake8
+ - id: black
+ stages: [commit]
+ - repo: https://github.com/pycqa/flake8
rev: 3.7.9
hooks:
- - id: flake8
+ - id: flake8
args:
- - "F401,F841" # unused imports, unused variables
- - "--ignore=E501,W503,E203,E741" # Line too long, Line break occurred before a binary operator, Whitespace before ':'
+ - "F401,F841" # unused imports, unused variables
+ - "--ignore=E501,W503,E203,E741" # Line too long, Line break occurred before a binary operator, Whitespace before ':'
+ stages: [commit]
+ - repo: local
+ hooks:
+ - id: pytest-on-commit
+ name: Running single sample test
+ entry: pytest -k sample1
+ language: system
+ pass_filenames: false
+ always_run: true
+ fail_fast: true
+ stages: [commit]
+ - repo: local
+ hooks:
+ - id: pytest-on-push
+ name: Running all tests before push...
+ entry: pytest
+ language: system
+ pass_filenames: false
+ always_run: true
+ fail_fast: true
+ stages: [push]
diff --git a/README.md b/README.md
index 0121de5d..b4b2ce1f 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Read OMRs fast and accurately using a scanner 🖨 or your phone 🤳.
-#### **Quick Links**
+#### **Quick Links**
- [Installation](#getting-started)
- [User Guide](https://github.com/Udayraj123/OMRChecker/wiki)
- [Contributor Guide](https://github.com/Udayraj123/OMRChecker/blob/master/CONTRIBUTING.md)
@@ -104,7 +104,7 @@ Get a CSV sheet containing the detected responses and evaluated scores:
### 1. Install global dependencies
-![opencv 4.0.0](https://img.shields.io/badge/opencv-4.0.0-blue.svg) ![python 3.4+](https://img.shields.io/badge/python-3.4+-blue.svg)
+![opencv 4.0.0](https://img.shields.io/badge/opencv-4.0.0-blue.svg) ![python 3.5+](https://img.shields.io/badge/python-3.5+-blue.svg)
To check if python3 and pip is already installed:
@@ -215,7 +215,7 @@ Command: python3 -m pip install --user --upgrade pip
1. First, [create your own template.json](https://github.com/Udayraj123/OMRChecker/wiki/User-Guide).
2. Configure the tuning parameters.
3. Run OMRChecker with appropriate arguments (See full usage).
-
+
## Full Usage
@@ -231,10 +231,14 @@ Explanation for the arguments:
`--outputDir`: Specify an output directory.
-**Notes:**
+
+
+ Deprecation logs
+
- The old `--noCropping` flag has been replaced with the 'CropPage' plugin in "preProcessors" of the template.json(see [samples](https://github.com/Udayraj123/OMRChecker/tree/master/samples)).
- The old `--autoAlign` flag can now be toggled from config.json
+
+
## FAQ
@@ -314,7 +319,7 @@ Here's a snapshot of the [Android OMR Helper App (archived)](https://github.com/
-
+
@@ -340,7 +345,7 @@ For more details see [LICENSE](https://github.com/Udayraj123/OMRChecker/blob/mas
[![paypal](https://www.paypalobjects.com/en_GB/i/btn/btn_donate_LG.gif)](https://www.paypal.me/Udayraj123/500)
-_Find OMRChecker on_ [**_Product Hunt_**](https://www.producthunt.com/posts/omr-checker/) **|** [**_Reddit_**](https://www.reddit.com/r/computervision/comments/ccbj6f/omrchecker_grade_exams_using_python_and_opencv/) **|** [**Discord**](https://discord.gg/qFv2Vqf) **|** [**Linkedin**](https://www.linkedin.com/pulse/open-source-talks-udayraj-udayraj-deshmukh/) **|** [**goodfirstissue.dev**](https://goodfirstissue.dev/language/python) **|** [**codepeak.tech**](https://www.codepeak.tech/) **|** [**fossoverflow.dev**](https://fossoverflow.dev/projects) **|** [**Interview on Console by CodeSee**](https://console.substack.com/p/console-140) **|** [**Open Source Hub**](https://opensourcehub.io/udayraj123/omrchecker)
+_Find OMRChecker on_ [**_Product Hunt_**](https://www.producthunt.com/posts/omr-checker/) **|** [**_Reddit_**](https://www.reddit.com/r/computervision/comments/ccbj6f/omrchecker_grade_exams_using_python_and_opencv/) **|** [**Discord**](https://discord.gg/qFv2Vqf) **|** [**Linkedin**](https://www.linkedin.com/pulse/open-source-talks-udayraj-udayraj-deshmukh/) **|** [**goodfirstissue.dev**](https://goodfirstissue.dev/language/python) **|** [**codepeak.tech**](https://www.codepeak.tech/) **|** [**fossoverflow.dev**](https://fossoverflow.dev/projects) **|** [**Interview on Console by CodeSee**](https://console.substack.com/p/console-140) **|** [**Open Source Hub**](https://opensourcehub.io/udayraj123/omrchecker)
diff --git a/main.py b/main.py
index 70f113fa..fafaf813 100644
--- a/main.py
+++ b/main.py
@@ -13,56 +13,74 @@
from src.entry import entry_point
from src.logger import logger
-# construct the argument parse and parse the arguments
-argparser = argparse.ArgumentParser()
-
-argparser.add_argument(
- "-i",
- "--inputDir",
- default=["inputs"],
- # https://docs.python.org/3/library/argparse.html#nargs
- nargs="*",
- required=False,
- type=str,
- dest="input_paths",
- help="Specify an input directory.",
-)
-
-argparser.add_argument(
- "-o",
- "--outputDir",
- default="outputs",
- required=False,
- dest="output_dir",
- help="Specify an output directory.",
-)
-
-argparser.add_argument(
- "-l",
- "--setLayout",
- required=False,
- dest="setLayout",
- action="store_true",
- help="Set up OMR template layout - modify your json file and \
- run again until the template is set.",
-)
-
-
-(
- args,
- unknown,
-) = argparser.parse_known_args()
-args = vars(args)
-
-# FIX: remove join
-if len(unknown) > 0:
- logger.warning("".join(["\nError: Unknown arguments: ", unknown]))
- argparser.print_help()
- exit(11)
-
-for root in args["input_paths"]:
- entry_point(
- Path(root),
- Path(root),
- args,
+
+def parse_args():
+ # construct the argument parse and parse the arguments
+ argparser = argparse.ArgumentParser()
+
+ argparser.add_argument(
+ "-i",
+ "--inputDir",
+ default=["inputs"],
+ # https://docs.python.org/3/library/argparse.html#nargs
+ nargs="*",
+ required=False,
+ type=str,
+ dest="input_paths",
+ help="Specify an input directory.",
+ )
+
+ argparser.add_argument(
+ "-o",
+ "--outputDir",
+ default="outputs",
+ required=False,
+ dest="output_dir",
+ help="Specify an output directory.",
+ )
+
+ argparser.add_argument(
+ "-a",
+ "--autoAlign",
+ required=False,
+ dest="autoAlign",
+ action="store_true",
+ help="(experimental) Enables automatic template alignment - \
+ use if the scans show slight misalignments.",
+ )
+
+ argparser.add_argument(
+ "-l",
+ "--setLayout",
+ required=False,
+ dest="setLayout",
+ action="store_true",
+ help="Set up OMR template layout - modify your json file and \
+ run again until the template is set.",
)
+
+ (
+ args,
+ unknown,
+ ) = argparser.parse_known_args()
+
+ args = vars(args)
+
+ if len(unknown) > 0:
+ logger.warning(f"\nError: Unknown arguments: {unknown}", unknown)
+ argparser.print_help()
+ exit(11)
+ return args
+
+
+def entry_point_for_args(args):
+ for root in args["input_paths"]:
+ entry_point(
+ Path(root),
+ args,
+ )
+
+
+if __name__ == "__main__":
+ args = parse_args()
+ entry_point_for_args(args)
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..84008a21
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,6 @@
+# pytest.ini
+[pytest]
+minversion = 7.0
+addopts = -qq --capture=no
+testpaths =
+ src/tests
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 4f184f5a..2756b677 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -1,2 +1,6 @@
-r requirements.txt
-pre-commit
+pytest>=7.1.3
+pytest-mock>=3.10.0
+syrupy>=3.0.6
+freezegun>=1.2.2
+pre-commit>=2.20.0
diff --git a/samples/community/Antibodyy/template.json b/samples/community/Antibodyy/template.json
index 11abfbd3..bf4bd2c0 100644
--- a/samples/community/Antibodyy/template.json
+++ b/samples/community/Antibodyy/template.json
@@ -1,19 +1,13 @@
{
- "dimensions": [ 299, 398 ],
+ "pageDimensions": [ 299, 398 ],
"bubbleDimensions": [ 42, 42 ],
- "concatenations": {},
- "singles": [ "q1", "q2", "q3", "q4", "q5", "q6" ],
- "qBlocks": {
+ "fieldBlocks": {
"MCQBlock1": {
- "qType": "QTYPE_MCQ5",
- "orig": [ 65, 79 ],
- "qNos": [
- [
- [ "q1", "q2", "q3", "q4", "q5", "q6" ]
- ]
- ],
- "gaps": [ 43, 50 ],
- "bigGaps": [ 30, 30 ]
+ "fieldType": "QTYPE_MCQ5",
+ "origin": [65, 79],
+ "bubblesGap":43,
+ "labelsGap": 50,
+ "fieldLabels": ["q1..6"]
}
},
"preProcessors": [
@@ -24,4 +18,4 @@
}
}
]
-}
+}
diff --git a/samples/community/Sandeep-1507/template.json b/samples/community/Sandeep-1507/template.json
index 34bc45d1..e73a9ce4 100644
--- a/samples/community/Sandeep-1507/template.json
+++ b/samples/community/Sandeep-1507/template.json
@@ -1,810 +1,138 @@
{
- "Globals": {
- "display_width": 1189,
- "display_height": 1682
- },
- "dimensions": [
- 1189,
- 1682
- ],
- "bubbleDimensions": [
- 15,
- 15
- ],
+ "pageDimensions": [1189, 1682],
+ "bubbleDimensions": [15, 15],
"preProcessors": [
{
"name": "GaussianBlur",
"options": {
- "kSize": [
- 3,
- 3
- ],
+ "kSize": [3, 3],
"sigmaX": 0
}
}
],
- "concatenations": {
- "Booklet_no": [
- "b1",
- "b2",
- "b3",
- "b4",
- "b5",
- "b6",
- "b7"
- ]
+ "customLabels": {
+ "Booklet_No": ["b1..7"]
},
- "singles": [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20",
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34",
- "q35",
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50",
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60",
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67",
- "q68",
- "q69",
- "q70",
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84",
- "q85",
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100",
- "q101",
- "q102",
- "q103",
- "q104",
- "q105",
- "q106",
- "q107",
- "q108",
- "q109",
- "q110",
- "q111",
- "q112",
- "q113",
- "q114",
- "q115",
- "q116",
- "q117",
- "q118",
- "q119",
- "q120",
- "q121",
- "q122",
- "q123",
- "q124",
- "q125",
- "q126",
- "q127",
- "q128",
- "q129",
- "q130",
- "q131",
- "q132",
- "q133",
- "q134",
- "q135",
- "q136",
- "q137",
- "q138",
- "q139",
- "q140",
- "q141",
- "q142",
- "q143",
- "q144",
- "q145",
- "q146",
- "q147",
- "q148",
- "q149",
- "q150",
- "q151",
- "q152",
- "q153",
- "q154",
- "q155",
- "q156",
- "q157",
- "q158",
- "q159",
- "q160",
- "q161",
- "q162",
- "q163",
- "q164",
- "q165",
- "q166",
- "q167",
- "q168",
- "q169",
- "q170",
- "q171",
- "q172",
- "q173",
- "q174",
- "q175",
- "q176",
- "q177",
- "q178",
- "q179",
- "q180",
- "q181",
- "q182",
- "q183",
- "q184",
- "q185",
- "q186",
- "q187",
- "q188",
- "q189",
- "q190",
- "q191",
- "q192",
- "q193",
- "q194",
- "q195",
- "q196",
- "q197",
- "q198",
- "q199",
- "q200"
- ],
- "qBlocks": {
- "Booklet_no": {
- "qType": "QTYPE_ROLL",
- "orig": [
- 112,
- 530
- ],
- "qNos": [
- [
- [
- "b1",
- "b2",
- "b3",
- "b4",
- "b5",
- "b6",
- "b7"
- ]
- ]
- ],
- "gaps": [
- 28,
- 26.5
- ],
- "bigGaps": [
- 10,
- 20
- ]
+ "fieldBlocks": {
+ "Booklet_No": {
+ "fieldType": "QTYPE_INT",
+ "origin": [112, 530],
+ "fieldLabels": ["b1..7"],
+ "emptyValue": "no",
+ "bubblesGap": 28,
+ "labelsGap": 26.5
},
"MCQBlock1a1": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 476,
- 100
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q1..10"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [476, 100]
},
"MCQBlock1a2": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 476,
- 370
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q11..20"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [476, 370]
},
"MCQBlock1a3": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 476,
- 638
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
-
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34",
- "q35"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q21..35"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [476, 638]
},
"MCQBlock2a1": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 645,
- 100
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60"
-
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q51..60"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [645, 100]
},
"MCQBlock2a2": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 645,
- 370
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67",
- "q68",
- "q69",
- "q70"
-
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q61..70"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [645, 370]
},
"MCQBlock2a3": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 645,
- 638
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84",
- "q85"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q71..85"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [645, 638]
},
"MCQBlock3a1": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 815,
- 100
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q101",
- "q102",
- "q103",
- "q104",
- "q105",
- "q106",
- "q107",
- "q108",
- "q109",
- "q110"
-
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q101..110"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [815, 100]
},
"MCQBlock3a2": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 815,
- 370
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q111",
- "q112",
- "q113",
- "q114",
- "q115",
- "q116",
- "q117",
- "q118",
- "q119",
- "q120"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q111..120"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [815, 370]
},
"MCQBlock3a3": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 815,
- 638
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q121",
- "q122",
- "q123",
- "q124",
- "q125",
- "q126",
- "q127",
- "q128",
- "q129",
- "q130",
- "q131",
- "q132",
- "q133",
- "q134",
- "q135"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q121..135"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [815, 638]
},
"MCQBlock4a1": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 983,
- 100
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q151",
- "q152",
- "q153",
- "q154",
- "q155",
- "q156",
- "q157",
- "q158",
- "q159",
- "q160"
-
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q151..160"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [983, 100]
},
"MCQBlock4a2": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 983,
- 370
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q161",
- "q162",
- "q163",
- "q164",
- "q165",
- "q166",
- "q167",
- "q168",
- "q169",
- "q170"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q161..170"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [983, 370]
},
"MCQBlock4a3": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 983,
- 638
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ],
- "qNos": [
- [
- [
- "q171",
- "q172",
- "q173",
- "q174",
- "q175",
- "q176",
- "q177",
- "q178",
- "q179",
- "q180",
- "q181",
- "q182",
- "q183",
- "q184",
- "q185"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q171..185"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [983, 638]
},
"MCQBlock1a": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 480,
- 1061
- ],
- "qNos": [
- [
- [
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50"
- ]
- ]
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q36..50"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [480, 1061]
},
"MCQBlock2a": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 648,
- 1061
- ],
- "qNos": [
- [
- [
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ]
- ]
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q86..100"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [648, 1061]
},
"MCQBlock3a": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 815,
- 1061
- ],
- "qNos": [
- [
- [
- "q136",
- "q137",
- "q138",
- "q139",
- "q140",
- "q141",
- "q142",
- "q143",
- "q144",
- "q145",
- "q146",
- "q147",
- "q148",
- "q149",
- "q150"
- ]
- ]
- ],
- "gaps": [
- 28.7,
- 26.7
- ],
- "bigGaps": [
- 10,
- 20
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q136..150"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.7,
+ "origin": [815, 1061]
},
"MCQBlock4a": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 986,
- 1061
- ],
- "qNos": [
- [
- [
- "q186",
- "q187",
- "q188",
- "q189",
- "q190",
- "q191",
- "q192",
- "q193",
- "q194",
- "q195",
- "q196",
- "q197",
- "q198",
- "q199",
- "q200"
- ]
- ]
- ],
- "gaps": [
- 28.7,
- 26.6
- ],
- "bigGaps": [
- 10,
- 20
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q186..200"],
+ "bubblesGap": 28.7,
+ "labelsGap": 26.6,
+ "origin": [986, 1061]
}
}
}
diff --git a/samples/community/Shamanth/template.json b/samples/community/Shamanth/template.json
index 6bb41409..321a3c0b 100644
--- a/samples/community/Shamanth/template.json
+++ b/samples/community/Shamanth/template.json
@@ -1,19 +1,13 @@
{
- "dimensions": [ 300, 400 ],
+ "pageDimensions": [ 300, 400 ],
"bubbleDimensions": [ 20, 20 ],
- "concatenations": {},
- "singles": ["q21", "q22", "q23", "q24", "q25" , "q26", "q27", "q28"],
- "qBlocks": {
+ "fieldBlocks": {
"MCQBlock1": {
- "qType": "QTYPE_MCQ4",
- "orig": [ 78, 41 ],
- "qNos": [
- [
- ["q21", "q22", "q23", "q24", "q25" , "q26", "q27", "q28"]
- ]
- ],
- "gaps": [ 56, 46 ],
- "bigGaps": [ 40, 40 ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [ 78, 41 ],
+ "fieldLabels": ["q21..28"],
+ "bubblesGap": 56,
+ "labelsGap": 46
}
},
"preProcessors": [
diff --git a/samples/community/UPSC-mock/answer_key.jpg b/samples/community/UPSC-mock/answer_key.jpg
index 3e0e3efa..7a229c1b 100644
Binary files a/samples/community/UPSC-mock/answer_key.jpg and b/samples/community/UPSC-mock/answer_key.jpg differ
diff --git a/samples/community/UPSC-mock/scan-angles/angle-1.jpg b/samples/community/UPSC-mock/scan-angles/angle-1.jpg
index 9fd88262..9c75d7be 100644
Binary files a/samples/community/UPSC-mock/scan-angles/angle-1.jpg and b/samples/community/UPSC-mock/scan-angles/angle-1.jpg differ
diff --git a/samples/community/UPSC-mock/scan-angles/angle-2.jpg b/samples/community/UPSC-mock/scan-angles/angle-2.jpg
index f755c973..edab6060 100644
Binary files a/samples/community/UPSC-mock/scan-angles/angle-2.jpg and b/samples/community/UPSC-mock/scan-angles/angle-2.jpg differ
diff --git a/samples/community/UPSC-mock/scan-angles/angle-3.jpg b/samples/community/UPSC-mock/scan-angles/angle-3.jpg
index 11ced893..f1da4af9 100644
Binary files a/samples/community/UPSC-mock/scan-angles/angle-3.jpg and b/samples/community/UPSC-mock/scan-angles/angle-3.jpg differ
diff --git a/samples/community/UPSC-mock/template.json b/samples/community/UPSC-mock/template.json
index 289ac698..eba9dfb9 100644
--- a/samples/community/UPSC-mock/template.json
+++ b/samples/community/UPSC-mock/template.json
@@ -1,264 +1,103 @@
{
- "dimensions": [1800, 2400],
- "bubbleDimensions": [23, 20],
- "concatenations": {
+ "pageDimensions": [1800, 2400],
+ "bubbleDimensions": [30, 25],
+ "customLabels": {
"Subject Code": ["subjectCode1", "subjectCode2"],
- "Roll": [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8",
- "roll9"
- ]
+ "Roll": ["roll1..10"]
},
- "singles": [
- "bookletNo",
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20",
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34",
- "q35",
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50",
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60",
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67",
- "q68",
- "q69",
- "q70",
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84",
- "q85",
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ],
- "qBlocks": {
+
+ "fieldBlocks": {
"bookletNo": {
- "orig": [595, 545],
- "bigGaps": [11, 11],
- "gaps": [75, 68],
- "qNos": [[["bookletNo"]]],
- "numQuestions": 1,
- "vals": ["A", "B", "C", "D"],
- "orient": "V"
+ "origin": [595, 545],
+ "bubblesGap": 68,
+ "labelsGap": 0,
+ "fieldLabels": ["bookletNo"],
+ "bubbleValues": ["A", "B", "C", "D"],
+ "direction": "vertical"
},
"subjectCode": {
- "orig": [914, 510],
- "bigGaps": [11, 11],
- "gaps": [43.0, 32.65],
- "qNos": [[["subjectCode1", "subjectCode2"]]],
- "qType": "QTYPE_INT"
+ "origin": [912, 512],
+ "bubblesGap": 33,
+ "labelsGap": 42.5,
+ "fieldLabels": ["subjectCode1", "subjectCode2"],
+ "fieldType": "QTYPE_INT"
},
"roll": {
- "orig": [1200, 510],
- "bigGaps": [11, 11],
- "gaps": [43.0, 32.65],
- "qNos": [
- [
- [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8",
- "roll9"
- ]
- ]
- ],
- "qType": "QTYPE_INT"
+ "origin": [1200, 510],
+ "bubblesGap": 33,
+ "labelsGap": 42.8,
+ "fieldLabels": ["roll1..10"],
+ "fieldType": "QTYPE_INT"
},
"q01block": {
- "orig": [500, 927],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [[["q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10"]]],
- "qType": "QTYPE_MCQ4"
+ "origin": [500, 927],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q1..10"],
+ "fieldType": "QTYPE_MCQ4"
},
"q11block": {
- "orig": [500, 1258],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q11", "q12", "q13", "q14", "q15", "q16", "q17", "q18", "q19", "q20"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [500, 1258],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q11..20"],
+ "fieldType": "QTYPE_MCQ4"
},
"q21block": {
- "orig": [495, 1589],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q21", "q22", "q23", "q24", "q25", "q26", "q27", "q28", "q29", "q30"]]
- ],
- "numQuestions": 10,
- "qType": "QTYPE_MCQ4"
+ "origin": [500, 1589],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q21..30"],
+ "fieldType": "QTYPE_MCQ4"
},
"q31block": {
- "orig": [495, 1925],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q31", "q32", "q33", "q34", "q35", "q36", "q37", "q38", "q39", "q40"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [495, 1925],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q31..40"],
+ "fieldType": "QTYPE_MCQ4"
},
"q41block": {
- "orig": [811, 927],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q41", "q42", "q43", "q44", "q45", "q46", "q47", "q48", "q49", "q50"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 927],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q41..50"],
+ "fieldType": "QTYPE_MCQ4"
},
"q51block": {
- "orig": [811, 1258],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q51", "q52", "q53", "q54", "q55", "q56", "q57", "q58", "q59", "q60"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 1258],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q51..60"],
+ "fieldType": "QTYPE_MCQ4"
},
"q61block": {
- "orig": [811, 1589],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q61", "q62", "q63", "q64", "q65", "q66", "q67", "q68", "q69", "q70"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 1589],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q61..70"],
+ "fieldType": "QTYPE_MCQ4"
},
"q71block": {
- "orig": [811, 1925],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q71", "q72", "q73", "q74", "q75", "q76", "q77", "q78", "q79", "q80"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 1925],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q71..80"],
+ "fieldType": "QTYPE_MCQ4"
},
"q81block": {
- "orig": [1125, 927],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q81", "q82", "q83", "q84", "q85", "q86", "q87", "q88", "q89", "q90"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [1125, 927],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q81..90"],
+ "fieldType": "QTYPE_MCQ4"
},
"q91block": {
- "orig": [1125, 1258],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [
- [
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ]
- ]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [1125, 1258],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q91..100"],
+ "fieldType": "QTYPE_MCQ4"
}
},
"preProcessors": [
diff --git a/samples/community/UmarFarootAPS/scans/scan-type-1.jpg b/samples/community/UmarFarootAPS/scans/scan-type-1.jpg
index 75ad2709..0932b240 100644
Binary files a/samples/community/UmarFarootAPS/scans/scan-type-1.jpg and b/samples/community/UmarFarootAPS/scans/scan-type-1.jpg differ
diff --git a/samples/community/UmarFarootAPS/scans/scan-type-2.jpg b/samples/community/UmarFarootAPS/scans/scan-type-2.jpg
index ffd5bd61..2eef7c32 100644
Binary files a/samples/community/UmarFarootAPS/scans/scan-type-2.jpg and b/samples/community/UmarFarootAPS/scans/scan-type-2.jpg differ
diff --git a/samples/community/UmarFarootAPS/template.json b/samples/community/UmarFarootAPS/template.json
index 80a89566..0a23717c 100644
--- a/samples/community/UmarFarootAPS/template.json
+++ b/samples/community/UmarFarootAPS/template.json
@@ -1,5 +1,5 @@
{
- "dimensions": [2550, 3300],
+ "pageDimensions": [2550, 3300],
"bubbleDimensions": [32, 32],
"preProcessors": [
{
@@ -10,562 +10,101 @@
}
}
],
- "concatenations": {
+ "customLabels": {
"Roll_no": ["r1", "r2", "r3", "r4"]
},
- "singles": [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20",
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34",
- "q35",
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50",
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60",
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67",
- "q68",
- "q69",
- "q70",
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84",
- "q85",
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100",
- "q101",
- "q102",
- "q103",
- "q104",
- "q105",
- "q106",
- "q107",
- "q108",
- "q109",
- "q110",
- "q111",
- "q112",
- "q113",
- "q114",
- "q115",
- "q116",
- "q117",
- "q118",
- "q119",
- "q120",
- "q121",
- "q122",
- "q123",
- "q124",
- "q125",
- "q126",
- "q127",
- "q128",
- "q129",
- "q130",
- "q131",
- "q132",
- "q133",
- "q134",
- "q135",
- "q136",
- "q137",
- "q138",
- "q139",
- "q140",
- "q141",
- "q142",
- "q143",
- "q144",
- "q145",
- "q146",
- "q147",
- "q148",
- "q149",
- "q150",
- "q151",
- "q152",
- "q153",
- "q154",
- "q155",
- "q156",
- "q157",
- "q158",
- "q159",
- "q160",
- "q161",
- "q162",
- "q163",
- "q164",
- "q165",
- "q166",
- "q167",
- "q168",
- "q169",
- "q170",
- "q171",
- "q172",
- "q173",
- "q174",
- "q175",
- "q176",
- "q177",
- "q178",
- "q179",
- "q180",
- "q181",
- "q182",
- "q183",
- "q184",
- "q185",
- "q186",
- "q187",
- "q188",
- "q189",
- "q190",
- "q191",
- "q192",
- "q193",
- "q194",
- "q195",
- "q196",
- "q197",
- "q198",
- "q199",
- "q200"
- ],
- "qBlocks": {
+
+ "fieldBlocks": {
"Roll_no": {
- "qType": "QTYPE_ROLL",
- "orig": [2169, 180],
- "qNos": [[["r1", "r2", "r3", "r4"]]],
- "gaps": [93, 61],
- "bigGaps": [1, 1]
+ "fieldType": "QTYPE_INT",
+ "origin": [2169, 180],
+ "fieldLabels": ["r1", "r2", "r3", "r4"],
+ "bubblesGap": 61,
+ "labelsGap": 93
},
"MCQBlock1a1": {
- "qType": "QTYPE_MCQ4",
- "orig": [197, 300],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [197, 300],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q1..17"]
},
"MCQBlock1a2": {
- "qType": "QTYPE_MCQ4",
- "orig": [197, 1310],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q18",
- "q19",
- "q20",
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [197, 1310],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q18..34"]
},
"MCQBlock1a3": {
- "qType": "QTYPE_MCQ4",
- "orig": [197, 2316],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q35",
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [197, 2316],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q35..50"]
},
"MCQBlock1a4": {
- "qType": "QTYPE_MCQ4",
- "orig": [725, 300],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60",
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [725, 300],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q51..67"]
},
"MCQBlock1a5": {
- "qType": "QTYPE_MCQ4",
- "orig": [725, 1310],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q68",
- "q69",
- "q70",
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [725, 1310],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q68..84"]
},
"MCQBlock1a6": {
- "qType": "QTYPE_MCQ4",
- "orig": [725, 2316],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q85",
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [725, 2316],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q85..100"]
},
"MCQBlock1a7": {
- "qType": "QTYPE_MCQ4",
- "orig": [1250, 300],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q101",
- "q102",
- "q103",
- "q104",
- "q105",
- "q106",
- "q107",
- "q108",
- "q109",
- "q110",
- "q111",
- "q112",
- "q113",
- "q114",
- "q115",
- "q116",
- "q117"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1250, 300],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q101..117"]
},
"MCQBlock1a8": {
- "qType": "QTYPE_MCQ4",
- "orig": [1250, 1310],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q118",
- "q119",
- "q120",
- "q121",
- "q122",
- "q123",
- "q124",
- "q125",
- "q126",
- "q127",
- "q128",
- "q129",
- "q130",
- "q131",
- "q132",
- "q133",
- "q134"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1250, 1310],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q118..134"]
},
"MCQBlock1a9": {
- "qType": "QTYPE_MCQ4",
- "orig": [1250, 2316],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q135",
- "q136",
- "q137",
- "q138",
- "q139",
- "q140",
- "q141",
- "q142",
- "q143",
- "q144",
- "q145",
- "q146",
- "q147",
- "q148",
- "q149",
- "q150"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1250, 2316],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q135..150"]
},
"MCQBlock1a10": {
- "qType": "QTYPE_MCQ4",
- "orig": [1776, 300],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q151",
- "q152",
- "q153",
- "q154",
- "q155",
- "q156",
- "q157",
- "q158",
- "q159",
- "q160",
- "q161",
- "q162",
- "q163",
- "q164",
- "q165",
- "q166",
- "q167"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1770, 300],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q151..167"]
},
"MCQBlock1a11": {
- "qType": "QTYPE_MCQ4",
- "orig": [1776, 1310],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q168",
- "q169",
- "q170",
- "q171",
- "q172",
- "q173",
- "q174",
- "q175",
- "q176",
- "q177",
- "q178",
- "q179",
- "q180",
- "q181",
- "q182",
- "q183",
- "q184"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1770, 1310],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q168..184"]
},
"MCQBlock1a12": {
- "qType": "QTYPE_MCQ4",
- "orig": [1776, 2316],
- "gaps": [92, 59.6],
- "bigGaps": [10, 20],
- "qNos": [
- [
- [
- "q185",
- "q186",
- "q187",
- "q188",
- "q189",
- "q190",
- "q191",
- "q192",
- "q193",
- "q194",
- "q195",
- "q196",
- "q197",
- "q198",
- "q199",
- "q200"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1770, 2316],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q185..200"]
}
}
}
diff --git a/samples/community/ibrahimkilic/template.json b/samples/community/ibrahimkilic/template.json
index b4435d37..37a3f933 100644
--- a/samples/community/ibrahimkilic/template.json
+++ b/samples/community/ibrahimkilic/template.json
@@ -1,20 +1,18 @@
{
- "dimensions": [ 299, 328 ],
+ "pageDimensions": [ 299, 328 ],
"bubbleDimensions": [ 20, 20 ],
- "concatenations": {},
- "singles": [ "q1", "q2", "q3", "q4", "q5" ],
- "emptyVal": "no",
- "qBlocks": {
+ "emptyValue": "no",
+ "fieldBlocks": {
"YesNoBlock1": {
- "orient": "H",
- "vals": ["yes"],
- "orig": [ 15, 55 ],
- "emptyVal": "no",
- "qNos": [ [ [ "q1", "q2", "q3", "q4", "q5"] ] ],
- "gaps": [ 48, 48 ],
- "bigGaps": [ 48, 48 ]
+ "direction": "horizontal",
+ "bubbleValues": ["yes"],
+ "origin": [ 15, 55 ],
+ "emptyValue": "no",
+ "bubblesGap":48,
+ "labelsGap": 48,
+ "fieldLabels": ["q1..5"]
}
},
"preProcessors": [
]
-}
+}
diff --git a/samples/sample1/template.json b/samples/sample1/template.json
index ba495f4d..5cde0e13 100644
--- a/samples/sample1/template.json
+++ b/samples/sample1/template.json
@@ -1,20 +1,99 @@
{
- "dimensions": [
- 1846,
- 1500
- ],
- "bubbleDimensions": [
- 40,
- 40
- ],
+ "pageDimensions": [1846, 1500],
+ "bubbleDimensions": [40, 40],
+ "customLabels": {
+ "Roll": ["Medium", "roll1..9"],
+ "q5": ["q5_1", "q5_2"],
+ "q6": ["q6_1", "q6_2"],
+ "q7": ["q7_1", "q7_2"],
+ "q8": ["q8_1", "q8_2"],
+ "q9": ["q9_1", "q9_2"]
+ },
+ "fieldBlocks": {
+ "Medium": {
+ "bubblesGap": 41,
+ "bubbleValues": ["E", "H"],
+ "direction": "vertical",
+ "fieldLabels": ["Medium"],
+ "labelsGap": 0,
+ "origin": [170, 282]
+ },
+ "Roll": {
+ "fieldType": "QTYPE_INT",
+ "fieldLabels": ["roll1..9"],
+ "bubblesGap": 46,
+ "labelsGap": 58,
+ "origin": [225, 282]
+ },
+ "Int_Block_Q5": {
+ "fieldType": "QTYPE_INT",
+ "fieldLabels": ["q5_1", "q5_2"],
+ "bubblesGap": 46,
+ "labelsGap": 60,
+ "origin": [903, 282]
+ },
+ "Int_Block_Q6": {
+ "fieldType": "QTYPE_INT",
+ "fieldLabels": ["q6_1", "q6_2"],
+ "bubblesGap": 46,
+ "labelsGap": 60,
+ "origin": [1077, 282]
+ },
+ "Int_Block_Q7": {
+ "fieldType": "QTYPE_INT",
+ "fieldLabels": ["q7_1", "q7_2"],
+ "bubblesGap": 46,
+ "labelsGap": 60,
+ "origin": [1240, 282]
+ },
+ "Int_Block_Q8": {
+ "fieldType": "QTYPE_INT",
+ "fieldLabels": ["q8_1", "q8_2"],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "origin": [1410, 282]
+ },
+ "Int_Block_Q9": {
+ "fieldType": "QTYPE_INT",
+ "fieldLabels": ["q9_1", "q9_2"],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "origin": [1580, 282]
+ },
+ "MCQ_Block_Q1": {
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q1..4"],
+ "bubblesGap": 59,
+ "labelsGap": 50,
+ "origin": [121, 860]
+ },
+ "MCQ_Block_Q10": {
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q10..13"],
+ "bubblesGap": 59,
+ "labelsGap": 50,
+ "origin": [121, 1195]
+ },
+ "MCQ_Block_Q14": {
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q14..16"],
+ "bubblesGap": 57,
+ "labelsGap": 50,
+ "origin": [905, 860]
+ },
+ "MCQ_Block_Q17": {
+ "fieldType": "QTYPE_MCQ4",
+ "fieldLabels": ["q17..20"],
+ "bubblesGap": 57,
+ "labelsGap": 50,
+ "origin": [905, 1195]
+ }
+ },
"preProcessors": [
{
"name": "CropPage",
"options": {
- "morphKernel": [
- 10,
- 10
- ]
+ "morphKernel": [10, 10]
}
},
{
@@ -24,251 +103,5 @@
"sheetToMarkerWidthRatio": 17
}
}
- ],
-
- "concatenations": {
- "Roll": [
- "Squad",
- "Medium",
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8"
- ],
- "q5": [
- "q5.1",
- "q5.2"
- ],
- "q6": [
- "q6.1",
- "q6.2"
- ],
- "q7": [
- "q7.1",
- "q7.2"
- ],
- "q8": [
- "q8.1",
- "q8.2"
- ],
- "q9": [
- "q9.1",
- "q9.2"
- ]
- },
- "singles": [
- "q1",
- "q2",
- "q3",
- "q4",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20"
- ],
- "qBlocks": {
- "Medium": {
- "vals": ["E", "H"],
- "orient": "V",
- "orig": [
- 160,
- 285
- ],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [
- 59,
- 46
- ],
- "qNos": [
- [
- [
- "Medium"
- ]
- ]
- ]
- },
- "Roll": {
- "qType": "QTYPE_ROLL",
- "orig": [
- 218,
- 285
- ],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [
- 58,
- 46
- ],
- "qNos": [
- [
- [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8"
- ]
- ]
- ]
- },
- "Int1": {
- "qType": "QTYPE_INT",
- "orig": [
- 903,
- 285
- ],
- "bigGaps": [
- 128,
- 11
- ],
- "gaps": [
- 59,
- 46
- ],
- "qNos": [
- [
- [
- "q5.1",
- "q5.2"
- ],
- [
- "q6.1",
- "q6.2"
- ],
- [
- "q7.1",
- "q7.2"
- ]
- ]
- ]
- },
- "Int2": {
- "qType": "QTYPE_INT",
- "orig": [
- 1418,
- 285
- ],
- "bigGaps": [
- 128,
- 11
- ],
- "gaps": [
- 59,
- 46
- ],
- "qNos": [
- [
- [
- "q8.1",
- "q8.2"
- ],
- [
- "q9.1",
- "q9.2"
- ]
- ]
- ]
- },
- "Mcq1": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 118,
- 857
- ],
- "bigGaps": [
- 115,
- 181
- ],
- "gaps": [
- 59,
- 53
- ],
- "qNos": [
- [
- [
- "q1",
- "q2",
- "q3",
- "q4"
- ],
- [
- "q10",
- "q11",
- "q12",
- "q13"
- ]
- ]
- ]
- },
- "Mcq2": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 905,
- 860
- ],
- "bigGaps": [
- 115,
- 180
- ],
- "gaps": [
- 59,
- 53
- ],
- "qNos": [
- [
- [
- "q14",
- "q15",
- "q16"
- ]
- ]
- ]
- },
- "Mcq3": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 905,
- 1198
- ],
- "bigGaps": [
- 115,
- 180
- ],
- "gaps": [
- 59,
- 53
- ],
- "qNos": [
- [
- [
- "q17",
- "q18",
- "q19",
- "q20"
- ]
- ]
- ]
- }
- }
+ ]
}
diff --git a/samples/sample2/template.json b/samples/sample2/template.json
index a8dbed29..cb15d51e 100644
--- a/samples/sample2/template.json
+++ b/samples/sample2/template.json
@@ -1,57 +1,21 @@
{
- "dimensions": [
- 300,
- 400
- ],
- "bubbleDimensions": [
- 25,
- 25
- ],
+ "pageDimensions": [300, 400],
+ "bubbleDimensions": [25, 25],
"preProcessors": [
{
"name": "CropPage",
"options": {
- "morphKernel": [
- 10,
- 10
- ]
+ "morphKernel": [10, 10]
}
}
],
- "concatenations": {},
- "singles": [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5"
- ],
- "qBlocks": {
- "MCQBlock1": {
- "qType": "QTYPE_MCQ5",
- "orig": [
- 65,
- 60
- ],
- "qNos": [
- [
- [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5"
- ]
- ]
- ],
- "gaps": [
- 41,
- 52
- ],
- "bigGaps": [
- 30,
- 30
- ]
+ "fieldBlocks": {
+ "MCQ_Block_1": {
+ "fieldType": "QTYPE_MCQ5",
+ "origin": [65, 60],
+ "fieldLabels": ["q1..5"],
+ "labelsGap": 52,
+ "bubblesGap": 41
}
}
}
diff --git a/samples/sample3/colored-thick-sheet/template.json b/samples/sample3/colored-thick-sheet/template.json
index b1cb6c95..0d421969 100644
--- a/samples/sample3/colored-thick-sheet/template.json
+++ b/samples/sample3/colored-thick-sheet/template.json
@@ -1,226 +1,76 @@
{
- "dimensions": [1800, 2400],
+ "pageDimensions": [1800, 2400],
"bubbleDimensions": [23, 20],
- "concatenations": {
- "Subject Code": ["subjectCode1", "subjectCode2"],
- "Roll": [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8",
- "roll9"
- ]
- },
- "singles": [
- "bookletNo",
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20",
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34",
- "q35",
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50",
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60",
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67",
- "q68",
- "q69",
- "q70",
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84",
- "q85",
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ],
- "qBlocks": {
+ "fieldBlocks": {
"q01block": {
- "orig": [504, 927],
- "bigGaps": [11, 11],
- "gaps": [60.35, 31.75],
- "qNos": [[["q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10"]]],
- "qType": "QTYPE_MCQ4"
+ "origin": [504, 927],
+ "bubblesGap": 60.35,
+ "labelsGap": 31.75,
+ "fieldLabels": ["q1..10"],
+ "fieldType": "QTYPE_MCQ4"
},
"q11block": {
- "orig": [504, 1242],
- "bigGaps": [11, 11],
- "gaps": [60.35, 31.75],
- "qNos": [
- [["q11", "q12", "q13", "q14", "q15", "q16", "q17", "q18", "q19", "q20"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [504, 1242],
+ "bubblesGap": 60.35,
+ "labelsGap": 31.75,
+ "fieldLabels": ["q11..20"],
+ "fieldType": "QTYPE_MCQ4"
},
"q21block": {
- "orig": [500, 1562],
- "bigGaps": [11, 11],
- "gaps": [61.25, 32.5],
- "qNos": [
- [["q21", "q22", "q23", "q24", "q25", "q26", "q27", "q28", "q29", "q30"]]
- ],
- "numQuestions": 10,
- "qType": "QTYPE_MCQ4"
+ "origin": [500, 1562],
+ "bubblesGap": 61.25,
+ "labelsGap": 32.5,
+ "fieldLabels": ["q21..30"],
+ "fieldType": "QTYPE_MCQ4"
},
"q31block": {
- "orig": [500, 1885],
- "bigGaps": [11, 11],
- "gaps": [62.25, 33.5],
- "qNos": [
- [["q31", "q32", "q33", "q34", "q35", "q36", "q37", "q38", "q39", "q40"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [500, 1885],
+ "bubblesGap": 62.25,
+ "labelsGap": 33.5,
+ "fieldLabels": ["q31..40"],
+ "fieldType": "QTYPE_MCQ4"
},
"q41block": {
- "orig": [811, 927],
- "bigGaps": [11, 11],
- "gaps": [60.35, 31.75],
- "qNos": [
- [["q41", "q42", "q43", "q44", "q45", "q46", "q47", "q48", "q49", "q50"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 927],
+ "bubblesGap": 60.35,
+ "labelsGap": 31.75,
+ "fieldLabels": ["q41..50"],
+ "fieldType": "QTYPE_MCQ4"
},
"q51block": {
- "orig": [811, 1242],
- "bigGaps": [11, 11],
- "gaps": [60.35, 31.75],
- "qNos": [
- [["q51", "q52", "q53", "q54", "q55", "q56", "q57", "q58", "q59", "q60"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 1242],
+ "bubblesGap": 60.35,
+ "labelsGap": 31.75,
+ "fieldLabels": ["q51..60"],
+ "fieldType": "QTYPE_MCQ4"
},
"q61block": {
- "orig": [811, 1562],
- "bigGaps": [11, 11],
- "gaps": [61.25, 32.5],
- "qNos": [
- [["q61", "q62", "q63", "q64", "q65", "q66", "q67", "q68", "q69", "q70"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 1562],
+ "bubblesGap": 61.25,
+ "labelsGap": 32.5,
+ "fieldLabels": ["q61..70"],
+ "fieldType": "QTYPE_MCQ4"
},
"q71block": {
- "orig": [811, 1885],
- "bigGaps": [11, 11],
- "gaps": [62.25, 33.5],
- "qNos": [
- [["q71", "q72", "q73", "q74", "q75", "q76", "q77", "q78", "q79", "q80"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [811, 1885],
+ "bubblesGap": 62.25,
+ "labelsGap": 33.5,
+ "fieldLabels": ["q71..80"],
+ "fieldType": "QTYPE_MCQ4"
},
"q81block": {
- "orig": [1120, 927],
- "bigGaps": [11, 11],
- "gaps": [60.35, 31.75],
- "qNos": [
- [["q81", "q82", "q83", "q84", "q85", "q86", "q87", "q88", "q89", "q90"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [1120, 927],
+ "bubblesGap": 60.35,
+ "labelsGap": 31.75,
+ "fieldLabels": ["q81..90"],
+ "fieldType": "QTYPE_MCQ4"
},
"q91block": {
- "orig": [1120, 1242],
- "bigGaps": [11, 11],
- "gaps": [60.35, 31.75],
- "qNos": [
- [
- [
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ]
- ]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [1120, 1242],
+ "bubblesGap": 60.35,
+ "labelsGap": 31.75,
+ "fieldLabels": ["q91..100"],
+ "fieldType": "QTYPE_MCQ4"
}
},
"preProcessors": [
diff --git a/samples/sample3/xeroxed-thin-sheet/template.json b/samples/sample3/xeroxed-thin-sheet/template.json
index d24f9daa..472cefa3 100644
--- a/samples/sample3/xeroxed-thin-sheet/template.json
+++ b/samples/sample3/xeroxed-thin-sheet/template.json
@@ -1,226 +1,76 @@
{
- "dimensions": [1800, 2400],
+ "pageDimensions": [1800, 2400],
"bubbleDimensions": [23, 20],
- "concatenations": {
- "Subject Code": ["subjectCode1", "subjectCode2"],
- "Roll": [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8",
- "roll9"
- ]
- },
- "singles": [
- "bookletNo",
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20",
- "q21",
- "q22",
- "q23",
- "q24",
- "q25",
- "q26",
- "q27",
- "q28",
- "q29",
- "q30",
- "q31",
- "q32",
- "q33",
- "q34",
- "q35",
- "q36",
- "q37",
- "q38",
- "q39",
- "q40",
- "q41",
- "q42",
- "q43",
- "q44",
- "q45",
- "q46",
- "q47",
- "q48",
- "q49",
- "q50",
- "q51",
- "q52",
- "q53",
- "q54",
- "q55",
- "q56",
- "q57",
- "q58",
- "q59",
- "q60",
- "q61",
- "q62",
- "q63",
- "q64",
- "q65",
- "q66",
- "q67",
- "q68",
- "q69",
- "q70",
- "q71",
- "q72",
- "q73",
- "q74",
- "q75",
- "q76",
- "q77",
- "q78",
- "q79",
- "q80",
- "q81",
- "q82",
- "q83",
- "q84",
- "q85",
- "q86",
- "q87",
- "q88",
- "q89",
- "q90",
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ],
- "qBlocks": {
+ "fieldBlocks": {
"q01block": {
- "orig": [492, 924],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [[["q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10"]]],
- "qType": "QTYPE_MCQ4"
+ "origin": [492, 924],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q1..10"],
+ "fieldType": "QTYPE_MCQ4"
},
"q11block": {
- "orig": [492, 1258],
- "bigGaps": [11, 11],
- "gaps": [59.75, 32.65],
- "qNos": [
- [["q11", "q12", "q13", "q14", "q15", "q16", "q17", "q18", "q19", "q20"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [492, 1258],
+ "bubblesGap": 59.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q11..20"],
+ "fieldType": "QTYPE_MCQ4"
},
"q21block": {
- "orig": [492, 1589],
- "bigGaps": [11, 11],
- "gaps": [60.75, 32.65],
- "qNos": [
- [["q21", "q22", "q23", "q24", "q25", "q26", "q27", "q28", "q29", "q30"]]
- ],
- "numQuestions": 10,
- "qType": "QTYPE_MCQ4"
+ "origin": [492, 1589],
+ "bubblesGap": 60.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q21..30"],
+ "fieldType": "QTYPE_MCQ4"
},
"q31block": {
- "orig": [487, 1920],
- "bigGaps": [11, 11],
- "gaps": [61.75, 32.65],
- "qNos": [
- [["q31", "q32", "q33", "q34", "q35", "q36", "q37", "q38", "q39", "q40"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [487, 1920],
+ "bubblesGap": 61.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q31..40"],
+ "fieldType": "QTYPE_MCQ4"
},
"q41block": {
- "orig": [807, 924],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q41", "q42", "q43", "q44", "q45", "q46", "q47", "q48", "q49", "q50"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [807, 924],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q41..50"],
+ "fieldType": "QTYPE_MCQ4"
},
"q51block": {
- "orig": [803, 1258],
- "bigGaps": [11, 11],
- "gaps": [59.75, 32.65],
- "qNos": [
- [["q51", "q52", "q53", "q54", "q55", "q56", "q57", "q58", "q59", "q60"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [803, 1258],
+ "bubblesGap": 59.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q51..60"],
+ "fieldType": "QTYPE_MCQ4"
},
"q61block": {
- "orig": [803, 1589],
- "bigGaps": [11, 11],
- "gaps": [60.75, 32.65],
- "qNos": [
- [["q61", "q62", "q63", "q64", "q65", "q66", "q67", "q68", "q69", "q70"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [803, 1589],
+ "bubblesGap": 60.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q61..70"],
+ "fieldType": "QTYPE_MCQ4"
},
"q71block": {
- "orig": [803, 1920],
- "bigGaps": [11, 11],
- "gaps": [60.75, 32.65],
- "qNos": [
- [["q71", "q72", "q73", "q74", "q75", "q76", "q77", "q78", "q79", "q80"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [803, 1920],
+ "bubblesGap": 60.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q71..80"],
+ "fieldType": "QTYPE_MCQ4"
},
"q81block": {
- "orig": [1115, 924],
- "bigGaps": [11, 11],
- "gaps": [58.75, 32.65],
- "qNos": [
- [["q81", "q82", "q83", "q84", "q85", "q86", "q87", "q88", "q89", "q90"]]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [1115, 924],
+ "bubblesGap": 58.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q81..90"],
+ "fieldType": "QTYPE_MCQ4"
},
"q91block": {
- "orig": [1115, 1258],
- "bigGaps": [11, 11],
- "gaps": [59.75, 32.65],
- "qNos": [
- [
- [
- "q91",
- "q92",
- "q93",
- "q94",
- "q95",
- "q96",
- "q97",
- "q98",
- "q99",
- "q100"
- ]
- ]
- ],
- "qType": "QTYPE_MCQ4"
+ "origin": [1115, 1258],
+ "bubblesGap": 59.75,
+ "labelsGap": 32.65,
+ "fieldLabels": ["q91..100"],
+ "fieldType": "QTYPE_MCQ4"
}
},
"preProcessors": [
diff --git a/samples/sample4/config.json b/samples/sample4/config.json
index 87561207..4d662afb 100644
--- a/samples/sample4/config.json
+++ b/samples/sample4/config.json
@@ -1,4 +1,8 @@
{
+ "dimensions": {
+ "display_width": 1189,
+ "display_height": 1682
+ },
"threshold_params": {
"MIN_JUMP": 30
},
diff --git a/samples/sample4/template.json b/samples/sample4/template.json
index ffeaf005..38597f45 100644
--- a/samples/sample4/template.json
+++ b/samples/sample4/template.json
@@ -1,9 +1,5 @@
{
- "Globals": {
- "display_width": 1189,
- "display_height": 1682
- },
- "dimensions": [1189, 1682],
+ "pageDimensions": [1189, 1682],
"bubbleDimensions": [30, 30],
"preProcessors": [
{
@@ -20,29 +16,15 @@
}
}
],
- "concatenations": {},
- "singles": [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q6",
- "q7",
- "q8",
- "q9",
- "q10",
- "q11"
- ],
- "qBlocks": {
+ "fieldBlocks": {
"MCQBlock1": {
- "qType": "QTYPE_MCQ4",
- "orig": [134, 684],
- "qNos": [
- [["q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "q9", "q10", "q11"]]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [134, 684],
+ "fieldLabels": [
+ "q1..11"
],
- "gaps": [79, 62],
- "bigGaps": [10, 30]
+ "bubblesGap": 79,
+ "labelsGap": 62
}
}
}
diff --git a/samples/sample5/README.md b/samples/sample5/README.md
index 2247e5cf..b5ed8ccb 100644
--- a/samples/sample5/README.md
+++ b/samples/sample5/README.md
@@ -3,5 +3,4 @@
This sample demonstrates multiple things, namely -
- Running OMRChecker on images scanned using popular document scanning apps
- Using a common template.json file for sub-folders (e.g. multiple scan batches)
-- Using evaluation.json file with custom marking
-- The example implements complex marking schemes like that of [Technothlon 2019](https://drive.google.com/file/d/1BhjgHgAItq305B6nKwGJE5bde-SOAdln/view) question paper
+- Using evaluation.json file with custom marking (without streak-based marking)
diff --git a/samples/sample5/evaluation.json b/samples/sample5/evaluation.json
index db077e56..8ac7dcf4 100644
--- a/samples/sample5/evaluation.json
+++ b/samples/sample5/evaluation.json
@@ -2,30 +2,53 @@
"source_type": "custom",
"options": {
"questions_in_order": ["q1..22"],
- "answers_in_order": [ "C", "C", "B", "C", "C", ["1", "01"], "19", "10", "10", "18", "D", "A", "D", "D", "D", "C", "C", "C", "C", "D", "B", "A" ],
+ "answers_in_order": [
+ "C",
+ "C",
+ "B",
+ "C",
+ "C",
+ ["1", "01"],
+ "19",
+ "10",
+ "10",
+ "18",
+ "D",
+ "A",
+ "D",
+ "D",
+ "D",
+ "C",
+ "C",
+ "C",
+ "C",
+ "D",
+ "B",
+ "A"
+ ],
"should_explain_scoring": true
},
"marking_scheme": {
"DEFAULT": { "correct": "1", "incorrect": "0", "unmarked": "0" },
- "BOOMERANG_1":{
+ "BOOMERANG_1": {
"questions": ["q1..5"],
- "marking": {"correct": 4, "incorrect": -1, "unmarked": 0}
+ "marking": { "correct": 4, "incorrect": -1, "unmarked": 0 }
},
- "PROXIMITY_1":{
+ "PROXIMITY_1": {
"questions": ["q6..10"],
- "marking": {"correct": 3, "incorrect": -1, "unmarked": 0}
+ "marking": { "correct": 3, "incorrect": -1, "unmarked": 0 }
},
- "FIBONACCI_SEQ_1":{
+ "FIBONACCI_SECTION_1": {
"questions": ["q11..14"],
- "marking": {"correct": [2, 3, 5, 8], "incorrect": [-1, -1, -2, -3], "unmarked": 0}
+ "marking": { "correct": 2, "incorrect": -1, "unmarked": 0 }
},
- "POWER_SCHEME_1":{
+ "POWER_SECTION_1": {
"questions": ["q15..18"],
- "marking": { "correct": [1, 2, 4, 8], "incorrect": 0, "unmarked": 0 }
+ "marking": { "correct": 1, "incorrect": 0, "unmarked": 0 }
},
- "FIBONACCI_SEQ_2":{
+ "FIBONACCI_SECTION_2": {
"questions": ["q19..22"],
- "marking": {"correct": [2, 3, 5, 8], "incorrect": [-1, -1, -2, -3], "unmarked": 0}
+ "marking": { "correct": 2, "incorrect": -1, "unmarked": 0 }
}
}
}
diff --git a/samples/sample5/template.json b/samples/sample5/template.json
index dd844839..c666cc1d 100644
--- a/samples/sample5/template.json
+++ b/samples/sample5/template.json
@@ -1,12 +1,6 @@
{
- "dimensions": [
- 1846,
- 1500
- ],
- "bubbleDimensions": [
- 40,
- 40
- ],
+ "pageDimensions": [1846, 1500],
+ "bubbleDimensions": [40, 40],
"preProcessors": [
{
"name": "CropOnMarkers",
@@ -16,234 +10,92 @@
}
}
],
- "concatenations": {
- "Roll": [
- "Squad",
- "Medium",
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8"
- ],
- "q6": [
- "q6.1",
- "q6.2"
- ],
- "q7": [
- "q7.1",
- "q7.2"
- ],
- "q8": [
- "q8.1",
- "q8.2"
- ],
- "q9": [
- "q9.1",
- "q9.2"
- ],
- "q10": [
- "q10.1",
- "q10.2"
- ]
+ "customLabels": {
+ "Roll": ["Medium", "roll1..9"],
+ "q6": ["q6_1", "q6_2"],
+ "q7": ["q7_1", "q7_2"],
+ "q8": ["q8_1", "q8_2"],
+ "q9": ["q9_1", "q9_2"],
+ "q10": ["q10_1", "q10_2"]
},
- "singles": [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5",
- "q11",
- "q12",
- "q13",
- "q14",
- "q15",
- "q16",
- "q17",
- "q18",
- "q19",
- "q20",
- "q21",
- "q22"
- ],
- "qBlocks": {
+ "fieldBlocks": {
"Medium": {
- "vals": ["E", "H"],
- "orient": "V",
- "orig": [
- 208,
- 205
- ],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [
- 59,
- 46
- ],
- "qNos": [
- [
- [
- "Medium"
- ]
- ]
- ]
+ "bubbleValues": ["E", "H"],
+ "direction": "vertical",
+ "origin": [200, 215],
+ "bubblesGap": 46,
+ "labelsGap": 0,
+ "fieldLabels": ["Medium"]
},
"Roll": {
- "qType": "QTYPE_ROLL",
- "orig": [
- 261,
- 210
- ],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [
- 58,
- 46
- ],
- "qNos": [
- [
- [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "roll7",
- "roll8"
- ]
- ]
- ]
+ "fieldType": "QTYPE_INT",
+ "origin": [261, 210],
+ "bubblesGap": 46.5,
+ "labelsGap": 58,
+ "fieldLabels": ["roll1..9"]
},
"Int1": {
- "qType": "QTYPE_INT",
- "orig": [
- 935,
- 211
- ],
- "bigGaps": [
- 124,
- 11
- ],
- "gaps": [
- 57,
- 46
- ],
- "qNos": [
- [
- [
- "q6.1",
- "q6.2"
- ],
- [
- "q7.1",
- "q7.2"
- ],
- [
- "q8.1",
- "q8.2"
- ],
- [
- "q9.1",
- "q9.2"
- ],
- [
- "q10.1",
- "q10.2"
- ]
- ]
- ]
+ "fieldType": "QTYPE_INT",
+ "origin": [935, 211],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "fieldLabels": ["q6_1", "q6_2"]
+ },
+ "Int2": {
+ "fieldType": "QTYPE_INT",
+ "origin": [1100, 211],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "fieldLabels": ["q7_1", "q7_2"]
+ },
+ "Int3": {
+ "fieldType": "QTYPE_INT",
+ "origin": [1275, 211],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "fieldLabels": ["q8_1", "q8_2"]
+ },
+ "Int4": {
+ "fieldType": "QTYPE_INT",
+ "origin": [1449, 211],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "fieldLabels": ["q9_1", "q9_2"]
+ },
+ "Int5": {
+ "fieldType": "QTYPE_INT",
+ "origin": [1620, 211],
+ "bubblesGap": 46,
+ "labelsGap": 57,
+ "fieldLabels": ["q10_1", "q10_2"]
},
"Mcq1": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 198,
- 826
- ],
- "bigGaps": [
- 115,
- 183
- ],
- "gaps": [
- 93,
- 62
- ],
- "qNos": [
- [
- [
- "q1",
- "q2",
- "q3",
- "q4",
- "q5"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [198, 826],
+ "bubblesGap": 93,
+ "labelsGap": 62,
+ "fieldLabels": ["q1..5"]
},
"Mcq2": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 833,
- 830
- ],
- "bigGaps": [
- 127,
- 254
- ],
- "gaps": [
- 71,
- 61
- ],
- "qNos": [
- [
- [
- "q11",
- "q12",
- "q13",
- "q14"
- ],
- [
- "q15",
- "q16",
- "q17",
- "q18"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [833, 830],
+ "bubblesGap": 71,
+ "labelsGap": 61,
+ "fieldLabels": ["q11..14"]
},
"Mcq3": {
- "qType": "QTYPE_MCQ4",
- "orig": [
- 1481,
- 830
- ],
- "bigGaps": [
- 115,
- 183
- ],
- "gaps": [
- 73,
- 61
- ],
- "qNos": [
- [
- [
- "q19",
- "q20",
- "q21",
- "q22"
- ]
- ]
- ]
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [833, 1270],
+ "bubblesGap": 71,
+ "labelsGap": 61,
+ "fieldLabels": ["q15..18"]
+ },
+ "Mcq4": {
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1481, 830],
+ "bubblesGap": 73,
+ "labelsGap": 61,
+ "fieldLabels": ["q19..22"]
}
}
}
diff --git a/samples/sample6/config.json b/samples/sample6/config.json
new file mode 100644
index 00000000..708bfd8d
--- /dev/null
+++ b/samples/sample6/config.json
@@ -0,0 +1,6 @@
+{
+ "dimensions": {
+ "display_width": 2480,
+ "display_height": 3508
+ }
+}
\ No newline at end of file
diff --git a/samples/sample6/template.json b/samples/sample6/template.json
index 3c28eb48..2efa0950 100644
--- a/samples/sample6/template.json
+++ b/samples/sample6/template.json
@@ -1,21 +1,11 @@
{
- "Globals": {
- "display_width": 2480,
- "display_height": 3508
- },
- "dimensions": [
- 2480,
- 3508
- ],
- "bubbleDimensions": [
- 42,
- 42
- ],
+ "pageDimensions": [2480, 3508],
+ "bubbleDimensions": [42, 42],
"preProcessors": [
{
"name": "Levels",
"options": {
- "low": 0.70,
+ "low": 0.7,
"high": 0.8
}
},
@@ -27,137 +17,40 @@
}
}
],
- "concatenations": {
- "Roll": [
- "stu",
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "check"
- ]
+ "customLabels": {
+ "Roll": ["stu", "roll1..7", "check_1", "check_2"]
},
- "singles": [],
- "qBlocks": {
+ "fieldBlocks": {
"Check1": {
- "orig": [
- 2033,
- 1290
- ],
- "gaps": [
- 50,
- 50
- ],
- "bigGaps": [
- 20,
- 20
- ],
- "qNos": [
- [
- [
- "check"
- ]
- ]
- ],
- "vals": [
- "A",
- "B",
- "E",
- "H",
- "J",
- "L",
- "M"
- ],
- "orient": "V"
+ "origin": [2033, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["check_1"],
+ "bubbleValues": ["A", "B", "E", "H", "J", "L", "M"],
+ "direction": "vertical"
},
"Check2": {
- "orig": [
- 2083,
- 1290
- ],
- "gaps": [
- 50,
- 50
- ],
- "bigGaps": [
- 20,
- 20
- ],
- "qNos": [
- [
- [
- "check"
- ]
- ]
- ],
- "vals": [
- "N",
- "R",
- "U",
- "W",
- "X",
- "Y"
- ],
- "orient": "V"
+ "origin": [2083, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["check_2"],
+ "bubbleValues": ["N", "R", "U", "W", "X", "Y"],
+ "direction": "vertical"
},
"Stu": {
- "orig": [
- 1636,
- 1290
- ],
- "gaps": [
- 50,
- 50
- ],
- "bigGaps": [
- 20,
- 20
- ],
- "qNos": [
- [
- [
- "stu"
- ]
- ]
- ],
- "vals": [
- "U",
- "A",
- "HT",
- "GT"
- ],
- "orient": "V"
+ "origin": [1636, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["stu"],
+ "bubbleValues": ["U", "A", "HT", "GT"],
+ "direction": "vertical"
},
"Roll": {
- "qType": "QTYPE_ROLL",
- "orig": [
- 1685,
- 1290
- ],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [
- 50.5,
- 50.5
- ],
- "qNos": [
- [
- [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6"
- ]
- ]
- ]
+ "fieldType": "QTYPE_INT",
+ "origin": [1685, 1290],
+ "bubblesGap": 50.5,
+ "labelsGap": 50.5,
+ "fieldLabels": ["roll1..7"]
}
}
}
diff --git a/samples/sample6/template_fb_align.json b/samples/sample6/template_fb_align.json
index 3f0923a1..bdaceb26 100644
--- a/samples/sample6/template_fb_align.json
+++ b/samples/sample6/template_fb_align.json
@@ -1,16 +1,11 @@
{
- "Globals": {
- "display_width": 2480 ,
- "display_height": 3508
- },
- "dimensions": [2480, 3508 ],
- "bubbleDimensions": [42, 42 ],
-
+ "pageDimensions": [2480, 3508],
+ "bubbleDimensions": [42, 42],
"preProcessors": [
{
"name": "Levels",
"options": {
- "low": 0.70,
+ "low": 0.7,
"high": 0.8
}
},
@@ -30,57 +25,40 @@
}
}
],
- "concatenations" : {
- "Roll" : ["stu","roll0", "roll1", "roll2", "roll3", "roll4", "roll5", "roll6", "check"]
+ "customLabels": {
+ "Roll": ["stu", "roll1..7", "check_1", "check_2"]
},
- "singles":[
- ],
- "qBlocks": {
+ "fieldBlocks": {
"Check1": {
- "orig": [2033, 1290],
- "gaps": [ 50, 50],
- "bigGaps": [20, 20],
- "qNos": [[["check"]]],
- "vals": ["A","B","E","H","J","L","M"],
- "orient": "V"
+ "origin": [2033, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["check_1"],
+ "bubbleValues": ["A", "B", "E", "H", "J", "L", "M"],
+ "direction": "vertical"
},
"Check2": {
- "orig": [2083, 1290],
- "gaps": [ 50, 50],
- "bigGaps": [20, 20],
- "qNos": [[["check"]]],
- "vals": ["N","R","U","W", "X","Y"],
- "orient": "V"
+ "origin": [2083, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["check_2"],
+ "bubbleValues": ["N", "R", "U", "W", "X", "Y"],
+ "direction": "vertical"
},
- "Stu": {
- "orig": [1636, 1290],
- "gaps": [ 50, 50],
- "bigGaps": [20, 20],
- "qNos": [[["stu"]]],
- "vals": ["U", "A", "HT", "GT"],
- "orient": "V"
+ "Stu": {
+ "origin": [1636, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["stu"],
+ "bubbleValues": ["U", "A", "HT", "GT"],
+ "direction": "vertical"
},
"Roll": {
- "qType": "QTYPE_ROLL",
- "orig": [1685, 1290],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [50.5, 50.5],
- "qNos": [
- [
- [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6"
- ]
- ]
- ]
+ "fieldType": "QTYPE_INT",
+ "origin": [1685, 1290],
+ "bubblesGap": 50.5,
+ "labelsGap": 50.5,
+ "fieldLabels": ["roll1..7"]
}
}
}
diff --git a/samples/sample6/template_no_fb_align.json b/samples/sample6/template_no_fb_align.json
index 3c28eb48..2efa0950 100644
--- a/samples/sample6/template_no_fb_align.json
+++ b/samples/sample6/template_no_fb_align.json
@@ -1,21 +1,11 @@
{
- "Globals": {
- "display_width": 2480,
- "display_height": 3508
- },
- "dimensions": [
- 2480,
- 3508
- ],
- "bubbleDimensions": [
- 42,
- 42
- ],
+ "pageDimensions": [2480, 3508],
+ "bubbleDimensions": [42, 42],
"preProcessors": [
{
"name": "Levels",
"options": {
- "low": 0.70,
+ "low": 0.7,
"high": 0.8
}
},
@@ -27,137 +17,40 @@
}
}
],
- "concatenations": {
- "Roll": [
- "stu",
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6",
- "check"
- ]
+ "customLabels": {
+ "Roll": ["stu", "roll1..7", "check_1", "check_2"]
},
- "singles": [],
- "qBlocks": {
+ "fieldBlocks": {
"Check1": {
- "orig": [
- 2033,
- 1290
- ],
- "gaps": [
- 50,
- 50
- ],
- "bigGaps": [
- 20,
- 20
- ],
- "qNos": [
- [
- [
- "check"
- ]
- ]
- ],
- "vals": [
- "A",
- "B",
- "E",
- "H",
- "J",
- "L",
- "M"
- ],
- "orient": "V"
+ "origin": [2033, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["check_1"],
+ "bubbleValues": ["A", "B", "E", "H", "J", "L", "M"],
+ "direction": "vertical"
},
"Check2": {
- "orig": [
- 2083,
- 1290
- ],
- "gaps": [
- 50,
- 50
- ],
- "bigGaps": [
- 20,
- 20
- ],
- "qNos": [
- [
- [
- "check"
- ]
- ]
- ],
- "vals": [
- "N",
- "R",
- "U",
- "W",
- "X",
- "Y"
- ],
- "orient": "V"
+ "origin": [2083, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["check_2"],
+ "bubbleValues": ["N", "R", "U", "W", "X", "Y"],
+ "direction": "vertical"
},
"Stu": {
- "orig": [
- 1636,
- 1290
- ],
- "gaps": [
- 50,
- 50
- ],
- "bigGaps": [
- 20,
- 20
- ],
- "qNos": [
- [
- [
- "stu"
- ]
- ]
- ],
- "vals": [
- "U",
- "A",
- "HT",
- "GT"
- ],
- "orient": "V"
+ "origin": [1636, 1290],
+ "bubblesGap": 50,
+ "labelsGap": 50,
+ "fieldLabels": ["stu"],
+ "bubbleValues": ["U", "A", "HT", "GT"],
+ "direction": "vertical"
},
"Roll": {
- "qType": "QTYPE_ROLL",
- "orig": [
- 1685,
- 1290
- ],
- "bigGaps": [
- 115,
- 11
- ],
- "gaps": [
- 50.5,
- 50.5
- ],
- "qNos": [
- [
- [
- "roll0",
- "roll1",
- "roll2",
- "roll3",
- "roll4",
- "roll5",
- "roll6"
- ]
- ]
- ]
+ "fieldType": "QTYPE_INT",
+ "origin": [1685, 1290],
+ "bubblesGap": 50.5,
+ "labelsGap": 50.5,
+ "fieldLabels": ["roll1..7"]
}
}
}
diff --git a/src/constants.py b/src/constants.py
index 8d1a341c..f1cdc87b 100644
--- a/src/constants.py
+++ b/src/constants.py
@@ -6,7 +6,6 @@
Github: https://github.com/Udayraj123
"""
-
from dotmap import DotMap
# Filenames
@@ -14,6 +13,7 @@
EVALUATION_FILENAME = "evaluation.json"
CONFIG_FILENAME = "config.json"
+FIELD_LABEL_NUMBER_REGEX = r"([^\d]+)(\d*)"
#
ERROR_CODES = DotMap(
{
@@ -23,14 +23,22 @@
_dynamic=False,
)
-QTYPE_DATA = {
- "QTYPE_ROLL": {"vals": range(10), "orient": "V"},
- "QTYPE_INT": {"vals": range(10), "orient": "V"},
- "QTYPE_INT_11": {"vals": range(11), "orient": "V"},
- "QTYPE_MCQ4": {"vals": ["A", "B", "C", "D"], "orient": "H"},
- "QTYPE_MCQ5": {"vals": ["A", "B", "C", "D", "E"], "orient": "H"},
+FIELD_TYPES = {
+ "QTYPE_INT": {
+ "bubbleValues": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
+ "direction": "vertical",
+ },
+ "QTYPE_INT_FROM_1": {
+ "bubbleValues": ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
+ "direction": "vertical",
+ },
+ "QTYPE_MCQ4": {"bubbleValues": ["A", "B", "C", "D"], "direction": "horizontal"},
+ "QTYPE_MCQ5": {
+ "bubbleValues": ["A", "B", "C", "D", "E"],
+ "direction": "horizontal",
+ },
#
- # You can create and append custom question types here-
+ # You can create and append custom field types here-
#
}
diff --git a/src/core.py b/src/core.py
index 8297c42f..8e7e4f03 100644
--- a/src/core.py
+++ b/src/core.py
@@ -77,13 +77,13 @@ def apply_preprocessors(self, file_path, in_omr, template):
@staticmethod
def draw_template_layout(img, template, shifted=True, draw_qvals=False, border=-1):
img = ImageUtils.resize_util(
- img, template.dimensions[0], template.dimensions[1]
+ img, template.page_dimensions[0], template.page_dimensions[1]
)
final_align = img.copy()
box_w, box_h = template.bubble_dimensions
- for q_block in template.q_blocks:
- s, d = q_block.orig, q_block.dimensions
- shift = q_block.shift
+ for field_block in template.field_blocks:
+ s, d = field_block.origin, field_block.dimensions
+ shift = field_block.shift
if shifted:
cv2.rectangle(
final_align,
@@ -100,9 +100,9 @@ def draw_template_layout(img, template, shifted=True, draw_qvals=False, border=-
constants.CLR_BLACK,
3,
)
- for _, qbox_pts in q_block.traverse_pts:
- for pt in qbox_pts:
- x, y = (pt.x + q_block.shift, pt.y) if shifted else (pt.x, pt.y)
+ for field_block_bubbles in field_block.traverse_bubbles:
+ for pt in field_block_bubbles:
+ x, y = (pt.x + field_block.shift, pt.y) if shifted else (pt.x, pt.y)
cv2.rectangle(
final_align,
(int(x + box_w / 10), int(y + box_h / 10)),
@@ -123,11 +123,11 @@ def draw_template_layout(img, template, shifted=True, draw_qvals=False, border=-
)
if shifted:
text_in_px = cv2.getTextSize(
- q_block.key, cv2.FONT_HERSHEY_SIMPLEX, constants.TEXT_SIZE, 4
+ field_block.name, cv2.FONT_HERSHEY_SIMPLEX, constants.TEXT_SIZE, 4
)
cv2.putText(
final_align,
- f"{q_block.key}",
+ field_block.name,
(int(s[0] + d[0] - text_in_px[0][0]), int(s[1] - text_in_px[0][1])),
cv2.FONT_HERSHEY_SIMPLEX,
constants.TEXT_SIZE,
@@ -185,8 +185,8 @@ def get_global_threshold(
else constants.GLOBAL_PAGE_THRESHOLD_BLACK
)
- # Sort the Q vals
- # Change var name of q_vals
+ # Sort the Q bubbleValues
+ # TODO: Change var name of q_vals
q_vals = sorted(q_vals_orig)
# Find the FIRST LARGE GAP and set it as threshold:
ls = (looseness + 1) // 2
@@ -267,7 +267,7 @@ def get_local_threshold(
"""
config = self.tuning_config
- # Sort the Q vals
+ # Sort the Q bubbleValues
q_vals = sorted(q_vals)
# Small no of pts cases:
@@ -291,7 +291,7 @@ def get_local_threshold(
# no_outliers = False
# # ^Stackoverflow method
- # print(q_no, no_outliers,"qstd",round(np.std(q_vals),2), "gstd", gstd,
+ # print(field_label, no_outliers,"qstd",round(np.std(q_vals),2), "gstd", gstd,
# "Gaps in gvals",sorted([round(abs(g-gmean),2) for g in GVals],reverse=True),
# '\t',round(DISCRETION*gstd,2), L2MaxGap)
@@ -304,7 +304,7 @@ def get_local_threshold(
if jump > max1:
max1 = jump
thr1 = q_vals[i - 1] + jump / 2
- # print(q_no,q_vals,max1)
+ # print(field_label,q_vals,max1)
confident_jump = (
config.threshold_params.MIN_JUMP
@@ -347,7 +347,7 @@ def read_omr_response(self, template, image, name, save_dir=None):
img = image.copy()
# origDim = img.shape[:2]
img = ImageUtils.resize_util(
- img, template.dimensions[0], template.dimensions[1]
+ img, template.page_dimensions[0], template.page_dimensions[1]
)
if img.max() > img.min():
img = ImageUtils.normalize_util(img)
@@ -387,10 +387,10 @@ def read_omr_response(self, template, image, name, save_dir=None):
if config.outputs.show_image_level >= 5:
all_c_box_vals = {"int": [], "mcq": []}
- # ,"QTYPE_ROLL":[]}#,"QTYPE_MED":[]}
+ # TODO: simplify this logic
q_nums = {"int": [], "mcq": []}
- # Find Shifts for the q_blocks --> Before calculating threshold!
+ # Find Shifts for the field_blocks --> Before calculating threshold!
if auto_align:
# print("Begin Alignment")
# Open : erode then dilate
@@ -432,8 +432,8 @@ def read_omr_response(self, template, image, name, save_dir=None):
self.append_save_img(6, morph_v)
# template relative alignment code
- for q_block in template.q_blocks:
- s, d = q_block.orig, q_block.dimensions
+ for field_block in template.field_blocks:
+ s, d = field_block.origin, field_block.dimensions
match_col, max_steps, align_stride, thk = map(
config.alignment_params.get,
@@ -467,7 +467,7 @@ def read_omr_response(self, template, image, name, save_dir=None):
)
# For demonstration purposes-
- # if(q_block.key == "int1"):
+ # if(field_block.name == "int1"):
# ret = morph_v.copy()
# cv2.rectangle(ret,
# (s[0]+shift-thk,s[1]),
@@ -489,10 +489,10 @@ def read_omr_response(self, template, image, name, save_dir=None):
break
steps += 1
- q_block.shift = shift
- # print("Aligned q_block: ",q_block.key,"Corrected Shift:",
- # q_block.shift,", dimensions:", q_block.dimensions,
- # "orig:", q_block.orig,'\n')
+ field_block.shift = shift
+ # print("Aligned field_block: ",field_block.name,"Corrected Shift:",
+ # field_block.shift,", dimensions:", field_block.dimensions,
+ # "origin:", field_block.origin,'\n')
# print("End Alignment")
final_align = None
@@ -509,16 +509,16 @@ def read_omr_response(self, template, image, name, save_dir=None):
final_align = np.hstack((initial_align, final_align))
self.append_save_img(5, img)
- # Get mean vals n other stats
+ # Get mean bubbleValues n other stats
all_q_vals, all_q_strip_arrs, all_q_std_vals = [], [], []
total_q_strip_no = 0
- for q_block in template.q_blocks:
+ for field_block in template.field_blocks:
q_std_vals = []
- for _, qbox_pts in q_block.traverse_pts:
+ for field_block_bubbles in field_block.traverse_bubbles:
q_strip_vals = []
- for pt in qbox_pts:
+ for pt in field_block_bubbles:
# shifted
- x, y = (pt.x + q_block.shift, pt.y)
+ x, y = (pt.x + field_block.shift, pt.y)
rect = [y, y + box_h, x, x + box_w]
q_strip_vals.append(
cv2.mean(img[rect[0] : rect[1], rect[2] : rect[3]])[0]
@@ -529,9 +529,9 @@ def read_omr_response(self, template, image, name, save_dir=None):
# _, _, _ = get_global_threshold(q_strip_vals, "QStrip Plot",
# plot_show=False, sort_in_plot=True)
# hist = getPlotImg()
- # InteractionUtils.show("QStrip "+qbox_pts[0].q_no, hist, 0, 1,config=config)
+ # InteractionUtils.show("QStrip "+field_block_bubbles[0].field_label, hist, 0, 1,config=config)
all_q_vals.extend(q_strip_vals)
- # print(total_q_strip_no, qbox_pts[0].q_no, q_std_vals[len(q_std_vals)-1])
+ # print(total_q_strip_no, field_block_bubbles[0].field_label, q_std_vals[len(q_std_vals)-1])
total_q_strip_no += 1
all_q_std_vals.extend(q_std_vals)
@@ -562,52 +562,53 @@ def read_omr_response(self, template, image, name, save_dir=None):
# appendSaveImg(2,hist)
per_omr_threshold_avg, total_q_strip_no, total_q_box_no = 0, 0, 0
- non_empty_qnos = set()
- for q_block in template.q_blocks:
+ for field_block in template.field_blocks:
block_q_strip_no = 1
- shift = q_block.shift
- s, d = q_block.orig, q_block.dimensions
- key = q_block.key[:3]
+ shift = field_block.shift
+ s, d = field_block.origin, field_block.dimensions
+ key = field_block.name[:3]
# cv2.rectangle(final_marked,(s[0]+shift,s[1]),(s[0]+shift+d[0],
# s[1]+d[1]),CLR_BLACK,3)
- for _, qbox_pts in q_block.traverse_pts:
+ for field_block_bubbles in field_block.traverse_bubbles:
# All Black or All White case
no_outliers = all_q_std_vals[total_q_strip_no] < global_std_thresh
- # print(total_q_strip_no, qbox_pts[0].q_no,
+ # print(total_q_strip_no, field_block_bubbles[0].field_label,
# all_q_std_vals[total_q_strip_no], "no_outliers:", no_outliers)
per_q_strip_threshold = self.get_local_threshold(
all_q_strip_arrs[total_q_strip_no],
global_thr,
no_outliers,
- "Mean Intensity Histogram for "
- + key
- + "."
- + qbox_pts[0].q_no
- + "."
- + str(block_q_strip_no),
+ f"Mean Intensity Histogram for {key}.{field_block_bubbles[0].field_label}.{block_q_strip_no}",
config.outputs.show_image_level >= 6,
)
- # print(qbox_pts[0].q_no,key,block_q_strip_no, "THR: ",
+ # print(field_block_bubbles[0].field_label,key,block_q_strip_no, "THR: ",
# round(per_q_strip_threshold,2))
per_omr_threshold_avg += per_q_strip_threshold
# Note: Little debugging visualization - view the particular Qstrip
# if(
# 0
- # # or "q17" in (qbox_pts[0].q_no)
- # # or (qbox_pts[0].q_no+str(block_q_strip_no))=="q15"
+ # # or "q17" in (field_block_bubbles[0].field_label)
+ # # or (field_block_bubbles[0].field_label+str(block_q_strip_no))=="q15"
# ):
# st, end = qStrip
# InteractionUtils.show("QStrip: "+key+"-"+str(block_q_strip_no),
# img[st[1] : end[1], st[0]+shift : end[0]+shift],0,config=config)
- for pt in qbox_pts:
- # shifted
- x, y = (pt.x + q_block.shift, pt.y)
- boxval0 = all_q_vals[total_q_box_no]
- detected = per_q_strip_threshold > boxval0
-
- if detected:
+ # TODO: get rid of total_q_box_no
+ detected_bubbles = []
+ for bubble in field_block_bubbles:
+ bubble_is_marked = (
+ per_q_strip_threshold > all_q_vals[total_q_box_no]
+ )
+ total_q_box_no += 1
+ if bubble_is_marked:
+ detected_bubbles.append(bubble)
+ x, y, field_value = (
+ bubble.x + field_block.shift,
+ bubble.y,
+ bubble.field_value,
+ )
cv2.rectangle(
final_marked,
(int(x + box_w / 12), int(y + box_h / 12)),
@@ -618,6 +619,16 @@ def read_omr_response(self, template, image, name, save_dir=None):
constants.CLR_DARK_GRAY,
3,
)
+
+ cv2.putText(
+ final_marked,
+ str(field_value),
+ (x, y),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ constants.TEXT_SIZE,
+ (20, 20, 10),
+ int(1 + 3.5 * constants.TEXT_SIZE),
+ )
else:
cv2.rectangle(
final_marked,
@@ -630,32 +641,25 @@ def read_omr_response(self, template, image, name, save_dir=None):
-1,
)
- if detected:
- q, val = pt.q_no, str(pt.val)
- cv2.putText(
- final_marked,
- val,
- (x, y),
- cv2.FONT_HERSHEY_SIMPLEX,
- constants.TEXT_SIZE,
- (20, 20, 10),
- int(1 + 3.5 * constants.TEXT_SIZE),
- )
- # Only send rolls multi-marked in the directory
- multi_marked_l = q in omr_response
- multi_marked = multi_marked_l or multi_marked
- omr_response[q] = (
- (omr_response[q] + val) if multi_marked_l else val
- )
- non_empty_qnos.add(q)
- multi_roll = multi_marked_l and "Roll" in str(q)
- # blackVals.append(boxval0)
- # else:
- # whiteVals.append(boxval0)
+ for bubble in detected_bubbles:
+ field_label, field_value = (
+ bubble.field_label,
+ bubble.field_value,
+ )
+ # Only send rolls multi-marked in the directory
+ multi_marked_local = field_label in omr_response
+ omr_response[field_label] = (
+ (omr_response[field_label] + field_value)
+ if multi_marked_local
+ else field_value
+ )
+ # TODO: generalize this into identifier
+ # multi_roll = multi_marked_local and "Roll" in str(q)
+ multi_marked = multi_marked or multi_marked_local
- total_q_box_no += 1
- # /for qbox_pts
- # /for qStrip
+ if len(detected_bubbles) == 0:
+ field_label = field_block_bubbles[0].field_label
+ omr_response[field_label] = field_block.empty_val
if config.outputs.show_image_level >= 5:
if key in all_c_box_vals:
@@ -666,20 +670,7 @@ def read_omr_response(self, template, image, name, save_dir=None):
block_q_strip_no += 1
total_q_strip_no += 1
- # /for q_block
-
- # Populate empty responses
-
- # loop over qNos in concatentations
- for concatQ in template.concatenations:
- for q in concatQ:
- if q not in non_empty_qnos:
- omr_response[q] = q_block.empty_val
-
- # loop over qNos in singles
- for q in template.singles:
- if q not in non_empty_qnos:
- omr_response[q] = q_block.empty_val
+ # /for field_block
per_omr_threshold_avg /= total_q_strip_no
per_omr_threshold_avg = round(per_omr_threshold_avg, 2)
diff --git a/src/defaults/config.py b/src/defaults/config.py
index 2a5ae120..28b0ea34 100644
--- a/src/defaults/config.py
+++ b/src/defaults/config.py
@@ -25,7 +25,7 @@
"thickness": 3,
},
"outputs": {
- "show_image_level": 4,
+ "show_image_level": 0,
"save_image_level": 0,
"save_detections": True,
},
diff --git a/src/defaults/template.py b/src/defaults/template.py
index 8346abb8..d0a2a831 100644
--- a/src/defaults/template.py
+++ b/src/defaults/template.py
@@ -1 +1,6 @@
-TEMPLATE_DEFAULTS = {"preProcessors": [], "emptyVal": ""}
+TEMPLATE_DEFAULTS = {
+ "preProcessors": [],
+ "emptyValue": "",
+ "customLabels": {},
+ "outputColumns": [],
+}
diff --git a/src/entry.py b/src/entry.py
index d929f910..e8388e6b 100644
--- a/src/entry.py
+++ b/src/entry.py
@@ -6,7 +6,6 @@
Github: https://github.com/Udayraj123
"""
-
import os
from csv import QUOTE_NONNUMERIC
from pathlib import Path
@@ -14,13 +13,12 @@
import cv2
import pandas as pd
+from rich.table import Table
from src import constants
-from src.core import ImageInstanceOps
from src.defaults import CONFIG_DEFAULTS
from src.evaluation import EvaluationConfig, evaluate_concatenated_response
-from src.logger import logger
-from src.processors.manager import ProcessorManager
+from src.logger import console, logger
from src.template import Template
from src.utils.file import Paths, setup_dirs_for_paths, setup_outputs_for_template
from src.utils.image import ImageUtils
@@ -28,13 +26,13 @@
from src.utils.parsing import get_concatenated_response, open_config_with_defaults
# Load processors
-PROCESSOR_MANAGER = ProcessorManager()
STATS = Stats()
-def entry_point(input_dir, curr_dir, args):
+def entry_point(input_dir, args):
if not os.path.exists(input_dir):
raise Exception(f"Given input directory does not exist: '{input_dir}'")
+ curr_dir = input_dir
return process_dir(input_dir, curr_dir, args)
@@ -45,7 +43,6 @@ def process_dir(
template=None,
tuning_config=CONFIG_DEFAULTS,
evaluation_config=None,
- image_instance_ops=None,
):
# Update local tuning_config (in current recursion stack)
local_config_path = curr_dir.joinpath(constants.CONFIG_FILENAME)
@@ -56,12 +53,9 @@ def process_dir(
local_template_path = curr_dir.joinpath(constants.TEMPLATE_FILENAME)
local_template_exists = os.path.exists(local_template_path)
if local_template_exists:
- # TODO: consider moving template inside image_instance_ops as an attribute
- image_instance_ops = ImageInstanceOps(tuning_config)
template = Template(
local_template_path,
- image_instance_ops,
- PROCESSOR_MANAGER.processors,
+ tuning_config,
)
# Look for subdirectories for processing
subdirs = [d for d in curr_dir.iterdir() if d.is_dir()]
@@ -88,32 +82,34 @@ def process_dir(
of '{curr_dir}'. \nPlace {constants.TEMPLATE_FILENAME} in the \
appropriate directory."
)
- return
-
- logger.info(
- "------------------------------------------------------------------"
- )
- logger.info(f'Processing directory "{curr_dir}" with settings- ')
- logger.info(f"\t{'Total images':<22}: {len(omr_files)}")
- logger.info(
- f"\t{'Cropping Enabled':<22}: {str('CropOnMarkers' in template.pre_processors)}"
+ raise Exception(
+ f"No template file found in the directory tree of {curr_dir}"
+ )
+ logger.info("")
+ table = Table(
+ title="Current Configurations", show_header=False, show_lines=False
)
- logger.info(
- f"\t{'Auto Alignment':<22}: {tuning_config.alignment_params.auto_align}"
+ table.add_column("Key", style="cyan", no_wrap=True)
+ table.add_column("Value", style="magenta")
+ table.add_row("Directory Path", f"{curr_dir}")
+ table.add_row("Count of Images", f"{len(omr_files)}")
+ table.add_row(
+ "Markers Detection",
+ "ON" if "CropOnMarkers" in template.pre_processors else "OFF",
)
- logger.info(f"\t{'Using Template':<22}: { str(template)}")
- logger.info(
- f"\t{'Using pre-processors':<22}: {[pp.__class__.__name__ for pp in template.pre_processors]}"
+ table.add_row("Auto Alignment", f"{tuning_config.alignment_params.auto_align}")
+ table.add_row("Detected Template Path", f"{template}")
+ table.add_row(
+ "Detected pre-processors",
+ f"{[pp.__class__.__name__ for pp in template.pre_processors]}",
)
- logger.info("")
+ console.print(table, justify="center")
setup_dirs_for_paths(paths)
outputs_namespace = setup_outputs_for_template(paths, template)
if args["setLayout"]:
- show_template_layouts(
- omr_files, image_instance_ops, template, tuning_config
- )
+ show_template_layouts(omr_files, template, tuning_config)
else:
local_evaluation_path = curr_dir.joinpath(constants.EVALUATION_FILENAME)
if os.path.exists(local_evaluation_path):
@@ -122,7 +118,7 @@ def process_dir(
f"Found an evaluation file without a parent template file: {local_evaluation_path}"
)
evaluation_config = EvaluationConfig(
- local_evaluation_path, template, image_instance_ops, curr_dir
+ local_evaluation_path, template, curr_dir
)
excluded_files.extend(
@@ -137,7 +133,6 @@ def process_dir(
tuning_config,
evaluation_config,
outputs_namespace,
- image_instance_ops,
)
elif not subdirs:
@@ -156,17 +151,18 @@ def process_dir(
template,
tuning_config,
evaluation_config,
- image_instance_ops,
)
-def show_template_layouts(omr_files, image_instance_ops, template, tuning_config):
+def show_template_layouts(omr_files, template, tuning_config):
for file_path in omr_files:
file_name = file_path.name
file_path = str(file_path)
in_omr = cv2.imread(file_path, cv2.IMREAD_GRAYSCALE)
- in_omr = image_instance_ops.apply_preprocessors(file_path, in_omr, template)
- template_layout = image_instance_ops.draw_template_layout(
+ in_omr = template.image_instance_ops.apply_preprocessors(
+ file_path, in_omr, template
+ )
+ template_layout = template.image_instance_ops.draw_template_layout(
in_omr, template, shifted=False, border=2
)
InteractionUtils.show(
@@ -180,7 +176,6 @@ def process_files(
tuning_config,
evaluation_config,
outputs_namespace,
- image_instance_ops,
):
start_time = int(time())
files_counter = 0
@@ -197,11 +192,13 @@ def process_files(
f"({files_counter}) Opening image: \t'{file_path}'\tResolution: {in_omr.shape}"
)
- image_instance_ops.reset_all_save_img()
+ template.image_instance_ops.reset_all_save_img()
- image_instance_ops.append_save_img(1, in_omr)
+ template.image_instance_ops.append_save_img(1, in_omr)
- in_omr = image_instance_ops.apply_preprocessors(file_path, in_omr, template)
+ in_omr = template.image_instance_ops.apply_preprocessors(
+ file_path, in_omr, template
+ )
if in_omr is None:
# Error OMR case
@@ -235,7 +232,7 @@ def process_files(
final_marked,
multi_marked,
_,
- ) = image_instance_ops.read_omr_response(
+ ) = template.image_instance_ops.read_omr_response(
template, image=in_omr, name=file_id, save_dir=save_dir
)
@@ -270,7 +267,7 @@ def process_files(
)
resp_array = []
- for k in outputs_namespace.resp_cols:
+ for k in template.output_columns:
resp_array.append(omr_response[k])
outputs_namespace.OUTPUT_SET.append([file_name] + resp_array)
@@ -311,7 +308,7 @@ def process_files(
def print_stats(start_time, files_counter, tuning_config):
- time_checking = round(time() - start_time, 2) if files_counter else 1
+ time_checking = max(1, round(time() - start_time, 2))
log = logger.info
log("")
log(f"{'Total file(s) moved':<27}: {STATS.files_moved}")
diff --git a/src/evaluation.py b/src/evaluation.py
index ce9b1178..e8feb376 100644
--- a/src/evaluation.py
+++ b/src/evaluation.py
@@ -2,28 +2,23 @@
import os
import re
from copy import deepcopy
-from fractions import Fraction
import cv2
import pandas as pd
from rich.table import Table
from src.logger import console, logger
-from src.schemas.evaluation_schema import (
+from src.schemas.constants import (
BONUS_SECTION_PREFIX,
DEFAULT_SECTION_KEY,
MARKING_VERDICT_TYPES,
- QUESTION_STRING_REGEX_GROUPS,
)
-from src.utils.parsing import get_concatenated_response, open_evaluation_with_validation
-
-
-def parse_float_or_fraction(result):
- if type(result) == str and "/" in result:
- result = float(Fraction(result))
- else:
- result = float(result)
- return result
+from src.utils.parsing import (
+ get_concatenated_response,
+ open_evaluation_with_validation,
+ parse_fields,
+ parse_float_or_fraction,
+)
class AnswerMatcher:
@@ -60,13 +55,6 @@ def get_answer_type(self, answer_item):
and type(answer_item[1]) == list
):
return "multi-weighted"
- # TODO: add more conditions here
- # elif (
- # len(answer_item) == 3
- # and type(answer_item[0]) == list
- # and type(answer_item[1]) == list
- # ):
- # return "multi-weighted"
else:
logger.critical(
f"Unable to determine answer type for answer item: {answer_item}"
@@ -90,13 +78,11 @@ def set_defaults_from_scheme(self, marking_scheme):
for allowed_answer in parsed_answer:
self.marking[f"correct-{allowed_answer}"] = self.marking["correct"]
elif answer_type == "multi-weighted":
- # TODO: think about streaks in multi-weighted scenario (or invalidate such cases)
custom_marking = list(map(parse_float_or_fraction, parsed_answer[1]))
verdict_types_length = min(len(MARKING_VERDICT_TYPES), len(custom_marking))
# override the given marking
for i in range(verdict_types_length):
verdict_type = MARKING_VERDICT_TYPES[i]
- # Note: copies over the marking regardless of streak or not -
self.marking[verdict_type] = custom_marking[i]
if type(parsed_answer[0] == str):
@@ -154,128 +140,48 @@ def __init__(self, section_key, section_scheme, empty_val):
# TODO: get local empty_val from qblock
self.empty_val = empty_val
self.section_key = section_key
- self.has_streak_scheme = False
- self.reset_side_effects()
# DEFAULT marking scheme follows a shorthand
if section_key == DEFAULT_SECTION_KEY:
self.questions = None
self.marking = self.parse_scheme_marking(section_scheme)
- if self.has_streak_scheme:
- raise Exception(
- f"Default schema '{DEFAULT_SECTION_KEY}' cannot have streak marking. Create a new section and specify questions range in it."
- )
-
else:
- self.questions = self.parse_questions(
- section_key, section_scheme["questions"]
- )
+ self.questions = parse_fields(section_key, section_scheme["questions"])
self.marking = self.parse_scheme_marking(section_scheme["marking"])
- def reset_side_effects(self):
- self.streaks = {
- "correct": 0,
- "incorrect": 0,
- "unmarked": 0,
- }
-
def parse_scheme_marking(self, marking):
parsed_marking = {}
for verdict_type in MARKING_VERDICT_TYPES:
- verdict_marking = marking[verdict_type]
- if type(verdict_marking) == str:
- verdict_marking = parse_float_or_fraction(verdict_marking)
- section_key = self.section_key
- if (
- verdict_marking > 0
- and verdict_type == "incorrect"
- and not section_key.startswith(BONUS_SECTION_PREFIX)
- ):
- logger.warning(
- f"Found positive marks({round(verdict_marking, 2)}) for incorrect answer in the schema '{section_key}'. For Bonus sections, add a prefix 'BONUS_' to them."
- )
- elif type(verdict_marking) == list:
- self.has_streak_scheme = True
- verdict_marking = list(map(parse_float_or_fraction, verdict_marking))
-
+ verdict_marking = parse_float_or_fraction(marking[verdict_type])
+ if (
+ verdict_marking > 0
+ and verdict_type == "incorrect"
+ and not self.section_key.startswith(BONUS_SECTION_PREFIX)
+ ):
+ logger.warning(
+ f"Found positive marks({round(verdict_marking, 2)}) for incorrect answer in the schema '{self.section_key}'. For Bonus sections, add a prefix 'BONUS_' to them."
+ )
parsed_marking[verdict_type] = verdict_marking
return parsed_marking
- def get_has_streak_scheme(self):
- return self.has_streak_scheme
-
- @staticmethod
- def parse_questions(key, questions):
- parsed_questions = []
- questions_set = set()
- for question_string in questions:
- questions_array = SectionMarkingScheme.parse_question_string(
- question_string
- )
- current_set = set(questions_array)
- if not questions_set.isdisjoint(current_set):
- raise Exception(
- f"Given question string '{question_string}' has overlapping question(s) with other questions in '{key}': {questions}"
- )
- parsed_questions.extend(questions_array)
- return parsed_questions
-
- @staticmethod
- def parse_question_string(question_string):
- if "." in question_string:
- question_prefix, start, end = re.findall(
- QUESTION_STRING_REGEX_GROUPS, question_string
- )[0]
- start, end = int(start), int(end)
- if start >= end:
- raise Exception(
- f"Invalid range in question string: '{question_string}', start: {start} is not less than end: {end}"
- )
- return [
- f"{question_prefix}{question_number}"
- for question_number in range(start, end + 1)
- ]
- else:
- return [question_string]
-
def match_answer(self, marked_answer, answer_matcher):
question_verdict, verdict_marking = answer_matcher.get_verdict_marking(
marked_answer
)
- if type(verdict_marking) == list:
- current_streak = self.streaks[question_verdict]
- delta = verdict_marking[min(current_streak, len(verdict_marking) - 1)]
- else:
- delta = verdict_marking
-
- if question_verdict in MARKING_VERDICT_TYPES: # TODO: refine this check
- self.update_streaks_for_verdict(question_verdict)
-
- return delta, question_verdict
-
- def update_streaks_for_verdict(self, question_verdict):
- current_streak = self.streaks[question_verdict]
- for verdict_type in MARKING_VERDICT_TYPES:
- if question_verdict == verdict_type:
- # increase current streak
- self.streaks[verdict_type] = current_streak + 1
- else:
- # reset other streaks
- self.streaks[verdict_type] = 0
+ return verdict_marking, question_verdict
class EvaluationConfig:
"""Note: this instance will be reused for multiple omr sheets"""
- def __init__(self, local_evaluation_path, template, image_instance_ops, curr_dir):
+ def __init__(self, local_evaluation_path, template, curr_dir):
evaluation_json = open_evaluation_with_validation(local_evaluation_path)
options, marking_scheme, source_type = map(
evaluation_json.get, ["options", "marking_scheme", "source_type"]
)
self.should_explain_scoring = options.get("should_explain_scoring", False)
self.has_non_default_section = False
- self.has_streak_scheme = False
self.exclude_files = []
marking_scheme = marking_scheme
@@ -311,7 +217,7 @@ def __init__(self, local_evaluation_path, template, image_instance_ops, curr_dir
)
# TODO: use a common function for below changes?
in_omr = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
- in_omr = image_instance_ops.apply_preprocessors(
+ in_omr = template.image_instance_ops.apply_preprocessors(
image_path, in_omr, template
)
if in_omr is None:
@@ -323,7 +229,7 @@ def __init__(self, local_evaluation_path, template, image_instance_ops, curr_dir
_final_marked,
_multi_marked,
_multi_roll,
- ) = image_instance_ops.read_omr_response(
+ ) = template.image_instance_ops.read_omr_response(
template,
image=in_omr,
name=image_path,
@@ -374,7 +280,7 @@ def __init__(self, local_evaluation_path, template, image_instance_ops, curr_dir
self.validate_questions(answers_in_order)
self.marking_scheme, self.question_to_scheme = {}, {}
- for (section_key, section_scheme) in marking_scheme.items():
+ for section_key, section_scheme in marking_scheme.items():
section_marking_scheme = SectionMarkingScheme(
section_key, section_scheme, template.global_empty_val
)
@@ -387,9 +293,6 @@ def __init__(self, local_evaluation_path, template, image_instance_ops, curr_dir
else:
self.default_marking_scheme = section_marking_scheme
- if section_marking_scheme.get_has_streak_scheme():
- self.has_streak_scheme = True
-
self.validate_marking_scheme()
self.question_to_answer_matcher = self.parse_answers_and_map_questions(
@@ -411,15 +314,11 @@ def get_marking_scheme_for_question(self, question):
def match_answer_for_question(self, current_score, question, marked_answer):
answer_matcher = self.question_to_answer_matcher[question]
- question_marking_scheme = answer_matcher.get_marking_scheme()
- delta, question_verdict = question_marking_scheme.match_answer(
- marked_answer, answer_matcher
- )
+ question_verdict, delta = answer_matcher.get_verdict_marking(marked_answer)
self.conditionally_add_explanation(
answer_matcher,
delta,
marked_answer,
- question_marking_scheme,
question_verdict,
question,
current_score,
@@ -448,7 +347,7 @@ def validate_questions(self, answers_in_order):
def validate_marking_scheme(self):
marking_scheme = self.marking_scheme
section_questions = set()
- for (section_key, section_scheme) in marking_scheme.items():
+ for section_key, section_scheme in marking_scheme.items():
if section_key == DEFAULT_SECTION_KEY:
continue
current_set = set(section_scheme.questions)
@@ -457,16 +356,6 @@ def validate_marking_scheme(self):
f"Section '{section_key}' has overlapping question(s) with other sections"
)
section_questions = section_questions.union(current_set)
- questions_count = len(section_scheme.questions)
- for verdict_type in MARKING_VERDICT_TYPES:
- marking = section_scheme.marking[verdict_type]
- if type(marking) == list and questions_count > len(marking):
- logger.critical(
- f"Section '{section_key}' with {questions_count} questions is more than the capacity for '{verdict_type}' marking with {len(marking)} scores."
- )
- raise Exception(
- f"Section '{section_key}' has more questions than streak for '{verdict_type}' type"
- )
all_questions = set(self.questions_in_order)
missing_questions = sorted(section_questions.difference(all_questions))
@@ -478,7 +367,6 @@ def validate_marking_scheme(self):
def prepare_and_validate_omr_response(self, omr_response):
self.reset_explanation_table()
- self.reset_sections()
omr_response_questions = set(omr_response.keys())
all_questions = set(self.questions_in_order)
@@ -502,7 +390,7 @@ def prepare_and_validate_omr_response(self, omr_response):
def parse_answers_and_map_questions(self, answers_in_order):
question_to_answer_matcher = {}
- for (question, answer_item) in zip(self.questions_in_order, answers_in_order):
+ for question, answer_item in zip(self.questions_in_order, answers_in_order):
section_marking_scheme = self.get_marking_scheme_for_question(question)
question_to_answer_matcher[question] = AnswerMatcher(
answer_item, section_marking_scheme
@@ -510,16 +398,13 @@ def parse_answers_and_map_questions(self, answers_in_order):
return question_to_answer_matcher
def parse_questions_in_order(self, questions_in_order):
- return SectionMarkingScheme.parse_questions(
- "questions_in_order", questions_in_order
- )
+ return parse_fields("questions_in_order", questions_in_order)
def prepare_explanation_table(self):
- # TODO: provide a way to export this as csv
-
+ # TODO: provide a way to export this as csv/pdf
if not self.should_explain_scoring:
return
- table = Table(show_lines=True)
+ table = Table(title="Evaluation Explanation Table", show_lines=True)
table.add_column("Question")
table.add_column("Marked")
table.add_column("Answer(s)")
@@ -529,8 +414,6 @@ def prepare_explanation_table(self):
# TODO: Add max and min score in explanation (row-wise and total)
if self.has_non_default_section:
table.add_column("Section")
- if self.has_streak_scheme:
- table.add_column("Section Streak", justify="right")
self.explanation_table = table
def conditionally_add_explanation(
@@ -538,7 +421,6 @@ def conditionally_add_explanation(
answer_matcher,
delta,
marked_answer,
- question_marking_scheme,
question_verdict,
question,
current_score,
@@ -558,13 +440,6 @@ def conditionally_add_explanation(
answer_matcher.get_section_explanation()
if self.has_non_default_section
else None,
- (
- str(question_marking_scheme.streaks[question_verdict])
- if question_verdict in question_marking_scheme.streaks
- else "custom"
- )
- if self.has_streak_scheme
- else None,
]
if item is not None
]
@@ -578,10 +453,6 @@ def reset_explanation_table(self):
self.explanation_table = None
self.prepare_explanation_table()
- def reset_sections(self):
- for section_scheme in self.marking_scheme.values():
- section_scheme.reset_side_effects()
-
def evaluate_concatenated_response(concatenated_response, evaluation_config):
evaluation_config.prepare_and_validate_omr_response(concatenated_response)
diff --git a/src/processors/CropOnMarkers.py b/src/processors/CropOnMarkers.py
index 1b7894ca..cf705c6d 100644
--- a/src/processors/CropOnMarkers.py
+++ b/src/processors/CropOnMarkers.py
@@ -138,11 +138,11 @@ def apply_filter(self, image, file_path):
_h, w = optimal_marker.shape[:2]
centres = []
sum_t, max_t = 0, 0
- logger.info("Matching Marker:\t", end=" ")
+ quarter_match_log = "Matching Marker: "
for k in range(0, 4):
res = cv2.matchTemplate(quads[k], optimal_marker, cv2.TM_CCOEFF_NORMED)
max_t = res.max()
- logger.info(f"Quarter{str(k + 1)}: {str(round(max_t, 3))} ", end="\t")
+ quarter_match_log += f"Quarter{str(k + 1)}: {str(round(max_t, 3))}\t"
if (
max_t < self.min_matching_threshold
or abs(all_max_t - max_t) >= self.max_matching_variation
@@ -193,6 +193,8 @@ def apply_filter(self, image, file_path):
)
centres.append([pt[0] + w / 2, pt[1] + _h / 2])
sum_t += max_t
+
+ logger.info(quarter_match_log)
logger.info(f"Optimal Scale: {best_scale}")
# analysis data
self.threshold_circles.append(sum_t / 4)
diff --git a/src/processors/CropPage.py b/src/processors/CropPage.py
index 07f332a9..2ed86106 100644
--- a/src/processors/CropPage.py
+++ b/src/processors/CropPage.py
@@ -1,8 +1,6 @@
"""
https://www.pyimagesearch.com/2015/04/06/zero-parameter-automatic-canny-edge-detection-with-python-and-opencv/
"""
-
-
import cv2
import numpy as np
@@ -95,12 +93,11 @@ def find_page(self, image, file_path):
return sheet
def apply_filter(self, image, file_path):
- # TODO: Take this out into separate preprocessor
image = normalize(cv2.GaussianBlur(image, (3, 3), 0))
# Resize should be done with another preprocessor is needed
sheet = self.find_page(image, file_path)
- if sheet == []:
+ if len(sheet) == 0:
logger.error(
f"\tError: Paper boundary not found for: '{file_path}'\nHave you accidentally included CropPage preprocessor?"
)
diff --git a/src/processors/FeatureBasedAlignment.py b/src/processors/FeatureBasedAlignment.py
index 734cfd17..62f470af 100644
--- a/src/processors/FeatureBasedAlignment.py
+++ b/src/processors/FeatureBasedAlignment.py
@@ -2,7 +2,6 @@
Image based feature alignment
Credits: https://www.learnopencv.com/image-alignment-feature-based-using-opencv-c-python/
"""
-
import cv2
import numpy as np
diff --git a/src/processors/manager.py b/src/processors/manager.py
index 661be55b..0a4e81f3 100644
--- a/src/processors/manager.py
+++ b/src/processors/manager.py
@@ -2,7 +2,6 @@
Processor/Extension framework
Adapated from https://github.com/gdiepen/python_processor_example
"""
-
import inspect
import pkgutil
@@ -75,3 +74,7 @@ def walk_package(self, package):
loaded_packages.append(c.__name__)
logger.info(f"Loaded processors: {loaded_packages}")
+
+
+# Singleton export
+PROCESSOR_MANAGER = ProcessorManager()
diff --git a/src/schemas/config_schema.py b/src/schemas/config_schema.py
index 830fbfa0..d9bd6560 100644
--- a/src/schemas/config_schema.py
+++ b/src/schemas/config_schema.py
@@ -1,8 +1,8 @@
CONFIG_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/Udayraj123/OMRChecker/tree/master/src/schemas/config-schema.json",
- "title": "Evaluation Schema",
- "description": "OMRChecker evaluation schema i.e. the marking scheme",
+ "title": "Config Schema",
+ "description": "OMRChecker config schema for custom tuning",
"type": "object",
"additionalProperties": False,
"properties": {
diff --git a/src/schemas/constants.py b/src/schemas/constants.py
new file mode 100644
index 00000000..ccaa6f38
--- /dev/null
+++ b/src/schemas/constants.py
@@ -0,0 +1,17 @@
+DEFAULT_SECTION_KEY = "DEFAULT"
+
+BONUS_SECTION_PREFIX = "BONUS"
+
+MARKING_VERDICT_TYPES = ["correct", "incorrect", "unmarked"]
+
+ARRAY_OF_STRINGS = {
+ "type": "array",
+ "items": {"type": "string"},
+}
+
+FIELD_STRING_TYPE = {
+ "type": "string",
+ "pattern": "^([^\\.]+|[^\\.\\d]+\\d+\\.{2,3}\\d+)$",
+}
+
+FIELD_STRING_REGEX_GROUPS = r"([^\.\d]+)(\d+)\.{2,3}(\d+)"
diff --git a/src/schemas/evaluation_schema.py b/src/schemas/evaluation_schema.py
index 69a20e3a..eb29d3a6 100644
--- a/src/schemas/evaluation_schema.py
+++ b/src/schemas/evaluation_schema.py
@@ -1,34 +1,27 @@
-DEFAULT_SECTION_KEY = "DEFAULT"
-BONUS_SECTION_PREFIX = "BONUS"
-MARKING_VERDICT_TYPES = ["correct", "incorrect", "unmarked"]
-array_of_strings = {
- "type": "array",
- "items": {"type": "string"},
-}
+from src.schemas.constants import (
+ ARRAY_OF_STRINGS,
+ DEFAULT_SECTION_KEY,
+ FIELD_STRING_TYPE,
+)
+
marking_score = {
"oneOf": [
{"type": "string", "pattern": "-?(\\d+)(/(\\d+))?"},
{"type": "number"},
]
}
-marking_score_or_streak_array = {
- "oneOf": [marking_score, {"type": "array", "items": marking_score}]
-}
marking_object_properties = {
"additionalProperties": False,
"required": ["correct", "incorrect", "unmarked"],
"type": "object",
"properties": {
- "correct": marking_score_or_streak_array,
- "incorrect": marking_score_or_streak_array,
- "unmarked": marking_score_or_streak_array,
+ "correct": marking_score,
+ "incorrect": marking_score,
+ "unmarked": marking_score,
},
}
-question_string_pattern = "^([^\\.]+)*?([^\\.\\d]+(\\d+)\\.{2,3}(\\d+))*?$"
-QUESTION_STRING_REGEX_GROUPS = r"([^\.\d]+)(\d+)\.{2,3}(\d+)"
-
EVALUATION_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/Udayraj123/OMRChecker/tree/master/src/schemas/evaluation-schema.json",
@@ -53,14 +46,11 @@
"properties": {
"questions": {
"oneOf": [
+ FIELD_STRING_TYPE,
{
"type": "array",
- "items": {
- "type": "string",
- "pattern": question_string_pattern,
- },
+ "items": FIELD_STRING_TYPE,
},
- {"type": "string", "pattern": question_string_pattern},
]
},
"marking": marking_object_properties,
@@ -88,7 +78,7 @@
"should_explain_scoring": {"type": "boolean"},
"answer_key_csv_path": {"type": "string"},
"answer_key_image_path": {"type": "string"},
- "questions_in_order": array_of_strings,
+ "questions_in_order": ARRAY_OF_STRINGS,
},
}
}
@@ -128,7 +118,7 @@
{"type": "string"},
{
"type": "array",
- "items": marking_score_or_streak_array,
+ "items": marking_score,
"minItems": 1,
"maxItems": 3,
},
@@ -152,7 +142,7 @@
},
{
"type": "array",
- "items": marking_score_or_streak_array,
+ "items": marking_score,
"minItems": 1,
"maxItems": 3,
},
@@ -160,7 +150,7 @@
},
]
},
- "questions_in_order": array_of_strings,
+ "questions_in_order": ARRAY_OF_STRINGS,
},
}
}
diff --git a/src/schemas/template_schema.py b/src/schemas/template_schema.py
index 9000f7ee..451d499d 100644
--- a/src/schemas/template_schema.py
+++ b/src/schemas/template_schema.py
@@ -1,3 +1,32 @@
+from src.constants import FIELD_TYPES
+from src.schemas.constants import ARRAY_OF_STRINGS, FIELD_STRING_TYPE
+
+positive_number = {"type": "number", "minimum": 0}
+positive_integer = {"type": "integer", "minimum": 0}
+two_positive_integers = {
+ "type": "array",
+ "prefixItems": [
+ positive_integer,
+ positive_integer,
+ ],
+ "maxItems": 2,
+ "minItems": 2,
+}
+two_positive_numbers = {
+ "type": "array",
+ "prefixItems": [
+ positive_number,
+ positive_number,
+ ],
+ "maxItems": 2,
+ "minItems": 2,
+}
+zero_to_one_number = {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+}
+
TEMPLATE_SCHEMA = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/Udayraj123/OMRChecker/tree/master/src/schemas/template-schema.json",
@@ -5,26 +34,32 @@
"description": "OMRChecker input template schema",
"type": "object",
"required": [
- "dimensions",
"bubbleDimensions",
- "concatenations",
- "singles",
+ "pageDimensions",
"preProcessors",
+ "fieldBlocks",
],
+ "additionalProperties": False,
"properties": {
- "dimensions": {
- "description": "The dimensions to which each input image will be resized to before processing",
- "type": "array",
- "items": {"type": "integer"},
- "minItems": 2,
- "maxItems": 2,
- },
"bubbleDimensions": {
- "description": "The dimensions of the overlay bubble area",
+ **two_positive_integers,
+ "description": "The dimensions of the overlay bubble area: [width, height]",
+ },
+ "customLabels": {
+ "description": "The customLabels contain fields that need to be joined together before generating the results sheet",
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {"type": "array", "items": FIELD_STRING_TYPE}
+ },
+ },
+ "outputColumns": {
"type": "array",
- "items": {"type": "integer"},
- "minItems": 2,
- "maxItems": 2,
+ "items": FIELD_STRING_TYPE,
+ "description": "The ordered list of columns to be contained in the output csv(default order: alphabetical)",
+ },
+ "pageDimensions": {
+ **two_positive_integers,
+ "description": "The dimensions(width, height) to which the page will be resized to before applying template",
},
"preProcessors": {
"description": "Custom configuration values to use in the template's directory",
@@ -33,18 +68,16 @@
"type": "object",
"properties": {
"name": {
+ "type": "string",
"enum": [
"CropOnMarkers",
"CropPage",
"FeatureBasedAlignment",
- "ReadBarcode",
"GaussianBlur",
"Levels",
"MedianBlur",
],
- "type": "string",
},
- "options": {"type": "object"},
},
"required": ["name", "options"],
"allOf": [
@@ -56,20 +89,12 @@
"type": "object",
"additionalProperties": False,
"properties": {
- "relativePath": {"type": "string"},
- "min_matching_threshold": {"type": "number"},
- "max_matching_variation": {"type": "number"},
- "marker_rescale_range": {
- "type": "array",
- "prefixItems": [
- {"type": "integer"},
- {"type": "integer"},
- ],
- "maxItems": 2,
- "minItems": 2,
- },
- "marker_rescale_steps": {"type": "integer"},
"apply_erode_subtract": {"type": "boolean"},
+ "marker_rescale_range": two_positive_numbers,
+ "marker_rescale_steps": {"type": "number"},
+ "max_matching_variation": {"type": "number"},
+ "min_matching_threshold": {"type": "number"},
+ "relativePath": {"type": "string"},
"sheetToMarkerWidthRatio": {"type": "number"},
},
"required": ["relativePath"],
@@ -87,10 +112,10 @@
"type": "object",
"additionalProperties": False,
"properties": {
- "reference": {"type": "string"},
- "maxFeatures": {"type": "integer"},
- "goodMatchPercent": {"type": "number"},
"2d": {"type": "boolean"},
+ "goodMatchPercent": {"type": "number"},
+ "maxFeatures": {"type": "integer"},
+ "reference": {"type": "string"},
},
"required": ["reference"],
}
@@ -105,21 +130,9 @@
"type": "object",
"additionalProperties": False,
"properties": {
- "low": {
- "type": "number",
- "minimum": 0,
- "maximum": 1,
- },
- "high": {
- "type": "number",
- "minimum": 0,
- "maximum": 1,
- },
- "gamma": {
- "type": "number",
- "minimum": 0,
- "maximum": 1,
- },
+ "gamma": zero_to_one_number,
+ "high": zero_to_one_number,
+ "low": zero_to_one_number,
},
}
}
@@ -145,15 +158,7 @@
"type": "object",
"additionalProperties": False,
"properties": {
- "kSize": {
- "type": "array",
- "prefixItems": [
- {"type": "integer"},
- {"type": "integer"},
- ],
- "maxItems": 2,
- "minItems": 2,
- },
+ "kSize": two_positive_integers,
"sigmaX": {"type": "number"},
},
}
@@ -168,15 +173,7 @@
"type": "object",
"additionalProperties": False,
"properties": {
- "morphKernel": {
- "type": "array",
- "prefixItems": [
- {"type": "integer"},
- {"type": "integer"},
- ],
- "maxItems": 2,
- "minItems": 2,
- }
+ "morphKernel": two_positive_integers
},
}
}
@@ -185,15 +182,43 @@
],
},
},
- "concatenations": {
- "description": "The Concatenations parameter is a way to tell OMRChecker which fields need to be joined together before outputting into the csv"
- },
- "singles": {
- "description": "The remaining fields(in order) whose readings shall be forwarded directly in the output csv",
- "type": "array",
- "items": {"type": "string"},
+ "fieldBlocks": {
+ "description": "The fieldBlocks denote small groups of adjacent fields",
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "type": "object",
+ "required": [
+ "origin",
+ "bubblesGap",
+ "labelsGap",
+ "fieldLabels",
+ ],
+ "oneOf": [
+ {"required": ["fieldType"]},
+ {"required": ["bubbleValues", "direction"]},
+ ],
+ "properties": {
+ "bubbleDimensions": two_positive_numbers,
+ "bubblesGap": positive_number,
+ "bubbleValues": ARRAY_OF_STRINGS,
+ "direction": {
+ "type": "string",
+ "enum": ["horizontal", "vertical"],
+ },
+ "emptyValue": {"type": "string"},
+ "fieldLabels": {"type": "array", "items": FIELD_STRING_TYPE},
+ "labelsGap": positive_number,
+ "origin": two_positive_integers,
+ "fieldType": {
+ "type": "string",
+ "enum": list(FIELD_TYPES.keys()),
+ },
+ },
+ }
+ },
},
- "emptyVal": {
+ "emptyValue": {
"description": "The value to be used in case of empty bubble detected at global level.",
"type": "string",
},
diff --git a/src/template.py b/src/template.py
index a8fea77c..214e63c6 100644
--- a/src/template.py
+++ b/src/template.py
@@ -6,207 +6,321 @@
Github: https://github.com/Udayraj123
"""
-
-import numpy as np
-
-from src.constants import QTYPE_DATA
-from src.utils.parsing import OVERRIDE_MERGER, open_template_with_defaults
-
-
-# Coordinates Part
-class Pt:
+from src.constants import FIELD_TYPES
+from src.core import ImageInstanceOps
+from src.logger import logger
+from src.processors.manager import PROCESSOR_MANAGER
+from src.utils.parsing import (
+ custom_sort_output_columns,
+ open_template_with_defaults,
+ parse_fields,
+)
+
+
+class Bubble:
"""
Container for a Point Box on the OMR
- q_no is the point's property- question to which this point belongs to
+ field_label is the point's property- field to which this point belongs to
It can be used as a roll number column as well. (eg roll1)
It can also correspond to a single digit of integer type Q (eg q5d1)
"""
- def __init__(self, pt, q_no, q_type, val):
+ def __init__(self, pt, field_label, field_type, field_value):
self.x = round(pt[0])
self.y = round(pt[1])
- self.q_no = q_no
- self.q_type = q_type
- self.val = val
-
-
-class QBlock:
- def __init__(self, dimensions, key, orig, traverse_pts, empty_val):
- self.dimensions = tuple(round(x) for x in dimensions)
- self.key = key
- self.orig = orig
- self.traverse_pts = traverse_pts
- self.empty_val = empty_val
+ self.field_label = field_label
+ self.field_type = field_type
+ self.field_value = field_value
+
+ def __str__(self):
+ return str([self.x, self.y])
+
+
+class FieldBlock:
+ def __init__(self, block_name, field_block_object):
+ self.name = block_name
self.shift = 0
+ self.setup_field_block(field_block_object)
+
+ def setup_field_block(self, field_block_object):
+ # case mapping
+ (
+ bubble_dimensions,
+ bubble_values,
+ bubbles_gap,
+ direction,
+ field_labels,
+ field_type,
+ labels_gap,
+ origin,
+ self.empty_val,
+ ) = map(
+ field_block_object.get,
+ [
+ "bubbleDimensions",
+ "bubbleValues",
+ "bubblesGap",
+ "direction",
+ "fieldLabels",
+ "fieldType",
+ "labelsGap",
+ "origin",
+ "emptyValue",
+ ],
+ )
+ self.parsed_field_labels = parse_fields(
+ f"Field Block Labels: {self.name}", field_labels
+ )
+ self.origin = origin
+ self.calculate_block_dimensions(
+ bubble_dimensions,
+ bubble_values,
+ bubbles_gap,
+ direction,
+ labels_gap,
+ )
+ self.generate_bubble_grid(
+ bubble_values,
+ bubbles_gap,
+ direction,
+ field_type,
+ labels_gap,
+ )
+
+ def calculate_block_dimensions(
+ self,
+ bubble_dimensions,
+ bubble_values,
+ bubbles_gap,
+ direction,
+ labels_gap,
+ ):
+ _h, _v = (1, 0) if (direction == "vertical") else (0, 1)
+
+ values_dimension = int(
+ bubbles_gap * (len(bubble_values) - 1) + bubble_dimensions[_h]
+ )
+ fields_dimension = int(
+ labels_gap * (len(self.parsed_field_labels) - 1) + bubble_dimensions[_v]
+ )
+ self.dimensions = (
+ [fields_dimension, values_dimension]
+ if (direction == "vertical")
+ else [values_dimension, fields_dimension]
+ )
+
+ def generate_bubble_grid(
+ self,
+ bubble_values,
+ bubbles_gap,
+ direction,
+ field_type,
+ labels_gap,
+ ):
+ _h, _v = (1, 0) if (direction == "vertical") else (0, 1)
+ self.traverse_bubbles = []
+ # Generate the bubble grid
+ lead_point = [float(self.origin[0]), float(self.origin[1])]
+ for field_label in self.parsed_field_labels:
+ bubble_point = lead_point.copy()
+ field_bubbles = []
+ for bubble_value in bubble_values:
+ field_bubbles.append(
+ Bubble(bubble_point.copy(), field_label, field_type, bubble_value)
+ )
+ bubble_point[_h] += bubbles_gap
+ self.traverse_bubbles.append(field_bubbles)
+ lead_point[_v] += labels_gap
class Template:
- def __init__(self, template_path, image_instance_ops, extensions):
- json_obj = open_template_with_defaults(template_path)
- self.q_blocks, self.path = [], template_path
+ def __init__(self, template_path, tuning_config):
+ self.path = template_path
+ self.image_instance_ops = ImageInstanceOps(tuning_config)
+
+ json_object = open_template_with_defaults(template_path)
(
- self.dimensions,
- self.global_empty_val,
+ custom_labels_object,
+ field_blocks_object,
+ output_columns_array,
+ pre_processors_object,
self.bubble_dimensions,
- self.concatenations,
- self.singles,
+ self.global_empty_val,
+ self.options,
+ self.page_dimensions,
) = map(
- json_obj.get,
- ["dimensions", "emptyVal", "bubbleDimensions", "concatenations", "singles"],
+ json_object.get,
+ [
+ "customLabels",
+ "fieldBlocks",
+ "outputColumns",
+ "preProcessors",
+ "bubbleDimensions",
+ "emptyValue",
+ "options",
+ "pageDimensions",
+ ],
)
- # Add new qTypes from template
- if "qTypes" in json_obj:
- QTYPE_DATA.update(json_obj["qTypes"])
+ self.parse_output_columns(output_columns_array)
+ self.setup_pre_processors(pre_processors_object, template_path.parent)
+ self.setup_field_blocks(field_blocks_object)
+ self.parse_custom_labels(custom_labels_object)
+
+ non_custom_columns, all_custom_columns = (
+ list(self.non_custom_labels),
+ list(custom_labels_object.keys()),
+ )
+
+ if len(self.output_columns) == 0:
+ self.fill_output_columns(non_custom_columns, all_custom_columns)
+
+ self.validate_template_columns(non_custom_columns, all_custom_columns)
+ def setup_pre_processors(self, pre_processors_object, relative_dir):
# load image pre_processors
- self.pre_processors = [
- extensions[pre_processor["name"]](
+ self.pre_processors = []
+ for pre_processor in pre_processors_object:
+ ProcessorClass = PROCESSOR_MANAGER.processors[pre_processor["name"]]
+ pre_processor_instance = ProcessorClass(
options=pre_processor["options"],
- relative_dir=template_path.parent,
- image_instance_ops=image_instance_ops,
+ relative_dir=relative_dir,
+ image_instance_ops=self.image_instance_ops,
)
- for pre_processor in json_obj.get("preProcessors", [])
- ]
-
- # Add options
- self.options = json_obj.get("options", {})
-
- # Add q_blocks
- for name, block in json_obj["qBlocks"].items():
- self.add_q_blocks(name, block)
-
- # TODO: also validate these
- # - concatenations and singles together should be mutually exclusive
- # - All qNos in template are unique
- # - template bubbles don't overflow the image (already in instance)
-
- # Expects bubble_dimensions to be set already
- def add_q_blocks(self, key, rect):
- assert self.bubble_dimensions != [-1, -1]
- # For q_type defined in q_blocks
- if "qType" in rect:
- rect.update(**QTYPE_DATA[rect["qType"]])
- else:
- rect.update(**{"vals": rect["vals"], "orient": rect["orient"]})
+ self.pre_processors.append(pre_processor_instance)
+
+ def setup_field_blocks(self, field_blocks_object):
+ # Add field_blocks
+ self.field_blocks = []
+ self.all_parsed_labels = set()
+ for block_name, field_block_object in field_blocks_object.items():
+ self.parse_and_add_field_block(block_name, field_block_object)
+
+ def parse_and_add_field_block(self, block_name, field_block_object):
+ field_block_object = self.pre_fill_field_block(field_block_object)
+ block_instance = FieldBlock(block_name, field_block_object)
+ self.field_blocks.append(block_instance)
+ self.validate_parsed_labels(field_block_object["fieldLabels"], block_instance)
+
+ def validate_parsed_labels(self, field_labels, block_instance):
+ parsed_field_labels, block_name = (
+ block_instance.parsed_field_labels,
+ block_instance.name,
+ )
+ field_labels_set = set(parsed_field_labels)
+ if not self.all_parsed_labels.isdisjoint(field_labels_set):
+ # Note: in case of two fields pointing to same column, use a custom column instead of same field labels.
+ logger.critical(
+ f"An overlap found between field string: {field_labels} in block '{block_name}' and existing labels: {self.all_parsed_labels}"
+ )
+ raise Exception(
+ f"The field strings for field block {block_name} overlap with other existing fields"
+ )
+ self.all_parsed_labels.update(field_labels_set)
- self.q_blocks += gen_grid(
- self.bubble_dimensions, self.global_empty_val, key, rect
+ page_width, page_height = self.page_dimensions
+ block_width, block_height = block_instance.dimensions
+ [block_start_x, block_start_y] = block_instance.origin
+
+ block_end_x, block_end_y = (
+ block_start_x + block_width,
+ block_start_y + block_height,
)
- def __str__(self):
- return str(self.path)
+ if (
+ block_end_x >= page_width
+ or block_end_y >= page_height
+ or block_start_x <= 0
+ or block_start_y <= 0
+ ):
+ raise Exception(
+ f"Overflowing field block '{block_name}' with origin {block_instance.origin} and dimensions {block_instance.dimensions} in template with dimensions {self.page_dimensions}"
+ )
+ def pre_fill_field_block(self, field_block_object):
+ if "fieldType" in field_block_object:
+ field_block_object = {
+ **field_block_object,
+ **FIELD_TYPES[field_block_object["fieldType"]],
+ }
+ else:
+ field_block_object = {**field_block_object, "fieldType": "__CUSTOM__"}
+
+ return {
+ "direction": "vertical",
+ "emptyValue": self.global_empty_val,
+ "bubbleDimensions": self.bubble_dimensions,
+ **field_block_object,
+ }
+
+ def parse_output_columns(self, output_columns_array):
+ self.output_columns = parse_fields(f"Output Columns", output_columns_array)
+
+ def parse_custom_labels(self, custom_labels_object):
+ all_parsed_custom_labels = set()
+ self.custom_labels = {}
+ for custom_label, label_strings in custom_labels_object.items():
+ parsed_labels = parse_fields(f"Custom Label: {custom_label}", label_strings)
+ parsed_labels_set = set(parsed_labels)
+ self.custom_labels[custom_label] = parsed_labels
+
+ missing_custom_labels = sorted(
+ parsed_labels_set.difference(self.all_parsed_labels)
+ )
+ if len(missing_custom_labels) > 0:
+ logger.critical(
+ f"For '{custom_label}', Missing labels - {missing_custom_labels}"
+ )
+ raise Exception(
+ f"Missing field block label(s) in the given template for {missing_custom_labels} from '{custom_label}'"
+ )
-def gen_q_block(
- bubble_dimensions,
- q_block_dims,
- key,
- orig,
- q_nos,
- gaps,
- vals,
- q_type,
- orient,
- col_orient,
- empty_val,
-):
- _h, _v = (0, 1) if (orient == "H") else (1, 0)
- traverse_pts = []
- o = [float(i) for i in orig]
-
- if col_orient == orient:
- for (q, _) in enumerate(q_nos):
- pt = o.copy()
- pts = []
- for (v, _) in enumerate(vals):
- pts.append(Pt(pt.copy(), q_nos[q], q_type, vals[v]))
- pt[_h] += gaps[_h]
- # For diagonal endpoint of QBlock
- pt[_h] += bubble_dimensions[_h] - gaps[_h]
- pt[_v] += bubble_dimensions[_v]
- traverse_pts.append(([o.copy(), pt.copy()], pts))
- o[_v] += gaps[_v]
- else:
- for (v, _) in enumerate(vals):
- pt = o.copy()
- pts = []
- for (q, _) in enumerate(q_nos):
- pts.append(Pt(pt.copy(), q_nos[q], q_type, vals[v]))
- pt[_v] += gaps[_v]
- # For diagonal endpoint of QBlock
- pt[_v] += bubble_dimensions[_v] - gaps[_v]
- pt[_h] += bubble_dimensions[_h]
- traverse_pts.append(([o.copy(), pt.copy()], pts))
- o[_h] += gaps[_h]
-
- return QBlock(q_block_dims, key, orig, traverse_pts, empty_val)
-
-
-def gen_grid(bubble_dimensions, global_empty_val, key, rectParams):
- rect = OVERRIDE_MERGER.merge(
- {"orient": "V", "col_orient": "V", "emptyVal": global_empty_val}, rectParams
- )
- # case mapping
- (q_type, orig, big_gaps, gaps, q_nos, vals, orient, col_orient, empty_val) = map(
- rect.get,
- [
- "qType",
- "orig",
- "bigGaps",
- "gaps",
- "qNos",
- "vals",
- "orient",
- "col_orient",
- "emptyVal",
- ],
- )
-
- q_blocks, orig_gap, grid_data, orig = [], [0, 0], np.array(q_nos), np.array(orig)
-
- num_qs_max = max([max([len(qb) for qb in row]) for row in grid_data])
-
- num_dims = [num_qs_max, len(vals)]
-
- _h, _v = (0, 1) if (orient == "H") else (1, 0)
-
- q_start = orig.copy()
- # Usually single row
- for row in grid_data:
- q_start[_v] = orig[_v]
-
- # Usually multiple qTuples
- for q_tuple in row:
- # Update num_dims and origGaps
- num_dims[0] = len(q_tuple)
- # big_gaps is independent of orientation
- orig_gap[0] = big_gaps[0] + (num_dims[_v] - 1) * gaps[_h]
- orig_gap[1] = big_gaps[1] + (num_dims[_h] - 1) * gaps[_v]
- # each q_tuple will have q_nos
- q_block_dims = [
- # width x height in pixels
- gaps[0] * (num_dims[_v] - 1) + bubble_dimensions[_h],
- gaps[1] * (num_dims[_h] - 1) + bubble_dimensions[_v],
- ]
-
- q_blocks.append(
- gen_q_block(
- bubble_dimensions,
- q_block_dims,
- key,
- q_start.copy(),
- q_tuple,
- gaps,
- vals,
- q_type,
- orient,
- col_orient,
- empty_val,
+ if not all_parsed_custom_labels.isdisjoint(parsed_labels_set):
+ # Note: this can be made a warning, but it's a choice
+ logger.critical(
+ f"field strings overlap for labels: {label_strings} and existing custom labels: {all_parsed_custom_labels}"
+ )
+ raise Exception(
+ f"The field strings for custom label '{custom_label}' overlap with other existing custom labels"
)
+
+ all_parsed_custom_labels.update(parsed_labels)
+
+ self.non_custom_labels = self.all_parsed_labels.difference(
+ all_parsed_custom_labels
+ )
+
+ def fill_output_columns(self, non_custom_columns, all_custom_columns):
+ all_template_columns = non_custom_columns + all_custom_columns
+ # Typical case: sort alpha-numerical (natural sort)
+ self.output_columns = sorted(
+ all_template_columns, key=custom_sort_output_columns
+ )
+
+ def validate_template_columns(self, non_custom_columns, all_custom_columns):
+ output_columns_set = set(self.output_columns)
+ all_custom_columns_set = set(all_custom_columns)
+
+ missing_output_columns = sorted(
+ output_columns_set.difference(all_custom_columns_set).difference(
+ self.all_parsed_labels
+ )
+ )
+ if len(missing_output_columns) > 0:
+ logger.critical(f"Missing output columns: {missing_output_columns}")
+ raise Exception(
+ f"Some columns are missing in the field blocks for the given output columns"
+ )
+
+ all_template_columns_set = set(non_custom_columns + all_custom_columns)
+ missing_label_columns = sorted(
+ all_template_columns_set.difference(output_columns_set)
+ )
+ if len(missing_label_columns) > 0:
+ logger.warning(
+ f"Some label columns are not covered in the given output columns: {missing_label_columns}"
)
- # Goes vertically down first
- q_start[_v] += orig_gap[_v]
- q_start[_h] += orig_gap[_h]
- return q_blocks
+
+ def __str__(self):
+ return str(self.path)
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
new file mode 100644
index 00000000..855c16a5
--- /dev/null
+++ b/src/tests/__init__.py
@@ -0,0 +1 @@
+# https://stackoverflow.com/a/50169991/6242649
diff --git a/src/tests/__snapshots__/test_all_samples.ambr b/src/tests/__snapshots__/test_all_samples.ambr
new file mode 100644
index 00000000..48ed995d
--- /dev/null
+++ b/src/tests/__snapshots__/test_all_samples.ambr
@@ -0,0 +1,266 @@
+# name: test_run_community_Antibodyy
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6"
+ "simple_omr_sheet.jpg","samples/community/Antibodyy/simple_omr_sheet.jpg","outputs/community/Antibodyy/CheckedOMRs/simple_omr_sheet.jpg","0","A","C","B","D","E","B"
+
+ ''',
+ })
+# ---
+# name: test_run_community_Sandeep_1507
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Booklet_No","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100","q101","q102","q103","q104","q105","q106","q107","q108","q109","q110","q111","q112","q113","q114","q115","q116","q117","q118","q119","q120","q121","q122","q123","q124","q125","q126","q127","q128","q129","q130","q131","q132","q133","q134","q135","q136","q137","q138","q139","q140","q141","q142","q143","q144","q145","q146","q147","q148","q149","q150","q151","q152","q153","q154","q155","q156","q157","q158","q159","q160","q161","q162","q163","q164","q165","q166","q167","q168","q169","q170","q171","q172","q173","q174","q175","q176","q177","q178","q179","q180","q181","q182","q183","q184","q185","q186","q187","q188","q189","q190","q191","q192","q193","q194","q195","q196","q197","q198","q199","q200"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Booklet_No","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100","q101","q102","q103","q104","q105","q106","q107","q108","q109","q110","q111","q112","q113","q114","q115","q116","q117","q118","q119","q120","q121","q122","q123","q124","q125","q126","q127","q128","q129","q130","q131","q132","q133","q134","q135","q136","q137","q138","q139","q140","q141","q142","q143","q144","q145","q146","q147","q148","q149","q150","q151","q152","q153","q154","q155","q156","q157","q158","q159","q160","q161","q162","q163","q164","q165","q166","q167","q168","q169","q170","q171","q172","q173","q174","q175","q176","q177","q178","q179","q180","q181","q182","q183","q184","q185","q186","q187","q188","q189","q190","q191","q192","q193","q194","q195","q196","q197","q198","q199","q200"
+ "omr-1.png","samples/community/Sandeep-1507/omr-1.png","outputs/community/Sandeep-1507/Manual/MultiMarkedFiles/omr-1.png","NA","0190880","D","C","B","A","A","B","C","D","D","C","B","A","D","A","B","C","D","","B","D","C","A","C","C","B","A","D","A","AC","C","B","D","C","B","A","B","B","D","D","A","C","B","D","A","C","B","D","B","D","A","A","B","C","D","C","B","A","D","D","A","B","C","D","C","B","A","B","C","D","A","B","C","D","B","A","C","D","C","B","A","D","B","D","A","A","B","A","C","B","D","C","D","B","A","C","C","B","D","B","C","B","A","D","C","B","A","B","C","D","A","A","A","B","B","A","B","C","D","A","A","D","C","B","A","","A","B","C","D","D","D","B","B","C","C","D","C","C","D","D","C","C","B","B","A","A","D","D","B","A","D","C","B","A","A","D","D","B","B","A","A","B","C","D","D","C","B","A","B","D","A","C","C","C","A","A","B","B","D","D","A","A","B","C","D","B","D","A","B","C","D","AD","C","D","B","C","A","B","C","D"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Booklet_No","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100","q101","q102","q103","q104","q105","q106","q107","q108","q109","q110","q111","q112","q113","q114","q115","q116","q117","q118","q119","q120","q121","q122","q123","q124","q125","q126","q127","q128","q129","q130","q131","q132","q133","q134","q135","q136","q137","q138","q139","q140","q141","q142","q143","q144","q145","q146","q147","q148","q149","q150","q151","q152","q153","q154","q155","q156","q157","q158","q159","q160","q161","q162","q163","q164","q165","q166","q167","q168","q169","q170","q171","q172","q173","q174","q175","q176","q177","q178","q179","q180","q181","q182","q183","q184","q185","q186","q187","q188","q189","q190","q191","q192","q193","q194","q195","q196","q197","q198","q199","q200"
+ "omr-2.png","samples/community/Sandeep-1507/omr-2.png","outputs/community/Sandeep-1507/CheckedOMRs/omr-2.png","0","0no22nonono","A","B","B","A","D","C","B","D","C","D","D","D","B","B","D","D","D","B","C","C","A","A","B","A","D","A","A","B","A","C","A","C","D","D","D","","","C","C","B","B","B","","D","","C","D","","D","B","A","D","B","A","C","A","C","A","C","B","A","D","C","B","C","B","C","D","B","B","D","C","C","D","D","A","D","A","D","C","B","D","C","A","C","","C","B","B","","A","A","D","","B","A","","C","A","D","D","C","C","A","C","A","C","D","A","A","A","D","D","B","C","B","B","B","D","A","C","D","D","A","A","A","C","D","C","C","B","D","A","A","C","B","","D","A","C","C","C","","","","A","C","","D","A","B","A","A","C","A","D","B","B","A","D","A","B","C","A","C","D","D","D","C","A","C","A","C","D","A","A","A","D","A","B","A","B","C","B","A","","B","C","D","D","","","D","C","C","C","","C","A",""
+ "omr-3.png","samples/community/Sandeep-1507/omr-3.png","outputs/community/Sandeep-1507/CheckedOMRs/omr-3.png","0","0nononono73","B","A","C","D","A","D","D","A","C","A","A","B","C","A","A","C","A","B","A","D","C","C","A","D","D","C","C","C","A","C","C","B","B","D","D","C","","","C","B","","","D","A","A","A","A","","A","C","C","C","D","C","","A","B","C","D","B","C","C","C","D","A","B","B","B","D","D","B","B","C","D","B","D","A","B","A","B","C","A","C","A","C","D","","","A","B","","B","C","D","A","D","D","","","C","D","B","B","A","A","D","D","B","A","B","B","C","C","D","D","C","A","D","C","D","C","C","B","C","D","C","D","A","B","D","C","B","D","B","B","","D","","B","D","B","B","C","A","D","","C","","C","","B","C","A","B","B","D","D","D","B","A","D","D","A","D","D","C","B","B","D","C","B","A","C","D","A","D","D","A","C","A","B","D","C","C","C","A","D","","","B","B","","C","C","B","B","C","","","B"
+
+ ''',
+ })
+# ---
+# name: test_run_community_Shamanth
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q21","q22","q23","q24","q25","q26","q27","q28"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q21","q22","q23","q24","q25","q26","q27","q28"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q21","q22","q23","q24","q25","q26","q27","q28"
+ "omr_sheet_01.png","samples/community/Shamanth/omr_sheet_01.png","outputs/community/Shamanth/CheckedOMRs/omr_sheet_01.png","0","A","B","C","D","A","C","C","D"
+
+ ''',
+ })
+# ---
+# name: test_run_community_UPSC_mock
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","Subject Code","bookletNo","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","Subject Code","bookletNo","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll","Subject Code","bookletNo","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+ "answer_key.jpg","samples/community/UPSC-mock/answer_key.jpg","outputs/community/UPSC-mock/CheckedOMRs/answer_key.jpg","200.0","","","","C","D","A","C","C","C","B","A","C","C","B","D","B","D","C","C","B","D","B","D","C","C","C","B","D","D","D","B","A","D","D","C","A","B","C","A","D","A","A","A","D","D","B","A","B","C","B","A","C","D","C","D","A","B","C","A","C","C","C","D","B","C","C","C","C","A","D","A","D","A","D","C","C","D","C","D","A","A","C","B","C","D","C","A","B","C","B","D","A","A","C","A","B","D","C","D","A","C","B","A"
+
+ ''',
+ 'scan-angles/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","Subject Code","bookletNo","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'scan-angles/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","Subject Code","bookletNo","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'scan-angles/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll","Subject Code","bookletNo","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+ "angle-1.jpg","samples/community/UPSC-mock/scan-angles/angle-1.jpg","outputs/community/UPSC-mock/scan-angles/CheckedOMRs/angle-1.jpg","70.66666666666669","","","","D","D","A","","C","C","B","","A","C","C","D","A","D","A","C","A","D","B","D","D","C","D","D","D","D","","B","A","D","D","C","","B","","C","D","","","A","","A","C","C","B","C","A","A","C","","C","","D","B","C","","B","C","D","","","C","C","","C","A","B","C","","","","","D","D","C","D","A","","","B","","B","D","C","C","","D","","D","C","D","A","","A","","","A","C","B","A"
+ "angle-2.jpg","samples/community/UPSC-mock/scan-angles/angle-2.jpg","outputs/community/UPSC-mock/scan-angles/CheckedOMRs/angle-2.jpg","70.66666666666669","","","","D","D","A","","C","C","B","","A","C","C","D","A","D","A","C","A","D","B","D","D","C","D","D","D","D","","B","A","D","D","C","","B","","C","D","","","A","","A","C","C","B","C","A","A","C","","C","","D","B","C","","B","C","D","","","C","C","","C","A","B","C","","","","","D","D","C","D","A","","","B","","B","D","C","C","","D","","D","C","D","A","","A","","","A","C","B","A"
+ "angle-3.jpg","samples/community/UPSC-mock/scan-angles/angle-3.jpg","outputs/community/UPSC-mock/scan-angles/CheckedOMRs/angle-3.jpg","70.66666666666669","","","","D","D","A","","C","C","B","","A","C","C","D","A","D","A","C","A","D","B","D","D","C","D","D","D","D","","B","A","D","D","C","","B","","C","D","","","A","","A","C","C","B","C","A","A","C","","C","","D","B","C","","B","C","D","","","C","C","","C","A","B","C","","","","","D","D","C","D","A","","","B","","B","D","C","C","","D","","D","C","D","A","","A","","","A","C","B","A"
+
+ ''',
+ })
+# ---
+# name: test_run_community_UmarFarootAPS
+ dict({
+ 'scans/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll_no","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100","q101","q102","q103","q104","q105","q106","q107","q108","q109","q110","q111","q112","q113","q114","q115","q116","q117","q118","q119","q120","q121","q122","q123","q124","q125","q126","q127","q128","q129","q130","q131","q132","q133","q134","q135","q136","q137","q138","q139","q140","q141","q142","q143","q144","q145","q146","q147","q148","q149","q150","q151","q152","q153","q154","q155","q156","q157","q158","q159","q160","q161","q162","q163","q164","q165","q166","q167","q168","q169","q170","q171","q172","q173","q174","q175","q176","q177","q178","q179","q180","q181","q182","q183","q184","q185","q186","q187","q188","q189","q190","q191","q192","q193","q194","q195","q196","q197","q198","q199","q200"
+
+ ''',
+ 'scans/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll_no","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100","q101","q102","q103","q104","q105","q106","q107","q108","q109","q110","q111","q112","q113","q114","q115","q116","q117","q118","q119","q120","q121","q122","q123","q124","q125","q126","q127","q128","q129","q130","q131","q132","q133","q134","q135","q136","q137","q138","q139","q140","q141","q142","q143","q144","q145","q146","q147","q148","q149","q150","q151","q152","q153","q154","q155","q156","q157","q158","q159","q160","q161","q162","q163","q164","q165","q166","q167","q168","q169","q170","q171","q172","q173","q174","q175","q176","q177","q178","q179","q180","q181","q182","q183","q184","q185","q186","q187","q188","q189","q190","q191","q192","q193","q194","q195","q196","q197","q198","q199","q200"
+ "scan-type-2.jpg","samples/community/UmarFarootAPS/scans/scan-type-2.jpg","outputs/community/UmarFarootAPS/scans/Manual/MultiMarkedFiles/scan-type-2.jpg","NA","0234","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","A","D","","","AD","","","","A","D","","","","","","","D","A","","D","","A","","D","","","","A","","","C","","","D","","","A","","","","D","","C","","A","","C","","D","B","B","","","A","","D","","","","D","","","","","A","D","","","B","","","D","","","A","","","D","","","","","","D","","","","A","D","","","A","","B","","D","","","","C","C","D","D","A","","D","","A","D","","","D","","B","D","","","D","","D","B","","","","D","","A","","","","D","","B","","","","","","D","","","A","","","A","","D","","","D"
+
+ ''',
+ 'scans/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll_no","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100","q101","q102","q103","q104","q105","q106","q107","q108","q109","q110","q111","q112","q113","q114","q115","q116","q117","q118","q119","q120","q121","q122","q123","q124","q125","q126","q127","q128","q129","q130","q131","q132","q133","q134","q135","q136","q137","q138","q139","q140","q141","q142","q143","q144","q145","q146","q147","q148","q149","q150","q151","q152","q153","q154","q155","q156","q157","q158","q159","q160","q161","q162","q163","q164","q165","q166","q167","q168","q169","q170","q171","q172","q173","q174","q175","q176","q177","q178","q179","q180","q181","q182","q183","q184","q185","q186","q187","q188","q189","q190","q191","q192","q193","q194","q195","q196","q197","q198","q199","q200"
+ "scan-type-1.jpg","samples/community/UmarFarootAPS/scans/scan-type-1.jpg","outputs/community/UmarFarootAPS/scans/CheckedOMRs/scan-type-1.jpg","0","2468","A","C","B","C","A","D","B","C","B","D","C","A","C","D","B","C","A","B","C","A","C","B","D","C","A","B","D","C","A","C","B","D","B","A","C","D","B","C","A","C","D","A","C","D","A","B","D","C","A","C","D","B","C","A","C","D","B","C","D","A","B","C","B","C","D","B","D","A","C","B","D","A","B","C","B","A","C","D","B","A","C","B","C","B","A","D","B","A","C","D","B","D","B","C","B","D","A","C","B","C","B","C","D","B","C","A","B","C","A","D","C","B","D","B","A","B","C","D","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","D","C","B","A","B","C","B","A","C","B","A","C","A","B","C","B","C","B","A","C","A","C","B","B","C","B","A","C","A","B","A","B","A","B","C","D","B","C","A","C","D","C","A","C","B","A","C","A","B","C","B","D","A","B","C","D","C","B","B","C","A","B","C","B"
+
+ ''',
+ })
+# ---
+# name: test_run_community_ibrahimkilic
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5"
+ "yes_no_questionnarie.jpg","samples/community/ibrahimkilic/yes_no_questionnarie.jpg","outputs/community/ibrahimkilic/CheckedOMRs/yes_no_questionnarie.jpg","0","no","no","no","no","no"
+
+ ''',
+ })
+# ---
+# name: test_run_sample1
+ dict({
+ 'MobileCamera/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20"
+
+ ''',
+ 'MobileCamera/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20"
+
+ ''',
+ 'MobileCamera/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20"
+ "sheet1.jpg","samples/sample1/MobileCamera/sheet1.jpg","outputs/sample1/MobileCamera/CheckedOMRs/sheet1.jpg","0","E503110026","B","","D","B","6","11","20","7","16","B","D","C","D","A","D","B","A","C","C","D"
+
+ ''',
+ })
+# ---
+# name: test_run_sample2
+ dict({
+ 'AdrianSample/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5"
+
+ ''',
+ 'AdrianSample/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5"
+
+ ''',
+ 'AdrianSample/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5"
+ "adrian_omr.png","samples/sample2/AdrianSample/adrian_omr.png","outputs/sample2/AdrianSample/CheckedOMRs/adrian_omr.png","0","B","E","A","C","B"
+ "adrian_omr_2.png","samples/sample2/AdrianSample/adrian_omr_2.png","outputs/sample2/AdrianSample/CheckedOMRs/adrian_omr_2.png","0","C","E","A","B","B"
+
+ ''',
+ })
+# ---
+# name: test_run_sample3
+ dict({
+ 'colored-thick-sheet/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'colored-thick-sheet/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'colored-thick-sheet/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+ "rgb-100-gsm.jpg","samples/sample3/colored-thick-sheet/rgb-100-gsm.jpg","outputs/sample3/colored-thick-sheet/CheckedOMRs/rgb-100-gsm.jpg","0","D","D","A","","C","C","B","","A","C","C","D","A","D","A","C","A","D","B","D","D","C","D","D","D","D","","B","A","D","D","C","","B","","C","D","","","A","","A","C","C","B","C","A","A","C","","C","","D","B","C","","B","C","D","","","C","C","","C","A","B","C","","","","","D","D","C","D","A","","","B","","B","D","C","C","","D","","D","C","D","A","","A","","","A","C","B","A"
+
+ ''',
+ 'xeroxed-thin-sheet/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'xeroxed-thin-sheet/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+
+ ''',
+ 'xeroxed-thin-sheet/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22","q23","q24","q25","q26","q27","q28","q29","q30","q31","q32","q33","q34","q35","q36","q37","q38","q39","q40","q41","q42","q43","q44","q45","q46","q47","q48","q49","q50","q51","q52","q53","q54","q55","q56","q57","q58","q59","q60","q61","q62","q63","q64","q65","q66","q67","q68","q69","q70","q71","q72","q73","q74","q75","q76","q77","q78","q79","q80","q81","q82","q83","q84","q85","q86","q87","q88","q89","q90","q91","q92","q93","q94","q95","q96","q97","q98","q99","q100"
+ "grayscale-80-gsm.jpg","samples/sample3/xeroxed-thin-sheet/grayscale-80-gsm.jpg","outputs/sample3/xeroxed-thin-sheet/CheckedOMRs/grayscale-80-gsm.jpg","0","C","D","A","C","C","C","B","A","C","C","B","D","B","D","C","C","B","D","B","D","C","C","C","B","D","D","D","B","A","D","D","C","A","B","C","A","D","A","A","A","D","D","B","A","B","C","B","A","C","D","C","D","A","B","C","A","C","C","C","D","B","C","C","C","C","A","D","A","D","A","D","C","C","D","C","D","A","A","C","B","C","D","C","A","B","C","B","D","A","A","C","A","B","D","C","D","A","C","B","A"
+
+ ''',
+ })
+# ---
+# name: test_run_sample4
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11"
+ "IMG_20201116_143512.jpg","samples/sample4/IMG_20201116_143512.jpg","outputs/sample4/Manual/MultiMarkedFiles/IMG_20201116_143512.jpg","NA","B","D","C","B","D","C","BC","A","C","D","C"
+ "IMG_20201116_150717658.jpg","samples/sample4/IMG_20201116_150717658.jpg","outputs/sample4/Manual/MultiMarkedFiles/IMG_20201116_150717658.jpg","NA","B","D","C","B","D","C","BC","A","C","D","C"
+ "IMG_20201116_150750830.jpg","samples/sample4/IMG_20201116_150750830.jpg","outputs/sample4/Manual/MultiMarkedFiles/IMG_20201116_150750830.jpg","NA","A","","D","C","AC","A","D","B","C","D","D"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11"
+
+ ''',
+ })
+# ---
+# name: test_run_sample5
+ dict({
+ 'ScanBatch1/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22"
+
+ ''',
+ 'ScanBatch1/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22"
+ "camscanner-1.jpg","samples/sample5/ScanBatch1/camscanner-1.jpg","outputs/sample5/ScanBatch1/Manual/MultiMarkedFiles/camscanner-1.jpg","NA","E204420102","D","C","A","C","B","08","52","21","85","36","B","C","A","A","D","C","C","AD","A","A","D",""
+
+ ''',
+ 'ScanBatch1/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22"
+
+ ''',
+ 'ScanBatch2/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22"
+
+ ''',
+ 'ScanBatch2/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22"
+
+ ''',
+ 'ScanBatch2/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15","q16","q17","q18","q19","q20","q21","q22"
+ "camscanner-2.jpg","samples/sample5/ScanBatch2/camscanner-2.jpg","outputs/sample5/ScanBatch2/CheckedOMRs/camscanner-2.jpg","0","E204420109","C","C","B","C","C","01","19","10","10","18","D","A","D","D","D","C","C","C","C","D","B","A"
+
+ ''',
+ })
+# ---
+# name: test_run_sample6
+ dict({
+ 'Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll"
+
+ ''',
+ 'Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll"
+
+ ''',
+ 'Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll"
+ "reference.png","samples/sample6/reference.png","outputs/sample6/CheckedOMRs/reference.png","0","A"
+
+ ''',
+ 'doc-scans/Manual/ErrorFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll"
+
+ ''',
+ 'doc-scans/Manual/MultiMarkedFiles.csv': '''
+ "file_id","input_path","output_path","score","Roll"
+
+ ''',
+ 'doc-scans/Results/Results_05AM.csv': '''
+ "file_id","input_path","output_path","score","Roll"
+ "sample_roll_01.jpg","samples/sample6/doc-scans/sample_roll_01.jpg","outputs/sample6/doc-scans/CheckedOMRs/sample_roll_01.jpg","0","A0188877Y"
+ "sample_roll_02.jpg","samples/sample6/doc-scans/sample_roll_02.jpg","outputs/sample6/doc-scans/CheckedOMRs/sample_roll_02.jpg","0","A020395W"
+ "sample_roll_03.jpg","samples/sample6/doc-scans/sample_roll_03.jpg","outputs/sample6/doc-scans/CheckedOMRs/sample_roll_03.jpg","0","A0204729A"
+
+ ''',
+ })
+# ---
diff --git a/src/tests/test_all_samples.py b/src/tests/test_all_samples.py
new file mode 100644
index 00000000..00a25ff6
--- /dev/null
+++ b/src/tests/test_all_samples.py
@@ -0,0 +1,102 @@
+import os
+import shutil
+from glob import glob
+
+from src.tests.utils import run_entry_point, setup_mocker_patches
+
+
+def read_file(path):
+ with open(path) as file:
+ return file.read()
+
+
+def run_sample(mocker, sample_path):
+ setup_mocker_patches(mocker)
+
+ input_path = os.path.join("samples", sample_path)
+ output_dir = os.path.join("outputs", sample_path)
+ if os.path.exists(output_dir):
+ print(
+ f"Warning: output directory already exists: {output_dir}. This may affect the test execution."
+ )
+
+ run_entry_point(input_path, output_dir)
+
+ sample_outputs = extract_sample_outputs(output_dir)
+
+ print(f"Note: removing output directory: {output_dir}")
+ shutil.rmtree(output_dir)
+
+ return sample_outputs
+
+
+EXT = "*.csv"
+
+
+def extract_sample_outputs(output_dir):
+ sample_outputs = {}
+ for _dir, _subdir, _files in os.walk(output_dir):
+ for file in glob(os.path.join(_dir, EXT)):
+ relative_path = os.path.relpath(file, output_dir)
+ sample_outputs[relative_path] = read_file(file)
+ return sample_outputs
+
+
+def test_run_sample1(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "sample1")
+ assert snapshot == sample_outputs
+
+
+def test_run_sample2(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "sample2")
+ assert snapshot == sample_outputs
+
+
+def test_run_sample3(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "sample3")
+ assert snapshot == sample_outputs
+
+
+def test_run_sample4(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "sample4")
+ assert snapshot == sample_outputs
+
+
+def test_run_sample5(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "sample5")
+ assert snapshot == sample_outputs
+
+
+def test_run_sample6(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "sample6")
+ assert snapshot == sample_outputs
+
+
+def test_run_community_Antibodyy(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "community/Antibodyy")
+ assert snapshot == sample_outputs
+
+
+def test_run_community_ibrahimkilic(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "community/ibrahimkilic")
+ assert snapshot == sample_outputs
+
+
+def test_run_community_Sandeep_1507(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "community/Sandeep-1507")
+ assert snapshot == sample_outputs
+
+
+def test_run_community_Shamanth(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "community/Shamanth")
+ assert snapshot == sample_outputs
+
+
+def test_run_community_UmarFarootAPS(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "community/UmarFarootAPS")
+ assert snapshot == sample_outputs
+
+
+def test_run_community_UPSC_mock(mocker, snapshot):
+ sample_outputs = run_sample(mocker, "community/UPSC-mock")
+ assert snapshot == sample_outputs
diff --git a/src/tests/test_edge_cases.py b/src/tests/test_edge_cases.py
new file mode 100644
index 00000000..c6650e04
--- /dev/null
+++ b/src/tests/test_edge_cases.py
@@ -0,0 +1,41 @@
+import os
+from pathlib import Path
+
+from src.tests.test_samples.sample2.boilerplate import (
+ CONFIG_BOILERPLATE,
+ TEMPLATE_BOILERPLATE,
+)
+from src.tests.utils import (
+ generate_write_jsons_and_run,
+ run_entry_point,
+ setup_mocker_patches,
+)
+
+FROZEN_TIMESTAMP = "1970-01-01"
+CURRENT_DIR = Path("src/tests")
+BASE_SAMPLE_PATH = CURRENT_DIR.joinpath("test_samples", "sample2")
+
+
+def run_sample(mocker, input_path):
+ setup_mocker_patches(mocker)
+ output_dir = os.path.join("outputs", input_path)
+ run_entry_point(input_path, output_dir)
+ # sample_outputs = extract_sample_outputs(output_dir)
+
+
+write_jsons_and_run = generate_write_jsons_and_run(
+ run_sample,
+ sample_path=BASE_SAMPLE_PATH,
+ template_boilerplate=TEMPLATE_BOILERPLATE,
+ config_boilerplate=CONFIG_BOILERPLATE,
+)
+
+
+def test_config_low_dimensions(mocker):
+ def modify_config(config):
+ config["dimensions"]["processing_height"] = 1000
+ config["dimensions"]["processing_width"] = 1000
+
+ exception = write_jsons_and_run(mocker, modify_config=modify_config)
+
+ assert str(exception) == "No Error"
diff --git a/src/tests/test_samples/sample1/boilerplate.py b/src/tests/test_samples/sample1/boilerplate.py
new file mode 100644
index 00000000..74dda465
--- /dev/null
+++ b/src/tests/test_samples/sample1/boilerplate.py
@@ -0,0 +1,14 @@
+TEMPLATE_BOILERPLATE = {
+ "pageDimensions": [300, 400],
+ "bubbleDimensions": [25, 25],
+ "preProcessors": [{"name": "CropPage", "options": {"morphKernel": [10, 10]}}],
+ "fieldBlocks": {
+ "MCQ_Block_1": {
+ "fieldType": "QTYPE_MCQ5",
+ "origin": [65, 60],
+ "fieldLabels": ["q1..5"],
+ "labelsGap": 52,
+ "bubblesGap": 41,
+ }
+ },
+}
diff --git a/src/tests/test_samples/sample1/sample.png b/src/tests/test_samples/sample1/sample.png
new file mode 100644
index 00000000..d8db0994
Binary files /dev/null and b/src/tests/test_samples/sample1/sample.png differ
diff --git a/src/tests/test_samples/sample2/boilerplate.py b/src/tests/test_samples/sample2/boilerplate.py
new file mode 100644
index 00000000..e27b012e
--- /dev/null
+++ b/src/tests/test_samples/sample2/boilerplate.py
@@ -0,0 +1,39 @@
+TEMPLATE_BOILERPLATE = {
+ "pageDimensions": [2550, 3300],
+ "bubbleDimensions": [32, 32],
+ "preProcessors": [
+ {
+ "name": "CropOnMarkers",
+ "options": {
+ "relativePath": "omr_marker.jpg",
+ "sheetToMarkerWidthRatio": 17,
+ },
+ }
+ ],
+ "fieldBlocks": {
+ "MCQBlock1a1": {
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [197, 300],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q1..17"],
+ },
+ "MCQBlock1a11": {
+ "fieldType": "QTYPE_MCQ4",
+ "origin": [1770, 1310],
+ "bubblesGap": 92,
+ "labelsGap": 59.6,
+ "fieldLabels": ["q168..184"],
+ },
+ },
+}
+
+CONFIG_BOILERPLATE = {
+ "dimensions": {
+ "display_height": 960,
+ "display_width": 1280,
+ "processing_height": 1640,
+ "processing_width": 1332,
+ },
+ "outputs": {"show_image_level": 0},
+}
diff --git a/src/tests/test_samples/sample2/omr_marker.jpg b/src/tests/test_samples/sample2/omr_marker.jpg
new file mode 100644
index 00000000..0929feec
Binary files /dev/null and b/src/tests/test_samples/sample2/omr_marker.jpg differ
diff --git a/src/tests/test_samples/sample2/sample.jpg b/src/tests/test_samples/sample2/sample.jpg
new file mode 100644
index 00000000..2eef7c32
Binary files /dev/null and b/src/tests/test_samples/sample2/sample.jpg differ
diff --git a/src/tests/test_template_validations.py b/src/tests/test_template_validations.py
new file mode 100644
index 00000000..bd321a72
--- /dev/null
+++ b/src/tests/test_template_validations.py
@@ -0,0 +1,159 @@
+import os
+from pathlib import Path
+
+from src.tests.test_samples.sample1.boilerplate import TEMPLATE_BOILERPLATE
+from src.tests.utils import (
+ generate_write_jsons_and_run,
+ run_entry_point,
+ setup_mocker_patches,
+)
+
+FROZEN_TIMESTAMP = "1970-01-01"
+CURRENT_DIR = Path("src/tests")
+BASE_SAMPLE_PATH = CURRENT_DIR.joinpath("test_samples", "sample1")
+BASE_SAMPLE_TEMPLATE_PATH = BASE_SAMPLE_PATH.joinpath("template.json")
+
+
+def run_sample(mocker, input_path):
+ setup_mocker_patches(mocker)
+ output_dir = os.path.join("outputs", input_path)
+ run_entry_point(input_path, output_dir)
+
+
+write_jsons_and_run = generate_write_jsons_and_run(
+ run_sample,
+ sample_path=BASE_SAMPLE_PATH,
+ template_boilerplate=TEMPLATE_BOILERPLATE,
+)
+
+
+def test_no_input_dir(mocker):
+ try:
+ run_sample(mocker, "X")
+ except Exception as e:
+ assert str(e) == "Given input directory does not exist: 'X'"
+
+
+def test_no_template(mocker):
+ if os.path.exists(BASE_SAMPLE_TEMPLATE_PATH):
+ os.remove(BASE_SAMPLE_TEMPLATE_PATH)
+ try:
+ run_sample(mocker, BASE_SAMPLE_PATH)
+ except Exception as e:
+ assert (
+ str(e)
+ == "No template file found in the directory tree of src/tests/test_samples/sample1"
+ )
+
+
+def test_empty_template(mocker):
+ def modify_template(_):
+ return {}
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert (
+ str(exception)
+ == f"Provided Template JSON is Invalid: '{BASE_SAMPLE_TEMPLATE_PATH}'"
+ )
+
+
+def test_invalid_field_type(mocker):
+ def modify_template(template):
+ template["fieldBlocks"]["MCQ_Block_1"]["fieldType"] = "X"
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert (
+ str(exception)
+ == f"Provided Template JSON is Invalid: '{BASE_SAMPLE_TEMPLATE_PATH}'"
+ )
+
+
+def test_overflow_labels(mocker):
+ def modify_template(template):
+ template["fieldBlocks"]["MCQ_Block_1"]["fieldLabels"] = ["q1..100"]
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert (
+ str(exception)
+ == "Overflowing field block 'MCQ_Block_1' with origin [65, 60] and dimensions [189, 5173] in template with dimensions [300, 400]"
+ )
+
+
+def test_overflow_safe_dimensions(mocker):
+ def modify_template(template):
+ template["pageDimensions"] = [255, 400]
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert str(exception) == "No Error"
+
+
+def test_field_strings_overlap(mocker):
+ def modify_template(template):
+ template["fieldBlocks"] = {
+ **template["fieldBlocks"],
+ "New_Block": {
+ **template["fieldBlocks"]["MCQ_Block_1"],
+ "fieldLabels": ["q5"],
+ },
+ }
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert str(exception) == (
+ "The field strings for field block New_Block overlap with other existing fields"
+ )
+
+
+def test_custom_label_strings_overlap_single(mocker):
+ def modify_template(template):
+ template["customLabels"] = {
+ "label1": ["q1..2", "q2..3"],
+ }
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert (
+ str(exception)
+ == "Given field string 'q2..3' has overlapping field(s) with other fields in 'Custom Label: label1': ['q1..2', 'q2..3']"
+ )
+
+
+def test_custom_label_strings_overlap_multiple(mocker):
+ def modify_template(template):
+ template["customLabels"] = {
+ "label1": ["q1..2"],
+ "label2": ["q2..3"],
+ }
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert (
+ str(exception)
+ == "The field strings for custom label 'label2' overlap with other existing custom labels"
+ )
+
+
+def test_missing_field_block_labels(mocker):
+ def modify_template(template):
+ template["customLabels"] = {"Combined": ["qX", "qY"]}
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert (
+ str(exception)
+ == "Missing field block label(s) in the given template for ['qX', 'qY'] from 'Combined'"
+ )
+
+
+def test_missing_output_columns(mocker):
+ def modify_template(template):
+ template["outputColumns"] = ["qX", "q1..5"]
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert str(exception) == (
+ "Some columns are missing in the field blocks for the given output columns"
+ )
+
+
+def test_safe_missing_label_columns(mocker):
+ def modify_template(template):
+ template["outputColumns"] = ["q1..4"]
+
+ exception = write_jsons_and_run(mocker, modify_template=modify_template)
+ assert str(exception) == "No Error"
diff --git a/src/tests/utils.py b/src/tests/utils.py
new file mode 100644
index 00000000..f80d568f
--- /dev/null
+++ b/src/tests/utils.py
@@ -0,0 +1,91 @@
+import json
+import os
+from copy import deepcopy
+
+from freezegun import freeze_time
+
+from main import entry_point_for_args
+
+FROZEN_TIMESTAMP = "1970-01-01"
+
+
+def setup_mocker_patches(mocker):
+ mock_imshow = mocker.patch("cv2.imshow")
+ mock_imshow.return_value = True
+
+ mock_destroy_all_windows = mocker.patch("cv2.destroyAllWindows")
+ mock_destroy_all_windows.return_value = True
+
+ mock_wait_key = mocker.patch("cv2.waitKey")
+ mock_wait_key.return_value = ord("q")
+
+
+def run_entry_point(input_path, output_dir):
+ args = {
+ "input_paths": [input_path],
+ "output_dir": output_dir,
+ "autoAlign": False,
+ "setLayout": False,
+ "silent": True,
+ }
+ with freeze_time(FROZEN_TIMESTAMP):
+ entry_point_for_args(args)
+
+
+def write_modified(modify_content, boilerplate, sample_json_path):
+ if boilerplate is None:
+ return
+
+ content = deepcopy(boilerplate)
+
+ if modify_content is not None:
+ returned_value = modify_content(content)
+ if returned_value is not None:
+ content = returned_value
+
+ with open(sample_json_path, "w") as f:
+ json.dump(content, f)
+
+
+def remove_modified(sample_json_path):
+ if os.path.exists(sample_json_path):
+ os.remove(sample_json_path)
+
+
+def generate_write_jsons_and_run(
+ run_sample,
+ sample_path,
+ template_boilerplate=None,
+ config_boilerplate=None,
+ evaluation_boilerplate=None,
+):
+ def write_jsons_and_run(
+ mocker,
+ modify_template=None,
+ modify_config=None,
+ modify_evaluation=None,
+ ):
+ sample_template_path, sample_config_path, sample_evaluation_path = (
+ sample_path.joinpath("template.json"),
+ sample_path.joinpath("config.json"),
+ sample_path.joinpath("evaluation.json"),
+ )
+ write_modified(modify_template, template_boilerplate, sample_template_path)
+ write_modified(modify_config, config_boilerplate, sample_config_path)
+ write_modified(
+ modify_evaluation, evaluation_boilerplate, sample_evaluation_path
+ )
+
+ exception = "No Error"
+ try:
+ run_sample(mocker, sample_path)
+ except Exception as e:
+ exception = e
+
+ remove_modified(sample_template_path)
+ remove_modified(sample_config_path)
+ remove_modified(sample_evaluation_path)
+
+ return exception
+
+ return write_jsons_and_run
diff --git a/src/utils/file.py b/src/utils/file.py
index f2ab8950..785cd10c 100644
--- a/src/utils/file.py
+++ b/src/utils/file.py
@@ -20,20 +20,20 @@ def load_json(path, **rest):
def setup_outputs_for_template(paths, template):
+ # TODO: consider moving this into a class instance
ns = argparse.Namespace()
logger.info("Checking Files...")
# Include current output paths
ns.paths = paths
- # Custom sort: avoids q1, q10, q2, ... and orders them q1, q2, ..., q10
- ns.resp_cols = sorted(
- list(template.concatenations.keys()) + template.singles,
- key=lambda x: int(x[1:]) if ord(x[1]) in range(48, 58) else 0,
- )
- # TODO: consider using emptyVal for empty_resp
- ns.empty_resp = [""] * len(ns.resp_cols)
- ns.sheetCols = ["file_id", "input_path", "output_path", "score"] + ns.resp_cols
+ ns.empty_resp = [""] * len(template.output_columns)
+ ns.sheetCols = [
+ "file_id",
+ "input_path",
+ "output_path",
+ "score",
+ ] + template.output_columns
ns.OUTPUT_SET = []
ns.files_obj = {}
TIME_NOW_HRS = strftime("%I%p", localtime())
diff --git a/src/utils/interaction.py b/src/utils/interaction.py
index 9c648ffc..81b5a730 100644
--- a/src/utils/interaction.py
+++ b/src/utils/interaction.py
@@ -41,24 +41,26 @@ class InteractionUtils:
image_metrics = ImageMetrics()
@staticmethod
- def show(name, orig, pause=1, resize=False, reset_pos=None, config=None):
+ def show(name, origin, pause=1, resize=False, reset_pos=None, config=None):
image_metrics = InteractionUtils.image_metrics
- if orig is None:
+ if origin is None:
logger.info(f"'{name}' - NoneType image to show!")
if pause:
cv2.destroyAllWindows()
return
- # origDim = orig.shape[:2]
if resize:
if not config:
raise Exception("config not provided for resizing the image to show")
- img = ImageUtils.resize_util(orig, config.dimensions.display_width)
+ img = ImageUtils.resize_util(origin, config.dimensions.display_width)
else:
- img = orig
+ img = origin
+
cv2.imshow(name, img)
+
if reset_pos:
image_metrics.window_x = reset_pos[0]
image_metrics.window_y = reset_pos[1]
+
cv2.moveWindow(
name,
image_metrics.window_x,
diff --git a/src/utils/parsing.py b/src/utils/parsing.py
index 0dee9ad5..0a9962f0 100644
--- a/src/utils/parsing.py
+++ b/src/utils/parsing.py
@@ -1,10 +1,13 @@
+import re
from copy import deepcopy
+from fractions import Fraction
from deepmerge import Merger
from dotmap import DotMap
+from src.constants import FIELD_LABEL_NUMBER_REGEX
from src.defaults import CONFIG_DEFAULTS, TEMPLATE_DEFAULTS
-from src.logger import logger
+from src.schemas.constants import FIELD_STRING_REGEX_GROUPS
from src.utils.file import load_json
from src.utils.validations import (
validate_config_json,
@@ -29,21 +32,55 @@
)
-def get_concatenated_response(omr_response, template):
- concatenated_response = {}
+def parse_field_string(field_string):
+ if "." in field_string:
+ field_prefix, start, end = re.findall(FIELD_STRING_REGEX_GROUPS, field_string)[
+ 0
+ ]
+ start, end = int(start), int(end)
+ if start >= end:
+ raise Exception(
+ f"Invalid range in fields string: '{field_string}', start: {start} is not less than end: {end}"
+ )
+ return [
+ f"{field_prefix}{field_number}" for field_number in range(start, end + 1)
+ ]
+ else:
+ return [field_string]
+
+
+def parse_float_or_fraction(result):
+ if type(result) == str and "/" in result:
+ result = float(Fraction(result))
+ else:
+ result = float(result)
+ return result
+
+
+def parse_fields(key, fields):
+ parsed_fields = []
+ fields_set = set()
+ for field_string in fields:
+ fields_array = parse_field_string(field_string)
+ current_set = set(fields_array)
+ if not fields_set.isdisjoint(current_set):
+ raise Exception(
+ f"Given field string '{field_string}' has overlapping field(s) with other fields in '{key}': {fields}"
+ )
+ fields_set.update(current_set)
+ parsed_fields.extend(fields_array)
+ return parsed_fields
- # TODO: get correct local/global emptyVal here for each question
- unmarked_symbol = ""
+def get_concatenated_response(omr_response, template):
# Multi-column/multi-row questions which need to be concatenated
- for q_no, resp_keys in template.concatenations.items():
- concatenated_response[q_no] = "".join(
- [omr_response.get(k, unmarked_symbol) for k in resp_keys]
- )
+ concatenated_response = {}
+ for field_label, concatenate_keys in template.custom_labels.items():
+ custom_label = "".join([omr_response[k] for k in concatenate_keys])
+ concatenated_response[field_label] = custom_label
- # Single-column/single-row questions
- for q_no in template.singles:
- concatenated_response[q_no] = omr_response.get(q_no, unmarked_symbol)
+ for field_label in template.non_custom_labels:
+ concatenated_response[field_label] = omr_response[field_label]
return concatenated_response
@@ -53,33 +90,24 @@ def open_config_with_defaults(config_path):
user_tuning_config = OVERRIDE_MERGER.merge(
deepcopy(CONFIG_DEFAULTS), user_tuning_config
)
- is_valid = validate_config_json(user_tuning_config, config_path)
-
- if is_valid:
- # https://github.com/drgrib/dotmap/issues/74
- return DotMap(user_tuning_config, _dynamic=False)
- else:
- logger.critical("\nExiting program")
- exit()
+ validate_config_json(user_tuning_config, config_path)
+ # https://github.com/drgrib/dotmap/issues/74
+ return DotMap(user_tuning_config, _dynamic=False)
def open_template_with_defaults(template_path):
user_template = load_json(template_path)
user_template = OVERRIDE_MERGER.merge(deepcopy(TEMPLATE_DEFAULTS), user_template)
- is_valid = validate_template_json(user_template, template_path)
- if is_valid:
- return user_template
- else:
- logger.critical("\nExiting program")
- exit()
+ validate_template_json(user_template, template_path)
+ return user_template
def open_evaluation_with_validation(evaluation_path):
user_evaluation_config = load_json(evaluation_path)
- is_valid = validate_evaluation_json(user_evaluation_config, evaluation_path)
+ validate_evaluation_json(user_evaluation_config, evaluation_path)
+ return user_evaluation_config
- if is_valid:
- return user_evaluation_config
- else:
- logger.critical("\nExiting program")
- exit()
+
+def custom_sort_output_columns(field_label):
+ label_prefix, label_suffix = re.findall(FIELD_LABEL_NUMBER_REGEX, field_label)[0]
+ return [label_prefix, int(label_suffix) if len(label_suffix) > 0 else 0]
diff --git a/src/utils/validations.py b/src/utils/validations.py
index 3e34d6b4..0bc18d2d 100644
--- a/src/utils/validations.py
+++ b/src/utils/validations.py
@@ -18,7 +18,7 @@
def parse_validation_error(error):
return (
- (error.path[0] if len(error.path) > 0 else "root key"),
+ (error.path[0] if len(error.path) > 0 else "$root"),
error.validator,
error.message,
)
@@ -29,7 +29,6 @@ def validate_evaluation_json(json_data, evaluation_path):
try:
validate(instance=json_data, schema=SCHEMA_JSONS["evaluation"])
except jsonschema.exceptions.ValidationError as _err: # NOQA
-
table = Table(show_lines=True)
table.add_column("Key", style="cyan", no_wrap=True)
table.add_column("Error", style="magenta")
@@ -41,17 +40,15 @@ def validate_evaluation_json(json_data, evaluation_path):
for error in errors:
key, validator, msg = parse_validation_error(error)
if validator == "required":
+ requiredProperty = re.findall(r"'(.*?)'", msg)[0]
table.add_row(
- re.findall(r"'(.*?)'", msg)[0],
+ f"{key}.{requiredProperty}",
msg + ". Make sure the spelling of the key is correct",
)
else:
table.add_row(key, msg)
console.print(table, justify="center")
- logger.critical(f"Provided Evaluation JSON is Invalid: '{evaluation_path}'")
- return False
-
- return True
+ raise Exception(f"Provided Evaluation JSON is Invalid: '{evaluation_path}'")
def validate_template_json(json_data, template_path):
@@ -59,7 +56,6 @@ def validate_template_json(json_data, template_path):
try:
validate(instance=json_data, schema=SCHEMA_JSONS["template"])
except jsonschema.exceptions.ValidationError as _err: # NOQA
-
table = Table(show_lines=True)
table.add_column("Key", style="cyan", no_wrap=True)
table.add_column("Error", style="magenta")
@@ -77,18 +73,15 @@ def validate_template_json(json_data, template_path):
preProcessorKey = error.path[2]
table.add_row(f"{key}.{preProcessorName}.{preProcessorKey}", msg)
elif validator == "required":
+ requiredProperty = re.findall(r"'(.*?)'", msg)[0]
table.add_row(
- re.findall(r"'(.*?)'", msg)[0],
- msg
- + ". Make sure the spelling of the key is correct and it is in camelCase",
+ f"{key}.{requiredProperty}",
+ f"{msg}. Check for spelling errors and make sure it is in camelCase",
)
else:
table.add_row(key, msg)
console.print(table, justify="center")
- logger.critical(f"Provided Template JSON is Invalid: '{template_path}'")
- return False
-
- return True
+ raise Exception(f"Provided Template JSON is Invalid: '{template_path}'")
def validate_config_json(json_data, config_path):
@@ -107,14 +100,12 @@ def validate_config_json(json_data, config_path):
key, validator, msg = parse_validation_error(error)
if validator == "required":
+ requiredProperty = re.findall(r"'(.*?)'", msg)[0]
table.add_row(
- re.findall(r"'(.*?)'", msg)[0],
- msg
- + ". Make sure the spelling of the key is correct and it is in camelCase",
+ f"{key}.{requiredProperty}",
+ f"{msg}. Check for spelling errors and make sure it is in camelCase",
)
else:
table.add_row(key, msg)
console.print(table, justify="center")
- logger.critical(f"Provided config JSON is Invalid: '{config_path}'")
- return False
- return True
+ raise Exception(f"Provided config JSON is Invalid: '{config_path}'")