Created
November 20, 2017 10:50
-
-
Save seanmtracey/75013cd50ce51cc6300becf6a8c1dd09 to your computer and use it in GitHub Desktop.
A Node-red flow for extracting keyframes from videos and analysing them with Watson Visual Recogntion
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[{"id":"146f4acc.4733c5","type":"function","z":"ceee673f.122fb8","name":"Determine File Path","func":"if (msg.req.files) {\n var files = Object.keys(msg.req.files);\n msg.payload.filePath = msg.req.files[files[0]][0].path; \n}\nreturn msg;","outputs":1,"noerr":0,"x":278.5,"y":452,"wires":[["c4cd6b26.2acfc8"]]},{"id":"3bf669a6.9b88e6","type":"httpInMultipart","z":"ceee673f.122fb8","name":"","url":"/analyse","method":"post","fields":"[ { \"name\": \"video\", \"maxCount\" : 1 } ]","swaggerDoc":"","x":145,"y":387,"wires":[["146f4acc.4733c5"]]},{"id":"e1f10989.eab7c8","type":"visual-recognition-v3","z":"ceee673f.122fb8","name":"Analyse frame","apikey":"50a86834a2d8947a442a1ba81e9dde323f074786","image-feature":"classifyImage","lang":"en","x":628,"y":652,"wires":[["1a442f8b.94618","5f0f5910.f08c08"]]},{"id":"5f0f5910.f08c08","type":"function","z":"ceee673f.122fb8","name":"Make Sense","func":"\nconst jobData = flow.get(msg.analysisUUID);\nconst frameData = {\n time : msg.keyframeTimeoffset,\n result : msg.result\n};\n\njobData.frames.push(frameData);\nflow.set(msg.analysisUUID, jobData);\n\nif(jobData.frames.length === jobData.total){\n \n \n jobData.frames.sort( (a, b) => {\n if(a.time > b.time){\n return 1\n } else {\n return -1;\n }\n });\n \n msg.payload = jobData;\n \n \n return msg;\n\n}\n","outputs":1,"noerr":0,"x":766.5,"y":713,"wires":[["4a91b005.51ccb"]]},{"id":"a9429c5e.b1b59","type":"function","z":"ceee673f.122fb8","name":"Filter","func":"if(!msg.finished){\n \n if(msg.firstFrame === true){\n flow.set(msg.analysisUUID, {\n frames : [],\n total : -1\n });\n return [msg, msg];\n }\n return [null, msg];\n} else {\n const existingData = flow.get(msg.analysisUUID);\n existingData.total = msg.totalFrames;\n flow.set(msg.analysisUUID, existingData);\n}\n","outputs":"2","noerr":0,"x":519.5,"y":579,"wires":[["aac384ef.f4d3e8"],["e1f10989.eab7c8"]]},{"id":"4a91b005.51ccb","type":"debug","z":"ceee673f.122fb8","name":"","active":true,"console":"false","complete":"true","x":959.5,"y":636,"wires":[]},{"id":"887589b5.759de8","type":"template","z":"ceee673f.122fb8","name":"HTML","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <title>Watson Keyframe Visual Recognition Demo</title>\n <style>{{{payload.styles}}}</style>\n</head>\n<body>\n\n<header></header>\n \n<h1>Watson Keyframe Visual Recognition Demo</h1>\n\n<div id=\"dropzone\">\n <p>Drop a media file</p>\n</div>\n\n<div id=\"describe\"></div>\n\n<div id=\"keyframes\">\n <input type=\"text\" placeholder=\"Filter keyframes\" />\n <div id=\"frames\"></div>\n</div>\n\n<script>\n \n {{{payload.script}}}\n \n</script>\n\n</body>\n</html>","output":"str","x":427.5,"y":273,"wires":[["e545c9c5.1eae48"]]},{"id":"189c82ba.8ae70d","type":"http in","z":"ceee673f.122fb8","name":"","url":"/demo","method":"get","upload":false,"swaggerDoc":"","x":143.5,"y":149,"wires":[["574bc94c.630238"]]},{"id":"e545c9c5.1eae48","type":"http response","z":"ceee673f.122fb8","name":"","statusCode":"","headers":{},"x":508.5,"y":321,"wires":[]},{"id":"98a6819b.d2ebe","type":"websocket out","z":"ceee673f.122fb8","name":"Emit Frames","server":"82e881e.8e4028","client":"","x":996.5,"y":562,"wires":[]},{"id":"1a442f8b.94618","type":"function","z":"ceee673f.122fb8","name":"Format Result","func":"return {\n payload : {\n result : msg.result,\n time : msg.keyframeTimeoffset,\n uuid : msg.analysisUUID,\n image : msg.payload,\n frameNumber : msg.frameNumber\n }\n};","outputs":1,"noerr":0,"x":755.5,"y":566,"wires":[["98a6819b.d2ebe","4a91b005.51ccb"]]},{"id":"4cfaea4f.e63364","type":"http response","z":"ceee673f.122fb8","name":"","statusCode":"","headers":{},"x":733.5,"y":448,"wires":[]},{"id":"aac384ef.f4d3e8","type":"function","z":"ceee673f.122fb8","name":"Is OK","func":"return {\n payload : JSON.stringify({\n \"status\" : \"OK\",\n \"analysisUUID\" : msg.analysisUUID\n }),\n res : msg.res\n}","outputs":1,"noerr":0,"x":618.5,"y":510,"wires":[["4cfaea4f.e63364"]]},{"id":"83845e3b.6e93e","type":"template","z":"ceee673f.122fb8","name":"JavaScript","field":"payload.script","fieldType":"msg","format":"javascript","syntax":"mustache","template":"(function(){\n \n function prevent(event){\n event.preventDefault();\n event.stopPropagation();\n }\n \n // Sourced from https://stackoverflow.com/questions/11089732/display-image-from-blob-using-javascript-and-websockets\n function encode (input) {\n var keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n var output = \"\";\n var chr1, chr2, chr3, enc1, enc2, enc3, enc4;\n var i = 0;\n \n while (i < input.length) {\n chr1 = input[i++];\n chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index\n chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here\n \n enc1 = chr1 >> 2;\n enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n enc4 = chr3 & 63;\n \n if (isNaN(chr2)) {\n enc3 = enc4 = 64;\n } else if (isNaN(chr3)) {\n enc4 = 64;\n }\n output += keyStr.charAt(enc1) + keyStr.charAt(enc2) +\n keyStr.charAt(enc3) + keyStr.charAt(enc4);\n }\n return output;\n }\n \n let analysisResults = [];\n let classifierTerms = {};\n \n var dropZone = document.querySelector('#dropzone');\n const descriptionBox = document.querySelector('#describe');\n const keyframesHolder = document.querySelector('#keyframes');\n const frames = keyframesHolder.querySelector('#frames');\n const filter = keyframesHolder.querySelector('input[type=\"text\"]');\n \n window.addEventListener(\"dragover\",function(e){\n prevent(e);\n document.body.dataset.dragging = \"true\";\n },false);\n \n window.addEventListener(\"drop\",function(e){\n prevent(e);\n document.body.dataset.dragging = \"false\";\n },false);\n \n window.addEventListener('dragleave', function(e){\n prevent(e);\n document.body.dataset.dragging = \"false\";\n }, false);\n \n descriptionBox.addEventListener('click', function(){\n \n if(this.dataset.time){\n const v = dropZone.querySelector('video');\n v.pause();\n v.currentTime = Number(this.dataset.time);\n }\n \n }, false);\n \n filter.addEventListener('keyup', function(){\n \n console.log('KEY');\n \n clearTimeout(this.debounce);\n \n const element = this;\n \n this.debounce = setTimeout(function(){\n console.log('Bounce!');\n \n if(element.value === \"\"){\n Array.from(frames.querySelectorAll('img')).forEach(img => {\n img.dataset.active = 'true';\n });\n \n } else {\n \n Array.from(frames.querySelectorAll('img')).forEach(img => {\n img.dataset.active = 'false';\n });\n \n let matchingFrames = classifierTerms[element.value.toLowerCase()] || [];\n \n Object.keys(classifierTerms).map(term => {\n if(term.indexOf(element.value.toLowerCase()) > -1){\n return classifierTerms[term];\n }\n }).filter(x => { return !!x; } )\n .forEach(setOfFrames => {\n setOfFrames.forEach(f => matchingFrames.push(f));\n });\n \n matchingFrames.forEach(frame => {\n frame.dataset.active = 'true';\n });\n \n }\n \n \n }, 200);\n \n }, false);\n \n dropZone.addEventListener('drop', function(e){\n \n console.log(e);\n \n analysisResults = [];\n classifierTerms = {};\n \n const file = event.dataTransfer.files[0];;\n const reader = new FileReader();\n \n // Create video element to show video;\n const v = document.createElement('video');\n v.src = window.URL.createObjectURL(file);\n \n this.dataset.dropped = 'true';\n this.querySelector('p').dataset.active = 'false';\n \n v.addEventListener('mouseenter', function(){\n this.controls = 'true';\n }, true);\n \n v.addEventListener('mouseleave', function(){\n this.removeAttribute('controls');\n }, true);\n \n v.addEventListener('timeupdate', function(){\n \n let latestResult = undefined;\n \n analysisResults.forEach(result => {\n if(result.time <= v.currentTime){\n \n if(result.result.images){\n if(result.result.images.length > 0){\n latestResult = result;\n }\n }\n \n }\n })\n \n descriptionBox.dataset.time = latestResult.time;\n descriptionBox.textContent = latestResult.time + ' >>> ' + latestResult.result.images[0].classifiers[0].classes.map(r => {return r.class}).join(' : ');\n \n });\n \n const exisitingVideo = dropZone.querySelector('video');\n if(exisitingVideo){\n dropZone.removeChild(exisitingVideo);\n }\n dropZone.appendChild(v);\n \n descriptionBox.textContent = '';\n frames.innerHTML = '';\n filter.value = '';\n \n // We read the file and call the upload function with the result\n reader.onload = function(e){\n console.log(e);\n console.log(reader.result);\n \n var formData = new FormData();\n \n formData.append('video', file);\n \n fetch('/analyse', {\n method : 'POST',\n body : formData\n })\n .then(res => {\n if(res.ok){\n return res.json();\n } else {\n return res;\n }\n })\n .then(function(response){\n console.log(response);\n \n if(response.analysisUUID){\n \n const analysisUUID = response.analysisUUID;\n \n var ws = new WebSocket('ws://' + window.location.host + '/frames');\n \n ws.addEventListener('open', () => {\n console.log('WebSocket connection to server opened.');\n });\n \n ws.addEventListener('message', (e) => {\n \n const serverResults = JSON.parse(e.data);\n serverResults.time = Number(serverResults.time);\n \n if(serverResults.uuid === analysisUUID){\n console.log(analysisUUID, serverResults);\n \n analysisResults.push(serverResults);\n \n analysisResults = analysisResults.sort( (a, b) => {\n if(a.time < b.time){\n return -1;\n } else {\n return 1;\n }\n });\n \n console.log(analysisResults);\n \n var arrayBuffer = serverResults.image.data;\n var bytes = new Uint8Array(arrayBuffer);\n \n var image = document.createElement('img');\n image.src = 'data:image/jpg;base64,' + encode(bytes);\n image.dataset.time = serverResults.time;\n \n image.setAttribute('title', serverResults.time);\n \n image.addEventListener('click', function(){\n console.log(this.dataset.time);\n const video = dropZone.querySelector('video');\n video.pause();\n video.currentTime = Number(this.dataset.time);\n }, false);\n \n frames.appendChild(image);\n \n if(!serverResults.result.error){\n\n serverResults.result.images[0].classifiers[0].classes.forEach(classifier => {\n \n const term = classifier.class.toLowerCase();\n \n if(!classifierTerms[term]){\n classifierTerms[term] = [];\n }\n \n classifierTerms[term].push(image);\n \n });\n \n }\n\n \n }\n \n \n });\n \n ws.addEventListener('close', function(e){\n console.log('The ws connection closed');\n }, false);\n \n }\n \n })\n .catch(err => {\n console.log(err);\n })\n \n };\n reader.readAsArrayBuffer(file);\n \n }, true);\n \n }());\n \n ","output":"str","x":326.5,"y":228,"wires":[["887589b5.759de8"]]},{"id":"1d69ad7a.7aa9a3","type":"comment","z":"ceee673f.122fb8","name":"Render page for demo","info":"","x":171.5,"y":108,"wires":[]},{"id":"9c30cdb.4bb7d3","type":"comment","z":"ceee673f.122fb8","name":"Handle file uploading / analysis","info":"","x":192.5,"y":347,"wires":[]},{"id":"574bc94c.630238","type":"template","z":"ceee673f.122fb8","name":"Style","field":"payload.styles","fieldType":"msg","format":"css","syntax":"mustache","template":"*[data-active='false']{\n display: none;\n}\n\nhtml, body{\n width: 100%;\n height: 100%;\n padding: 0;\n margin: 0;\n overflow: scroll;\n font-family: sans-serif;\n}\n\nbody{\n padding: 1em;\n padding-top: 50px;\n display: flex;\n flex-direction: column;\n height: auto;\n justify-content: center;\n align-items: center;\n box-sizing: border-box;\n}\n\nh1, h2, h3, h4, h5, h6{\n font-weight: 200;\n}\n\nheader{\n width: 100%;\n height: 50px;\n border-bottom: 1px solid #4178be;\n position: fixed;\n top: 0;\n left: 0;\n background-image: url(http://www.uidownload.com/files/529/205/221/ibm-icon.png);\n background-position: 10px;\n background-size: 60px;\n background-color: black;\n background-repeat: no-repeat;\n}\n\n#dropzone{\n max-width: 640px;\n max-height: 450px;\n background: #4178be;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 5px;\n width: 100%;\n padding: 2em 0;\n}\n\n#dropzone[data-dropped=\"true\"]{\n background: black;\n}\n\n#dropzone video{\n width: 100%;\n}\n\n#dropzone p{\n border: white 1px dashed;\n border-radius: 20px;\n color: white;\n padding: 0.5em 1em;\n background: rgba(0,0,0,0.1);\n}\n\n#describe{\n background: black;\n color: white;\n padding: 1em;\n margin: 1em;\n box-sizing: border-box;\n width: 100%;\n max-width: 850px;\n border-radius: 3px;\n text-align: center;\n line-height: 1.75em;\n}\n\n#keyframes{\n width: 100%;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n height: auto;\n background: rgb(245,245,245);\n min-height: 40px;\n padding: 1em;\n box-sizing: border-box;\n margin-top: 1em;\n border-radius: 3px;\n justify-content: center;\n max-width: 850px;\n}\n\n#keyframes input{\n width: 100%;\n font-size: 1em;\n background: transparent;\n border: 0px solid;\n border-bottom: 1px solid #b5b5b5;\n padding-bottom: 0.5em;\n outline: none;\n margin-bottom: 1em;\n}\n\n#keyframes #frames{\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n justify-content: center;\n max-height: 220px;\n overflow: scroll;\n}\n\n#keyframes #frames img {\n width: 100px;\n height: 65px;\n margin: 0.5em;\n border-radius: 2px;\n cursor: pointer;\n transition: ease-in-out transform 0.1s;\n}\n\n#keyframes #frames img:hover{\n transform: scale(1.5);\n box-shadow: 0 2px 5px black;\n}","output":"str","x":236.5,"y":185,"wires":[["83845e3b.6e93e"]]},{"id":"69ae97af.395288","type":"debug","z":"ceee673f.122fb8","name":"","active":true,"console":"false","complete":"true","x":515.5,"y":450,"wires":[]},{"id":"c4cd6b26.2acfc8","type":"extract-keyframes","z":"ceee673f.122fb8","name":"","x":359.5,"y":521,"wires":[["69ae97af.395288","a9429c5e.b1b59"]]},{"id":"82e881e.8e4028","type":"websocket-listener","z":"","path":"/frames","wholemsg":"false"}] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment