|
| 1 | +<html> |
| 2 | + <head> |
| 3 | +<!-- 2016 Gordon Williams, [email protected] |
| 4 | +
|
| 5 | +Any copyright is dedicated to the Public Domain. |
| 6 | +http://creativecommons.org/publicdomain/zero/1.0/ |
| 7 | +
|
| 8 | +--> |
| 9 | + <meta charset="utf-8"> |
| 10 | + <meta name="viewport" content="width=320, initial-scale=1"> |
| 11 | + <title>Online Heart Rate Monitor</title> |
| 12 | + </head> |
| 13 | + <body> |
| 14 | + <p>Allow this website to use your webcam, then place your finger lightly over the camera and wait for the trace to stabilise.</p> |
| 15 | + <p>You will have most success when there is light behind your finger.</p> |
| 16 | + <video id="v" width="100" height="100" style="display:none"></video> |
| 17 | + <canvas id="c" width="100" height="100" style="display:none"></canvas> |
| 18 | + <canvas id="g" width="320" height="30"></canvas><br/> |
| 19 | + <div id="bpm">--</div> |
| 20 | + <p><a href="https://github.com/gfwilliams/HeartRate">GitHub</a></p> |
| 21 | + <p>Also, check out <a href="http://www.puck-js.com/">Puck.js</a></p> |
| 22 | + <script> |
| 23 | +var video, width, height, context, graphCanvas, graphContext, bpm; |
| 24 | +var hist = []; |
| 25 | +navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia; |
| 26 | + |
| 27 | +var constraints = {video: true, audio:false}; |
| 28 | + |
| 29 | +function initialize() { |
| 30 | +navigator.mediaDevices.enumerateDevices().then(function(devices) { |
| 31 | + devices.forEach(function(device) { |
| 32 | + console.log(device.kind + ": " + device.label + |
| 33 | + " id = " + device.deviceId/*, JSON.stringify(device,null,2)*/); |
| 34 | + if (device.kind=="videoinput" /*&& constraints.video===true*/) |
| 35 | + constraints.video = { optional: [{sourceId: device.deviceId}, { fillLightMode: "on" }] }; |
| 36 | + }); |
| 37 | + initialize2(); |
| 38 | +}).catch(function(err) { |
| 39 | + console.log(err.name + ": " + err.message); |
| 40 | +}); |
| 41 | +} |
| 42 | + |
| 43 | + function initialize2() { |
| 44 | + // The source video. |
| 45 | + video = document.getElementById("v"); |
| 46 | + width = video.width; |
| 47 | + height = video.height; |
| 48 | + |
| 49 | + // The target canvas. |
| 50 | + var canvas = document.getElementById("c"); |
| 51 | + context = canvas.getContext("2d"); |
| 52 | + |
| 53 | + // The canvas for the graph |
| 54 | + graphCanvas = document.getElementById("g"); |
| 55 | + graphContext = graphCanvas.getContext("2d"); |
| 56 | + |
| 57 | + // The bpm meter |
| 58 | + bpm = document.getElementById("bpm"); |
| 59 | + |
| 60 | + // Get the webcam's stream. |
| 61 | + navigator.getUserMedia(constraints, startStream, function () {}); |
| 62 | + } |
| 63 | + |
| 64 | + function startStream(stream) { |
| 65 | + video.src = URL.createObjectURL(stream); |
| 66 | + video.play(); |
| 67 | + // Ready! Let's start drawing. |
| 68 | + requestAnimationFrame(draw); |
| 69 | + } |
| 70 | + |
| 71 | + function draw() { |
| 72 | + var frame = readFrame(); |
| 73 | + if (frame) { |
| 74 | + getIntensity(frame.data); |
| 75 | + } |
| 76 | + |
| 77 | + // Wait for the next frame. |
| 78 | + requestAnimationFrame(draw); |
| 79 | + } |
| 80 | + |
| 81 | + function readFrame() { |
| 82 | + try { |
| 83 | + context.drawImage(video, 0, 0, width, height); |
| 84 | + } catch (e) { |
| 85 | + // The video may not be ready, yet. |
| 86 | + return null; |
| 87 | + } |
| 88 | + |
| 89 | + return context.getImageData(0, 0, width, height); |
| 90 | + } |
| 91 | + |
| 92 | + function getIntensity(data) { |
| 93 | + var len = data.length; |
| 94 | + var sum = 0; |
| 95 | + |
| 96 | + for (var i = 0, j = 0; j < len; i++, j += 4) { |
| 97 | + sum += data[j] + data[j+1] + data[j+2]; |
| 98 | + } |
| 99 | + //console.log(sum / len); |
| 100 | + hist.push({ bright : sum/len, time : Date.now() }); |
| 101 | + while (hist.length>graphCanvas.width) hist.shift(); |
| 102 | + // max and min |
| 103 | + var max = hist[0].bright; |
| 104 | + var min = hist[0].bright; |
| 105 | + hist.forEach(function(v) { |
| 106 | + if (v.bright>max) max=v.bright; |
| 107 | + if (v.bright<min) min=v.bright; |
| 108 | + }); |
| 109 | + // thresholds for bpm |
| 110 | + var lo = min*0.6 + max*0.4; |
| 111 | + var hi = min*0.4 + max*0.6; |
| 112 | + var pulseAvr = 0, pulseCnt = 0; |
| 113 | + // draw |
| 114 | + var ctx = graphContext; |
| 115 | + ctx.clearRect(0, 0, graphCanvas.width, graphCanvas.height); |
| 116 | + ctx.beginPath(); |
| 117 | + ctx.moveTo(0,0); |
| 118 | + hist.forEach(function(v,x) { |
| 119 | + var y = graphCanvas.height*(v.bright-min)/(max-min); |
| 120 | + ctx.lineTo(x,y); |
| 121 | + }); |
| 122 | + ctx.stroke(); |
| 123 | + // work out bpm |
| 124 | + var isHi = undefined; |
| 125 | + var lastHi = undefined; |
| 126 | + var lastLo = undefined; |
| 127 | + ctx.fillStyle = "red"; |
| 128 | + hist.forEach(function(v, x) { |
| 129 | + if (isHi!=true && v.bright>hi) { |
| 130 | + isHi = true; |
| 131 | + lastLo = x; |
| 132 | + } |
| 133 | + if (isHi!=false && v.bright<lo) { |
| 134 | + if (lastHi !== undefined && lastLo !== undefined) { |
| 135 | + pulseAvr += hist[x].time-hist[lastHi].time; |
| 136 | + pulseCnt++; |
| 137 | + ctx.fillRect(lastLo,graphCanvas.height-4,lastHi-lastLo,4); |
| 138 | + } |
| 139 | + isHi = false; |
| 140 | + lastHi = x; |
| 141 | + } |
| 142 | + }); |
| 143 | + // write bpm |
| 144 | + if (pulseCnt) { |
| 145 | + var pulseRate = 60000 / (pulseAvr / pulseCnt); |
| 146 | + bpm.innerHTML = pulseRate.toFixed(0)+" BPM ("+pulseCnt+" pulses)"; |
| 147 | + } else { |
| 148 | + bpm.innerHTML = "-- BPM"; |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + addEventListener("DOMContentLoaded", initialize); |
| 153 | + </script> |
| 154 | + </body> |
| 155 | +</html> |
0 commit comments