def voxel_matches_polygon(self, coordinate_list): for voxel_coords in coordinate_list: voxel_coords = np.asarray(voxel_coords) rmax = voxel_coords[:, 0].max() rmin = voxel_coords[:, 0].min() zmax = voxel_coords[:, 1].max() zmin = voxel_coords[:, 1].min() router = 1.5 * rmax rinner = 0.5 * rmin zupper = 1.5 * zmax if zmax > 0 else 0.5 * zmax zlower = 0.5 * zmin if zmin > 0 else 1.5 * zmin test_rs = np.linspace(rinner, router, int(50 * (router - rinner))) test_zs = np.linspace(zlower, zupper, int(50 * (zupper - zlower))) # Test for 0 area: not supported by mesh representation x, y = voxel_coords.T area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) if area == 0: continue voxel = AxisymmetricVoxel(voxel_coords, primitive_type='mesh') polygon = Polygon(voxel_coords, closed=True).get_path() test_verts = list(itertools.product(test_rs, test_zs)) inside_poly = polygon.contains_points(test_verts) inside_voxel = [any(child.contains(Point3D(r, 0, z)) for child in voxel.children) for (r, z) in test_verts] # Due to numerical precision, some points may be inside the # Matplotlib polygon but not the Mesh. Check in this case that the # "failing" points are just very close to the edge of the polygon fails = np.nonzero(np.not_equal(inside_voxel, inside_poly))[0] for fail in fails: if inside_voxel[fail] and not inside_poly[fail]: # Polygon should be made slightly bigger inside_poly[fail] = polygon.contains_point(test_verts[fail], radius=-0.01) elif inside_poly[fail] and not inside_voxel[fail]: # Polygon should be made slightly smaller inside_poly[fail] = polygon.contains_point(test_verts[fail], radius=0.01) self.assertSequenceEqual(inside_voxel, inside_poly.tolist(), "Failed for vertices {}".format(voxel_coords))
def find_glass_designation(nd, vd): """ find the designation and rgb color for the input index and V number Args: nd: refractive index, d line vd: V-number, d line Returns: designation label, RGBA list """ for key, poly in polygons.items(): p = Polygon(poly, closed=True) if p.contains_point([vd, nd]): c = rgb[key] return key, c return None, None
def convert_contour_data_to_roi_indices(contour_data, aff, shape, radius=None, use_contour_orientation=True): """Convert RTSTRUCT 2D polygon contour data to 3D indices Parameters ---------- contour_data : list of contour data as returned from read_rtstruct_contour_data() aff: 2d 4x4 numpy array affine matrix that maps from voxel to world coordinates of volume where ROIs should be applied shape : 3 element tuple shape of the of volume where ROIs should be applied radius : float, optional passed to matplotlib.patches.Polygon.contains_point() use_contour_orientation: bool whether to use the orientation of a contour (clockwise vs counter clockwise) to determine whether a contour defines a ROI or a holes "within" an ROI. This approach is used by some vendors to store "holes" in 2D slices of 3D segmentations. Returns ------- list containing the voxel indices of all ROIs Note ---- (1) matplotlib.patches.Polygon.contains_point() is used to determine whether a voxel is inside a 2D RTSTRUCT polygon. There is ambiguity for voxels that only lie partly inside the polygon. Example ------- dcm = pymirc.fileio.DicomVolume('mydcm_dir/*.dcm') vol = dcm.get_data() contour_data = pymirc.fileio.read_rtstruct_contour_data('my_rtstruct_file.dcm') roi_inds = pymirc.fileio.convert_contour_data_to_roi_indices(contour_data, dcm.affine, vol.hape) print('ROI name.....:', [x['ROIName'] for x in contour_data]) print('ROI number...:', [x['ROINumber'] for x in contour_data]) print('ROI mean.....:', [vol[x].mean() for x in roi_inds]) """ roi_inds = [] for iroi in range(len(contour_data)): contour_points = contour_data[iroi]['contour_points'] contour_orientations = np.array( contour_data[iroi]['contour_orientations']) roi_number = int(contour_data[iroi]['Number']) roi_inds0 = [] roi_inds1 = [] roi_inds2 = [] # calculate the slices of all contours sls = np.array([ int(round( (np.linalg.inv(aff) @ np.concatenate([x[0, :], [1]]))[2])) for x in contour_points ]) sls_uniq = np.unique(sls) for sl in sls_uniq: sl_inds = np.where(sls == sl)[0] if np.any(np.logical_not(contour_orientations[sl_inds])): # case where we have negative contours (holes) in the slices bin_img = np.zeros(shape[:2], dtype=np.int16) for ip in sl_inds: cp = contour_points[ip] # get the minimum and maximum voxel coordinate of the contour in the slice i_min = np.floor((np.linalg.inv(aff) @ np.concatenate( [cp.min(axis=0), [1]]))[:2]).astype(int) i_max = np.ceil((np.linalg.inv(aff) @ np.concatenate( [cp.max(axis=0), [1]]))[:2]).astype(int) n_test = i_max + 1 - i_min poly = Polygon(cp[:, :-1], True) contour_orientation = contour_orientations[ip] for i in np.arange(i_min[0], i_min[0] + n_test[0]): for j in np.arange(i_min[1], i_min[1] + n_test[1]): if poly.contains_point( (aff @ np.array([i, j, sl, 1]))[:2], radius=radius): if use_contour_orientation: if contour_orientation: bin_img[i, j] += 1 else: bin_img[i, j] -= 1 else: bin_img[i, j] += 1 inds0, inds1 = np.where(bin_img > 0) inds2 = np.repeat(sl, len(inds0)) roi_inds0 = roi_inds0 + inds0.tolist() roi_inds1 = roi_inds1 + inds1.tolist() roi_inds2 = roi_inds2 + inds2.tolist() else: # case where we don't have negative contours (holes) in the slices for ip in sl_inds: cp = contour_points[ip] # get the minimum and maximum voxel coordinate of the contour in the slice i_min = np.floor((np.linalg.inv(aff) @ np.concatenate( [cp.min(axis=0), [1]]))[:2]).astype(int) i_max = np.ceil((np.linalg.inv(aff) @ np.concatenate( [cp.max(axis=0), [1]]))[:2]).astype(int) n_test = i_max + 1 - i_min poly = Polygon(cp[:, :-1], True) for i in np.arange(i_min[0], i_min[0] + n_test[0]): for j in np.arange(i_min[1], i_min[1] + n_test[1]): if poly.contains_point( (aff @ np.array([i, j, sl, 1]))[:2], radius=radius): roi_inds0.append(i) roi_inds1.append(j) roi_inds2.append(sl) roi_inds.append( (np.array(roi_inds0), np.array(roi_inds1), np.array(roi_inds2))) return roi_inds
def is_point_in_poly(point_x, point_y, polygon: Polygon): """Returns True if received polygon object contains received point, False otherwise""" return polygon.contains_point([point_x, point_y])
def main(): ok_txts_cnt = 0 empty_txts_cnt = 0 missed_txts_cnt = 0 damaged_txts_cnt = 0 regions_outside_cnt = 0 # total regions outside regions_outside_files_cnt = 0 # total files with regions outside # create output dir; nested dirs should be passed sequentially, each as separate parameter utility.create_directories(OUTPUT_DIR, OUTPUT_EMPTY_DIR, OUTPUT_DAMAGED_DIR, OUTPUT_REGS_OUTSIDE_DIR) slash = utility.get_path_slash() # create images list img_files_paths = [] for extension in IMAGES_EXTENSIONS: img_files_paths += glob.glob(INPUT_DIR + "*" + extension) # check if there any images if len(img_files_paths) == 0: input( INPUT_DIR + " contains no files (images) with specified in settings extensions. Press enter to exit:" ) exit(0) print("Current progress print frequency is 1 per", PROGRESS_PRINT_RATE, "image(s).") print("Starting processing", len(img_files_paths), "images...") # do cropping and regions conversion for each image file for cur_file_no, in_img_path in enumerate(img_files_paths, start=1): # load image and do some pre calculations in_img = cv.imread(in_img_path) in_img_w, in_img_h = in_img.shape[1], in_img.shape[0] # make paths in_txt_path = in_img_path[:in_img_path.rfind(".")] + ".txt" out_img_path_ok = OUTPUT_DIR + in_img_path.split(slash)[-1] out_txt_path_ok = OUTPUT_DIR + in_txt_path.split(slash)[-1] out_img_path_empty = OUTPUT_EMPTY_DIR + in_img_path.split(slash)[-1] out_txt_path_empty = OUTPUT_EMPTY_DIR + in_txt_path.split(slash)[-1] out_img_path_damaged = OUTPUT_DAMAGED_DIR + in_img_path.split( slash)[-1] out_txt_path_damaged = OUTPUT_DAMAGED_DIR + in_txt_path.split( slash)[-1] out_img_path_reg_out = OUTPUT_REGS_OUTSIDE_DIR + in_img_path.split( slash)[-1] out_txt_path_reg_out = OUTPUT_REGS_OUTSIDE_DIR + in_txt_path.split( slash)[-1] # copy img and txt to output dir if img H==W if in_img_w == in_img_h: # check for txt availability if os.path.isfile(in_txt_path): # check for txt emptiness if len(load_regions(in_txt_path)) == 0: empty_txts_cnt += 1 shutil.copyfile(in_img_path, out_img_path_empty) shutil.copyfile(in_txt_path, out_txt_path_empty) # TODO: a bug: file is passed as ok if it is damaged (regions are present but len of any region is != 5) else: ok_txts_cnt += 1 shutil.copyfile(in_img_path, out_img_path_ok) shutil.copyfile(in_txt_path, out_txt_path_ok) else: missed_txts_cnt += 1 shutil.copyfile(in_img_path, out_img_path_empty) with open(out_txt_path_empty, "w"): pass continue # else process this image out_img_path_cur = out_img_path_ok out_txt_path_cur = out_txt_path_ok side_reduce_val = int(abs(in_img_w - in_img_h) / 2) if in_img_w > in_img_h: out_img = in_img[:, side_reduce_val:-side_reduce_val] out_img_rect_points = [[side_reduce_val, in_img_h], [side_reduce_val, 0], [in_img_w - side_reduce_val, 0], [in_img_w - side_reduce_val, in_img_h]] else: out_img = in_img[side_reduce_val:-side_reduce_val, :] out_img_rect_points = [[0, in_img_h - side_reduce_val], [0, side_reduce_val], [in_img_w, side_reduce_val], [in_img_w, in_img_h - side_reduce_val]] out_img_w, out_img_h = out_img.shape[1], out_img.shape[0] # check for regions txt file availability if not os.path.isfile(in_txt_path): missed_txts_cnt += 1 cv.imwrite(out_img_path_empty, out_img) with open(out_txt_path_empty, "w"): pass continue # load regions and check is txt file empty in_regions = load_regions(in_txt_path) if len(in_regions) == 0: empty_txts_cnt += 1 cv.imwrite(out_img_path_empty, out_img) shutil.copyfile(in_txt_path, out_txt_path_empty) continue out_regions = [] out_img_rect = Polygon(out_img_rect_points) file_contains_out_regs = False # convert regions for in_region in in_regions: # [object_class, x_center, y_center, width, height] in_region_items = [float(item) for item in in_region.split(" ")] # check for region values count if len(in_region_items) != 5: damaged_txts_cnt += 1 cv.imwrite(out_img_path_damaged, out_img) shutil.copyfile(in_txt_path, out_txt_path_damaged) break # convert to px (for input image size) in_reg_x_c_px, in_reg_y_c_px, in_reg_w_px, in_reg_h_px = int(in_region_items[1] * in_img_w), \ int(in_region_items[2] * in_img_h), \ int(in_region_items[3] * in_img_w), \ int(in_region_items[4] * in_img_h) in_reg_left, in_reg_right, in_reg_top, in_reg_bot = int(in_reg_x_c_px - in_reg_w_px / 2), \ int(in_reg_x_c_px + in_reg_w_px / 2), \ int(in_reg_y_c_px - in_reg_h_px / 2), \ int(in_reg_y_c_px + in_reg_h_px / 2) in_reg_px_pts = [(in_reg_left, in_reg_bot), (in_reg_left, in_reg_top), (in_reg_right, in_reg_top), (in_reg_right, in_reg_bot)] # check if px region is not outside cropped image borders for point in in_reg_px_pts: if not out_img_rect.contains_point(point): if not file_contains_out_regs: file_contains_out_regs = True regions_outside_files_cnt += 1 regions_outside_cnt += 1 out_img_path_cur = out_img_path_reg_out out_txt_path_cur = out_txt_path_reg_out break else: # compute output px region if in_img_w > in_img_h: out_reg_left, out_reg_right, out_reg_top, out_reg_bot = in_reg_left - side_reduce_val, \ in_reg_right - side_reduce_val, \ in_reg_top, in_reg_bot else: out_reg_left, out_reg_right, out_reg_top, out_reg_bot = in_reg_left, in_reg_right, \ in_reg_top - side_reduce_val, \ in_reg_bot - side_reduce_val # no need to convert to int as these values will be converted to yolo format which are floats out_reg_x_c_px, out_reg_y_c_px = out_reg_left + in_reg_w_px / 2, out_reg_top + in_reg_h_px / 2 # convert to yolo format x_center, y_center, width, height = round(out_reg_x_c_px / out_img_w, 6), \ round(out_reg_y_c_px / out_img_h, 6), \ round(in_reg_w_px / out_img_w, 6), \ round(in_reg_h_px / out_img_h, 6) # format as text: "object_class_index x_center y_center width height" converted_reg = in_region[0] + " " + str(x_center) + " " + str(y_center) + " " + str(width) + " " + \ str(height) out_regions.append(converted_reg) else: # save regions and image to defined during processing path if out_txt_path_cur == out_txt_path_ok: ok_txts_cnt += 1 cv.imwrite(out_img_path_cur, out_img) with open(out_txt_path_cur, "w") as out_txt_file: for out_region in out_regions: out_txt_file.write(out_region + "\n") # display progress if cur_file_no % PROGRESS_PRINT_RATE == 0: print("Processed", cur_file_no, "of", len(img_files_paths), "images...") print("Processed all of", len(img_files_paths), "images.") print("Ok txts:", ok_txts_cnt, "(replaced to", OUTPUT_DIR, "directory)") print("Empty txts:", empty_txts_cnt, "(replaced to", OUTPUT_EMPTY_DIR, "directory)") print("Missed txts:", missed_txts_cnt, "(replaced to", OUTPUT_EMPTY_DIR, "directory)") print("Damaged txts:", damaged_txts_cnt, "(replaced to", OUTPUT_DAMAGED_DIR, "directory)") print("Files with regions outside:", regions_outside_files_cnt, "(replaced to", OUTPUT_REGS_OUTSIDE_DIR, "directory)") print("Regions outside count:", regions_outside_cnt) input("Done! Press enter to exit:")
def is_point_in_poly(point_x, point_y, polygon: Polygon): return polygon.contains_point([point_x, point_y])
def main(): # check input dirs if not os.path.isfile(INPUT_JSON_FILE): print("Couldn't find json file:", INPUT_JSON_FILE) exit(0) if not os.path.isfile(INPUT_CLASSES_FILE_PATH): print("Couldn't find classes file:", INPUT_CLASSES_FILE_PATH) exit(0) if not os.path.exists(INPUT_DATASET_IMAGES_DIR): print("Couldn't find images directory:", INPUT_DATASET_IMAGES_DIR) exit(0) # define and create output dirs output_jpg_dir = OUTPUT_DATA_DIR + "jpg/" output_txt_dir = OUTPUT_DATA_DIR + "txt/" utility.create_directories(OUTPUT_DATA_DIR, output_jpg_dir, output_txt_dir) # statistics and report data img_processed = 0 regions_processed = 0 regions_converted = 0 unsupported_regions_count = 0 unsupported_regions_files = set() regions_outside_prec_area_count = 0 regions_outside_prec_area_files = set() missing_dataset_images_count = 0 missing_dataset_images_files = set() missing_json_region_types_count = 0 missing_json_region_types_files = set() missing_classes_count = 0 missing_classes = set() w_from = SCENE_CENTER_X - PRECISE_ZONE_LEFT w_to = SCENE_CENTER_X + PRECISE_ZONE_RIGHT h_from = SCENE_CENTER_Y - PRECISE_ZONE_TOP h_to = SCENE_CENTER_Y + PRECISE_ZONE_BOTTOM precise_zone = Polygon([[w_from, h_from], [w_to, h_from], [w_to, h_to], [w_from, h_to]]) output_jpg_dir = OUTPUT_DATA_DIR + "jpg/" # load json with open(INPUT_JSON_FILE, "r") as file: data = json.loads(file.read()) # load classes classes = detection.YoloOpenCVDetection.load_class_names( INPUT_CLASSES_FILE_PATH) # loop over images list in json cur_file_no = 1 for file_key in data["_via_img_metadata"]: if cur_file_no % 10 == 0: print("Processed", cur_file_no, "of", len(data["_via_img_metadata"]), "files.") cur_file_no += 1 file_name = data["_via_img_metadata"][file_key]["filename"] file_path = INPUT_DATASET_IMAGES_DIR + file_name # check if image jpg file exists and is not empty if os.path.isfile(file_path) and os.stat(file_path).st_size > 0: img = cv.imread(file_path) img_processed += 1 else: missing_dataset_images_count += 1 missing_dataset_images_files.add(file_name) continue reg_img_file_counter = 1 # loop over regions of current image file for region in data["_via_img_metadata"][file_key]["regions"]: regions_processed += 1 # check if region type is present if REGION_TYPE_KEY not in region["region_attributes"]: missing_json_region_types_count += 1 missing_json_region_types_files.add(file_name) continue region_type = region["region_attributes"][REGION_TYPE_KEY] # regions whitelist check if APPLY_REGIONS_WHITE_LIST and region_type not in REGIONS_WHITE_LIST: continue # check if type is present in yolo names file if region_type not in classes: missing_classes_count += 1 missing_classes.add(region_type) continue # get region info and convert to rect if needed if region["shape_attributes"]["name"] == "rect": crop_reg_left = int(region["shape_attributes"]["x"]) crop_reg_right = int(region["shape_attributes"]["x"] + region["shape_attributes"]["width"]) crop_reg_top = int(region["shape_attributes"]["y"]) crop_reg_bottom = int(region["shape_attributes"]["y"] + region["shape_attributes"]["height"]) reg_center_x = int(region["shape_attributes"]["x"] + region["shape_attributes"]["width"] / 2) reg_center_y = int(region["shape_attributes"]["y"] + region["shape_attributes"]["height"] / 2) elif region["shape_attributes"]["name"] == "circle": crop_reg_left = int(region["shape_attributes"]["cx"] - region["shape_attributes"]["r"]) crop_reg_right = int(region["shape_attributes"]["cx"] + region["shape_attributes"]["r"]) crop_reg_top = int(region["shape_attributes"]["cy"] - region["shape_attributes"]["r"]) crop_reg_bottom = int(region["shape_attributes"]["cy"] + region["shape_attributes"]["r"]) reg_center_x = int(region["shape_attributes"]["cx"]) reg_center_y = int(region["shape_attributes"]["cy"]) else: # print("Unsupported region shape '", region["shape_attributes"]["name"], "', THIS REGION WON'T BE SHIFTED. See info in file ", MISSED_REGIONS_FILE, sep="") unsupported_regions_count += 1 unsupported_regions_files.add(file_name) continue # precise zone check if APPLY_PRECISE_ZONE and not precise_zone.contains_point( [reg_center_x, reg_center_y]): regions_outside_prec_area_count += 1 regions_outside_prec_area_files.add(file_name) continue # reduce region sizes if allowed if APPLY_REGIONS_SIZE_REDUCING: yolo_reg_left = crop_reg_left + int( crop_reg_left * REGIONS_SIZE_REDUCING_PERCENT / 2) yolo_reg_right = crop_reg_right - int( crop_reg_right * REGIONS_SIZE_REDUCING_PERCENT / 2) yolo_reg_top = crop_reg_top + int( crop_reg_top * REGIONS_SIZE_REDUCING_PERCENT / 2) yolo_reg_bottom = crop_reg_bottom - int( crop_reg_bottom * REGIONS_SIZE_REDUCING_PERCENT / 2) else: yolo_reg_left = crop_reg_left yolo_reg_right = crop_reg_right yolo_reg_top = crop_reg_top yolo_reg_bottom = crop_reg_bottom # crop and save image region reg_img = img[crop_reg_top:crop_reg_bottom, crop_reg_left:crop_reg_right] reg_img_name = file_name[:-4] + " " + str( reg_img_file_counter) + file_name[-4:] cv.imwrite(output_jpg_dir + reg_img_name, reg_img) # convert region to yolo format reg_center_x = int( (yolo_reg_right - yolo_reg_left) / 2) # TODO: a bug - not int not none happens here reg_center_y = int((yolo_reg_bottom - yolo_reg_top) / 2) reg_width = yolo_reg_right - yolo_reg_left reg_height = yolo_reg_bottom - yolo_reg_top img_x_size = crop_reg_right - crop_reg_left img_y_size = crop_reg_bottom - crop_reg_top obj_class = classes.index(region_type) x_center = round(reg_center_x / img_x_size, 6) y_center = round(reg_center_y / img_y_size, 6) width = round(reg_width / img_x_size, 6) height = round(reg_height / img_y_size, 6) # <object-class> <x_center> <y_center> <width> <height> record_line = str(obj_class) + " " + str(x_center) + " " + str(y_center) + " " + str(width) + " " + \ str(height) + "\n" # save converted to yolo region into txt with open( output_txt_dir + file_name[:-4] + " " + str(reg_img_file_counter) + ".txt", "w") as txt_file: txt_file.write(record_line) reg_img_file_counter += 1 regions_converted += 1 current_time = utility.get_current_time() + " " # save passed unsupported regions info if len(unsupported_regions_files) > 0: with open(current_time + UNSUPPORTED_REGIONS_LIST_FILE, "w") as file: file.write( "These images are present in VIA's json project but their regions types are not supported:\n" ) for item in unsupported_regions_files: file.write(item + "\n") # save missing image files (present in json, absent in dir) if len(missing_dataset_images_files) > 0: with open(current_time + MISSING_DATASET_IMAGES_LIST_FILE, "w") as file: file.write( "These images are present in VIA's json project but absent in given images directory (images are required for conversion to YOLO format):\n" ) for item in missing_dataset_images_files: file.write(item + "\n") # save missing in json region types if len(missing_json_region_types_files) > 0: with open(current_time + MISSING_JSON_REGION_TYPES_LIST_FILE, "w") as file: file.write( "These images are added to VIA's json project but their regions have no information about region types:\n" ) for item in missing_json_region_types_files: file.write(item + "\n") # save missing classes in classes file if len(missing_classes) > 0: with open(current_time + MISSING_CLASSES_LIST_FILE, "w") as file: file.write( "These classes are present in given json, but absent in " + INPUT_CLASSES_FILE_PATH + " classes file:\n") for item in missing_classes: file.write(item + "\n") # save list of files with regions outside the precise area if len(regions_outside_prec_area_files) > 0: with open(current_time + REGIONS_OUTSIDE_PRECISE_ZONE_LIST_FILE, "w") as file: file.write( "List of files whose regions were outside of the precise zone (it's ok):\n" ) for item in regions_outside_prec_area_files: file.write(item + "\n") # show report print("Processed", img_processed, "images") print("Processed", regions_processed, "regions") print("Successfully converted", regions_converted, "regions") conversion_failed = False if unsupported_regions_count > 0: conversion_failed = True print("Passed", unsupported_regions_count, "regions due to unsupported shape. See'", UNSUPPORTED_REGIONS_LIST_FILE, "'file for details.") if missing_dataset_images_count > 0: conversion_failed = True print("Missing", missing_dataset_images_count, "images in given images directory. See'", MISSING_DATASET_IMAGES_LIST_FILE, "'file for details.") if missing_json_region_types_count > 0: conversion_failed = True print("Missing", missing_json_region_types_count, "regions types in given json file. See'", MISSING_JSON_REGION_TYPES_LIST_FILE, "'file for details.") if missing_classes_count > 0: conversion_failed = True print("Missing", missing_classes_count, "classes count in given classes file. See'", MISSING_CLASSES_LIST_FILE, "'file for details.") if regions_outside_prec_area_count > 0: print("Passed", regions_outside_prec_area_count, "regions which were outside of precise area (it's ok). See'", REGIONS_OUTSIDE_PRECISE_ZONE_LIST_FILE, "'file for details.") if conversion_failed: print( "Total: conversion is FAILED! See report files mentioned above for details." ) else: print("Total: conversion is SUCCESSFUL!")