Source code for sensors.GUIclasses.picam_stream

from bokeh.plotting import figure
from bokeh.events import PointEvent
from bokeh.models import Panel, Button, Div, ColumnDataSource
from bokeh.events import ButtonClick
from bokeh.layouts import layout, column, row, gridplot, Spacer
from bokeh.document import without_document_lock

from sensors.GUIclasses.taxonomy_autocomplete_widget import AutocompleteTaxonomy
from sensors.GUIclasses.camera_settings_widget import CameraSettings
from sensors.Utilities.frame_decoder import FrameDecoder
from sensors.Utilities.write_meta_data import WriteMetaData

import picamerax as picamera

from tornado import gen
import numpy as np

from time import sleep, time
from datetime import datetime
import os
import RPi.GPIO as GPIO

from colorama import Fore

import uuid
import json

import cv2


[docs]class CamStreamTab: """Class to create a Bokeh tab that displays a life stream from a picamera. Pictures can be taken manually or are triggered with a light barrier. :param config: configuration including :ref:`config.yaml` dictionary and current Bokeh document :type config: object :param camera: picamera object initialized in :mod:`sensors.app_hooks` with \ :class:`sensors.Utilities.initdevices.InitDevices` :type camera: object """ def __init__(self, config): self.config = config self.curdoc = config.curdoc self.autocomplete_tax = AutocompleteTaxonomy(config) self.camera_settings = CameraSettings(config) [self.CAMERA_WIDTH, self.CAMERA_HEIGHT] = config.camera_res_preview self.stream_callback = None self.update_in_progress = False self.save_in_progress = False self.camera = config.camera self.template = """<div class='content' style="width: {w}; background-color: {background};color: {colour};"> <div class='info'> {status_text} </div> </div>""" self.template_png = """<div class='image_preview'>""" # call tab method to create tab self._tab = self.make_tab() self.image_div_list = (self.image_div_1, self.image_div_2, self.image_div_3, self.image_div_4) self.plot_counter = 0 self.executor = config.executor self.GPIO_Switch = self.config.GPIO_Switch if len(self.config.connected_GPIO_IR_pins) == 0: self.btn_barrier.visible = False self.barrier = False self.trigger = False self.trigger_timeout = 5 self.trigger_time = 0 self.trigger_start = 0 self.png_to_plot_queue = [] self.decode_jobs = 0 self.block_video_save = False self.decode_callback = None self.show_histogram = False # 5 % margin for start histogram statistic self.pan = False self.pan_start = [self.CAMERA_WIDTH // 20, self.CAMERA_WIDTH // 20] self.pan_end = [self.CAMERA_WIDTH - self.CAMERA_WIDTH // 20, self.CAMERA_HEIGHT - self.CAMERA_WIDTH // 20] self.calculate_image_ROI() @property def tab(self): """Calls method :meth:`make_tab`. """ return self._tab
[docs] def make_tab(self): """ Creates and arranges the elements of the corresponding Bokeh tab. :return: Bokeh panel for picamera stream visualization :rtype: Bokeh object """ # define buttons self.btn_preview = Button(label='Start Stream', width=310) self.btn_preview.on_event(ButtonClick, self.do_preview) self.btn_save = Button(label='Save Image Sequence (%d Images)' % self.config.number_frames, width=310) self.btn_save.disabled = True self.btn_save.on_event(ButtonClick, self.manual_image_save) self.btn_barrier = Button(label='Activate Light Barrier', width=200) self.btn_barrier.disabled = True self.btn_barrier.on_event(ButtonClick, self.barrier_on_off) self.btn_LED = Button(label='Activate LED flash', width=200) self.btn_LED.disabled = True self.btn_LED.on_event(ButtonClick, self.LED_on_off) self.text_status = Div(text=self.template.format(status_text='Stream inactive', background='#DC524C', colour='#FFFFFF', w='200px'), render_as_text=False) # define figure for histogram self.hist_y_range = [0, 0.25] hist_low = 145 hist_high = 155 hist_center = (hist_high + hist_low) // 2 self.histogram = figure(plot_width=208, plot_height=160, x_range=(0, 255), y_range=tuple(self.hist_y_range), y_axis_type=None, tools="", toolbar_location=None, name='hist') self.histogram.background_fill_color = "#eaeaea" self.histogram.outline_line_color = '#595959' self.histogram.xgrid.grid_line_color = None self.histogram.xaxis.major_tick_line_color = None self.histogram.xaxis.ticker = [0, hist_center, 255] self.histogram.xaxis.major_label_overrides = {0: '0'.ljust(2, ' '), hist_center: str(hist_center), 255: '255'.ljust(8, ' ')} self.binning = 2**2 self.edges = np.linspace(0, 256, 256//self.binning + 1).astype(np.uint16) hist = np.zeros(256//self.binning) self.hist_source_red = ColumnDataSource(data=dict(top=hist, left=self.edges[:-1], right=self.edges[1:])) self.hist_source_green = ColumnDataSource(data=dict(top=hist, left=self.edges[:-1], right=self.edges[1:])) self.hist_source_blue = ColumnDataSource(data=dict(top=hist, left=self.edges[:-1], right=self.edges[1:])) self.histogram.quad(bottom=0, top=self.hist_y_range[1], left=hist_low, right=hist_high, fill_color='navy', line_color=None, alpha=0.10) self.histogram.quad(bottom=0, fill_color='#DC524C', line_color=None, alpha=0.25, source=self.hist_source_red) self.histogram.quad(bottom=0, fill_color='#3EA639', line_color=None, alpha=0.25, source=self.hist_source_green) self.histogram.quad(bottom=0, fill_color='#21618C', line_color=None, alpha=0.25, source=self.hist_source_blue) self.mean_red_source = ColumnDataSource(data=dict(x=[0, 0], y=self.hist_y_range)) self.mean_green_source = ColumnDataSource(data=dict(x=[0, 0], y=self.hist_y_range)) self.mean_blue_source = ColumnDataSource(data=dict(x=[0, 0], y=self.hist_y_range)) self.histogram.line(x="x", y="y", line_color='#DC524C', source=self.mean_red_source) self.histogram.line(x="x", y="y", line_color='#3EA639', source=self.mean_green_source) self.histogram.line(x="x", y="y", line_color='#21618C', source=self.mean_blue_source) self.hist_button = Button(label='Activate histogram', width=200) self.hist_button.on_event(ButtonClick, self.update_hist) # define figure for image data for stream preview self.image = np.empty((self.CAMERA_WIDTH, self.CAMERA_HEIGHT, 4), dtype=np.uint8) self.image_source = ColumnDataSource(dict(image=[])) self.imagefig = figure(plot_width=self.CAMERA_WIDTH, plot_height=self.CAMERA_HEIGHT, x_range=(0, self.CAMERA_WIDTH), y_range=(0, self.CAMERA_HEIGHT), x_axis_type=None, y_axis_type=None, tools="", toolbar_location=None, name='image') self.imagefig.background_fill_color = "#eaeaea" self.imagefig.outline_line_color = '#595959' # fill imagefig with rgba image data source self.imagefig.image_rgba('image', x=0, y=0, dw=self.CAMERA_WIDTH, dh=self.CAMERA_HEIGHT, source=self.image_source) self.hist_ROI_source = ColumnDataSource(data=dict(bottom=[0], top=[0], left=[0], right=[0])) self.imagefig.quad(fill_color='#ddac4f', line_color='#ddac4f', line_alpha=1, fill_alpha=0.25, source=self.hist_ROI_source) self.imagefig.on_event('panstart', self.on_PanStart) self.imagefig.on_event('panend', self.on_PanEnd) self.imagefig.on_event('mousemove', self.MouseMove) self.imagefig.on_event('mouseleave', self.MouseLeave) # define div containers for preview of last 4 images taken/triggered self.image_div_1 = Div(text=self.template_png) self.image_div_2 = Div(text=self.template_png) self.image_div_3 = Div(text=self.template_png) self.image_div_4 = Div(text=self.template_png) self.camera_settings.white_balance_1.on_change('value', self.change_camera_settings) self.camera_settings.white_balance_2.on_change('value', self.change_camera_settings) self.camera_settings.analog_gain.on_change('value', self.change_camera_settings) self.camera_settings.digital_gain.on_change('value', self.change_camera_settings) self.camera_settings.sharpness.on_change('value', self.change_camera_settings) self.camera_settings.contrast.on_change('value', self.change_camera_settings) self.camera_settings.video_denoise.on_change('active', self.change_camera_settings) # align buttons, image preview and taxonomy widget start_preview = column(self.btn_preview, self.imagefig, self.autocomplete_tax.widget) # grid of last 4 saved images grid = gridplot([self.image_div_1, self.image_div_2, self.image_div_3, self.image_div_4], ncols=2, merge_tools=False) save_grid = column(self.btn_save, grid) status_LED_barrier = column(self.text_status, self.btn_LED, self.btn_barrier, self.hist_button, self.histogram) # put layout together _layout = layout(column(row(start_preview, status_LED_barrier, save_grid), self.camera_settings.widget)) return Panel(child=_layout, title='PiCamera')
[docs] def on_PanStart(self, event: PointEvent): """Returns relative coordinates of mouse position in image figure of picamera preview on start of panning. \ Updates ROI. """ if self.hist_button.label == 'Deactivate histogram': self.pan_start = [int(event.x), int(event.y)] self.pan = True
[docs] def on_PanEnd(self, event: PointEvent): """Returns relative coordinates of mouse position in image figure of picamera preview on end of panning. \ Updates ROI. """ if self.hist_button.label == 'Deactivate histogram': self.pan_end = [int(event.x), int(event.y)] self.hist_ROI_source.data.update({'bottom': [self.pan_start[1]], 'top': [event.y], 'left': [self.pan_start[0]], 'right': [event.x]}) self.calculate_image_ROI() self.pan = False
[docs] def MouseMove(self, event: PointEvent): """Returns relative coordinates of mouse position in image figure of picamera preview. Updates ROI. """ if self.hist_button.label == 'Deactivate histogram': if self.pan: self.hist_ROI_source.data.update({'bottom': [self.pan_start[1]], 'top': [event.y], 'left': [self.pan_start[0]], 'right': [event.x]}) self.pan_end = [int(event.x), int(event.y)] self.calculate_image_ROI()
[docs] def MouseLeave(self, event: PointEvent): """Returns relative coordinates of position where mouse left image figure of picamera preview. Updates ROI. """ if self.hist_button.label == 'Deactivate histogram': if self.pan: self.pan_end = [int(event.x), int(event.y)] self.hist_ROI_source.data.update({'bottom': [self.pan_start[1]], 'top': [event.y], 'left': [self.pan_start[0]], 'right': [event.x]}) self.calculate_image_ROI()
[docs] def calculate_image_ROI(self): """Calculate coordinates of ROI for histogram data from mouse positions and handle possible cases. """ if self.pan_start[0] < 0: self.pan_start[0] = 0 if self.pan_start[0] > self.CAMERA_WIDTH: self.pan_start[0] = self.CAMERA_WIDTH if self.pan_start[1] > self.CAMERA_HEIGHT: self.pan_start[1] = self.CAMERA_HEIGHT if self.pan_end[0] < 0: self.pan_end[0] = 0 if self.pan_end[0] > self.CAMERA_WIDTH: self.pan_end[0] = self.CAMERA_WIDTH if self.pan_end[1] > self.CAMERA_HEIGHT: self.pan_end[1] = self.CAMERA_HEIGHT if self.pan_start[0] < self.pan_end[0]: self.left, self.right = self.pan_start[0], self.pan_end[0] if self.pan_start[0] > self.pan_end[0]: self.left, self.right = self.pan_end[0], self.pan_start[0] if self.pan_start[1] < self.pan_end[1]: self.bottom, self.top = self.CAMERA_HEIGHT - self.pan_end[1], self.CAMERA_HEIGHT - self.pan_start[1] if self.pan_start[1] > self.pan_end[1]: self.bottom, self.top = self.CAMERA_HEIGHT - self.pan_start[1], self.CAMERA_HEIGHT - self.pan_end[1] self.count = (self.right - self.left) * (self.top - self.bottom)
[docs] def check_light_barrier_interruption(self): """ The method checks if any GPIO pin that is connected to a light barrier is HIGH which corresponds to an interruption. :return: Status flag of light barrier interruption :rtype: boolean """ for pin in self.config.connected_GPIO_IR_pins: if GPIO.input(pin) == GPIO.HIGH: return True return False
[docs] def init_circular_stream(self): """ Initializes a circular buffer for recording a h264 video. Parameters can be tuned via Picamera widget. """ self.camera.framerate = self.config.camera_fps self.camera.resolution = self.config.video_resolution self.camera.shutter_speed = self.camera_settings.shutter_speed.value self.camera.exposure_mode = 'off' self.camera.analog_gain = self.camera_settings.analog_gain.value self.camera.digital_gain = self.camera_settings.digital_gain.value self.camera.awb_mode = 'off' self.camera.awb_gains = (self.camera_settings.white_balance_1.value, self.camera_settings.white_balance_2.value) self.circular_stream = picamera.PiCameraCircularIO(self.camera, seconds=self.config.video_length, bitrate=25000000) self.camera.start_recording(self.circular_stream, format='h264', intra_period=1, quality=20)
[docs] @gen.coroutine @without_document_lock def camera_future(self): """Targeted by next tick callback from :meth:`periodic_callback`. The method creates a future picamera image capture in a parallel thread that is not blocking the main Bokeh visualization thread. :return: future :rtype: object """ # get future object of picamera capture from video port self.update_in_progress = yield self.executor.submit(self.camera_capture) if self.stream_callback is not None: self.curdoc.add_next_tick_callback(self.update_Bokeh)
[docs] def update_hist(self): """Activate respectively deactivates histogram in camera preview. """ if self.hist_button.label == 'Activate histogram': self.hist_button.label = 'Deactivate histogram' self.show_histogram = True self.hist_ROI_source.data.update({'bottom': [self.pan_start[1]], 'top': [self.pan_end[1]], 'left': [self.pan_start[0]], 'right': [self.pan_end[0]]}) else: self.hist_button.label = 'Activate histogram' self.show_histogram = False self.hist_source_red.data['top'] = np.zeros(256 // self.binning) self.hist_source_green.data['top'] = np.zeros(256 // self.binning) self.hist_source_blue.data['top'] = np.zeros(256 // self.binning) self.mean_red_source.data['x'] = [0, 0] self.mean_green_source.data['x'] = [0, 0] self.mean_blue_source.data['x'] = [0, 0] self.hist_ROI_source.data.update({'bottom': [0], 'top': [0], 'left': [0], 'right': [0]})
[docs] def camera_capture(self): """ Targeted by ThreadPoolExecutor from :meth:`camera_future`. Method to capture a single image with picamera video port. :return: False flag for image capture in progress (capture done) :rtype: bool """ self.camera.capture(self.image, 'rgba', use_video_port=True, resize=self.config.camera_res_preview) if self.show_histogram: image_rgba = self.image.reshape((self.CAMERA_HEIGHT, self.CAMERA_WIDTH, 4)) image_rgb_ROI = image_rgba[self.bottom:self.top, self.left:self.right, :3] # cv2.imwrite('/home/pi/roi.png', cv2.cvtColor(image_rgb_ROI, cv2.COLOR_RGB2BGR)) self.hist_red, _ = np.histogram(image_rgb_ROI[:, :, 0], bins=self.edges) self.hist_green, _ = np.histogram(image_rgb_ROI[:, :, 1], bins=self.edges) self.hist_blue, _ = np.histogram(image_rgb_ROI[:, :, 2], bins=self.edges) self.mean_red = np.mean(image_rgb_ROI[:, :, 0]) self.mean_green = np.mean(image_rgb_ROI[:, :, 1]) self.mean_blue = np.mean(image_rgb_ROI[:, :, 2]) return False
[docs] @gen.coroutine def update_Bokeh(self): """ Targeted by next tick callback from :meth:`camera_future`. Updates the picamera preview. """ view = self.image.view(dtype=np.uint32).reshape(self.CAMERA_HEIGHT, self.CAMERA_WIDTH) view = np.flip(view, axis=0) self.image_source.data['image'] = [view] if self.show_histogram: self.hist_source_red.data['top'] = self.hist_red / self.count self.hist_source_green.data['top'] = self.hist_green / self.count self.hist_source_blue.data['top'] = self.hist_blue / self.count self.mean_red_source.data['x'] = [self.mean_red, self.mean_red] self.mean_green_source.data['x'] = [self.mean_green, self.mean_green] self.mean_blue_source.data['x'] = [self.mean_blue, self.mean_blue]
[docs] def periodic_callback(self): """Targeted by a Bokeh periodic callback from :meth:`do_preview`. Checks if the light barrier got interrupted and sets the corresponding flag. If the current Bokeh document update is done a new camera capture for the preview with :meth:`camera_future` gets initiated. If one of the save sequence flags is raised the :meth:`save_h264_video` gets called before a new preview image is taken. """ # check if barrier is activated and triggered if self.barrier: if self.check_light_barrier_interruption(): if self.trigger_time >= self.trigger_timeout: print('Light barrier triggered!') self.trigger = True self.trigger_time = 0 self.trigger_start = time() elif self.trigger_time < self.trigger_timeout/2: self.trigger_time = time() - self.trigger_start else: self.trigger_time = time() - self.trigger_start self.trigger = False if self.png_to_plot_queue: datetime_path = self.png_to_plot_queue.pop(0) self.plot_decoded_frame(datetime_path) # let periodic callbacks from bokeh pass if blocking computations are performed to # not let callback ticks pile up if self.update_in_progress: # Bokeh updating in progress, do nothing so js callbacks aren't blocked pass # save image sequence OR create camera future that is used to update the bokeh visuals else: # check if image sequence should be captured before new camera future gets initiated # initiating a new image capture while the old capture in progress is not possible with picamera # both flags are set to False after capture if self.save_in_progress or self.trigger: # set flags to false after video capture self.trigger, self.save_in_progress = False, False if not self.block_video_save: self.executor.submit(self.save_h264_video) else: self.update_in_progress = True self.curdoc.add_next_tick_callback(self.camera_future)
[docs] def do_preview(self): """ Gets called on button click. Starts respectively ends the preview and changes the text status corresponding to button label. """ if self.btn_preview.label == 'Start Stream': self.btn_preview.label = 'Stop Stream' self.text_status.text = self.template.format(status_text='Stream active', background='#3EA639', colour='#FFFFFF', w='200px') # initialize circular buffer self.init_circular_stream() # callback preview set to 10 ms to check light barrier self.stream_callback = self.curdoc.add_periodic_callback(self.periodic_callback, 10) # activate save button self.btn_save.disabled = False self.btn_barrier.disabled = False self.btn_LED.disabled = False if self.decode_callback is not None: self.curdoc.remove_periodic_callback(self.decode_callback) self.decode_callback = None else: self.btn_preview.label = 'Start Stream' self.text_status.text = self.template.format(status_text='Stream inactive', background='#DC524C', colour='#FFFFFF', w='200px') self.camera.stop_recording() self.btn_save.disabled = True if self.btn_barrier.label == 'Deactivate Light Barrier': self.barrier_on_off() self.btn_barrier.disabled = True if self.btn_LED.label == 'Deactivate LED flash': self.LED_on_off() self.btn_LED.disabled = True self.barrier = False self.btn_barrier.label = 'Activate Light Barrier' self.btn_barrier.button_type = 'default' self.curdoc.remove_periodic_callback(self.stream_callback) self.stream_callback = None if self.decode_jobs != 0: self.decode_callback = self.curdoc.add_periodic_callback(self.finish_decodes, 500)
[docs] def manual_image_save(self): """ Gets called on button click. Raises flag to capture an image sequence in :meth:`periodic_callback` to call :meth:`save_h264_video`. """ self.save_in_progress = True
[docs] def save_h264_video(self): """Gets called on button click or when triggered by an interruption of the light barrier. Saves a h264 video and decodes `n` images at the relative path `p`, where `n` is defined by `number_frames` and `p` by `img_path` in :ref:`config.yaml`. """ self.block_video_save = True # prepare data json with taxonomy information and construct directory now = datetime.now() self.data_dict_temp = self.autocomplete_tax.write_tax_to_data() self.data_dict_temp["date_time"] = now.strftime("%Y-%m-%dT%H:%M:%SZ") timestamp = now.strftime("%Y-%m-%d_%H-%M-%S") folder_name = '{}_{}'.format(self.config.sensor_id, timestamp) self.datetime_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), self.config.img_path, folder_name) os.makedirs(self.datetime_path) GPIO.output(self.GPIO_Switch, GPIO.HIGH) self.camera.wait_recording(0.5 * self.config.video_length) self.camera.stop_recording() # Write the stream to disk UUID_vid = uuid.uuid1() self.data_dict_temp["video"]["filename"] = '{}.{}'.format(UUID_vid, 'mp4') video_filename = os.path.join(self.datetime_path, '{}.{}'.format(UUID_vid, 'h264')) rel_path = os.path.dirname(video_filename).split(os.path.dirname( os.path.dirname(os.path.dirname(__file__))))[-1] print(Fore.RED + 'Write h264 video to ' + rel_path + Fore.WHITE) self.circular_stream.copy_to(video_filename, seconds=self.config.video_length) # deactivate LED again if not activated before video capture if self.btn_LED.label == 'Activate LED flash': GPIO.output(self.GPIO_Switch, GPIO.LOW) self.camera.start_recording(self.circular_stream, format='h264', intra_period=1, quality=20) # converts video to mp4 and decodes frames in parallel thread h264_decoder = FrameDecoder(self.config, self.datetime_path, self.data_dict_temp) h264_decoder.start() self.block_video_save = False self.decode_frames_write_data(h264_decoder)
[docs] def decode_frames_write_data(self, h264_decoder): """Gets called by :meth:`save_h264_video`. :class:`FrameDecoder` converts h264 video to mp4 and \ decodes frames from h264 video and :class:`WriteMetaData` writes meta data measured with sensors into `data.json` dictionary. :parameter h264_decoder: object to decode frames from mp4 video :type h264_decoder: object """ self.decode_jobs += 1 # decode frames from h264 video h264_decoder.frames() data_dict_temp = h264_decoder.data_json # measure meta data and write into json Metadata = WriteMetaData(self.config, data_dict_temp) Metadata.write_all_data() data_dict_temp = Metadata.data_json # save json in datetime path with open(os.path.join(h264_decoder.video_path, 'data.json'), 'w') as outfile: json.dump(data_dict_temp, outfile, indent=4, ensure_ascii=False) self.png_to_plot_queue.append(h264_decoder.plot_png_filename)
[docs] def plot_decoded_frame(self, plot_png_filename): """Gets called by :meth:`periodic_callback`. Plots a decoded frame. """ div_text_img = '<div class="image_preview"><img src="' + plot_png_filename + \ '" width="100%" height="100%"></div>' self.image_div_list[self.plot_counter].text = div_text_img # set counter to cycle through preview plots self.plot_counter += 1 if self.plot_counter == 4: self.plot_counter = 0 # decoded frame has been plotted self.decode_jobs -= 1 if self.decode_jobs == 0: print('All plotting jobs done!') else: print('%d more plotting jobs awaited!' % self.decode_jobs)
[docs] def finish_decodes(self): """Periodic callback targeted from :meth:`do_preview`. Finishes plots of decoded frames in preview if picamera stream gets stopped before all plots were done. """ if self.decode_jobs != 0: if self.png_to_plot_queue: datetime_path = self.png_to_plot_queue.pop(0) self.plot_decoded_frame(datetime_path) else: self.curdoc.remove_periodic_callback(self.decode_callback) self.decode_callback = None
[docs] def barrier_on_off(self): """Gets called on button click. Activates respectively deactivates light barrier corresponding to button label. The barrier status gets checked in :meth:`periodic_callback`. """ if self.btn_barrier.label == 'Activate Light Barrier': self.btn_barrier.label = 'Deactivate Light Barrier' self.btn_barrier.button_type = 'warning' self.barrier = True else: self.btn_barrier.label = 'Activate Light Barrier' self.btn_barrier.button_type = 'default' self.barrier = False
[docs] def LED_on_off(self): """Gets called on button click. Activates respectively deactivates LED corresponding to button label. """ if self.btn_LED.label == 'Activate LED flash': self.btn_LED.label = 'Deactivate LED flash' self.btn_LED.button_type = 'warning' GPIO.output(self.GPIO_Switch, GPIO.HIGH) else: self.btn_LED.label = 'Activate LED flash' self.btn_LED.button_type = 'default' GPIO.output(self.GPIO_Switch, GPIO.LOW)
[docs] def change_camera_settings(self, attr, old, new): """Gets called on value change of gains respectively white balance. Changes camera settings accordingly """ self.camera.awb_gains = (self.camera_settings.white_balance_1.value, self.camera_settings.white_balance_2.value) self.camera.analog_gain = self.camera_settings.analog_gain.value self.camera.digital_gain = self.camera_settings.digital_gain.value self.camera.sharpness = self.camera_settings.sharpness.value self.camera.contrast = self.camera_settings.contrast.value self.camera.video_denoise = bool(int(self.camera_settings.video_denoise.active - 1))