Add basic barcode scanner

This commit is contained in:
= 2021-01-17 20:41:51 +01:00
parent 1a611fbea1
commit 26c4406401
6 changed files with 443 additions and 116 deletions

View File

@ -13,7 +13,7 @@ import {
import { useDispatch } from 'react-redux'
import {Add as AddIcon} from "@material-ui/icons";
import SearchInput from 'components/SearchInput'
import BarcodeScanner from 'components/BarcodeScanner'
import BarcodeScanner from 'containers/BarcodeScanner'
import { createStructuredSelector } from 'reselect'
import { useSelector } from 'react-redux'
import { makeSelectProducts } from 'pages/Home/selectors'

View File

@ -1,17 +0,0 @@
import React from 'react';
import {IconButton, Grid} from '@material-ui/core'
import {CropFree as CropFreeIcon} from '@material-ui/icons'
const BarcodeScanner = () => {
return (
<React.Fragment>
<Grid item xs={2}>
<IconButton color="primary">
<CropFreeIcon />
</IconButton>
</Grid>
</React.Fragment>
);
}
export default BarcodeScanner;

View File

@ -0,0 +1,312 @@
import React, { useState, useEffect } from 'react';
import {IconButton, Grid} from '@material-ui/core'
import {CropFree as CropFreeIcon} from '@material-ui/icons'
import './styles.css'
const BarcodeScanner = () => {
const [decodecBarcode, setDecodedBarcode] = useState('');
const dimensions = {
height: 0,
width: 0,
start: 0,
end: 0
}
const elements = {
video: null,
canvas: null,
ctx: null,
canvasg: null,
ctxg: null
}
const upc = {
'0': [3, 2, 1, 1],
'1': [2, 2, 2, 1],
'2': [2, 1, 2, 2],
'3': [1, 4, 1, 1],
'4': [1, 1, 3, 2],
'5': [1, 2, 3, 1],
'6': [1, 1, 1, 4],
'7': [1, 3, 1, 2],
'8': [1, 2, 1, 3],
'9': [3, 1, 1, 2]
};
const check = {
'oooooo': '0',
'ooeoee': '1',
'ooeeoe': '2',
'ooeeeo': '3',
'oeooee': '4',
'oeeooe': '5',
'oeeeoo': '6',
'oeoeoe': '7',
'oeoeeo': '8',
'oeeoeo': '9'
}
const config = {
strokeColor: '#f00',
start: 0.1,
end: 0.9,
threshold: 160,
quality: 0.45,
delay: 100,
video: '#barcodevideo',
canvas: '#barcodecanvas',
canvasg: '#barcodecanvasg'
}
const init = () => {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
elements.video = document.querySelector(config.video);
elements.canvas = document.querySelector(config.canvas);
elements.ctx = elements.canvas.getContext('2d');
elements.canvasg = document.querySelector(config.canvasg);
elements.ctxg = elements.canvasg.getContext('2d');
if (navigator.getUserMedia) {
navigator.getUserMedia({audio: false, video: true}, function(stream) {
elements.video.srcObject = stream;
}, function(error) {
console.log(error);
});
}
elements.video.addEventListener('canplay', () => {
dimensions.height = elements.video.videoHeight;
dimensions.width = elements.video.videoWidth;
dimensions.start = dimensions.width * config.start;
dimensions.end = dimensions.width * config.end;
elements.canvas.width = dimensions.width;
elements.canvas.height = dimensions.height;
elements.canvasg.width = dimensions.width;
elements.canvasg.height = dimensions.height;
drawLine(elements.ctxg);
setInterval(() => {
const snapshot = takeSnapshot(elements.ctx, elements.video)
processSnapshot(snapshot)
}, config.delay);
}, false);
}
const takeSnapshot = (canvasElement, videoElement) => {
canvasElement.drawImage(videoElement, 0, 0, dimensions.width, dimensions.height);
return canvasElement.getImageData(dimensions.start, dimensions.height * 0.5, dimensions.end - dimensions.start, 1).data;
}
const processSnapshot = (snapshot) => {
const bars = [];
const pixels = [];
let pixelBars = [];
// convert to grayscale
for (let i = 0; i < snapshot.length; i += 4) {
pixels.push(Math.round(snapshot[i] * 0.2126 + snapshot[i + 1] * 0.7152 + snapshot[ i + 2] * 0.0722));
}
// normalize and convert to binary
const minPixel = Math.min(...pixels);
const maxPixel = Math.max(...pixels);
const binary = pixels.reduce((arr, val) => {
const binaryValue = Math.round((val - minPixel) / (maxPixel - minPixel) * 255) > config.threshold ? 1 : 0
return [...arr, binaryValue]
}, []);
// determine bar widths
let current = binary[0];
let count = 0;
for (let i = 0; i < binary.length; i++) {
if (binary[i] === current) {
count++;
} else {
pixelBars.push(count);
count = 1;
current = binary[i]
}
}
pixelBars.push(count);
// quality check
if (pixelBars.length < (3 + 24 + 5 + 24 + 3 + 1)) {
return;
}
// find starting sequence
let startIndex = 0;
const minFactor = 0.5;
const maxFactor = 1.5;
for (let i = 3; i < pixelBars.length; i++) {
const refLength = (pixelBars[i] + pixelBars[i-1] + pixelBars[i-2]) / 3;
if (
(pixelBars[i] > (minFactor * refLength) || pixelBars[i] < (maxFactor * refLength))
&& (pixelBars[i-1] > (minFactor * refLength) || pixelBars[i-1] < (maxFactor * refLength))
&& (pixelBars[i-2] > (minFactor * refLength) || pixelBars[i-2] < (maxFactor * refLength))
&& (pixelBars[i-3] > 3 * refLength)
) {
startIndex = i - 2;
break;
}
}
console.log("startIndex: " + startIndex );
// return if no starting sequence found
if (startIndex === 0) {
return;
}
// discard leading and trailing patterns
pixelBars = pixelBars.slice(startIndex, startIndex + 3 + 24 + 5 + 24 + 3);
console.log("pixelBars: " + pixelBars );
// calculate relative widths
const ref = (pixelBars[0] + pixelBars[1] + pixelBars[2]) / 3;
for (let i = 0; i < pixelBars.length; i++) {
bars.push(Math.round(pixelBars[i] / ref * 100) / 100);
}
// analyze pattern
analyzePattern(bars);
}
const analyzePattern = (bars) => {
console.clear();
console.log("analyzing");
// determine parity first digit and reverse sequence if necessary
const first = normalize(bars.slice(3, 3 + 4), 7);
if (!isOdd(Math.round(first[1] + first[3]))) {
bars = bars.reverse();
}
// split into digits
const digits = [
normalize(bars.slice(3, 3 + 4), 7),
normalize(bars.slice(7, 7 + 4), 7),
normalize(bars.slice(11, 11 + 4), 7),
normalize(bars.slice(15, 15 + 4), 7),
normalize(bars.slice(19, 19 + 4), 7),
normalize(bars.slice(23, 23 + 4), 7),
normalize(bars.slice(32, 32 + 4), 7),
normalize(bars.slice(36, 36 + 4), 7),
normalize(bars.slice(40, 40 + 4), 7),
normalize(bars.slice(44, 44 + 4), 7),
normalize(bars.slice(48, 48 + 4), 7),
normalize(bars.slice(52, 52 + 4), 7)
]
console.log("digits: " + digits);
// determine parity and reverse if necessary
const parities = [];
for (let i = 0; i < 6; i++) {
if (parity(digits[i])) {
parities.push('o');
} else {
parities.push('e');
digits[i] = digits[i].reverse();
}
}
// identify digits
const result = [];
let quality = 0;
for (let i = 0; i < digits.length; i++) {
let distance = 9;
let bestKey = '';
for (let key in upc) {
if (maxDistance(digits[i], upc[key]) < distance) {
distance = maxDistance(digits[i], upc[key]);
bestKey = key;
}
}
result.push(bestKey);
if (distance > quality) {
quality = distance;
}
}
console.log("result: " + result);
// check digit
const checkDigit = check[parities.join('')];
// output
console.log("quality: " + quality);
if(quality < config.quality) {
setDecodedBarcode(checkDigit + result.join(''))
}
}
const normalize = (input, total) => {
const sum = input.reduce((acc, val) => acc + val, 0);
return input.reduce((acc, val) => [...acc, val / sum * total], []);
}
const isOdd = (num) => num % 2;
const maxDistance = (a, b) =>
a.reduce((max, value, index) => {
const current = Math.abs(value - b[index])
return current > max ? current : max
}, 0)
const parity = (digit) => isOdd(Math.round(digit[1] + digit[3]))
const drawLine = (canvasContext) => {
canvasContext.strokeStyle = config.strokeColor;
canvasContext.lineWidth = 3;
canvasContext.beginPath();
canvasContext.moveTo(dimensions.start, dimensions.height * 0.5);
canvasContext.lineTo(dimensions.end, dimensions.height * 0.5);
canvasContext.stroke();
}
useEffect(() => {
init()
}, [])
return (
<React.Fragment>
<div id="barcode">
<video id="barcodevideo" autoPlay />
<canvas id="barcodecanvasg" />
</div>
<canvas id="barcodecanvas" />
{decodecBarcode === "5449000136350"}
{decodecBarcode}
</React.Fragment>
);
}
export default BarcodeScanner;

View File

@ -0,0 +1,22 @@
#barcodevideo, #barcodecanvas, #barcodecanvasg {
height: 400px;
}
#barcodecanvasg {
position: absolute;
top: 0px;
left: 0px;
}
#result {
font-family: verdana;
font-size: 1.5em;
}
#barcode {
position: relative;
}
#barcodecanvas {
display: none;
}

View File

@ -11,6 +11,8 @@ import { getMealsAction } from './actions'
import MealCard from "components/MealCard";
import { createStructuredSelector } from 'reselect'
import BarcodeScanner from 'containers/BarcodeScanner'
const stateSelector = createStructuredSelector({
meals: makeSelectMeals()
})

View File

@ -1,100 +1,108 @@
import React from 'react';
import { useInjectReducer, useInjectSaga } from 'redux-injectors';
import { useDispatch } from 'react-redux';
import {
Container,
Grid,
Button,
Typography,
FormControlLabel,
Checkbox,
Link,
} from '@material-ui/core'
import {routes} from "utils";
import InputField from 'components/InputField'
import { Formik, Form } from 'formik';
import { loginAction } from './actions'
import useStyles from './styles'
import validationSchema from './FormModel/validationSchema'
import formInitialValues from './FormModel/formInitialValues'
import loginFormModel from './FormModel/loginFormModel'
import saga from "./saga";
// import React from 'react';
// import { useInjectReducer, useInjectSaga } from 'redux-injectors';
// import { useDispatch } from 'react-redux';
// import {
// Container,
// Grid,
// Button,
// Typography,
// FormControlLabel,
// Checkbox,
// Link,
// } from '@material-ui/core'
// import {routes} from "utils";
// import InputField from 'components/InputField'
// import { Formik, Form } from 'formik';
// import { loginAction } from './actions'
// import useStyles from './styles'
// import validationSchema from './FormModel/validationSchema'
// import formInitialValues from './FormModel/formInitialValues'
// import loginFormModel from './FormModel/loginFormModel'
// import saga from "./saga";
//
// const {
// formId,
// formField: {
// email,
// password
// }
// } = loginFormModel;
//
// const key = 'loginPage'
//
// const Login = () => {
// useInjectSaga({ key, saga });
// const classes = useStyles()
// const dispatch = useDispatch();
//
// const handleSubmit = (values, actions) => {
// dispatch(loginAction(values))
// }
//
// return (
// <Container component="main" maxWidth="xs">
// <div className={classes.paper}>
// <Typography component="h1" variant="h5">
// Login to Account
// </Typography>
// <Formik
// initialValues={formInitialValues}
// validationSchema={validationSchema}
// onSubmit={handleSubmit}
// >
// {({ isSubmitting }) => (
// <Form id={formId}>
// <InputField
// type="text"
// label={email.label}
// name={email.name}
// variant="outlined"
// fullWidth
// className={classes.input}
// />
// <InputField
// type="password"
// label={password.label}
// name={password.name}
// variant="outlined"
// fullWidth
// className={classes.input}
// />
// <FormControlLabel
// control={<Checkbox value="remember" color="primary" />}
// label="Remember me"
// />
// <Button
// type="submit"
// fullWidth
// variant="contained"
// color="primary"
// className={classes.submit}
// disabled={isSubmitting}
// >
// Sign Up
// </Button>
// </Form>
// )}
// </Formik>
// <Grid container alignItems="flex-end">
// <Grid item>
// <Link href={routes.register.path} color="secondary" variant="body2">
// Don't have an account? Sign Up
// </Link>
// </Grid>
// </Grid>
// </div>
// </Container>
// );
// };
//
// export default Login;
const {
formId,
formField: {
email,
password
}
} = loginFormModel;
import BarcodeScanner from "../../containers/BarcodeScanner";
import {Container} from "@material-ui/core";
import React from "react";
const key = 'loginPage'
const Login = () => {
useInjectSaga({ key, saga });
const classes = useStyles()
const dispatch = useDispatch();
const handleSubmit = (values, actions) => {
dispatch(loginAction(values))
}
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Login to Account
</Typography>
<Formik
initialValues={formInitialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting }) => (
<Form id={formId}>
<InputField
type="text"
label={email.label}
name={email.name}
variant="outlined"
fullWidth
className={classes.input}
/>
<InputField
type="password"
label={password.label}
name={password.name}
variant="outlined"
fullWidth
className={classes.input}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
disabled={isSubmitting}
>
Sign Up
</Button>
</Form>
)}
</Formik>
<Grid container alignItems="flex-end">
<Grid item>
<Link href={routes.register.path} color="secondary" variant="body2">
Don't have an account? Sign Up
</Link>
</Grid>
</Grid>
</div>
</Container>
);
};
export default Login;
export default () => (
<BarcodeScanner />
)