@votingworks/hmpb-interpreter

Interprets hand-marked paper ballots.

Usage no npm install needed!

<script type="module">
  import votingworksHmpbInterpreter from 'https://cdn.skypack.dev/@votingworks/hmpb-interpreter';
</script>

README

hmpb-interpreter

Interprets VotingWorks ballots marked by hand and scanned into images.

Install

# Local install for API usage.
$ yarn add @votingworks/hmpb-interpreter
$ npm install @votingworks/hmpb-interpreter # or, with npm

# Global install for CLI usage, or just use `npx hmpb-interpreter`.
$ yarn global add @votingworks/hmpb-interpreter
$ npm install -g @votingworks/hmpb-interpreter # or, with npm

API Usage

import { Interpreter } from '@votingworks/hmpb-interpreter'

const interpreter = new Interpreter({
  // Configure contests via an election.json. Contests on printed ballots must
  // appear in the same order they appear in this configuration.
  election,

  // Require at least 20% filled in targets.
  markScoreVoteThreshold: 0.2,
})

while (interpreter.hasMissingTemplates()) {
  // Templates are images of blank ballots.
  await interpreter.addTemplate(await getNextImage())
}

console.log('Interpreter has templates for all ballot styles and contests!')

const imageData = await getNextImage()
const { ballot } = await interpreter.interpretBallot(imageData)

console.log('Interpreted ballot:', ballot)

Customizing QR Code Decoder

This library uses node-quirc to decode QR codes. If you wish to supply your own decoder, pass decodeQRCode to Interpreter like so:

// Example custom QR code reader using jsQR
import jsQR from 'jsqr'
import { Interpreter } from '@votingworks/hmpb-interpreter'

const interpreter = new Interpreter({
  election,
  async decodeQRCode(imageData: ImageData): Promise<Buffer | undefined> {
    const code = jsQR(imageData.data, imageData.width, imageData.height)
    return code ? Buffer.from(code.binaryData) : undefined
  })
})

CLI Usage

# To try this example out, install globally then clone the repository and `cd`
# to `test/fixtures/election-4e31cb17d8-ballot-style-77-precinct-oaklawn-branch-library`.
$ hmpb interpret -e election.json \
  blank-p1.jpg \
  blank-p2.jpg \
  filled-in-p1.jpg \
  filled-in-p2.jpg
╔═══════════════════════════════════════════════════════╤═══════════════════════╤══════════════════╗
║ Contest                                               │ filled-in-p1.jpg      │ filled-in-p2.jpg ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Member, U.S. Senate                                   │ Tim Smith             │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Member, U.S. House, District 30                       │ Eddie Bernice Johnson │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Judge, Texas Supreme Court, Place 6                   │ Jane Bland            │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Member, Texas House of Representatives, District 111  │ Write-In              │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Dallas County Tax Assessor-Collector                  │ John Ames             │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Dallas County Sheriff                                 │ Chad Prda             │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Member, Dallas County Commissioners Court, Precinct 3 │                       │ Andrew Jewell    ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Retain Robert Demergue as Chief Justice?              │                       │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Proposition R: Countywide Recycling Program           │                       │ no               ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ City Council                                          │                       │                  ║
╟───────────────────────────────────────────────────────┼───────────────────────┼──────────────────╢
║ Mayor                                                 │                       │                  ║
╚═══════════════════════════════════════════════════════╧═══════════════════════╧══════════════════╝

# You can also request output as JSON.
$ hmpb interpret -e election.json -f json \
  blank-p1.jpg \
  blank-p2.jpg \
  filled-in-p1.jpg \
  filled-in-p2.jpg
[
  {
    "input": "filled-in-p1.jpg",
    "interpreted": {
      "metadata": {
        "locales": {
          "primary": "en-US"
        },
        "ballotStyleId": "77",
        "precinctId": "42",
        "isTestMode": false,
        "pageNumber": 1
      },
      "votes": {
        "us-senate": [
          {
            "id": "tim-smith",
            "name": "Tim Smith",
            "partyId": "6"
          }
        ],
        "us-house-district-30": [
          {
            "id": "eddie-bernice-johnson",
            "name": "Eddie Bernice Johnson",
            "partyId": "2"
          }
        ],
        "texas-sc-judge-place-6": [
          {
            "id": "jane-bland",
            "name": "Jane Bland",
            "partyId": "3"
          }
        ],
        "texas-house-district-111": [
          {
            "id": "__write-in-0",
            "name": "Write-In",
            "isWriteIn": true
          }
        ],
        "dallas-county-tax-assessor": [
          {
            "id": "john-ames",
            "name": "John Ames",
            "partyId": "2"
          }
        ],
        "dallas-county-sheriff": [
          {
            "id": "chad-prda",
            "name": "Chad Prda",
            "partyId": "3"
          }
        ]
      },
      "marks": [
        {
          "type": "candidate",
          "contest": "us-senate",
          "option": "tim-smith",
          "score": 0.7901554404145078,
          "bounds": {
            "x": 470,
            "y": 411,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 411,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 472,
              "y": 413,
              "width": 28,
              "height": 17
            }
          }
        },
        {
          "type": "candidate",
          "contest": "us-house-district-30",
          "option": "eddie-bernice-johnson",
          "score": 0.6709844559585493,
          "bounds": {
            "x": 470,
            "y": 831,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 831,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 472,
              "y": 833,
              "width": 28,
              "height": 17
            }
          }
        },
        {
          "type": "candidate",
          "contest": "texas-sc-judge-place-6",
          "option": "jane-bland",
          "score": 0.5717884130982368,
          "bounds": {
            "x": 470,
            "y": 1173,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 1173,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 472,
              "y": 1175,
              "width": 28,
              "height": 18
            }
          }
        },
        {
          "type": "candidate",
          "contest": "texas-house-district-111",
          "option": "__write-in-0",
          "score": 0.66,
          "bounds": {
            "x": 872,
            "y": 320,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 872,
              "y": 320,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 874,
              "y": 322,
              "width": 28,
              "height": 18
            }
          }
        },
        {
          "type": "candidate",
          "contest": "dallas-county-tax-assessor",
          "option": "john-ames",
          "score": 0.7860824742268041,
          "bounds": {
            "x": 872,
            "y": 556,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 872,
              "y": 556,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 874,
              "y": 558,
              "width": 28,
              "height": 17
            }
          }
        },
        {
          "type": "candidate",
          "contest": "dallas-county-sheriff",
          "option": "chad-prda",
          "score": 0.601010101010101,
          "bounds": {
            "x": 872,
            "y": 916,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 872,
              "y": 916,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 874,
              "y": 917,
              "width": 28,
              "height": 18
            }
          }
        }
      ]
    }
  },
  {
    "input": "filled-in-p2.jpg",
    "interpreted": {
      "metadata": {
        "locales": {
          "primary": "en-US"
        },
        "ballotStyleId": "77",
        "precinctId": "42",
        "isTestMode": false,
        "pageNumber": 2
      },
      "votes": {
        "dallas-county-commissioners-court-pct-3": [
          {
            "id": "andrew-jewell",
            "name": "Andrew Jewell",
            "partyId": "7"
          }
        ],
        "dallas-county-proposition-r": [
          "no"
        ]
      },
      "marks": [
        {
          "type": "candidate",
          "contest": "dallas-county-commissioners-court-pct-3",
          "option": "andrew-jewell",
          "score": 0.6624685138539043,
          "bounds": {
            "x": 67,
            "y": 398,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 67,
              "y": 398,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 69,
              "y": 400,
              "width": 28,
              "height": 18
            }
          }
        },
        {
          "type": "yesno",
          "contest": "dallas-county-retain-chief-justice",
          "option": "yes",
          "score": 0.14910025706940874,
          "bounds": {
            "x": 67,
            "y": 869,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 67,
              "y": 869,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 69,
              "y": 870,
              "width": 28,
              "height": 18
            }
          }
        },
        {
          "type": "yesno",
          "contest": "dallas-county-proposition-r",
          "option": "no",
          "score": 0.7455470737913485,
          "bounds": {
            "x": 470,
            "y": 365,
            "width": 32,
            "height": 22
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 365,
              "width": 32,
              "height": 22
            },
            "inner": {
              "x": 472,
              "y": 367,
              "width": 28,
              "height": 18
            }
          }
        },
        {
          "type": "candidate",
          "contest": "dallas-city-council",
          "option": "randall-rupp",
          "score": 0.13110539845758354,
          "bounds": {
            "x": 470,
            "y": 647,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 647,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 472,
              "y": 649,
              "width": 28,
              "height": 17
            }
          }
        },
        {
          "type": "candidate",
          "contest": "dallas-city-council",
          "option": "donald-davis",
          "score": 0.13212435233160622,
          "bounds": {
            "x": 470,
            "y": 881,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 881,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 472,
              "y": 883,
              "width": 28,
              "height": 17
            }
          }
        },
        {
          "type": "candidate",
          "contest": "dallas-city-council",
          "option": "__write-in-1",
          "score": 0.09090909090909091,
          "bounds": {
            "x": 470,
            "y": 1087,
            "width": 32,
            "height": 21
          },
          "target": {
            "bounds": {
              "x": 470,
              "y": 1087,
              "width": 32,
              "height": 21
            },
            "inner": {
              "x": 472,
              "y": 1089,
              "width": 28,
              "height": 18
            }
          }
        }
      ]
    }
  }
]

License

GPL-3.0