Skip to content

Instantly share code, notes, and snippets.

@cgutwin
Last active September 17, 2020 06:38
Show Gist options
  • Select an option

  • Save cgutwin/b3473e87b912ef0349927ee20624a607 to your computer and use it in GitHub Desktop.

Select an option

Save cgutwin/b3473e87b912ef0349927ee20624a607 to your computer and use it in GitHub Desktop.
An implementation of the Quagga QR scanner as a React component.
// Copyright (c) 2020 Chris Gutwin. All rights reserved.
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://choosealicense.com/licenses/mit/>.
//
// An implementation of the Quagga 1 QR scanner as a React component.
// Mitigates false positives by quickly taking multiple scans, and using the most detected of n = 40 scans.
// This isn't really needed anymore from my limited testing, Quagga 2 (@ericblade/quagga2) seems to have fewer false positives.
// Feel free to abstract away the non-React functions. They clutter up the component and make it feel non-component like.
import Quagga from 'quagga'
import React, { useEffect, useState } from 'react'
function getWindowDimensions() {
const width = window.innerWidth
const height = window.outerHeight
return {
width,
height
}
}
/**
* Counts how many of each entry is in an array.
* Used for arrays with a depth of 1.
*
* Returns an object, with the length of the initial array, with each of the unique array entries as keys, with their
* respective counts as values.
*/
function countArrayValues(array) {
const countedArray = {
// Used in the keyCountWinRate function so I don't have to create an iterable of this returned object and
// count it that way. Simpler to include the length of the initial array in the passed parameter.
totalCountedItems: array.length,
count: {}
}
for (const entry of array) {
const entryName = entry.toString()
// If the array value exists in the counted array, increment its count by one.
if (countedArray.count[entryName]) ++countedArray.count[entryName]
// Else initialize the entry with a value of 1.
else countedArray.count[entryName] = 1
}
return countedArray
}
/**
* Takes an object in the form returned by countArrayValues and determines which value has the highest count.
*/
function getHighestRateOfValues(countedArray) {
// The current winning value of the countedArray where rate is the percentage the value occurs.
const winner = {
value: "",
rate: 0
}
// Object.entries(object) makes an object iterable based on its entries.
// Value is the number of occurrences of key
for (const [ key, value ] of Object.entries(countedArray.count)) {
const rate = value / countedArray.totalCountedItems
// If the new rate calculated is greater than the last highest rate, set the new K/V to be the winner.
if (rate >= winner.rate) {
winner.value = key.toString()
winner.rate = rate
}
}
return winner
}
function Scanner() {
const [ codes, setCodes ] = useState([])
// The win percent is the confidence in the rate to be deemed "correct"
const detectCodeWinner = (winPercent) => {
const itemCounts = countArrayValues(codes)
const highestRate = getHighestRateOfValues(itemCounts)
if (highestRate.rate > winPercent) {
Quagga.offDetected()
// Do what you want with the scanned winner here. You probably still want offDetected and stop to kill the camera.
Quagga.stop()
alert(highestRate.value)
}
}
useEffect(() => {
// noinspection JSSuspiciousNameCombination
// You can abstract this config to clean up this file.
Quagga.init({
inputStream: {
name: 'Live',
type: 'LiveStream',
// It seems that the width and height constraints affect their counterparts? Still does in Quagga 2.
constraints: {
width: getWindowDimensions().height,
height: getWindowDimensions().width,
facingMode: 'environment'
},
target: '#scanner_viewport'
},
locator: {
halfSample: false,
patchSize: 'medium'
},
locate: true,
numOfWorkers: 4,
decoder: {
readers: ['upc_reader'],
multiple: false
}
}, (err) => {
if (err) {
console.error(err)
window.alert(err)
}
Quagga.start()
})
Quagga.onDetected(result => {
setCodes([...codes, result.codeResult.code])
// Start with a sample of 6, and if the code isn't of a majority, scan more up to 40.
// Not a switch statement as fallthrough is intended.
if (scannedCodes.length === 6) detectCodeWinner(0.50)
if (scannedCodes.length === 16) detectCodeWinner(0.50)
if (scannedCodes.length === 40) detectCodeWinner(0.50)
if (scannedCodes.length === 41) detectCodeWinner(0.25)
})
return () => {
Quagga.offDetected()
Quagga.stop()
}
// detectCodeWinner, according to the linter, should be in the deps list
// I just want this code to execute onMount and unmount.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div style={{ maxHeight: "100vh", overflow: "hidden" }}>
<div id="scanner_viewport" />
</div>
)
}
export default Scanner
@cgutwin
Copy link
Author

cgutwin commented Sep 17, 2020

Changed the supporting functions for false-positive mitigation.

Also introduced a local state, and changed over some variables from that. I didn't test it as I've moved to @ericblade/quagga2 and no longer need this mitigation. To see how the previous version was implemented without state (if my changes with it don't work) view this revision.

Also removed the styled-components dependency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment