lronaccal performs radiometric corrections to images acquired by the Narrow Angle
- Camera aboard the Lunar Reconnaissance Orbiter spacecraft. The LRO NAC camera
- will make observations simulteously with the HiRise camera.
+ Camera aboard the Lunar Reconnaissance Orbiter spacecraft.
@@ -71,39 +70,6 @@
-
-
-
- Let's look at the case of a calibrated image for which the true signal
- is zero, a dark image. In calibration the mean bias and dark current are
- subtracted. The random noise term is then randomly sampled from a known
- distribution with a mean of zero. Since the distribution has a mean of
- zero, values for the random noise can be positive or negative.
- Therefore, the addition of random noise to a pixel with true signal near
- zero can result in negative DN values.
-
-
-
- Negative reported DNs are possible when E < -1 * TrueDN. These are
- pixels in a very dark image that happen to have a strongly negative
- random noise value.
-
-
-
- Note: ObservedDN and TrueDN both must be greater than or equal to zero.
- For ObservedDN, it's because the hardware is not able to report negative
- DN values . For TrueDN, it's because radiance and reflectivity cannot be
- negative. The dimmest target is one that is completely dark, and for
- that target TrueDN = 0.
-
-
-
- If run on a non-spiceinited cube, this program requires access to local mission-specific
- SPICE kernels, in order to find the distance between the sun and the target body.
- When run on a spiceinited cube, this can be determined using the camera model.
- Using a spiceinited cube as input has the advantage of not requiring that local
- mission-specific kernels be available. (See spiceinit web=true.)
-
The dark average produced is dependant on which options are selected.
There are three options for dark calibration/correction. If the image has exposure code of zero,
@@ -138,7 +104,39 @@
This option uses a single custom dark file that the user must supply with the DarkFile parameter.
This option is typically only used in special cases, such as calibrating very long exposure NAC images.
+
+
+ Let's look at the case of a calibrated image for which the true signal
+ is zero, a dark image. In calibration the mean bias and dark current are
+ subtracted. The random noise term is then randomly sampled from a known
+ distribution with a mean of zero. Since the distribution has a mean of
+ zero, values for the random noise can be positive or negative.
+ Therefore, the addition of random noise to a pixel with true signal near
+ zero can result in negative DN values.
+
+
+
+ Negative reported DNs are possible when E < -1 * TrueDN. These are
+ pixels in a very dark image that happen to have a strongly negative
+ random noise value.
+
+
+ Note: ObservedDN and TrueDN both must be greater than or equal to zero.
+ For ObservedDN, it's because the hardware is not able to report negative
+ DN values . For TrueDN, it's because radiance and reflectivity cannot be
+ negative. The dimmest target is one that is completely dark, and for
+ that target TrueDN = 0.
+
+
+
+ If run on a non-spiceinited cube, this program requires access to local mission-specific
+ SPICE kernels, in order to find the distance between the sun and the target body.
+ When run on a spiceinited cube, this can be determined using the camera model.
+ Using a spiceinited cube as input has the advantage of not requiring that local
+ mission-specific kernels be available. (See spiceinit web=true.)
+
+
From d24c091bd25fdc6a8ba00819323cd6ab488e587a Mon Sep 17 00:00:00 2001
From: Victor Silva
Date: Mon, 4 Oct 2021 12:59:46 -0700
Subject: [PATCH 06/10] removed extra tabs
---
isis/src/lro/apps/lronaccal/lronaccal.xml | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/isis/src/lro/apps/lronaccal/lronaccal.xml b/isis/src/lro/apps/lronaccal/lronaccal.xml
index 9df3a039eb..9ca15ece4c 100644
--- a/isis/src/lro/apps/lronaccal/lronaccal.xml
+++ b/isis/src/lro/apps/lronaccal/lronaccal.xml
@@ -24,6 +24,7 @@
will reduce the number of samples in the output image by a factor of at
most the SpatialSumming mode value.
+
The LROC NAC camera has the ability to acquire images of differing sizes in
both line and sample. The starting hardware detector pixel for the
@@ -44,7 +45,7 @@
pixels and the B channel dark average is subtracted from the B channel
image pixels.
-
+
The DN level in an uncalibrated image is the sum of the true signal from the scene,
the bias, the dark current, and random noise in all 3 components. The random noise in
@@ -69,7 +70,7 @@
TrueDN is the signal that would be reported in an idealized case of an instrument with zero noise.
-
+
The dark average produced is dependant on which options are selected.
There are three options for dark calibration/correction. If the image has exposure code of zero,
@@ -82,7 +83,7 @@
dark observations under given settings (i.e., exposure code, summing mode, etc.). The maximum
time range between pairs of dark files is 45 days. If a suitable pair is not found, the latest
dark file taken before the image will be used.
-
+
pixel_dark_average =
avgDarkLine1_pixel * |darkfile1_time - time| + avgDarkLine2_pixel * |darkfile2_time - time|
/ (1.0 * ( |darkFile1_time - time| + |darkFile2_time - time| ) )
@@ -104,7 +105,7 @@
This option uses a single custom dark file that the user must supply with the DarkFile parameter.
This option is typically only used in special cases, such as calibrating very long exposure NAC images.
-
+
Let's look at the case of a calibrated image for which the true signal
is zero, a dark image. In calibration the mean bias and dark current are
@@ -136,7 +137,7 @@
Using a spiceinited cube as input has the advantage of not requiring that local
mission-specific kernels be available. (See spiceinit web=true.)
-
+
@@ -155,7 +156,6 @@
Updated dark calibration documentation to provide more clarity to dark file options
-
@@ -242,7 +242,7 @@
There are three options for dark calibration/correction. If the image has exposure code of zero,
the nearest dark files with exposure code of zero will be used.
-
+
PAIR (Nearest pair of darks) [Default]:
This option selects the two closest dark files (before and after the observation) and
computes a time-weighted average. The maximum time range between pairs of dark files
@@ -253,7 +253,7 @@
This option selects the nearest dark file (before or after the observations).
Note: While this is not a default option, if only one dark is available, lronaccal will
automatically apply the nearest dark file.
-
+
CUSTOM (Custom dark):
This option uses a single custom dark file that the user must supply with the DarkFile parameter.
This option is typically only used in special cases, such as calibrating very long exposure NAC images.
From 80aa14bc10bb8dd7b135a2f905fc1ffbdd471b20 Mon Sep 17 00:00:00 2001
From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com>
Date: Wed, 23 Mar 2022 13:24:22 -0700
Subject: [PATCH 07/10] Add radiance units label in lrowaccal output cube
---
isis/src/lro/apps/lrowaccal/main.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/isis/src/lro/apps/lrowaccal/main.cpp b/isis/src/lro/apps/lrowaccal/main.cpp
index dd55a3540f..afbb399617 100644
--- a/isis/src/lro/apps/lrowaccal/main.cpp
+++ b/isis/src/lro/apps/lrowaccal/main.cpp
@@ -336,7 +336,7 @@ void IsisMain () {
vals.addValue(toString(g_iofResponsivity[i]));
}
else {
- calgrp += PvlKeyword("RadiometricType", "AbsoluteRadiance");
+ calgrp += PvlKeyword("RadiometricType", "AbsoluteRadiance", "W/m2/sr/um");
for (unsigned int i=0; i< g_radianceResponsivity.size(); i++)
vals.addValue(toString(g_radianceResponsivity[i]));
}
From 8eb71a045435ac956236ba9d98db56c79836a21b Mon Sep 17 00:00:00 2001
From: victoronline
Date: Fri, 29 Apr 2022 01:34:24 -0700
Subject: [PATCH 08/10] refactored code to allow callable for compatibility
with gtest
---
isis/src/lro/apps/lronaccal/lronaccal.cpp | 952 ++++++++++++++++++++++
isis/src/lro/apps/lronaccal/lronaccal.h | 13 +
isis/src/lro/apps/lronaccal/main.cpp | 941 +--------------------
3 files changed, 983 insertions(+), 923 deletions(-)
create mode 100644 isis/src/lro/apps/lronaccal/lronaccal.cpp
create mode 100644 isis/src/lro/apps/lronaccal/lronaccal.h
diff --git a/isis/src/lro/apps/lronaccal/lronaccal.cpp b/isis/src/lro/apps/lronaccal/lronaccal.cpp
new file mode 100644
index 0000000000..fb1faeca7b
--- /dev/null
+++ b/isis/src/lro/apps/lronaccal/lronaccal.cpp
@@ -0,0 +1,952 @@
+/** This is free and unencumbered software released into the public domain.
+
+The authors of ISIS do not claim copyright on the contents of this file.
+For more details about the LICENSE terms and the AUTHORS, you will
+find files of those names at the top level of this repository. **/
+
+/* SPDX-License-Identifier: CC0-1.0 */
+
+#include "ProcessByLine.h"
+#include "SpecialPixel.h"
+#include "Message.h"
+#include "Camera.h"
+#include "iTime.h"
+#include "IException.h"
+#include "TextFile.h"
+#include "Brick.h"
+#include "Table.h"
+#include "PvlGroup.h"
+#include "Statistics.h"
+#include "UserInterface.h"
+#include "lronaccal.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std;
+namespace Isis {
+
+ // Working functions and parameters
+ void ResetGlobals();
+ void CopyCubeIntoVector(QString &fileString, vector &data);
+ void ReadTextDataFile(QString &fileString, vector &data);
+ void ReadTextDataFile(QString &fileString, vector > &data);
+ void Calibrate(Buffer &in, Buffer &out);
+ void RemoveMaskedOffset(Buffer &line);
+ void CorrectDark(Buffer &in);
+ void CorrectNonlinearity(Buffer &in);
+ void CorrectFlatfield(Buffer &in);
+ void RadiometricCalibration(Buffer &in);
+ void GetNearestDarkFile(QString fileString, QString &file);
+ void GetNearestDarkFilePair(QString &fileString, QString &file0, QString &file1);
+ void GetCalibrationDirectory(QString calibrationType, QString &calibrationDirectory);
+ void GetWeightedDarkAverages();
+ bool AllowedSpecialPixelType(double pixelValue);
+
+ #define LINE_SIZE 5064
+ #define MAXNONLIN 600
+ #define SOLAR_RADIUS 695500
+ #define KM_PER_AU 149597871
+ #define MASKED_PIXEL_VALUES 8
+
+ /**
+ * DarkFileInfo comparison object.
+ *
+ * Used for sorting DarkFileInfo objects. Sort first by difference from NAC time
+ *
+ */
+ struct DarkFileComparison {
+ int nacTime;
+
+ DarkFileComparison(int nacTime)
+ {
+ this->nacTime = nacTime;
+ }
+
+ // sort dark files by distance from NAC time
+ bool operator() ( int A, int B) {
+ if (abs(nacTime - A) < abs(nacTime - B))
+ return true;
+ return false;
+ }
+ };
+
+ double g_radianceLeft, g_radianceRight, g_iofLeft, g_iofRight, g_imgTime;
+ double g_exposure; // Exposure duration
+ double g_solarDistance; // average distance in [AU]
+
+ bool g_summed, g_masked, g_maskedLeftOnly, g_dark, g_nonlinear, g_flatfield, g_radiometric, g_iof, g_isLeftNac;
+ bool g_nearestDark, g_nearestDarkPair, g_customDark;
+ vector g_maskedPixelsLeft, g_maskedPixelsRight;
+ vector g_avgDarkLineCube0, g_avgDarkLineCube1, g_linearOffsetLine, g_flatfieldLine, g_darkTimes, g_weightedDarkTimeAvgs;
+ vector > g_linearityCoefficients;
+ Buffer *g_darkCube0, *g_darkCube1;
+
+
+
+
+ /**
+ * @brief Calling method of the application
+ *
+ * Performs radiometric corrections to images acquired by the Narrow Angle
+ * Camera aboard the Lunar Reconnaissance Orbiter spacecraft.
+ *
+ * @param ui The user interfact to parse the parameters from.
+ */
+ void lronaccal(UserInterface &ui){
+ //Cube *iCube = p.SetInputCube("FROM", OneBand);
+ Cube iCube(ui.GetCubeName("FROM"));
+ lronaccal(&iCube, ui);
+ }
+ /**
+ * This is the main constructor lronaccal method. Lronaccal is used to calibrate LRO images
+ *
+ * @internal
+ * @history 2020-01-06 Victor Silva - Added option for base calibration directory
+ * @history 2020-07-19 Victor Silva - Updated dark calibration to use dark file option
+ * custom, nearest dark, or nearest dark pair.
+ * @history 2021-01-09 Victor Silva - Added code to check for exp_code = zero and if so
+ * then use only exp_code_zero dark files for dark calibration
+ * @history 2022-04-18 Victor Silva - Refactored to make callable for GTest framework
+ *
+ */
+ void lronaccal(Cube *iCube, UserInterface &ui) {
+ ResetGlobals();
+ // We will be processing by line
+ ProcessByLine p;
+
+
+ // Setup the input and make sure it is a ctx file
+ //UserInterface &ui = Application::GetUserInterface();
+
+ g_masked = ui.GetBoolean("MASKED");
+ g_dark = ui.GetBoolean("DARK");
+ g_nonlinear = ui.GetBoolean("NONLINEARITY");
+ g_flatfield = ui.GetBoolean("FLATFIELD");
+ g_radiometric = ui.GetBoolean("RADIOMETRIC");
+ g_iof = (ui.GetString("RADIOMETRICTYPE") == "IOF");
+
+ Isis::Pvl lab(ui.GetCubeName("FROM"));
+ Isis::PvlGroup &inst = lab.findGroup("Instrument", Pvl::Traverse);
+
+ // Check if it is a NAC image
+ QString instId = (QString) inst["InstrumentId"];
+ instId = instId.toUpper();
+ if(instId != "NACL" && instId != "NACR") {
+ QString msg = "This is not a NAC image. lrocnaccal requires a NAC image.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+
+ // And check if it has already run through calibration
+ if(lab.findObject("IsisCube").hasGroup("Radiometry")) {
+ QString msg = "This image has already been calibrated";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+
+ if(lab.findObject("IsisCube").hasGroup("AlphaCube")) {
+ QString msg = "This application can not be run on any image that has been geometrically transformed (i.e. scaled, rotated, sheared, or reflected) or cropped.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+
+ if(instId == "NACL")
+ g_isLeftNac = true;
+ else
+ g_isLeftNac = false;
+
+ if((int) inst["SpatialSumming"] == 1)
+ g_summed = false;
+ else
+ g_summed = true;
+
+ g_exposure = inst["LineExposureDuration"];
+
+ //Cube *iCube = p.SetInputCube("FROM", OneBand);
+ //p.SetInputCube(iCube, OneBand);
+ p.SetInputCube(iCube, OneBand);
+
+ // If there is any pixel in the image with a DN > 1000
+ // then the "left" masked pixels are likely wiped out and useless
+ if(iCube->statistics()->Maximum() > 1000)
+ g_maskedLeftOnly = true;
+
+ QString flatFile, offsetFile, coefficientFile;
+
+ if(g_masked) {
+ QString maskedFile = ui.GetAsString("MASKEDFILE");
+
+ if(maskedFile.toLower() == "default" || maskedFile.length() == 0){
+ GetCalibrationDirectory("", maskedFile);
+ maskedFile = maskedFile + instId + "_MaskedPixels.????.pvl";
+ }
+
+ FileName maskedFileName(maskedFile);
+ if(maskedFileName.isVersioned())
+ maskedFileName = maskedFileName.highestVersion();
+ if(!maskedFileName.fileExists()) {
+ QString msg = maskedFile + " does not exist.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+
+ Pvl maskedPvl(maskedFileName.expanded());
+ PvlKeyword maskedPixels;
+ int cutoff;
+ if(g_summed) {
+ maskedPixels = maskedPvl["Summed"];
+ cutoff = LINE_SIZE / 4;
+ }
+ else {
+ maskedPixels = maskedPvl["FullResolution"];
+ cutoff = LINE_SIZE / 2;
+ }
+
+ for(int i = 0; i < maskedPixels.size(); i++)
+ if((g_isLeftNac && toInt(maskedPixels[i]) < cutoff) || (!g_isLeftNac && toInt(maskedPixels[i]) > cutoff))
+ g_maskedPixelsLeft.push_back(toInt(maskedPixels[i]));
+ else
+ g_maskedPixelsRight.push_back(toInt(maskedPixels[i]));
+ }
+
+ vector darkFiles;
+
+ if(g_dark) {
+ QString darkFileType = ui.GetString("DARKFILETYPE");
+ darkFileType = darkFileType.toUpper();
+ if (darkFileType == "CUSTOM") {
+ g_customDark = true;
+ ui.GetAsString("DARKFILE", darkFiles);
+ }
+ else if (darkFileType == "PAIR" || darkFileType == "")
+ g_nearestDarkPair = true;
+ else if (darkFileType == "NEAREST"){
+ g_nearestDark = true;
+ }
+ else {
+ QString msg = "Error: Dark File Type selection failed.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ //Options are NEAREST, PAIR, and CUSTOM
+ if(g_customDark){
+ if(darkFiles.size() == 1 && darkFiles[0] != "") {
+ CopyCubeIntoVector(darkFiles[0], g_avgDarkLineCube0);
+ }
+ else {
+ QString msg = "Custom dark file not provided. Please provide file or choose another option.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ }
+ else {
+ QString darkFile;
+ g_imgTime = iTime(inst["StartTime"][0]).Et();
+ GetCalibrationDirectory("nac_darks", darkFile);
+ darkFile = darkFile + instId + "_AverageDarks_*T";
+
+ if(g_summed)
+ darkFile += "_Summed";
+ // use exp0 dark files if cube's exp_code=0
+ Isis::PvlGroup &pvl_archive_group = lab.findGroup("Archive", Pvl::Traverse);
+ if((int) pvl_archive_group["LineExposureCode"] == 0)
+ darkFile += "_exp0";
+
+ darkFile += ".????.cub";
+
+ if(g_nearestDark){
+ darkFiles.resize(1);
+ GetNearestDarkFile(darkFile, darkFiles[0]);
+ }
+ else {
+ darkFiles.resize(2);
+ GetNearestDarkFilePair(darkFile, darkFiles[0], darkFiles[1]);
+ //get weigted time avgs
+ if(g_darkTimes.size() == 2)
+ GetWeightedDarkAverages();
+ }
+ }
+ }
+
+ if(g_nonlinear) {
+ offsetFile = ui.GetAsString("OFFSETFILE");
+
+ if(offsetFile.toLower() == "default" || offsetFile.length() == 0) {
+ GetCalibrationDirectory("", offsetFile);
+ offsetFile = offsetFile + instId + "_LinearizationOffsets";
+ if(g_summed)
+ offsetFile += "_Summed";
+ offsetFile += ".????.cub";
+ }
+ CopyCubeIntoVector(offsetFile, g_linearOffsetLine);
+ coefficientFile = ui.GetAsString("NONLINEARITYFILE");
+ if(coefficientFile.toLower() == "default" || coefficientFile.length() == 0) {
+ GetCalibrationDirectory("", coefficientFile);
+ coefficientFile = coefficientFile + instId + "_LinearizationCoefficients.????.txt";
+ }
+ ReadTextDataFile(coefficientFile, g_linearityCoefficients);
+ }
+
+ if(g_flatfield) {
+ flatFile = ui.GetAsString("FLATFIELDFILE");
+
+ if(flatFile.toLower() == "default" || flatFile.length() == 0) {
+ GetCalibrationDirectory("", flatFile);
+ flatFile = flatFile + instId + "_Flatfield";
+ if(g_summed)
+ flatFile += "_Summed";
+ flatFile += ".????.cub";
+ }
+ CopyCubeIntoVector(flatFile, g_flatfieldLine);
+ }
+
+ if(g_radiometric) {
+ QString radFile = ui.GetAsString("RADIOMETRICFILE");
+
+ if(radFile.toLower() == "default" || radFile.length() == 0){
+ GetCalibrationDirectory("", radFile);
+ radFile = radFile + "NAC_RadiometricResponsivity.????.pvl";
+ }
+
+ FileName radFileName(radFile);
+ if(radFileName.isVersioned())
+ radFileName = radFileName.highestVersion();
+ if(!radFileName.fileExists()) {
+ QString msg = radFile + " does not exist.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+
+ Pvl radPvl(radFileName.expanded());
+
+ if(g_iof) {
+ iTime startTime((QString) inst["StartTime"]);
+
+ try {
+ Camera *cam;
+ cam = iCube->camera();
+ cam->setTime(startTime);
+ g_solarDistance = cam->sunToBodyDist() / KM_PER_AU;
+
+ }
+ catch(IException &e) {
+ // Failed to instantiate a camera, try furnishing kernels directly
+ try {
+
+ double etStart = startTime.Et();
+ // Get the distance between the Moon and the Sun at the given time in
+ // Astronomical Units (AU)
+ QString bspKernel1 = p.MissionData("lro", "/kernels/tspk/moon_pa_de421_1900-2050.bpc", false);
+ QString bspKernel2 = p.MissionData("lro", "/kernels/tspk/de421.bsp", false);
+ furnsh_c(bspKernel1.toLatin1().data());
+ furnsh_c(bspKernel2.toLatin1().data());
+ QString pckKernel1 = p.MissionData("base", "/kernels/pck/pck?????.tpc", true);
+ QString pckKernel2 = p.MissionData("lro", "/kernels/pck/moon_080317.tf", false);
+ QString pckKernel3 = p.MissionData("lro", "/kernels/pck/moon_assoc_me.tf", false);
+ furnsh_c(pckKernel1.toLatin1().data());
+ furnsh_c(pckKernel2.toLatin1().data());
+ furnsh_c(pckKernel3.toLatin1().data());
+ double sunpos[6], lt;
+ spkezr_c("sun", etStart, "MOON_ME", "LT+S", "MOON", sunpos, <);
+ g_solarDistance = vnorm_c(sunpos) / KM_PER_AU;
+ unload_c(bspKernel1.toLatin1().data());
+ unload_c(bspKernel2.toLatin1().data());
+ unload_c(pckKernel1.toLatin1().data());
+ unload_c(pckKernel2.toLatin1().data());
+ unload_c(pckKernel3.toLatin1().data());
+ }
+ catch(IException &e) {
+ QString msg = "Unable to find the necessary SPICE kernels for converting to IOF";
+ throw IException(e, IException::User, msg, _FILEINFO_);
+ }
+ }
+ g_iofLeft = radPvl["IOF_LEFT"];
+ g_iofRight = radPvl["IOF_RIGHT"];
+ }
+ else {
+ g_radianceLeft = radPvl["Radiance_LEFT"];
+ g_radianceRight = radPvl["Radiance_RIGHT"];
+ }
+ }
+
+ // Setup the output cube
+ Cube *ocube = p.SetOutputCube("TO");
+
+ // Start the line-by-line calibration sequence
+ p.StartProcess(Calibrate);
+
+ PvlGroup calgrp("Radiometry");
+ if(g_masked) {
+ PvlKeyword darkColumns("DarkColumns");
+ for(unsigned int i = 0; i < g_maskedPixelsLeft.size(); i++)
+ darkColumns += toString(g_maskedPixelsLeft[i]);
+ for(unsigned int i = 0; i < g_maskedPixelsRight.size(); i++)
+ darkColumns += toString(g_maskedPixelsRight[i]);
+ calgrp += darkColumns;
+ }
+ if(g_dark){
+ PvlKeyword darks("DarkFiles");
+ darks.addValue(darkFiles[0]);
+ if(g_nearestDark)
+ calgrp += PvlKeyword("DarkFileType", "NearestDarkFile");
+ else if (g_nearestDarkPair){
+ calgrp += PvlKeyword("DarkFileType", "NearestDarkFilePair");
+ darks.addValue(darkFiles[1]);
+ }
+ else
+ calgrp += PvlKeyword("DarkFileType", "CustomDarkFile");
+
+ calgrp += darks;
+ }
+ if(g_nonlinear) {
+ calgrp += PvlKeyword("NonlinearOffset", offsetFile);
+ calgrp += PvlKeyword("LinearizationCoefficients", coefficientFile);
+ }
+ if(g_flatfield)
+ calgrp += PvlKeyword("FlatFile", flatFile);
+ if(g_radiometric) {
+ if(g_iof) {
+ calgrp += PvlKeyword("RadiometricType", "IOF");
+ if(g_isLeftNac)
+ calgrp += PvlKeyword("ResponsivityValue", toString(g_iofLeft));
+ else
+ calgrp += PvlKeyword("ResponsivityValue", toString(g_iofRight));
+ }
+ else {
+ calgrp += PvlKeyword("RadiometricType", "AbsoluteRadiance");
+ if(g_isLeftNac)
+ calgrp += PvlKeyword("ResponsivityValue", toString(g_radianceLeft));
+ else
+ calgrp += PvlKeyword("ResponsivityValue", toString(g_radianceRight));
+ }
+ calgrp += PvlKeyword("SolarDistance", toString(g_solarDistance));
+ }
+ ocube->putGroup(calgrp);
+ p.EndProcess();
+ }
+
+ /**
+ * This method resets global variables
+ *
+ */
+ void ResetGlobals() {
+ g_exposure = 1.0; // Exposure duration
+ g_solarDistance = 1.01; // average distance in [AU]
+
+ g_maskedPixelsLeft.clear();
+ g_maskedPixelsRight.clear();
+
+ g_radianceLeft = 1.0;
+ g_radianceRight = 1.0;
+ g_iofLeft = 1.0;
+ g_iofRight = 1.0;
+
+ g_summed = true;
+ g_masked = true;
+ g_dark = true;
+ g_nonlinear = true;
+ g_flatfield = true;
+ g_radiometric = true;
+ g_iof = true;
+ g_isLeftNac = true;
+ g_maskedLeftOnly = false;
+ g_nearestDarkPair = false;
+ g_nearestDark = false;
+ g_customDark = false;
+ g_avgDarkLineCube0.clear();
+ g_avgDarkLineCube1.clear();
+ g_linearOffsetLine.clear();
+ g_darkTimes.clear();
+ g_weightedDarkTimeAvgs.clear();
+ g_flatfieldLine.clear();
+ g_linearityCoefficients.clear();
+ g_imgTime = 0.0;
+ }
+
+ /**
+ * This method processes buffer by line to calibrate
+ *
+ * @param in Buffer to hold 1 line of cube data
+ * @param out Buffer to hold 1 line of cube data
+ *
+ */
+ void Calibrate(Buffer &in, Buffer &out) {
+ for(int i = 0; i < in.size(); i++)
+ out[i] = in[i];
+
+ if(g_masked)
+ RemoveMaskedOffset(out);
+
+ if(g_dark)
+ CorrectDark(out);
+
+ if(g_nonlinear)
+ CorrectNonlinearity(out);
+
+ if(g_flatfield)
+ CorrectFlatfield(out);
+
+ if(g_radiometric)
+ RadiometricCalibration(out);
+ }
+
+ /**
+ * Read text data file - overloaded method
+ *
+ * @param fileString QString
+ * @param data vector of double
+ *
+ */
+ void ReadTextDataFile(QString &fileString, vector &data) {
+ FileName filename(fileString);
+ if(filename.isVersioned())
+ filename = filename.highestVersion();
+ if(!filename.fileExists()) {
+ QString msg = fileString + " does not exist.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ TextFile file(filename.expanded());
+ QString lineString;
+ unsigned int line = 0;
+ while(file.GetLine(lineString)) {
+ data.push_back(toDouble(lineString.split(QRegExp("[ ,;]")).first()));
+ line++;
+ }
+ fileString = filename.expanded();
+ }
+
+ /**
+ * Read the text data file - overloaded method
+ *
+ * @param fileString QString
+ * @param data multi-dimensional vector of double
+ *
+ */
+ void ReadTextDataFile(QString &fileString, vector > &data) {
+ FileName filename(fileString);
+ if(filename.isVersioned())
+ filename = filename.highestVersion();
+ if(!filename.fileExists()) {
+ QString msg = fileString + " does not exist.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ TextFile file(filename.expanded());
+ QString lineString;
+ while(file.GetLine(lineString)) {
+ vector line;
+ lineString = lineString.simplified().remove(QRegExp("^[ ,]*")).trimmed();
+
+ QStringList lineTokens = lineString.split(QRegExp("[ ,]"), QString::SkipEmptyParts);
+ foreach (QString value, lineTokens) {
+ line.push_back(toDouble(value));
+ }
+
+ data.push_back(line);
+ }
+
+ fileString = filename.expanded();
+ }
+
+ /**
+ * Remove masked offset
+ *
+ * @param in Buffer
+ *
+ */
+ void RemoveMaskedOffset(Buffer &in) {
+ int numMasked = MASKED_PIXEL_VALUES;
+ if(g_summed)
+ numMasked /= 2;
+
+ vector statsLeft(numMasked, Statistics());
+ vector statsRight(numMasked, Statistics());
+
+ vector leftRef(numMasked, 0);
+ vector rightRef(numMasked, 0);
+
+ for(unsigned int i = 0; i < g_maskedPixelsLeft.size(); i++) {
+ statsLeft[g_maskedPixelsLeft[i] % numMasked].AddData(&in[g_maskedPixelsLeft[i]], 1);
+ leftRef[g_maskedPixelsLeft[i] % numMasked] += g_maskedPixelsLeft[i];
+ }
+
+ for(unsigned int i = 0; i < g_maskedPixelsRight.size(); i++) {
+ statsRight[g_maskedPixelsRight[i] % numMasked].AddData(&in[g_maskedPixelsRight[i]], 1);
+ rightRef[g_maskedPixelsRight[i] % numMasked] += g_maskedPixelsRight[i];
+ }
+
+ // left/rightRef is the center (average) of all the masked pixels in the set
+ for(int i = 0; i < numMasked; i++) {
+ leftRef[i] /= statsLeft[i].TotalPixels();
+ rightRef[i] /= statsRight[i].TotalPixels();
+ }
+
+ if(g_maskedLeftOnly) {
+ for(int i = 0; i < in.size(); i++) {
+ in[i] -= statsLeft[i % numMasked].Average();
+ }
+ }
+ else {
+ // If we are using both sides, we interpolate between them
+
+ for(int i = 0; i < in.size(); i++) {
+ in[i] -= (statsLeft[i % numMasked].Average() * (rightRef[i % numMasked] - i) + statsRight[i % numMasked].Average()
+ * (i - leftRef[i % numMasked])) / (rightRef[i % numMasked] - leftRef[i % numMasked]);
+ }
+ }
+ }
+
+ /**
+ * Dark Correction - will find 2 nearest dark files to perform
+ * dark correction of the pixel being processed
+ *
+ * @param in Buffer
+ *
+ */
+ void CorrectDark(Buffer &in) {
+ for (int i = 0; i < in.size(); i++) {
+ if(g_nearestDarkPair &&
+ (!IsSpecial(in[i]) || AllowedSpecialPixelType(in[i])) &&
+ (!IsSpecial(g_avgDarkLineCube0[i]) || AllowedSpecialPixelType(g_avgDarkLineCube0[i])) &&
+ (!IsSpecial(g_avgDarkLineCube1[i]) || AllowedSpecialPixelType(g_avgDarkLineCube1[i])) &&
+ (!IsSpecial(in[i]) || AllowedSpecialPixelType(in[i])) ){
+ double w0 = g_weightedDarkTimeAvgs[0];
+ double w1 = g_weightedDarkTimeAvgs[1];
+ double pixelDarkAvg = (g_avgDarkLineCube0[i]*w0)+(g_avgDarkLineCube1[i]*w1);
+
+ in[i] -= pixelDarkAvg;
+
+ } else if
+ ((!IsSpecial(g_avgDarkLineCube0[i]) || AllowedSpecialPixelType(g_avgDarkLineCube0[i])) &&
+ (!IsSpecial(in[i]) || AllowedSpecialPixelType(in[i])) ) {
+
+ in[i] -= g_avgDarkLineCube0[i];
+
+ }
+ else {
+
+ in[i] = Isis::Null;
+
+ }
+ }
+ }
+
+ /**
+ * Correct non-linearity of the pixel being processed
+ *
+ *
+ * @param in Buffer
+ */
+ void CorrectNonlinearity(Buffer &in) {
+ for(int i = 0; i < in.size(); i++) {
+ if(!IsSpecial(in[i])) {
+ in[i] += g_linearOffsetLine[i];
+
+ if(in[i] < MAXNONLIN) {
+ if(g_summed)
+ in[i] -= (1.0 / (g_linearityCoefficients[2* i ][0] * pow(g_linearityCoefficients[2* i ][1], in[i])
+ + g_linearityCoefficients[2* i ][2]) + 1.0 / (g_linearityCoefficients[2* i + 1][0] * pow(
+ g_linearityCoefficients[2* i + 1][1], in[i]) + g_linearityCoefficients[2* i + 1][2])) / 2;
+ else
+ in[i] -= 1.0 / (g_linearityCoefficients[i][0] * pow(g_linearityCoefficients[i][1], in[i])
+ + g_linearityCoefficients[i][2]);
+ }
+ }
+ else
+ in[i] = Isis::Null;
+ }
+ }
+
+ void CorrectFlatfield(Buffer &in) {
+ for(int i = 0; i < in.size(); i++) {
+ if(!IsSpecial(in[i]) && g_flatfieldLine[i] > 0)
+ in[i] /= g_flatfieldLine[i];
+ else
+ in[i] = Isis::Null;
+ }
+ }
+
+ /**
+ * Radiometric Calibration of the pixel being processed
+ *
+ *
+ * @param in Buffer
+ */
+ void RadiometricCalibration(Buffer &in) {
+ for(int i = 0; i < in.size(); i++) {
+ if(!IsSpecial(in[i])) {
+ in[i] /= g_exposure;
+ if(g_iof) {
+ if(g_isLeftNac)
+ in[i] = in[i] * pow(g_solarDistance, 2) / g_iofLeft;
+ else
+ in[i] = in[i] * pow(g_solarDistance, 2) / g_iofRight;
+ }
+ else {
+ if(g_isLeftNac)
+ in[i] = in[i] / g_radianceLeft;
+ else
+ in[i] = in[i] / g_radianceRight;
+ }
+ }
+ else
+ in[i] = Isis::Null;
+ }
+ }
+
+ /**
+ * This method returns an QString containing the path of an
+ * LRO calibration directory
+ *
+ * @param calibrationType
+ * @param calibrationDirectory Path of the calibration directory
+ *
+ * @internal
+ * @history 2020-01-06 Victor Silva - Added option for base calibration directory
+ */
+ void GetCalibrationDirectory(QString calibrationType, QString &calibrationDirectory) {
+ PvlGroup &dataDir = Preference::Preferences().findGroup("DataDirectory");
+ QString missionDir = (QString) dataDir["LRO"];
+ if(calibrationType != "")
+ calibrationType += "/";
+
+ calibrationDirectory = missionDir + "/calibration/" + calibrationType;
+ }
+
+ /**
+ * Finds the best dark files for NAC calibration.
+ *
+ * GetNearestDarkFile will get the dark file with the
+ * closest time (before or after) to the image time
+ * to be used for calibration.
+ *
+ * @param fileString String pattern defining dark files to search
+ * @param file0 Filename of dark file 1
+ */
+ void GetNearestDarkFile(QString fileString, QString &file) {
+ FileName filename(fileString);
+ QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub"
+ // create a regular expression to capture time from filenames
+ QString regexPattern(basename);
+ regexPattern.replace("*", "([0-9\\.-]*)");
+ QRegExp regex(regexPattern);
+ // create a filter for the QDir to only load files matching our name
+ QString filter(basename);
+ filter.append(".*");
+ // get a list of dark files that match our basename
+ QDir dir( filename.path(), filter );
+ vector matchedDarkTimes;
+ matchedDarkTimes.reserve(dir.count());
+ // Loop through all files in the dir that match our basename and extract time
+ for (unsigned int i=0; i < dir.count(); i++) {
+ // match against our regular expression
+ int pos = regex.indexIn(dir[i]);
+ if (pos == -1) {
+ continue; // filename did not match basename regex (time contain non-digit)
+ }
+ // Get a list of regex matches. Item 0 should be the full QString, item 1 is time.
+ QStringList texts = regex.capturedTexts();
+ if (texts.size() < 1) {
+ continue; // could not find time
+ }
+ // extract time from regex texts
+ bool timeOK;
+ int fileTime = texts[1].toInt(&timeOK);
+ if (!timeOK) {
+ continue; // time was not a valid numeric value
+ }
+ matchedDarkTimes.push_back(fileTime);
+ }
+ // sort the files by distance from nac time
+ DarkFileComparison darkComp((int)g_imgTime);
+ sort(matchedDarkTimes.begin(), matchedDarkTimes.end(), darkComp);
+ int darkTime = matchedDarkTimes[0];
+ int fileTimeIndex = fileString.indexOf("*T");
+ file = fileString;
+ file.replace(fileTimeIndex, 1, toString(darkTime));
+ CopyCubeIntoVector(file, g_avgDarkLineCube0);
+ }
+
+ /**
+ * Finds the best dark files for NAC calibration.
+ *
+ * GetNearestDarkFilePair will get the average between the two darks files
+ * that the image lies between (time-wise).
+
+ * If this pair is not found, the nearest dark file will be used
+ * for calibration.
+ *
+ * @param fileString String pattern defining dark files to search (ie. lro/calibration/nac_darks/NAC*_AverageDarks_*T_.????.cub)
+ * @param file0 Filename of dark file 1
+ * @param file1 Filename of dark file 2
+ */
+ void GetNearestDarkFilePair(QString &fileString, QString &file0, QString &file1) {
+ FileName filename(fileString);
+ QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub"
+ // create a regular expression to capture time from filenames
+ QString regexPattern(basename);
+ regexPattern.replace("*", "([0-9\\.-]*)");
+ QRegExp regex(regexPattern);
+ // create a filter for the QDir to only load files matching our name
+ QString filter(basename);
+ filter.append(".*");
+ // get a list of dark files that match our basename
+ QDir dir( filename.path(), filter );
+ vector matchedDarkTimes;
+ matchedDarkTimes.reserve(dir.count());
+ if (dir.count() < 1){
+ QString msg = "Could not find any dark file of type " + filter + ".\n";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ // Loop through all files in the dir that match our basename and extract time
+ for (unsigned int i=0; i < dir.count(); i++) {
+ // match against our regular expression
+ int pos = regex.indexIn(dir[i]);
+ if (pos == -1) {
+ continue; // filename did not match basename regex (time contain non-digit)
+ }
+ // Get a list of regex matches. Item 0 should be the full QString, item 1
+ // is time.
+ QStringList texts = regex.capturedTexts();
+ if (texts.size() < 1) {
+ continue; // could not find time
+ }
+ // extract time from regex texts
+ bool timeOK;
+ int fileTime = texts[1].toInt(&timeOK);
+ if (!timeOK) {
+ continue; // time was not a valid numeric value
+ }
+ matchedDarkTimes.push_back(fileTime);
+ }
+ // sort the files by distance from nac time
+ DarkFileComparison darkComp((int)g_imgTime);
+ sort(matchedDarkTimes.begin(), matchedDarkTimes.end(), darkComp);
+
+ int fileTimeIndex = fileString.indexOf("*T");
+ int t0 = 0;
+ int t1 = 0;
+ //Let's find the first time before the image
+ for(size_t i = 0; i < matchedDarkTimes.size(); i++){
+ if(matchedDarkTimes[i] <= (int)g_imgTime){
+ t0 = matchedDarkTimes[i];
+ break;
+ }
+ }
+ //Let's find the second time
+ for (size_t i = 0; i < matchedDarkTimes.size(); i++) {
+ if (matchedDarkTimes[i] >= (int)g_imgTime) {
+ t1 = matchedDarkTimes[i];
+ break;
+ }
+ }
+ if((t0 && t1) && (t0!=t1)){
+ int timeDayDiff = abs(t1 -t0)/86400.0;
+
+ //check time range between darks is within 45 day window
+ if (timeDayDiff < 0 || timeDayDiff > 45) {
+ QString msg = "Could not find a pair of dark files within 45 day range that includes the image [" + basename + "]. Check to make sure your set of dark files is complete.\n";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ else {
+ file0 = fileString;
+ file0.replace(fileTimeIndex, 1, toString(t0));
+ CopyCubeIntoVector(file0, g_avgDarkLineCube0);
+ g_darkTimes.push_back(t0);
+ file1 = fileString;
+ file1.replace(fileTimeIndex, 1, toString(t1));
+ CopyCubeIntoVector(file1, g_avgDarkLineCube1);
+ g_darkTimes.push_back(t1);
+ }
+ }
+ else {
+ g_nearestDark = true;
+ g_nearestDarkPair = false;
+ int darkTime = matchedDarkTimes[0];
+ file0 = fileString;
+ file0.replace(fileTimeIndex, 1, toString(darkTime));
+ CopyCubeIntoVector(file0, g_avgDarkLineCube0);
+ g_darkTimes.push_back(darkTime);
+ }
+ }
+
+ /**
+ * This method copies cube into vector
+ * LRO calibration directory
+ *
+ * @param fileString QString pointer
+ * @param data vector of double
+ *
+ */
+ void CopyCubeIntoVector(QString &fileString, vector &data) {
+ Cube cube;
+ FileName filename(fileString);
+ if(filename.isVersioned())
+ filename = filename.highestVersion();
+ if(!filename.fileExists()) {
+ QString msg = fileString + " does not exist.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+ cube.open(filename.expanded());
+ Brick brick(cube.sampleCount(), cube.lineCount(), cube.bandCount(), cube.pixelType());
+ brick.SetBasePosition(1, 1, 1);
+ cube.read(brick);
+ data.clear();
+ for(int i = 0; i < cube.sampleCount(); i++)
+ data.push_back(brick[i]);
+
+ fileString = filename.expanded();
+
+ if(data.empty()){
+ QString msg = "Copy from + " + fileString + " into vector failed.";
+ throw IException(IException::User, msg, _FILEINFO_);
+ }
+
+ }
+ /**
+ * Allow special pixel types
+ *
+ * @param pixelValue double
+ *
+ * @return bool
+ *
+ */
+ bool AllowedSpecialPixelType(double pixelValue) {
+ bool result = false;
+ result = result || IsHisPixel(pixelValue);
+ result = result || IsLisPixel(pixelValue);
+ result = result || IsHrsPixel(pixelValue);
+ result = result || IsLrsPixel(pixelValue);
+ return result;
+ }
+
+ /**
+ * Get weighted time average for calculating pixel dark
+ * average
+ *
+ * @param w0 double Weighted Time Average for dark file
+ * @param w1 double Weighted time Average for dark file
+ *
+ */
+ void GetWeightedDarkAverages() {
+
+ int iTime = (int)g_imgTime;
+ int t0 = 0;
+ int t1 = 0;
+
+ if (!g_darkTimes.empty()){
+ if (g_darkTimes.size() == 2){
+ t0 = g_darkTimes[0];
+ t1 = g_darkTimes[1];
+ double weight0 =
+ (( t1!=iTime ) * ( (t1 > iTime ) * ( t1 - iTime) ))
+ / (((( t1!=iTime ) * ( (t1 > iTime ) * ( t1 - iTime) )) +
+ (( t0!=iTime ) * ( (t0 < iTime ) * ( iTime - t0) )) ) * 1.0);
+
+ double weight1 = (( t0!=iTime ) * ( (t0 < iTime ) * ( iTime - t0) ))
+ / (((( t1!=iTime ) * ( (t1 > iTime ) * ( t1 - iTime) )) +
+ (( t0!=iTime ) * ( (t0 < iTime ) * ( iTime - t0) )) ) * 1.0);
+
+ g_weightedDarkTimeAvgs.clear();
+ g_weightedDarkTimeAvgs.push_back(weight0);
+ g_weightedDarkTimeAvgs.push_back(weight1);
+ }
+ }
+ }
+}
diff --git a/isis/src/lro/apps/lronaccal/lronaccal.h b/isis/src/lro/apps/lronaccal/lronaccal.h
new file mode 100644
index 0000000000..61cb1bee1d
--- /dev/null
+++ b/isis/src/lro/apps/lronaccal/lronaccal.h
@@ -0,0 +1,13 @@
+#ifndef lronaccal_h
+#define lronaccal_h
+
+#include "Cube.h"
+#include "lronaccal.h"
+#include "UserInterface.h"
+
+namespace Isis{
+ extern void lronaccal(UserInterface &ui);
+ extern void lronaccal(Cube *iCube, UserInterface &ui);
+}
+
+#endif
\ No newline at end of file
diff --git a/isis/src/lro/apps/lronaccal/main.cpp b/isis/src/lro/apps/lronaccal/main.cpp
index d500889b1c..aae1e1fb4e 100644
--- a/isis/src/lro/apps/lronaccal/main.cpp
+++ b/isis/src/lro/apps/lronaccal/main.cpp
@@ -1,930 +1,25 @@
-/** This is free and unencumbered software released into the public domain.
-
-The authors of ISIS do not claim copyright on the contents of this file.
-For more details about the LICENSE terms and the AUTHORS, you will
-find files of those names at the top level of this repository. **/
-
-/* SPDX-License-Identifier: CC0-1.0 */
-
#include "Isis.h"
-#include "ProcessByLine.h"
-#include "SpecialPixel.h"
-#include "Message.h"
-#include "Camera.h"
-#include "iTime.h"
-#include "IException.h"
-#include "TextFile.h"
-#include "Brick.h"
-#include "Table.h"
-#include "PvlGroup.h"
-#include "Statistics.h"
-#include
-#include
-#include
-#include
-#include
-#include
+#include "Application.h"
+#include "lronaccal.h"
-using namespace std;
using namespace Isis;
-// Working functions and parameters
-void ResetGlobals();
-void CopyCubeIntoVector(QString &fileString, vector &data);
-void ReadTextDataFile(QString &fileString, vector &data);
-void ReadTextDataFile(QString &fileString, vector > &data);
-void Calibrate(Buffer &in, Buffer &out);
-void RemoveMaskedOffset(Buffer &line);
-void CorrectDark(Buffer &in);
-void CorrectNonlinearity(Buffer &in);
-void CorrectFlatfield(Buffer &in);
-void RadiometricCalibration(Buffer &in);
-void GetNearestDarkFile(QString fileString, QString &file);
-void GetNearestDarkFilePair(QString &fileString, QString &file0, QString &file1);
-void GetCalibrationDirectory(QString calibrationType, QString &calibrationDirectory);
-void GetWeightedDarkAverages();
-bool AllowedSpecialPixelType(double pixelValue);
-
-#define LINE_SIZE 5064
-#define MAXNONLIN 600
-#define SOLAR_RADIUS 695500
-#define KM_PER_AU 149597871
-#define MASKED_PIXEL_VALUES 8
-
-/**
- * DarkFileInfo comparison object.
- *
- * Used for sorting DarkFileInfo objects. Sort first by difference from NAC time
- *
- */
-struct DarkFileComparison {
- int nacTime;
-
- DarkFileComparison(int nacTime)
- {
- this->nacTime = nacTime;
- }
-
- // sort dark files by distance from NAC time
- bool operator() ( int A, int B) {
- if (abs(nacTime - A) < abs(nacTime - B))
- return true;
- return false;
- }
-};
-
-double g_radianceLeft, g_radianceRight, g_iofLeft, g_iofRight, g_imgTime;
-double g_exposure; // Exposure duration
-double g_solarDistance; // average distance in [AU]
-
-bool g_summed, g_masked, g_maskedLeftOnly, g_dark, g_nonlinear, g_flatfield, g_radiometric, g_iof, g_isLeftNac;
-bool g_nearestDark, g_nearestDarkPair, g_customDark;
-vector g_maskedPixelsLeft, g_maskedPixelsRight;
-vector g_avgDarkLineCube0, g_avgDarkLineCube1, g_linearOffsetLine, g_flatfieldLine, g_darkTimes, g_weightedDarkTimeAvgs;
-vector > g_linearityCoefficients;
-Buffer *g_darkCube0, *g_darkCube1;
-
/**
- * This is the main lronaccal method. Lronaccal is used to calibrate LRO images
- * *
- * @internal
- * @history 2020-01-06 Victor Silva - Added option for base calibration directory
- * @history 2020-07-19 Victor Silva - Updated dark calibration to use dark file option
- * custom, nearest dark, or nearest dark pair.
- * @history 2021-01-09 Victor Silva - Added code to check for exp_code = zero and if so
- * then use only exp_code_zero dark files for dark calibration
- *
- */
-void IsisMain() {
- ResetGlobals();
- // We will be processing by line
- ProcessByLine p;
-
- // Setup the input and make sure it is a ctx file
+ *
+ * @brief Lronaccal
+ *
+ * Performs radiometric corrections to images acquired by the Narrow Angle
+ * Camera aboard the Lunar Reconnaissance Orbiter spacecraft.
+ *
+ * @author 2016-09-16 Victor Silva
+ *
+ * @internal
+ * @history 2016-09-19 Victor Silva - Adapted from lrowacpho written by Kris Becker
+ * @history 2021-03-12 Victor Silva - Updates include ability to run with default values
+ * Added new values for 2019 version of LROC Empirical function.
+ * @history 2022-04-18 Victor Silva - Refactored to make callable for GTest framework
+ */
+void IsisMain (){
UserInterface &ui = Application::GetUserInterface();
-
- g_masked = ui.GetBoolean("MASKED");
- g_dark = ui.GetBoolean("DARK");
- g_nonlinear = ui.GetBoolean("NONLINEARITY");
- g_flatfield = ui.GetBoolean("FLATFIELD");
- g_radiometric = ui.GetBoolean("RADIOMETRIC");
- g_iof = (ui.GetString("RADIOMETRICTYPE") == "IOF");
-
- Isis::Pvl lab(ui.GetCubeName("FROM"));
- Isis::PvlGroup &inst = lab.findGroup("Instrument", Pvl::Traverse);
-
- // Check if it is a NAC image
- QString instId = (QString) inst["InstrumentId"];
- instId = instId.toUpper();
- if(instId != "NACL" && instId != "NACR") {
- QString msg = "This is not a NAC image. lrocnaccal requires a NAC image.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
-
- // And check if it has already run through calibration
- if(lab.findObject("IsisCube").hasGroup("Radiometry")) {
- QString msg = "This image has already been calibrated";
- throw IException(IException::User, msg, _FILEINFO_);
- }
-
- if(lab.findObject("IsisCube").hasGroup("AlphaCube")) {
- QString msg = "This application can not be run on any image that has been geometrically transformed (i.e. scaled, rotated, sheared, or reflected) or cropped.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
-
- if(instId == "NACL")
- g_isLeftNac = true;
- else
- g_isLeftNac = false;
-
- if((int) inst["SpatialSumming"] == 1)
- g_summed = false;
- else
- g_summed = true;
-
- g_exposure = inst["LineExposureDuration"];
-
- Cube *iCube = p.SetInputCube("FROM", OneBand);
-
- // If there is any pixel in the image with a DN > 1000
- // then the "left" masked pixels are likely wiped out and useless
- if(iCube->statistics()->Maximum() > 1000)
- g_maskedLeftOnly = true;
-
- QString flatFile, offsetFile, coefficientFile;
-
- if(g_masked) {
- QString maskedFile = ui.GetAsString("MASKEDFILE");
-
- if(maskedFile.toLower() == "default" || maskedFile.length() == 0){
- GetCalibrationDirectory("", maskedFile);
- maskedFile = maskedFile + instId + "_MaskedPixels.????.pvl";
- }
-
- FileName maskedFileName(maskedFile);
- if(maskedFileName.isVersioned())
- maskedFileName = maskedFileName.highestVersion();
- if(!maskedFileName.fileExists()) {
- QString msg = maskedFile + " does not exist.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
-
- Pvl maskedPvl(maskedFileName.expanded());
- PvlKeyword maskedPixels;
- int cutoff;
- if(g_summed) {
- maskedPixels = maskedPvl["Summed"];
- cutoff = LINE_SIZE / 4;
- }
- else {
- maskedPixels = maskedPvl["FullResolution"];
- cutoff = LINE_SIZE / 2;
- }
-
- for(int i = 0; i < maskedPixels.size(); i++)
- if((g_isLeftNac && toInt(maskedPixels[i]) < cutoff) || (!g_isLeftNac && toInt(maskedPixels[i]) > cutoff))
- g_maskedPixelsLeft.push_back(toInt(maskedPixels[i]));
- else
- g_maskedPixelsRight.push_back(toInt(maskedPixels[i]));
- }
-
- vector darkFiles;
-
- if(g_dark) {
- QString darkFileType = ui.GetString("DARKFILETYPE");
- darkFileType = darkFileType.toUpper();
- if (darkFileType == "CUSTOM") {
- g_customDark = true;
- ui.GetAsString("DARKFILE", darkFiles);
- }
- else if (darkFileType == "PAIR" || darkFileType == "")
- g_nearestDarkPair = true;
- else if (darkFileType == "NEAREST"){
- g_nearestDark = true;
- }
- else {
- QString msg = "Error: Dark File Type selection failed.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- //Options are NEAREST, PAIR, and CUSTOM
- if(g_customDark){
- if(darkFiles.size() == 1 && darkFiles[0] != "") {
- CopyCubeIntoVector(darkFiles[0], g_avgDarkLineCube0);
- }
- else {
- QString msg = "Custom dark file not provided. Please provide file or choose another option.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- }
- else {
- QString darkFile;
- g_imgTime = iTime(inst["StartTime"][0]).Et();
- GetCalibrationDirectory("nac_darks", darkFile);
- darkFile = darkFile + instId + "_AverageDarks_*T";
-
- if(g_summed)
- darkFile += "_Summed";
- // use exp0 dark files if cube's exp_code=0
- Isis::PvlGroup &pvl_archive_group = lab.findGroup("Archive", Pvl::Traverse);
- if((int) pvl_archive_group["LineExposureCode"] == 0)
- darkFile += "_exp0";
-
- darkFile += ".????.cub";
-
- if(g_nearestDark){
- darkFiles.resize(1);
- GetNearestDarkFile(darkFile, darkFiles[0]);
- }
- else {
- darkFiles.resize(2);
- GetNearestDarkFilePair(darkFile, darkFiles[0], darkFiles[1]);
- //get weigted time avgs
- if(g_darkTimes.size() == 2)
- GetWeightedDarkAverages();
- }
- }
- }
-
- if(g_nonlinear) {
- offsetFile = ui.GetAsString("OFFSETFILE");
-
- if(offsetFile.toLower() == "default" || offsetFile.length() == 0) {
- GetCalibrationDirectory("", offsetFile);
- offsetFile = offsetFile + instId + "_LinearizationOffsets";
- if(g_summed)
- offsetFile += "_Summed";
- offsetFile += ".????.cub";
- }
- CopyCubeIntoVector(offsetFile, g_linearOffsetLine);
- coefficientFile = ui.GetAsString("NONLINEARITYFILE");
- if(coefficientFile.toLower() == "default" || coefficientFile.length() == 0) {
- GetCalibrationDirectory("", coefficientFile);
- coefficientFile = coefficientFile + instId + "_LinearizationCoefficients.????.txt";
- }
- ReadTextDataFile(coefficientFile, g_linearityCoefficients);
- }
-
- if(g_flatfield) {
- flatFile = ui.GetAsString("FLATFIELDFILE");
-
- if(flatFile.toLower() == "default" || flatFile.length() == 0) {
- GetCalibrationDirectory("", flatFile);
- flatFile = flatFile + instId + "_Flatfield";
- if(g_summed)
- flatFile += "_Summed";
- flatFile += ".????.cub";
- }
- CopyCubeIntoVector(flatFile, g_flatfieldLine);
- }
-
- if(g_radiometric) {
- QString radFile = ui.GetAsString("RADIOMETRICFILE");
-
- if(radFile.toLower() == "default" || radFile.length() == 0){
- GetCalibrationDirectory("", radFile);
- radFile = radFile + "NAC_RadiometricResponsivity.????.pvl";
- }
-
- FileName radFileName(radFile);
- if(radFileName.isVersioned())
- radFileName = radFileName.highestVersion();
- if(!radFileName.fileExists()) {
- QString msg = radFile + " does not exist.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
-
- Pvl radPvl(radFileName.expanded());
-
- if(g_iof) {
- iTime startTime((QString) inst["StartTime"]);
-
- try {
- Camera *cam;
- cam = iCube->camera();
- cam->setTime(startTime);
- g_solarDistance = cam->sunToBodyDist() / KM_PER_AU;
-
- }
- catch(IException &e) {
- // Failed to instantiate a camera, try furnishing kernels directly
- try {
-
- double etStart = startTime.Et();
- // Get the distance between the Moon and the Sun at the given time in
- // Astronomical Units (AU)
- QString bspKernel1 = p.MissionData("lro", "/kernels/tspk/moon_pa_de421_1900-2050.bpc", false);
- QString bspKernel2 = p.MissionData("lro", "/kernels/tspk/de421.bsp", false);
- furnsh_c(bspKernel1.toLatin1().data());
- furnsh_c(bspKernel2.toLatin1().data());
- QString pckKernel1 = p.MissionData("base", "/kernels/pck/pck?????.tpc", true);
- QString pckKernel2 = p.MissionData("lro", "/kernels/pck/moon_080317.tf", false);
- QString pckKernel3 = p.MissionData("lro", "/kernels/pck/moon_assoc_me.tf", false);
- furnsh_c(pckKernel1.toLatin1().data());
- furnsh_c(pckKernel2.toLatin1().data());
- furnsh_c(pckKernel3.toLatin1().data());
- double sunpos[6], lt;
- spkezr_c("sun", etStart, "MOON_ME", "LT+S", "MOON", sunpos, <);
- g_solarDistance = vnorm_c(sunpos) / KM_PER_AU;
- unload_c(bspKernel1.toLatin1().data());
- unload_c(bspKernel2.toLatin1().data());
- unload_c(pckKernel1.toLatin1().data());
- unload_c(pckKernel2.toLatin1().data());
- unload_c(pckKernel3.toLatin1().data());
- }
- catch(IException &e) {
- QString msg = "Unable to find the necessary SPICE kernels for converting to IOF";
- throw IException(e, IException::User, msg, _FILEINFO_);
- }
- }
- g_iofLeft = radPvl["IOF_LEFT"];
- g_iofRight = radPvl["IOF_RIGHT"];
- }
- else {
- g_radianceLeft = radPvl["Radiance_LEFT"];
- g_radianceRight = radPvl["Radiance_RIGHT"];
- }
- }
-
- // Setup the output cube
- Cube *ocube = p.SetOutputCube("TO");
-
- // Start the line-by-line calibration sequence
- p.StartProcess(Calibrate);
-
- PvlGroup calgrp("Radiometry");
- if(g_masked) {
- PvlKeyword darkColumns("DarkColumns");
- for(unsigned int i = 0; i < g_maskedPixelsLeft.size(); i++)
- darkColumns += toString(g_maskedPixelsLeft[i]);
- for(unsigned int i = 0; i < g_maskedPixelsRight.size(); i++)
- darkColumns += toString(g_maskedPixelsRight[i]);
- calgrp += darkColumns;
- }
- if(g_dark){
- PvlKeyword darks("DarkFiles");
- darks.addValue(darkFiles[0]);
- if(g_nearestDark)
- calgrp += PvlKeyword("DarkFileType", "NearestDarkFile");
- else if (g_nearestDarkPair){
- calgrp += PvlKeyword("DarkFileType", "NearestDarkFilePair");
- darks.addValue(darkFiles[1]);
- }
- else
- calgrp += PvlKeyword("DarkFileType", "CustomDarkFile");
-
- calgrp += darks;
- }
- if(g_nonlinear) {
- calgrp += PvlKeyword("NonlinearOffset", offsetFile);
- calgrp += PvlKeyword("LinearizationCoefficients", coefficientFile);
- }
- if(g_flatfield)
- calgrp += PvlKeyword("FlatFile", flatFile);
- if(g_radiometric) {
- if(g_iof) {
- calgrp += PvlKeyword("RadiometricType", "IOF");
- if(g_isLeftNac)
- calgrp += PvlKeyword("ResponsivityValue", toString(g_iofLeft));
- else
- calgrp += PvlKeyword("ResponsivityValue", toString(g_iofRight));
- }
- else {
- calgrp += PvlKeyword("RadiometricType", "AbsoluteRadiance");
- if(g_isLeftNac)
- calgrp += PvlKeyword("ResponsivityValue", toString(g_radianceLeft));
- else
- calgrp += PvlKeyword("ResponsivityValue", toString(g_radianceRight));
- }
- calgrp += PvlKeyword("SolarDistance", toString(g_solarDistance));
- }
- ocube->putGroup(calgrp);
- p.EndProcess();
-}
-
-/**
- * This method resets global variables
- *
- */
-void ResetGlobals() {
- g_exposure = 1.0; // Exposure duration
- g_solarDistance = 1.01; // average distance in [AU]
-
- g_maskedPixelsLeft.clear();
- g_maskedPixelsRight.clear();
-
- g_radianceLeft = 1.0;
- g_radianceRight = 1.0;
- g_iofLeft = 1.0;
- g_iofRight = 1.0;
-
- g_summed = true;
- g_masked = true;
- g_dark = true;
- g_nonlinear = true;
- g_flatfield = true;
- g_radiometric = true;
- g_iof = true;
- g_isLeftNac = true;
- g_maskedLeftOnly = false;
- g_nearestDarkPair = false;
- g_nearestDark = false;
- g_customDark = false;
- g_avgDarkLineCube0.clear();
- g_avgDarkLineCube1.clear();
- g_linearOffsetLine.clear();
- g_darkTimes.clear();
- g_weightedDarkTimeAvgs.clear();
- g_flatfieldLine.clear();
- g_linearityCoefficients.clear();
- g_imgTime = 0.0;
-}
-
-/**
- * This method processes buffer by line to calibrate
- *
- * @param in Buffer to hold 1 line of cube data
- * @param out Buffer to hold 1 line of cube data
- *
- */
-void Calibrate(Buffer &in, Buffer &out) {
- for(int i = 0; i < in.size(); i++)
- out[i] = in[i];
-
- if(g_masked)
- RemoveMaskedOffset(out);
-
- if(g_dark)
- CorrectDark(out);
-
- if(g_nonlinear)
- CorrectNonlinearity(out);
-
- if(g_flatfield)
- CorrectFlatfield(out);
-
- if(g_radiometric)
- RadiometricCalibration(out);
-}
-
-/**
- * Read text data file - overloaded method
- *
- * @param fileString QString
- * @param data vector of double
- *
- */
-void ReadTextDataFile(QString &fileString, vector &data) {
- FileName filename(fileString);
- if(filename.isVersioned())
- filename = filename.highestVersion();
- if(!filename.fileExists()) {
- QString msg = fileString + " does not exist.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- TextFile file(filename.expanded());
- QString lineString;
- unsigned int line = 0;
- while(file.GetLine(lineString)) {
- data.push_back(toDouble(lineString.split(QRegExp("[ ,;]")).first()));
- line++;
- }
- fileString = filename.expanded();
-}
-
-/**
- * Read the text data file - overloaded method
- *
- * @param fileString QString
- * @param data multi-dimensional vector of double
- *
- */
-void ReadTextDataFile(QString &fileString, vector > &data) {
- FileName filename(fileString);
- if(filename.isVersioned())
- filename = filename.highestVersion();
- if(!filename.fileExists()) {
- QString msg = fileString + " does not exist.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- TextFile file(filename.expanded());
- QString lineString;
- while(file.GetLine(lineString)) {
- vector line;
- lineString = lineString.simplified().remove(QRegExp("^[ ,]*")).trimmed();
-
- QStringList lineTokens = lineString.split(QRegExp("[ ,]"), QString::SkipEmptyParts);
- foreach (QString value, lineTokens) {
- line.push_back(toDouble(value));
- }
-
- data.push_back(line);
- }
-
- fileString = filename.expanded();
-}
-
-/**
- * Remove masked offset
- *
- * @param in Buffer
- *
- */
-void RemoveMaskedOffset(Buffer &in) {
- int numMasked = MASKED_PIXEL_VALUES;
- if(g_summed)
- numMasked /= 2;
-
- vector statsLeft(numMasked, Statistics());
- vector statsRight(numMasked, Statistics());
-
- vector leftRef(numMasked, 0);
- vector rightRef(numMasked, 0);
-
- for(unsigned int i = 0; i < g_maskedPixelsLeft.size(); i++) {
- statsLeft[g_maskedPixelsLeft[i] % numMasked].AddData(&in[g_maskedPixelsLeft[i]], 1);
- leftRef[g_maskedPixelsLeft[i] % numMasked] += g_maskedPixelsLeft[i];
- }
-
- for(unsigned int i = 0; i < g_maskedPixelsRight.size(); i++) {
- statsRight[g_maskedPixelsRight[i] % numMasked].AddData(&in[g_maskedPixelsRight[i]], 1);
- rightRef[g_maskedPixelsRight[i] % numMasked] += g_maskedPixelsRight[i];
- }
-
- // left/rightRef is the center (average) of all the masked pixels in the set
- for(int i = 0; i < numMasked; i++) {
- leftRef[i] /= statsLeft[i].TotalPixels();
- rightRef[i] /= statsRight[i].TotalPixels();
- }
-
- if(g_maskedLeftOnly) {
- for(int i = 0; i < in.size(); i++) {
- in[i] -= statsLeft[i % numMasked].Average();
- }
- }
- else {
- // If we are using both sides, we interpolate between them
-
- for(int i = 0; i < in.size(); i++) {
- in[i] -= (statsLeft[i % numMasked].Average() * (rightRef[i % numMasked] - i) + statsRight[i % numMasked].Average()
- * (i - leftRef[i % numMasked])) / (rightRef[i % numMasked] - leftRef[i % numMasked]);
- }
- }
-}
-
-/**
- * Dark Correction - will find 2 nearest dark files to perform
- * dark correction of the pixel being processed
- *
- * @param in Buffer
- *
- */
-void CorrectDark(Buffer &in) {
- for (int i = 0; i < in.size(); i++) {
- if(g_nearestDarkPair &&
- (!IsSpecial(in[i]) || AllowedSpecialPixelType(in[i])) &&
- (!IsSpecial(g_avgDarkLineCube0[i]) || AllowedSpecialPixelType(g_avgDarkLineCube0[i])) &&
- (!IsSpecial(g_avgDarkLineCube1[i]) || AllowedSpecialPixelType(g_avgDarkLineCube1[i])) &&
- (!IsSpecial(in[i]) || AllowedSpecialPixelType(in[i])) ){
- double w0 = g_weightedDarkTimeAvgs[0];
- double w1 = g_weightedDarkTimeAvgs[1];
- double pixelDarkAvg = (g_avgDarkLineCube0[i]*w0)+(g_avgDarkLineCube1[i]*w1);
-
- in[i] -= pixelDarkAvg;
-
- } else if
- ((!IsSpecial(g_avgDarkLineCube0[i]) || AllowedSpecialPixelType(g_avgDarkLineCube0[i])) &&
- (!IsSpecial(in[i]) || AllowedSpecialPixelType(in[i])) ) {
-
- in[i] -= g_avgDarkLineCube0[i];
-
- }
- else {
-
- in[i] = Isis::Null;
-
- }
- }
-}
-
-/**
- * Correct non-linearity of the pixel being processed
- *
- *
- * @param in Buffer
- */
-void CorrectNonlinearity(Buffer &in) {
- for(int i = 0; i < in.size(); i++) {
- if(!IsSpecial(in[i])) {
- in[i] += g_linearOffsetLine[i];
-
- if(in[i] < MAXNONLIN) {
- if(g_summed)
- in[i] -= (1.0 / (g_linearityCoefficients[2* i ][0] * pow(g_linearityCoefficients[2* i ][1], in[i])
- + g_linearityCoefficients[2* i ][2]) + 1.0 / (g_linearityCoefficients[2* i + 1][0] * pow(
- g_linearityCoefficients[2* i + 1][1], in[i]) + g_linearityCoefficients[2* i + 1][2])) / 2;
- else
- in[i] -= 1.0 / (g_linearityCoefficients[i][0] * pow(g_linearityCoefficients[i][1], in[i])
- + g_linearityCoefficients[i][2]);
- }
- }
- else
- in[i] = Isis::Null;
- }
-}
-
-void CorrectFlatfield(Buffer &in) {
- for(int i = 0; i < in.size(); i++) {
- if(!IsSpecial(in[i]) && g_flatfieldLine[i] > 0)
- in[i] /= g_flatfieldLine[i];
- else
- in[i] = Isis::Null;
- }
-}
-
-/**
- * Radiometric Calibration of the pixel being processed
- *
- *
- * @param in Buffer
- */
-void RadiometricCalibration(Buffer &in) {
- for(int i = 0; i < in.size(); i++) {
- if(!IsSpecial(in[i])) {
- in[i] /= g_exposure;
- if(g_iof) {
- if(g_isLeftNac)
- in[i] = in[i] * pow(g_solarDistance, 2) / g_iofLeft;
- else
- in[i] = in[i] * pow(g_solarDistance, 2) / g_iofRight;
- }
- else {
- if(g_isLeftNac)
- in[i] = in[i] / g_radianceLeft;
- else
- in[i] = in[i] / g_radianceRight;
- }
- }
- else
- in[i] = Isis::Null;
- }
-}
-
-/**
- * This method returns an QString containing the path of an
- * LRO calibration directory
- *
- * @param calibrationType
- * @param calibrationDirectory Path of the calibration directory
- *
- * @internal
- * @history 2020-01-06 Victor Silva - Added option for base calibration directory
- */
-void GetCalibrationDirectory(QString calibrationType, QString &calibrationDirectory) {
- PvlGroup &dataDir = Preference::Preferences().findGroup("DataDirectory");
- QString missionDir = (QString) dataDir["LRO"];
- if(calibrationType != "")
- calibrationType += "/";
-
- calibrationDirectory = missionDir + "/calibration/" + calibrationType;
-}
-
-/**
- * Finds the best dark files for NAC calibration.
- *
- * GetNearestDarkFile will get the dark file with the
- * closest time (before or after) to the image time
- * to be used for calibration.
- *
- * @param fileString String pattern defining dark files to search
- * @param file0 Filename of dark file 1
- */
-void GetNearestDarkFile(QString fileString, QString &file) {
- FileName filename(fileString);
- QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub"
- // create a regular expression to capture time from filenames
- QString regexPattern(basename);
- regexPattern.replace("*", "([0-9\\.-]*)");
- QRegExp regex(regexPattern);
- // create a filter for the QDir to only load files matching our name
- QString filter(basename);
- filter.append(".*");
- // get a list of dark files that match our basename
- QDir dir( filename.path(), filter );
- vector matchedDarkTimes;
- matchedDarkTimes.reserve(dir.count());
- // Loop through all files in the dir that match our basename and extract time
- for (unsigned int i=0; i < dir.count(); i++) {
- // match against our regular expression
- int pos = regex.indexIn(dir[i]);
- if (pos == -1) {
- continue; // filename did not match basename regex (time contain non-digit)
- }
- // Get a list of regex matches. Item 0 should be the full QString, item 1 is time.
- QStringList texts = regex.capturedTexts();
- if (texts.size() < 1) {
- continue; // could not find time
- }
- // extract time from regex texts
- bool timeOK;
- int fileTime = texts[1].toInt(&timeOK);
- if (!timeOK) {
- continue; // time was not a valid numeric value
- }
- matchedDarkTimes.push_back(fileTime);
- }
- // sort the files by distance from nac time
- DarkFileComparison darkComp((int)g_imgTime);
- sort(matchedDarkTimes.begin(), matchedDarkTimes.end(), darkComp);
- int darkTime = matchedDarkTimes[0];
- int fileTimeIndex = fileString.indexOf("*T");
- file = fileString;
- file.replace(fileTimeIndex, 1, toString(darkTime));
- CopyCubeIntoVector(file, g_avgDarkLineCube0);
-}
-
-/**
- * Finds the best dark files for NAC calibration.
- *
- * GetNearestDarkFilePair will get the average between the two darks files
- * that the image lies between (time-wise).
-
- * If this pair is not found, the nearest dark file will be used
- * for calibration.
- *
- * @param fileString String pattern defining dark files to search (ie. lro/calibration/nac_darks/NAC*_AverageDarks_*T_.????.cub)
- * @param file0 Filename of dark file 1
- * @param file1 Filename of dark file 2
- */
-void GetNearestDarkFilePair(QString &fileString, QString &file0, QString &file1) {
- FileName filename(fileString);
- QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub"
- // create a regular expression to capture time from filenames
- QString regexPattern(basename);
- regexPattern.replace("*", "([0-9\\.-]*)");
- QRegExp regex(regexPattern);
- // create a filter for the QDir to only load files matching our name
- QString filter(basename);
- filter.append(".*");
- // get a list of dark files that match our basename
- QDir dir( filename.path(), filter );
- vector matchedDarkTimes;
- matchedDarkTimes.reserve(dir.count());
- if (dir.count() < 1){
- QString msg = "Could not find any dark file of type " + filter + ".\n";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- // Loop through all files in the dir that match our basename and extract time
- for (unsigned int i=0; i < dir.count(); i++) {
- // match against our regular expression
- int pos = regex.indexIn(dir[i]);
- if (pos == -1) {
- continue; // filename did not match basename regex (time contain non-digit)
- }
- // Get a list of regex matches. Item 0 should be the full QString, item 1
- // is time.
- QStringList texts = regex.capturedTexts();
- if (texts.size() < 1) {
- continue; // could not find time
- }
- // extract time from regex texts
- bool timeOK;
- int fileTime = texts[1].toInt(&timeOK);
- if (!timeOK) {
- continue; // time was not a valid numeric value
- }
- matchedDarkTimes.push_back(fileTime);
- }
- // sort the files by distance from nac time
- DarkFileComparison darkComp((int)g_imgTime);
- sort(matchedDarkTimes.begin(), matchedDarkTimes.end(), darkComp);
-
- int fileTimeIndex = fileString.indexOf("*T");
- int t0 = 0;
- int t1 = 0;
- //Let's find the first time before the image
- for(size_t i = 0; i < matchedDarkTimes.size(); i++){
- if(matchedDarkTimes[i] <= (int)g_imgTime){
- t0 = matchedDarkTimes[i];
- break;
- }
- }
- //Let's find the second time
- for (size_t i = 0; i < matchedDarkTimes.size(); i++) {
- if (matchedDarkTimes[i] >= (int)g_imgTime) {
- t1 = matchedDarkTimes[i];
- break;
- }
- }
- if((t0 && t1) && (t0!=t1)){
- int timeDayDiff = abs(t1 -t0)/86400.0;
-
- //check time range between darks is within 45 day window
- if (timeDayDiff < 0 || timeDayDiff > 45) {
- QString msg = "Could not find a pair of dark files within 45 day range that includes the image [" + basename + "]. Check to make sure your set of dark files is complete.\n";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- else {
- file0 = fileString;
- file0.replace(fileTimeIndex, 1, toString(t0));
- CopyCubeIntoVector(file0, g_avgDarkLineCube0);
- g_darkTimes.push_back(t0);
- file1 = fileString;
- file1.replace(fileTimeIndex, 1, toString(t1));
- CopyCubeIntoVector(file1, g_avgDarkLineCube1);
- g_darkTimes.push_back(t1);
- }
- }
- else {
- g_nearestDark = true;
- g_nearestDarkPair = false;
- int darkTime = matchedDarkTimes[0];
- file0 = fileString;
- file0.replace(fileTimeIndex, 1, toString(darkTime));
- CopyCubeIntoVector(file0, g_avgDarkLineCube0);
- g_darkTimes.push_back(darkTime);
- }
-}
-
-/**
- * This method copies cube into vector
- * LRO calibration directory
- *
- * @param fileString QString pointer
- * @param data vector of double
- *
- */
-void CopyCubeIntoVector(QString &fileString, vector &data) {
- Cube cube;
- FileName filename(fileString);
- if(filename.isVersioned())
- filename = filename.highestVersion();
- if(!filename.fileExists()) {
- QString msg = fileString + " does not exist.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
- cube.open(filename.expanded());
- Brick brick(cube.sampleCount(), cube.lineCount(), cube.bandCount(), cube.pixelType());
- brick.SetBasePosition(1, 1, 1);
- cube.read(brick);
- data.clear();
- for(int i = 0; i < cube.sampleCount(); i++)
- data.push_back(brick[i]);
-
- fileString = filename.expanded();
-
- if(data.empty()){
- QString msg = "Copy from + " + fileString + " into vector failed.";
- throw IException(IException::User, msg, _FILEINFO_);
- }
-
-}
-/**
- * Allow special pixel types
- *
- * @param pixelValue double
- *
- * @return bool
- *
- */
-bool AllowedSpecialPixelType(double pixelValue) {
- bool result = false;
- result = result || IsHisPixel(pixelValue);
- result = result || IsLisPixel(pixelValue);
- result = result || IsHrsPixel(pixelValue);
- result = result || IsLrsPixel(pixelValue);
- return result;
-}
-
-/**
- * Get weighted time average for calculating pixel dark
- * average
- *
- * @param w0 double Weighted Time Average for dark file
- * @param w1 double Weighted time Average for dark file
- *
- */
-void GetWeightedDarkAverages() {
-
- int iTime = (int)g_imgTime;
- int t0 = 0;
- int t1 = 0;
-
- if (!g_darkTimes.empty()){
- if (g_darkTimes.size() == 2){
- t0 = g_darkTimes[0];
- t1 = g_darkTimes[1];
- double weight0 =
- (( t1!=iTime ) * ( (t1 > iTime ) * ( t1 - iTime) ))
- / (((( t1!=iTime ) * ( (t1 > iTime ) * ( t1 - iTime) )) +
- (( t0!=iTime ) * ( (t0 < iTime ) * ( iTime - t0) )) ) * 1.0);
-
- double weight1 = (( t0!=iTime ) * ( (t0 < iTime ) * ( iTime - t0) ))
- / (((( t1!=iTime ) * ( (t1 > iTime ) * ( t1 - iTime) )) +
- (( t0!=iTime ) * ( (t0 < iTime ) * ( iTime - t0) )) ) * 1.0);
-
- g_weightedDarkTimeAvgs.clear();
- g_weightedDarkTimeAvgs.push_back(weight0);
- g_weightedDarkTimeAvgs.push_back(weight1);
- }
- }
+ lronaccal(ui);
}
From 4bb81fbac0822858ec1e97d81e5196b99be1485f Mon Sep 17 00:00:00 2001
From: victoronline
Date: Tue, 7 Jun 2022 13:27:35 -0700
Subject: [PATCH 09/10] added gtests for 3 file types and made app callable
---
isis/src/lro/apps/lronaccal/lronaccal.cpp | 38 ++--
isis/tests/FunctionalTestsLronaccal.cpp | 173 ++++++++++++++++++
.../truth/M1333276014R.default.crop.cub | Bin 0 -> 97875 bytes
.../truth/M1333276014R.near.crop.cub | Bin 0 -> 97851 bytes
.../truth/M1333276014R.pair.crop.cub | Bin 0 -> 97848 bytes
5 files changed, 185 insertions(+), 26 deletions(-)
create mode 100644 isis/tests/FunctionalTestsLronaccal.cpp
create mode 100644 isis/tests/data/lronaccal/truth/M1333276014R.default.crop.cub
create mode 100644 isis/tests/data/lronaccal/truth/M1333276014R.near.crop.cub
create mode 100644 isis/tests/data/lronaccal/truth/M1333276014R.pair.crop.cub
diff --git a/isis/src/lro/apps/lronaccal/lronaccal.cpp b/isis/src/lro/apps/lronaccal/lronaccal.cpp
index fb1faeca7b..aa787246a1 100644
--- a/isis/src/lro/apps/lronaccal/lronaccal.cpp
+++ b/isis/src/lro/apps/lronaccal/lronaccal.cpp
@@ -85,9 +85,6 @@ namespace Isis {
vector > g_linearityCoefficients;
Buffer *g_darkCube0, *g_darkCube1;
-
-
-
/**
* @brief Calling method of the application
*
@@ -101,6 +98,7 @@ namespace Isis {
Cube iCube(ui.GetCubeName("FROM"));
lronaccal(&iCube, ui);
}
+
/**
* This is the main constructor lronaccal method. Lronaccal is used to calibrate LRO images
*
@@ -117,10 +115,6 @@ namespace Isis {
ResetGlobals();
// We will be processing by line
ProcessByLine p;
-
-
- // Setup the input and make sure it is a ctx file
- //UserInterface &ui = Application::GetUserInterface();
g_masked = ui.GetBoolean("MASKED");
g_dark = ui.GetBoolean("DARK");
@@ -163,8 +157,6 @@ namespace Isis {
g_exposure = inst["LineExposureDuration"];
- //Cube *iCube = p.SetInputCube("FROM", OneBand);
- //p.SetInputCube(iCube, OneBand);
p.SetInputCube(iCube, OneBand);
// If there is any pixel in the image with a DN > 1000
@@ -176,12 +168,10 @@ namespace Isis {
if(g_masked) {
QString maskedFile = ui.GetAsString("MASKEDFILE");
-
if(maskedFile.toLower() == "default" || maskedFile.length() == 0){
GetCalibrationDirectory("", maskedFile);
maskedFile = maskedFile + instId + "_MaskedPixels.????.pvl";
}
-
FileName maskedFileName(maskedFile);
if(maskedFileName.isVersioned())
maskedFileName = maskedFileName.highestVersion();
@@ -189,7 +179,6 @@ namespace Isis {
QString msg = maskedFile + " does not exist.";
throw IException(IException::User, msg, _FILEINFO_);
}
-
Pvl maskedPvl(maskedFileName.expanded());
PvlKeyword maskedPixels;
int cutoff;
@@ -208,7 +197,7 @@ namespace Isis {
else
g_maskedPixelsRight.push_back(toInt(maskedPixels[i]));
}
-
+
vector darkFiles;
if(g_dark) {
@@ -333,13 +322,13 @@ namespace Isis {
double etStart = startTime.Et();
// Get the distance between the Moon and the Sun at the given time in
// Astronomical Units (AU)
- QString bspKernel1 = p.MissionData("lro", "/kernels/tspk/moon_pa_de421_1900-2050.bpc", false);
- QString bspKernel2 = p.MissionData("lro", "/kernels/tspk/de421.bsp", false);
+ QString bspKernel1 = p.MissionData("lro", "$base/kernels/tspk/moon_pa_de421_1900-2050.bpc", false);
+ QString bspKernel2 = p.MissionData("lro", "$base/kernels/tspk/de421.bsp", false);
furnsh_c(bspKernel1.toLatin1().data());
furnsh_c(bspKernel2.toLatin1().data());
QString pckKernel1 = p.MissionData("base", "/kernels/pck/pck?????.tpc", true);
- QString pckKernel2 = p.MissionData("lro", "/kernels/pck/moon_080317.tf", false);
- QString pckKernel3 = p.MissionData("lro", "/kernels/pck/moon_assoc_me.tf", false);
+ QString pckKernel2 = p.MissionData("lro", "$base/kernels/pck/moon_080317.tf", false);
+ QString pckKernel3 = p.MissionData("lro", "$base/kernels/pck/moon_assoc_me.tf", false);
furnsh_c(pckKernel1.toLatin1().data());
furnsh_c(pckKernel2.toLatin1().data());
furnsh_c(pckKernel3.toLatin1().data());
@@ -365,10 +354,8 @@ namespace Isis {
g_radianceRight = radPvl["Radiance_RIGHT"];
}
}
-
// Setup the output cube
- Cube *ocube = p.SetOutputCube("TO");
-
+ Cube * oCube = p.SetOutputCube(ui.GetCubeName("TO"), ui.GetOutputAttribute("TO"));
// Start the line-by-line calibration sequence
p.StartProcess(Calibrate);
@@ -381,6 +368,7 @@ namespace Isis {
darkColumns += toString(g_maskedPixelsRight[i]);
calgrp += darkColumns;
}
+
if(g_dark){
PvlKeyword darks("DarkFiles");
darks.addValue(darkFiles[0]);
@@ -395,10 +383,12 @@ namespace Isis {
calgrp += darks;
}
+
if(g_nonlinear) {
calgrp += PvlKeyword("NonlinearOffset", offsetFile);
calgrp += PvlKeyword("LinearizationCoefficients", coefficientFile);
}
+
if(g_flatfield)
calgrp += PvlKeyword("FlatFile", flatFile);
if(g_radiometric) {
@@ -418,7 +408,8 @@ namespace Isis {
}
calgrp += PvlKeyword("SolarDistance", toString(g_solarDistance));
}
- ocube->putGroup(calgrp);
+
+ oCube->putGroup(calgrp);
p.EndProcess();
}
@@ -429,15 +420,12 @@ namespace Isis {
void ResetGlobals() {
g_exposure = 1.0; // Exposure duration
g_solarDistance = 1.01; // average distance in [AU]
-
g_maskedPixelsLeft.clear();
g_maskedPixelsRight.clear();
-
g_radianceLeft = 1.0;
g_radianceRight = 1.0;
g_iofLeft = 1.0;
g_iofRight = 1.0;
-
g_summed = true;
g_masked = true;
g_dark = true;
@@ -620,9 +608,7 @@ namespace Isis {
}
else {
-
in[i] = Isis::Null;
-
}
}
}
diff --git a/isis/tests/FunctionalTestsLronaccal.cpp b/isis/tests/FunctionalTestsLronaccal.cpp
new file mode 100644
index 0000000000..45007f347f
--- /dev/null
+++ b/isis/tests/FunctionalTestsLronaccal.cpp
@@ -0,0 +1,173 @@
+#include
+#include "Fixtures.h"
+#include "Pvl.h"
+#include "PvlGroup.h"
+#include "TestUtilities.h"
+#include "Endian.h"
+#include "PixelType.h"
+#include "lronaccal.h"
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "Histogram.h"
+#include "crop.h"
+
+using namespace std;
+using namespace Isis;
+using namespace testing;
+
+/**
+ *
+ * @brief Calibration application for the LRO NAC cameras
+ *
+ * lronaccal performs radiometric corrections to images acquired by the Narrow Angle
+ * Camera aboard the Lunar Reconnaissance Orbiter spacecraft.
+ *
+ * @author 2016-09-16 Victor Silva
+ *
+ * @internal
+ * @history 2022-04-26 Victor Silva - Original Version - Functional test is against known value for input
+ * cub since fx is not yet callable
+ */
+
+static QString APP_XML = FileName("$ISISROOT/bin/xml/lronaccal.xml").expanded();
+
+TEST(LronaccalDefault, FunctionalTestsLronaccal) {
+ QTemporaryDir outputDir;
+
+ ASSERT_TRUE(outputDir.isValid());
+
+ /*This application can not be run on any image that has been
+ geometrically transformed (i.e. scaled, rotated, sheared, or
+ reflected) or cropped
+ */
+ QString iCubeFile = "data/lronaccal/input/M1333276014R.cub";
+ QString oCubeFile = outputDir.path() + "/out.default.cub";
+ QString oCubeCropFile = outputDir.path() + "/out.default.crop.cub";
+ QString tCubeFile = "data/lronaccal/truth/M1333276014R.default.crop.cub";
+
+ QVector args = {"from="+iCubeFile, "to="+oCubeFile };
+
+ UserInterface options(APP_XML, args);
+ try {
+ lronaccal(options);
+ }
+ catch (IException &e) {
+ FAIL() << "Unable to calibrate the LRO image: " < argsCrop = {"from=" + oCubeFile, "to=" + oCubeCropFile, "sample=80", "nsamples=80", "line=80", "nlines=80"};
+ UserInterface options2(CROP_XML, argsCrop);
+ crop(options2);
+
+ Cube oCube(oCubeCropFile, "r");
+ Cube tCube(tCubeFile, "r");
+
+ Histogram *oCubeStats = oCube.histogram();
+ Histogram *tCubeStats = tCube.histogram();
+
+ EXPECT_NEAR(oCubeStats->Average(), tCubeStats->Average(), 0.001);
+ EXPECT_NEAR(oCubeStats->Sum(), tCubeStats->Sum(), 0.001);
+ EXPECT_NEAR(oCubeStats->ValidPixels(), tCubeStats->ValidPixels(), 0.001);
+ EXPECT_NEAR(oCubeStats->StandardDeviation(), tCubeStats->StandardDeviation(), 0.001);
+
+ tCube.close();
+ oCube.close();
+ }
+ catch(IException &e){
+ FAIL() << "Unable to compare stats: " < args = {"from="+iCubeFile, "to="+oCubeFile, "DarkFileType=NEAREST"};
+
+ UserInterface options(APP_XML, args);
+ try {
+ lronaccal(options);
+ }
+ catch (IException &e) {
+ FAIL() << "Unable to calibrate the LRO image: " < argsCrop = {"from=" + oCubeFile, "to=" + oCubeCropFile, "sample=80", "nsamples=80", "line=80", "nlines=80"};
+ UserInterface options2(CROP_XML, argsCrop);
+ crop(options2);
+
+ Cube oCube(oCubeCropFile, "r");
+ Cube tCube(tCubeFile, "r");
+
+ Histogram *oCubeStats = oCube.histogram();
+ Histogram *tCubeStats = tCube.histogram();
+
+ EXPECT_NEAR(oCubeStats->Average(), tCubeStats->Average(), 0.001);
+ EXPECT_NEAR(oCubeStats->Sum(), tCubeStats->Sum(), 0.001);
+ EXPECT_NEAR(oCubeStats->ValidPixels(), tCubeStats->ValidPixels(), 0.001);
+ EXPECT_NEAR(oCubeStats->StandardDeviation(), tCubeStats->StandardDeviation(), 0.001);
+
+ tCube.close();
+ oCube.close();
+ }
+ catch(IException &e){
+ FAIL() << "Unable to open output cube: " < args = {"from="+iCubeFile, "to="+oCubeFile, "DarkFileType=PAIR"};
+
+ UserInterface options(APP_XML, args);
+ try {
+ lronaccal(options);
+ }
+ catch (IException &e) {
+ FAIL() << "Unable to calibrate the LRO image: " < argsCrop = {"from=" + oCubeFile, "to=" + oCubeCropFile, "sample=80", "nsamples=80", "line=80", "nlines=80"};
+ UserInterface options2(CROP_XML, argsCrop);
+ crop(options2);
+
+ Cube oCube(oCubeCropFile, "r");
+ Cube tCube(tCubeFile, "r");
+
+ Histogram *oCubeStats = oCube.histogram();
+ Histogram *tCubeStats = tCube.histogram();
+
+ EXPECT_NEAR(oCubeStats->Average(), tCubeStats->Average(), 0.001);
+ EXPECT_NEAR(oCubeStats->Sum(), tCubeStats->Sum(), 0.001);
+ EXPECT_NEAR(oCubeStats->ValidPixels(), tCubeStats->ValidPixels(), 0.001);
+ EXPECT_NEAR(oCubeStats->StandardDeviation(), tCubeStats->StandardDeviation(), 0.001);
+
+ tCube.close();
+ oCube.close();
+ }
+ catch(IException &e){
+ FAIL() << "Unable to open output cube: " <h|8gU`&tO)U}J>8JqZ)>tBEN
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
m!_ygBM7^8W)tS6Czf
literal 0
HcmV?d00001
diff --git a/isis/tests/data/lronaccal/truth/M1333276014R.pair.crop.cub b/isis/tests/data/lronaccal/truth/M1333276014R.pair.crop.cub
new file mode 100644
index 0000000000000000000000000000000000000000..2aa70a029f6422071af4fc263bda66f895a4fae1
GIT binary patch
literal 97848
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
M4n4))AagGNKav-STmS$7
literal 0
HcmV?d00001
From 4af3b9d7b67b76b31362aa48f72b1b117e037f39 Mon Sep 17 00:00:00 2001
From: Victor Silva
Date: Tue, 7 Jun 2022 13:59:17 -0700
Subject: [PATCH 10/10] Added changes for lronaccal to CHANGELOG.md
Added changes made to lronaccal and gtests to CHANGELOG.md.
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0381395f7..aaf08a0fd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,7 +37,7 @@ release.
- Added check to determine if poles were a valid projection point in ImagePolygon when generating footprint for a map projected image. [#4390](https://github.com/USGS-Astrogeology/ISIS3/issues/4390)
- Updated the LRO photometry application Lronacpho, to use by default the current 2019 photometric model (LROC_Empirical). The model's coefficients are found in the PVL file that exists in the LRO data/mission/calibration directory. If the old parameter file is provided, the old algorithm(2014) will be used. This functionality is desired for calculation comparisons. Issue: [#4512](https://github.com/USGS-Astrogeology/ISIS3/issues/4512), PR: [#4519](https://github.com/USGS-Astrogeology/ISIS3/pull/4519)
- Added a new application, framestitch, for stitching even and odd push frame images back together prior to processing in other applications. [4924](https://github.com/USGS-Astrogeology/ISIS3/issues/4924)
-
+- Added changes to lronaccal to use time-dependent dark files for dark correction and use of specific dark files for images with exp code of zero. Also added GTests for lronaccal and refactored code to make callable. Added 3 truth cubes to testing directory. PR[#4520](https://github.com/USGS-Astrogeology/ISIS3/pull/4520)
### Changed
- Updated the LRO calibration application Lrowaccal to add a units label to the RadiometricType keyword of the Radiometry group in the output cube label if the RadiometricType parameter is Radiance. No functionality is changed if the RadiometricType parameter is IOF. Lrowaccal has also been refactored to be callable for testing purposes. Issue: [#4939](https://github.com/USGS-Astrogeology/ISIS3/issues/4939), PR: [#4940](https://github.com/USGS-Astrogeology/ISIS3/pull/4940)