Source code for science_jubilee.tools.Camera

import platform
import time

import cv2
import matplotlib
import numpy as np

matplotlib.use("TkAgg")
from typing import Tuple

import yaml
from matplotlib import pyplot as plt

from science_jubilee.labware.Labware import Well
from science_jubilee.tools.Tool import Tool, requires_active_tool

if platform.system() == "Linux":
    import picamera  # Note that this can only be installed on raspbery pi.


[docs] class Camera(Tool): """A class representation of a Raspberry Pi camera. :param Tool: The base tool class :type Tool: class:`Tool` """ def __init__(self, index, name): """Constructor method""" super().__init__(index, name)
[docs] self._camera_matrix = None
[docs] self._dist_matrix = None
# self.load_coefficients( # "/home/pi/POSE/science-jubilee/science-jubilee/tools/configs/calibration_checkerboard.yml" # )
[docs] def load_coefficients(self, path): """Loads camera matrix and distortion coefficients. :param path: Path to your camera calibration file :type path: str :return: A list containing your camera matrix (index 0) and distortion matrix (index 1) :rtype: list """ """""" with open(path, "r") as file: config = yaml.safe_load(file) k_data = config["K"]["data"] camera_matrix = np.array( [k_data[i : i + 3] for i in range(0, len(k_data), 3)], dtype=object ) d_data = config["D"]["data"] dist_matrix = np.array( [d_data[i : i + 3] for i in range(0, len(d_data), 3)], dtype=object ) self._camera_matrix = camera_matrix self._dist_matrix = dist_matrix return [camera_matrix, dist_matrix]
[docs] def get_camera_indices(self): """Returns valid camera indices for use with OpenCV :return: A list of valid camera indices :rtype: list """ index = 0 arr = [] i = 4 while i > 0: try: cap = cv2.VideoCapture(index) if cap.read()[0]: arr.append(index) cap.release() except: print("exception") index += 1 i -= 1 return arr
@requires_active_tool
[docs] def get_frame(self, resolution=[1200, 1200], uvc=False): """Take a picture and return the image. Compensates for lens distortion using camera calibration file. :param resolution: Camera resolution, defaults to [1200, 1200] :type resolution: list, optional :param uvc: True if the camera is a USB video class (UVC) camera for programmatically setting focus, defaults to False :type uvc: bool, optional :return: The captured frame :rtype: ndarray """ with picamera.PiCamera() as camera: camera.resolution = (1200, 1200) camera.framerate = 24 time.sleep(5) output = np.empty((resolution[1], resolution[0], 3), dtype=np.uint8) camera.capture(output, "rgb", use_video_port=True) tpose = np.transpose(output, axes=(1, 0, 2)) # undistorted = cv2.undistort( # tpose, self._camera_matrix, self._dist_matrix, None, self._camera_matrix # ) return tpose
[docs] def show_frame(self, frame, grid=False, save=False, save_path="fig.png"): """Show a captured frame using matplotlib. :param frame: The captured frame to show :type frame: ndarray :param grid: Show grid lines, defaults to False :type grid: bool, optional :param save: Save to file, defaults to False :type save: bool, optional :param save_path: File path to save image, defaults to "fig.png" :type save_path: str, optional """ plt.imshow(frame) plt.title("frame capture") if grid: plt.grid() # add a grid h, w, z = frame.shape plt.plot( [w / 2], [h / 2], marker="o" ) # put a marker in the center of the image if save: plt.axis("off") plt.savefig(f"{save_path}") plt.show()
[docs] def get_show_frame(self): """Get and show a frame.""" self.show_frame(self.get_frame())
@requires_active_tool
[docs] def video_stream(self, camera_index=0): """Start a video stream from the camera. :param camera_index: The camera index, defaults to 0 :type camera_index: int, optional """ cap = cv2.VideoCapture( camera_index ) # Note that the index corresponding to your camera may not be zero but this is the most common default # draw a circle in the center of the frame center = None while center is None: # the first frame grab is sometimes empty ret, frame = cap.read() h, w = frame.shape[0:2] center = (int(w / 2), int(h / 2)) while True: ret, frame = cap.read() target = cv2.circle(frame, center, 5, (0, 255, 0), -1) cv2.imshow("Input", frame) c = cv2.waitKey(1) if ( c == 27 ): # 27 is the built in code for ESC so press escape to close the window. break cap.release() cv2.destroyAllWindows()
@requires_active_tool
[docs] def image_wells(self, resolution=[1200, 1200], uvc=False, wells: Well = None): """Move to a number of wells to take and show images. :param resolution: Camera resolution, defaults to [1200, 1200] :type resolution: list, optional :param uvc: True if the camera is a USB video class (UVC) camera for programmatically setting focus, defaults to False :type uvc: bool, optional :param wells: A list of wells to image, defaults to None :type wells: :class:`Well`, optional """ # TODO: different functions for saving many images, showing images, or getting frames for analysis? if type(wells) != list: wells = [wells] for well in wells: x, y, z_bottom = self._get_xyz(well=well) self._machine.safe_z_movement() self._machine.move_to(x=x, y=y) self._machine.move_to(z=30) # focus height; read in from config time.sleep(1) # ToDo: Better way to sync gcode movements & images f = self.get_frame() self.show_frame(f)
@requires_active_tool
[docs] def get_well_image(self, resolution=[1200, 1200], uvc=False, well: Well = None): """Move to a single well to take a picture and return the frame. :param resolution: Camera resolution, defaults to [1200, 1200] :type resolution: list, optional :param uvc: True if the camera is a USB video class (UVC) camera for programmatically setting focus, defaults to False :type uvc: bool, optional :param well: The well to image, defaults to None :type well: :class:`Well`, optional :return: The captured frame :rtype: ndarray """ x, y, z_bottom = self._get_xyz(well=well) self._machine.safe_z_movement() self._machine.move_to(x=x, y=y) self._machine.move_to(z=30) # focus height; read in from config time.sleep(1) # ToDo: Better way to sync gcode movements & images f = self.get_frame() return f
@staticmethod
[docs] def _get_xyz(well: Well = None, location: Tuple[float] = None): """Get the (x,y,z) position of a well. :param well: The well to fetch position of, defaults to None :type well: :class:`Well`, optional :param location: Directly specify an (x,y,z) location, defaults to None :type location: Tuple[float], optional :raises ValueError: Must specify either a well or a location :return: The well location :rtype: Tuple[float, float, float] """ if well is not None and location is not None: raise ValueError("Specify only one of Well or x,y,z location") elif well is not None: x, y, z = well.x, well.y, well.z else: x, y, z = location return x, y, z
@staticmethod
[docs] def _get_top_bottom(well: Well = None): """Get the top and bottom heights of a well. :param well: The well to fetch position of, defaults to None :type well: Well, optional :return: The z-height of the top and bottom of the well :rtype: Tuple[float, float] """ top = well.top bottom = well.bottom return top, bottom