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",
+ "
\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",
+ "
\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")