def safe_angle(input_angle, radian=False, warning=True, debug=True): ''' make ensure the rotation is in [-180, 180] in degree parameters: input_angle: an angle which is supposed to be in degree radian: if True, the unit is replaced to radian instead of degree outputs: angle: an angle in degree within (-180, 180] ''' angle = copy.copy(input_angle) if debug: assert isscalar(angle), 'the input angle should be a scalar' if isnparray(angle): angle = angle[0] # single numpy scalar value if radian: while angle > np.pi: angle -= np.pi while angle <= -np.pi: angle += np.pi else: while angle > 180: angle -= 360 while angle <= -180: angle += 360 return angle
def image_draw_mask(input_image, input_image_mask, transparency=0.3, warning=True, debug=True): ''' draw a mask on top of an image with certain transparency parameters: input_image: a pil or numpy image input_image_mask: a pil or numpy image transparency: transparency factor outputs: masked_image: uint8 numpy image ''' np_image, _ = safe_image(input_image, warning=warning, debug=debug) np_image_mask, _ = safe_image(input_image_mask, warning=warning, debug=debug) if debug: assert isscalar(transparency), 'the transparency should be a scalar' assert np_image.shape == np_image_mask.shape, 'the shape of mask should be equal to the shape of input image' if isfloatimage(np_image): np_image = (np_image * 255.).astype('uint8') if isfloatimage(np_image_mask): np_image_mask = (np_image_mask * 255.).astype('uint8') pil_image, pil_image_mask = Image.fromarray(np_image), Image.fromarray( np_image_mask) masked_image = np.array( Image.blend(pil_image, pil_image_mask, alpha=transparency)) return masked_image
def radian2degree(radian, debug=True): ''' this function return radian given degree, difference from default math.degrees is that this function normalize the output in range [0, 360) ''' if debug: assert isscalar(degree), 'input radian number is not correct' degree = math.degrees(radian) while degree < 0: degree += 360.0 while degree >= 360.0: degree -= 360.0 return degree
def degree2radian(degree, debug=True): ''' this function return degree given radians, difference from default math.degrees is that this function normalize the output in range [0, 2*pi) ''' if debug: assert isscalar(degree), 'input degree number is not correct' radian = math.radians(degree) while radian < 0: radian += 2*math.pi while radian >= 2*math.pi: radian -= 2*math.pi return radian
def safe_npdata(input_data, warning=True, debug=True): ''' copy a list of data or a numpy data to the buffer for use parameters: input_data: a list, a scalar or numpy data outputs: np_data: a copy of numpy data ''' if islist(input_data): np_data = np.array(input_data) elif isscalar(input_data): np_data = np.array(input_data).reshape((1, )) elif isnparray(input_data): np_data = input_data.copy() else: assert False, 'only list of data, scalar or numpy data are supported' return np_data
def generate_gaussian_heatmap(input_pts, image_size, std, warning=True, debug=True): ''' generate a heatmap based on the input points array, create a 2-D gaussian with given std around each points provided the mask is generated by the occlusion from the point array: only occlusion with -1 will be masked out 0 -> invisible points without location 1 -> visible points with location -1 -> visible points without location, masked parameters: input_pts: a list of 3 elements, a listoflist of 3 elements: e.g., [[1,2], [5,6], [0, 1]], a numpy array with shape or (3, N) or (3, ) image_size: a tuple or list of numpy array with 2 elements, representing (height, width) std: the standard deviation used for gaussian distribution outputs: masked_heatmap: numpy float32 multichannel numpy array, (height, width, num_pts + 1) mask_valid: numpy float32 multichannel numpy array, (1, 1, num_pts + 1) mask_visible: numpy float32 multichannel numpy array, (1, 1, num_pts + 1) ''' pts_array = safe_2dptsarray_occlusion(input_pts, warning=warning, debug=debug) if debug: assert isscalar(std), 'the standard deviation should be a scalar' assert isimsize(image_size), 'the image size is not correct' height, width = image_size[0], image_size[1] num_pts, threshold = pts_array.shape[1], 0.01 heatmap = np.fromfunction( lambda y, x, pts_id : ((x - pts_array[0, pts_id])**2 \ + (y - pts_array[1, pts_id])**2) \ / -2.0 / std / std, (height, width, num_pts), dtype=int) heatmap = np.exp(heatmap) valid = np.logical_or(pts_array[2, :] == 0, pts_array[2, :] == 1) # mask out invalid points with -1 in the third visible = pts_array[2, :] == 1 # mask out invalid and occuluded points mask_valid = np.ones((1, 1, num_pts+1), dtype='float32') mask_valid[0, 0, :num_pts] = valid # never mask out the background channel mask_visible = np.ones((1, 1, num_pts+1), dtype='float32') mask_visible[0, 0, :num_pts] = visible # never mask out the background channel # mask out the invalid channel heatmap[heatmap < threshold] = 0 # ceiling and flooring heatmap[heatmap > 1] = 1 masked_heatmap = heatmap * mask_valid[:, :, :num_pts] # (height, width, num_pts) background_label = 1 - np.amax(masked_heatmap, axis=2) # (height, width), maximize along the channel axis background_label[background_label < 0] = 0 # (height, width, 1) masked_heatmap = np.concatenate((masked_heatmap, np.expand_dims(background_label, axis=2)), axis=2).astype('float32') return masked_heatmap, mask_valid, mask_visible
def data_normalize(input_data, method='max', data_range=None, sum=1, warning=True, debug=True): ''' this function normalizes N-d data in different ways: 1) normalize the data from a range to [0, 1]; 2) normalize the data which sums to a value parameters: input_data: a list or a numpy N-d data to normalize method: max: normalize the data from a range to [0, 1], when the range is not given, the max and min are obtained from the data sum: normalize the data such that all elements are summed to a value, the default value is 1 data_range: None or 2-element tuple, list or array sum: a scalar outputs: normalized_data: a float32 numpy array with same shape as the input data ''' np_data = safe_npdata(input_data, warning=warning, debug=debug).astype('float32') if debug: assert isnparray(np_data), 'the input data is not a numpy data' assert method in ['max', 'sum'], 'the method for normalization is not correct' if method == 'max': if data_range is None: max_value, min_value = np.max(np_data), np.min(np_data) else: if debug: assert isrange(data_range), 'data range is not correct' max_value, min_value = data_range[1], data_range[0] elif method == 'sum': if debug: assert isscalar(sum), 'the sum is not correct' max_value, min_value = np.sum(np_data) / sum, 0 normalized_data = (np_data - min_value) / (max_value - min_value ) # normalization return normalized_data
def nparray_resize(input_nparray, resize_factor=None, target_size=None, interp='bicubic', warning=True, debug=True): ''' resize the numpy array given a resize factor (e.g., 0.25), or given a target size (height, width) e.g., the numpy array has 600 x 800: 1. given a resize factor of 0.25 -> results in an image with 150 x 200 2. given a target size of (300, 400) -> results in an image with 300 x 400 note that: resize_factor and target_size cannot exist at the same time parameters: input_nparray: a numpy array resize_factor: a scalar target_size: a list of tuple or numpy array with 2 elements, representing height and width interp: interpolation methods: bicubic or bilinear outputs: resized_nparray: a numpy array ''' np_array = safe_npdata(input_nparray, warning=warning, debug=debug) if debug: assert interp in ['bicubic', 'bilinear'], 'the interpolation method is not correct' assert (resize_factor is not None and target_size is None) or (resize_factor is None and target_size is not None), 'resize_factor and target_size cannot co-exist' if target_size is not None: if debug: assert isimsize(target_size), 'the input target size is not correct' target_width, target_height = int(round(target_size[1])), int(round(target_size[0])) elif resize_factor is not None: if debug: assert isscalar(resize_factor), 'the resize factor is not a scalar' height, width = np_array.shape[:2] target_width, target_height = int(round(resize_factor * width)), int(round(resize_factor * height)) else: assert False, 'the target_size and resize_factor do not exist' if interp == 'bicubic': resized_nparray = cv2.resize(np_array, (target_width, target_height), interpolation = cv2.INTER_CUBIC) elif interp == 'bilinear': resized_nparray = cv2.resize(np_array, (target_width, target_height), interpolation = cv2.INTER_LINEAR) else: assert False, 'interpolation is wrong' return resized_nparray
def image_rotate(input_image, input_angle, warning=True, debug=True): ''' rotate the image given an angle in degree (e.g., 90). The rotation is counter-clockwise parameters: input_image: an pil or numpy image input_angle: a scalar, counterclockwise rotation in degree outputs: rotated_image: a numpy uint8 image ''' if debug: assert isscalar(input_angle), 'the input angle is not a scalar' rotation_angle = safe_angle(input_angle, warning=warning, debug=True) # ensure to be in [-180, 180] np_image, _ = safe_image(input_image, warning=warning, debug=debug) if input_angle == 0: return np_image if isfloatimage(np_image): np_image = (np_image * 255.).astype('uint8') pil_image = Image.fromarray(np_image) if rotation_angle != 0: pil_image = pil_image.rotate(rotation_angle, expand=True) rotated_image = np.array(pil_image).astype('uint8') return rotated_image
def image_concatenate(input_image, target_size=[1600, 2560], grid_size=None, edge_factor=0.99, fill_value=0, warning=True, debug=True): ''' concatenate a list of images automatically parameters: input_image: NHWC numpy image, uint8 or float32 target_size: a tuple or list or numpy array with 2 elements, for [H, W] grid_size: a tuple or list or numpy array with 2 elements, for [num_rows, num_cols] edge_factor: the margin between images after concatenation, bigger, the edge is smaller, [0, 1] fill_value: float between 0-1 to fill the gap outputs: image_merged: CHW uint8 numpy image with size of target_size ''' np_image, _ = safe_batch_image(input_image, warning=warning, debug=debug) if debug: assert isimsize(target_size), 'the input image size is not correct' if grid_size is not None: assert isimsize(grid_size), 'the input grid size is not correct' assert isscalar( edge_factor ) and edge_factor <= 1 and edge_factor >= 0, 'the edge factor is not correct' num_images = np_image.shape[0] if grid_size is None: num_rows = int(np.sqrt(num_images)) num_cols = int(np.ceil(num_images * 1.0 / num_rows)) else: num_rows, num_cols = np.ceil(grid_size[0]), np.ceil(grid_size[1]) window_height, window_width = target_size[0], target_size[1] grid_height = int(window_height / num_rows) grid_width = int(window_width / num_cols) im_height = int(grid_height * edge_factor) im_width = int(grid_width * edge_factor) im_channel = np_image.shape[-1] # concatenate image_merged = np.zeros((window_height, window_width, im_channel), dtype='uint8') image_merged.fill(fill_value) for image_index in range(num_images): image_tmp = np_image[image_index, :, :, :] image_tmp = image_resize(image_tmp, target_size=(im_height, im_width), warning=warning, debug=debug) rows_index = int(np.ceil((image_index + 1.0) / num_cols)) # 1-indexed cols_index = int(image_index + 1 - (rows_index - 1) * num_cols) # 1-indexed rows_start = int((rows_index - 1) * grid_height) # 0-indexed rows_end = int(rows_start + im_height) # 0-indexed cols_start = int((cols_index - 1) * grid_width) # 0-indexed cols_end = int(cols_start + im_width) # 0-indexed image_merged[rows_start:rows_end, cols_start:cols_end, :] = image_tmp.reshape( (im_height, im_width, im_channel)) return image_merged
def image_resize(input_image, resize_factor=None, target_size=None, interp='bicubic', warning=True, debug=True): ''' resize the image given a resize factor (e.g., 0.25), or given a target size (height, width) e.g., the input image has 600 x 800: 1. given a resize factor of 0.25 -> results in an image with 150 x 200 2. given a target size of (300, 400) -> results in an image with 300 x 400 note that: resize_factor and target_size cannot exist at the same time parameters: input_image: an pil or numpy image resize_factor: a scalar target_size: a list of tuple or numpy array with 2 elements, representing height and width interp: interpolation methods: bicubic or bilinear outputs: resized_image: a numpy uint8 image ''' np_image, _ = safe_image(input_image, warning=warning, debug=debug) if isfloatimage(np_image): np_image = (np_image * 255.).astype('uint8') if debug: assert interp in ['bicubic', 'bilinear' ], 'the interpolation method is not correct' assert (resize_factor is not None and target_size is None) or ( resize_factor is None and target_size is not None), 'resize_factor and target_size cannot co-exist' if target_size is not None: if debug: assert isimsize( target_size), 'the input target size is not correct' target_width, target_height = int(round(target_size[1])), int( round(target_size[0])) if target_width == np_image.shape[ 1] and target_height == np_image.shape[0]: return np_image elif resize_factor is not None: if debug: assert isscalar( resize_factor ) and resize_factor > 0, 'the resize factor is not a scalar' if resize_factor == 1: return np_image # no resizing height, width = np_image.shape[:2] target_width, target_height = int(round(resize_factor * width)), int( round(resize_factor * height)) else: assert False, 'the target_size and resize_factor do not exist' if interp == 'bicubic': resized_image = cv2.resize(np_image, (target_width, target_height), interpolation=cv2.INTER_CUBIC) elif interp == 'bilinear': resized_image = cv2.resize(np_image, (target_width, target_height), interpolation=cv2.INTER_LINEAR) else: assert False, 'interpolation is wrong' return resized_image
def image_find_peaks(input_image, percent_threshold=0.5, warning=True, debug=True): ''' this function find all strict local peaks and a strict global peak from a grayscale image the strict local maximum means that the pixel value must be larger than all nearby pixel values parameters: input_image: a pil or numpy grayscale image percent_threshold: determine to what pixel value to be smoothed out. e.g., when 0.4, all pixel values less than 0.4 * np.max(input_image) are smoothed out to be 0 outputs: peak_array: a numpy float32 array, 3 x num_peaks, (x, y, score) peak_global: a numpy float32 array, 3 x 1: (x, y, score) ''' np_image, _ = safe_image_like(input_image, warning=warning, debug=debug) if isuintimage(np_image): np_image = np_image.astype('float32') / 255. if debug: assert isgrayimage_dimension(np_image) and isfloatimage( np_image), 'the input image is not a grayscale and float image' assert isscalar( percent_threshold ) and percent_threshold >= 0 and percent_threshold <= 1, 'the percent_threshold is not correct' max_value = np.max(np_image) np_image[np_image < percent_threshold * max_value] = 0.0 height, width = np_image.shape[0], np_image.shape[1] npimage_center, npimage_top, npimage_bottom, npimage_left, npimage_right = np.zeros( [height + 2, width + 2]), np.zeros([height + 2, width + 2]), np.zeros([ height + 2, width + 2 ]), np.zeros([height + 2, width + 2]), np.zeros([height + 2, width + 2]) # shift in different directions to find local peak, only works for convex blob npimage_center[1:-1, 1:-1] = np_image npimage_left[1:-1, 0:-2] = np_image npimage_right[1:-1, 2:] = np_image npimage_top[0:-2, 1:-1] = np_image npimage_bottom[2:, 1:-1] = np_image # compute pixels larger than its shifted version of heatmap right_bool = npimage_center > npimage_right left_bool = npimage_center > npimage_left bottom_bool = npimage_center > npimage_bottom top_bool = npimage_center > npimage_top # the strict local maximum must be bigger than all nearby pixel values peakMap = np.logical_and( np.logical_and(np.logical_and(right_bool, left_bool), top_bool), bottom_bool) peakMap = peakMap[1:-1, 1:-1] peak_location_tuple = np.nonzero(peakMap) # find true num_peaks = len(peak_location_tuple[0]) if num_peaks == 0: if warning: print('No single local peak found') return np.zeros((3, 0), dtype='float32'), np.zeros((3, 0), dtype='float32') # convert to a numpy array format peak_array = np.zeros((3, num_peaks), dtype='float32') peak_array[0, :], peak_array[ 1, :] = peak_location_tuple[1], peak_location_tuple[0] for peak_index in range(num_peaks): peak_array[2, peak_index] = np_image[int(peak_array[1, peak_index]), int(peak_array[0, peak_index])] # find the global peak from all local peaks global_peak_index = np.argmax(peak_array[2, :]) peak_global = peak_array[:, global_peak_index].reshape((3, 1)) return peak_array, peak_global