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: Georgi_Lab/ALVINN_f7#56
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
<path v-else-if="icon == 'photo_library'" d="M360-400h400L622-580l-92 120-62-80-108 140Zm-40 160q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/>
|
||||
<path v-else-if="icon == 'no_photography'" d="m880-195-80-80v-405H638l-73-80H395l-38 42-57-57 60-65h240l74 80h126q33 0 56.5 23.5T880-680v485Zm-720 75q-33 0-56.5-23.5T80-200v-480q0-33 23.5-56.5T160-760h41l80 80H160v480h601l80 80H160Zm466-215q-25 34-62.5 54.5T480-260q-75 0-127.5-52.5T300-440q0-46 20.5-83.5T375-586l58 58q-24 13-38.5 36T380-440q0 42 29 71t71 29q29 0 52-14.5t36-38.5l58 58Zm-18-233q25 24 38.5 57t13.5 71v12q0 6-1 12L456-619q6-1 12-1h12q38 0 71 13.5t57 38.5ZM819-28 27-820l57-57L876-85l-57 57ZM407-440Zm171-57Z"/>
|
||||
<path v-else-if="icon == 'photo_camera'" d="M480-260q75 0 127.5-52.5T660-440q0-75-52.5-127.5T480-620q-75 0-127.5 52.5T300-440q0 75 52.5 127.5T480-260Zm0-80q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM160-120q-33 0-56.5-23.5T80-200v-480q0-33 23.5-56.5T160-760h126l74-80h240l74 80h126q33 0 56.5 23.5T880-680v480q0 33-23.5 56.5T800-120H160Zm0-80h640v-480H638l-73-80H395l-73 80H160v480Zm320-240Z"/>
|
||||
<path v-else-if="icon == 'cloud_upload'" d="M260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H520q-33 0-56.5-23.5T440-240v-206l-64 62-56-56 160-160 160 160-56 56-64-62v206h220q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41h100v80H260Zm220-280Z"/>
|
||||
<path v-else-if="icon == 'cloud_done'" d="m414-280 226-226-58-58-169 169-84-84-57 57 142 142ZM260-160q-91 0-155.5-63T40-377q0-78 47-139t123-78q25-92 100-149t170-57q117 0 198.5 81.5T760-520q69 8 114.5 59.5T920-340q0 75-52.5 127.5T740-160H260Zm0-80h480q42 0 71-29t29-71q0-42-29-71t-71-29h-60v-80q0-83-58.5-141.5T480-720q-83 0-141.5 58.5T280-520h-20q-58 0-99 41t-41 99q0 58 41 99t99 41Zm220-240Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -24,20 +24,20 @@
|
||||
@delete="deleteChip(result.resultIndex)"
|
||||
:style="chipGradient(result.confidence)"
|
||||
/>
|
||||
<span v-if="showResults.filter( r => { return r.aboveThreshold && r.isSearched && !r.isDeleted }).length == 0" style="height: var(--f7-chip-height); font-size: calc(var(--f7-chip-height) - 4px); font-weight: bolder; margin: 2px;">No results.</span>
|
||||
<span v-if="numResults == 0" style="height: var(--f7-chip-height); font-size: calc(var(--f7-chip-height) - 4px); font-weight: bolder; margin: 2px;">No results.</span>
|
||||
</div>
|
||||
<f7-segmented class="image-menu" raised>
|
||||
<f7-button popover-open="#region-popover">
|
||||
<RegionIcon :region="activeRegion" />
|
||||
</f7-button>
|
||||
<f7-button popover-open="#capture-popover">
|
||||
<SvgIcon icon="image" fill-color="var(--avn-theme-color)"/>
|
||||
<SvgIcon icon="image"/>
|
||||
</f7-button>
|
||||
<f7-button @click="setData" :class="(imageLoaded) ? '' : 'disabled'">
|
||||
<SvgIcon icon="visibility" fill-color="var(--avn-theme-color)"/>
|
||||
<SvgIcon icon="visibility"/>
|
||||
</f7-button>
|
||||
<f7-button class="disabled" @click="setData">
|
||||
<SvgIcon icon="videocam" fill-color="var(--avn-theme-color)"/>
|
||||
<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;"/>
|
||||
@@ -56,7 +56,6 @@
|
||||
</f7-list>
|
||||
</f7-accordion-content>
|
||||
</f7-list-item>
|
||||
<f7-list-item title="Cordova">{{ isCordova }}</f7-list-item>
|
||||
<f7-list-item title="Turn on debugging">
|
||||
<f7-toggle v-model:checked="debugOn" style="margin-right: 16px;" />
|
||||
</f7-list-item>
|
||||
@@ -84,6 +83,9 @@
|
||||
|
||||
<f7-popover id="capture-popover">
|
||||
<f7-segmented raised style="flex-wrap: wrap; flex-direction: column;">
|
||||
<f7-button style="height: auto; width: auto;" popover-close="#capture-popover" class="disabled" @click="videoStream">
|
||||
<SvgIcon icon="videocam"/>
|
||||
</f7-button>
|
||||
<f7-button style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('camera')">
|
||||
<SvgIcon icon="photo_camera" />
|
||||
</f7-button>
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
src/pages/submit-mixin.js
Normal file
67
src/pages/submit-mixin.js
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user