Camera settings for labthings-picamera2

I've updated URLs and tidied things a bit, so that we can
now adjust the key camera settings with the new camera
drivers.
This commit is contained in:
Richard Bowman 2023-11-02 16:54:22 +00:00
parent 13229d9b99
commit 79752a2a4c
5 changed files with 460 additions and 277 deletions

View file

@ -0,0 +1,121 @@
<template>
<div>
<label class="uk-form-label">{{ label }}</label>
<div class="input-and-buttons-container">
<input
v-for="(_v, key) in value"
class="uk-form-small numeric-setting-line-input"
type="number"
v-model="value[key]"
:key="key"
@focusin="focusIn"
@focusout="focusOut"
@keydown="keyDown"
/>
<a class="button-next-to-input" @click="readProperty">
<i class="material-icons">refresh</i>
</a>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "MultiNumericSettingLine",
props: {
label: {
type: String,
required: true
},
propertyUrl: {
type: String,
required: true
},
readBackDelay: {
type: Number,
default: undefined,
required: false
},
},
data: () => {
return {
value: {},
valueOnEnter: undefined
}
},
computed: {
readBack: function() {
return this.readBackDelay !== undefined;
}
},
mounted() {
this.readProperty();
},
methods: {
readProperty: async function() {
let response = await axios.get(this.propertyUrl)
this.value = response.data
console.log("Read property", this.propertyUrl, response.data)
return response.data
},
writeProperty: async function() {
try {
let requestedValue = Number(this.value)
await axios.post(this.propertyUrl, requestedValue)
if(this.readBack) {
await new Promise(r => setTimeout(r, this.readBackDelay))
let newVal = await this.readProperty()
if(newVal == requestedValue) {
await this.modalNotify(`Set ${this.label} to ${newVal}.`)
} else {
await this.modalNotify(`Set ${this.label} to ${newVal} (requested ${requestedValue}).`)
}
} else {
await this.modalNotify(`Set ${this.label} to ${this.value}.`);
}
} catch(error) {
this.modalError(error); // Let mixin handle error
}
},
focusIn: function(event) {
this.valueOnEnter = event.target.value;
},
focusOut: function(event) {
if (this.valueOnEnter != event.target.value) {
this.writeProperty(event.target.value);
}
},
keyDown: function(event) {
// Pressing enter should set the property, whether or not we think it's changed.
if (event.keyCode == 13) {
this.writeProperty();
}
}
}
};
</script>
<style scoped>
.input-and-buttons-container {
display: table;
width: 100%;
}
.numeric-setting-line-input {
display: table-cell;
width:100%;
}
.button-next-to-input {
display: table-cell;
padding-left: 20px;
padding-right: 5px;
vertical-align: middle;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,127 @@
<template>
<div>
<label class="uk-form-label">{{ label }}</label>
<div class="input-and-buttons-container">
<input
v-for="i in value.length"
class="uk-form-small numeric-setting-line-input"
type="number"
v-model="value[i - 1]"
:key="i"
@focusin="focusIn"
@focusout="focusOut"
@keydown="keyDown"
/>
<a class="button-next-to-input" @click="readProperty">
<i class="material-icons">refresh</i>
</a>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "NumericArraySettingLine",
props: {
label: {
type: String,
required: true
},
propertyUrl: {
type: String,
required: true
},
readBackDelay: {
type: Number,
default: undefined,
required: false
},
},
data: () => {
return {
value: {},
valueOnEnter: undefined
}
},
computed: {
readBack: function() {
return this.readBackDelay !== undefined;
}
},
mounted() {
this.readProperty();
},
methods: {
readProperty: async function() {
let response = await axios.get(this.propertyUrl)
this.value = response.data
console.log("Read property", this.propertyUrl, response.data)
return response.data
},
writeProperty: async function() {
try {
let requestedValue = this.value
await axios.post(this.propertyUrl, requestedValue)
if(this.readBack) {
await new Promise(r => setTimeout(r, this.readBackDelay))
let newVal = await this.readProperty()
if(newVal == requestedValue) {
await this.modalNotify(`Set ${this.label} to ${newVal}.`)
} else {
await this.modalNotify(`Set ${this.label} to ${newVal} (requested ${requestedValue}).`)
}
} else {
await this.modalNotify(`Set ${this.label} to ${this.value}.`);
}
} catch(error) {
this.modalError(error); // Let mixin handle error
}
},
focusIn: function(event) {
this.valueOnEnter = event.target.value;
},
focusOut: function(event) {
if (this.valueOnEnter != event.target.value) {
this.writeProperty(event.target.value);
}
},
keyDown: function(event) {
// Pressing enter should set the property, whether or not we think it's changed.
if (event.keyCode == 13) {
this.writeProperty();
}
}
}
};
</script>
<style scoped>
.input-and-buttons-container {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-content: stretch;
align-items: center;
width: 100%;
}
.numeric-setting-line-input {
flex-grow: 1;
margin-left: 5px;
margin-right: 5px;
width: 6em;
}
.button-next-to-input {
flex-grow: 0;
padding-left: 5px;
padding-right: 5px;
vertical-align: middle;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,125 @@
<template>
<div>
<label class="uk-form-label">{{ label }}</label>
<div class="input-and-buttons-container">
<input
class="uk-form-small numeric-setting-line-input"
type="number"
v-model="value"
@focusin="focusIn"
@focusout="focusOut"
@keydown="keyDown"
/>
<a class="button-next-to-input" @click="readProperty">
<i class="material-icons">refresh</i>
</a>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "NumericSettingLine",
props: {
label: {
type: String,
required: true
},
propertyUrl: {
type: String,
required: true
},
readBackDelay: {
type: Number,
default: undefined,
required: false
}
},
data: () => {
return {
value: undefined,
valueOnEnter: undefined
}
},
computed: {
readBack: function() {
return this.readBackDelay !== undefined;
}
},
mounted() {
this.readProperty();
},
methods: {
readProperty: async function() {
let response = await axios.get(this.propertyUrl)
this.value = response.data
console.log("Read property", this.propertyUrl, response.data)
return response.data
},
writeProperty: async function() {
try {
let requestedValue = Number(this.value)
await axios.post(this.propertyUrl, requestedValue)
if(this.readBack) {
await new Promise(r => setTimeout(r, this.readBackDelay))
let newVal = await this.readProperty()
if(newVal == requestedValue) {
await this.modalNotify(`Set ${this.label} to ${newVal}.`)
} else {
await this.modalNotify(`Set ${this.label} to ${newVal} (requested ${requestedValue}).`)
}
} else {
await this.modalNotify(`Set ${this.label} to ${this.value}.`);
}
} catch(error) {
this.modalError(error); // Let mixin handle error
}
},
focusIn: function(event) {
this.valueOnEnter = event.target.value;
},
focusOut: function(event) {
if (this.valueOnEnter != event.target.value) {
this.writeProperty(event.target.value);
}
},
keyDown: function(event) {
// Pressing enter should set the property, whether or not we think it's changed.
if (event.keyCode == 13) {
this.writeProperty();
}
}
}
};
</script>
<style scoped>
.input-and-buttons-container {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-content: stretch;
align-items: center;
width: 100%;
}
.numeric-setting-line-input {
flex-grow: 1;
margin-left: 5px;
margin-right: 5px;
width: 6em;
}
.button-next-to-input {
flex-grow: 0;
padding-left: 5px;
padding-right: 5px;
vertical-align: middle;
cursor: pointer;
}
</style>

View file

@ -2,6 +2,9 @@
<div id="cameraSettings"> <div id="cameraSettings">
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid> <div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
<div class="uk-width-large"> <div class="uk-width-large">
<h3>Automatic calibration</h3>
<cameraCalibrationSettings :camera-uri="cameraUri"/>
<h3>Manual camera settings</h3> <h3>Manual camera settings</h3>
<form @submit.prevent="applySettingsRequest"> <form @submit.prevent="applySettingsRequest">
<div class="uk-margin-small-bottom"> <div class="uk-margin-small-bottom">
@ -9,139 +12,42 @@
<li class="uk-open"> <li class="uk-open">
<a class="uk-accordion-title" href="#">Pi Camera Settings</a> <a class="uk-accordion-title" href="#">Pi Camera Settings</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<div v-if="picamera.shutter_speed !== undefined"> <NumericSettingLine
<label class="uk-form-label" for="form-stacked-text" label="Exposure time"
>Exposure time</label :property-url="cameraUri + 'exposure_time'"
> :read-back-delay="1000"
<div class="uk-form-controls"> />
<input <NumericSettingLine
v-model="picamera.shutter_speed" label="Analogue gain"
class="uk-input uk-form-small" :property-url="cameraUri + 'analogue_gain'"
type="number" :read-back-delay="1000"
/> />
</div> <NumericArraySettingLine
</div> label="Colour gains"
:property-url="cameraUri + 'colour_gains'"
<div v-if="picamera.analog_gain !== undefined"> :read-back-delay="1000"
<label class="uk-form-label" for="form-stacked-text" />
>Analogue gain</label
>
<div class="uk-form-controls">
<input
v-model="picamera.analog_gain"
class="uk-input uk-form-small"
type="number"
step="0.0000000000000001"
/>
</div>
</div>
<div v-if="picamera.digital_gain !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Digital gain</label
>
<div class="uk-form-controls">
<input
v-model="picamera.digital_gain"
class="uk-input uk-form-small"
type="number"
step="0.0000000000000001"
/>
</div>
</div>
<div v-if="picamera.awb_gains !== undefined">
<label class="uk-form-label" for="form-stacked-text">
White Balance gains
</label>
<div class="uk-form-controls">
<label class="uk-form-label">R:</label>
<input
v-model="picamera.awb_gains[0]"
class="uk-input uk-form-small"
type="number"
step="0.0000000000000001"
/>
<label class="uk-form-label">B:</label>
<input
v-model="picamera.awb_gains[1]"
class="uk-input uk-form-small"
type="number"
step="0.0000000000000001"
/>
</div>
</div>
</div> </div>
</li> </li>
<li class="uk-open"> <li class="uk-open">
<a class="uk-accordion-title" href="#">Image Quality</a> <a class="uk-accordion-title" href="#">Image Quality</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<div class="uk-child-width-1-2" uk-grid> <NumericSettingLine
<div v-if="jpeg_quality !== undefined"> label="MJPEG stream bit rate"
<label class="uk-form-label" for="form-stacked-text" :property-url="cameraUri + 'mjpeg_bitrate'"
>JPEG capture quality (%)</label :read-back-delay="100"
> />
<div class="uk-form-controls"> <NumericArraySettingLine
<input label="MJPEG stream resolution"
v-model="jpeg_quality" :property-url="cameraUri +'stream_resolution'"
class="uk-input uk-form-small" :read-back-delay="100"
type="number" />
step="1"
/>
</div>
</div>
<div v-if="stream_resolution !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Stream resolution</label
>
<select
v-model="stream_resolution"
class="uk-select uk-form-small"
>
<option
v-for="option in resolutionOptions"
:key="option.value[0]"
:value="option.value"
>
{{ option.text }}
</option>
</select>
</div>
</div>
</div> </div>
</li> </li>
<!--
<li> <li>
<a class="uk-accordion-title" href="#">Advanced</a> <a class="uk-accordion-title" href="#">Advanced</a>
<div class="uk-accordion-content"> <div class="uk-accordion-content">
<div v-if="mjpeg_bitrate !== undefined">
<label class="uk-form-label" for="form-stacked-text"
>Camera bitrate</label
>
<select
v-model="mjpeg_bitrate"
class="uk-select uk-form-small"
>
<option
v-for="option in bitrateOptions"
:key="option.value"
:value="option.value"
>
{{ option.text }}
</option>
</select>
<p class="uk-margin-remove">
This option sets the target bitrate for camera
recordings.<br />
<b
>Limiting bitrate may impact fast-autofocus
reliability.</b
>
However is guaranteed to fix the live stream disappearing
for some highly detailed samples.
</p>
</div>
<div <div
v-if="picamera.framerate !== undefined" v-if="picamera.framerate !== undefined"
@ -166,27 +72,14 @@
This option sets the framerate of the Pi Camera video This option sets the framerate of the Pi Camera video
encoder. The actual stream framerate is determined by encoder. The actual stream framerate is determined by
network speed, and is limited by the encoder framerate.<br /> network speed, and is limited by the encoder framerate.<br />
<b <b>Lowering framerate may impact fast-autofocus accuracy.</b>
>Lowering framerate may impact fast-autofocus
accuracy.</b
>
</p> </p>
</div> </div>
</div> </div>
</li> </li>-->
</ul> </ul>
</div> </div>
<button
type="submit"
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
>
Apply Settings
</button>
</form> </form>
<h3>Automatic calibration</h3>
<cameraCalibrationSettings></cameraCalibrationSettings>
</div> </div>
<div id="mini-stream"> <div id="mini-stream">
@ -197,9 +90,10 @@
</template> </template>
<script> <script>
import axios from "axios";
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue"; import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
import NumericSettingLine from "../../genericComponents/numericSettingLine.vue";
import NumericArraySettingLine from "../../genericComponents/numericArraySettingLine.vue";
// Export main app // Export main app
export default { export default {
@ -207,8 +101,10 @@ export default {
components: { components: {
cameraCalibrationSettings, cameraCalibrationSettings,
miniStreamDisplay miniStreamDisplay,
}, NumericSettingLine,
NumericArraySettingLine
},
data: function() { data: function() {
return { return {
@ -242,74 +138,8 @@ export default {
}, },
computed: { computed: {
settingsUri: function() { cameraUri: function() {
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`; return `${this.$store.getters.baseUri}/camera/`;
}
},
mounted() {
this.updateSettings();
},
methods: {
updateSettings: function() {
axios
.get(this.settingsUri)
.then(response => {
const cameraSettings = response.data.camera;
// Get base camera settings
this.mjpeg_bitrate = cameraSettings.mjpeg_bitrate;
this.jpeg_quality = cameraSettings.jpeg_quality;
this.stream_resolution = cameraSettings.stream_resolution;
// Get Pi Camera settings if they exist
if (cameraSettings.picamera) {
this.picamera.analog_gain = cameraSettings.picamera.analog_gain;
this.picamera.digital_gain = cameraSettings.picamera.digital_gain;
this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed;
this.picamera.framerate = cameraSettings.picamera.framerate;
this.picamera.awb_gains = cameraSettings.picamera.awb_gains;
}
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
applySettingsRequest: function() {
// We have to use parseInt/parseFloat because JS sometimes seems to
// make the numbers be strings... TypeScript would solve this...
var payload = {
camera: {
mjpeg_bitrate: parseInt(this.mjpeg_bitrate),
jpeg_quality: parseInt(this.jpeg_quality),
stream_resolution: this.stream_resolution,
picamera: {
shutter_speed: parseFloat(this.picamera.shutter_speed),
analog_gain: parseFloat(this.picamera.analog_gain),
digital_gain: parseFloat(this.picamera.digital_gain),
framerate: parseInt(this.picamera.framerate),
awb_gains: [
parseFloat(this.picamera.awb_gains[0]),
parseFloat(this.picamera.awb_gains[1])
]
}
}
};
console.log(payload);
// Send request
axios
.put(this.settingsUri, payload)
.then(() => {
return new Promise(r => setTimeout(r, 500));
}) // why is there no built-in for this??!
.then(() => {
// Update local settings
this.updateSettings();
this.modalNotify("Camera settings applied.");
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
} }
} }
}; };

View file

@ -1,14 +1,14 @@
<template> <template>
<div> <div>
<!--Show auto calibrate if default plugin is enabled--> <!--Show auto calibrate if default plugin is enabled-->
<div v-if="'recalibrate' in recalibrationLinks" class="uk-margin-small"> <div v-if="'full_auto_calibrate' in actions" class="uk-margin-small">
<taskSubmitter <taskSubmitter
:can-terminate="false" :can-terminate="false"
:requires-confirmation="true" :requires-confirmation="true"
:confirmation-message=" :confirmation-message="
'Start recalibration? This may take a while, and the microscope will be locked during this time.' 'Start recalibration? This may take a while, and the microscope will be locked during this time.'
" "
:submit-url="recalibrationLinks.recalibrate.href" :submit-url="cameraUri + 'full_auto_calibrate'"
:submit-label="'Full Auto-Calibrate'" :submit-label="'Full Auto-Calibrate'"
@response="onRecalibrateResponse" @response="onRecalibrateResponse"
@error="modalError" @error="modalError"
@ -16,13 +16,13 @@
</taskSubmitter> </taskSubmitter>
</div> </div>
<div <div
v-if="'auto_exposure_from_raw' in recalibrationLinks" v-if="'auto_expose_from_minimum' in actions"
class="uk-margin-small" class="uk-margin-small"
> >
<taskSubmitter <taskSubmitter
:can-terminate="false" :can-terminate="false"
:requires-confirmation="false" :requires-confirmation="false"
:submit-url="recalibrationLinks.auto_exposure_from_raw.href" :submit-url="cameraUri + 'auto_expose_from_minimum'"
:submit-label="'Auto gain &amp; shutter speed'" :submit-label="'Auto gain &amp; shutter speed'"
@response="onRecalibrateResponse" @response="onRecalibrateResponse"
@error="modalError" @error="modalError"
@ -30,13 +30,13 @@
</taskSubmitter> </taskSubmitter>
</div> </div>
<div <div
v-if="'auto_white_balance_from_raw' in recalibrationLinks" v-if="'calibrate_white_balance' in actions"
class="uk-margin-small" class="uk-margin-small"
> >
<taskSubmitter <taskSubmitter
:can-terminate="false" :can-terminate="false"
:requires-confirmation="false" :requires-confirmation="false"
:submit-url="recalibrationLinks.auto_white_balance_from_raw.href" :submit-url="cameraUri + 'calibrate_white_balance'"
:submit-label="'Auto white balance'" :submit-label="'Auto white balance'"
@response="onRecalibrateResponse" @response="onRecalibrateResponse"
@error="modalError" @error="modalError"
@ -44,7 +44,7 @@
</taskSubmitter> </taskSubmitter>
</div> </div>
<div <div
v-if="'auto_lens_shading_table' in recalibrationLinks" v-if="'calibrate_lens_shading' in actions"
class="uk-margin-small" class="uk-margin-small"
> >
<taskSubmitter <taskSubmitter
@ -54,7 +54,7 @@
'Is the microscope looking at an evenly illuminated, empty field of view? ' + 'Is the microscope looking at an evenly illuminated, empty field of view? ' +
'If not, the current image will show through in any images captured afterwards.' 'If not, the current image will show through in any images captured afterwards.'
" "
:submit-url="recalibrationLinks.auto_lens_shading_table.href" :submit-url="cameraUri + 'calibrate_lens_shading'"
:submit-label="'Auto flat field correction'" :submit-label="'Auto flat field correction'"
@response="onRecalibrateResponse" @response="onRecalibrateResponse"
@error="modalError" @error="modalError"
@ -62,23 +62,34 @@
</taskSubmitter> </taskSubmitter>
</div> </div>
<div v-show="showExtraSettings" class="uk-child-width-expand"> <div
<button v-show="showExtraSettings"
v-if="'flatten_lens_shading_table' in recalibrationLinks" v-if="'flatten_lens_shading_table' in actions"
class="uk-button uk-button-danger uk-width-1-1" class="uk-child-width-expand"
@click="flattenLensShadingTableRequest" >
<taskSubmitter
:can-terminate="false"
:requires-confirmation="false"
:submit-url="cameraUri + 'flat_lens_shading'"
:submit-label="'Disable flat field correction'"
@response="onRecalibrateResponse"
@error="modalError"
> >
Disable flat field correction </taskSubmitter>
</button>
</div> </div>
<div
<div v-if="LstDownloadEnabled"> v-show="showExtraSettings"
<a v-if="'reset_lens_shading' in actions"
class="uk-button uk-button-default uk-width-large uk-margin-small-top uk-align-center" class="uk-child-width-expand"
:href="LstDownloadUri" > <taskSubmitter
download :can-terminate="false"
>Download Lens-Shading Table</a :requires-confirmation="false"
:submit-url="cameraUri + 'reset_lens_shading'"
:submit-label="'Reset flat field correction'"
@response="onRecalibrateResponse"
@error="modalError"
> >
</taskSubmitter>
</div> </div>
</div> </div>
</template> </template>
@ -100,66 +111,35 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true
},
cameraUri: {
type: String,
required: true
} }
}, },
data: function() { data: function() {
return { return {
recalibrationLinks: {}, actions: {},
isCalibrating: false, isCalibrating: false,
LstDownloadEnabled: false LstDownloadEnabled: false
}; };
}, },
computed: {
pluginsUri: function() {
return `${this.$store.getters.baseUri}/api/v2/extensions`;
},
LstDownloadUri: function() {
return `${this.$store.getters.baseUri}/api/v2/instrument/camera/lst`;
}
},
mounted() { mounted() {
this.updateRecalibrationLinks(); this.updateActions();
this.checkLstDownload();
}, },
methods: { methods: {
updateRecalibrationLinks: function() { updateActions: async function() {
axios try{
.get(this.pluginsUri) // Get a list of plugins let response = await axios.get(this.cameraUri) // Get the thing description
.then(response => { let td = response.data
var plugins = response.data; this.actions = td.actions
var foundExtension = plugins.find( console.log("full auto calibrate in actions", 'full_auto_calibrate' in this.actions)
e => e.title === "org.openflexure.calibration.picamera" } catch(error) {
); this.modalError(error) // Let mixin handle error
// if AutocalibrationPlugin is enabled }
if (foundExtension) {
// Get plugin action link
this.recalibrationLinks = foundExtension.links;
} else {
this.recalibrationLinks = {};
}
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
},
checkLstDownload: function() {
axios
.get(this.LstDownloadUri) // Get a list of plugins
.then(response => {
if (response.status === 200) {
this.LstDownloadEnabled = true;
} else {
this.LstDownloadEnabled = false;
}
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
}, },
onRecalibrateResponse: function() { onRecalibrateResponse: function() {