Complete UI for arbitrary dectector settings. Required better method for invoking actions.
Error handling was fixed in the process of getting the above correct.
This commit is contained in:
parent
92d15d921b
commit
33413a591a
5 changed files with 102 additions and 25 deletions
|
|
@ -8,7 +8,7 @@ current camera field of view contains sample.
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from pydantic.errors import PydanticUserError
|
from pydantic.errors import PydanticUserError
|
||||||
from scipy.stats import norm
|
from scipy.stats import norm
|
||||||
from labthings_fastapi.thing_description import type_to_dataschema
|
from labthings_fastapi.thing_description import type_to_dataschema
|
||||||
|
|
@ -153,13 +153,18 @@ class ChannelDistributions(BaseModel):
|
||||||
class ColourChannelDetectSettings(BaseModel):
|
class ColourChannelDetectSettings(BaseModel):
|
||||||
"""A BaseModel for storing the settings for colour channel detectors."""
|
"""A BaseModel for storing the settings for colour channel detectors."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
channel_tolerance: float = 7.0
|
channel_tolerance: float = 7.0
|
||||||
"""Channel Tolerance
|
"""Channel Tolerance
|
||||||
|
|
||||||
The number of standard deviations a pixel value must be from the background mean
|
The number of standard deviations a pixel value must be from the background mean
|
||||||
to be considered sample.
|
to be considered sample.
|
||||||
"""
|
"""
|
||||||
min_sample_coverage: float = 25.0
|
|
||||||
|
# Use Field to set Title reported to UI. By default Pydantic will convert the name
|
||||||
|
# from snake_case to Title Case.
|
||||||
|
min_sample_coverage: float = Field(25, title="Sample Coverage Required (%)")
|
||||||
"""Sample Coverage Required (%)
|
"""Sample Coverage Required (%)
|
||||||
|
|
||||||
The minimum percentage of the image that needs to be identified as sample for the
|
The minimum percentage of the image that needs to be identified as sample for the
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,20 @@ class BaseCamera(lt.Thing):
|
||||||
"""The status of the active detector for the UI."""
|
"""The status of the active detector for the UI."""
|
||||||
return self.active_detector.status
|
return self.active_detector.status
|
||||||
|
|
||||||
|
@lt.thing_action
|
||||||
|
def update_detector_settings(self, data) -> None:
|
||||||
|
"""Update the settings of the current detector.
|
||||||
|
|
||||||
|
This is an action not a setting/property as the data model depends on the
|
||||||
|
selected detector. As such, it cannot be specified with the necessary precision
|
||||||
|
to be included in a ThingDescription as a setting/property, while retaining
|
||||||
|
enough useful information to communicate to the UI how it is set and read.
|
||||||
|
|
||||||
|
The information on how to read the settings is exposed in
|
||||||
|
``background_detector_status``.
|
||||||
|
"""
|
||||||
|
self.active_detector.settings = data
|
||||||
|
|
||||||
@lt.thing_setting
|
@lt.thing_setting
|
||||||
def background_detector_data(self) -> dict:
|
def background_detector_data(self) -> dict:
|
||||||
"""The data for each background detector, used to save to disk."""
|
"""The data for each background detector, used to save to disk."""
|
||||||
|
|
|
||||||
|
|
@ -51,20 +51,21 @@
|
||||||
</label>
|
</label>
|
||||||
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
||||||
>{{ label }}
|
>{{ label }}
|
||||||
<div class="input-and-buttons-container">
|
<div v-for="(val, key) in value" :key="key">
|
||||||
<input
|
<label>{{internalLabels[key]}}</label>
|
||||||
v-for="(_v, key) in value"
|
<div class="input-and-buttons-container" >
|
||||||
:key="key"
|
<input
|
||||||
v-model="internalValue[key]"
|
v-model="internalValue[key]"
|
||||||
class="uk-form-small numeric-setting-line-input"
|
class="uk-form-small numeric-setting-line-input"
|
||||||
type="number"
|
type="number"
|
||||||
@focusin="focusIn"
|
@focusin="focusIn"
|
||||||
@focusout="focusOut"
|
@focusout="focusOut"
|
||||||
@keydown="keyDown"
|
@keydown="keyDown"
|
||||||
/>
|
/>
|
||||||
<a class="button-next-to-input" @click="requestUpdate">
|
<a class="button-next-to-input" @click="requestUpdate">
|
||||||
<span class="material-symbols-outlined">refresh</span>
|
<span class="material-symbols-outlined">refresh</span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label v-if="dataType == 'other'" class="uk-form-label"
|
<label v-if="dataType == 'other'" class="uk-form-label"
|
||||||
|
|
@ -112,6 +113,16 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
internalLabels: function() {
|
||||||
|
if (this.dataType == "number_object") {
|
||||||
|
let labels = {};
|
||||||
|
for (const key in this.internalValue){
|
||||||
|
labels[key] = this.dataSchema.properties[key].title
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
valueLength: function() {
|
valueLength: function() {
|
||||||
if (this.dataType == "number_array") {
|
if (this.dataType == "number_array") {
|
||||||
if (this.internalValue == undefined) {
|
if (this.internalValue == undefined) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
v-model="backgroundDetectorStatus.settings"
|
v-model="backgroundDetectorStatus.settings"
|
||||||
:data-schema="backgroundDetectorStatus.settings_schema"
|
:data-schema="backgroundDetectorStatus.settings_schema"
|
||||||
label=""
|
label=""
|
||||||
|
@requestUpdate="readSettings"
|
||||||
|
@sendValue="writeSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -72,6 +74,19 @@ export default {
|
||||||
alertImageLabel(r) {
|
alertImageLabel(r) {
|
||||||
let label = r.output[0] ? "sample" : "background";
|
let label = r.output[0] ? "sample" : "background";
|
||||||
this.modalNotify(`Current image is ${label} (${r.output[1]})`);
|
this.modalNotify(`Current image is ${label} (${r.output[1]})`);
|
||||||
|
},
|
||||||
|
readSettings: async function() {
|
||||||
|
this.backgroundDetectorStatus = await this.readThingProperty(
|
||||||
|
"camera",
|
||||||
|
"background_detector_status"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
writeSettings: async function(requestedValue) {
|
||||||
|
await this.invokeAction(
|
||||||
|
"camera",
|
||||||
|
"update_detector_settings",
|
||||||
|
{"data": requestedValue}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,21 @@ Vue.mixin({
|
||||||
}
|
}
|
||||||
await axios.put(url, value);
|
await axios.put(url, value);
|
||||||
},
|
},
|
||||||
|
async invokeAction(thing, action, data) {
|
||||||
|
let url = this.$store.getters["wot/thingActionUrl"](
|
||||||
|
thing,
|
||||||
|
action,
|
||||||
|
"invokeaction",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
let response = await axios.post(url, data);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.modalError(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
thingActionUrl(thing, action, allow_missing = false) {
|
thingActionUrl(thing, action, allow_missing = false) {
|
||||||
let url = this.$store.getters["wot/thingActionUrl"](
|
let url = this.$store.getters["wot/thingActionUrl"](
|
||||||
thing,
|
thing,
|
||||||
|
|
@ -132,7 +147,6 @@ Vue.mixin({
|
||||||
modalError: function(error) {
|
modalError: function(error) {
|
||||||
var errormsg = this.getErrorMessage(error);
|
var errormsg = this.getErrorMessage(error);
|
||||||
this.$store.commit("setErrorMessage", errormsg);
|
this.$store.commit("setErrorMessage", errormsg);
|
||||||
console.log("Modal error:", error);
|
|
||||||
UIkit.notification({
|
UIkit.notification({
|
||||||
message: `${errormsg}`,
|
message: `${errormsg}`,
|
||||||
status: "danger"
|
status: "danger"
|
||||||
|
|
@ -140,25 +154,43 @@ Vue.mixin({
|
||||||
},
|
},
|
||||||
|
|
||||||
getErrorMessage: function(error) {
|
getErrorMessage: function(error) {
|
||||||
// If a response was obtained, format it nicely and return it
|
// Format the error.
|
||||||
|
let data = this.getErrorData(error)
|
||||||
|
// Get error data may get an object. Handle edge cases and if not try to use
|
||||||
|
// JSON to get the best string. This stops "objectObject" showing as an error.
|
||||||
|
if (data === null) return "null";
|
||||||
|
if (data === undefined) return "undefined";
|
||||||
|
if (typeof data === 'string') return data;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getErrorData: function(error){
|
||||||
|
// If a response was obtained, extract the most specific message
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// If the response is a nicely formatted JSON response from the server
|
// If the response is a nicely formatted JSON response from the server
|
||||||
if (error.response.data.message) {
|
if (error.response.data.message) {
|
||||||
return `${error.response.data.message}`;
|
return error.response.data.message;
|
||||||
}
|
}
|
||||||
if (error.response.data.detail) {
|
if (error.response.data.detail) {
|
||||||
return `${error.response.data.detail}`;
|
try {
|
||||||
|
return error.response.data.detail[0].msg
|
||||||
|
} catch (err) {
|
||||||
|
return error.response.data.detail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If the response is just some generic error response
|
// If the response is just some generic error response
|
||||||
if (error.response.data) {
|
if (error.response.data) {
|
||||||
return `${error.response.data}`;
|
return error.response.data;
|
||||||
}
|
}
|
||||||
return `${error.response}`;
|
return error.response;
|
||||||
}
|
}
|
||||||
// If we have an error object with a message, use that
|
// If we have an error object with a message, use that
|
||||||
if (error.message) return `${error.message}`;
|
if (error.message) return error.message;
|
||||||
// Otherwise attempt to cast it to a string.
|
// At this point just formatting the whole error object is the best we can do.
|
||||||
return `${error}`;
|
return error;
|
||||||
},
|
},
|
||||||
|
|
||||||
showModalElement: function(element) {
|
showModalElement: function(element) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue