Allow images and detection upload for future data #56

Merged
jgeorgi merged 4 commits from msv-submit-data into main 2023-12-19 16:52:18 +00:00
3 changed files with 118 additions and 9 deletions

View File

@@ -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 == '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 == '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 == '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> </svg>
</template> </template>
@@ -16,7 +18,17 @@
icon: { icon: {
type: String, type: String,
validator(value) { 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: { fillColor: {

View File

@@ -24,20 +24,20 @@
@delete="deleteChip(result.resultIndex)" @delete="deleteChip(result.resultIndex)"
:style="chipGradient(result.confidence)" :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> </div>
<f7-segmented class="image-menu" raised> <f7-segmented class="image-menu" raised>
<f7-button popover-open="#region-popover"> <f7-button popover-open="#region-popover">
<RegionIcon :region="activeRegion" /> <RegionIcon :region="activeRegion" />
</f7-button> </f7-button>
<f7-button popover-open="#capture-popover"> <f7-button popover-open="#capture-popover">
<SvgIcon icon="image" fill-color="var(--avn-theme-color)"/> <SvgIcon icon="image"/>
</f7-button> </f7-button>
<f7-button @click="setData" :class="(imageLoaded) ? '' : 'disabled'"> <f7-button @click="setData" :class="(imageLoaded) ? '' : 'disabled'">
<SvgIcon icon="visibility" fill-color="var(--avn-theme-color)"/> <SvgIcon icon="visibility"/>
</f7-button> </f7-button>
<f7-button class="disabled" @click="setData"> <f7-button :class="(numResults && uploadDirty && viewedAll) ? '' : 'disabled'" @click="submitData">
<SvgIcon icon="videocam" fill-color="var(--avn-theme-color)"/> <SvgIcon :icon="(uploadUid) ? 'cloud_done' : 'cloud_upload'"/>
</f7-button> </f7-button>
</f7-segmented> </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/*" capture="environment" style="display: none;"/>
@@ -56,7 +56,6 @@
</f7-list> </f7-list>
</f7-accordion-content> </f7-accordion-content>
</f7-list-item> </f7-list-item>
<f7-list-item title="Cordova">{{ isCordova }}</f7-list-item>
<f7-list-item title="Turn on debugging"> <f7-list-item title="Turn on debugging">
<f7-toggle v-model:checked="debugOn" style="margin-right: 16px;" /> <f7-toggle v-model:checked="debugOn" style="margin-right: 16px;" />
</f7-list-item> </f7-list-item>
@@ -84,6 +83,9 @@
<f7-popover id="capture-popover"> <f7-popover id="capture-popover">
<f7-segmented raised style="flex-wrap: wrap; flex-direction: column;"> <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')"> <f7-button style="height: auto; width: auto;" popover-close="#capture-popover" @click="selectImage('camera')">
<SvgIcon icon="photo_camera" /> <SvgIcon icon="photo_camera" />
</f7-button> </f7-button>
@@ -221,7 +223,10 @@
import RegionIcon from '../components/region-icon.vue' import RegionIcon from '../components/region-icon.vue'
import SvgIcon from '../components/svg-icon.vue' import SvgIcon from '../components/svg-icon.vue'
import submitMixin from './submit-mixin'
export default { export default {
mixins: [submitMixin],
props: { props: {
f7route: Object, f7route: Object,
}, },
@@ -245,7 +250,9 @@
serverSettings: {}, serverSettings: {},
debugOn: false, debugOn: false,
debugText: ['Variables loaded'], debugText: ['Variables loaded'],
isCordova: !!window.cordova isCordova: !!window.cordova,
uploadUid: null,
uploadDirty: false
} }
}, },
created () { created () {
@@ -301,6 +308,7 @@
}, },
showResults () { showResults () {
var filteredResults = this.resultData.detections var filteredResults = this.resultData.detections
if (!filteredResults) return []
var allSelect = this.detectorLabels.every( s => { return s.detect } ) var allSelect = this.detectorLabels.every( s => { return s.detect } )
var selectedLabels = this.detectorLabels var selectedLabels = this.detectorLabels
.filter( l => { return l.detect }) .filter( l => { return l.detect })
@@ -311,6 +319,14 @@
filteredResults[i].isSearched = allSelect || selectedLabels.includes(d.label) filteredResults[i].isSearched = allSelect || selectedLabels.includes(d.label)
}) })
return filteredResults 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: { methods: {
@@ -334,6 +350,7 @@
return; return;
} }
self.resultData = JSON.parse(xhr.response) self.resultData = JSON.parse(xhr.response)
self.uploadDirty = true
} }
var doodsData = { var doodsData = {
@@ -399,11 +416,13 @@
box.style.top = `${(img.offsetHeight - imgHeight) / 2 + this.resultData.detections[iChip].top * imgHeight}px` 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.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` 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 ) { 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?`, () => { 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.resultData.detections.splice(iChip, 1)
this.resetView() this.resetView()
this.uploadDirty = true
}); });
}, },
getImage (searchImage) { getImage (searchImage) {
@@ -419,7 +438,7 @@
} }
resolve() resolve()
}) })
loadImage.then((imageData) => { loadImage.then(() => {
this.imageLoaded = true this.imageLoaded = true
this.resultData = {} this.resultData = {}
this.resetView() this.resetView()
@@ -432,6 +451,17 @@
this.selectedChip = -1 this.selectedChip = -1
const box = this.$refs.structure_box const box = this.$refs.structure_box
box.style.display = 'none' 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
View 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
})
}
}
}