def remove_background( *, image: Image, smooth_kernel_size: int = 2, threshold_value: int = 100, background_kernel_size: int = 5, debug: bool = False ) -> None: """Remove the background milimeter pattern.""" if debug: debug_path = get_debug_path("remove_background") save(np.ndarray, debug_path, "input") image.invert() # We want the main features to be white image.blur(smooth_kernel_size) image.threshold(threshold_value) # Removes most of the milimiter markers remove_structured_background( image_array=image.image, background_kernel_size=(background_kernel_size, background_kernel_size), smoothing_kernel_size=(smooth_kernel_size, smooth_kernel_size), debug=debug ) if debug: save(np.ndarray, debug_path, "output") image.invert() image.checkpoint("preprocessed")
def remove_background( *, image: Image, smooth_kernel_size: int = 2, threshold_value: int = 100, background_kernel_size: int = 5, ) -> None: image.invert() # We want the main features to be white image.blur(smooth_kernel_size) image.threshold(threshold_value) # Removes most of the milimiter markers remove_structured_background( image_array=image.image, background_kernel_size=(background_kernel_size, background_kernel_size), smoothing_kernel_size=(smooth_kernel_size, smooth_kernel_size)) image.invert() image.checkpoint("preprocessed")
def remove_contours(image: Image, contours: tp.Sequence[np.ndarray], fill_value: int = None): """Remove the pixels inside the contours.""" if len(contours) == 0: assert False, "No contours" shape = m, n, *_ = image.image.shape _fill_value = fill_value if _fill_value is None: _fill_value = np.argmax(np.bincount(image.image.ravel())) image.image = cv2.drawContours(image.image, contours, -2, int(_fill_value), cv2.FILLED)
def split_image(image: Image, *, kernel_length: int = 5, blur_kernel_size: int = 9, threshold_value: float = 150, num_iterations: int = 4) -> tp.List[Image]: """Split an EEG scan into manageable parts. Detect the black square fiduciary markers and split the scan into parts such that each part contains two such markers. kernel_length: Govern how aggressive to be when removing horisontal and vertical background structures. blur_kernel_size: The size of the blur kernel for the initial blur then threshold operation. threshold_value: Thresholding is done right after the blurring. num_iterations: Number of iterations in the erode and dilate transformations. """ image.bgr_to_gray() rectangles = markers( image, kernel_length=kernel_length, blur_kernel_size=blur_kernel_size, threshold_value=threshold_value, num_iterations=num_iterations, ) rectangle_array = np.zeros((len(rectangles), 4, 2)) for i, rec in enumerate(rectangles): for n, vertex in enumerate(rec): rectangle_array[i, n, :] = vertex[0] centres = np.mean(rectangle_array, axis=1) max_dx = np.max(centres[:, 0]) max_dy = np.max(centres[:, 1]) new_image_list = [] horisontal_image = max_dx >= max_dy if horisontal_image: sorted_indices = np.argsort(centres[:, 0]) sorted_rectangle_vertices = rectangle_array[sorted_indices, :, 0] else: sorted_indices = np.argsort(centres[:, 1]) sorted_rectangle_vertices = rectangle_array[sorted_indices, :, 1] rectangle_indices = np.arange(2) for i in range(sorted_rectangle_vertices.shape[0] - 1): square1, square2 = sorted_rectangle_vertices[rectangle_indices] min_index = math.floor(min(map(np.min, (square1, square2)))) max_index = math.ceil(max(map(np.max, (square1, square2)))) if i == 0: min_index = 0 if i == sorted_rectangle_vertices.shape[0] - 2: max_index = None # include the last index if horisontal_image: new_image = image.image[:, min_index:max_index] else: new_image = image.image[min_index:max_index, :] new_image_list.append(Image(new_image)) rectangle_indices += 1 return new_image_list
def markers(image: Image, *, kernel_length: int = 5, blur_kernel_size: int = 9, threshold_value: float = 150, num_iterations: int = 4, debug: bool = False) -> tp.List[np.ndarray]: """Return the contours of the black square markers.""" from .plots import plot # cv2.imwrite("foo.png", image.image) # assert False, image.image.dtype if debug: debug_path = get_debug_path("markers") save(image.image, debug_path, "input") assert len(image.image.shape) == 2, f"Expecting binary image" image.invert() image.blur(blur_kernel_size) image.threshold(threshold_value) if debug: save(image.image, debug_path, "threshold") if kernel_length > 0 and num_iterations > 0: vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_length)) horisontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_length, 1)) # vertial lines vertical_image = cv2.erode(image.image, vertical_kernel, iterations=num_iterations) cv2.dilate(vertical_image, vertical_kernel, iterations=num_iterations, dst=vertical_image) # Horisontal lines horisontal_image = cv2.erode(image.image, horisontal_kernel, iterations=num_iterations) cv2.dilate(image.image, horisontal_kernel, iterations=num_iterations, dst=vertical_image) # Compute intersection of horisontal and vertical cv2.bitwise_and(horisontal_image, vertical_image, dst=image.image) contours = get_contours(image=image, min_size=4) if debug: image_draw = Image(image.copy_image()) image_draw.gray_to_bgr() image_draw = cv2.drawContours(image_draw.image, contours, -2, (0, 255, 0), 2) save(image_draw, debug_path, "morphed") features = match_contours(matcher=get_marker_matcher(image=image), contours=contours) if debug: image_draw = Image(image.copy_image()) image_draw.gray_to_bgr() image_draw = cv2.drawContours(image_draw.image, features, -2, (0, 0, 255), 2) save(image_draw, debug_path, "features") image.reset_image() return features
image_copy = image.image.copy() image.gaussian_blur(5) image.threshold() features = match_contours(matcher=image.match_graph_candidate, contours=contours) image.filter_contours(features) image.blur(3) contours = cv2.findContours(blur, contour_mode, contour_method) contours = imutils.grab_contours(contours) contours = list(filter(lambda c: c.size > 6, contours)) features = match_contours(matcher=image.match_graph_candidate, contours=contours) # Restore colors image.reset_image() image.bgr_to_gray() image.filter_contours(features) image.gray_to_bgr() image.invert() image.draw(features, image.image) if __name__ == "__main__": image = Image() filepath = Path("../data/scan1.png") image.read_image(filepath) preprocess(image) markers(image) graphs(image)
def run( *, input_image_path: Path, output_directory: Path, identifier: str, scale: float = None, debug: bool = False, ): """Segment the contours from a black and white image and save the segmented lines.""" image = read_image(input_image_path) if debug: debug_path = get_debug_path("remove_background") save(image.image, debug_path, "input") image.bgr_to_gray() if debug: save(image.iamge, debug_path, "match_contours") contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(), contours=contours) if debug: debug_path = get_debug_path("extract_contours_bw") save(image.image, debug_path, "input") output_directory.mkdir(exist_ok=True, parents=True) for i, c in enumerate(features): tmp_image = image.copy_image() filter_contours(image_array=tmp_image, contours=[c]) clipped_contour = tmp_image[~np.all(tmp_image == 0, axis=1)] save_image(output_directory / f"{identifier}_trace{i}.png", Image(clipped_contour)) ############################ ### Make annotated image ### ############################ fig, ax = plt.subplots(1, figsize=(15, 10), dpi=500) tmp_image = image.image_orig color_iterator = itertools.cycle(mtableau_brg()) for i, c in enumerate(features): color = next(color_iterator) tmp_image = cv2.drawContours(tmp_image, features, i, color_to_256_RGB(color), cv2.FILLED) polygon = Polygon(c.reshape(-1, 2)) x0, y0, x1, y1 = polygon.bounds ann = ax.annotate( f"Contour {i}", xy=(x1, y1), # (x0, y1) xycoords="data", xytext=(0, 35), textcoords="offset points", size=10, bbox=dict( boxstyle="round", fc=color # normalised color )) ax.imshow(tmp_image) ax.set_title("A digitised paper strip") if scale is not None: # multiply by distance between black sqaures? ax.set_xticklabels( ["{:.1f} cm".format(15 * i / scale) for i in ax.get_xticks()]) ax.set_yticklabels( ["{:.1f} cm".format(15 * i / scale) for i in ax.get_yticks()]) ax.set_ylabel("Voltage") ax.set_xlabel("Time") fig.savefig(output_directory / f"{identifier}_annotated.png", dpi=500)
def extract_contours( *, image: Image, blur_kernel_size: int = 3, dilate_kernel_size: int = 3, num_dilate_iterations: int = 3) -> tp.Sequence[np.ndarray]: # Remove initial guess at contours. This should leave the text blocks. image.invert() contours = get_contours(image=image) features = match_contours( matcher=get_graph_matcher(approximation_tolerance=1e-2), contours=contours) remove_contours(image, features) # Remove all the remaining text blobs image.blur(blur_kernel_size) image.threshold(100) image.morph(cv2.MORPH_DILATE, (dilate_kernel_size, dilate_kernel_size), num_dilate_iterations) contours = get_contours(image=image, min_size=6) # Compute all the bounding boxes and filter based on the aspect ratio? features = match_contours( matcher=get_bounding_rectangle_matcher(min_solidity=0.7), contours=contours) filter_contours(image_array=image.image, contours=features) image.morph(cv2.MORPH_DILATE, (dilate_kernel_size, dilate_kernel_size), num_dilate_iterations) image_mask = image.copy_image() image.reset_image("preprocessed") filter_image(image_array=image.image, binary_mask=image_mask == 255) image.checkpoint("preprocessed") # Match the remaining graph candidates, and remove everything else image.invert() image.blur(blur_kernel_size) image.threshold() image.morph(cv2.MORPH_DILATE, (dilate_kernel_size, dilate_kernel_size), num_dilate_iterations) contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(), contours=contours) filter_contours(image_array=image.image, contours=features) image.reset_image("resampled") # TODO: Why invert? image.invert() filter_contours(image_array=image.image, contours=features) image.invert() image.blur(blur_kernel_size) image.threshold(100) image.invert() contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(), contours=contours) return features
def markers(image: Image, blur_kernel_size=9, kernel_length=5): image.bgr_to_gray() image.invert() image.blur(blur_kernel_size) image.threshold(150) vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_length)) horisontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_length, 1)) # vertial lines vertical_image = cv2.erode(image.image, vertical_kernel, iterations=4) cv2.dilate(vertical_image, vertical_kernel, iterations=4, dst=vertical_image) # Horisontal lines horisontal_image = cv2.erode(image.image, horisontal_kernel, iterations=4) cv2.dilate(image.image, horisontal_kernel, iterations=4, dst=vertical_image) # Compute intersection of horisontal and vertical cv2.bitwise_and(horisontal_image, vertical_image, dst=image.image) contours = get_contours(image=image) features = match_contours(matcher=get_marker_matcher(image=image), contours=contours) axis, scale = get_axis(image, features) image.set_axis(axis) image.set_scale(scale)
def extract_contours( *, image: Image, blur_kernel_size: int = 3, dilate_kernel_size: int = 3, num_dilate_iterations: int= 3, debug: bool = True ) -> tp.List[np.ndarray]: """Segment the EEG traces. Use a series of convolutions, filtering and edge tracking to extract the contours. blur_kernel_size: Used in conjunction with thresholding to binarise the image. dilate_kernel_size: Dilation is used with filtering to be aggressive in removing elements. num_dilate_iterations: THe number of dilate iterations. """ if debug: debug_path = get_debug_path("extract_contours") save(np.ndarray, debug_path, "input") # Remove initial guess at contours. This should leave the text blocks. image.invert() contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(approximation_tolerance=1e-2), contours=contours) remove_contours(image, features) if debug: save(np.ndarray, debug_path, "remove_contours") # Remove all the remaining text blobs image.blur(blur_kernel_size) image.threshold(100) image.morph(cv2.MORPH_DILATE, (dilate_kernel_size, dilate_kernel_size), num_dilate_iterations) contours = get_contours(image=image, min_size=6) # Compute all the bounding boxes and filter based on the aspect ratio? features = match_contours( matcher=get_bounding_rectangle_matcher(min_solidity=0.7), contours=contours ) filter_contours(image_array=image.image, contours=features) image.morph(cv2.MORPH_DILATE, (dilate_kernel_size, dilate_kernel_size), num_dilate_iterations) image_mask = image.copy_image() image.reset_image("preprocessed") filter_image(image_array=image.image, binary_mask=image_mask == 255) image.checkpoint("preprocessed") if debug: save(np.ndarray, debug_path, "filter_contours1") # Match the remaining graph candidates, and remove everything else image.invert() image.blur(blur_kernel_size) image.threshold() image.morph(cv2.MORPH_DILATE, (dilate_kernel_size, dilate_kernel_size), num_dilate_iterations) contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(), contours=contours) filter_contours(image_array=image.image, contours=features) if debug: save(np.ndarray, debug_path, "filter_contours2") image.reset_image("resampled") # TODO: Why invert? Has something to do with the fill color in filter_contours image.invert() filter_contours(image_array=image.image, contours=features) image.invert() if debug: save(np.ndarray, debug_path, "filter_contours3") image.blur(blur_kernel_size) image.threshold(100) image.invert() contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(), contours=contours) return features
def run( input_image_path: Path, output_directory: Path, identifier: str, scale: float = None, debug:bool ): """Remove the background, segment the contours and save the segmented lines as pngs.""" image = read_image(input_image_path) if debug: debug_path = get_debug_path("prepare_lines") save(np.ndarray, debug_path, "input") image.bgr_to_gray() image.checkpoint("resampled") remove_background(image=image, smooth_kernel_size=3) features = extract_contours(image=image, blur_kernel_size=3) if debug: save(np.ndarray, debug_path, "remove_background") image.invert() image.reset_image("resampled") image.invert() output_directory.mkdir(exist_ok=True, parents=True) for i, c in enumerate(features): tmp_image = image.copy_image() filter_contours(image_array=tmp_image, contours=[c]) clipped_contour = tmp_image[~np.all(tmp_image == 0, axis=1)] save_image(output_directory / f"{identifier}_trace{i}.png", Image(clipped_contour)) ################## ### Make image ### ################## tmp_image = np.ones((*image.image.shape, 3), dtype=np.uint8) tmp_image[:] = (255, 255, 255) # White fig, ax = plt.subplots(1) ax.imshow(tmp_image) color_iterator = itertools.cycle(mtableau_brg()) for i, c in enumerate(features): color = next(color_iterator) tmp_image = cv2.drawContours(tmp_image, features, i, color_to_256_RGB(color), cv2.FILLED) polygon = Polygon(c.reshape(-1, 2)) x0, y0, x1, y1 = polygon.bounds tmp_image2 = np.zeros(tuple(map(math.ceil, (y1, x1))), dtype=np.uint8) tmp_image2 = cv2.drawContours(tmp_image2, features, i, 255, cv2.FILLED) ann = ax.annotate( f"Contour {i}", xy=(x0, y1), xycoords="data", xytext=(0, 35), textcoords="offset points", size=10, bbox=dict( boxstyle="round", fc=color # normalised color ) ) ax.imshow(tmp_image) ax.set_title("A digitised paper strip") if scale is not None: # multiply by distance between black sqaures? ax.set_xticklabels(["{:.1f} cm".format(15*i/scale) for i in ax.get_xticks()]) ax.set_yticklabels(["{:.1f} cm".format(15*i/scale) for i in ax.get_yticks()]) ax.set_ylabel("Voltage") ax.set_xlabel("Time") fig.savefig(output_directory / f"{identifier}_annotated.png")
def run(*, input_image_path: Path, output_directory: Path, identifier: str, smooth_kernel_size: int = 3, threshold_value: int = 100, background_kernel_size=5, scale: float = None, debug: bool = False, blue_color_filter: tp.Sequence[int] = None, red_color_filter: tp.Sequence[int] = None, horisontal_kernel_length: int = None, x_interval: tp.Tuple[int, int] = None): """Remove the background, segment the contours and save the segmented lines as pngs.""" image = read_image(input_image_path) if debug: debug_path = get_debug_path("prepare_lines") save(image.image, debug_path, "input") if blue_color_filter is not None: lower = tuple(map(int, blue_color_filter[:3])) upper = tuple(map(int, blue_color_filter[3:])) print(lower, upper) blue_mask = cv2.inRange(image.image, lower, upper) image.image[blue_mask == 255] = 255 if red_color_filter is not None: lower = tuple(map(int, red_color_filter[:3])) upper = tuple(map(int, red_color_filter[3:])) red_mask = cv2.inRange(image.image, lower, upper) image.image[red_mask == 255] = 255 image.invert() image.bgr_to_gray() image.checkpoint("resampled") if horisontal_kernel_length is not None: horisontal_kernel = cv2.getStructuringElement( cv2.MORPH_RECT, (horisontal_kernel_length, 1)) x0, x1 = x_interval if x1 == -1: # Set -1 to be inclusive last element x1 = None detected_lines = cv2.morphologyEx(image.image[x0:x1, :], cv2.MORPH_OPEN, horisontal_kernel, iterations=1) # findContours returns (contours, hierarchy) contours = cv2.findContours(detected_lines, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] for c in contours: cv2.drawContours(image.image[x0:x1, :], [c], -1, 0, -10) remove_background(image=image, smooth_kernel_size=smooth_kernel_size, threshold_value=threshold_value, background_kernel_size=background_kernel_size, debug=debug) image.checkpoint("remove_background") if debug: save(image.image, debug_path, "match_contours") contours = get_contours(image=image) features = match_contours(matcher=get_graph_matcher(), contours=contours) if features is None: # In case of a blank image return quality_control_image = image.draw(features, show=False) cv2.imwrite(str(output_directory / f"QC_{identifier}.png"), quality_control_image) if debug: save(image.image, debug_path, "remove_background") # image.reset_image("resampled") image.reset_image("remove_background") # image.invert() output_directory.mkdir(exist_ok=True, parents=True) for i, c in enumerate(features): tmp_image = image.copy_image() filter_contours(image_array=tmp_image, contours=[c]) clipped_contour = tmp_image[~np.all(tmp_image == 0, axis=1)] save_image(output_directory / f"{identifier}_trace{i}.png", Image(clipped_contour)) ############################ ### Make annotated image ### ############################ fig, ax = plt.subplots(1, figsize=(15, 10), dpi=500) tmp_image = image.image_orig color_iterator = itertools.cycle(mtableau_brg()) for i, c in enumerate(features): color = next(color_iterator) tmp_image = cv2.drawContours(tmp_image, features, i, color_to_256_RGB(color), cv2.FILLED) polygon = Polygon(c.reshape(-1, 2)) x0, y0, x1, y1 = polygon.bounds ann = ax.annotate( f"Contour {i}", xy=((x0 + x1) / 2, y1), # (x0, y1) size=5, color=color, arrowprops=dict(arrowstyle='->', connectionstyle="arc3,rad=-0.5", color=color)) ax.imshow(tmp_image) ax.set_title("A digitised paper strip") if scale is not None: # multiply by distance between black sqaures? ax.set_xticklabels( ["{:.1f} cm".format(15 * i / scale) for i in ax.get_xticks()]) ax.set_yticklabels( ["{:.1f} cm".format(15 * i / scale) for i in ax.get_yticks()]) ax.set_ylabel("Voltage") ax.set_xlabel("Time") fig.savefig(output_directory / f"{identifier}_annotated.png", dpi=500)