diff --git a/catkit2/services/flir_camera/flir_camera.py b/catkit2/services/flir_camera/flir_camera.py index 2c73638ec..f8dd3e0c1 100644 --- a/catkit2/services/flir_camera/flir_camera.py +++ b/catkit2/services/flir_camera/flir_camera.py @@ -133,6 +133,9 @@ def make_property_helper(name, read_only=False): self.height = self.config['height'] self.offset_x = self.config['offset_x'] self.offset_y = self.config['offset_y'] + self.rot90 = self.config.get('rot90', False) + self.flip_x = self.config.get('flip_x', False) + self.flip_y = self.config.get('flip_y', False) self.pixel_format = self.config['pixel_format'] self.adc_bit_depth = self.config['adc_bit_depth'] @@ -144,6 +147,9 @@ def make_property_helper(name, read_only=False): make_property_helper('height') make_property_helper('offset_x') make_property_helper('offset_y') + make_property_helper('rot90', read_only=True) + make_property_helper('flip_x', read_only=True) + make_property_helper('flip_y', read_only=True) make_property_helper('sensor_width', read_only=True) make_property_helper('sensor_height', read_only=True) @@ -186,11 +192,18 @@ def acquisition_loop(self): pixel_format = PySpin.PixelFormat_Mono16 # Make sure the data stream has the right size and datatype. - has_correct_parameters = np.allclose(self.images.shape, [self.height, self.width]) + if self.rot90: + has_correct_parameters = np.allclose(self.images.shape, [self.width, self.height]) + else: + has_correct_parameters = np.allclose(self.images.shape, [self.height, self.width]) + has_correct_parameters = has_correct_parameters and (self.images.dtype == pixel_dtype) if not has_correct_parameters: - self.images.update_parameters(pixel_dtype, [self.height, self.width], self.NUM_FRAMES_IN_BUFFER) + if self.rot90: + self.images.update_parameters(pixel_dtype, [self.width, self.height], self.NUM_FRAMES_IN_BUFFER) + else: + self.images.update_parameters(pixel_dtype, [self.height, self.width], self.NUM_FRAMES_IN_BUFFER) self.cam.BeginAcquisition() self.is_acquiring.submit_data(np.array([1], dtype='int8')) @@ -214,7 +227,7 @@ def acquisition_loop(self): continue img = image_result.Convert(pixel_format).GetNDArray().astype(pixel_dtype, copy=False) - + img = self.rot_flip_image(img) # Submit image to datastream. self.images.submit_data(img) @@ -263,6 +276,17 @@ def get_temperature(self): with self.mutex: return self.cam.DeviceTemperature.GetValue() + def rot_flip_image(self, img): + # rotation needs to happen first + if self.rot90: + img = np.rot90(img) + if self.flip_x: + img = np.flipud(img) + if self.flip_y: + img = np.fliplr(img) + return img + + if __name__ == '__main__': service = FlirCamera() service.run() diff --git a/catkit2/services/zwo_camera/zwo_camera.py b/catkit2/services/zwo_camera/zwo_camera.py index 8c1a71d6c..3c50b3a03 100644 --- a/catkit2/services/zwo_camera/zwo_camera.py +++ b/catkit2/services/zwo_camera/zwo_camera.py @@ -116,14 +116,25 @@ def open(self): # Set image format to be RAW16, although camera is only 12-bit. self.camera.set_image_type(zwoasi.ASI_IMG_RAW16) - # Set device values from config file (set width and height before offsets) + # Set device values from config file. offset_x = self.config.get('offset_x', 0) offset_y = self.config.get('offset_y', 0) + self.rot90 = self.config.get('rot90', False) + self.flip_x = self.config.get('flip_x', False) + self.flip_y = self.config.get('flip_y', False) self.width = self.config.get('width', self.sensor_width - offset_x) self.height = self.config.get('height', self.sensor_height - offset_y) - self.offset_x = offset_x - self.offset_y = offset_y + + self.offset_x, self.offset_y = self.get_camera_offset(offset_x, offset_y) + + # Note that get_camera_offset must be called before updating the values of self.width and self.height. + if self.rot90: + # If rotating by 90 degrees swap the values for width and height. + w = self.width + h = self.height + self.width = h + self.height = w self.gain = self.config.get('gain', 0) self.exposure_time_step_size = self.config.get('exposure_time_step_size', 1) @@ -133,7 +144,13 @@ def open(self): # Create datastreams # Use the full sensor size here to always allocate enough shared memory. - self.images = self.make_data_stream('images', 'float32', [self.sensor_height, self.sensor_width], self.NUM_FRAMES_IN_BUFFER) + # If the sensor is rotated by 90 degrees, make sure to flip the axes in the datastream + if self.rot90: + self.images = self.make_data_stream('images', 'float32', [self.sensor_width, self.sensor_height], + self.NUM_FRAMES_IN_BUFFER) + else: + self.images = self.make_data_stream('images', 'float32', [self.sensor_height, self.sensor_width], + self.NUM_FRAMES_IN_BUFFER) self.temperature = self.make_data_stream('temperature', 'float64', [1], self.NUM_FRAMES_IN_BUFFER) self.is_acquiring = self.make_data_stream('is_acquiring', 'int8', [1], self.NUM_FRAMES_IN_BUFFER) @@ -162,6 +179,9 @@ def setter(val): make_property_helper('height', requires_stopped_acquisition=True) make_property_helper('offset_x') make_property_helper('offset_y') + make_property_helper('rot90', read_only=True) + make_property_helper('flip_x', read_only=True) + make_property_helper('flip_y', read_only=True) make_property_helper('sensor_width', read_only=True) make_property_helper('sensor_height', read_only=True) @@ -186,10 +206,16 @@ def close(self): def acquisition_loop(self): # Make sure the data stream has the right size and datatype. - has_correct_parameters = np.allclose(self.images.shape, [self.height, self.width]) + if self.rot90: + has_correct_parameters = np.allclose(self.images.shape, [self.width, self.height]) + else: + has_correct_parameters = np.allclose(self.images.shape, [self.height, self.width]) if not has_correct_parameters: - self.images.update_parameters('float32', [self.height, self.width], self.NUM_FRAMES_IN_BUFFER) + if self.rot90: + self.images.update_parameters('float32', [self.width, self.height], self.NUM_FRAMES_IN_BUFFER) + else: + self.images.update_parameters('float32', [self.height, self.width], self.NUM_FRAMES_IN_BUFFER) # Start acquisition. self.camera.start_video_capture() @@ -200,7 +226,8 @@ def acquisition_loop(self): try: while self.should_be_acquiring.is_set() and not self.should_shut_down: img = self.camera.capture_video_frame(timeout=timeout) - + # make sure image has proper rotation and flips for the camera + img = self.rot_flip_image(img) self.images.submit_data(img.astype('float32')) finally: # Stop acquisition. @@ -316,6 +343,89 @@ def offset_y(self): def offset_y(self, offset_y): self.camera.set_roi_start_position(self.offset_x, offset_y) + def rot_flip_image(self, img): + # rotation needs to happen first + if self.rot90: + img = np.rot90(img) + if self.flip_x: + img = np.flipud(img) + if self.flip_y: + img = np.fliplr(img) + return np.ascontiguousarray(img) + + def get_camera_offset(self, x, y): + """Convert relative camera offsets given by the user to absolute offsets in camera array coordinates. + + This is done by performing the following procedure: + 1.) Translate the origin to the center of the ROI. + 2.) if rot90 is True, rotate 90 degrees counter-clockwise about the center of the ROI + 3.) Translate the origin back to the upper left fo the ROI. + 4.) If flip_x is True, reflect in x. + 5.) If flip_y is True, reflect in y. + + Parameters + ---------- + x: float + The x-offset coordinate + y: float + The y-offset coordinate + Returns + ------- + new_x, new_y: float, float + The transformed x and y coordinates for their location in camera array coordinates. If there is no rotation + or flip in x or y, then this returns the same x, y values that are input. + """ + # Define the translation matrix T to get to the center of the ROI. + T = np.zeros((3, 3)) + np.fill_diagonal(T, 1) + T[0][-1] = -self.width / 2 + T[1][-1] = -self.height / 2 + + # Initialize rotation matrix R. + R = np.zeros((3, 3)) + + # Initialize x reflection matrix, X. Defaults to unity matrix. + X = np.eye(3, 3) + + # Initialize y reflection matrix, Y. Defaults to unity matrix. + Y = np.eye(3, 3) + + if self.rot90: + # Define rotation matrix. + R[0][1] = -1 + R[1][0] = 1 + R[2][2] = 1 + else: + # Default to unity matrix. + np.fill_diagonal(R, 1) + + if self.flip_x: + # Define x reflection matrix. + X[0][0] = -1 + + if self.flip_y: + # Define y reflection matrix. + Y[1][1] = -1 + + # Define translation matrix back so that the origin is in the upper left as expected. + T_back = np.eye(3, 3) + + if self.rot90: + # Want to come back to new origin for which the height/width dimensions will be flipped if rotated. + T_back[0][-1] = self.height / 2 + T_back[1][-1] = self.width / 2 + else: + T_back[0][-1] = self.width / 2 + T_back[1][-1] = self.height / 2 + + # Perform the dot product. First flip in x to establish top left origin, then translate to ROI center, rotate, + # translate back to origin, flip in x, and finally flip in y. + coords = [x, y, 1] + new_coords = np.linalg.multi_dot([T_back, Y, X, R, T, coords]) + + return new_coords[0], new_coords[1] + + if __name__ == '__main__': service = ZwoCamera() service.run()