Add conf.yaml PWA config (#168)

Closes: #160
Reviewed-on: #168
This commit is contained in:
2024-03-30 21:00:35 -07:00
parent 0420e6b411
commit c1542283ea
8 changed files with 174 additions and 54 deletions

View File

@@ -9,12 +9,17 @@ Anatomy Lab Visual Identification Neural Net (A.L.V.I.N.N) is a f7 based app for
* **Run from source:** Clone this repository and in the root directory run `npm install` followed by `npm start`. For more information see [f7 info](f7_info.md).
## Quick Start
1. From the main screen of the app, select the menu icon in the upper left corner and go to `Settings`.
1. Make sure that `Use external server` option is selected and fill in address and port parameters to connect to a back end serving the ALVINN models (Doods2 is the default backend).
1. Save the settings and return to the main screen.
1. Select the region of the body you want to identify structures from.
1. In the region page, click on the camera icon to take a new picture or load a picture from storage. When the picture load, any identifiable structures will be listed as tags below the image.
1. Click on each tag to see the structure highlighted in the image.
1. Load an image in one of the following ways:
* Click on the camera icon to take a new picture.
* ALVINN will highlight areas with potential structures as you aim the camera.
* Press Capture to use the current camera view.
* Click on the image file icon to load a picture from the device storage.
* If demo mode is turned on, you can click on the marked image icon to load an ALVINN sample image.
1. When the picture is captured or loaded, any identifiable structures will be listed as tags below the image:
* Click on each tag to see the structure highlighted in the image.
* Tag color and proportion filled indicate ALVINN's level of confidence in the identification.
* If there are potential structures that do not satisfy the current detection threshold, a badge on the detection menu icon will indicate the number of un-displayed structures.
## Advanced Features
### Detection Parameters
@@ -29,3 +34,23 @@ The default threshold is 50% confidence.
If all of the detection tags that are currently visible have been viewed, then the final button (cloud upload) on the detection menu will be enabled.
This button will cause the image and the verified structures to be uploaded to the ALVINN project servers where that data will be available for further training of the neural net.
If after the image has been uploaded, the available detection tags change, then the option to re-upload the image will be available if all the new tags have been viewed and verified.
## Configuration
Configuring aspects of the hosted ALVINN PWA is done through the `conf.yaml` file in the `conf` folder.
### Site settings
The following site settings are avaible:
| name | description | values | default |
| --- | --- | --- | --- |
| `agreeExpire` | number of months before users are shown the site agreement dialog again | integer >= 1 | 3 |
| `demo` | set to **true** to enable demo mode by default | boolean | false
| `regions` | array of regions names to enable | thorax, abdomen, limbs, head | [thorax, abdomen, limbs, head] |
| `useExternal` | detemines the ability to use an external detection server:<br />**none** - external server cannot be configured<br />**optional** - external server can be configured in the app's settings page<br />**list** - external server can be selected in the app's settings page but only the configured server(s) may be selected<br />**required** - external server settings from conf file will be used by default and disable server options in the settings page | none, optional, list, required | **optional** |
| `external` | properties of the external server(s) ALVINN may connect t.<br />This setting must be a single element array if **useExternal** is set to **required**.<br />This setting must be an array of one or more elements if **useExternal** is set to **list** | external server settings array | []|
### External server settings
ALVINN can use an external object detection server instead of the built in models; settings for that external server are configured here. These settings must be configured if **site - useExternal** is set to **list** or **required**.
| name | description | default |
| --- | --- | --- |
| `name` | identifier for external server | *none* |
| `address` | ip or url of external server | *none* |
| `port` | port to access on external server | 9001 |

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "alvinn",
"version": "0.4.0",
"version": "0.5.0-rc",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "alvinn",
"version": "0.4.0",
"version": "0.5.0-rc",
"hasInstallScript": true,
"license": "UNLICENSED",
"dependencies": {
@@ -17,7 +17,8 @@
"framework7-vue": "^8.3.0",
"skeleton-elements": "^4.0.1",
"swiper": "^11.0.3",
"vue": "^3.3.8"
"vue": "^3.3.8",
"yaml": "^2.4.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.1",
@@ -9844,6 +9845,17 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"node_modules/yaml": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
"integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -16397,6 +16409,11 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
"yaml": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
"integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",

View File

@@ -30,7 +30,8 @@
"framework7-vue": "^8.3.0",
"skeleton-elements": "^4.0.1",
"swiper": "^11.0.3",
"vue": "^3.3.8"
"vue": "^3.3.8",
"yaml": "^2.4.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.1",

14
public/conf/conf.yaml Normal file
View File

@@ -0,0 +1,14 @@
demo: true
agreeExpire: 3
regions:
- thorax
- abdomen
- limbs
useExternal: required
external:
- name: Mserver
address: "192.169.1.105"
port: 9001
- name: Georgi lab server
address: "10.188.0.98"
port: 9001

View File

@@ -52,13 +52,15 @@
</style>
<script>
import { ref, onMounted } from 'vue';
import { f7, f7ready } from 'framework7-vue';
import { getDevice } from 'framework7/lite-bundle';
import cordovaApp from '../js/cordova-app.js';
import { ref, onMounted } from 'vue'
import { f7, f7ready } from 'framework7-vue'
import { getDevice } from 'framework7/lite-bundle'
import cordovaApp from '../js/cordova-app.js'
import routes from '../js/routes.js';
import store from '../js/store';
import YAML from 'yaml'
import routes from '../js/routes.js'
import store from '../js/store'
export default {
data () {
@@ -66,27 +68,49 @@
rememberAgreement: false,
siteAgreement: false,
dateAgreement: null,
showDisclaimer: true,
alvinnVersion: store().getVersion
showDisclaimer: false,
alvinnVersion: store().getVersion,
siteConf: {}
}
},
created () {
var loadSiteSettings = localStorage.getItem('siteSettings')
async created () {
if (!window.cordova) {
const confText = await fetch('./conf/conf.yaml')
.then((mod) => { return mod.text() })
this.siteConf = YAML.parse(confText)
}
const loadSiteSettings = localStorage.getItem('siteSettings')
if (loadSiteSettings) {
var loadedSettings = JSON.parse(loadSiteSettings)
let loadedSettings = JSON.parse(loadSiteSettings)
this.siteAgreement = loadedSettings.siteAgreement
this.rememberAgreement = loadedSettings.rememberAgreement
this.dateAgreement = loadedSettings.dateAgreement && new Date(loadedSettings.dateAgreement)
}
var curDate = new Date ()
var agreeStillValid = this.dateAgreement && (curDate < this.dateAgreement.setMonth(this.dateAgreement.getMonth() + 3))
const curDate = new Date ()
const expireMonth = (this.dateAgreement?.getMonth() || 0) + (this.siteConf?.agreeExpire || 3)
const agreeStillValid = this.dateAgreement && (curDate < this.dateAgreement.setMonth(expireMonth))
if (this.siteAgreement && this.rememberAgreement && agreeStillValid) {
this.showDisclaimer = false
store().agree()
} else {
this.showDisclaimer = true
}
var loadServerSettings = localStorage.getItem('serverSettings')
if (!loadServerSettings) {
store().set('enabledRegions',this.siteConf?.regions)
store().set('siteDemo',this.siteConf?.demo)
const loadServerSettings = localStorage.getItem('serverSettings')
if (this.siteConf?.useExternal) {
if (!['none','list','optional','required'].includes(this.siteConf.useExternal)) {
console.warn(`'${this.siteConf.useExternal}' is not a valid value for useExternal configuration: using 'optional'`)
} else {
store().set('useExternal',this.siteConf.useExternal)
if (this.siteConf.external) {
store().set('externalServerList',this.siteConf.external)
}
}
}
if (!loadServerSettings && !this.siteConf.external) {
localStorage.setItem('serverSettings','{"use":false,"address":"10.188.0.98","port":"9001","previous":{"10.188.0.98":"9001"}}')
} else if (this.siteConf.useExternal == 'required') {
localStorage.setItem('serverSettings',`{"use":true,"address":"${this.siteConf.external[0].address}","port":${this.siteConf.external[0].port}}`)
}
},
methods: {
@@ -113,7 +137,7 @@
this.showDisclaimer = false
},
() => {
var toast = f7.toast.create({
const toast = f7.toast.create({
text: 'ERROR: No settings saved',
closeTimeout: 2000
})
@@ -125,13 +149,11 @@
setup() {
const device = getDevice();
// Framework7 Parameters
var loadThemeSettings = localStorage.getItem('themeSettings')
if (loadThemeSettings) var themeSettings = JSON.parse(loadThemeSettings)
try {
if (themeSettings.darkMode.toString()) var darkTheme = themeSettings.darkMode
} catch {
var darkTheme = 'auto'
}
const loadThemeSettings = localStorage.getItem('themeSettings')
let themeSettings = {}
let darkTheme = 'auto'
if (loadThemeSettings) { themeSettings = JSON.parse(loadThemeSettings) }
if (themeSettings?.darkMode) darkTheme = themeSettings.darkMode
const f7params = {
name: 'ALVINN', // App name
theme: 'auto', // Automatic theme detection

View File

@@ -2,17 +2,37 @@ import { reactive, computed } from 'vue';
const state = reactive({
disclaimerAgreement: false,
enabledRegions: ['thorax','abdomen','limbs'],
version: '0.5.0-rc'
enabledRegions: ['thorax','abdomen','limbs','head'],
version: '0.5.0-rc',
useExternal: 'optional',
siteDemo: false,
externalServerList: []
})
const set = (config, confObj) => {
if (confObj === undefined) { return }
state[config] = confObj
}
const agree = () => {
state.disclaimerAgreement = true
}
const getServerList = () => {
if (state.useExternal == 'required') {
return state.externalServerList[0]
} else {
return state.externalServerList
}
}
export default () => ({
isAgreed: computed(() => state.disclaimerAgreement),
demoMode: computed(() => state.siteDemo),
externalType: computed(() => state.useExternal),
getRegions: computed(() => state.enabledRegions),
getVersion: computed(() => state.version),
agree
set,
agree,
getServerList
})

View File

@@ -99,7 +99,7 @@
<f7-button style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('file')">
<SvgIcon icon="photo_library" />
</f7-button>
<f7-button v-if="otherSettings.demo" style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('sample')">
<f7-button v-if="demoEnabled" style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('sample')">
<SvgIcon icon="photo_sample"/>
</f7-button>
</f7-segmented>
@@ -120,6 +120,7 @@
import submitMixin from './submit-mixin'
import detectionMixin from './detection-mixin'
import cameraMixin from './camera-mixin'
import { Conv2DBackpropFilter } from '@tensorflow/tfjs'
export default {
mixins: [submitMixin, detectionMixin, cameraMixin],
@@ -252,6 +253,9 @@
} else {
return false
}
},
demoEnabled () {
return this.otherSettings.demo || this.demoMode
}
},
methods: {

View File

@@ -31,19 +31,22 @@
<span style="margin-left: 16px;">Disable video estimates<f7-icon size="16" style="padding-left: 5px;" f7="question_diamond_fill" tooltip="faster: recommended for slower devices" /></span>
<f7-toggle v-model:checked="otherSettings.disableVideo" style="margin-right: 16px;" />
</div>
<div style="display:flex; justify-content:space-between; width: 100%">
<span style="margin-left: 16px;">Use external server</span>
<f7-toggle v-model:checked="serverSettings.use" style="margin-right: 16px;" @change="setDirty()" />
<div v-if="serverToggle">
<div style="display:flex; justify-content:space-between; width: 100%">
<span style="margin-left: 16px;">Use external server</span>
<f7-toggle v-model:checked="serverSettings.use" style="margin-right: 16px;" @change="setDirty()" />
</div>
<f7-list >
<f7-list-input :disabled="!serverSettings.use || serverList" v-model:value="serverSettings.address" label="Server address" type="text" placeholder="127.0.0.1" />
<f7-list-input :disabled="!serverSettings.use || serverList" v-model:value="serverSettings.port" label="Server port" type="text" placeholder="9001" />
</f7-list>
<span>Other servers</span>
<f7-list :dividers="true" :outline="true" :strong="true" :inset="true" style="width: calc(100% - 32px); margin-top: 0;">
<f7-list-item v-for="(addObj) in externalIp" :disabled="!serverSettings.use" :title="addObj.name" @click="setServerProps(addObj.address, addObj.port)"></f7-list-item>
<f7-list-item v-if="!serverList" v-for="(port, add) in otherIp" :disabled="!serverSettings.use" :title="add" @click="setServerProps(add, port)">{{ port }}</f7-list-item>
<f7-list-item v-if="Object.keys(otherIp).length == 0 && externalIp.length == 0" title="No previous server settings"></f7-list-item>
</f7-list>
</div>
<f7-list>
<f7-list-input :disabled="!serverSettings.use" v-model:value="serverSettings.address" label="Server address" type="text" placeholder="127.0.0.1" />
<f7-list-input :disabled="!serverSettings.use" v-model:value="serverSettings.port" label="Server port" type="text" placeholder="9001" />
</f7-list>
<span>Other servers</span>
<f7-list :dividers="true" :outline="true" :strong="true" :inset="true" style="width: calc(100% - 32px); margin-top: 0;">
<f7-list-item v-for="(port, add) in otherIp" :disabled="!serverSettings.use" :title="add" @click="setServerProps(add, port)">{{ port }}</f7-list-item>
<f7-list-item v-if="Object.keys(otherIp).length == 0" title="No previous server settings"></f7-list-item>
</f7-list>
</div>
<f7-button fill @click="saveAllSettings">SAVE</f7-button>
</div>
@@ -61,6 +64,7 @@
<script>
import { f7 } from 'framework7-vue'
import store from '../js/store'
export default {
data () {
@@ -72,8 +76,8 @@
},
serverSettings: {
use: false,
address: '10.170.64.22',
port: '9001',
address: '127.0.0.1',
port: '9000',
previous: {}
},
themeSettings: {
@@ -81,6 +85,9 @@
}
}
},
setup() {
return store()
},
computed: {
otherIp () {
let filteredIps = {}
@@ -90,6 +97,15 @@
}
}
return filteredIps
},
serverToggle () {
return ['optional','list'].includes(this.externalType)
},
serverList () {
return this.externalType == 'list'
},
externalIp () {
return this.getServerList()
}
},
created () {
@@ -106,7 +122,7 @@
let saveSetting = new Promise(
(saved,failed) => {
try {
if (this.serverSettings.use) {
if (this.serverSettings.use && !this.externalIp.some( (srv) => srv.address == this.serverSettings.address)) {
this.serverSettings.previous[this.serverSettings.address] = this.serverSettings.port
}
localStorage.setItem('serverSettings',JSON.stringify(this.serverSettings))
@@ -151,7 +167,8 @@
},
toggleSettingsView () {
this.showAdvanced = !this.showAdvanced
this.$refs.advancedSettings.style.maxHeight = `${this.showAdvanced ? this.$refs.advancedSettings.scrollHeight : 0}px`
//this.$refs.advancedSettings.style.maxHeight = `${this.showAdvanced ? this.$refs.advancedSettings.scrollHeight : 0}px`
this.$refs.advancedSettings.style.maxHeight = this.showAdvanced ? '100%' : '0px'
},
confirmBack () {
if (this.isDirty) {