From 619211e827bb049b3e8862256e5103a40c6baff0 Mon Sep 17 00:00:00 2001 From: Justin Georgi Date: Tue, 19 Dec 2023 09:52:18 -0700 Subject: [PATCH] Allow images and detection upload for future data (#56) Closes: #48 This PR should allow the user to upload an image that has been taken with the camera and the associated detection information to a file server. These file can then be used to add data to ALVINN's models. Current upload destination is set to a file drop folder in Georgi Lab Nextcloud, but eventually (maybe) should be changed to some dedicated ALVINN backend. Also, prediction data is still not formatted in any specific way. It is just a JSON array of (top, left, bottom, right, label) objects. Reviewed-on: https://gitea.azgeorgis.net/Georgi_Lab/ALVINN_f7/pulls/56 --- src/components/svg-icon.vue | 14 +++++++- src/pages/detect.vue | 46 ++++++++++++++++++++----- src/pages/submit-mixin.js | 67 +++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/pages/submit-mixin.js diff --git a/src/components/svg-icon.vue b/src/components/svg-icon.vue index b4d2c16..a8f80b9 100644 --- a/src/components/svg-icon.vue +++ b/src/components/svg-icon.vue @@ -6,6 +6,8 @@ + + @@ -16,7 +18,17 @@ icon: { type: String, validator(value) { - return ['image','videocam','visibility','photo_library','no_photography','photo_camera'].includes(value) + const iconList = [ + 'image', + 'videocam', + 'visibility', + 'photo_library', + 'no_photography', + 'photo_camera', + 'cloud_upload', + 'cloud_done' + ] + return iconList.includes(value) } }, fillColor: { diff --git a/src/pages/detect.vue b/src/pages/detect.vue index ae4b5c6..2c2cf87 100644 --- a/src/pages/detect.vue +++ b/src/pages/detect.vue @@ -24,20 +24,20 @@ @delete="deleteChip(result.resultIndex)" :style="chipGradient(result.confidence)" /> - No results. + No results. - + - + - - + + @@ -56,7 +56,6 @@ - {{ isCordova }} @@ -84,6 +83,9 @@ + + + @@ -221,7 +223,10 @@ import RegionIcon from '../components/region-icon.vue' import SvgIcon from '../components/svg-icon.vue' + import submitMixin from './submit-mixin' + export default { + mixins: [submitMixin], props: { f7route: Object, }, @@ -245,7 +250,9 @@ serverSettings: {}, debugOn: false, debugText: ['Variables loaded'], - isCordova: !!window.cordova + isCordova: !!window.cordova, + uploadUid: null, + uploadDirty: false } }, created () { @@ -301,6 +308,7 @@ }, showResults () { var filteredResults = this.resultData.detections + if (!filteredResults) return [] var allSelect = this.detectorLabels.every( s => { return s.detect } ) var selectedLabels = this.detectorLabels .filter( l => { return l.detect }) @@ -311,6 +319,14 @@ filteredResults[i].isSearched = allSelect || selectedLabels.includes(d.label) }) return filteredResults + }, + numResults () { + return this.showResults.filter( r => { return r.aboveThreshold && r.isSearched && !r.isDeleted }).length + }, + viewedAll () { + return this.resultData.detections + .filter( s => { return s.confidence >= this.detectorLevel}) + .every( s => { return s.beenViewed }) } }, methods: { @@ -334,6 +350,7 @@ return; } self.resultData = JSON.parse(xhr.response) + self.uploadDirty = true } var doodsData = { @@ -399,11 +416,13 @@ box.style.top = `${(img.offsetHeight - imgHeight) / 2 + this.resultData.detections[iChip].top * imgHeight}px` box.style.width = `${(Math.min(this.resultData.detections[iChip].right, 1) - Math.max(this.resultData.detections[iChip].left, 0)) * imgWidth}px` box.style.height = `${(Math.min(this.resultData.detections[iChip].bottom, 1) - Math.max(this.resultData.detections[iChip].top, 0)) * imgHeight}px` + this.resultData.detections[iChip].beenViewed = true }, deleteChip ( iChip ) { f7.dialog.confirm(`${this.resultData.detections[iChip].label} is identified with ${this.resultData.detections[iChip].confidence.toFixed(1)}% confidence. Are you sure you want to delete it?`, () => { this.resultData.detections.splice(iChip, 1) this.resetView() + this.uploadDirty = true }); }, getImage (searchImage) { @@ -419,7 +438,7 @@ } resolve() }) - loadImage.then((imageData) => { + loadImage.then(() => { this.imageLoaded = true this.resultData = {} this.resetView() @@ -432,6 +451,17 @@ this.selectedChip = -1 const box = this.$refs.structure_box box.style.display = 'none' + }, + videoStream() { + //TODO + return null + }, + async submitData () { + var uploadData = this.showResults + .filter( d => { return d.aboveThreshold && d.isSearched && !d.isDeleted }) + .map( r => { return {"top": r.top, "left": r.left, "bottom": r.bottom, "right": r.right, "label": r.label}}) + this.uploadUid = await this.uploadData(this.imageView.split(',')[1],uploadData,this.uploadUid) + if (this.uploadUid) { this.uploadDirty = false } } } } diff --git a/src/pages/submit-mixin.js b/src/pages/submit-mixin.js new file mode 100644 index 0000000..bc46fd7 --- /dev/null +++ b/src/pages/submit-mixin.js @@ -0,0 +1,67 @@ +import { f7 } from 'framework7-vue' + +export default { + methods: { + newUid (length) { + const uidLength = length || 16 + const uidChars = 'abcdefghijklmnopqrstuvwxyz0123456789' + var uid = [] + for (var i = 0; i < uidLength; i++) { + uid.push(uidChars.charAt(Math.floor(Math.random() * ((i < 4) ? 26 : 36)))) + } + return uid.join('') + }, + uploadData (imagePayload, classPayload, prevUid) { + let uploadImage = new Promise (resolve => { + const dataUid = prevUid || this.newUid(16) + var byteChars = window.atob(imagePayload) + var byteArrays = [] + var len = byteChars.length + + for (var offset = 0; offset < len; offset += 1024) { + var slice = byteChars.slice(offset, offset + 1024) + var byteNumbers = new Array(slice.length) + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i) + } + + var byteArray = new Uint8Array(byteNumbers) + byteArrays.push(byteArray) + } + var imageBlob = new Blob(byteArrays, {type: 'image/jpeg'}) + + var xhrJpg = new XMLHttpRequest() + var uploadUrl = `https://nextcloud.azgeorgis.net/public.php/webdav/${dataUid}.jpeg` + xhrJpg.open("PUT", uploadUrl) + xhrJpg.setRequestHeader('Content-Type', 'image/jpeg') + xhrJpg.setRequestHeader('X-Method-Override', 'PUT') + xhrJpg.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + xhrJpg.setRequestHeader("Authorization", "Basic " + btoa("LKBm3H6JdSaywyg:")) + xhrJpg.send(imageBlob) + + var xhrTxt = new XMLHttpRequest() + var uploadUrl = `https://nextcloud.azgeorgis.net/public.php/webdav/${dataUid}.txt` + xhrTxt.open("PUT", uploadUrl) + xhrTxt.setRequestHeader('Content-Type', 'text/plain') + xhrTxt.setRequestHeader('X-Method-Override', 'PUT') + xhrTxt.setRequestHeader('X-Requested-With', 'XMLHttpRequest') + xhrTxt.setRequestHeader("Authorization", "Basic " + btoa("LKBm3H6JdSaywyg:")) + xhrTxt.send(JSON.stringify(classPayload)) + + resolve(dataUid) + }) + return uploadImage.then((newUid) => { + var toast = f7.toast.create({ + text: 'Detections Uploaded: thank you.', + closeTimeout: 2000 + }) + toast.open() + return newUid + }).catch((e) => { + console.log(e.message) + f7.dialog.alert(`Error uploading image: ${e.message}`) + return null + }) + } + } +} \ No newline at end of file