51 Commits

Author SHA1 Message Date
5860939f65 Update release version
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-27 19:38:29 -07:00
ecdf74a483 Fill chip media propotationally to confidence value (#156)
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Reviewed-on: #156
2024-03-27 19:20:10 -07:00
e8778f3e24 Improve the help page info and formatting (#155)
Closes: #146

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

Reviewed-on: #155
2024-03-27 16:33:37 -07:00
b7119a240e Restore material icon set (#154)
Closes: #153

Fixes a regression from the culling of extra icons and build files. Restores the basic material icon set which is used by the buttons in the f7 text editor.

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

Reviewed-on: #154
2024-03-27 08:58:35 -07:00
e311b34e00 Add alpha version warning to home screen
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-27 08:35:05 -07:00
596c28a880 Move assets to vite public folder (#152)
Closes: #151

Models and samples are now properly structured in the vite public folder so they are properly automatically incorporated into the the builds and work with both builds and dev tests.

Reviewed-on: #152
2024-03-26 20:23:33 -07:00
42faae18eb Fix real-time target location on camera stream
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-26 12:58:13 -07:00
a3f63749b1 Add fullscreen setting to detect pages (#149)
Closes: #147

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

Reviewed-on: #149
2024-03-26 08:33:56 -07:00
90d23a70b3 Improve model location determination (#148)
Vite and cordova properties now automatically determine the path to the models.

Reviewed-on: #148
2024-03-25 21:03:56 -07:00
53869aa268 Add help page (#144)
Closes: #140

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

Reviewed-on: #144
2024-03-24 20:23:23 -07:00
e88feb17b6 Add repo link to About page
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-24 19:41:08 -07:00
22cbb157ee Fix coco data class list
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-24 09:36:53 -07:00
79316bb83b Add real-time detection to camera stream (#143)
Closes: #30

When the camera is being used to find an image to capture, the region mini model now runs in real time to give an estimate of where there are identifiable structures.

Reviewed-on: #143
2024-03-24 08:51:08 -07:00
f09180875a Add mini models to tech specs and improve styling (#142)
Closes: #133

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

Reviewed-on: #142
2024-03-24 08:06:28 -07:00
c16e44016b Fix Android sample image fetching
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-14 16:35:56 -07:00
c6f17d7187 Add custom navigation guard to settings page (#137)
Closes: #131

There is probably a better ("more native") way to do this, but I spent hours trying to figure it out and in the end it was way easier to circumvent the built-in f7 router events and just use my own custom back button. D.U.M. - dumb, but not my fault (I think...).

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

Reviewed-on: #137
2024-03-14 16:03:03 -07:00
f39c811e40 Add sample image option (#136)
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Reviewed-on: #136
2024-03-13 17:19:44 -07:00
4756f49423 Cleanup unused plugins and fonts
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-13 10:36:24 -07:00
6e35fcb141 Add alvinn mini settings
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-11 21:10:06 -07:00
76a0cba2af Add dynamic camera size request
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-11 20:22:28 -07:00
a665fb591d Fix empty detection bug (#135)
Closes: #134

Fixes regression in post-processing from #129.

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

Reviewed-on: #135
2024-03-11 09:47:26 -07:00
2cb128fd51 Auto-hide advanced settings
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-10 21:20:56 -07:00
9d64cbd2d6 Set local detect as default
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-10 20:35:37 -07:00
6e867e00d1 Reduce timeout wait beofre setData
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-10 20:27:34 -07:00
ea43aa3306 Add specs page and link from version in panel (#130)
Closes: #126

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

Reviewed-on: #130
2024-03-10 19:23:31 -07:00
76b2fe5b0c Optimize detection steps (#129)
Closes: #128
Reviewed-on: #129
2024-03-09 21:06:50 -07:00
1cc76c5950 Update version to 0.4.0
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-07 16:00:21 -07:00
091a38f7ab Clear file input on image load
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-07 15:18:35 -07:00
5ec51d9ad9 Add hidden structures badge
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-07 14:52:48 -07:00
4d356df02a Add stop camera button
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-07 14:41:58 -07:00
4af06f0fe5 Update thorax model and improve model performance (#125)
Closes: #117

This bumps the thorax model from the yolo nano to the yolo sm (64 x 640 size) but greatly improves model performance by running a fake detection event on page load to get the model parameters in memory.

As a result of that change a new loading message was required so the f7 preloader was switched out for an f7 progress bar and more messaging was added during various stages.  The progress bar fixes the previous issue with the preloader.

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

Reviewed-on: #125
2024-03-07 13:06:14 -07:00
0e99679e00 Fix PWA build errors
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-06 16:41:18 -07:00
1f25a75cec Split detect.vue (#122)
Closes: #112

New mixins for camera and detection (remote and local) and new css for detection page.

Reviewed-on: #122
2024-03-06 14:36:27 -07:00
b00b5cf492 Add performance tracking to local detection
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-05 15:51:28 -07:00
5086a52849 Return to standard model folder locations
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-05 15:06:01 -07:00
77240fe9bf Fix missing detection progress indicator on camera load
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-05 06:48:07 -07:00
7f9a7cb2d2 Cordova thorax model location update
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-04 17:18:15 -07:00
c452776689 Add better camera access for non-cordova deployments (#116)
This adds direct javascript access to the camera (with permission requests). Capturing an image with the camera now happens right on the detect page and not a separate window.

This also opens the possibility of live detection on the web app.

Reviewed-on: Georgi_Lab/ALVINN_f7#116
2024-03-04 17:03:28 -07:00
239558194e Cordova package upgrade
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-03-04 16:58:59 -07:00
069ad74e30 Add full yolov8 compatible post-processing (#115)
This updates the local detection to yolov8-based models from the older yolov5 based models.

This includes significant additional post-processing of the raw yolov8 output, but also means that the smaller nano yolo model is available as well as all of the updated ALVINN thorax training data.

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

Reviewed-on: Georgi_Lab/ALVINN_f7#115
2024-03-04 15:03:20 -07:00
7f1aa63f07 Add site agreement expiration
Closes: #88

Reviewed-on: Georgi_Lab/ALVINN_f7#111
2024-02-23 22:00:05 -07:00
e644aa26f0 Add PWA to README
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-02-22 20:28:45 -07:00
19a669dc98 Fix decoding error on android image load (#108)
Closes: #105

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

Reviewed-on: Georgi_Lab/ALVINN_f7#108
2024-02-22 17:29:44 -07:00
6ff63bfd65 Release 0.3.0
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-02-22 09:34:40 -07:00
45a86399e4 Fix chip results scroll bar always on
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
2024-02-22 09:27:49 -07:00
72f2d5c488 Fix Cordova mobile and PWA model locations (#104)
Signed-off-by: Justin Georgi <justin.georgi@gmail.com>
Reviewed-on: Georgi_Lab/ALVINN_f7#104
2024-02-21 17:55:23 -07:00
ede015ef70 Fix results scrolling (#102)
Closes: #96

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

Reviewed-on: Georgi_Lab/ALVINN_f7#102
2024-02-20 20:19:35 -07:00
55ecae0961 Fix structure box visibility errors (#101)
Closes: #100

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

Reviewed-on: Georgi_Lab/ALVINN_f7#101
2024-02-20 20:08:16 -07:00
69ede91f7b Fix fetch issue in android cordova (#99)
Tensorflow fetch failed on android with basic configuration.  More specific protocol and location for model.json required.

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

Reviewed-on: Georgi_Lab/ALVINN_f7#99
2024-02-20 08:44:37 -07:00
79d2d1bc83 Add contact form (#98)
Closes: #92

This adds a new link to the left panel to open a contact form.  Filling out the form will allow the user to submit a comment which will be registered as a new issue on this repo.

Reviewed-on: Georgi_Lab/ALVINN_f7#98
2024-02-16 21:49:54 -07:00
94d4bbb979 Add Tensorflow based local detection (#95)
Closes: #12
Reviewed-on: Georgi_Lab/ALVINN_f7#95
2024-02-14 19:42:32 -07:00
86 changed files with 2249 additions and 865 deletions

View File

@@ -5,7 +5,7 @@ Anatomy Lab Visual Identification Neural Net (A.L.V.I.N.N) is a f7 based app for
## Install
* **Android:** Download the latest Android apk in [packages](https://gitea.azgeorgis.net/Georgi_Lab/ALVINN_f7/packages) and open the downloaded file to install.
* **iOS:** To do
* **Web app:** To do
* **Web app:** Download the latest Web zip file in [packages](https://gitea.azgeorgis.net/Georgi_Lab/ALVINN_f7/packages) and extract the files to a folder location available via web access then visit that location in your web browser.
* **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

View File

@@ -17,6 +17,7 @@ const build = async () => {
format: 'iife',
name: 'MyApp',
sourcemap: false,
inlineDynamicImports :true
});
// Remove old chunk files

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="edu.midwestern.alvinn" version="0.2.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<widget id="edu.midwestern.alvinn" version="0.5.0-rc" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
<name>ALVINN</name>
<description>Anatomy Lab Visual Identification Neural Network.</description>
<author email="jgeorg@midwestern.edu" href="https://midwestern.edu">

View File

@@ -1,20 +1,18 @@
{
"name": "edu.midwestern.alvinn",
"version": "0.1.0-b",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "edu.midwestern.alvinn",
"version": "0.1.0-b",
"version": "0.4.0",
"license": "Apache-2.0",
"devDependencies": {
"cordova-android": "^12.0.1",
"cordova-browser": "^7.0.0",
"cordova-ios": "^7.0.1",
"cordova-plugin-camera": "^7.0.0",
"cordova-plugin-keyboard": "^1.2.0",
"cordova-plugin-statusbar": "^4.0.0"
"cordova-plugin-camera": "^7.0.0"
}
},
"node_modules/@netflix/nerror": {
@@ -542,39 +540,6 @@
}
}
},
"node_modules/cordova-plugin-keyboard": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-keyboard/-/cordova-plugin-keyboard-1.2.0.tgz",
"integrity": "sha512-Zng4SgDQQ2BCqeDOvrsgNlM9tcjnxmJoh0Qhex0KltMsoR0g/ONbMTpaVvI8EhNKVO8HJPnhFxxzHxrCPLQ7sQ==",
"dev": true,
"engines": [
{
"name": "cordova",
"version": ">=3.2.0"
}
]
},
"node_modules/cordova-plugin-statusbar": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-4.0.0.tgz",
"integrity": "sha512-NTVdQhj9msydnarCH8ijOhASx+6fO16hg17AilAjfzWRvcBoF+4kKkPhTkZM7D2bOBePhfHEaMhEnxnA3M4Dlg==",
"dev": true,
"engines": {
"cordovaDependencies": {
"0.1.0": {
"cordova": ">=3.0.0"
},
"4.0.0": {
"cordova": ">=3.0.0",
"cordova-android": ">=10.0.0",
"cordova-ios": ">=6.0.0"
},
"5.0.0": {
"cordova": ">100"
}
}
}
},
"node_modules/cordova-serve": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/cordova-serve/-/cordova-serve-4.0.1.tgz",
@@ -2652,18 +2617,6 @@
"integrity": "sha512-OVQWZTBb18Y6e5c+bbXt3E4Z1yGnYqaywh2h5vVr/+nxMcdMIE+lm527bRK5vLN/RUqhGYP/Z+5n+O7Fk7fVNw==",
"dev": true
},
"cordova-plugin-keyboard": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-keyboard/-/cordova-plugin-keyboard-1.2.0.tgz",
"integrity": "sha512-Zng4SgDQQ2BCqeDOvrsgNlM9tcjnxmJoh0Qhex0KltMsoR0g/ONbMTpaVvI8EhNKVO8HJPnhFxxzHxrCPLQ7sQ==",
"dev": true
},
"cordova-plugin-statusbar": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-4.0.0.tgz",
"integrity": "sha512-NTVdQhj9msydnarCH8ijOhASx+6fO16hg17AilAjfzWRvcBoF+4kKkPhTkZM7D2bOBePhfHEaMhEnxnA3M4Dlg==",
"dev": true
},
"cordova-serve": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/cordova-serve/-/cordova-serve-4.0.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "edu.midwestern.alvinn",
"displayName": "ALVINN",
"version": "0.2.1",
"version": "0.5.0-rc",
"description": "Anatomy Lab Visual Identification Neural Network.",
"main": "index.js",
"scripts": {
@@ -16,14 +16,10 @@
"cordova-android": "^12.0.1",
"cordova-browser": "^7.0.0",
"cordova-ios": "^7.0.1",
"cordova-plugin-camera": "^7.0.0",
"cordova-plugin-keyboard": "^1.2.0",
"cordova-plugin-statusbar": "^4.0.0"
"cordova-plugin-camera": "^7.0.0"
},
"cordova": {
"plugins": {
"cordova-plugin-statusbar": {},
"cordova-plugin-keyboard": {},
"cordova-plugin-camera": {
"ANDROIDX_CORE_VERSION": "1.6.+"
}

682
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "alvinn",
"private": true,
"version": "0.2.1",
"version": "0.5.0-rc",
"description": "ALVINN",
"repository": "",
"license": "UNLICENSED",
@@ -14,7 +14,7 @@
"cordova-ios": "cross-env TARGET=cordova cross-env NODE_ENV=production vite build && node ./build/build-cordova.js && cd cordova && cordova run ios",
"build-cordova-android": "cross-env TARGET=cordova cross-env NODE_ENV=production vite build && node ./build/build-cordova.js && cd cordova && cordova build android",
"cordova-android": "cross-env TARGET=cordova cross-env NODE_ENV=production vite build && node ./build/build-cordova.js && cd cordova && cordova run android",
"postinstall": "cpy --flat ./node_modules/framework7-icons/fonts/*.* ./src/fonts/ && cpy --flat ./node_modules/material-icons/iconfont/*.* ./src/fonts/"
"postinstall": "cpy --flat ./node_modules/framework7-icons/fonts/*.* ./src/fonts/"
},
"browserslist": [
"IOS >= 15",
@@ -23,11 +23,11 @@
"last 5 Firefox versions"
],
"dependencies": {
"@tensorflow/tfjs": "^4.17.0",
"dom7": "^4.0.6",
"framework7": "^8.3.0",
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.3.0",
"material-icons": "^1.13.12",
"skeleton-elements": "^4.0.1",
"swiper": "^11.0.3",
"vue": "^3.3.8"

View File

@@ -0,0 +1,11 @@
{
"version": "0.0.0-n1",
"region": "Coco",
"size": 640,
"epochs": 1000,
"name": "coco128 test",
"yolo-version": "8.1.20 docker",
"date": "2024-03-12",
"export": "coco128.yaml"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,92 @@
description: Ultralytics best model trained on /usr/src/ultralytics/ultralytics/cfg/datasets/coco128.yaml
author: Ultralytics
license: AGPL-3.0 https://ultralytics.com/license
date: '2024-03-12T16:25:00.089873'
version: 8.1.20
stride: 32
task: detect
batch: 1
imgsz:
- 640
- 640
names:
0: person
1: bicycle
2: car
3: motorcycle
4: airplane
5: bus
6: train
7: truck
8: boat
9: traffic light
10: fire hydrant
11: stop sign
12: parking meter
13: bench
14: bird
15: cat
16: dog
17: horse
18: sheep
19: cow
20: elephant
21: bear
22: zebra
23: giraffe
24: backpack
25: umbrella
26: handbag
27: tie
28: suitcase
29: frisbee
30: skis
31: snowboard
32: sports ball
33: kite
34: baseball bat
35: baseball glove
36: skateboard
37: surfboard
38: tennis racket
39: bottle
40: wine glass
41: cup
42: fork
43: knife
44: spoon
45: bowl
46: banana
47: apple
48: sandwich
49: orange
50: broccoli
51: carrot
52: hot dog
53: pizza
54: donut
55: cake
56: chair
57: couch
58: potted plant
59: bed
60: dining table
61: toilet
62: tv
63: laptop
64: mouse
65: remote
66: keyboard
67: cell phone
68: microwave
69: oven
70: toaster
71: sink
72: refrigerator
73: book
74: clock
75: vase
76: scissors
77: teddy bear
78: hair drier
79: toothbrush

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,82 @@
[
"person",
"bicycle",
"car",
"motorcycle",
"airplane",
"bus",
"train",
"truck",
"boat",
"traffic light",
"fire hydrant",
"stop sign",
"parking meter",
"bench",
"bird",
"cat",
"dog",
"horse",
"sheep",
"cow",
"elephant",
"bear",
"zebra",
"giraffe",
"backpack",
"umbrella",
"handbag",
"tie",
"suitcase",
"frisbee",
"skis",
"snowboard",
"sports ball",
"kite",
"baseball bat",
"baseball glove",
"skateboard",
"surfboard",
"tennis racket",
"bottle",
"wine glass",
"cup",
"fork",
"knife",
"spoon",
"bowl",
"banana",
"apple",
"sandwich",
"orange",
"broccoli",
"carrot",
"hot dog",
"pizza",
"donut",
"cake",
"chair",
"couch",
"potted plant",
"bed",
"dining table",
"toilet",
"tv",
"laptop",
"mouse",
"remote",
"keyboard",
"cell phone",
"microwave",
"oven",
"toaster",
"sink",
"refrigerator",
"book",
"clock",
"vase",
"scissors",
"teddy bear",
"hair drier",
"toothbrush"
]

View File

@@ -0,0 +1,11 @@
{
"version": "0.0.0-n1",
"region": "Coco",
"size": 640,
"epochs": 1000,
"name": "coco128 test",
"yolo-version": "8.1.20 docker",
"date": "2024-03-12",
"export": "coco128.yaml"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,92 @@
description: Ultralytics best model trained on /usr/src/ultralytics/ultralytics/cfg/datasets/coco128.yaml
author: Ultralytics
license: AGPL-3.0 https://ultralytics.com/license
date: '2024-03-12T16:25:00.089873'
version: 8.1.20
stride: 32
task: detect
batch: 1
imgsz:
- 640
- 640
names:
0: person
1: bicycle
2: car
3: motorcycle
4: airplane
5: bus
6: train
7: truck
8: boat
9: traffic light
10: fire hydrant
11: stop sign
12: parking meter
13: bench
14: bird
15: cat
16: dog
17: horse
18: sheep
19: cow
20: elephant
21: bear
22: zebra
23: giraffe
24: backpack
25: umbrella
26: handbag
27: tie
28: suitcase
29: frisbee
30: skis
31: snowboard
32: sports ball
33: kite
34: baseball bat
35: baseball glove
36: skateboard
37: surfboard
38: tennis racket
39: bottle
40: wine glass
41: cup
42: fork
43: knife
44: spoon
45: bowl
46: banana
47: apple
48: sandwich
49: orange
50: broccoli
51: carrot
52: hot dog
53: pizza
54: donut
55: cake
56: chair
57: couch
58: potted plant
59: bed
60: dining table
61: toilet
62: tv
63: laptop
64: mouse
65: remote
66: keyboard
67: cell phone
68: microwave
69: oven
70: toaster
71: sink
72: refrigerator
73: book
74: clock
75: vase
76: scissors
77: teddy bear
78: hair drier
79: toothbrush

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
{
"version": "0.1.0-n4",
"region": "Thorax",
"size": 640,
"epochs": 1000,
"name": "nano4",
"yolo-version": "8.1.20 docker",
"date": "2024-03-08",
"export": "0.1.0-th"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,53 @@
description: Ultralytics best model trained on /data/ALVINN/Thorax/Thorax 0.1.0/thorax.yaml
author: Ultralytics
license: AGPL-3.0 https://ultralytics.com/license
date: '2024-03-08T20:14:34.118186'
version: 8.1.20
stride: 32
task: detect
batch: 1
imgsz:
- 640
- 640
names:
0: Abdominal diaphragm
1: Aorta
2: Azygous vein
3: Brachiocephalic trunk
4: Caudal vena cava
5: Cranial vena cava
6: Esophagus
7: External abdominal oblique
8: Iliocostalis
9: Latissimus dorsi
10: Left atrium
11: Left auricle
12: Left lung
13: Left subclavian artery
14: Left ventricle
15: Longissimus
16: Pectoralis profundus
17: Pectoralis superficialis
18: Pericardium
19: Phrenic nerve
20: Primary bronchus
21: Pulmonary artery
22: Pulmonary trunk
23: Pulmonary vein
24: Rectus abdominis
25: Rectus thoracis
26: Recurrent laryngeal nerve
27: Rhomboideus
28: Right atrium
29: Right auricle
30: Right lung
31: Right ventricle
32: Scalenus
33: Serratus dorsalis caudalis
34: Serratus dorsalis cranialis
35: Serratus ventralis
36: Spinalis
37: Sympathetic chain
38: Trachea
39: Trapezius
40: Vagus nerve

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
[
"Abdominal diaphragm",
"Aorta",
"Azygous vein",
"Brachiocephalic trunk",
"Caudal vena cava",
"Cranial vena cava",
"Esophagus",
"External abdominal oblique",
"Iliocostalis",
"Latissimus dorsi",
"Left atrium",
"Left auricle",
"Left lung",
"Left subclavian artery",
"Left ventricle",
"Longissimus",
"Pectoralis profundus",
"Pectoralis superficialis",
"Pericardium",
"Phrenic nerve",
"Primary bronchus",
"Pulmonary artery",
"Pulmonary trunk",
"Pulmonary vein",
"Rectus abdominis",
"Rectus thoracis",
"Recurrent laryngeal nerve",
"Rhomboideus",
"Right atrium",
"Right auricle",
"Right lung",
"Right ventricle",
"Scalenus",
"Serratus dorsalis caudalis",
"Serratus dorsalis cranialis",
"Serratus ventralis",
"Spinalis",
"Sympathetic chain",
"Trachea",
"Trapezius",
"Vagus nerve"
]

View File

@@ -0,0 +1,10 @@
{
"version": "0.1.0-s1",
"region": "Thorax",
"size": 640,
"epochs": 1000,
"name": "small1",
"yolo-version": "8.1.20 docker",
"date": "2024-03-07",
"export": "0.1.0-th"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,53 @@
description: Ultralytics best model trained on /data/ALVINN/Thorax/Thorax 0.1.0/thorax.yaml
author: Ultralytics
license: AGPL-3.0 https://ultralytics.com/license
date: '2024-03-07T16:03:03.296997'
version: 8.1.20
stride: 32
task: detect
batch: 1
imgsz:
- 640
- 640
names:
0: Abdominal diaphragm
1: Aorta
2: Azygous vein
3: Brachiocephalic trunk
4: Caudal vena cava
5: Cranial vena cava
6: Esophagus
7: External abdominal oblique
8: Iliocostalis
9: Latissimus dorsi
10: Left atrium
11: Left auricle
12: Left lung
13: Left subclavian artery
14: Left ventricle
15: Longissimus
16: Pectoralis profundus
17: Pectoralis superficialis
18: Pericardium
19: Phrenic nerve
20: Primary bronchus
21: Pulmonary artery
22: Pulmonary trunk
23: Pulmonary vein
24: Rectus abdominis
25: Rectus thoracis
26: Recurrent laryngeal nerve
27: Rhomboideus
28: Right atrium
29: Right auricle
30: Right lung
31: Right ventricle
32: Scalenus
33: Serratus dorsalis caudalis
34: Serratus dorsalis cranialis
35: Serratus ventralis
36: Spinalis
37: Sympathetic chain
38: Trachea
39: Trapezius
40: Vagus nerve

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

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

@@ -7,10 +7,11 @@
<f7-navbar title="ALVINN"></f7-navbar>
<f7-list>
<f7-list-item link="/settings/" view=".view-main" panel-close=".panel-left">Settings</f7-list-item>
<f7-list-item link="/about/">About ALVINN</f7-list-item>
<f7-list-item link="/about/" view=".view-main" panel-close=".panel-left">About ALVINN</f7-list-item>
<f7-list-item link="/contact/" view=".view-main" panel-close=".panel-left">Contact</f7-list-item>
</f7-list>
<f7-toolbar class="panel-bar" position="bottom">
<span>version 0.2.1</span>
<f7-link href="/specs/">version {{ alvinnVersion }}</f7-link>
</f7-toolbar>
</f7-page>
</f7-view>
@@ -64,7 +65,9 @@
return {
rememberAgreement: false,
siteAgreement: false,
showDisclaimer: true
dateAgreement: null,
showDisclaimer: true,
alvinnVersion: store().getVersion
}
},
created () {
@@ -73,15 +76,17 @@
var loadedSettings = JSON.parse(loadSiteSettings)
this.siteAgreement = loadedSettings.siteAgreement
this.rememberAgreement = loadedSettings.rememberAgreement
this.dateAgreement = loadedSettings.dateAgreement && new Date(loadedSettings.dateAgreement)
}
if (this.siteAgreement && this.rememberAgreement) {
var curDate = new Date ()
var agreeStillValid = this.dateAgreement && (curDate < this.dateAgreement.setMonth(this.dateAgreement.getMonth() + 3))
if (this.siteAgreement && this.rememberAgreement && agreeStillValid) {
this.showDisclaimer = false
store().agree()
}
var loadServerSettings = localStorage.getItem('serverSettings')
if (!loadServerSettings) {
//Temp default settings for MWU deployment
localStorage.setItem('serverSettings','{"use":true,"address":"10.188.0.98","port":"9001","previous":{"10.188.0.98":"9001"}}')
localStorage.setItem('serverSettings','{"use":false,"address":"10.188.0.98","port":"9001","previous":{"10.188.0.98":"9001"}}')
}
},
methods: {
@@ -90,7 +95,8 @@
store().agree()
let newSettings = {
siteAgreement: this.siteAgreement,
rememberAgreement: this.rememberAgreement
rememberAgreement: this.rememberAgreement,
dateAgreement: new Date()
}
let saveSiteSettings = new Promise(
(saved,failed) => {
@@ -126,7 +132,6 @@
} catch {
var darkTheme = 'auto'
}
//provide('isAgreed',siteAgreement)
const f7params = {
name: 'ALVINN', // App name
theme: 'auto', // Automatic theme detection
@@ -140,7 +145,7 @@
// Register service worker (only on production build)
serviceWorker: process.env.NODE_ENV ==='production' ? {
path: '/service-worker.js',
path: './service-worker.js',
} : {},
// Input settings

View File

@@ -15,6 +15,7 @@
<path v-else-if="icon == 'abdomen'" d="M120-80v-240q0-50 35-85t85-35h80q50 0 85-35t35-85q0-17-11.5-28.5T400-600q-33 0-56.5-23.5T320-680v-200h80v200q50 0 85 35t35 85q0 83-58.5 141.5T320-360h-80q-17 0-28.5 11.5T200-320v240h-80Zm240 0h-80v-80q0-50 35-85t85-35h160q83 0 141.5-58.5T760-480v-40q0-83-58.5-141.5T560-720q-33 0-56.5-23.5T480-800v-80h80v80q117 0 198.5 81.5T840-520v40q0 117-81.5 198.5T560-200H400q-17 0-28.5 11.5T360-160v80Zm-160 0v-240q0-17 11.5-28.5T240-360h80q83 0 141.5-58.5T520-560q0-50-35-85t-85-35v-200 200q50 0 85 35t35 85q0 83-58.5 141.5T320-360h-80q-17 0-28.5 11.5T200-320v240Z"/>
<path v-else-if="icon == 'limbs'" d="M540-440q17 0 28.5-11.5T580-480q0-7-1.5-12.5T574-503q11-4 18.5-14t7.5-23q0-17-11.5-28.5T560-580q-13 0-23 7t-14 19l-146-70q2-4 2.5-8t.5-8q0-17-11.5-28.5T340-680q-17 0-28.5 11.5T300-640q0 6 2 11.5t5 10.5q-11 4-19 14t-8 24q0 17 11.5 28.5T320-540q14 0 24-7.5t14-19.5l146 70-4 17q0 17 11.5 28.5T540-440ZM394-80q-16-47-24-92.5t-10-86q-2-40.5-.5-74.5t4.5-58q-1 0 0 0-22-5-50.5-12.5t-61-20.5Q220-437 186-455.5T119-500l50-70q39 35 81.5 55.5t78.5 32q36 11.5 60 15l24 3.5q18 1 28.5 15t7.5 32l-4.5 33.5q-4.5 33.5-5 83.5t7.5 109q8 59 33 111h-86Zm366 0h-80v-423q0-48-25.5-87T586-649L313-772l49-67 257 117q64 29 102.5 88T760-503v423Zm-280 0q-25-52-33-111t-7.5-109q.5-50 5-83.5L449-417q3-18-7.5-32T413-464l-24-3.5q-24-3.5-60-15t-78.5-32Q208-535 169-570q39 35 81.5 55.5t78.5 32q36 11.5 60 15l24 3.5q18 1 28.5 15t7.5 32l-4.5 33.5q-4.5 33.5-5 83.5t7.5 109q8 59 33 111Z"/>
<path v-else-if="icon == 'head'" d="M194-80v-395h80v315h280v-193l105-105q29-29 45-65t16-77q0-40-16.5-76T659-741l-25-26-127 127H347l-43 43-57-56 67-67h160l160-160 82 82q40 40 62 90.5T800-600q0 57-22 107.5T716-402l-82 82v240H194Zm197-187L183-475q-11-11-17-26t-6-31q0-16 6-30.5t17-25.5l84-85 124 123q28 28 43.5 64.5T450-409q0 40-15 76.5T391-267Z"/>
<path v-else-if="icon == 'photo_sample'" d="M240-80q-33 0-56.5-23.5T160-160v-640q0-33 23.5-56.5T240-880h480q33 0 56.5 23.5T800-800v640q0 33-23.5 56.5T720-80H240Zm0-80h480v-640h-80v280l-100-60-100 60v-280H240v640Zm40-80h400L545-420 440-280l-65-87-95 127Zm-40 80v-640 640Zm200-360 100-60 100 60-100-60-100 60Z"/>
</svg>
</template>
@@ -40,7 +41,8 @@
'thorax',
'abdomen',
'limbs',
'head'
'head',
'photo_sample'
]
return iconList.includes(value)
}

198
src/css/detect.css Normal file
View File

@@ -0,0 +1,198 @@
/*Styles for the structure detection page*/
/*Basic style*/
.detect-grid {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr minmax(calc(var(--f7-chip-height) + 33px), auto) auto min-content;
grid-template-areas:
"image-view"
"result-view"
"detect-settings"
"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 {
grid-area: image-view;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
position: relative;
display: flex;
align-self: stretch;
}
.popover-button-menu {
max-width: 90vw;
max-height: 90vh;
width: auto;
}
.segment-button-menu {
flex-wrap: nowrap;
flex-direction: column;
max-height: 100%;
min-height: 0px;
}
.chip-media {
/*background-color: var(--chip-media-background) !important;*/
background: var(--chip-media-gradient) !important;
}
.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;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
}
.chip-results .chip {
padding-left: 8px;
}
.progress-hide {
display: hidden;
}
.selected-chip {
font-weight: 500;
box-shadow: 4px 4px 1px var(--avn-theme-color);
transform: translate(-2px, -2px);
}
.detect-inputs {
display: flex;
align-items: center;
margin: 5px;
width: 100%;
max-width: 400px;
min-width: 192px;
}
.level-slide-vert {
display: none;
}
.image-menu {
grid-area: menu-view;
margin: 5px;
max-width: 400px;
min-width: 192px;
}
.image-menu .button {
aspect-ratio: 1;
height: auto;
padding: 5px;
flex: 1 1 0%;
}
.image-menu > .button > svg {
aspect-ratio: 1;
height: auto;
width: 100%;
}
.segment-button-menu .button {
padding: 8px;
aspect-ratio: 1;
width: auto;
flex: 1 1 0%;
max-height: 100px;
max-width: 100px;
}
.dialog-buttons {
align-items: center;
}
.avn-dialog-button {
font-size: 17px;
align-self: center;
}
/*Additional styles for small format landscape orientation*/
@media (max-height: 450px) and (orientation: landscape) {
.detect-grid {
grid-template-columns: minmax(0,1fr) minmax(56px,max-content) auto 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 detect-settings menu-view";
justify-items: stretch;
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;
}
.detect-inputs {
flex-direction: column;
min-width: 0;
max-width: 72px;
}
.level-slide-horz {
display: none;
}
.level-slide-vert {
display: block;
}
.image-container {
flex-direction: column;
}
.image-menu {
flex-direction: column;
aspect-ratio: .25;
width: auto;
min-width: 0;
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);
border-bottom-left-radius: 0px !important;
}
.segment-button-menu {
flex-direction: row;
max-height: 100%;
min-height: 0px;
}
.segment-button-menu .button {
height: auto;
flex: 1 1 0%;
max-height: 100px;
max-width: 100px;
}
.button > svg {
width: 100%;
height: auto;
}
}

View File

@@ -1,30 +1,3 @@
/* Material Icons Font (for MD theme) */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'),
url(../fonts/material-icons.woff2) format('woff2'),
url(../fonts/material-icons.woff) format('woff');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
}
/* Framework7 Icons Font (for iOS theme) */
@font-face {
font-family: 'Framework7 Icons';
@@ -54,3 +27,29 @@
font-feature-settings: 'liga';
text-align: center;
}
/* Material Icons for text editor toolbar */
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../fonts/material-icons.woff2") format("woff2"), url("../fonts/material-icons.woff") format("woff");
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,55 +0,0 @@
// @see https://github.com/twbs/bootstrap/blob/main/scss/_functions.scss
@function material-icons-str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace +
material-icons-str-replace(
str-slice($string, $index + str-length($search)),
$search,
$replace
);
}
@return $string;
}
@mixin material-icons-font-class($font-family) {
font-family: $font-family;
font-weight: normal;
font-style: normal;
font-size: $material-icons-font-size;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased; // Support for all WebKit browsers
-moz-osx-font-smoothing: grayscale; // Support for Firefox
text-rendering: optimizeLegibility; // Support for Safari and Chrome
font-feature-settings: 'liga'; // Support for IE
}
@mixin material-icons-font($font-family) {
$class-name: to-lower-case($font-family);
$class-name: material-icons-str-replace($class-name, ' ', '-');
$font-file: $material-icons-font-path + $class-name;
@font-face {
font-family: $font-family;
font-style: normal;
font-weight: 400;
font-display: $material-icons-font-display;
src: url('#{$font-file}.woff2') format('woff2'),
url('#{$font-file}.woff') format('woff');
}
.#{$class-name} {
@include material-icons-font-class($font-family);
}
}
@mixin material-icons() {
@warn "material-icons() Sass mixin has been deprecated as of 1.0. Use '@extend .material-icons;' instead of '@include material-icons();'.";
@include material-icons-font-class('Material Icons');
}

View File

@@ -1,3 +0,0 @@
$material-icons-font-path: './' !default;
$material-icons-font-size: 24px !default;
$material-icons-font-display: block !default;

View File

@@ -1,24 +0,0 @@
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons.woff2") format("woff2"), url("./material-icons.woff") format("woff");
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,4 +0,0 @@
@import 'variables';
@import 'mixins';
@include material-icons-font('Material Icons');

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,124 +0,0 @@
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons.woff2") format("woff2"), url("./material-icons.woff") format("woff");
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}
@font-face {
font-family: "Material Icons Outlined";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-outlined.woff2") format("woff2"), url("./material-icons-outlined.woff") format("woff");
}
.material-icons-outlined {
font-family: "Material Icons Outlined";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}
@font-face {
font-family: "Material Icons Round";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-round.woff2") format("woff2"), url("./material-icons-round.woff") format("woff");
}
.material-icons-round {
font-family: "Material Icons Round";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}
@font-face {
font-family: "Material Icons Sharp";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-sharp.woff2") format("woff2"), url("./material-icons-sharp.woff") format("woff");
}
.material-icons-sharp {
font-family: "Material Icons Sharp";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}
@font-face {
font-family: "Material Icons Two Tone";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-two-tone.woff2") format("woff2"), url("./material-icons-two-tone.woff") format("woff");
}
.material-icons-two-tone {
font-family: "Material Icons Two Tone";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,5 +0,0 @@
@import 'filled';
@import 'outlined';
@import 'round';
@import 'sharp';
@import 'two-tone';

View File

@@ -1,24 +0,0 @@
@font-face {
font-family: "Material Icons Outlined";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-outlined.woff2") format("woff2"), url("./material-icons-outlined.woff") format("woff");
}
.material-icons-outlined {
font-family: "Material Icons Outlined";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,4 +0,0 @@
@import 'variables';
@import 'mixins';
@include material-icons-font('Material Icons Outlined');

View File

@@ -1,24 +0,0 @@
@font-face {
font-family: "Material Icons Round";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-round.woff2") format("woff2"), url("./material-icons-round.woff") format("woff");
}
.material-icons-round {
font-family: "Material Icons Round";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,4 +0,0 @@
@import 'variables';
@import 'mixins';
@include material-icons-font('Material Icons Round');

View File

@@ -1,24 +0,0 @@
@font-face {
font-family: "Material Icons Sharp";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-sharp.woff2") format("woff2"), url("./material-icons-sharp.woff") format("woff");
}
.material-icons-sharp {
font-family: "Material Icons Sharp";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,4 +0,0 @@
@import 'variables';
@import 'mixins';
@include material-icons-font('Material Icons Sharp');

View File

@@ -1,24 +0,0 @@
@font-face {
font-family: "Material Icons Two Tone";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("./material-icons-two-tone.woff2") format("woff2"), url("./material-icons-two-tone.woff") format("woff");
}
.material-icons-two-tone {
font-family: "Material Icons Two Tone";
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-feature-settings: "liga";
}

View File

@@ -1,4 +0,0 @@
@import 'variables';
@import 'mixins';
@include material-icons-font('Material Icons Two Tone');

View File

@@ -10,7 +10,7 @@
* Disables use of inline scripts in order to mitigate risk of XSS vulnerabilities. To change this:
* Enable inline JS: add 'unsafe-inline' to default-src
-->
<meta http-equiv="Content-Security-Policy" content="worker-src blob:; child-src blob: gap:; img-src 'self' blob: data:; default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: content:">
<meta http-equiv="Content-Security-Policy" content="worker-src 'self' blob:; child-src blob: gap:; img-src 'self' blob: data:; default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: content:">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#fff">

View File

@@ -3,6 +3,9 @@ import HomePage from '../pages/home.vue';
import AboutPage from '../pages/about.vue';
import SettingsPage from '../pages/settings.vue';
import DetectPage from '../pages/detect.vue';
import ContactPage from '../pages/contact.vue';
import SpecsPage from '../pages/specs.vue';
import HelpPage from '../pages/help.vue';
import NotFoundPage from '../pages/404.vue';
@@ -15,13 +18,25 @@ var routes = [
path: '/about/',
component: AboutPage,
},
{
path: '/help/',
component: HelpPage,
},
{
path: '/specs/',
component: SpecsPage,
},
{
path: '/detect/:region/',
component: DetectPage,
},
{
path: '/settings/',
component: SettingsPage,
component: SettingsPage
},
{
path: '/contact/',
component: ContactPage,
},
{
path: '(.*)',

View File

@@ -2,7 +2,8 @@ import { reactive, computed } from 'vue';
const state = reactive({
disclaimerAgreement: false,
enabledRegions: ['thorax','abdomen','limbs']
enabledRegions: ['thorax','abdomen','limbs'],
version: '0.5.0-rc'
})
const agree = () => {
@@ -12,5 +13,6 @@ const agree = () => {
export default () => ({
isAgreed: computed(() => state.disclaimerAgreement),
getRegions: computed(() => state.enabledRegions),
getVersion: computed(() => state.version),
agree
})

View File

@@ -9,32 +9,32 @@
"theme_color": "#002f65",
"icons": [
{
"src": "/icons/128x128.png",
"src": "../icons/128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icons/144x144.png",
"src": "../icons/144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icons/152x152.png",
"src": "../icons/152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icons/192x192.png",
"src": "../icons/192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/256x256.png",
"src": "../icons/256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/icons/512x512.png",
"src": "../icons/512x512.png",
"sizes": "512x512",
"type": "image/png"
}

View File

@@ -1,8 +1,8 @@
<template>
<f7-page name="about">
<f7-navbar title="About" back-link="Back"></f7-navbar>
<f7-block-title>About A.L.V.I.N.N.</f7-block-title>
<f7-block>
<h2>About A.L.V.I.N.N.</h2>
<p>
ALVINN is an object detection neural network specializing in the identification of anatomical structures from imagery of dissected, embalmed specimens in anatomy teaching labratories.
ALVINN is intended as a learning aid only; it is not a tool for medical diagnosis, intervention, or treatment.
@@ -10,8 +10,14 @@
<p>
All of the images in ALVINN's models have been collected by students and faculty of Midwestern University's anatomy classes.
</p>
<p>
Source code for ALVINN can be <f7-link :external="true" target="_blank" href="https://gitea.azgeorgis.net/ALVINN/ALVINN_f7/issues">found here</f7-link>.
</p>
<h3>Credits</h3>
<p>
ALVINN is conceived and created by Justin Georgi.
</p>
<p>
Other contributions from:
<ul>
<li>Alexandra Davis</li>

41
src/pages/camera-mixin.js Normal file
View File

@@ -0,0 +1,41 @@
export default {
methods: {
async openCamera(imContain) {
var cameraLoaded = false
const devicesList = await navigator.mediaDevices.enumerateDevices()
this.videoDeviceAvailable = devicesList.some( d => d.kind == "videoinput")
if (this.videoDeviceAvailable) {
var vidConstraint = {
video: {
width: {
ideal: imContain.offsetWidth
},
height: {
ideal: imContain.offsetHeight
},
facingMode: 'environment'
},
audio: false
}
const stream = await navigator.mediaDevices.getUserMedia(vidConstraint);
cameraLoaded = true
this.cameraStream = stream
}
return cameraLoaded
},
closeCamera () {
this.cameraStream.getTracks().forEach( t => t.stop())
this.videoAvailable = false
},
captureVidFrame() {
const vidViewer = this.$refs.vid_viewer
vidViewer.pause()
let tempCVS = document.createElement('canvas')
tempCVS.height = vidViewer.videoHeight || parseInt(vidViewer.style.height)
tempCVS.width = vidViewer.videoWidth || parseInt(vidViewer.style.width)
const tempCtx = tempCVS.getContext('2d')
tempCtx.drawImage(vidViewer, 0, 0)
this.getImage(tempCVS.toDataURL())
}
}
}

95
src/pages/contact.vue Normal file
View File

@@ -0,0 +1,95 @@
<template>
<f7-page name="contact">
<f7-navbar title="Contact" back-link="Back"></f7-navbar>
<f7-block-title medium style="text-align: center;">Contact the ALVINN team</f7-block-title>
<f7-block style="display: flex; justify-content: center;">
<div class="form-container">
<p>
ALVINN can only get better with your feedback. Use the form below to send us any questions or let us know how we can improve.
</p>
<f7-list class="form-element">
<f7-list-input v-model:value="userEmail" label="E-mail (optional)" type="email" placeholder="Your e-mail" clear-button />
<f7-list-input v-model:value="commentType" label="This is a... (optional)" type="select" placeholder="Select comment type">
<option value="20">Bug</option>
<option value="22">Feature request</option>
<option value="25">Question</option>
</f7-list-input>
<f7-list-input v-model:value="commentTitle" label="Subject of comment (optional)" type="textarea" resizable placeholder="Type here" />
</f7-list>
<f7-text-editor class="form-element comment-editor"/>
<div style="align-self: flex-end; display: flex; gap: 15px;">
<f7-button fill @click="clearForm">
Clear form
</f7-button>
<f7-button fill @click="sendFeedback">
Send feedback
</f7-button>
</div>
</div>
</f7-block>
</f7-page>
</template>
<style>
.form-container {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.form-element {
align-self: stretch;
}
</style>
<script>
import { f7 } from 'framework7-vue'
export default {
data () {
return {
commentTitle: "",
userEmail: "",
commentType: ""
}
},
computed: {
commentText () {
var text = f7.textEditor.get('.comment-editor').getValue()
if (this.userEmail) {
text += `\\n\\nSubmitted by: ${this.userEmail}`
}
return text
}
},
methods: {
sendFeedback () {
var self = this
var issueURL = `https://gitea.azgeorgis.net/api/v1/repos/Georgi_Lab/ALVINN_f7/issues?access_token=9af8ae15b1ee5a98afcb3083bb488e4cf3c683af`
var xhr = new XMLHttpRequest()
xhr.open("POST", issueURL)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.setRequestHeader('accept', 'application/json')
xhr.onload = function () {
if (this.status !== 201) {
console.log(xhr.response)
const errorResponse = JSON.parse(xhr.response)
f7.dialog.alert(`ALVINN has encountered an error: ${errorResponse.error}`)
return;
}
f7.dialog.alert('Thank you for your feedback.')
self.clearForm()
}
xhr.send(`{"body": "${this.commentText}", "labels": [${this.commentType}], "title": "${this.commentTitle || 'User submitted comment'}"}`)
},
clearForm () {
this.commentTitle = ''
this.userEmail = ''
this.commentType = ''
f7.textEditor.get('.comment-editor').clearValue();
}
}
}
</script>

View File

@@ -3,13 +3,21 @@
<!-- Top Navbar -->
<f7-navbar :sliding="false" :back-link="true" back-link-url="/" back-link-force>
<f7-nav-title sliding>{{ regions[activeRegion] }}</f7-nav-title>
<f7-nav-right>
<f7-link v-if="!isCordova" :icon-only="true" tooltip="Fullscreen" :icon-f7="isFullscreen ? 'viewfinder_circle_fill' : 'viewfinder'" @click="toggleFullscreen"></f7-link>
<f7-link :icon-only="true" tooltip="ALVINN help" icon-f7="question_circle_fill" href="/help/"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-block class="detect-grid">
<div class="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" :icon="f7route.params.region" fill-color="var(--avn-theme-color)" @click="selectImage" />
<div class="image-container" ref="image_container">
<SvgIcon v-if="!imageView && !videoAvailable" :icon="f7route.params.region" fill-color="var(--avn-theme-color)" @click="selectImage" />
<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>
<f7-button @click="captureVidFrame()" style="position: absolute; bottom: 32px; left: 50%; transform: translateX(-50%); z-index: 3;" fill large>Capture</f7-button>
</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 v-if="(resultData && resultData.detections) || detecting" 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 })"
:class="(result.resultIndex == selectedChip) ? 'selected-chip' : ''"
:text="result.label"
@@ -20,8 +28,8 @@
@delete="deleteChip(result.resultIndex)"
:style="chipGradient(result.confidence)"
/>
<span v-if="numResults == 0 && !detecting" style="height: var(--f7-chip-height); font-size: calc(var(--f7-chip-height) - 4px); font-weight: bolder; margin: 2px;">No results.</span>
<f7-preloader v-if="detecting" size="32" style="color: var(--avn-theme-color);" />
<div v-if="!numResults" style="height: var(--f7-chip-height); width: 100%; text-align: center; font-size: calc(var(--f7-chip-height) - 4px); font-weight: bolder; margin: 2px;">{{ message }}</div>
<f7-progressbar v-if="(detecting || modelLoading)" style="width: 100%;" :infinite="true" />
</div>
<div v-if="showDetectSettings" class="detect-inputs" style="grid-area: detect-settings;">
<f7-range class="level-slide-horz" :min="0" :max="100" :step="1" @range:change="onLevelChange" v-model:value="detectorLevel" type="range" style="flex: 1 1 100%"/>
@@ -37,17 +45,22 @@
<f7-button popover-open="#region-popover">
<RegionIcon :region="activeRegion" />
</f7-button>
<f7-button popover-open="#capture-popover">
<f7-button v-if="!videoAvailable" :class="(!modelLoading) ? '' : 'disabled'" popover-open="#capture-popover">
<SvgIcon icon="camera_add"/>
</f7-button>
<f7-button v-if="videoAvailable" @click="closeCamera()">
<SvgIcon icon="no_photography"/>
</f7-button>
<f7-button @click="() => showDetectSettings = !showDetectSettings" :class="(imageLoaded) ? '' : 'disabled'">
<SvgIcon icon="visibility"/>
<f7-badge v-if="numResults && (showResults.length != numResults)" color="red" style="position: absolute; right: 15%; top: 15%;">{{ showResults.length - numResults }}</f7-badge>
</f7-button>
<f7-button :class="(numResults && uploadDirty && viewedAll) ? '' : 'disabled'" @click="submitData">
<SvgIcon :icon="(uploadUid) ? 'cloud_done' : 'cloud_upload'"/>
</f7-button>
</f7-segmented>
<input type="file" ref="image_chooser" @change="getImage()" accept="image/*" capture="environment" 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-panel :id="detectorName + '-settings'" right cover :backdrop="false" :container-el="`#${detectorName}-detect-page`">
@@ -86,8 +99,8 @@
<f7-button style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('file')">
<SvgIcon icon="photo_library" />
</f7-button>
<f7-button style="height: auto; width: auto;" popover-close="#capture-popover" class="disabled" @click="videoStream">
<SvgIcon icon="videocam"/>
<f7-button v-if="otherSettings.demo" style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('sample')">
<SvgIcon icon="photo_sample"/>
</f7-button>
</f7-segmented>
</f7-popover>
@@ -95,188 +108,7 @@
</f7-page>
</template>
<style>
.detect-grid {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 56px auto min-content;
grid-template-areas:
"image-view"
"result-view"
"detect-settings"
"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 {
grid-area: image-view;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
position: relative;
display: flex;
align-self: stretch;
}
.popover-button-menu {
max-width: 90vw;
max-height: 90vh;
width: auto;
}
.segment-button-menu {
flex-wrap: nowrap;
flex-direction: column;
max-height: 100%;
min-height: 0px;
}
.chip-media {
background-color: var(--chip-media-background) !important;
}
.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(--avn-theme-color);
transform: translate(-2px, -2px);
}
.detect-inputs {
display: flex;
align-items: center;
margin: 5px;
width: 100%;
max-width: 400px;
min-width: 192px;
}
.level-slide-vert {
display: none;
}
.image-menu {
grid-area: menu-view;
margin: 5px;
/*width: 90%;*/
max-width: 400px;
min-width: 192px;
}
.image-menu .button {
aspect-ratio: 1;
height: auto;
padding: 5px;
flex: 1 1 0%;
}
.image-menu > .button > svg {
aspect-ratio: 1;
height: auto;
width: 100%;
}
.segment-button-menu .button {
padding: 8px;
aspect-ratio: 1;
width: auto;
flex: 1 1 0%;
max-height: 100px;
max-width: 100px;
}
@media (max-height: 450px) and (orientation: landscape) {
.detect-grid {
grid-template-columns: minmax(0,1fr) minmax(56px,max-content) auto 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 detect-settings menu-view";
justify-items: stretch;
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;
}
.detect-inputs {
flex-direction: column;
min-width: 0;
max-width: 72px;
}
.level-slide-horz {
display: none;
}
.level-slide-vert {
display: block;
}
.image-container {
flex-direction: column;
}
.image-menu {
flex-direction: column;
aspect-ratio: .25;
width: auto;
min-width: 0;
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);
border-bottom-left-radius: 0px !important;
}
.segment-button-menu {
flex-direction: row;
max-height: 100%;
min-height: 0px;
}
.segment-button-menu .button {
height: auto;
flex: 1 1 0%;
max-height: 100px;
max-width: 100px;
}
.button > svg {
width: 100%;
height: auto;
}
}
</style>
<style src="../css/detect.css" />
<script>
import { f7 } from 'framework7-vue'
@@ -286,9 +118,11 @@
import SvgIcon from '../components/svg-icon.vue'
import submitMixin from './submit-mixin'
import detectionMixin from './detection-mixin'
import cameraMixin from './camera-mixin'
export default {
mixins: [submitMixin],
mixins: [submitMixin, detectionMixin, cameraMixin],
props: {
f7route: Object,
},
@@ -302,6 +136,7 @@
resultData: {},
selectedChip: -1,
activeRegion: 4,
classesList: [],
imageLoaded: false,
imageView: null,
imageLoadMode: "environment",
@@ -312,15 +147,27 @@
detectorLevel: 50,
detectorLabels: [],
serverSettings: {},
otherSettings: {},
isCordova: !!window.cordova,
isFullscreen: false,
uploadUid: null,
uploadDirty: false
uploadDirty: false,
modelLocation: '',
miniLocation: '',
modelLoading: true,
reloadModel: false,
videoDeviceAvailable: false,
videoAvailable: false,
cameraStream: null
}
},
setup() {
return store()
},
created () {
let loadOtherSettings = localStorage.getItem('otherSettings')
if (loadOtherSettings) this.otherSettings = JSON.parse(loadOtherSettings)
let modelRoot = this.isCordova ? 'https://localhost' : '.'
switch (this.f7route.params.region) {
case 'thorax':
this.activeRegion = 0
@@ -328,8 +175,8 @@
break;
case 'abdomen':
this.activeRegion = 1
this.detectorName = 'combined'
break;
this.detectorName = 'abdomen'
break;
case 'limbs':
this.activeRegion = 2
this.detectorName = 'defaultNew'
@@ -338,36 +185,45 @@
this.activeRegion = 3
break;
}
this.modelLocation = `${modelRoot}/models/${this.detectorName}${this.otherSettings.mini ? '-mini' : ''}/model.json`
this.miniLocation = `${modelRoot}/models/${this.detectorName}-mini/model.json`
fetch(`${this.isCordova ? 'https://localhost' : '.'}/models/${this.detectorName}/classes.json`)
.then((mod) => { return mod.json() })
.then((classes) => {
this.classesList = classes
this.detectorLabels = this.classesList.map( l => { return {'name': l, 'detect': true} } )
})
var loadServerSettings = localStorage.getItem('serverSettings')
if (loadServerSettings) this.serverSettings = JSON.parse(loadServerSettings)
var self = this
},
mounted () {
if (this.serverSettings && this.serverSettings.use) {
var modelURL = `http://${this.serverSettings.address}:${this.serverSettings.port}/detectors`
var xhr = new XMLHttpRequest()
xhr.open("GET", modelURL)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.timeout = 10000
xhr.ontimeout = this.remoteTimeout
xhr.onload = function () {
if (this.status !== 200) {
console.log(xhr.response)
const errorResponse = JSON.parse(xhr.response)
f7.dialog.alert(`ALVINN has encountered an error: ${errorResponse.error}`)
return;
}
var detectors = JSON.parse(xhr.response).detectors
var findLabel = detectors
.find( d => { return d.name == self.detectorName } )?.labels
.filter( l => { return l != "" } ).sort()
.map( l => { return {'name': l, 'detect': true} } )
self.detectorLabels = findLabel || []
}
xhr.send()
this.getRemoteLabels()
this.modelLoading = false
} else {
this.modelLoading = true
this.loadModel(this.modelLocation, true).then(() => {
this.modelLoading = false
}).catch((e) => {
console.log(e.message)
f7.dialog.alert(`ALVINN AI model error: ${e.message}`)
this.modelLoading = false
})
}
window.onresize = (e) => { this.selectChip('redraw') }
},
computed: {
message () {
if (this.modelLoading) {
return "Preparing ALVINN..."
} else if (this.detecting) {
return "Finding structures..."
} else if (this.numResults == 0 && this.imageLoaded) {
return "No results."
} else {
return "ALVINN is ready."
}
},
showResults () {
var filteredResults = this.resultData.detections
if (!filteredResults) return []
@@ -381,6 +237,10 @@
filteredResults[i].aboveThreshold = d.confidence >= this.detectorLevel
filteredResults[i].isSearched = allSelect || selectedLabels.includes(d.label)
})
if (!filteredResults.some( s => s.resultIndex == this.selectedChip && s.aboveThreshold && s.isSearched && !s.isDeleted)) {
this.selectChip(this.selectedChip)
}
return filteredResults
},
numResults () {
@@ -398,46 +258,28 @@
},
methods: {
chipGradient (confVal) {
return `--chip-media-background: hsl(${confVal / 100 * 120}deg 100% 50%)`
let confFactor = confVal / 100
return `--chip-media-gradient: conic-gradient(from ${270 - (confFactor * 360 / 2)}deg, hsl(${confFactor * 120}deg, 100%, 50%) ${confFactor}turn, hsl(${confFactor * 120}deg, 50%, 66%) ${confFactor}turn)`
},
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.timeout = 10000
xhr.ontimeout = this.remoteTimeout
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.onload = function () {
self.detecting = false
if (this.status !== 200) {
console.log(xhr.response)
const errorResponse = JSON.parse(xhr.response)
f7.dialog.alert(`ALVINN has encountered an error: ${errorResponse.error}`)
return;
}
self.resultData = JSON.parse(xhr.response)
self.uploadDirty = true
}
var doodsData = {
"detector_name": this.detectorName,
"detect": {
"*": 1
},
"data": this.imageView.src.split(',')[1]
}
xhr.send(JSON.stringify(doodsData))
} else {
//TODO
f7.dialog.alert('Using built-in model')
async setData () {
if (this.reloadModel) {
await this.loadModel(this.modelLocation)
this.reloadModel = false
}
if (this.serverSettings && this.serverSettings.use) {
this.remoteDetect()
} else {
this.localDetect(this.imageView).then(dets => {
this.detecting = false
this.resultData = dets
this.uploadDirty = true
}).catch((e) => {
console.log(e.message)
this.detecting = false
this.resultData = {}
f7.dialog.alert(`ALVINN structure finding error: ${e.message}`)
})
}
},
remoteTimeout () {
this.detecting = false
f7.dialog.alert('No connection to remote ALVINN instance. Please check app settings.')
},
selectAll (ev) {
if (ev.target.checked) {
@@ -446,18 +288,43 @@
this.detectorLabels.forEach( s => s.detect = false )
}
},
selectImage (mode) {
async selectImage (mode) {
this.imageLoadMode = mode
if (mode == "camera") {
this.$refs.image_chooser.setAttribute("capture","environment")
} else {
this.$refs.image_chooser.removeAttribute("capture")
}
if (this.isCordova && mode == "camera") {
navigator.camera.getPicture(this.getImage, this.onFail, { quality: 50, destinationType: Camera.DestinationType.DATA_URL, correctOrientation: true });
} else {
var loadResult = this.$refs.image_chooser.click()
navigator.camera.getPicture(this.getImage, this.onFail, { quality: 50, destinationType: Camera.DestinationType.DATA_URL, correctOrientation: true });
return
}
if (mode == "camera") {
this.videoAvailable = await this.openCamera(this.$refs.image_container)
if (this.videoAvailable) {
this.imageLoaded = false
this.imageView = null
this.$refs.image_cvs.style['background-image'] = 'none'
this.resultData = {}
var trackDetails = this.cameraStream.getVideoTracks()[0].getSettings()
var vidElement = this.$refs.vid_viewer
vidElement.width = trackDetails.width
vidElement.height = trackDetails.height
if (!this.otherSettings.disableVideo) {
this.videoFrameDetect(vidElement)
}
return
}
}
if (mode == 'sample') {
f7.dialog.create({
title: 'Sample images',
buttons: [
{text: 'Sample 1', close: true, onClick: () => this.getImage('sample1'), cssClass: 'avn-dialog-button'},
{text: 'Sample 2', close: true, onClick: () => this.getImage('sample2'), cssClass: 'avn-dialog-button'},
{text: 'Sample 3', close: true, onClick: () => this.getImage('sample3'), cssClass: 'avn-dialog-button'},
{text: 'Cancel', close: true, color: 'red', cssClass: 'avn-dialog-button'}
],
verticalButtons: true
}).open()
return
}
this.$refs.image_chooser.click()
},
onFail (message) {
alert(`Camera fail: ${message}`)
@@ -504,38 +371,58 @@
return [imCanvas, imageCtx]
},
getImage (searchImage) {
let loadImage =new Promise(resolve => {
if (this.isCordova && this.imageLoadMode == "camera") {
let loadImage = new Promise(resolve => {
if (this.videoAvailable) {
this.closeCamera()
this.detecting = true
this.reloadModel = true
resolve(searchImage)
} else if (this.isCordova && this.imageLoadMode == "camera") {
this.detecting = true
resolve('data:image/jpg;base64,' + searchImage)
} else {
const searchImage = this.$refs.image_chooser.files[0]
var reader = new FileReader()
reader.addEventListener("loadend", () => {
this.detecting = true
resolve(reader.result)
}
var reader = new FileReader()
reader.addEventListener("load", () => {
this.detecting = true
resolve(reader.result)
})
if (this.imageLoadMode == 'sample') {
fetch(`${this.isCordova ? 'https://localhost' : '.'}/samples/${this.detectorName}-${searchImage}.jpeg`).then( resp => {
return resp.blob()
}).then(respBlob => {
reader.readAsDataURL(respBlob)
}).catch((e) => {
console.log(e.message)
f7.dialog.alert(`Error loading image: ${e.message}`)
})
reader.readAsDataURL(searchImage)
} else {
const fileImage = this.$refs.image_chooser.files[0]
this.$refs.image_chooser.value=[]
reader.readAsDataURL(fileImage)
}
})
loadImage.then((imgData) => {
this.imageLoaded = true
this.resultData = {}
this.selectedChip = -1
this.imageView = new Image()
this.imageView.src = imgData
return(this.imageView.decode())
}).then( () => {
const [imCanvas, _] = this.resetView()
imCanvas.style['background-image'] = `url(${this.imageView.src})`
this.setData()
/******
* setTimeout is not a good solution, but it's the only way
* I can find to not cut off drawing of the canvas background
******/
setTimeout(() => {
this.setData()
}, 1)
}).catch((e) => {
console.log(e.message)
f7.dialog.alert(`Error loading image: ${e.message}`)
})
},
videoStream() {
//TODO
return null
},
async submitData () {
var uploadData = this.showResults
.filter( d => { return d.aboveThreshold && d.isSearched && !d.isDeleted })
@@ -560,7 +447,7 @@
this.selectChip(findBox >= 0 ? this.resultData.detections[findBox].resultIndex : this.selectedChip)
},
box2cvs(boxInput) {
if (!boxInput) return []
if (!boxInput || boxInput.length == 0) return []
const boxList = boxInput.length ? boxInput : [boxInput]
const [imCanvas, imageCtx] = this.resetView()
var imgWidth
@@ -583,6 +470,17 @@
}
})
return cvsCoords
},
toggleFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen().then( () => {
this.isFullscreen = false
})
} else {
app.requestFullscreen().then( () => {
this.isFullscreen = true
})
}
}
}
}

View File

@@ -0,0 +1,221 @@
import * as tf from '@tensorflow/tfjs'
import { f7 } from 'framework7-vue'
var model = null
export default {
methods: {
async loadModel(weights, preload) {
if (model && model.modelURL == weights) {
return model
} else if (model) {
model.dispose()
}
model = await tf.loadGraphModel(weights)
const [modelWidth, modelHeight] = model.inputs[0].shape.slice(1, 3)
/*****************
* If preloading then run model
* once on fake data to preload
* weights for a faster response
*****************/
if (preload) {
const dummyT = tf.ones([1,modelWidth,modelHeight,3])
model.predict(dummyT)
}
return model
},
async localDetect(imageData) {
console.time('pre-process')
const [modelWidth, modelHeight] = model.inputs[0].shape.slice(1, 3)
const input = tf.tidy(() => {
return tf.image.resizeBilinear(tf.browser.fromPixels(imageData), [modelWidth, modelHeight]).div(255.0).expandDims(0)
})
console.timeEnd('pre-process')
console.time('run prediction')
const res = model.predict(input)
const rawRes = tf.transpose(res,[0,2,1]).arraySync()[0]
console.timeEnd('run prediction')
console.time('post-process')
const outputSize = res.shape[1]
let rawBoxes = []
let rawScores = []
for (var i = 0; i < rawRes.length; i++) {
var getScores = rawRes[i].slice(4)
if (getScores.every( s => s < .05)) { continue }
var getBox = rawRes[i].slice(0,4)
var boxCalc = [
(getBox[0] - (getBox[2] / 2)) / modelWidth,
(getBox[1] - (getBox[3] / 2)) / modelHeight,
(getBox[0] + (getBox[2] / 2)) / modelWidth,
(getBox[1] + (getBox[3] / 2)) / modelHeight,
]
rawBoxes.push(boxCalc)
rawScores.push(getScores)
}
if (rawBoxes.length > 0) {
const tBoxes = tf.tensor2d(rawBoxes)
let tScores = null
let structureScores = null
let boxes_data = []
let scores_data = []
let classes_data = []
for (var c = 0; c < outputSize - 4; c++) {
structureScores = rawScores.map(x => x[c])
tScores = tf.tensor1d(structureScores)
var validBoxes = await tf.image.nonMaxSuppressionAsync(tBoxes,tScores,10,0.5,.05)
validBoxes = validBoxes.dataSync()
if (validBoxes) {
boxes_data.push(...rawBoxes.filter( (_, idx) => validBoxes.includes(idx)))
var outputScores = structureScores.filter( (_, idx) => validBoxes.includes(idx))
scores_data.push(...outputScores)
classes_data.push(...outputScores.fill(c))
}
}
tf.dispose(tBoxes)
tf.dispose(tScores)
const valid_detections_data = classes_data.length
var output = {
detections: []
}
for (var i =0; i < valid_detections_data; i++) {
var [dLeft, dTop, dRight, dBottom] = boxes_data[i]
output.detections.push({
"top": dTop,
"left": dLeft,
"bottom": dBottom,
"right": dRight,
"label": this.detectorLabels[classes_data[i]].name,
"confidence": scores_data[i] * 100
})
}
}
tf.dispose(res)
tf.dispose(input)
console.timeEnd('post-process')
return output || { detections: [] }
},
getRemoteLabels() {
var self = this
var modelURL = `http://${this.serverSettings.address}:${this.serverSettings.port}/detectors`
var xhr = new XMLHttpRequest()
xhr.open("GET", modelURL)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.timeout = 10000
xhr.ontimeout = this.remoteTimeout
xhr.onload = function () {
if (this.status !== 200) {
console.log(xhr.response)
const errorResponse = JSON.parse(xhr.response)
f7.dialog.alert(`ALVINN has encountered an error: ${errorResponse.error}`)
return
}
var detectors = JSON.parse(xhr.response).detectors
var findLabel = detectors
.find( d => { return d.name == self.detectorName } )?.labels
.filter( l => { return l != "" } ).sort()
.map( l => { return {'name': l, 'detect': true} } )
self.detectorLabels = findLabel || []
}
xhr.onerror = function (e) {
f7.dialog.alert('ALVINN has encountered an unknown server error')
return
}
xhr.send()
},
remoteDetect() {
var self = this
var modelURL = `http://${this.serverSettings.address}:${this.serverSettings.port}/detect`
var xhr = new XMLHttpRequest()
xhr.open("POST", modelURL)
xhr.timeout = 10000
xhr.ontimeout = this.remoteTimeout
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.onload = function () {
self.detecting = false
if (this.status !== 200) {
console.log(xhr.response)
const errorResponse = JSON.parse(xhr.response)
f7.dialog.alert(`ALVINN has encountered an error: ${errorResponse.error}`)
return;
}
self.resultData = JSON.parse(xhr.response)
self.uploadDirty = true
}
var doodsData = {
"detector_name": this.detectorName,
"detect": {
"*": 1
},
"data": this.imageView.src.split(',')[1]
}
xhr.send(JSON.stringify(doodsData))
},
remoteTimeout () {
this.detecting = false
f7.dialog.alert('No connection to remote ALVINN instance. Please check app settings.')
},
async videoFrameDetect (vidData) {
await this.loadModel(this.miniLocation)
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.width / vidData.height
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();
}
}
}
}

89
src/pages/help.vue Normal file
View File

@@ -0,0 +1,89 @@
<template>
<f7-page name="help">
<f7-navbar title="Help" back-link="Back"></f7-navbar>
<f7-block>
<h2>Quick Start</h2>
<ol>
<li>Select the region of the body you want to identify structures from.</li>
<li>Load an image:
<ul>
<li>Click on the camera icon <SvgIcon icon="photo_camera" class="list-svg"/> to take a new picture.
<ul>
<li>ALVINN will highlight areas with potential structures as you aim the camera.</li>
<li>Press <span class="cap-button">Capture</span> to use the current camera view.</li>
</ul>
</li>
<li>Click on the image file icon <SvgIcon icon="photo_library" class="list-svg"/> to load a picture from the device storage.</li>
<li>If demo mode is turned on, you can click on the marked image icon <SvgIcon icon="photo_sample" class="list-svg"/> to load an ALVINN sample image.</li>
</ul>
</li>
<li>When the picture is captured or loaded, any identifiable structures will be listed as tags below the image:
<f7-chip text="Structure name" media=" " class="demo-chip"/>
<ul>
<li>Click on each tag to see the structure highlighted in the image.</li>
<li>Tag color and proportion filled indicate ALVINN's level of confidence in the identification.</li>
<li>If there are potential structures that do not satisfy the current detection threshold, a badge on the detection menu icon <SvgIcon icon="visibility" class="list-svg"/> will indicate the number of un-displayed structures.</li>
</ul>
</li>
</ol>
<h2>Advanced Features</h2>
<h3>Detection Parameters</h3>
<p>
After an image has been loaded and structure detection has been performed, the detection parameters can be adjusted using the detection menu icon <SvgIcon icon="visibility" class="list-svg"/>.
This button will make three tools available:
</p>
<ol>
<li>Confidence slider: You can use the slider to change the confidence threshold for identifying structures. The default threshold is 50% confidence.</li>
<li>Refresh detections <SvgIcon icon="refresh_search" class="list-svg"/>: If there has been a permanent change to the structures detections, such as deleting a tag, the detection list can be reset to its original state.</li>
<li>Structure list <SvgIcon icon="check_list" class="list-svg"/>: you can view a list of all the structures available for detection in that region and select/deselect individual structures for detection.</li>
</ol>
<h3>Submitting Images</h3>
<p>
If all of the detection tags that are currently visible have been viewed, then the cloud upload button <SvgIcon icon="cloud_upload" class="list-svg"/> 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.
</p>
</f7-block>
</f7-page>
</template>
<style>
li, p {
font-size: 16px;
}
.list-svg {
width: 2em;
position:relative;
top: .5em;
}
.cap-button {
background-color: var(--f7-theme-color);
color: white;
border-radius: 4px;
padding-left: 8px;
padding-right: 8px;
}
.demo-chip {
height: 24px;
padding-left: 8px;
--f7-chip-border-radius: 12px;
--f7-chip-media-size: 24px;
--f7-chip-font-weight: normal;
}
.demo-chip .chip-media {
background: conic-gradient(from 135deg, #00cc00 .75turn, #00cc0088 .75turn) !important;
}
</style>
<script>
import SvgIcon from '../components/svg-icon.vue'
export default {
components: {
SvgIcon
}
}
</script>

View File

@@ -3,14 +3,20 @@
<!-- Top Navbar -->
<f7-navbar>
<f7-nav-left>
<f7-link icon-ios="f7:menu" icon-md="material:menu" panel-open="left"></f7-link>
<f7-link icon-ios="f7:bars" icon-md="f7:bars" panel-open="left"></f7-link>
</f7-nav-left>
<f7-nav-title sliding>A.L.V.I.N.N.</f7-nav-title>
</f7-navbar>
<!-- Page content-->
<div style="display: grid; grid-template-columns: 100%; grid-template-rows: min-content min-content min-content 1fr; align-content: stretch; gap: 15px; min-height: 0px; max-height: calc(100vh - (var(--f7-navbar-height) + var(--f7-safe-area-top))); height: calc(100vh - (var(--f7-navbar-height) + var(--f7-safe-area-top)));">
<h2 style="text-align: center; padding-left: 30px; padding-right: 30px;">Anatomy Lab Visual Identification Neural Network</h2>
<h4 style="text-align: center; margin: 0;">Veterinary Anatomy Edition</h4>
<h4 style="text-align: center; margin: 0;">
Veterinary Anatomy Edition
<f7-link @click="alphaWarn">
ALPHA RELEASE
<f7-badge style="margin-left: 3px;" v-if="!alphaCheck" color="red">!</f7-badge>
</f7-link>
</h4>
<p style="text-align: center; margin: 0;">Select a region to begin.</p>
<div class="region-grid">
<f7-button :class="`region-button thorax${isAgreed && getRegions.includes('thorax') ? '' : ' disabled'}`" :href="isAgreed && getRegions.includes('thorax') && '/detect/thorax/'">
@@ -89,13 +95,24 @@
<script>
import RegionIcon from '../components/region-icon.vue'
import store from '../js/store'
import { f7 } from 'framework7-vue'
export default {
components: {
RegionIcon
},
data () {
return {
alphaCheck: false
}
},
setup() {
return store()
},
methods: {
alphaWarn () {
f7.dialog.alert('This is an alpha release. Expect bugs and use the contact page to report any bugs you encounter.', 'Warning', () => { this.alphaCheck = true })
}
}
}
</script>

View File

@@ -1,15 +1,39 @@
<template>
<f7-page name="settings">
<!-- Top Navbar -->
<f7-navbar :sliding="false" back-link="Back">
<f7-nav-title sliding>Settings</f7-nav-title>
</f7-navbar>
<f7-block style="display: flex; flex-direction: column; align-items: center;">
<div style="display: flex; flex-direction: column; align-items: center;">
<f7-block-title medium>Server Settings</f7-block-title>
<f7-page name="settings">
<!-- Top Navbar -->
<f7-navbar :sliding="false">
<f7-nav-left>
<f7-link class="link icon-only ripple-inset" @click="confirmBack">
<f7-icon class="icon-back"></f7-icon>
</f7-link>
</f7-nav-left>
<f7-nav-title sliding>Settings</f7-nav-title>
</f7-navbar>
<f7-block style="display: flex; flex-direction: column; align-items: center;">
<div style="display: flex; flex-direction: column; align-items: center;">
<f7-block-title medium>Dark Mode</f7-block-title>
<f7-list style="width: 100%;">
<f7-list-item title="Auto" :checked="themeSettings.darkMode == 'auto'" radio name="darkmode" radio-icon="end" @change="setDarkMode('auto')" ></f7-list-item>
<f7-list-item title="Light" :checked="themeSettings.darkMode.toString() == 'false'" radio name="darkmode" radio-icon="end" @change="setDarkMode(false)" ></f7-list-item>
<f7-list-item title="Dark" :checked="themeSettings.darkMode.toString() == 'true'" radio name="darkmode" radio-icon="end" @change="setDarkMode(true)" ></f7-list-item>
</f7-list>
<f7-block-title @click="toggleSettingsView" medium><f7-icon :f7="showAdvanced ? 'chevron_down' : 'chevron_right'" /> Advanced Settings </f7-block-title>
<div ref="advancedSettings" class="settings-container">
<div style="display:flex; justify-content:space-between; width: 100%; margin-bottom: 10px;">
<span style="margin-left: 16px;">Use mini ALVINN<f7-icon size="16" style="padding-left: 5px;" f7="question_diamond_fill" tooltip="faster, less accurate: recommended for slower devices" /></span>
<f7-toggle v-model:checked="otherSettings.mini" style="margin-right: 16px;" @change="setDirty()" />
</div>
<div style="display:flex; justify-content:space-between; width: 100%; margin-bottom: 10px;">
<span style="margin-left: 16px;">Enable demo mode</span>
<f7-toggle v-model:checked="otherSettings.demo" style="margin-right: 16px;" @change="setDirty()" />
</div>
<div style="display:flex; justify-content:space-between; width: 100%; margin-bottom: 10px;">
<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;" />
<f7-toggle v-model:checked="serverSettings.use" style="margin-right: 16px;" @change="setDirty()" />
</div>
<f7-list>
<f7-list-input :disabled="!serverSettings.use" v-model:value="serverSettings.address" label="Server address" type="text" placeholder="127.0.0.1" />
@@ -20,94 +44,124 @@
<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>
<f7-block-title medium>Dark Mode</f7-block-title>
<f7-list style="width: 100%;">
<f7-list-item title="Auto" :checked="themeSettings.darkMode == 'auto'" radio name="darkmode" radio-icon="end" @change="setDarkMode('auto')" ></f7-list-item>
<f7-list-item title="Light" :checked="themeSettings.darkMode.toString() == 'false'" radio name="darkmode" radio-icon="end" @change="setDarkMode(false)" ></f7-list-item>
<f7-list-item title="Dark" :checked="themeSettings.darkMode.toString() == 'true'" radio name="darkmode" radio-icon="end" @change="setDarkMode(true)" ></f7-list-item>
</f7-list>
<f7-button @click="saveAllSettings" >Save</f7-button>
</div>
</f7-block>
</f7-page>
<f7-button fill @click="saveAllSettings">SAVE</f7-button>
</div>
</f7-block>
</f7-page>
</template>
<script>
import { f7 } from 'framework7-vue'
<style>
.settings-container {
max-height: 0px;
overflow: hidden;
transition: max-height 0.2s ease-out;
}
</style>
export default {
data () {
return {
serverSettings: {
use: false,
address: '10.170.64.22',
port: '9001',
previous: {}
},
themeSettings: {
darkMode: 'auto'
}
}
<script>
import { f7 } from 'framework7-vue'
export default {
data () {
return {
showAdvanced: false,
isDirty: false,
otherSettings: {
mini: false
},
computed: {
otherIp () {
let filteredIps = {}
for (var oldIp in this.serverSettings.previous) {
if (oldIp != this.serverSettings.address) {
filteredIps[oldIp] = this.serverSettings.previous[oldIp]
}
}
return filteredIps
}
serverSettings: {
use: false,
address: '10.170.64.22',
port: '9001',
previous: {}
},
created () {
var loadServerSettings = localStorage.getItem('serverSettings')
if (loadServerSettings) this.serverSettings = JSON.parse(loadServerSettings)
if (!this.serverSettings.previous) this.serverSettings.previous = {}
var loadThemeSettings = localStorage.getItem('themeSettings')
if (loadThemeSettings) this.themeSettings = JSON.parse(loadThemeSettings)
},
methods: {
saveAllSettings () {
let saveSetting = new Promise(
(saved,failed) => {
try {
if (this.serverSettings.use) {
this.serverSettings.previous[this.serverSettings.address] = this.serverSettings.port
}
localStorage.setItem('serverSettings',JSON.stringify(this.serverSettings))
localStorage.setItem('themeSettings',JSON.stringify(this.themeSettings))
saved()
} catch {
failed()
}
}
)
saveSetting.then(
() => {
var toast = f7.toast.create({
text: 'Settings saved',
closeTimeout: 2000
})
toast.open()
},
() => {
var toast = f7.toast.create({
text: 'ERROR: No settings saved',
closeTimeout: 2000
})
toast.open()
}
)
},
setDarkMode (mode) {
this.themeSettings.darkMode = mode
f7.setDarkMode(mode)
},
setServerProps (add, port) {
this.serverSettings.address = add
this.serverSettings.port = port
}
themeSettings: {
darkMode: 'auto'
}
}
},
computed: {
otherIp () {
let filteredIps = {}
for (var oldIp in this.serverSettings.previous) {
if (oldIp != this.serverSettings.address) {
filteredIps[oldIp] = this.serverSettings.previous[oldIp]
}
}
return filteredIps
}
},
created () {
var loadServerSettings = localStorage.getItem('serverSettings')
if (loadServerSettings) this.serverSettings = JSON.parse(loadServerSettings)
if (!this.serverSettings.previous) this.serverSettings.previous = {}
var loadThemeSettings = localStorage.getItem('themeSettings')
if (loadThemeSettings) this.themeSettings = JSON.parse(loadThemeSettings)
var loadOtherSettings = localStorage.getItem('otherSettings')
if (loadOtherSettings) this.otherSettings = JSON.parse(loadOtherSettings)
},
methods: {
saveAllSettings () {
let saveSetting = new Promise(
(saved,failed) => {
try {
if (this.serverSettings.use) {
this.serverSettings.previous[this.serverSettings.address] = this.serverSettings.port
}
localStorage.setItem('serverSettings',JSON.stringify(this.serverSettings))
localStorage.setItem('themeSettings',JSON.stringify(this.themeSettings))
localStorage.setItem('otherSettings',JSON.stringify(this.otherSettings))
saved()
} catch {
failed()
}
}
)
saveSetting.then(
() => {
var toast = f7.toast.create({
text: 'Settings saved',
closeTimeout: 2000
})
toast.open()
this.isDirty = false;
},
() => {
var toast = f7.toast.create({
text: 'ERROR: No settings saved',
closeTimeout: 2000
})
toast.open()
}
)
},
setDirty () {
this.isDirty = true
},
setDarkMode (mode) {
this.themeSettings.darkMode = mode
f7.setDarkMode(mode)
this.isDirty = true
},
setServerProps (add, port) {
this.serverSettings.address = add
this.serverSettings.port = port
this.isDirty = true
},
toggleSettingsView () {
this.showAdvanced = !this.showAdvanced
this.$refs.advancedSettings.style.maxHeight = `${this.showAdvanced ? this.$refs.advancedSettings.scrollHeight : 0}px`
},
confirmBack () {
if (this.isDirty) {
f7.dialog.confirm('If you leave this page you will loose unsaved changes to your settings.', () => {
f7.views.main.router.back()
})
} else {
f7.views.main.router.back()
}
}
}
}
</script>

74
src/pages/specs.vue Normal file
View File

@@ -0,0 +1,74 @@
<template>
<f7-page name="specs">
<f7-navbar :sliding="false" back-link="Back">
<f7-nav-title sliding>Tech Specs</f7-nav-title>
</f7-navbar>
<f7-block style="display: flex; flex-direction: column; align-items: stretch;">
<div style="display: flex; flex-direction: column; align-items: center;">
<f7-block-title medium>Details</f7-block-title>
<f7-list>
<f7-list-item title="Version" :after="alvinnVersion"></f7-list-item>
</f7-list>
<f7-block-title medium>Models</f7-block-title>
<f7-list style="width: 100%;">
<f7-list-item :class="otherSettings.mini ? 'unused-model' : ''" title="Thorax" :after="thoraxDetails.version"></f7-list-item>
<f7-list-item title="Thorax-m" :after="miniThoraxDetails.version"></f7-list-item>
<f7-list-item :class="otherSettings.mini ? 'unused-model' : ''" title="Abdomen/Pelvis" :after="abdomenDetails.version"></f7-list-item>
<f7-list-item title="Abd/Pel-m" :after="miniAbdomenDetails.version"></f7-list-item>
<f7-list-item title="Limbs" :after="limbsDetails.version"></f7-list-item>
<f7-list-item title="Head/Neck" :after="headneckDetails.version"></f7-list-item>
</f7-list>
</div>
</f7-block>
</f7-page>
</template>
<style>
.unused-model {
opacity: .75;
}
.unused-model .item-title {
color: var(--f7-list-item-after-text-color)
}
</style>
<script>
import store from '../js/store'
export default {
data () {
return {
thoraxDetails: {},
miniThoraxDetails: {},
abdomenDetails: {},
miniAbdomenDetails: {},
limbsDetails: { "version": "N/A" },
headneckDetails: { "version": "N/A" },
alvinnVersion: store().getVersion,
isCordova: !!window.cordova,
otherSettings: {}
}
},
setup() {
return store()
},
created () {
var loadOtherSettings = localStorage.getItem('otherSettings')
if (loadOtherSettings) this.otherSettings = JSON.parse(loadOtherSettings)
fetch(`${this.isCordova ? 'https://localhost' : '.'}/models/thorax/descript.json`)
.then((mod) => { return mod.json() })
.then((desc) => { this.thoraxDetails = desc })
fetch(`${this.isCordova ? 'https://localhost' : '.'}/models/thorax-mini/descript.json`)
.then((mod) => { return mod.json() })
.then((desc) => { this.miniThoraxDetails = desc })
fetch(`${this.isCordova ? 'https://localhost' : '.'}/models/abdomen/descript.json`)
.then((mod) => { return mod.json() })
.then((desc) => { this.abdomenDetails = desc })
fetch(`${this.isCordova ? 'https://localhost' : '.'}/models/abdomen-mini/descript.json`)
.then((mod) => { return mod.json() })
.then((desc) => { this.miniAbdomenDetails = desc })
},
methods: {
}
}
</script>