Add real-time detection to camera stream #143

Merged
jgeorgi merged 5 commits from xps-video-stream into main 2024-03-24 15:51:10 +00:00
19 changed files with 389 additions and 12 deletions
Showing only changes of commit 5a96a00c30 - Show all commits

14
src/assets/target.svg Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg version="1.1" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="filter1847" x="-.075" y="-.075" width="1.15" height="1.15" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="0.3125"/>
</filter>
<radialGradient id="radialGradient1903" cx="5" cy="5" r="5.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#ff0" offset="0"/>
<stop stop-color="#ff0" stop-opacity="0" offset="1"/>
</radialGradient>
</defs>
<path d="m0 5 3.833274-1.166726 1.166726-3.833274 1.166726 3.833274 3.833274 1.166726-3.833274 1.166726-1.166726 3.833274-1.166726-3.833274z" color="#000000" fill="url(#radialGradient1903)" fill-rule="evenodd" filter="url(#filter1847)" opacity=".63"/>
</svg>

After

Width:  |  Height:  |  Size: 870 B

View File

@@ -1,3 +1,5 @@
import * as tf from '@tensorflow/tfjs'
export default { export default {
methods: { methods: {
async openCamera(imContain) { async openCamera(imContain) {
@@ -5,7 +7,6 @@ export default {
const devicesList = await navigator.mediaDevices.enumerateDevices() const devicesList = await navigator.mediaDevices.enumerateDevices()
this.videoDeviceAvailable = devicesList.some( d => d.kind == "videoinput") this.videoDeviceAvailable = devicesList.some( d => d.kind == "videoinput")
if (this.videoDeviceAvailable) { if (this.videoDeviceAvailable) {
navigator.mediaDevices.getUserMedia({video: true})
var vidConstraint = { var vidConstraint = {
video: { video: {
width: { width: {
@@ -39,7 +40,22 @@ export default {
this.getImage(tempCVS.toDataURL()) this.getImage(tempCVS.toDataURL())
}, },
async videoStream () { async videoStream () {
//TODO const vidElement = this.$refs.vid_viewer
this.videoAvailable = false
const devicesList = await navigator.mediaDevices.enumerateDevices()
this.videoDeviceAvailable = devicesList.some( d => d.kind == "videoinput")
if (this.videoDeviceAvailable) {
var vidConstraint = {
//resizeWidth: 640,
//resizeHeight: 640,
facingMode: 'environment'
}
tf.data.webcam(vidElement, vidConstraint).then( webData => {
this.videoAvailable = true
this.cameraStream = vidElement.captureStream()
this.videoFrameDetect(webData)
})
}
} }
} }
} }

View File

@@ -6,12 +6,12 @@
</f7-navbar> </f7-navbar>
<f7-block class="detect-grid"> <f7-block class="detect-grid">
<div class="image-container" ref="image_container"> <div class="image-container" ref="image_container">
<canvas id="im-draw" ref="image_cvs" @click="structureClick" :style="`display: ${imageLoaded ? 'block' : 'none'}; flex: 1 1 0%; max-width: 100%; max-height: 100%; min-width: 0; min-height: 0; background-size: contain; background-position: center; background-repeat: no-repeat`" />
<SvgIcon v-if="!imageView && !videoAvailable" :icon="f7route.params.region" fill-color="var(--avn-theme-color)" @click="selectImage" /> <SvgIcon v-if="!imageView && !videoAvailable" :icon="f7route.params.region" fill-color="var(--avn-theme-color)" @click="selectImage" />
<div class="vid-container" v-if="videoAvailable" style="width: 100%; height: 100%"> <div class="vid-container" :style="`display: ${videoAvailable ? 'block' : 'none'}; position: absolute; width: 100%; height: 100%;`">
<video id="vid-view" ref="vid_viewer" :srcObject="cameraStream" :autoPlay="true" style="width: 100%; height: 100%"></video> <video id="vid-view" ref="vid_viewer" :srcObject="cameraStream" :autoPlay="true" style="width: 100%; height: 100%"></video>
<f7-button @click="captureVidFrame()" style="position: absolute; bottom: 32px; left: 50%; transform: translateX(-50%);" fill large>Capture</f7-button> <f7-button @click="captureVidFrame()" style="position: absolute; bottom: 32px; left: 50%; transform: translateX(-50%); z-index: 3;" fill large>Capture</f7-button>
</div> </div>
<canvas id="im-draw" ref="image_cvs" @click="structureClick" :style="`display: ${(imageLoaded || videoAvailable) ? 'block' : 'none'}; flex: 1 1 0%; max-width: 100%; max-height: 100%; min-width: 0; min-height: 0; background-size: contain; background-position: center; background-repeat: no-repeat; z-index: 2;`" />
</div> </div>
<div class="chip-results" style="grid-area: result-view; flex: 0 0 auto; align-self: center;"> <div class="chip-results" style="grid-area: result-view; flex: 0 0 auto; align-self: center;">
<f7-chip v-for="result in showResults.filter( r => { return r.aboveThreshold && r.isSearched && !r.isDeleted })" <f7-chip v-for="result in showResults.filter( r => { return r.aboveThreshold && r.isSearched && !r.isDeleted })"
@@ -56,6 +56,7 @@
</f7-button> </f7-button>
</f7-segmented> </f7-segmented>
<input type="file" ref="image_chooser" @change="getImage()" accept="image/*" style="display: none;"/> <input type="file" ref="image_chooser" @change="getImage()" accept="image/*" style="display: none;"/>
<img src="../assets/target.svg" ref="target_image" style="display: none;" />
</f7-block> </f7-block>
<f7-panel :id="detectorName + '-settings'" right cover :backdrop="false" :container-el="`#${detectorName}-detect-page`"> <f7-panel :id="detectorName + '-settings'" right cover :backdrop="false" :container-el="`#${detectorName}-detect-page`">
@@ -163,7 +164,6 @@
case 'thorax': case 'thorax':
this.activeRegion = 0 this.activeRegion = 0
this.detectorName = 'thorax' this.detectorName = 'thorax'
//this.classesList = thoraxClasses
/* VITE setting */ /* VITE setting */
this.modelLocation = `../models/thorax${this.otherSettings.mini ? '-mini' : ''}/model.json` this.modelLocation = `../models/thorax${this.otherSettings.mini ? '-mini' : ''}/model.json`
/* PWA Build setting */ /* PWA Build setting */
@@ -172,7 +172,12 @@
break; break;
case 'abdomen': case 'abdomen':
this.activeRegion = 1 this.activeRegion = 1
this.detectorName = 'combined' this.detectorName = 'abdomen'
/* VITE setting */
this.modelLocation = `../models/abdomen${this.otherSettings.mini ? '-mini' : ''}/model.json`
/* PWA Build setting */
//this.modelLocation = `./models/abdomen${this.otherSettings.mini ? '-mini' : ''}/model.json`
this.modelLocationCordova = `https://localhost/models/abdomen${this.otherSettings.mini ? '-mini' : ''}/model.json`
break; break;
case 'limbs': case 'limbs':
this.activeRegion = 2 this.activeRegion = 2
@@ -284,7 +289,14 @@
} }
if (mode == "camera") { if (mode == "camera") {
this.videoAvailable = await this.openCamera(this.$refs.image_container) this.videoAvailable = await this.openCamera(this.$refs.image_container)
if (this.videoAvailable) { return } if (this.videoAvailable) {
var trackDetails = this.cameraStream.getVideoTracks()[0].getSettings()
var vidElement = this.$refs.vid_viewer
vidElement.width = trackDetails.width
vidElement.height = trackDetails.height
this.videoFrameDetect(vidElement)
return
}
} }
if (mode == 'sample') { if (mode == 'sample') {
f7.dialog.create({ f7.dialog.create({
@@ -421,7 +433,7 @@
this.selectChip(findBox >= 0 ? this.resultData.detections[findBox].resultIndex : this.selectedChip) this.selectChip(findBox >= 0 ? this.resultData.detections[findBox].resultIndex : this.selectedChip)
}, },
box2cvs(boxInput) { box2cvs(boxInput) {
if (!boxInput) return [] if (!boxInput || boxInput.length == 0) return []
const boxList = boxInput.length ? boxInput : [boxInput] const boxList = boxInput.length ? boxInput : [boxInput]
const [imCanvas, imageCtx] = this.resetView() const [imCanvas, imageCtx] = this.resetView()
var imgWidth var imgWidth

View File

@@ -1,5 +1,7 @@
import * as tf from '@tensorflow/tfjs' import * as tf from '@tensorflow/tfjs'
import { image } from '@tensorflow/tfjs'
import { f7 } from 'framework7-vue' import { f7 } from 'framework7-vue'
import { nextTick } from 'vue'
var model = null var model = null
@@ -150,7 +152,59 @@ export default {
remoteTimeout () { remoteTimeout () {
this.detecting = false this.detecting = false
f7.dialog.alert('No connection to remote ALVINN instance. Please check app settings.') f7.dialog.alert('No connection to remote ALVINN instance. Please check app settings.')
},
async videoFrameDetect (vidData) {
const [modelWidth, modelHeight] = model.inputs[0].shape.slice(1, 3)
const imCanvas = this.$refs.image_cvs
const imageCtx = imCanvas.getContext("2d")
const target = this.$refs.target_image
await tf.nextFrame();
imCanvas.width = imCanvas.clientWidth
imCanvas.height = imCanvas.clientHeight
imageCtx.clearRect(0,0,imCanvas.width,imCanvas.height)
var imgWidth
var imgHeight
const imgAspect = vidData.clientWidth / vidData.clientHeight
const rendAspect = imCanvas.width / imCanvas.height
if (imgAspect >= rendAspect) {
imgWidth = imCanvas.width
imgHeight = imCanvas.width / imgAspect
} else {
imgWidth = imCanvas.height * imgAspect
imgHeight = imCanvas.height
}
while (this.videoAvailable) {
console.time('frame-process')
try {
const input = tf.tidy(() => {
return tf.image.resizeBilinear(tf.browser.fromPixels(vidData), [modelWidth, modelHeight]).div(255.0).expandDims(0)
})
const res = model.predict(input)
const rawRes = tf.transpose(res,[0,2,1]).arraySync()[0]
let rawCoords = []
if (rawRes) {
for (var i = 0; i < rawRes.length; i++) {
var getScores = rawRes[i].slice(4)
if (getScores.some( s => s > .5)) {
rawCoords.push(rawRes[i].slice(0,2))
}
} }
imageCtx.clearRect(0,0,imCanvas.width,imCanvas.height)
for (var coord of rawCoords) {
console.log(`x: ${coord[0]}, y: ${coord[1]}`)
let pointX = (imCanvas.width - imgWidth) / 2 + (coord[0] / modelWidth) * imgWidth -5
let pointY = (imCanvas.height - imgHeight) / 2 + (coord[1] / modelHeight) * imgHeight -5
imageCtx.drawImage(target, pointX, pointY, 20, 20)
}
}
} catch (e) {
console.log(e)
}
console.timeEnd('frame-process')
await tf.nextFrame();
}
}
} }
} }