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 Buy Me A Coffee [![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}'")