Add detection settings panel (#23)

Closes #14 and #20

Signed-off-by: Justin Georgi <justin.georgi@gmail.com>

Reviewed-on: Georgi_Lab/ALVINN_f7#23
This commit is contained in:
2023-11-29 15:51:36 -07:00
parent ff3a13826a
commit 3da3b9c1bb
3 changed files with 307 additions and 249 deletions

View File

@@ -29,7 +29,7 @@
<f7-button style="height: auto; width: auto;" href="/detect/limbs/" popover-close="#region-popover"> <f7-button style="height: auto; width: auto;" href="/detect/limbs/" popover-close="#region-popover">
<img src="../assets/regions/limb.svg" /> <img src="../assets/regions/limb.svg" />
</f7-button> </f7-button>
<f7-button disabled="true" style="height: auto; width: auto;" href="/detect/head/" popover-close="#region-popover"> <f7-button class="disabled" style="height: auto; width: auto;" href="/detect/head/" popover-close="#region-popover">
<img src="../assets/regions/headneck.svg" /> <img src="../assets/regions/headneck.svg" />
</f7-button> </f7-button>
</f7-segmented> </f7-segmented>

View File

@@ -1,270 +1,329 @@
<template> <template>
<f7-page name="detect"> <f7-page name="detect">
<!-- Top Navbar --> <!-- Top Navbar -->
<f7-navbar :sliding="false" back-link="Back"> <f7-navbar :sliding="false" back-link="Back">
<f7-nav-title sliding>{{ regions[activeRegion] }}</f7-nav-title> <f7-nav-title sliding>{{ regions[activeRegion] }}</f7-nav-title>
</f7-navbar> <f7-nav-right>
<f7-block class="detect-grid"> <f7-link icon-ios="f7:menu" icon-md="material:settings" panel-open="right"></f7-link>
<div class="image-container"> </f7-nav-right>
<img :src="imageView" id="im-display" ref="image_src" style="flex: 1 1 0%; object-fit: scale-down; max-width: 100%; max-height: 100%; min-width: 0; min-height: 0;" /> </f7-navbar>
<div ref="structure_box" style="border: solid 3px yellow; position: absolute; display: none;" /> <f7-block class="detect-grid">
</div> <div class="image-container">
<div v-if="resultData && resultData.detections" class="chip-results" style="grid-area: result-view; flex: 0 0 auto; align-self: center;"> <img :src="imageView" id="im-display" ref="image_src" style="flex: 1 1 0%; object-fit: contain; max-width: 100%; max-height: 100%; min-width: 0; min-height: 0;" />
<f7-chip v-for="(result, idx) in resultData.detections" :class="(idx == selectedChip) ? 'selected-chip' : ''" :text="result.label" media=" " :tooltip="result.confidence.toFixed(1)" :media-bg-color="chipColor(result.confidence)" deleteable @click="selectChip(idx)" @delete="deleteChip(idx)" /> <div ref="structure_box" style="border: solid 3px yellow; position: absolute; display: none;" />
</div> </div>
<f7-segmented class="image-menu" raised> <div v-if="resultData && resultData.detections" class="chip-results" style="grid-area: result-view; flex: 0 0 auto; align-self: center;">
<f7-button popover-open="#region-popover"> <f7-chip v-for="(result, idx) in resultData.detections" :class="(idx == selectedChip) ? 'selected-chip' : ''" :text="result.label" media=" " :tooltip="result.confidence.toFixed(1)" :media-bg-color="chipColor(result.confidence)" deleteable @click="selectChip(idx)" @delete="deleteChip(idx)" />
<img :src="imageRegion" /> </div>
</f7-button> <f7-segmented class="image-menu" raised>
<f7-button @click="setData" :class="(imageLoaded) ? '' : 'disabled'"> <f7-button popover-open="#region-popover">
<img src="../assets/icons/visibility.svg" /> <img :src="imageRegion" />
</f7-button> </f7-button>
<f7-button @click="selectImage"> <f7-button @click="selectImage">
<img src="../assets/icons/image.svg" /> <img src="../assets/icons/image.svg" />
</f7-button> </f7-button>
<f7-button @click="setData"> <f7-button @click="setData" :class="(imageLoaded) ? '' : 'disabled'">
<img src="../assets/icons/videocam.svg" /> <img src="../assets/icons/visibility.svg" />
</f7-button> </f7-button>
</f7-segmented> <f7-button @click="setData">
<input type="file" ref="image_chooser" @change="getImage()" accept="image/*" style="display: none;"/> <img src="../assets/icons/videocam.svg" />
</f7-block> </f7-button>
</f7-page> </f7-segmented>
<input type="file" ref="image_chooser" @change="getImage()" accept="image/*" style="display: none;"/>
</f7-block>
<f7-panel right cover>
<f7-page>
<f7-navbar title="Detection Settings"></f7-navbar>
<f7-list>
<f7-list-input v-model:value="detectSettings.level" :label="`Confidence % threshold: ${detectSettings.level}`" type="range" />
<f7-list-item accordion-item title="Structures">
<f7-accordion-content>
<f7-list>
<f7-list-item checkbox checked checkbox-icon="end" title="All/none" @change="selectAll"></f7-list-item>
<f7-list-item v-for="structure in detectSettings.filter" checkbox checkbox-icon="end" v-model:checked="structure.detect" :title="structure.name"></f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-page>
</f7-panel>
</f7-page>
</template> </template>
<style> <style>
.detect-grid { .detect-grid {
/*display: flex; display: grid;
justify-content: flex-start; grid-template-columns: 1fr;
flex-direction: column; grid-template-rows: 1fr auto min-content;
align-items: stretch; grid-template-areas:
height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom)); "image-view"
box-sizing: border-box; "result-view"
width: 100vw;*/ "menu-view";
display: grid; justify-items: center;
grid-template-columns: 1fr; height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
grid-template-rows: 1fr auto min-content; max-height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
grid-template-areas: }
"image-view"
"result-view"
"menu-view";
justify-items: center;
height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
max-height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
}
.image-container { .image-container {
grid-area: image-view; grid-area: image-view;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
position: relative; position: relative;
display: flex; display: flex;
align-self: stretch;
}
.chip-results {
display: flex;
flex-wrap: wrap;
gap: 5px;
padding: 10px;
--f7-chip-border-radius: 16px;
--f7-chip-media-size: 32px;
--f7-chip-font-weight: normal;
}
.chip-results .chip {
padding-left: 8px;
}
.selected-chip {
font-weight: 500;
box-shadow: 4px 4px 1px var(--f7-theme-color);
transform: translate(-2px, -2px);
}
.image-menu {
grid-area: menu-view;
margin: 5px;
width: 50%;
max-width: 400px;
min-width: 192px;
flex: 0 0 auto;
}
.image-menu .button {
aspect-ratio: 1;
height: auto;
padding: 5px;
}
.button > img {
height: 100%;
}
@media (max-height: 450px) and (orientation: landscape) {
.detect-grid {
grid-template-columns: minmax(0,1fr) max-content auto;
grid-template-rows: calc(100vh - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom) - 64px);
grid-template-areas:
"image-view result-view menu-view";
justify-items: center;
align-items: stretch;
height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
max-height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
position: relative;
} }
.chip-results { .chip-results {
display: flex; flex-direction: column;
flex-wrap: wrap; max-height: 100%;
gap: 5px; justify-self: start;
padding: 10px; flex-wrap: nowrap;
--f7-chip-border-radius: 16px; overflow-y: scroll;
--f7-chip-media-size: 32px;
--f7-chip-font-weight: normal;
} }
.chip-results .chip { .image-container {
padding-left: 8px; flex-direction: column;
}
.selected-chip {
font-weight: 500;
box-shadow: 4px 4px 1px var(--f7-theme-color);
transform: translate(-2px, -2px);
} }
.image-menu { .image-menu {
grid-area: menu-view; flex-direction: column;
margin: 5px; width: auto;
width: 50%; min-width: 0;
max-width: 400px; flex: 1 1 0%;
min-width: 192px; max-height: 100%;
flex: 0 0 auto;
} }
.image-menu .button { .image-menu .button {
aspect-ratio: 1; aspect-ratio: 1;
height: auto; width: auto;
padding: 5px; height: 100%;
flex: 1 1 0%;
border-bottom: 1px solid var(--f7-segmented-raised-divider-color);
} }
.button > img { .button > img {
height: 100%; width:auto;
} height: 100%;
@media (max-height: 450px) and (orientation: landscape) {
.detect-grid {
grid-template-columns: minmax(0,1fr) max-content auto;
grid-template-rows: calc(100vh - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom) - 64px);
grid-template-areas:
"image-view result-view menu-view";
justify-items: center;
align-items: stretch;
height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
max-height: calc(100% - var(--f7-navbar-height) - var(--f7-safe-area-top) - var(--f7-safe-area-bottom));
position: relative;
}
.chip-results {
flex-direction: column;
max-height: 100%;
justify-self: start;
flex-wrap: nowrap;
overflow-y: scroll;
}
.image-container {
flex-direction: column;
}
.image-menu {
flex-direction: column;
width: auto;
min-width: 0;
flex: 1 1 0%;
max-height: 100%;
}
.image-menu .button {
aspect-ratio: 1;
width: auto;
height: 100%;
flex: 1 1 0%;
border-bottom: 1px solid var(--f7-segmented-raised-divider-color);
}
.button > img {
width:auto;
height: 100%;
}
} }
}
</style> </style>
<script> <script>
import { f7 } from 'framework7-vue' import { f7 } from 'framework7-vue'
export default { export default {
props: { props: {
f7route: Object, f7route: Object,
}, },
data () { data () {
return { return {
regions: ['Thorax','Abdomen/Pelvis','Limbs','Head and Neck'], regions: ['Thorax','Abdomen/Pelvis','Limbs','Head and Neck'],
resultData: {}, resultData: {},
selectedChip: -1, selectedChip: -1,
activeRegion: 4, activeRegion: 4,
imageRegion: '', imageRegion: '',
imageLoaded: false, imageLoaded: false,
imageView: '../assets/icons/image.svg', imageView: '../assets/icons/image.svg',
reader: new FileReader(), reader: new FileReader(),
detectorName: '' detectorName: '',
} detectSettings: {
}, level: 50,
created () { filter: []
switch (this.f7route.params.region) { },
case 'thorax': serverSettings: {}
this.activeRegion = 0 }
this.imageRegion = '../assets/regions/thorax.svg' },
this.detectorName = 'thorax' created () {
break; switch (this.f7route.params.region) {
case 'abdomen': case 'thorax':
this.activeRegion = 1 this.activeRegion = 0
this.imageRegion = '../assets/regions/abdpel.svg' this.imageRegion = '../assets/regions/thorax.svg'
this.detectorName = 'combined' this.detectorName = 'thorax'
break; break;
case 'limbs': case 'abdomen':
this.activeRegion = 2 this.activeRegion = 1
this.imageRegion = '../assets/regions/limb.svg' this.imageRegion = '../assets/regions/abdpel.svg'
this.detectorName = 'defaultNew' this.detectorName = 'combined'
break; break;
case 'head': case 'limbs':
this.activeRegion = 3 this.activeRegion = 2
this.imageRegion = '../assets/regions/headneck.svg' this.imageRegion = '../assets/regions/limb.svg'
break; this.detectorName = 'defaultNew'
} break;
}, case 'head':
methods: { this.activeRegion = 3
chipColor (confVal) { this.imageRegion = '../assets/regions/headneck.svg'
if (confVal >= 90) return 'green' break;
if (confVal >= 70) return 'lime' }
return 'yellow' var loadServerSettings = localStorage.getItem('serverSettings')
}, if (loadServerSettings) this.serverSettings = JSON.parse(loadServerSettings)
setData () { var self = this
var self = this if (this.serverSettings && this.serverSettings.use) {
var loadServerSettings = localStorage.getItem('serverSettings') var modelURL = `http://${this.serverSettings.address}:${this.serverSettings.port}/detectors`
if (loadServerSettings) var serverSettings = JSON.parse(loadServerSettings) var xhr = new XMLHttpRequest()
if (serverSettings && serverSettings.use) { xhr.open("GET", modelURL)
var modelURL = `http://${serverSettings.address}:${serverSettings.port}/detect` xhr.setRequestHeader('Content-Type', 'application/json')
var xhr = new XMLHttpRequest() xhr.onload = function () {
xhr.open("POST", modelURL) if (this.status !== 200) {
xhr.setRequestHeader('Content-Type', 'application/json') //this.response.text().then(function(message){alert(message)})
xhr.onload = function () { console.log(this.response)
if (this.status !== 200) { return;
//this.response.text().then(function(message){alert(message)}) }
console.log(this.response) var detectors = JSON.parse(this.response).detectors
return; self.detectSettings.filter = detectors
} .find( d => { return d.name == self.detectorName } ).labels
self.resultData = JSON.parse(this.response) .filter( l => { return l != "" } ).sort()
} .map( l => { return {'name': l, 'detect': true} } )
var doodsData = {
"detector_name": this.detectorName,
"detect": {
"*":50
}
}
doodsData.data = this.reader.result.split(',')[1]
xhr.send(JSON.stringify(doodsData))
} else {
f7.dialog.alert('Using built-in model')
}
},
selectImage () {
var loadResult = this.$refs.image_chooser.click()
},
selectChip ( iChip ) {
var imgWidth
var imgHeight
this.selectedChip = iChip
const box = this.$refs.structure_box
const img = this.$refs.image_src
var imgAspect = img.naturalWidth / img.naturalHeight
var rendAspect = img.offsetWidth / img.offsetHeight
if (imgAspect >= rendAspect) {
imgWidth = img.offsetWidth
imgHeight = img.offsetWidth / imgAspect
} else {
imgWidth = img.offsetHeight * imgAspect
imgHeight = img.offsetHeight
}
box.style.display = "block"
box.style.left = `${(img.offsetWidth - imgWidth) / 2 + this.resultData.detections[iChip].left * imgWidth}px`
box.style.top = `${(img.offsetHeight - imgHeight) / 2 + this.resultData.detections[iChip].top * imgHeight}px`
box.style.width = `${(this.resultData.detections[iChip].right - this.resultData.detections[iChip].left) * imgWidth}px`
box.style.height = `${(this.resultData.detections[iChip].bottom - this.resultData.detections[iChip].top) * imgHeight}px`
},
deleteChip ( iChip ) {
f7.dialog.confirm(`${this.resultData.detections[iChip].label} is identified with ${this.resultData.detections[iChip].confidence.toFixed(1)}% confidence. Are you sure you want to delete it?`, () => {
this.resultData.detections.splice(iChip, 1)
});
},
getImage () {
const example = this.$refs.image_chooser.files[0];
this.imageView = URL.createObjectURL(example);
this.reader.readAsDataURL(example)
this.imageLoaded = true;
}
} }
xhr.send()
}
},
methods: {
chipColor (confVal) {
if (confVal >= 90) return 'green'
if (confVal >= 70) return 'lime'
return 'yellow'
},
setData () {
var self = this
if (this.serverSettings && this.serverSettings.use) {
var modelURL = `http://${this.serverSettings.address}:${this.serverSettings.port}/detect`
var xhr = new XMLHttpRequest()
xhr.open("POST", modelURL)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.onload = function () {
if (this.status !== 200) {
//this.response.text().then(function(message){alert(message)})
console.log(this.response)
return;
}
self.resultData = JSON.parse(this.response)
}
var detectStructures = {}
if (this.detectSettings.filter.every( s => { return s.detect } )) {
detectStructures['*'] = this.detectSettings.level
} else {
this.detectSettings.filter.forEach( s => {
if (s.detect) detectStructures[s.name] = this.detectSettings.level
})
}
var doodsData = {
"detector_name": this.detectorName,
"detect": detectStructures,
"data": this.reader.result.split(',')[1]
}
xhr.send(JSON.stringify(doodsData))
} else {
//TODO
f7.dialog.alert('Using built-in model')
}
},
selectAll (ev) {
if (ev.target.checked) {
this.detectSettings.filter.forEach( s => s.detect = true )
} else {
this.detectSettings.filter.forEach( s => s.detect = false )
}
},
selectImage () {
var loadResult = this.$refs.image_chooser.click()
},
selectChip ( iChip ) {
var imgWidth
var imgHeight
this.selectedChip = iChip
const box = this.$refs.structure_box
const img = this.$refs.image_src
var imgAspect = img.naturalWidth / img.naturalHeight
var rendAspect = img.offsetWidth / img.offsetHeight
if (imgAspect >= rendAspect) {
imgWidth = img.offsetWidth
imgHeight = img.offsetWidth / imgAspect
} else {
imgWidth = img.offsetHeight * imgAspect
imgHeight = img.offsetHeight
}
box.style.display = 'block'
box.style.left = `${(img.offsetWidth - imgWidth) / 2 + this.resultData.detections[iChip].left * imgWidth}px`
box.style.top = `${(img.offsetHeight - imgHeight) / 2 + this.resultData.detections[iChip].top * imgHeight}px`
box.style.width = `${(this.resultData.detections[iChip].right - this.resultData.detections[iChip].left) * imgWidth}px`
box.style.height = `${(this.resultData.detections[iChip].bottom - this.resultData.detections[iChip].top) * imgHeight}px`
},
deleteChip ( iChip ) {
f7.dialog.confirm(`${this.resultData.detections[iChip].label} is identified with ${this.resultData.detections[iChip].confidence.toFixed(1)}% confidence. Are you sure you want to delete it?`, () => {
this.resultData.detections.splice(iChip, 1)
});
},
getImage () {
const searchImage = this.$refs.image_chooser.files[0]
//Promise goes here?
this.imageView = URL.createObjectURL(searchImage)
this.reader.readAsDataURL(searchImage)
this.imageLoaded = true
this.resultData = {}
this.selectedChip = -1
const box = this.$refs.structure_box
box.style.display = 'none'
}
} }
}
</script> </script>

View File

@@ -2,7 +2,6 @@
<f7-page name="home"> <f7-page name="home">
<!-- Top Navbar --> <!-- Top Navbar -->
<f7-navbar> <f7-navbar>
<f7-nav-left> <f7-nav-left>
<f7-link icon-ios="f7:menu" icon-md="material:menu" panel-open="left"></f7-link> <f7-link icon-ios="f7:menu" icon-md="material:menu" panel-open="left"></f7-link>
</f7-nav-left> </f7-nav-left>
@@ -14,16 +13,16 @@
<h4 style="text-align: center; margin: 0;">Veterinary Anatomy Edition</h4> <h4 style="text-align: center; margin: 0;">Veterinary Anatomy Edition</h4>
<p style="text-align: center; margin: 0;">Select a region to begin</p> <p style="text-align: center; margin: 0;">Select a region to begin</p>
<div class="region-grid"> <div class="region-grid">
<f7-button class="region-button thorax" href="/detect/thorax/"> <f7-button class="region-button thorax" href="/detect/thorax/">
<img class="region-image" src="../assets/regions/thorax.svg" /> <img class="region-image" src="../assets/regions/thorax.svg" />
</f7-button> </f7-button>
<f7-button class="region-button abdomen" href="/detect/abdomen/"> <f7-button class="region-button abdomen" href="/detect/abdomen/">
<img class="region-image" src="../assets/regions/abdpel.svg" /> <img class="region-image" src="../assets/regions/abdpel.svg" />
</f7-button> </f7-button>
<f7-button class="region-button limbs" href="/detect/limbs/"> <f7-button class="region-button limbs" href="/detect/limbs/">
<img class="region-image" src="../assets/regions/limb.svg" /> <img class="region-image" src="../assets/regions/limb.svg" />
</f7-button> </f7-button>
<f7-button class="region-button headneck" disabled="true" href="/detect/head/"> <f7-button class="region-button headneck disabled" href="/detect/head/">
<img class="region-image" src="../assets/regions/headneck.svg" /> <img class="region-image" src="../assets/regions/headneck.svg" />
</f7-button> </f7-button>
</div> </div>