From 7dfb312b577e135866c40551d2d72634bd0cdfec Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Thu, 24 Jun 2021 15:08:02 -0700 Subject: [PATCH 01/18] new PR06 panel for tests --- data/0002SET/000/IMG_0183_1.tif | 3 +++ data/0002SET/000/IMG_0183_2.tif | 3 +++ data/0002SET/000/IMG_0183_3.tif | 3 +++ data/0002SET/000/IMG_0183_4.tif | 3 +++ data/0002SET/000/IMG_0183_5.tif | 3 +++ 5 files changed, 15 insertions(+) create mode 100644 data/0002SET/000/IMG_0183_1.tif create mode 100644 data/0002SET/000/IMG_0183_2.tif create mode 100644 data/0002SET/000/IMG_0183_3.tif create mode 100644 data/0002SET/000/IMG_0183_4.tif create mode 100644 data/0002SET/000/IMG_0183_5.tif diff --git a/data/0002SET/000/IMG_0183_1.tif b/data/0002SET/000/IMG_0183_1.tif new file mode 100644 index 00000000..b1940dcd --- /dev/null +++ b/data/0002SET/000/IMG_0183_1.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:355a1a43555ef9e6319ba68241c0aabe002531847c0170105ef8f190a1d53f96 +size 2465968 diff --git a/data/0002SET/000/IMG_0183_2.tif b/data/0002SET/000/IMG_0183_2.tif new file mode 100644 index 00000000..5a698040 --- /dev/null +++ b/data/0002SET/000/IMG_0183_2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0d9926375c832b670bb31a064c3fc28aa98343e0117877f0b82f505c19b293c +size 2465966 diff --git a/data/0002SET/000/IMG_0183_3.tif b/data/0002SET/000/IMG_0183_3.tif new file mode 100644 index 00000000..5303e80e --- /dev/null +++ b/data/0002SET/000/IMG_0183_3.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2bce2d536c20ce229b92eb6157d8a41317ea9ed8af0098d955acdf67382610 +size 2465972 diff --git a/data/0002SET/000/IMG_0183_4.tif b/data/0002SET/000/IMG_0183_4.tif new file mode 100644 index 00000000..37ac3f4b --- /dev/null +++ b/data/0002SET/000/IMG_0183_4.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f80dbc0284b5561d3144b27866048d0cebda4a050072eee744b03ab8a288ff4 +size 2465966 diff --git a/data/0002SET/000/IMG_0183_5.tif b/data/0002SET/000/IMG_0183_5.tif new file mode 100644 index 00000000..9aaa1b33 --- /dev/null +++ b/data/0002SET/000/IMG_0183_5.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfeb85a03c99ce1712a48ef127b9f0719bfdc01323989f4a1bc17c4bf173cdc4 +size 2465962 From e409d30ee3d5e61036ef4478cf520066e877f327 Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Thu, 24 Jun 2021 15:08:35 -0700 Subject: [PATCH 02/18] get panel area from physical measurments --- micasense/panel.py | 57 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/micasense/panel.py b/micasense/panel.py index 150ea982..731f0e76 100644 --- a/micasense/panel.py +++ b/micasense/panel.py @@ -157,25 +157,52 @@ def panel_corners(self): return None if self.panel_version < 3: - reference_panel_pts = np.asarray([[894, 469], [868, 232], [630, 258], [656, 496]], - dtype=np.int32) - reference_qr_pts = np.asarray([[898, 748], [880, 567], [701, 584], [718, 762]], - dtype=np.int32) - elif self.panel_version >= 3: - reference_panel_pts = np.asarray([[557, 350], [550, 480], [695, 480], [700, 350]], dtype=np.int32) - reference_qr_pts = np.asarray([[821, 324], [819, 506], [996, 509], [999, 330]], dtype=np.int32) - + # reference_panel_pts = np.asarray([[894, 469], [868, 232], [630, 258], [656, 496]], + # dtype=np.int32) + # reference_qr_pts = np.asarray([[898, 748], [880, 567], [701, 584], [718, 762]], + # dtype=np.int32) + + # use the actual panel measures here - we use units of [mm] + # the panel is 154.4 x 152.4 mm , vs. the 84 x 84 mm for the QR code + # it is left 143.20 mm from the QR code + # use the inner 50% square of the panel + s = 76.2 + p = 42 + T = np.array([-143.2,0]) + + elif (self.panel_version >= 3) and (self.panel_version<6): + s = 50 + p = 45 + T = np.array([-145.8,0]) + # reference_panel_pts = np.asarray([[557, 350], [550, 480], [695, 480], [700, 350]], dtype=np.int32) + # reference_qr_pts = np.asarray([[821, 324], [819, 506], [996, 509], [999, 330]], dtype=np.int32) + elif self.panel_version >= 6 : + # use the actual panel measures here - we use units of [mm] + # the panel is 100 x 100 mm , vs. the 91 x 91 mm for the QR code + # it is down 125.94 mm from the QR code + # use the inner 50% square of the panel + p = 41 + s = 50 + T = np.array([0,-130.84]) + + + reference_panel_pts = np.asarray([[-s, s], [s, s], [s, -s], [-s, -s]], dtype=np.float32)*.5+T + reference_qr_pts = np.asarray([[-p, p], [p, p], [p, -p], [-p, -p]], dtype=np.float32) bounds = [] costs = [] for rotation in range(0,4): qr_points = np.roll(reference_qr_pts, rotation, axis=0) - src = np.asarray([tuple(row) for row in qr_points[:3]], np.float32) - dst = np.asarray([tuple(row) for row in self.qr_corners()[:3]], np.float32) - warp_matrix = cv2.getAffineTransform(src, dst) + src = np.asarray([tuple(row) for row in qr_points[:]], np.float32) + dst = np.asarray([tuple(row) for row in self.qr_corners()[:]], np.float32) + + # we determine the homography from the 4 corner points + warp_matrix = cv2.getPerspectiveTransform(src,dst) + + #warp_matrix = cv2.getAffineTransform(src, dst) - pts = np.asarray([reference_panel_pts], 'int32') - panel_bounds = cv2.convexHull(cv2.transform(pts, warp_matrix), clockwise=False) + pts = np.asarray([reference_panel_pts], 'float32') + panel_bounds = cv2.convexHull(cv2.perspectiveTransform(pts, warp_matrix), clockwise=False) panel_bounds = np.squeeze(panel_bounds) # remove nested lists bounds_in_image = True @@ -184,7 +211,7 @@ def panel_corners(self): bounds_in_image = False if bounds_in_image: mean, std, _, _ = self.region_stats(self.image.raw(),panel_bounds, sat_threshold=65000) - bounds.append(panel_bounds) + bounds.append(panel_bounds.astype(np.int32)) costs.append(std/mean) idx = costs.index(min(costs)) @@ -195,7 +222,7 @@ def ordered_panel_coordinates(self): """ Return panel region coordinates in a predictable order. Panel region coordinates that are automatically detected by the camera are ordered differently than coordinates detected by Panel.panel_corners(). - :return: [ (lr), (ll), (ul), (ur) ] to mirror Image.panel_region attribute order + :return: [ (ur), (ul), (ll), (lr) ] to mirror Image.panel_region attribute order """ pc = self.panel_corners() pc = sorted(pc, key=lambda x: x[0]) From 266a02c90bed0735aa9b222a3dddf10343edd01e Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Thu, 24 Jun 2021 15:08:54 -0700 Subject: [PATCH 03/18] add new tests for PR06 panel --- tests/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index ea48195f..bb20568d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,6 +101,17 @@ def panel_image_name_red(): image_path = os.path.join('data', '0000SET', '000') return os.path.join(image_path, 'IMG_0000_2.tif') +@pytest.fixture() +def panel_image_name_RP06_blue(): + image_path = os.path.join('data', '0002SET', '000') + return os.path.join(image_path, 'IMG_0183_1.tif') + +@pytest.fixture() +def panel_images_RP06(): + image_path = os.path.join('data', '0002SET', '000') + return glob.glob(os.path.join(image_path, 'IMG*.tif')) + + @pytest.fixture() def flight_image_name(): image_path = os.path.join('data', '0000SET', '000') From d7f873317d49aaf778f6632b766ab9c6c3e3fd21 Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Thu, 24 Jun 2021 15:09:21 -0700 Subject: [PATCH 04/18] modify tests for new panel window method --- tests/test_capture.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_capture.py b/tests/test_capture.py index 75a5632e..a08a6a4b 100644 --- a/tests/test_capture.py +++ b/tests/test_capture.py @@ -125,7 +125,7 @@ def test_panel_radiance(panel_rededge_capture): 0.13081077851565506] assert len(rad) == len(expected_rad) for i,_ in enumerate(expected_rad): - assert rad[i] == pytest.approx(expected_rad[i], rel=0.001) + assert rad[i] == pytest.approx(expected_rad[i], rel=0.01) def test_panel_raw(panel_rededge_capture): raw = panel_rededge_capture.panel_raw() @@ -137,7 +137,7 @@ def test_panel_raw(panel_rededge_capture): 54479.170371812339] assert len(raw) == len(expected_raw) for i,_ in enumerate(expected_raw): - assert raw[i] == pytest.approx(expected_raw[i], rel=0.001) + assert raw[i] == pytest.approx(expected_raw[i], rel=0.01) def test_panel_irradiance(panel_rededge_capture): panel_reflectance_by_band = [0.67, 0.69, 0.68, 0.61, 0.67] @@ -145,7 +145,7 @@ def test_panel_irradiance(panel_rededge_capture): expected_rad = [0.79845135523772681, 0.81681533164998943, 0.74944205649335915, 0.54833776619262586, 0.61336444894797537] assert len(rad) == len(expected_rad) for i,_ in enumerate(expected_rad): - assert rad[i] == pytest.approx(expected_rad[i], rel=0.001) + assert rad[i] == pytest.approx(expected_rad[i], rel=0.01) def test_panel_albedo_not_preset(panel_rededge_capture): assert panel_rededge_capture.panels_in_all_expected_images() From f178c043f27b2289a6f47863dd018779e107b9ea Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Thu, 24 Jun 2021 15:09:39 -0700 Subject: [PATCH 05/18] add tests for new panel window method --- tests/test_panel.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/tests/test_panel.py b/tests/test_panel.py index 35d69909..88ce2261 100644 --- a/tests/test_panel.py +++ b/tests/test_panel.py @@ -30,6 +30,27 @@ import micasense.panel as panel import operator +def test_RP06_panel_ID(panel_image_name_RP06_blue): + img = image.Image(panel_image_name_RP06_blue) + pan = panel.Panel(img) + qr_corners = pan.qr_corners() + assert pan.panel_version == 6 + +def test_RP06_panel_raw(panel_images_RP06): + test_mean = [41064,39046,42419,35149,37266] + test_std = [680,534,560,607,507] + test_num = [2543,2100,2408,2484,2358] + test_sat = [0,0,0,0,0] + for i,m,s,n,sa in zip(panel_images_RP06,test_mean,test_std,test_num,test_sat): + img = image.Image(i) + pan = panel.Panel(img) + mean, std, num, sat = pan.raw() + print('mean {:f} std {:f} num {:f} sat {:f}'.format(mean,std,num,sat)) + assert mean == pytest.approx(m,rel=0.1) + assert std == pytest.approx(s,rel=0.1) + assert num == pytest.approx(n,rel=0.1) + assert sat == pytest.approx(sa,rel=0.1) + def test_qr_corners(panel_image_name): img = image.Image(panel_image_name) pan = panel.Panel(img) @@ -47,7 +68,8 @@ def test_panel_corners(panel_image_name): img = image.Image(panel_image_name) pan = panel.Panel(img) panel_pts = pan.panel_corners() - good_pts = [[809, 613], [648, 615], [646, 454], [808, 452]] + good_pts = [[785,594],[674,593],[673,483],[783,484]] + assert panel_pts is not None assert len(panel_pts) == len(good_pts) assert pan.serial == 'RP02-1603036-SC' @@ -102,8 +124,8 @@ def test_raw_panel(panel_image_name): pan = panel.Panel(img) mean, std, num, sat = pan.raw() assert mean == pytest.approx(45406.0, rel=0.01) - assert std == pytest.approx(738.0, rel=0.05) - assert num == pytest.approx(26005, rel=0.02) + assert std == pytest.approx(689.0, rel=0.05) + assert num == pytest.approx(12154, rel=0.02) assert sat == pytest.approx(0, abs=2) def test_intensity_panel(panel_image_name): @@ -111,8 +133,8 @@ def test_intensity_panel(panel_image_name): pan = panel.Panel(img) mean, std, num, sat = pan.intensity() assert mean == pytest.approx(1162, rel=0.01) - assert std == pytest.approx(23, rel=0.03) - assert num == pytest.approx(26005, rel=0.02) + assert std == pytest.approx(20, rel=0.03) + assert num == pytest.approx(12154, rel=0.02) assert sat == pytest.approx(0, abs=2) def test_radiance_panel(panel_image_name): @@ -120,8 +142,8 @@ def test_radiance_panel(panel_image_name): pan = panel.Panel(img) mean, std, num, sat = pan.radiance() assert mean == pytest.approx(0.170284, rel=0.01) - assert std == pytest.approx(0.0033872969661854742, rel=0.02) - assert num == pytest.approx(26005, rel=0.02) + assert std == pytest.approx(0.0029387953691472554, rel=0.02) + assert num == pytest.approx(12154, rel=0.02) assert sat == pytest.approx(0, abs=2) def test_irradiance_mean(panel_image_name): @@ -129,7 +151,7 @@ def test_irradiance_mean(panel_image_name): pan = panel.Panel(img) panel_reflectance = 0.67 mean = pan.irradiance_mean(panel_reflectance) - assert mean == pytest.approx(0.7984, rel=0.001) + assert mean == pytest.approx(0.7984, rel=0.01) def test_panel_detected(panel_image_name): img = image.Image(panel_image_name) From 26d35a5bf3cbb14c8c0c1ca0b3534134ec1582df Mon Sep 17 00:00:00 2001 From: Justin M Date: Sat, 10 Jul 2021 11:54:48 -0700 Subject: [PATCH 06/18] Update travis exiftool version --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index a3ffcfea..16059d0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,9 @@ before_install: # install library dependencies - sudo apt-get install libzbar0 make perl # Install exiftool - - wget https://exiftool.org/Image-ExifTool-12.01.tar.gz - - tar -xzf Image-ExifTool-12.01.tar.gz - - pushd Image-ExifTool-12.01/ + - wget https://cpan.metacpan.org/authors/id/E/EX/EXIFTOOL/Image-ExifTool-12.15.tar.gz + - tar -xzf Image-ExifTool-12.15.tar.gz + - pushd Image-ExifTool-12.15/ - perl Makefile.PL - make test - sudo make install From 3b8874bb8a42187e7d0efb88e9c56a66e8f5efc5 Mon Sep 17 00:00:00 2001 From: Justin M Date: Sat, 10 Jul 2021 12:02:52 -0700 Subject: [PATCH 07/18] Update setup notebook with exiftool path Point to CPAN since it keeps archival versions. --- MicaSense Image Processing Setup.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MicaSense Image Processing Setup.ipynb b/MicaSense Image Processing Setup.ipynb index da170b91..bdf8d3ea 100755 --- a/MicaSense Image Processing Setup.ipynb +++ b/MicaSense Image Processing Setup.ipynb @@ -47,9 +47,9 @@ " \n", "Next we installed [exiftool](https://exiftool.org/):\n", "\n", - " wget https://exiftool.org/Image-ExifTool-10.98.tar.gz\n", - " tar -xvzf Image-ExifTool-10.98.tar.gz \n", - " cd Image-ExifTool-10.98/\n", + " wget https://cpan.metacpan.org/authors/id/E/EX/EXIFTOOL/Image-ExifTool-12.15.tar.gz\n", + " tar -xvzf Image-ExifTool-12.15.tar.gz \n", + " cd Image-ExifTool-12.15/\n", " perl Makefile.PL \n", " make test\n", " sudo make install\n", From 78984aac113afa4f5082f4d27a0927112d587aa6 Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Tue, 13 Jul 2021 13:14:40 -0700 Subject: [PATCH 08/18] ordered loading of multiple images --- tests/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bb20568d..b28918a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,11 +44,11 @@ def ten_band_files_dir(): @pytest.fixture() def panel_rededge_file_list(files_dir): - return glob.glob(os.path.join(files_dir, 'IMG_0000_*.tif')) + return sorted(glob.glob(os.path.join(files_dir, 'IMG_0000_*.tif'))) @pytest.fixture() def non_panel_rededge_file_list(files_dir): - return glob.glob(os.path.join(files_dir, 'IMG_0001_*.tif')) + return sorted(glob.glob(os.path.join(files_dir, 'IMG_0001_*.tif'))) @pytest.fixture() def bad_file_list(files_dir): @@ -58,7 +58,7 @@ def bad_file_list(files_dir): @pytest.fixture() def panel_altum_file_list(altum_files_dir): - return glob.glob(os.path.join(altum_files_dir, 'IMG_0000_*.tif')) + return sorted(glob.glob(os.path.join(altum_files_dir, 'IMG_0000_*.tif'))) @pytest.fixture() def panel_rededge_capture(panel_rededge_file_list): @@ -70,11 +70,11 @@ def non_panel_rededge_capture(non_panel_rededge_file_list): @pytest.fixture() def panel_10band_rededge_file_list(ten_band_files_dir): - return glob.glob(os.path.join(ten_band_files_dir, 'IMG_0000_*.tif')) + return sorted(glob.glob(os.path.join(ten_band_files_dir, 'IMG_0000_*.tif'))) @pytest.fixture() def flight_10band_rededge_file_list(ten_band_files_dir): - return glob.glob(os.path.join(ten_band_files_dir, 'IMG_0431_*.tif')) + return sorted(glob.glob(os.path.join(ten_band_files_dir, 'IMG_0431_*.tif'))) @pytest.fixture() def panel_altum_capture(panel_altum_file_list): @@ -83,7 +83,7 @@ def panel_altum_capture(panel_altum_file_list): @pytest.fixture() def non_panel_altum_file_list(altum_files_dir): - return glob.glob(os.path.join(altum_files_dir, 'IMG_0008_*.tif')) + return sorted(glob.glob(os.path.join(altum_files_dir, 'IMG_0008_*.tif'))) @pytest.fixture() def non_panel_altum_capture(non_panel_altum_file_list): @@ -109,7 +109,7 @@ def panel_image_name_RP06_blue(): @pytest.fixture() def panel_images_RP06(): image_path = os.path.join('data', '0002SET', '000') - return glob.glob(os.path.join(image_path, 'IMG*.tif')) + return sorted(glob.glob(os.path.join(image_path, 'IMG*.tif'))) @pytest.fixture() From f63f6d791b5807c16983219f85ce54f4bcdd4b23 Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Tue, 13 Jul 2021 13:15:05 -0700 Subject: [PATCH 09/18] fixed order of tests, when multiple images are loaded --- tests/test_panel.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_panel.py b/tests/test_panel.py index 88ce2261..39b124a5 100644 --- a/tests/test_panel.py +++ b/tests/test_panel.py @@ -37,19 +37,21 @@ def test_RP06_panel_ID(panel_image_name_RP06_blue): assert pan.panel_version == 6 def test_RP06_panel_raw(panel_images_RP06): - test_mean = [41064,39046,42419,35149,37266] - test_std = [680,534,560,607,507] - test_num = [2543,2100,2408,2484,2358] + test_mean = [35149,41064,39046,42419,37266] + test_std = [607,680,534,560,507] + test_num = [2484,2543,2100,2408,2358] test_sat = [0,0,0,0,0] for i,m,s,n,sa in zip(panel_images_RP06,test_mean,test_std,test_num,test_sat): img = image.Image(i) pan = panel.Panel(img) mean, std, num, sat = pan.raw() - print('mean {:f} std {:f} num {:f} sat {:f}'.format(mean,std,num,sat)) + #print('mean {:f} std {:f} num {:f} sat {:f}'.format(mean,std,num,sat)) + #print('m {:f} s {:f} n {:f} sa {:f}'.format(m,s,n,sa)) assert mean == pytest.approx(m,rel=0.1) assert std == pytest.approx(s,rel=0.1) assert num == pytest.approx(n,rel=0.1) assert sat == pytest.approx(sa,rel=0.1) + def test_qr_corners(panel_image_name): img = image.Image(panel_image_name) From 2385750d34d5983905c922d0e2fe04ef697d587b Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Wed, 14 Jul 2021 12:19:46 -0700 Subject: [PATCH 10/18] set panel serial when panel is in the metadata - also added flag to force detection --- micasense/panel.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/micasense/panel.py b/micasense/panel.py index 731f0e76..2c314afe 100644 --- a/micasense/panel.py +++ b/micasense/panel.py @@ -35,7 +35,9 @@ class Panel(object): - def __init__(self, img,panelCorners=None): + def __init__(self, img,panelCorners=None,ignore_autocalibration=False): + # if we have panel images with QR metadata, panel detection is not called, + # so this can be forced here if img is None: raise IOError("Must provide an image") @@ -45,7 +47,7 @@ def __init__(self, img,panelCorners=None): self.gray8b = np.zeros(img.radiance().shape, dtype='uint8') cv2.convertScaleAbs(img.undistorted(img.radiance()), self.gray8b, 256.0/scale, -1.0*scale*bias) - if self.image.auto_calibration_image: + if (self.image.auto_calibration_image) and ~ignore_autocalibration: self.__panel_type = "auto" ## panels the camera found we call auto if panelCorners is not None: self.__panel_bounds = np.array(panelCorners) @@ -59,7 +61,9 @@ def __init__(self, img,panelCorners=None): self.saturated_panel_pixels_pct = None self.panel_pixels_mean = None self.panel_version = None - + if re.search(r'RP\d{2}-(\d{7})-\D{2}', self.image.panel_serial): + self.serial = self.image.panel_serial + self.panel_version = int(self.image.panel_serial[2:4]) else: self.__panel_type = "search" ## panels we search for we call search self.serial = None From 97ef98535d0fdd790a7c3a5d561173da8ee1fbbb Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Wed, 14 Jul 2021 12:20:22 -0700 Subject: [PATCH 11/18] change panel image for RP06 --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index b28918a3..c5f297a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,7 +104,7 @@ def panel_image_name_red(): @pytest.fixture() def panel_image_name_RP06_blue(): image_path = os.path.join('data', '0002SET', '000') - return os.path.join(image_path, 'IMG_0183_1.tif') + return os.path.join(image_path, 'IMG_0000_1.tif') @pytest.fixture() def panel_images_RP06(): From 6914fb40eaebd321d2ccbcfb04c0810cbb32e71e Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Wed, 14 Jul 2021 12:20:42 -0700 Subject: [PATCH 12/18] added another test for forced detection --- tests/test_panel.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_panel.py b/tests/test_panel.py index 39b124a5..ca2d34fa 100644 --- a/tests/test_panel.py +++ b/tests/test_panel.py @@ -35,22 +35,30 @@ def test_RP06_panel_ID(panel_image_name_RP06_blue): pan = panel.Panel(img) qr_corners = pan.qr_corners() assert pan.panel_version == 6 + +def test_RP06_panel_ID_autodetect(panel_image_name_RP06_blue): + img = image.Image(panel_image_name_RP06_blue) + pan = panel.Panel(img,ignore_autocalibration=True) + qr_corners = pan.qr_corners() + assert pan.panel_version == 6 def test_RP06_panel_raw(panel_images_RP06): - test_mean = [35149,41064,39046,42419,37266] - test_std = [607,680,534,560,507] - test_num = [2484,2543,2100,2408,2358] + test_mean = [33082,34347,33971,34186,33371] + test_std = [474.7,582.6,476.3,464,658.9] + test_num = [3616,3552,3669,3612,3729] test_sat = [0,0,0,0,0] for i,m,s,n,sa in zip(panel_images_RP06,test_mean,test_std,test_num,test_sat): img = image.Image(i) pan = panel.Panel(img) mean, std, num, sat = pan.raw() - #print('mean {:f} std {:f} num {:f} sat {:f}'.format(mean,std,num,sat)) - #print('m {:f} s {:f} n {:f} sa {:f}'.format(m,s,n,sa)) + assert pan.panel_detected() + print('mean {:f} std {:f} num {:f} sat {:f}'.format(mean,std,num,sat)) + print('m {:f} s {:f} n {:f} sa {:f}'.format(m,s,n,sa)) assert mean == pytest.approx(m,rel=0.1) assert std == pytest.approx(s,rel=0.1) assert num == pytest.approx(n,rel=0.1) assert sat == pytest.approx(sa,rel=0.1) + def test_qr_corners(panel_image_name): From f1b6ce915d7c4836474fcb359e13d2345c978deb Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Wed, 14 Jul 2021 12:58:23 -0700 Subject: [PATCH 13/18] new panel images for RP06 --- data/0002SET/000/IMG_0000_1.tif | 3 +++ data/0002SET/000/IMG_0000_2.tif | 3 +++ data/0002SET/000/IMG_0000_3.tif | 3 +++ data/0002SET/000/IMG_0000_4.tif | 3 +++ data/0002SET/000/IMG_0000_5.tif | 3 +++ 5 files changed, 15 insertions(+) create mode 100644 data/0002SET/000/IMG_0000_1.tif create mode 100644 data/0002SET/000/IMG_0000_2.tif create mode 100644 data/0002SET/000/IMG_0000_3.tif create mode 100644 data/0002SET/000/IMG_0000_4.tif create mode 100644 data/0002SET/000/IMG_0000_5.tif diff --git a/data/0002SET/000/IMG_0000_1.tif b/data/0002SET/000/IMG_0000_1.tif new file mode 100644 index 00000000..b2128f10 --- /dev/null +++ b/data/0002SET/000/IMG_0000_1.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0ef38734a94aae45347f96512d1d084d470956e805c0907a8d7aa659d289ce6 +size 2466318 diff --git a/data/0002SET/000/IMG_0000_2.tif b/data/0002SET/000/IMG_0000_2.tif new file mode 100644 index 00000000..421c52e0 --- /dev/null +++ b/data/0002SET/000/IMG_0000_2.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa00bc26b2037d67f1de7c4618b9d3ac6fc1bea5e22851dcf13a3cdcdad8a400 +size 2466308 diff --git a/data/0002SET/000/IMG_0000_3.tif b/data/0002SET/000/IMG_0000_3.tif new file mode 100644 index 00000000..b4915698 --- /dev/null +++ b/data/0002SET/000/IMG_0000_3.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e41e98bea9994eff7959f4e93c134f0185f8287e2cda1bd19493528de3610b6f +size 2466300 diff --git a/data/0002SET/000/IMG_0000_4.tif b/data/0002SET/000/IMG_0000_4.tif new file mode 100644 index 00000000..0e2702e2 --- /dev/null +++ b/data/0002SET/000/IMG_0000_4.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:008b19f85db896219d662eb88434103e8393af09e43fbbe00f0be0fa40e848b8 +size 2466336 diff --git a/data/0002SET/000/IMG_0000_5.tif b/data/0002SET/000/IMG_0000_5.tif new file mode 100644 index 00000000..df248a6b --- /dev/null +++ b/data/0002SET/000/IMG_0000_5.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c149e3bc4b0edc16c351c46922aec0aa42c14a820e00de818edf7c9a6252169 +size 2466332 From 45df69880ce71444e3f6d5b8bc913911c46f5c50 Mon Sep 17 00:00:00 2001 From: Felix Darvas Date: Wed, 14 Jul 2021 12:59:28 -0700 Subject: [PATCH 14/18] remove these panel images, as they dont work with all versions of zbar --- data/0002SET/000/IMG_0183_1.tif | 3 --- data/0002SET/000/IMG_0183_2.tif | 3 --- data/0002SET/000/IMG_0183_3.tif | 3 --- data/0002SET/000/IMG_0183_4.tif | 3 --- data/0002SET/000/IMG_0183_5.tif | 3 --- 5 files changed, 15 deletions(-) delete mode 100644 data/0002SET/000/IMG_0183_1.tif delete mode 100644 data/0002SET/000/IMG_0183_2.tif delete mode 100644 data/0002SET/000/IMG_0183_3.tif delete mode 100644 data/0002SET/000/IMG_0183_4.tif delete mode 100644 data/0002SET/000/IMG_0183_5.tif diff --git a/data/0002SET/000/IMG_0183_1.tif b/data/0002SET/000/IMG_0183_1.tif deleted file mode 100644 index b1940dcd..00000000 --- a/data/0002SET/000/IMG_0183_1.tif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:355a1a43555ef9e6319ba68241c0aabe002531847c0170105ef8f190a1d53f96 -size 2465968 diff --git a/data/0002SET/000/IMG_0183_2.tif b/data/0002SET/000/IMG_0183_2.tif deleted file mode 100644 index 5a698040..00000000 --- a/data/0002SET/000/IMG_0183_2.tif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0d9926375c832b670bb31a064c3fc28aa98343e0117877f0b82f505c19b293c -size 2465966 diff --git a/data/0002SET/000/IMG_0183_3.tif b/data/0002SET/000/IMG_0183_3.tif deleted file mode 100644 index 5303e80e..00000000 --- a/data/0002SET/000/IMG_0183_3.tif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d2bce2d536c20ce229b92eb6157d8a41317ea9ed8af0098d955acdf67382610 -size 2465972 diff --git a/data/0002SET/000/IMG_0183_4.tif b/data/0002SET/000/IMG_0183_4.tif deleted file mode 100644 index 37ac3f4b..00000000 --- a/data/0002SET/000/IMG_0183_4.tif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f80dbc0284b5561d3144b27866048d0cebda4a050072eee744b03ab8a288ff4 -size 2465966 diff --git a/data/0002SET/000/IMG_0183_5.tif b/data/0002SET/000/IMG_0183_5.tif deleted file mode 100644 index 9aaa1b33..00000000 --- a/data/0002SET/000/IMG_0183_5.tif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfeb85a03c99ce1712a48ef127b9f0719bfdc01323989f4a1bc17c4bf173cdc4 -size 2465962 From c10f93542955d50ebcedcba394e998755687f0e0 Mon Sep 17 00:00:00 2001 From: Justin M Date: Wed, 26 May 2021 13:02:08 -0700 Subject: [PATCH 15/18] reconciling save_capture with other changes --- micasense/capture.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/micasense/capture.py b/micasense/capture.py index aa6254c0..8aac72ae 100644 --- a/micasense/capture.py +++ b/micasense/capture.py @@ -552,6 +552,38 @@ def save_capture_as_stack(self, out_file_name, sort_by_wavelength=False, photome finally: out_raster = None + def save_bands_in_separate_file(self, outfilename, sort_by_wavelength=False,photometric='MINISBLACK'): + from osgeo.gdal import GetDriverByName, GDT_UInt16 + if self.__aligned_capture is None: + raise RuntimeError("call Capture.create_aligned_capture prior to saving as stack") + rows, cols, bands = self.__aligned_capture.shape + driver = GetDriverByName('GTiff') + + for i in range(0,5): + band_number = str(i+1) + outRaster = driver.Create(outfilename+'_'+band_number+'.tif', cols, rows, 1, GDT_UInt16, + options = [ 'INTERLEAVE=BAND','COMPRESS=DEFLATE',f'PHOTOMETRIC={photometric}']) + outband = outRaster.GetRasterBand(1) + outdata = self.__aligned_capture[:,:,i] + outdata = outdata*32768 + outdata[outdata<0] = 0 + outdata[outdata>65535] = 65535 + outband.WriteArray(outdata) + outband.FlushCache() + outRaster = None + + if bands == 6: + thermalRaster = driver.Create(outfilename+'_6.tif', cols, rows, 1, GDT_UInt16, + options = [ 'INTERLEAVE=BAND','COMPRESS=DEFLATE',f'PHOTOMETRIC={photometric}']) + outband = thermalRaster.GetRasterBand(1) + outdata = (self.__aligned_capture[:,:,5]) * 100 + outdata[outdata<0] = 0 + outdata[outdata>65535] = 65535 + outband.WriteArray(outdata) + outband.FlushCache() + thermalRaster = None + + def save_capture_as_rgb(self, out_file_name, gamma=1.4, downsample=1, white_balance='norm', hist_min_percent=0.5, hist_max_percent=99.5, sharpen=True, rgb_band_indices=(2, 1, 0)): """ From d6459d6371eff24d655274f013e0edc8109a5c1d Mon Sep 17 00:00:00 2001 From: and-viceversa <25871157+and-viceversa@users.noreply.github.com> Date: Tue, 1 Jun 2021 19:01:58 -0700 Subject: [PATCH 16/18] Force img_type to be either 'reflectance' or 'radiance'. Add out_data_type handling to save_capture_as_stack(). Force radiance output to correct units. Add save_capture_as_bands() method. --- micasense/capture.py | 152 +++++++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 41 deletions(-) diff --git a/micasense/capture.py b/micasense/capture.py index 8aac72ae..9ff9c230 100644 --- a/micasense/capture.py +++ b/micasense/capture.py @@ -29,6 +29,7 @@ import math import os +import warnings import cv2 import imageio @@ -38,6 +39,8 @@ import micasense.imageutils as imageutils import micasense.plotutils as plotutils +warnings.simplefilter(action="once") + class Capture(object): """ @@ -80,6 +83,7 @@ def __init__(self, images, panel_corners=None): else: self.panel_corners = panel_corners + self.__image_type = None self.__aligned_capture = None def set_panel_corners(self, panel_corners): @@ -201,6 +205,7 @@ def clear_image_data(self): for img in self.images: img.clear_image_data() self.__aligned_capture = None + self.__image_type = None def center_wavelengths(self): """Returns a list of the image center wavelengths in nanometers.""" @@ -468,26 +473,39 @@ def get_warp_matrices(self, ref_index=None): warp_matrices = [np.linalg.inv(im.get_homography(ref)) for im in self.images] return [w / w[2, 2] for w in warp_matrices] - def create_aligned_capture(self, irradiance_list=None, warp_matrices=None, normalize=False, img_type=None, + def create_aligned_capture(self, img_type, irradiance_list=None, warp_matrices=None, normalize=False, motion_type=cv2.MOTION_HOMOGRAPHY): """ Creates aligned Capture. Computes undistorted radiance or reflectance images if necessary. + :param img_type: str 'radiance' or 'reflectance' depending on image metadata. :param irradiance_list: List of mean panel region irradiance. :param warp_matrices: 2d List of warp matrices derived from Capture.get_warp_matrices() :param normalize: FIXME: This parameter isn't used? - :param img_type: str 'radiance' or 'reflectance' depending on image metadata. :param motion_type: OpenCV import. Also know as warp_mode. MOTION_HOMOGRAPHY or MOTION_AFFINE. For Altum images only use HOMOGRAPHY. :return: ndarray with alignment changes """ - if img_type is None and irradiance_list is None and self.dls_irradiance() is None: + if img_type == 'radiance': self.compute_undistorted_radiance() - img_type = 'radiance' - elif img_type is None: - if irradiance_list is None: + self.__image_type = 'radiance' + elif img_type == 'reflectance': + # TODO: Handle pre-flight Panel cap + post-flight Panel cap + DLS values + # Add use_dls or similar user option to properly scale irradiance_list in different configurations. + # Alternatively if this should be done elsewhere then dls_irradiance() should probably not be used here. + + # if no irradiance values provided, attempt to use DLS + if irradiance_list is None and self.dls_present(): irradiance_list = self.dls_irradiance() + [0] + elif irradiance_list is None and not self.dls_present(): + raise RuntimeError('Reflectance output requested, but no irradiance values given and no DLS values ' + 'found in image metadata.') + self.compute_undistorted_reflectance(irradiance_list) - img_type = 'reflectance' + self.__image_type = 'reflectance' + else: + raise RuntimeError('Unknown img_type output requested: {}\nMust be "radiance" or "reflectance".' + .format(img_type)) + if warp_matrices is None: warp_matrices = self.get_warp_matrices() cropped_dimensions, _ = imageutils.find_crop_bounds(self, warp_matrices, warp_mode=motion_type) @@ -508,25 +526,46 @@ def aligned_shape(self): raise RuntimeError("Call Capture.create_aligned_capture() prior to saving as stack.") return self.__aligned_capture.shape - def save_capture_as_stack(self, out_file_name, sort_by_wavelength=False, photometric='MINISBLACK'): + def save_capture_as_stack(self, out_file_name, out_data_type='GDT_UInt16', + sort_by_wavelength=False, photometric='MINISBLACK'): """ Output the Images in the Capture object as GTiff image stack. :param out_file_name: str system file path + :param out_data_type: str GDT_Float32 or GDT_UInt16 + Default: GDT_UInt16 will write images as scaled reflectance values. EO (32769=100%) + and LWIR in centi-Kelvin (0-65535). + GDT_Float32 will write images as floating point reflectance values. EO (1.0=100%) + and LWIR in floating point Celsius. + https://gdal.org/api/raster_c_api.html#_CPPv412GDALDataType :param sort_by_wavelength: boolean :param photometric: str GDAL argument for GTiff color matching """ - from osgeo.gdal import GetDriverByName, GDT_UInt16 + from osgeo.gdal import GetDriverByName, GDT_UInt16, GDT_Float32 if self.__aligned_capture is None: raise RuntimeError("Call Capture.create_aligned_capture() prior to saving as stack.") rows, cols, bands = self.__aligned_capture.shape driver = GetDriverByName('GTiff') - out_raster = driver.Create(out_file_name, cols, rows, bands, GDT_UInt16, + # force correct output datatype + if self.__image_type == 'radiance': + gdal_type = GDT_Float32 # force floating point values for radiance and degrees C + elif self.__image_type == 'reflectance' and out_data_type == 'GDT_UInt16': + gdal_type = GDT_UInt16 + elif self.__image_type == 'reflectance' and out_data_type == 'GDT_Float32': + gdal_type = GDT_Float32 + else: + warnings.warn(message='Output data type in Capture.save_capture_as_bands() was called as {}. ' + 'Must use "GDT_UInt16" or "GDT_Float32". Defaulting to GDT_UInt16...' + .format(out_data_type), + category=UserWarning) + gdal_type = GDT_UInt16 + + out_raster = driver.Create(out_file_name, cols, rows, bands, gdal_type, options=['INTERLEAVE=BAND', 'COMPRESS=DEFLATE', f'PHOTOMETRIC={photometric}']) try: if out_raster is None: - raise IOError("could not load gdal GeoTiff driver") + raise IOError("Could not load GDAL GeoTiff driver.") if sort_by_wavelength: eo_list = list(np.argsort(np.array(self.center_wavelengths())[self.eo_indices()])) @@ -538,13 +577,15 @@ def save_capture_as_stack(self, out_file_name, sort_by_wavelength=False, photome out_data = self.__aligned_capture[:, :, in_band] out_data[out_data < 0] = 0 out_data[out_data > 2] = 2 # limit reflectance data to 200% to allow some specular reflections - out_band.WriteArray(out_data * 32768) # scale reflectance images so 100% = 32768 + # scale reflectance images so 100% = 32768 + out_band.WriteArray(out_data * 32768 if gdal_type == 2 else out_data) out_band.FlushCache() for out_band, in_band in enumerate(self.lw_indices()): out_band = out_raster.GetRasterBand(len(eo_list) + out_band + 1) # scale data from float degC to back to centi-Kelvin to fit into uint16 - out_data = (self.__aligned_capture[:, :, in_band] + 273.15) * 100 + out_data = (self.__aligned_capture[:, :, in_band] + 273.15) * 100 if gdal_type == 2 \ + else self.__aligned_capture[:, :, in_band] out_data[out_data < 0] = 0 out_data[out_data > 65535] = 65535 out_band.WriteArray(out_data) @@ -552,37 +593,66 @@ def save_capture_as_stack(self, out_file_name, sort_by_wavelength=False, photome finally: out_raster = None - def save_bands_in_separate_file(self, outfilename, sort_by_wavelength=False,photometric='MINISBLACK'): - from osgeo.gdal import GetDriverByName, GDT_UInt16 + def save_capture_as_bands(self, out_file_name, out_data_type='GDT_UInt16', photometric='MINISBLACK'): + """ + Output the Images in the Capture object as separate GTiffs. + :param out_file_name: str system file path without file extension + :param out_data_type: str GDT_Float32 or GDT_UInt16 + Default: GDT_UInt16 will write images as scaled reflectance values. EO (32769=100%) + and LWIR in centi-Kelvin (0-65535). + GDT_Float32 will write images as floating point reflectance values. EO (1.0=100%) + and LWIR in floating point Celsius. + https://gdal.org/api/raster_c_api.html#_CPPv412GDALDataType + :param photometric: str GDAL argument for GTiff color matching + :return: None + """ + from osgeo.gdal import GetDriverByName, GDT_UInt16, GDT_Float32 if self.__aligned_capture is None: - raise RuntimeError("call Capture.create_aligned_capture prior to saving as stack") + raise RuntimeError("Call Capture.create_aligned_capture() prior to saving as stack.") + + # force correct output datatype + if self.__image_type == 'radiance': + gdal_type = GDT_Float32 # force floating point values for radiance and degrees C + elif self.__image_type == 'reflectance' and out_data_type == 'GDT_UInt16': + gdal_type = GDT_UInt16 + elif self.__image_type == 'reflectance' and out_data_type == 'GDT_Float32': + gdal_type = GDT_Float32 + else: + warnings.warn(message='Output data type in Capture.save_capture_as_bands() was called as {}. ' + 'Must use "GDT_UInt16" or "GDT_Float32". Defaulting to GDT_UInt16...' + .format(out_data_type), + category=UserWarning) + gdal_type = GDT_UInt16 + + # predictably handle accidental .tif.tif values + if out_file_name.endswith('.tif'): + out_file_path = out_file_name[:-4] + else: + out_file_path = out_file_name + rows, cols, bands = self.__aligned_capture.shape - driver = GetDriverByName('GTiff') - - for i in range(0,5): - band_number = str(i+1) - outRaster = driver.Create(outfilename+'_'+band_number+'.tif', cols, rows, 1, GDT_UInt16, - options = [ 'INTERLEAVE=BAND','COMPRESS=DEFLATE',f'PHOTOMETRIC={photometric}']) - outband = outRaster.GetRasterBand(1) - outdata = self.__aligned_capture[:,:,i] - outdata = outdata*32768 - outdata[outdata<0] = 0 - outdata[outdata>65535] = 65535 - outband.WriteArray(outdata) - outband.FlushCache() - outRaster = None - - if bands == 6: - thermalRaster = driver.Create(outfilename+'_6.tif', cols, rows, 1, GDT_UInt16, - options = [ 'INTERLEAVE=BAND','COMPRESS=DEFLATE',f'PHOTOMETRIC={photometric}']) - outband = thermalRaster.GetRasterBand(1) - outdata = (self.__aligned_capture[:,:,5]) * 100 - outdata[outdata<0] = 0 - outdata[outdata>65535] = 65535 - outband.WriteArray(outdata) - outband.FlushCache() - thermalRaster = None + driver = GetDriverByName('GTiff') + for i in self.eo_indices(): + out_raster = driver.Create(out_file_path + f'_{i + 1}.tif', cols, rows, 1, gdal_type, + options=['INTERLEAVE=BAND', 'COMPRESS=DEFLATE', f'PHOTOMETRIC={photometric}']) + out_band = out_raster.GetRasterBand(1) + out_data = self.__aligned_capture[:, :, i] + out_data[out_data < 0] = 0 + out_data[out_data > 2] = 2 # limit reflectance data to 200% to allow some specular reflections + # if GDT_UInt16, scale reflectance images so 100% = 32768. GDT_UInt16 resolves to 2. + out_band.WriteArray(out_data * 32768 if gdal_type == 2 else out_data) + out_band.FlushCache() + + for i in self.lw_indices(): + out_raster = driver.Create(out_file_path + f'_{i + 1}.tif', cols, rows, 1, gdal_type, + options=['INTERLEAVE=BAND', 'COMPRESS=DEFLATE', f'PHOTOMETRIC={photometric}']) + out_band = out_raster.GetRasterBand(1) + # if GDT_UInt16, scale data from float degC to back to centi-Kelvin to fit into UInt16. + out_data = (self.__aligned_capture[:, :, i] + 273.15) * 100 if gdal_type == 2 \ + else self.__aligned_capture[:, :, i] + out_band.WriteArray(out_data) + out_band.FlushCache() def save_capture_as_rgb(self, out_file_name, gamma=1.4, downsample=1, white_balance='norm', hist_min_percent=0.5, hist_max_percent=99.5, sharpen=True, rgb_band_indices=(2, 1, 0)): From 6a8880c5b9cc5cbfa9d500ed15d047f04740f913 Mon Sep 17 00:00:00 2001 From: and-viceversa <25871157+and-viceversa@users.noreply.github.com> Date: Tue, 1 Jun 2021 19:04:46 -0700 Subject: [PATCH 17/18] Update process_imageset() parameters, docstrings, and warning checks to match updates in capture.py. Force img_type parameter to be either 'radiance' or 'reflectance'. Add out_data_type parameter. --- micasense/imageset.py | 48 +++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/micasense/imageset.py b/micasense/imageset.py index f9f02401..6f9466fc 100644 --- a/micasense/imageset.py +++ b/micasense/imageset.py @@ -82,27 +82,26 @@ def parallel_process(function, iterable, parameters, progress_callback=None, use def save_capture(params, cap): """ - Process an ImageSet according to program parameters. Saves rgb - :param params: dict of program parameters from ImageSet.process_imageset() + Convenience function to save Captures in an ImageSet according to input parameters. + :param params: dict of input parameters from ImageSet.process_imageset() :param cap: micasense.capture.Capture object """ try: # align capture if len(cap.images) == params['capture_len']: cap.create_aligned_capture( - irradiance_list=params['irradiance'], - warp_matrices=params['warp_matrices'], - img_type=params['img_type'] + img_type=params['img_type'], + irradiance_list=params['irradiance_list'], + warp_matrices=params['warp_matrices'] ) else: print(f"\tCapture {cap.uuid} only has {len(cap.images)} Images. Should have {params['capture_len']}. " f"Skipping...") return - if params['output_stack_dir']: output_stack_file_path = os.path.join(params['output_stack_dir'], cap.uuid + '.tif') if params['overwrite'] or not os.path.exists(output_stack_file_path): - cap.save_capture_as_stack(output_stack_file_path) + cap.save_capture_as_stack(output_stack_file_path, out_data_type=params['out_data_type']) if params['output_rgb_dir']: output_rgb_file_path = os.path.join(params['output_rgb_dir'], cap.uuid + '.jpg') if params['overwrite'] or not os.path.exists(output_rgb_file_path): @@ -224,28 +223,40 @@ def dls_irradiance(self): return series def process_imageset(self, + img_type, output_stack_directory=None, output_rgb_directory=None, warp_matrices=None, - irradiance=None, - img_type=None, + irradiance_list=None, + out_data_type='GDT_UInt16', multiprocess=True, overwrite=False, progress_callback=None, use_tqdm=False): """ Write band stacks and rgb thumbnails to disk. + :param img_type: str 'radiance' or 'reflectance'. Desired image output type. + - A 'radiance' value will output EO Images as radiance floating point values, + and LWIR Images in floating point Celsius. + - A 'reflectance' value will output EO Images as reflectance UInt16 + and LWIR Images as UInt16 centi-Kelvin using the provided irradiance_list. :param warp_matrices: 2d List of warp matrices derived from Capture.get_warp_matrices() :param output_stack_directory: str system file path to output stack directory :param output_rgb_directory: str system file path to output thumbnail directory - :param irradiance: List returned from Capture.dls_irradiance() or Capture.panel_irradiance() <-- TODO: Write a better docstring for this - :param img_type: str 'radiance' or 'reflectance'. Desired image output type. + :param irradiance_list: List returned from Capture.dls_irradiance() or Capture.panel_irradiance() <-- TODO: Write a better docstring for this + :param out_data_type: str GDT_Float32 or GDT_UInt16 + Default: GDT_UInt16 will write images as scaled reflectance values. EO (32769=100%) + and LWIR in centi-Kelvin (0-65535). + GDT_Float32 will write images as floating point reflectance values. EO (1.0=100%) + and LWIR in floating point Celsius. + https://gdal.org/api/raster_c_api.html#_CPPv412GDALDataType :param multiprocess: boolean True to use multiprocessing module :param overwrite: boolean True to overwrite existing files :param progress_callback: function to report progress to :param use_tqdm: boolean True to use tqdm progress bar """ + # progress_callback deprecation warning if progress_callback is not None: warnings.warn(message='The progress_callback parameter will be deprecated in favor of use_tqdm', category=PendingDeprecationWarning) @@ -254,6 +265,17 @@ def process_imageset(self, if output_stack_directory is None and output_rgb_directory is None: raise RuntimeError('No output requested for the ImageSet.') + # ensure output type makes sense + if img_type not in ['radiance', 'reflectance']: + raise RuntimeError('Unknown img_type output requested: {}\nMust be "radiance" or "reflectance".' + .format(img_type)) + + # warn user if only DLS metadata used in reflectance processing. + if img_type == 'reflectance' and irradiance_list is None: + warnings.warn(message='Reflectance output requested, but no irradiance_list value given. ' + 'Attempting to process reflectance from DLS metadata...', + category=UserWarning) + # make output dirs if not exist if output_stack_directory is not None and not os.path.exists(output_stack_directory): os.mkdir(output_stack_directory) @@ -263,8 +285,9 @@ def process_imageset(self, # processing parameters params = { 'warp_matrices': warp_matrices, - 'irradiance': irradiance, + 'irradiance_list': irradiance_list, 'img_type': img_type, + 'out_data_type': out_data_type, 'capture_len': len(self.captures[0].images), 'output_stack_dir': output_stack_directory, 'output_rgb_dir': output_rgb_directory, @@ -272,7 +295,6 @@ def process_imageset(self, } print('Processing {} Captures ...'.format(len(self.captures))) - # multiprocessing with concurrent futures if multiprocess: parallel_process(function=save_capture, iterable=self.captures, parameters=params, From 9cd35f1edd5cac2bdb79d834039b9404efe411d8 Mon Sep 17 00:00:00 2001 From: and-viceversa <25871157+and-viceversa@users.noreply.github.com> Date: Tue, 1 Jun 2021 19:25:33 -0700 Subject: [PATCH 18/18] Fix LWIR UInt_16 scale --- micasense/capture.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/micasense/capture.py b/micasense/capture.py index 9ff9c230..86313056 100644 --- a/micasense/capture.py +++ b/micasense/capture.py @@ -586,8 +586,9 @@ def save_capture_as_stack(self, out_file_name, out_data_type='GDT_UInt16', # scale data from float degC to back to centi-Kelvin to fit into uint16 out_data = (self.__aligned_capture[:, :, in_band] + 273.15) * 100 if gdal_type == 2 \ else self.__aligned_capture[:, :, in_band] - out_data[out_data < 0] = 0 - out_data[out_data > 65535] = 65535 + if gdal_type == 2: + out_data[out_data < 0] = 0 + out_data[out_data > 65535] = 65535 out_band.WriteArray(out_data) out_band.FlushCache() finally: @@ -651,6 +652,9 @@ def save_capture_as_bands(self, out_file_name, out_data_type='GDT_UInt16', photo # if GDT_UInt16, scale data from float degC to back to centi-Kelvin to fit into UInt16. out_data = (self.__aligned_capture[:, :, i] + 273.15) * 100 if gdal_type == 2 \ else self.__aligned_capture[:, :, i] + if gdal_type == 2: + out_data[out_data < 0] = 0 + out_data[out_data > 65535] = 65535 out_band.WriteArray(out_data) out_band.FlushCache()