diff --git a/docs/_static/images/python_api/back2raw_save.png b/docs/_static/images/python_api/back2raw_save.png new file mode 100644 index 0000000..21d3ada Binary files /dev/null and b/docs/_static/images/python_api/back2raw_save.png differ diff --git a/docs/_static/images/python_api/back2raw_sort.png b/docs/_static/images/python_api/back2raw_sort.png new file mode 100644 index 0000000..406e2b5 Binary files /dev/null and b/docs/_static/images/python_api/back2raw_sort.png differ diff --git a/docs/jupyter/backward_projection.ipynb b/docs/jupyter/backward_projection.ipynb index b9096a1..a0236e0 100644 --- a/docs/jupyter/backward_projection.ipynb +++ b/docs/jupyter/backward_projection.ipynb @@ -306,6 +306,29 @@ "```" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "
\n", + "\n", + "Note\n", + "\n", + "You can save the results (json and cropped png) to given folder by:\n", + "\n", + "```python\n", + "img_dict_sort = roi.back2raw(ms, ..., save_folder=\"folder_to_save\")\n", + "```\n", + "\n", + "And will get the following results in the folder:\n", + "\n", + "
\"lotus
\n", + "\n", + "
\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -548,6 +571,28 @@ ")" ] }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "Note\n", + "\n", + "You can save the results (json and cropped png) to given folder by:\n", + "\n", + "```python\n", + "img_dict_sort = ms.sort_img_by_distance(..., save_folder=\"folder_to_save\")\n", + "```\n", + "\n", + "And will get the following results in the folder:\n", + "\n", + "
\"lotus
\n", + "\n", + "
" + ] + }, { "cell_type": "code", "execution_count": 14, @@ -703,7 +748,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.13" + "version": "3.8.13 (default, Mar 28 2022, 06:16:26) \n[Clang 12.0.0 ]" }, "vscode": { "interpreter": { diff --git a/easyidp/data.py b/easyidp/data.py index f6a807b..57cdbea 100644 --- a/easyidp/data.py +++ b/easyidp/data.py @@ -472,6 +472,7 @@ def __init__(self, test_out="./tests/out"): self.cv = self.CVDataset(self.data_dir, test_out) self.vis = self.VisualDataset(self.data_dir, test_out) + self.b2r = self.Back2rawDataset(self.data_dir, test_out) class MetashapeDataset(): @@ -646,3 +647,17 @@ def __init__(self, data_dir, test_out): def __truediv__(self, other): return self.data_dir / "visual_test" / other + + + + class Back2rawDataset(): + + def __init__(self, data_dir, test_out): + self.data_dir = data_dir + + if isinstance(test_out, str): + test_out = Path(test_out) + self.out = test_out / "back2raw_test" + + def __truediv__(self, other): + return self.data_dir / "back2raw_test" / other diff --git a/easyidp/metashape.py b/easyidp/metashape.py index 270ad0c..8eccee4 100644 --- a/easyidp/metashape.py +++ b/easyidp/metashape.py @@ -442,7 +442,7 @@ def _back2raw_one2one(self, points_np, photo_id, distortion_correct=True): else: return out - def back2raw_crs(self, points_xyz, save_folder=None, ignore=None, log=False): + def back2raw_crs(self, points_xyz, ignore=None, log=False): """Projs one GIS coordintates ROI (polygon) to all images Parameters @@ -457,11 +457,6 @@ def back2raw_crs(self, points_xyz, save_folder=None, ignore=None, log=False): - ``x``: only y (vertical) in image area, x can outside image; - ``y``: only x (horizontal) in image area, y can outside image. - save_folder : str | default "" - The folder to contain the output results (preview images and json coords) - - .. caution:: This feature has not been implemented - log : bool, optional whether print log for debugging, by default False @@ -543,7 +538,7 @@ def back2raw(self, roi, save_folder=None, **kwargs): roi : easyidp.ROI | dict the object created by easyidp.ROI() or dictionary save_folder : str, optional - the folder to save projected preview images and json files, by default "" + the folder to save json files and parts of ROI on raw images, by default None ignore : str | None, optional Whether tolerate small parts outside image, check :func:`easyidp.reconstruct.Sensor.in_img_boundary` for more details. @@ -619,11 +614,15 @@ def back2raw(self, roi, save_folder=None, **kwargs): if points_xyz.shape[1] != 3: raise ValueError(f"The back2raw function requires 3D roi with shape=(n, 3), but [{k}] is {points_xyz.shape}") - one_roi_dict= self.back2raw_crs(points_xyz, save_folder=save_path, **kwargs) + one_roi_dict= self.back2raw_crs(points_xyz, **kwargs) out_dict[k] = one_roi_dict self.crs = before_crs + + if save_folder is not None: + idp.reconstruct.save_back2raw_json_and_png(self, out_dict, save_folder) + return out_dict def get_photo_position(self, to_crs=None, refresh=False): @@ -714,7 +713,7 @@ def get_photo_position(self, to_crs=None, refresh=False): return out - def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None): + def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None, save_folder=None): """Advanced wrapper of sorting back2raw img_dict results by distance from photo to roi Parameters @@ -728,6 +727,8 @@ def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None Keep the closest {x} images distance_thresh : None or float Keep the images closer than this distance to ROI. + save_folder : str, optional + the folder to save json files and parts of ROI on raw images, by default None Returns ------- @@ -851,7 +852,7 @@ def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None if not self.enabled: raise TypeError("Unable to process disabled chunk (.enabled=False)") - return idp.reconstruct.sort_img_by_distance(self, img_dict_all, roi, distance_thresh, num) + return idp.reconstruct.sort_img_by_distance(self, img_dict_all, roi, distance_thresh, num, save_folder) def show_roi_on_img(self, img_dict, roi_name, img_name=None, **kwargs): """Visualize the specific backward projection results for given roi on the given image. diff --git a/easyidp/pix4d.py b/easyidp/pix4d.py index aee5e0c..e4ec953 100644 --- a/easyidp/pix4d.py +++ b/easyidp/pix4d.py @@ -519,7 +519,7 @@ def _pmatrix_calc(self, points, photo, distort_correct=True): return coords_b - def back2raw_crs(self, points_xyz, distort_correct=True, ignore=None, save_folder=None, log=False): + def back2raw_crs(self, points_xyz, distort_correct=True, ignore=None, log=False): """Projects one GIS coordintates ROI (polygon) to all images Parameters @@ -538,11 +538,6 @@ def back2raw_crs(self, points_xyz, distort_correct=True, ignore=None, save_folde - ``x``: only y (vertical) in image area, x can outside image; - ``y``: only x (horizontal) in image area, y can outside image. - save_folder : str | default "" - The folder to contain the output results (preview images and json coords) - - .. caution:: This feature has not been implemented - log : bool, optional whether print log for debugging, by default False @@ -606,13 +601,6 @@ def back2raw_crs(self, points_xyz, distort_correct=True, ignore=None, save_folde if coords is not None: out_dict[photo.label] = coords - if isinstance(save_folder, str) and os.path.isdir(save_folder): - # if not os.path.exists(save_folder): - # os.makedirs(save_folder) - # save to json file - # save to one image file () - raise NotImplementedError("This feature has not been implemented") - return out_dict @@ -698,10 +686,13 @@ def back2raw(self, roi, save_folder=None, **kwargs): f"The back2raw function requires 3D roi with shape=(n, 3)" f", but [{k}] is {points_xyz.shape}") - one_roi_dict= self.back2raw_crs(points_xyz, save_folder=save_path, **kwargs) + one_roi_dict= self.back2raw_crs(points_xyz, **kwargs) out_dict[k] = one_roi_dict + if save_folder is not None: + idp.reconstruct.save_back2raw_json_and_png(self, out_dict, save_folder) + return out_dict def get_photo_position(self, to_crs=None, refresh=False): @@ -731,9 +722,7 @@ def get_photo_position(self, to_crs=None, refresh=False): >>> import easyidp as idp >>> lotus = idp.data.Lotus() - >>> p4d = idp.Pix4D(project_path=lotus.pix4d.project, - >>> raw_img_folder=lotus.photo, - >>> param_folder=lotus.pix4d.param) + >>> p4d = idp.Pix4D(lotus.pix4d.project,lotus.photo, lotus.pix4d.param) Then use this function to get the photo position in 3D world: @@ -774,7 +763,7 @@ def get_photo_position(self, to_crs=None, refresh=False): return out - def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None): + def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None, save_folder=None): """Advanced wrapper of sorting back2raw img_dict results by distance from photo to roi Parameters @@ -788,6 +777,8 @@ def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None Keep the closest {x} images distance_thresh : None or float Keep the images closer than this distance to ROI. + save_folder : str, optional + the folder to save json files and parts of ROI on raw images, by default None Returns ------- @@ -913,7 +904,7 @@ def sort_img_by_distance(self, img_dict_all, roi, distance_thresh=None, num=None easyidp.reconstruct.sort_img_by_distance """ - return idp.reconstruct.sort_img_by_distance(self, img_dict_all, roi, distance_thresh, num) + return idp.reconstruct.sort_img_by_distance(self, img_dict_all, roi, distance_thresh, num, save_folder) def show_roi_on_img(self, img_dict, roi_name, img_name=None, **kwargs): """Visualize the specific backward projection results for given roi on the given image. diff --git a/easyidp/reconstruct.py b/easyidp/reconstruct.py index fa9bb55..b95c930 100644 --- a/easyidp/reconstruct.py +++ b/easyidp/reconstruct.py @@ -4,6 +4,7 @@ from pathlib import Path from tqdm import tqdm import warnings +from skimage.io import imread, imsave import easyidp as idp @@ -649,7 +650,7 @@ def _sort_img_by_distance_one_roi(recons, img_dict, plot_geo, cam_pos, distance_ return img_dict_sort -def sort_img_by_distance(recons, img_dict_all, roi, distance_thresh=None, num=None): +def sort_img_by_distance(recons, img_dict_all, roi, distance_thresh=None, num=None, save_folder=None): """Advanced wrapper of sorting back2raw img_dict results by distance from photo to roi Parameters @@ -665,6 +666,8 @@ def sort_img_by_distance(recons, img_dict_all, roi, distance_thresh=None, num=No Keep the closest {x} images distance_thresh : None or float Keep the images closer than this distance to ROI. + save_folder : str, optional + the folder to save json files and parts of ROI on raw images, by default None Returns ------- @@ -682,4 +685,83 @@ def sort_img_by_distance(recons, img_dict_all, roi, distance_thresh=None, num=No ) img_dict_sort_all[roi_name] = sort_dict - return img_dict_sort_all \ No newline at end of file + if save_folder is not None: + save_back2raw_json_and_png(recons, img_dict_sort_all, save_folder) + + return img_dict_sort_all + + +def save_back2raw_json_and_png(recons, results_dict, save_folder): + """Save the backward reversed results + + Parameters + ---------- + results_dict : dict + the outputs of :func:`back2raw() ` function results + save_path : str + the folder to save output files + + Example + ------- + + Data prepare + + .. code-block:: python + + >>> import easyidp as idp + >>> lotus = idp.data.Lotus() + >>> p4d = idp.Pix4D(lotus.pix4d.project, lotus.photo, lotus.pix4d.param) + + >>> roi = idp.ROI(lotus.shp, name_field=0) + >>> roi = roi[0:3] + >>> roi.get_z_from_dsm(lotus.pix4d.dsm) + + >>> out_p4d = roi.back2raw(p4d) + + Use this function: + + .. code-block:: python + + >>> idp.reconstruct.save_back2raw_json_and_png(p4d, out_p4d, "out_path") + + """ + # prepare the root folder + if isinstance(save_folder, (str, Path)): + save_folder = str(save_folder) + if not os.path.exists(save_folder): + os.makedirs(save_folder) + else: + raise TypeError(f"Only the string path is acceptable, not [{save_folder} {type(save_folder)}]") + + # reverse the current order results_dict[roi][image_name] to results[image_name][roi] + # this will save the IO cost when loading images. + print("Optimising data structures of produced results, this may take some time...") + rev_dict = {} + for roi_name, roi_dict in results_dict.items(): + # create the save path of each roi + roi_folder = os.path.join(save_folder, roi_name) + if not os.path.exists(roi_folder): + os.mkdir(roi_folder) + + for img_name, img_pos in roi_dict.items(): + if img_name not in rev_dict.keys(): + rev_dict[img_name] = {} + + rev_dict[img_name][roi_name] = img_pos + + # save the full results as json directly + idp.jsonfile.save_json(results_dict, os.path.join(save_folder, "roi_image_order.json")) + idp.jsonfile.save_json(rev_dict, os.path.join(save_folder, "image_roi_order.json")) + + # then doing the for loop for each image. + for img_name, img_rois in tqdm(rev_dict.items(), desc=f"Processing image [{img_name}]"): + + img_array = imread(recons.photos[img_name].path) + + for roi_name, img_pos in tqdm(img_rois.items(), leave=False): + + png_save_path = os.path.join(save_folder, roi_name, f"{roi_name}_{img_name}.png") + + cropped_png, _ = idp.cvtools.imarray_crop(img_array, img_pos) + + imsave(png_save_path, cropped_png) \ No newline at end of file diff --git a/easyidp/roi.py b/easyidp/roi.py index 8353f7c..aa7a3c2 100644 --- a/easyidp/roi.py +++ b/easyidp/roi.py @@ -873,7 +873,7 @@ def crop(self, target, save_folder=None): return out - def back2raw(self, recons, save_folder=None, **kwargs): + def back2raw(self, recons, **kwargs): """Projects several GIS coordintates ROIs (polygons) to all images Parameters @@ -948,9 +948,7 @@ def back2raw(self, recons, save_folder=None, **kwargs): >>> import easyidp as idp >>> lotus = idp.data.Lotus() - >>> p4d = idp.Pix4D(project_path=lotus.pix4d.project, - ... raw_img_folder=lotus.photo, - ... param_folder=lotus.pix4d.param) + >>> p4d = idp.Pix4D(lotus.pix4d.project, lotus.photo, lotus.pix4d.param) >>> ms = idp.Metashape(project_path=lotus.metashape.project, chunk_id=0) @@ -1090,6 +1088,7 @@ def load_detections(path): """ # boxes = pd.read_csv(path) + boxes = None if not all([x in ["image_path","xmin","ymin","xmax","ymax","image_path","label"] for x in boxes.columns]): raise IOError("{} is expected to be a .csv with columns, xmin, ymin, xmax, ymax, image_path, label for each detection") diff --git a/tests/__init__.py b/tests/__init__.py index 3da3c42..d60cfca 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,7 +8,7 @@ if not out_dir.exists(): out_dir.mkdir() -out_folders = ["json_test", "pcd_test", "cv_test", "tiff_test", "visual_test"] +out_folders = ["json_test", "pcd_test", "cv_test", "tiff_test", "visual_test", "back2raw_test"] for o in out_folders: sub_dir = out_dir / o diff --git a/tests/test_reconstruct.py b/tests/test_reconstruct.py index 188a529..41fc35b 100644 --- a/tests/test_reconstruct.py +++ b/tests/test_reconstruct.py @@ -1,7 +1,9 @@ import re +import os import pytest import numpy as np import easyidp as idp +import shutil test_data = idp.data.TestData() @@ -222,26 +224,24 @@ def test_func_sort_img_by_distance_ms(): filter_3_all_self = ms.sort_img_by_distance(out_all, roi, num=3) assert len(filter_3_all_self) == 2 -def test_func_sort_img_by_distance_p4d(): - # ============= - # pix4d outputs - # ============= - lotus = idp.data.Lotus() +# ============= +# pix4d outputs +# ============= +lotus = idp.data.Lotus() - p4d = idp.Pix4D(project_path=lotus.pix4d.project, - raw_img_folder=lotus.photo, - param_folder=lotus.pix4d.param) +p4d = idp.Pix4D(project_path=lotus.pix4d.project, + raw_img_folder=lotus.photo, + param_folder=lotus.pix4d.param) +ms = idp.Metashape(project_path=lotus.metashape.project, chunk_id=0) - roi = idp.ROI(lotus.shp, name_field=0) - # only pick 2 plots as testing data - key_list = list(roi.keys()) - for key in key_list: - if key not in ["N1W1", "N1W2"]: - del roi[key] - roi.get_z_from_dsm(lotus.pix4d.dsm) +roi = idp.ROI(lotus.shp, name_field=0) +# only pick 2 plots as testing data +roi = roi[0:3] +roi.get_z_from_dsm(lotus.pix4d.dsm) - out_all = p4d.back2raw(roi) +out_all = p4d.back2raw(roi) +def test_func_sort_img_by_distance_p4d(): cam_pos = p4d.get_photo_position() filter_3 = idp.reconstruct._sort_img_by_distance_one_roi(p4d, out_all["N1W2"], roi["N1W2"], cam_pos, num=3) assert len(filter_3) == 3 @@ -251,10 +251,71 @@ def test_func_sort_img_by_distance_p4d(): # test all roi filter_3_all = idp.reconstruct.sort_img_by_distance(p4d, out_all, roi, num=3) - assert len(filter_3_all) == 2 + assert len(filter_3_all) == 3 for v in filter_3_all.values(): assert len(v) == 3 # on self filter_3_all_self = p4d.sort_img_by_distance(out_all, roi, num=3) - assert len(filter_3_all_self) == 2 \ No newline at end of file + assert len(filter_3_all_self) == 3 + + +def test_func_save_back2raw_json_and_png(): + + with pytest.raises(TypeError, match=re.escape("Only the string path is acceptable, not [12345 ]")): + idp.reconstruct.save_back2raw_json_and_png(p4d, out_all, 12345) + + out_path = test_data.b2r.out / "def_path" + if os.path.exists(out_path): + shutil.rmtree(out_path) + + idp.reconstruct.save_back2raw_json_and_png(p4d, out_all, out_path) + + file_list = os.listdir(str(out_path)) + assert len(file_list) == len(out_all) + 2 + assert "roi_image_order.json" in file_list + assert "image_roi_order.json" in file_list + + roi_folder_list = os.listdir(str(out_path / "N1W1")) + assert len(roi_folder_list) == len(out_all['N1W1']) + assert "N1W1_DJI_0479.png" in roi_folder_list + +# test on the other easy-to-use functions +def test_func_save_back2raw_json_and_png_other_func(): + + # this is very time costy, often no need to run... + + # ms_out_path = test_data.b2r.out / "ms_back2raw" + # if os.path.exists(ms_out_path): + # shutil.rmtree(ms_out_path) + # ms_out = ms.back2raw(roi, save_folder=ms_out_path) + + # roi_folder_list1 = os.listdir(str(ms_out_path / "N1W1")) + # assert len(roi_folder_list1) == len(ms_out['N1W1']) + + + # p4d_out_path = test_data.b2r.out / "p4d_back2raw" + # if os.path.exists(p4d_out_path): + # shutil.rmtree(p4d_out_path) + # p4d_out = p4d.back2raw(roi, save_folder=p4d_out_path) + + # roi_folder_list2 = os.listdir(str(p4d_out_path / "N1W1")) + # assert len(roi_folder_list2) == len(p4d_out['N1W1']) + + + ms_sort_path = test_data.b2r.out / "ms_back2raw_sort" + if os.path.exists(ms_sort_path): + shutil.rmtree(ms_sort_path) + ms.sort_img_by_distance(ms_out, roi, num=3, save_folder=ms_sort_path) + + roi_folder_list3 = os.listdir(str(ms_sort_path / "N1W1")) + assert len(roi_folder_list3) == 3 + + + p4d_sort_path = test_data.b2r.out / "p4d_back2raw_sort" + if os.path.exists(p4d_sort_path): + shutil.rmtree(p4d_sort_path) + p4d.sort_img_by_distance(p4d_out, roi, num=3, save_folder=p4d_sort_path) + + roi_folder_list4 = os.listdir(str(p4d_sort_path / "N1W1")) + assert len(roi_folder_list4) == 3 diff --git a/tests/test_visualize.py b/tests/test_visualize.py index c151e61..35656e1 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -32,7 +32,7 @@ def test_class_back2raw_single(): out_dict = p4d.back2raw_crs(plot, distort_correct=True) # plot figures - img_name = "DJI_0198.JPG" + img_name = "DJI_0198" photo = p4d.photos[img_name] idp.visualize.draw_polygon_on_img( img_name, photo.path, out_dict[img_name], show=False, @@ -55,16 +55,16 @@ def test_visualize_one_roi_on_img_p4d(): with pytest.raises(IndexError, match=re.escape("Could not find backward results of plot [N1W2] on image [aaa]")): p4d.show_roi_on_img(img_dict_p4d, 'N1W2', 'aaa') - with pytest.raises(FileNotFoundError, match=re.escape("Could not find the image file [DJI_2233.JPG] in the Pix4D project")): - img_dict_p4d['N1W1']['DJI_2233.JPG'] = None - p4d.show_roi_on_img(img_dict_p4d, 'N1W1', 'DJI_2233.JPG') + with pytest.raises(FileNotFoundError, match=re.escape("Could not find the image file [DJI_2233] in the Pix4D project")): + img_dict_p4d['N1W1']['DJI_2233'] = None + p4d.show_roi_on_img(img_dict_p4d, 'N1W1', 'DJI_2233') out = p4d.show_roi_on_img( - img_dict_p4d, 'N1W1', "DJI_0500.JPG", title="AAAA", color='green', alpha=0.5, show=False, + img_dict_p4d, 'N1W1', "DJI_0500", title="AAAA", color='green', alpha=0.5, show=False, save_as=test_data.vis.out / "p4d_show_roi_on_img_diy.png") out = p4d.show_roi_on_img( - img_dict_p4d, 'N1W2', show=False, title=["AAAA", "BBBB"], color='green', alpha=0.5, show=False, + img_dict_p4d, 'N1W2', show=False, title=["AAAA", "BBBB"], color='green', alpha=0.5, save_as=test_data.vis.out / "p4d_show_one_roi_all.png")