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 01/19] 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 89b7a8b3dbe7800bc755f780a77286e5ec588cf5 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:05:17 -0700 Subject: [PATCH 02/19] Refactor lrowaccal app to be callable for testing and add radiance units to output PVL --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 809 +++++++++++++++++++++ isis/src/lro/apps/lrowaccal/lrowaccal.h | 33 + isis/src/lro/apps/lrowaccal/main.cpp | 810 +--------------------- 3 files changed, 849 insertions(+), 803 deletions(-) create mode 100644 isis/src/lro/apps/lrowaccal/lrowaccal.cpp create mode 100644 isis/src/lro/apps/lrowaccal/lrowaccal.h diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp new file mode 100644 index 0000000000..c23b954db6 --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -0,0 +1,809 @@ +#include "lrowaccal.h" + +using namespace std; + +#define POLAR_MODE_SAMPLES 1024 +#define NO_POLAR_MODE_SAMPLES 704 +#define BW_BANDS 1 +#define VIS_LINES 14 +#define COLOR_BANDS 5 +#define UV_SAMPLES 128 +#define UV_LINES 4 +#define UV_BANDS 2 +#define KM_PER_AU 149597871 + +using namespace std; +using namespace Isis; + + +namespace Isis { + // globals + /** + * Structure for store list of available dark file temps/times. + */ + struct DarkFileInfo { + double temp; + int time; + + DarkFileInfo(double temp, int time) + { + this->temp = temp; + this->time = time; + } + }; + + /** + * @brief DarkFileInfo comparison object. + * + * Used for sorting DarkFileInfo objects. Sort first by difference from WAC temp, then difference from WAC time + */ + struct DarkComp { + double wacTemp; + int wacTime; + + DarkComp(double wacTemp, int wacTime) + { + this->wacTemp = wacTemp; + this->wacTime = wacTime; + } + + // sort dark files by distance from wac temp, then distance from wac time + bool operator() (const DarkFileInfo &A, const DarkFileInfo &B) { + if (abs(wacTemp - A.temp) < abs(wacTemp - B.temp)) return true; + if (abs(wacTemp - A.temp) > abs(wacTemp - B.temp)) return false; + if (abs(wacTime - A.time) < abs(wacTime - B.time)) return true; + return false; + } + }; + + vector g_iofResponsivity; + vector g_radianceResponsivity; + double g_TempratureConstants[7][2]; + + bool g_dark = true, g_flatfield = true, g_radiometric = true, g_iof = true, g_specpix = true, g_temprature = true; + + double g_exposure; //!< Exposure duration + double g_solarDistance = 1.01; //!< average distance in [AU] + double g_startTemperature, g_endTemperature; + double g_temp1, g_temp2; + QString instModeId; + + int g_numFrames; + + vector g_bands; + + Buffer *g_darkCube1, *g_darkCube2, *g_flatCube, *g_specpixCube; + + // forward declared helper functions + void ResetGlobals(); + void Calibrate(Buffer &inCube, Buffer &outCube); + void CopyCubeIntoBuffer(QString &fileString, Buffer* &data); + double min(double a, double b); + QString GetCalibrationDirectory(QString calibrationType); + void GetDark(QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, double & temp1, double & temp2, QString & file1, QString & file2); + void GetMask(QString &fileString, double temp, Buffer* &data); + + /** + * @brief Calibrate a WAC cube. + * + * This is the programmatic interface to the lrowaccal application. + * + * @param ui the User Interface to parse the parameters from + */ + void lrowaccal(UserInterface &ui) { + ResetGlobals(); + + g_dark = ui.GetBoolean("DARK"); + g_flatfield = ui.GetBoolean("FLATFIELD"); + g_radiometric = ui.GetBoolean("RADIOMETRIC"); + g_iof = (ui.GetString("RADIOMETRICTYPE") == "IOF"); + g_specpix = ui.GetBoolean("SPECIALPIXELS"); + g_temprature = ui.GetBoolean("TEMPERATURE"); + + vector darkFiles; + ui.GetAsString("DARKFILE", darkFiles); + QString flatFile = ui.GetAsString("FLATFIELDFILE"); + QString radFile = ui.GetAsString("RADIOMETRICFILE"); + QString specpixFile = ui.GetAsString("SPECIALPIXELSFILE"); + QString tempFile = ui.GetAsString("TEMPERATUREFILE"); + + Cube *icube = new Cube(); + icube->open(ui.GetCubeName("FROM")); + + lrowaccal(icube, ui, darkFiles, flatFile, radFile, specpixFile, tempFile); + + delete icube; + } + + /** + * @brief Calibrate a WAC cube. + * + * This is the programmatic interface to the lrowaccal application. + * + * @param cube input cube to be calibrated + * @param ui the User Interface to parse the parameters from + */ + void lrowaccal(Cube *icube, UserInterface &ui, vector darkFiles, QString flatFile, QString radFile, QString specpixFile, QString tempFile) { + ProcessByBrick p; + p.SetInputCube(icube); + + // Make sure it is a WAC cube + Isis::PvlGroup &inst = icube->label()->findGroup("Instrument", Pvl::Traverse); + QString instId = (QString) inst["InstrumentId"]; + instId = instId.toUpper(); + if (instId != "WAC-VIS" && instId != "WAC-UV") { + QString msg = "This program is intended for use on LROC WAC images only. ["; + msg += icube->fileName() + "] does not appear to be a WAC image."; + throw IException(IException::User, msg, _FILEINFO_); + } + + // And check if it has already run through calibration + if (icube->label()->findObject("IsisCube").hasGroup("Radiometry")) { + QString msg = "This image has already been calibrated"; + throw IException(IException::User, msg, _FILEINFO_); + } + + if (icube->label()->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_); + } + + // Determine the dark/flat files to use + QString offset = (QString) inst["BackgroundOffset"]; + QString mode = (QString) inst["Mode"]; + QString instModeId = (QString) inst["InstrumentModeId"]; + instModeId = instModeId.toUpper(); + + if (instModeId == "COLOR" && (QString) inst["InstrumentId"] == "WAC-UV") + instModeId = "UV"; + else if (instModeId == "VIS") + instModeId = "COLOR"; + + g_startTemperature = (double) inst["BeginTemperatureFpa"]; + g_endTemperature = (double) inst["EndTemperatureFpa"]; + + g_numFrames = (int) inst["NumFramelets"]; + + // Figure out which bands are input + for (int i = 1; i <= icube->bandCount(); i++) { + g_bands.push_back(icube->physicalBand(i)); + } + + Isis::PvlGroup &bandBin = icube->label()->findGroup("BandBin", Pvl::Traverse); + QString filter = (QString) bandBin["Center"][0]; + QString filterNum = (QString) bandBin["FilterNumber"][0]; + //We have to pay special attention incase we are passed a + //single band image that has been "exploded" from a multiband wac + if (instModeId == "COLOR" && g_bands.size() == 1) + g_bands[0] = (toInt(filterNum) -2); + else if (instModeId == "UV" && g_bands.size() == 1) + g_bands[0] = (toInt(filterNum)); + + if (g_dark) { + if (darkFiles.size() == 0 || darkFiles[0] =="Default" || darkFiles[0].length() == 0) { + darkFiles.resize(2); + double temp = (double) inst["MiddleTemperatureFpa"]; + double time = iTime(inst["StartTime"][0]).Et(); + QString darkFile = GetCalibrationDirectory("wac_darks") + "WAC_" + instModeId; + if (instModeId == "BW") + darkFile += "_" + filter + "_Mode" + mode; + darkFile += "_Offset" + offset + "_*C_*T_Dark.????.cub"; + GetDark (darkFile, temp, time, g_darkCube1, g_darkCube2, g_temp1, g_temp2, darkFiles[0], darkFiles[1]); + } + else if (darkFiles.size() == 1) { + CopyCubeIntoBuffer(darkFiles[0], g_darkCube1); + g_temp1 = 0.0; + g_darkCube2 = new Buffer(*g_darkCube1); + g_temp2 = g_temp1; + } + else { + CopyCubeIntoBuffer(darkFiles[0], g_darkCube1); + int index = darkFiles[0].lastIndexOf("_"); + g_temp1 = IString(darkFiles[0].mid( darkFiles[0].lastIndexOf("_", index-1), index)).ToDouble(); + CopyCubeIntoBuffer(darkFiles[1], g_darkCube2); + index = darkFiles[1].lastIndexOf("_"); + g_temp2 = IString(darkFiles[1].mid( darkFiles[1].lastIndexOf("_", index-1), index)).ToDouble(); + } + } + + if (g_flatfield) { + if (flatFile.toLower() == "default" || flatFile.length() == 0) { + flatFile = GetCalibrationDirectory("wac_flats") + "WAC_" + instModeId; + if (instModeId == "BW") + flatFile += "_" + filter + "_Mode" + mode; + flatFile += "_Flatfield.????.cub"; + } + CopyCubeIntoBuffer(flatFile, g_flatCube); + + // invert the flat-field data here so we don't have to divide for every pixel of the wac + for (int i = 0; i < g_flatCube->size(); i++) { + (*g_flatCube)[i] = 1.0 / (*g_flatCube)[i]; + } + } + + PvlKeyword responsivity; + + if (g_radiometric) { + + Isis::PvlKeyword &bands = icube->label()->findGroup("BandBin", Pvl::Traverse).findKeyword("FilterNumber"); + + if (radFile.toLower() == "default" || radFile.length() == 0) + radFile = GetCalibrationDirectory("") + "WAC_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) { + responsivity = radPvl["IOF"]; + + for (int i = 0; i < bands.size(); i++) { + g_iofResponsivity.push_back(toDouble(responsivity[toInt(bands[i]) - 1])); + } + + try { + Camera *cam; + cam = icube->camera(); + iTime startTime((QString) inst["StartTime"]); + cam->setTime(startTime); + g_solarDistance = cam->sunToBodyDist() / KM_PER_AU; + } + catch(IException &e) { + try { + iTime startTime((QString) inst["StartTime"]); + 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); + NaifStatus::CheckErrors(); + furnsh_c(bspKernel1.toLatin1().data()); + NaifStatus::CheckErrors(); + furnsh_c(bspKernel2.toLatin1().data()); + NaifStatus::CheckErrors(); + 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); + NaifStatus::CheckErrors(); + furnsh_c(pckKernel1.toLatin1().data()); + NaifStatus::CheckErrors(); + furnsh_c(pckKernel2.toLatin1().data()); + NaifStatus::CheckErrors(); + furnsh_c(pckKernel3.toLatin1().data()); + NaifStatus::CheckErrors(); + 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 = "Can not find necessary SPICE kernels for converting to IOF"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + } + } + else { + responsivity = radPvl["Radiance"]; + for (int i = 0; i < bands.size(); i++) + g_radianceResponsivity.push_back(toDouble(responsivity[toInt(bands[i]) - 1])); + } + } + + if (g_specpix) { + if (specpixFile.toLower() == "default" || specpixFile.length() == 0) { + specpixFile = GetCalibrationDirectory("wac_masks") + "WAC_" + instModeId; + double temp = (double) inst["MiddleTemperatureFpa"]; + if (instModeId == "BW") + specpixFile += "_" + filter + "_Mode" + mode; + specpixFile += "_*C_SpecialPixels.????.cub"; + GetMask(specpixFile, temp, g_specpixCube); + } + else + CopyCubeIntoBuffer(specpixFile, g_specpixCube); + } + + PvlKeyword temperaturePvl("TemperatureFile"); + if (g_temprature) { + if (tempFile.toLower() == "default" || tempFile.length() == 0) + tempFile = GetCalibrationDirectory("") + "WAC_TempratureConstants.????.pvl"; + + FileName tempFileName(tempFile); + if (tempFileName.isVersioned()) + tempFileName = tempFileName.highestVersion(); + if (!tempFileName.fileExists()) { + QString msg = tempFile + " does not exist."; + throw IException(IException::User, msg, _FILEINFO_); + } + + Isis::PvlKeyword &bands = icube->label()->findGroup("BandBin", Pvl::Traverse).findKeyword("FilterNumber"); + Pvl tempPvl(tempFileName.expanded()); + temperaturePvl.addValue(tempFileName.expanded()); + for (int b = 0; b < bands.size(); b++){ + g_TempratureConstants[g_bands[b]][0]=toDouble(tempPvl[bands[b]][0]); + g_TempratureConstants[g_bands[b]][1]=toDouble(tempPvl[bands[b]][1]); + } + } + + if (instModeId == "BW") { + if (mode == "1" || mode == "0") + p.SetBrickSize(NO_POLAR_MODE_SAMPLES, VIS_LINES, (int)min(BW_BANDS, g_bands.size())); + else + p.SetBrickSize(POLAR_MODE_SAMPLES, VIS_LINES, (int)min(BW_BANDS, g_bands.size())); + } + else if (instModeId == "COLOR") { + p.SetBrickSize(NO_POLAR_MODE_SAMPLES, VIS_LINES, (int)min(COLOR_BANDS, g_bands.size())); + } + else if (instModeId == "UV") { + p.SetBrickSize(UV_SAMPLES, UV_LINES, (int)min(UV_BANDS, g_bands.size())); + } + + g_exposure = inst["ExposureDuration"]; + + Cube *ocube = p.SetOutputCube("TO"); + p.ProcessCube(Calibrate, false); + + // Add an output group with the appropriate information + PvlGroup calgrp("Radiometry"); + if (g_temprature) { + calgrp += PvlKeyword("TemperatureFile", temperaturePvl); + } + if (g_dark) { + PvlKeyword darks("DarkFiles"); + darks.addValue(darkFiles[0]); + if (darkFiles.size() > 1) + darks.addValue(darkFiles[1]); + calgrp += darks; + } + if (g_flatfield) + calgrp += PvlKeyword("FlatFile", flatFile); + if (g_radiometric) { + PvlKeyword vals("ResponsivityValues"); + if (g_iof) { + calgrp += PvlKeyword("RadiometricType", "IOF"); + for (unsigned int i=0; i< g_iofResponsivity.size(); i++) + vals.addValue(toString(g_iofResponsivity[i])); + } + else { + calgrp += PvlKeyword("RadiometricType", "AbsoluteRadiance", "W/m2/sr/um"); + for (unsigned int i=0; i< g_radianceResponsivity.size(); i++) + vals.addValue(toString(g_radianceResponsivity[i])); + } + calgrp += vals; + calgrp += PvlKeyword("SolarDistance", toString(g_solarDistance)); + } + if (g_specpix) + calgrp += PvlKeyword("SpecialPixelsFile", specpixFile); + ocube->putGroup(calgrp); + } + + void ResetGlobals () { + g_iofResponsivity.clear(); + g_radianceResponsivity.clear(); + for (int b = 0; b < 7; b++){ + g_TempratureConstants[b][0] = 0; + g_TempratureConstants[b][1] = 0; + } + + g_dark = true; + g_flatfield = true; + g_radiometric = true; + g_iof = true; + g_specpix = true; + g_temprature = true; + + g_bands.clear(); + + g_exposure = 1.0; // Exposure duration + g_solarDistance = 1.01; // average distance in [AU] + + g_temp1 = 0.0; + g_temp2 = 0.0; + + g_numFrames = 0; + + delete g_darkCube1; + delete g_darkCube2; + delete g_flatCube; + delete g_specpixCube; + } + + // Calibrate each framelet + void Calibrate (Buffer &inCube, Buffer &outCube) { + int correctBand = -1; + //If we are passed in a single band (img.cub+4) we need to pay special attention that we don't start with band1 + if (inCube.BandDimension() == 1 && g_bands.size() == 1) + correctBand = g_bands.front(); + + int frameHeight = inCube.LineDimension(); + int frameSize = inCube.SampleDimension()*inCube.LineDimension(); + int frame = inCube.Line() / frameHeight; + + // Calculate a temperature factor for the current frame (this is done to avoid doing this for each pixel + // Used in dark and temprature correction + // frameTemp: + // + // (WAC end temp - WAC start temp) + // ------------------------------- * frame + WAC start temp + // (WAC num framelets) + double frameTemp = (g_endTemperature - g_startTemperature)/g_numFrames * frame + g_startTemperature; + + for (int i = 0; i < outCube.size(); i++) + outCube[i] = inCube[i]; + + if (g_dark) { + double tempFactor = (frameTemp - g_temp2) / (g_temp1-g_temp2); + + for ( int b=0; bIndex(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, correctBand); + else + offset = g_darkCube1->Index(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, b+1); + + // We're bypassing Buffer::at for speed, so we need to make sure our + // index will not overrun the buffer + if(offset + frameSize > g_darkCube1->size()) { + QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 1)"; + throw IException(IException::Programmer, message, _FILEINFO_); + } + if(offset + frameSize > g_darkCube2->size()) { + QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 2)"; + throw IException(IException::Programmer, message, _FILEINFO_); + } + + for (int i = 0; i < frameSize; i++) { + double dark1Pixel = (*g_darkCube1)[offset + i]; + double dark2Pixel = (*g_darkCube2)[offset + i]; + double &outputPixel = outCube[i + b*frameSize]; + // Interpolate between the two darks with the current temperaturube1 + if (!IsSpecial(dark1Pixel) && !IsSpecial(dark2Pixel) && !IsSpecial(outputPixel)) { + if (g_temp1 != g_temp2) { + // Dark correction formula: + // + // (dark1Pixel - dark2Pixel) + // ------------------------- * (frameTemp - dark2Temp) + dark2Pixel + // (dark1Temp - dark2Temp) + // + // frameTemp: + // + // (WAC end temp - WAC start temp) + // ------------------------------- * frame + WAC start temp + // (WAC num framelets) + // + // tempFactor (calculated outside the loops for speed): + // + // (frameTemp - dark2Temp) + // ----------------------- + // (dark1Temp - dark2Temp) + // + outputPixel -= (dark1Pixel - dark2Pixel) * tempFactor + dark2Pixel; + } + else { + outputPixel -= dark1Pixel; + } + } + else { + outputPixel = Isis::Null; + } + } + } + } + + if (g_flatfield) { + for ( int b=0; bIndex(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, correctBand); + else + offset = g_flatCube->Index(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, b+1); + + // We're bypassing Buffer::at for speed, so we need to make sure our + // index will not overrun the buffer + if(offset + frameSize > g_flatCube->size()) { + QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Flat-field cube)"; + throw IException(IException::Programmer, message, _FILEINFO_); + } + + int outFrameOffset = b*frameSize; + for (int i = 0; i < frameSize; i++) { + double flatPixel = (*g_flatCube)[offset + i]; + double &outputPixel = outCube[i + outFrameOffset]; + + if (flatPixel > 0.0 && !IsSpecial(flatPixel) && !IsSpecial(outputPixel)) + outputPixel *= flatPixel; // The flat-field data was inverted during load so we don't have to divide here. + else + outputPixel = Isis::Null; + } + } + } + + if (g_radiometric) { + for (int i = 0; i < outCube.size(); i++) { + if (IsSpecial(outCube[i])) + outCube[i] = Isis::Null; + else { + outCube[i] /= g_exposure; + if (g_iof) + outCube[i] *= pow(g_solarDistance, 2) / g_iofResponsivity[outCube.Band(i) - 1]; + else + outCube[i] /= g_radianceResponsivity[outCube.Band(i) - 1]; + } + } + } + + if (g_specpix) { + for ( int b=0; bIndex(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, correctBand); + else + offset = g_specpixCube->Index(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, b+1); + + for (int i = 0; i < frameSize; i++) { + if (IsSpecial(g_specpixCube->at(offset + i))) + outCube[i+b*frameSize] = g_specpixCube->at(offset + i); + } + } + } + + if (g_temprature) { + for (int i = 0; i < outCube.size(); i++) { + if (IsSpecial(outCube[i])) + outCube[i] = Isis::Null; + else { + + // Temprature Correction Formula + // + // inputPixel + // --------------------- + // a*(frameTemp) + b + // + // Where: + // 'a' and 'b' are band dependant constants read in via a pvl file + // + // AND + // + // frameTemp: (Pre-Calculated as it is used in multiple places) + // + // (WAC end temp - WAC start temp) + // ------------------------------- * frame + WAC start temp + // (WAC num framelets) + // + // + // + if (correctBand != -1) + outCube[i] = outCube[i]/ (g_TempratureConstants[correctBand][0] * frameTemp + g_TempratureConstants[correctBand][1]); + else + outCube[i] = outCube[i]/ (g_TempratureConstants[outCube.Band(i)][0] * frameTemp + g_TempratureConstants[outCube.Band(i)][1]); + + } + } + } + } + + void CopyCubeIntoBuffer (QString &fileString, Buffer* &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 = new Buffer(brick); + + fileString = filename.expanded(); + } + + double min ( double a, double b ) { + if (a < b) + return a; + return b; + } + + /** + * Finds 2 best dark files for WAC calibration. + * + * GetDark will find the 2 closest available dark file tempuratures matching the given file name + * pattern. Then find the dark file at each tempurature with the time closest to the WAC tempurature. + * If there is only one tempurature, it will pick the 2 closest times at that tempurature. + * + * + * @param fileString String pattern defining dark files to search (ie. lro/calibration/wac_darks/WAC_COLOR_Offset68_*C_*T_Dark.????.cub) + * @param temp Tempurature of WAC being calibrated + * @param time Time of WAC being calibrated + * @param data1 Buffer to hold dark file 1 cub data + * @param data2 Buffer to hold dark file 2 cub data + * @param temp1 Tempurature of dark file 1 + * @param temp2 Tempurature of dark file 2 + * @param file1 Filename of dark file 1 + * @param file2 Filename of dark file 2 + */ + void GetDark(QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, double & temp1, double & temp2, QString & file1, QString & file2) { + FileName filename(fileString); + QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" + + // create a regular expression to capture the temp and 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 darkFiles; + darkFiles.reserve(dir.count()); + + // Loop through all files in the dir that match our basename and extract time and temp + for (unsigned int indx=0; indx < dir.count(); indx++) { + // match against our regular expression + int pos = regex.indexIn(dir[indx]); + if (pos == -1) { + continue; // filename did not match basename regex (time or temp contain non-digit) + } + + // Get a list of regex matches. Item 0 should be the full QString, item 1 + // is temp and item 2 is time. + QStringList texts = regex.capturedTexts(); + if (texts.size() < 3) { + continue; // could not find time and/or temp + } + + // extract time/temp from regex texts + bool tempOK, timeOK; + double fileTemp = texts[1].toDouble(&tempOK); + int fileTime = texts[2].toInt(&timeOK); + if (!tempOK || !timeOK) { + continue; // time or temp was not a valid numeric value + } + + DarkFileInfo info(fileTemp, fileTime); + darkFiles.push_back(info); + } + + // we require at least 2 different dark files to interpolate/extrapolate + if (darkFiles.size() < 2) { + QString msg = "Not enough Dark files exist for these image options [" + basename + "]. Need at least 2 files with different temperatures\n"; + throw IException(IException::User, msg, _FILEINFO_); + } + + // sort the files by distance from wac temp and time + DarkComp darkComp(temp, (int)time); + sort(darkFiles.begin(), darkFiles.end(), darkComp); + + size_t temp1Index = 0; + size_t temp2Index; + + temp1 = darkFiles[temp1Index].temp; + + for (temp2Index = temp1Index+1; temp2Index < darkFiles.size(); temp2Index++) { + if (darkFiles[temp2Index].temp != temp1) { + break; + } + } + + if (temp2Index >= darkFiles.size()) { + temp2Index = 1; + } + + temp2 = darkFiles[temp2Index].temp; + + int time1 = darkFiles[temp1Index].time; + int time2 = darkFiles[temp2Index].time; + + int tempIndex = fileString.indexOf("*C"); + int timeIndex = fileString.indexOf("*T"); + + file1 = fileString; + file1.replace(timeIndex, 1, toString(time1)); + file1.replace(tempIndex, 1, toString((int)temp1)); + + file2 = fileString; + file2.replace(timeIndex, 1, toString(time2)); + file2.replace(tempIndex, 1, toString((int)temp2)); + + CopyCubeIntoBuffer ( file1, data1 ); + CopyCubeIntoBuffer ( file2, data2 ); + } + + void GetMask(QString &fileString, double temp, Buffer* &data) { + FileName filename(fileString); + QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" + + unsigned int index = basename.indexOf("*"); + + // create a filter for the QDir to only load files matching our name + QString filter(basename); + filter.append(".*"); + + QDir dir( filename.path(), filter ); + + // create a regular expression to capture the temp and time from filenames + QString regexPattern(basename); + regexPattern.replace("*", "([0-9\\.-]*)"); + QRegExp regex(regexPattern); + + double bestTemp = DBL_MAX; + for (unsigned int indx=0; indx < dir.count(); indx++) { + // match against our regular expression + int pos = regex.indexIn(dir[indx]); + if (pos == -1) { + continue; // filename did not match basename regex (temp contain non-digit) + } + + // Get a list of regex matches. Item 0 should be the full QString, item 1 is temp + QStringList texts = regex.capturedTexts(); + if (texts.size() < 2) { + continue; // could not find temp + } + + // extract time/temp from regex texts + bool tempOK; + double fileTemp = texts[1].toDouble(&tempOK); + if (!tempOK) { + continue; // temp was not a valid numeric value + } + + if (abs(temp - fileTemp) < abs(temp - bestTemp)) { + bestTemp = fileTemp; + } + } + + if (bestTemp == DBL_MAX) { + QString msg = "No files exist for these mask options [" + basename + "]"; + throw IException(IException::User, msg, _FILEINFO_); + } + + index = fileString.indexOf("*"); + fileString.replace(index, 1, toString((int)bestTemp)); + + CopyCubeIntoBuffer ( fileString, data ); + } + + /** + * This method returns a QString containing the path of an + * LRO calibration directory + * + * @param calibrationType The type of calibration data + * + * @return @b QString Path of the calibration directory + * + * @internal + * @history 2008-11-05 Jeannie Walldren - Original version + * @history 2016-08-16 Victor Silva - Added option for base calibration directory + */ + QString GetCalibrationDirectory(QString calibrationType) { + // Get the directory where the CISS calibration directories are. + PvlGroup &dataDir = Preference::Preferences().findGroup("DataDirectory"); + QString missionDir = (QString) dataDir["LRO"]; + if(calibrationType != "") { + calibrationType += "/"; + } + + return missionDir + "/calibration/" + calibrationType; + } +} + + diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.h b/isis/src/lro/apps/lrowaccal/lrowaccal.h new file mode 100644 index 0000000000..b731e3a77c --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.h @@ -0,0 +1,33 @@ +#ifndef lrowaccal_h +#define lrowaccal_h + +#include "Cube.h" +#include "UserInterface.h" +#include +#include +#include +#include + +#include "Brick.h" +#include "Camera.h" +#include "Constants.h" +#include "CubeAttribute.h" +#include "iTime.h" +#include "Message.h" +#include "NaifStatus.h" +#include "Preference.h" +#include "ProcessByBrick.h" +#include "PvlGroup.h" +#include "SpecialPixel.h" +#include "Statistics.h" + +using namespace std; +using namespace Isis; + +namespace Isis { + extern void lrowaccal(UserInterface &ui); + extern void lrowaccal(Cube *icube, UserInterface &ui, vector darkFiles, + QString flatFile, QString radFile, QString specpixFile, QString tempFile); +} + +#endif \ No newline at end of file diff --git a/isis/src/lro/apps/lrowaccal/main.cpp b/isis/src/lro/apps/lrowaccal/main.cpp index afbb399617..e654a9761d 100644 --- a/isis/src/lro/apps/lrowaccal/main.cpp +++ b/isis/src/lro/apps/lrowaccal/main.cpp @@ -1,808 +1,12 @@ -/** 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 - -#include -#include -#include - -#include "Brick.h" -#include "Camera.h" -#include "Constants.h" -#include "Cube.h" -#include "CubeAttribute.h" -#include "iTime.h" -#include "Message.h" -#include "NaifStatus.h" -#include "Preference.h" -#include "ProcessByBrick.h" -#include "PvlGroup.h" -#include "SpecialPixel.h" -#include "Statistics.h" +//#include "Application.h" +//#include "UserInterface.h" +#include "lrowaccal.h" using namespace Isis; -using namespace std; - -#define POLAR_MODE_SAMPLES 1024 -#define NO_POLAR_MODE_SAMPLES 704 -#define BW_BANDS 1 -#define VIS_LINES 14 -#define COLOR_BANDS 5 -#define UV_SAMPLES 128 -#define UV_LINES 4 -#define UV_BANDS 2 -#define KM_PER_AU 149597871 - -void ResetGlobals (); -void Calibrate ( Buffer &in, Buffer &out ); -void CopyCubeIntoBuffer ( QString &fileString, Buffer* &data); -double min ( double a, double b ); -QString GetCalibrationDirectory(QString calibrationType); - -void GetDark(QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, double & temp1, double & temp2, QString & file1, QString & file2); -void GetMask(QString &fileString, double temp, Buffer* &data); - -vector g_iofResponsivity; -vector g_radianceResponsivity; -double g_TempratureConstants[7][2]; - -bool g_dark = true, g_flatfield = true, g_radiometric = true, g_iof = true, g_specpix = true, g_temprature = true; - -double g_exposure; //!< Exposure duration -double g_solarDistance = 1.01; //!< average distance in [AU] -double g_startTemperature, g_endTemperature; -double g_temp1, g_temp2; -QString instModeId; - -int g_numFrames; - -vector g_bands; - -Buffer *g_darkCube1, *g_darkCube2, *g_flatCube, *g_specpixCube; - -void IsisMain () { - ResetGlobals(); - UserInterface &ui = Application::GetUserInterface(); - - ProcessByBrick p; - Cube *icube = p.SetInputCube("FROM"); - - // Make sure it is a WAC cube - Isis::PvlGroup &inst = icube->label()->findGroup("Instrument", Pvl::Traverse); - QString instId = (QString) inst["InstrumentId"]; - instId = instId.toUpper(); - if (instId != "WAC-VIS" && instId != "WAC-UV") { - QString msg = "This program is intended for use on LROC WAC images only. ["; - msg += icube->fileName() + "] does not appear to be a WAC image."; - throw IException(IException::User, msg, _FILEINFO_); - } - - // And check if it has already run through calibration - if (icube->label()->findObject("IsisCube").hasGroup("Radiometry")) { - QString msg = "This image has already been calibrated"; - throw IException(IException::User, msg, _FILEINFO_); - } - - if (icube->label()->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_); - } - - g_dark = ui.GetBoolean("DARK"); - g_flatfield = ui.GetBoolean("FLATFIELD"); - g_radiometric = ui.GetBoolean("RADIOMETRIC"); - g_iof = (ui.GetString("RADIOMETRICTYPE") == "IOF"); - g_specpix = ui.GetBoolean("SPECIALPIXELS"); - g_temprature = ui.GetBoolean("TEMPERATURE"); - - // Determine the dark/flat files to use - QString offset = (QString) inst["BackgroundOffset"]; - QString mode = (QString) inst["Mode"]; - QString instModeId = (QString) inst["InstrumentModeId"]; - instModeId = instModeId.toUpper(); - - if (instModeId == "COLOR" && (QString) inst["InstrumentId"] == "WAC-UV") - instModeId = "UV"; - else if (instModeId == "VIS") - instModeId = "COLOR"; - - g_startTemperature = (double) inst["BeginTemperatureFpa"]; - g_endTemperature = (double) inst["EndTemperatureFpa"]; - - g_numFrames = (int) inst["NumFramelets"]; - - vector darkFiles; - ui.GetAsString("DARKFILE", darkFiles); - QString flatFile = ui.GetAsString("FLATFIELDFILE"); - QString radFile = ui.GetAsString("RADIOMETRICFILE"); - QString specpixFile = ui.GetAsString("SPECIALPIXELSFILE"); - QString tempFile = ui.GetAsString("TEMPERATUREFILE"); - - // Figure out which bands are input - for (int i = 1; i <= icube->bandCount(); i++) { - g_bands.push_back(icube->physicalBand(i)); - } - - Isis::PvlGroup &bandBin = icube->label()->findGroup("BandBin", Pvl::Traverse); - QString filter = (QString) bandBin["Center"][0]; - QString filterNum = (QString) bandBin["FilterNumber"][0]; - //We have to pay special attention incase we are passed a - //single band image that has been "exploded" from a multiband wac - if (instModeId == "COLOR" && g_bands.size() == 1) - g_bands[0] = (toInt(filterNum) -2); - else if (instModeId == "UV" && g_bands.size() == 1) - g_bands[0] = (toInt(filterNum)); - - if (g_dark) { - if (darkFiles.size() == 0 || darkFiles[0] =="Default" || darkFiles[0].length() == 0) { - darkFiles.resize(2); - double temp = (double) inst["MiddleTemperatureFpa"]; - double time = iTime(inst["StartTime"][0]).Et(); - QString darkFile = GetCalibrationDirectory("wac_darks") + "WAC_" + instModeId; - if (instModeId == "BW") - darkFile += "_" + filter + "_Mode" + mode; - darkFile += "_Offset" + offset + "_*C_*T_Dark.????.cub"; - GetDark (darkFile, temp, time, g_darkCube1, g_darkCube2, g_temp1, g_temp2, darkFiles[0], darkFiles[1]); - } - else if (darkFiles.size() == 1) { - CopyCubeIntoBuffer(darkFiles[0], g_darkCube1); - g_temp1 = 0.0; - g_darkCube2 = new Buffer(*g_darkCube1); - g_temp2 = g_temp1; - } - else { - CopyCubeIntoBuffer(darkFiles[0], g_darkCube1); - int index = darkFiles[0].lastIndexOf("_"); - g_temp1 = IString(darkFiles[0].mid( darkFiles[0].lastIndexOf("_", index-1), index)).ToDouble(); - CopyCubeIntoBuffer(darkFiles[1], g_darkCube2); - index = darkFiles[1].lastIndexOf("_"); - g_temp2 = IString(darkFiles[1].mid( darkFiles[1].lastIndexOf("_", index-1), index)).ToDouble(); - } - } - - if (g_flatfield) { - if (flatFile.toLower() == "default" || flatFile.length() == 0) { - flatFile = GetCalibrationDirectory("wac_flats") + "WAC_" + instModeId; - if (instModeId == "BW") - flatFile += "_" + filter + "_Mode" + mode; - flatFile += "_Flatfield.????.cub"; - } - CopyCubeIntoBuffer(flatFile, g_flatCube); - - // invert the flat-field data here so we don't have to divide for every pixel of the wac - for (int i = 0; i < g_flatCube->size(); i++) { - (*g_flatCube)[i] = 1.0 / (*g_flatCube)[i]; - } - } - - PvlKeyword responsivity; - - if (g_radiometric) { - - Isis::PvlKeyword &bands = icube->label()->findGroup("BandBin", Pvl::Traverse).findKeyword("FilterNumber"); - - if (radFile.toLower() == "default" || radFile.length() == 0) - radFile = GetCalibrationDirectory("") + "WAC_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) { - responsivity = radPvl["IOF"]; - - for (int i = 0; i < bands.size(); i++) { - g_iofResponsivity.push_back(toDouble(responsivity[toInt(bands[i]) - 1])); - } - - try { - Camera *cam; - cam = icube->camera(); - iTime startTime((QString) inst["StartTime"]); - cam->setTime(startTime); - g_solarDistance = cam->sunToBodyDist() / KM_PER_AU; - } - catch(IException &e) { - try { - iTime startTime((QString) inst["StartTime"]); - 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); - NaifStatus::CheckErrors(); - furnsh_c(bspKernel1.toLatin1().data()); - NaifStatus::CheckErrors(); - furnsh_c(bspKernel2.toLatin1().data()); - NaifStatus::CheckErrors(); - 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); - NaifStatus::CheckErrors(); - furnsh_c(pckKernel1.toLatin1().data()); - NaifStatus::CheckErrors(); - furnsh_c(pckKernel2.toLatin1().data()); - NaifStatus::CheckErrors(); - furnsh_c(pckKernel3.toLatin1().data()); - NaifStatus::CheckErrors(); - 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 = "Can not find necessary SPICE kernels for converting to IOF"; - throw IException(e, IException::User, msg, _FILEINFO_); - } - } - } - else { - responsivity = radPvl["Radiance"]; - for (int i = 0; i < bands.size(); i++) - g_radianceResponsivity.push_back(toDouble(responsivity[toInt(bands[i]) - 1])); - } - } - - if (g_specpix) { - if (specpixFile.toLower() == "default" || specpixFile.length() == 0) { - specpixFile = GetCalibrationDirectory("wac_masks") + "WAC_" + instModeId; - double temp = (double) inst["MiddleTemperatureFpa"]; - if (instModeId == "BW") - specpixFile += "_" + filter + "_Mode" + mode; - specpixFile += "_*C_SpecialPixels.????.cub"; - GetMask(specpixFile, temp, g_specpixCube); - } - else - CopyCubeIntoBuffer(specpixFile, g_specpixCube); - } - - PvlKeyword temperaturePvl("TemperatureFile"); - if (g_temprature) { - if (tempFile.toLower() == "default" || tempFile.length() == 0) - tempFile = GetCalibrationDirectory("") + "WAC_TempratureConstants.????.pvl"; - - FileName tempFileName(tempFile); - if (tempFileName.isVersioned()) - tempFileName = tempFileName.highestVersion(); - if (!tempFileName.fileExists()) { - QString msg = tempFile + " does not exist."; - throw IException(IException::User, msg, _FILEINFO_); - } - - Isis::PvlKeyword &bands = icube->label()->findGroup("BandBin", Pvl::Traverse).findKeyword("FilterNumber"); - Pvl tempPvl(tempFileName.expanded()); - temperaturePvl.addValue(tempFileName.expanded()); - for (int b = 0; b < bands.size(); b++){ - g_TempratureConstants[g_bands[b]][0]=toDouble(tempPvl[bands[b]][0]); - g_TempratureConstants[g_bands[b]][1]=toDouble(tempPvl[bands[b]][1]); - } - } - - if (instModeId == "BW") { - if (mode == "1" || mode == "0") - p.SetBrickSize(NO_POLAR_MODE_SAMPLES, VIS_LINES, (int)min(BW_BANDS, g_bands.size())); - else - p.SetBrickSize(POLAR_MODE_SAMPLES, VIS_LINES, (int)min(BW_BANDS, g_bands.size())); - } - else if (instModeId == "COLOR") { - p.SetBrickSize(NO_POLAR_MODE_SAMPLES, VIS_LINES, (int)min(COLOR_BANDS, g_bands.size())); - } - else if (instModeId == "UV") { - p.SetBrickSize(UV_SAMPLES, UV_LINES, (int)min(UV_BANDS, g_bands.size())); - } - - g_exposure = inst["ExposureDuration"]; - - Cube *ocube = p.SetOutputCube("TO"); - p.ProcessCube(Calibrate, false); - - // Add an output group with the appropriate information - PvlGroup calgrp("Radiometry"); - if (g_temprature) { - calgrp += PvlKeyword("TemperatureFile", temperaturePvl); - } - if (g_dark) { - PvlKeyword darks("DarkFiles"); - darks.addValue(darkFiles[0]); - if (darkFiles.size() > 1) - darks.addValue(darkFiles[1]); - calgrp += darks; - } - if (g_flatfield) - calgrp += PvlKeyword("FlatFile", flatFile); - if (g_radiometric) { - PvlKeyword vals("ResponsivityValues"); - if (g_iof) { - calgrp += PvlKeyword("RadiometricType", "IOF"); - for (unsigned int i=0; i< g_iofResponsivity.size(); i++) - vals.addValue(toString(g_iofResponsivity[i])); - } - else { - calgrp += PvlKeyword("RadiometricType", "AbsoluteRadiance", "W/m2/sr/um"); - for (unsigned int i=0; i< g_radianceResponsivity.size(); i++) - vals.addValue(toString(g_radianceResponsivity[i])); - } - calgrp += vals; - calgrp += PvlKeyword("SolarDistance", toString(g_solarDistance)); - } - if (g_specpix) - calgrp += PvlKeyword("SpecialPixelsFile", specpixFile); - ocube->putGroup(calgrp); - -} - -void ResetGlobals () { - g_iofResponsivity.clear(); - g_radianceResponsivity.clear(); - for (int b = 0; b < 7; b++){ - g_TempratureConstants[b][0] = 0; - g_TempratureConstants[b][1] = 0; - } - - g_dark = true; - g_flatfield = true; - g_radiometric = true; - g_iof = true; - g_specpix = true; - g_temprature = true; - - g_bands.clear(); - - g_exposure = 1.0; // Exposure duration - g_solarDistance = 1.01; // average distance in [AU] - - g_temp1 = 0.0; - g_temp2 = 0.0; - - g_numFrames = 0; - - delete g_darkCube1; - delete g_darkCube2; - delete g_flatCube; - delete g_specpixCube; -} - -// Calibrate each framelet -void Calibrate ( Buffer &inCube, Buffer &outCube ) { - int correctBand = -1; - //If we are passed in a single band (img.cub+4) we need to pay special attention that we don't start with band1 - if (inCube.BandDimension() == 1 && g_bands.size() == 1) - correctBand = g_bands.front(); - - int frameHeight = inCube.LineDimension(); - int frameSize = inCube.SampleDimension()*inCube.LineDimension(); - int frame = inCube.Line() / frameHeight; - - // Calculate a temperature factor for the current frame (this is done to avoid doing this for each pixel - // Used in dark and temprature correction - // frameTemp: - // - // (WAC end temp - WAC start temp) - // ------------------------------- * frame + WAC start temp - // (WAC num framelets) - double frameTemp = (g_endTemperature - g_startTemperature)/g_numFrames * frame + g_startTemperature; - - for (int i = 0; i < outCube.size(); i++) - outCube[i] = inCube[i]; - - if (g_dark) { - double tempFactor = (frameTemp - g_temp2) / (g_temp1-g_temp2); - - for ( int b=0; bIndex(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, correctBand); - else - offset = g_darkCube1->Index(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, b+1); - - // We're bypassing Buffer::at for speed, so we need to make sure our - // index will not overrun the buffer - if(offset + frameSize > g_darkCube1->size()) { - QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 1)"; - throw IException(IException::Programmer, message, _FILEINFO_); - } - if(offset + frameSize > g_darkCube2->size()) { - QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 2)"; - throw IException(IException::Programmer, message, _FILEINFO_); - } - - for (int i = 0; i < frameSize; i++) { - double dark1Pixel = (*g_darkCube1)[offset + i]; - double dark2Pixel = (*g_darkCube2)[offset + i]; - double &outputPixel = outCube[i + b*frameSize]; - // Interpolate between the two darks with the current temperaturube1 - if (!IsSpecial(dark1Pixel) && !IsSpecial(dark2Pixel) && !IsSpecial(outputPixel)) { - if (g_temp1 != g_temp2) { - // Dark correction formula: - // - // (dark1Pixel - dark2Pixel) - // ------------------------- * (frameTemp - dark2Temp) + dark2Pixel - // (dark1Temp - dark2Temp) - // - // frameTemp: - // - // (WAC end temp - WAC start temp) - // ------------------------------- * frame + WAC start temp - // (WAC num framelets) - // - // tempFactor (calculated outside the loops for speed): - // - // (frameTemp - dark2Temp) - // ----------------------- - // (dark1Temp - dark2Temp) - // - outputPixel -= (dark1Pixel - dark2Pixel) * tempFactor + dark2Pixel; - } - else { - outputPixel -= dark1Pixel; - } - } - else { - outputPixel = Isis::Null; - } - } - } - } - - if (g_flatfield) { - for ( int b=0; bIndex(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, correctBand); - else - offset = g_flatCube->Index(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, b+1); - - // We're bypassing Buffer::at for speed, so we need to make sure our - // index will not overrun the buffer - if(offset + frameSize > g_flatCube->size()) { - QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Flat-field cube)"; - throw IException(IException::Programmer, message, _FILEINFO_); - } - - int outFrameOffset = b*frameSize; - for (int i = 0; i < frameSize; i++) { - double flatPixel = (*g_flatCube)[offset + i]; - double &outputPixel = outCube[i + outFrameOffset]; - - if (flatPixel > 0.0 && !IsSpecial(flatPixel) && !IsSpecial(outputPixel)) - outputPixel *= flatPixel; // The flat-field data was inverted during load so we don't have to divide here. - else - outputPixel = Isis::Null; - } - } - } - - if (g_radiometric) { - for (int i = 0; i < outCube.size(); i++) { - if (IsSpecial(outCube[i])) - outCube[i] = Isis::Null; - else { - outCube[i] /= g_exposure; - if (g_iof) - outCube[i] *= pow(g_solarDistance, 2) / g_iofResponsivity[outCube.Band(i) - 1]; - else - outCube[i] /= g_radianceResponsivity[outCube.Band(i) - 1]; - } - } - } - - if (g_specpix) { - for ( int b=0; bIndex(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, correctBand); - else - offset = g_specpixCube->Index(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, b+1); - - for (int i = 0; i < frameSize; i++) { - if (IsSpecial(g_specpixCube->at(offset + i))) - outCube[i+b*frameSize] = g_specpixCube->at(offset + i); - } - } - } - - if (g_temprature) { - for (int i = 0; i < outCube.size(); i++) { - if (IsSpecial(outCube[i])) - outCube[i] = Isis::Null; - else { - - // Temprature Correction Formula - // - // inputPixel - // --------------------- - // a*(frameTemp) + b - // - // Where: - // 'a' and 'b' are band dependant constants read in via a pvl file - // - // AND - // - // frameTemp: (Pre-Calculated as it is used in multiple places) - // - // (WAC end temp - WAC start temp) - // ------------------------------- * frame + WAC start temp - // (WAC num framelets) - // - // - // - if (correctBand != -1) - outCube[i] = outCube[i]/ (g_TempratureConstants[correctBand][0] * frameTemp + g_TempratureConstants[correctBand][1]); - else - outCube[i] = outCube[i]/ (g_TempratureConstants[outCube.Band(i)][0] * frameTemp + g_TempratureConstants[outCube.Band(i)][1]); - - } - } - } -} - -void CopyCubeIntoBuffer ( QString &fileString, Buffer* &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 = new Buffer(brick); - - fileString = filename.expanded(); -} - -double min ( double a, double b ) { - if (a < b) - return a; - return b; -} - -/** - * Structure for store list of available dark file temps/times. - */ -struct DarkFileInfo { - double temp; - int time; - - DarkFileInfo(double temp, int time) - { - this->temp = temp; - this->time = time; - } -}; - -/** - * @brief DarkFileInfo comparison object. - * - * Used for sorting DarkFileInfo objects. Sort first by difference from WAC temp, then difference from WAC time - * - */ -struct DarkComp { - double wacTemp; - int wacTime; - - DarkComp(double wacTemp, int wacTime) - { - this->wacTemp = wacTemp; - this->wacTime = wacTime; - } - - // sort dark files by distance from wac temp, then distance from wac time - bool operator() (const DarkFileInfo &A, const DarkFileInfo &B) { - if (abs(wacTemp - A.temp) < abs(wacTemp - B.temp)) return true; - if (abs(wacTemp - A.temp) > abs(wacTemp - B.temp)) return false; - if (abs(wacTime - A.time) < abs(wacTime - B.time)) return true; - return false; - } -}; - -/** - * Finds 2 best dark files for WAC calibration. - * - * GetDark will find the 2 closest available dark file tempuratures matching the given file name - * pattern. Then find the dark file at each tempurature with the time closest to the WAC tempurature. - * If there is only one tempurature, it will pick the 2 closest times at that tempurature. - * - * - * @param fileString String pattern defining dark files to search (ie. lro/calibration/wac_darks/WAC_COLOR_Offset68_*C_*T_Dark.????.cub) - * @param temp Tempurature of WAC being calibrated - * @param time Time of WAC being calibrated - * @param data1 Buffer to hold dark file 1 cub data - * @param data2 Buffer to hold dark file 2 cub data - * @param temp1 Tempurature of dark file 1 - * @param temp2 Tempurature of dark file 2 - * @param file1 Filename of dark file 1 - * @param file2 Filename of dark file 2 - */ -void GetDark(QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, double & temp1, double & temp2, QString & file1, QString & file2) { - FileName filename(fileString); - QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" - - // create a regular expression to capture the temp and 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 darkFiles; - darkFiles.reserve(dir.count()); - - // Loop through all files in the dir that match our basename and extract time and temp - for (unsigned int indx=0; indx < dir.count(); indx++) { - // match against our regular expression - int pos = regex.indexIn(dir[indx]); - if (pos == -1) { - continue; // filename did not match basename regex (time or temp contain non-digit) - } - - // Get a list of regex matches. Item 0 should be the full QString, item 1 - // is temp and item 2 is time. - QStringList texts = regex.capturedTexts(); - if (texts.size() < 3) { - continue; // could not find time and/or temp - } - - // extract time/temp from regex texts - bool tempOK, timeOK; - double fileTemp = texts[1].toDouble(&tempOK); - int fileTime = texts[2].toInt(&timeOK); - if (!tempOK || !timeOK) { - continue; // time or temp was not a valid numeric value - } - - DarkFileInfo info(fileTemp, fileTime); - darkFiles.push_back(info); - } - - // we require at least 2 different dark files to interpolate/extrapolate - if (darkFiles.size() < 2) { - QString msg = "Not enough Dark files exist for these image options [" + basename + "]. Need at least 2 files with different temperatures\n"; - throw IException(IException::User, msg, _FILEINFO_); - } - - // sort the files by distance from wac temp and time - DarkComp darkComp(temp, (int)time); - sort(darkFiles.begin(), darkFiles.end(), darkComp); - - size_t temp1Index = 0; - size_t temp2Index; - - temp1 = darkFiles[temp1Index].temp; - - for (temp2Index = temp1Index+1; temp2Index < darkFiles.size(); temp2Index++) { - if (darkFiles[temp2Index].temp != temp1) { - break; - } - } - - if (temp2Index >= darkFiles.size()) { - temp2Index = 1; - } - - temp2 = darkFiles[temp2Index].temp; - - int time1 = darkFiles[temp1Index].time; - int time2 = darkFiles[temp2Index].time; - - int tempIndex = fileString.indexOf("*C"); - int timeIndex = fileString.indexOf("*T"); - - file1 = fileString; - file1.replace(timeIndex, 1, toString(time1)); - file1.replace(tempIndex, 1, toString((int)temp1)); - - file2 = fileString; - file2.replace(timeIndex, 1, toString(time2)); - file2.replace(tempIndex, 1, toString((int)temp2)); - - CopyCubeIntoBuffer ( file1, data1 ); - CopyCubeIntoBuffer ( file2, data2 ); -} - -void GetMask(QString &fileString, double temp, Buffer* &data) { - FileName filename(fileString); - QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" - - unsigned int index = basename.indexOf("*"); - - // create a filter for the QDir to only load files matching our name - QString filter(basename); - filter.append(".*"); - - QDir dir( filename.path(), filter ); - - // create a regular expression to capture the temp and time from filenames - QString regexPattern(basename); - regexPattern.replace("*", "([0-9\\.-]*)"); - QRegExp regex(regexPattern); - - double bestTemp = DBL_MAX; - for (unsigned int indx=0; indx < dir.count(); indx++) { - // match against our regular expression - int pos = regex.indexIn(dir[indx]); - if (pos == -1) { - continue; // filename did not match basename regex (temp contain non-digit) - } - - // Get a list of regex matches. Item 0 should be the full QString, item 1 is temp - QStringList texts = regex.capturedTexts(); - if (texts.size() < 2) { - continue; // could not find temp - } - - // extract time/temp from regex texts - bool tempOK; - double fileTemp = texts[1].toDouble(&tempOK); - if (!tempOK) { - continue; // temp was not a valid numeric value - } - - if (abs(temp - fileTemp) < abs(temp - bestTemp)) { - bestTemp = fileTemp; - } - } - - if (bestTemp == DBL_MAX) { - QString msg = "No files exist for these mask options [" + basename + "]"; - throw IException(IException::User, msg, _FILEINFO_); - } - - index = fileString.indexOf("*"); - fileString.replace(index, 1, toString((int)bestTemp)); - - CopyCubeIntoBuffer ( fileString, data ); -} - -/** - * This method returns a QString containing the path of an - * LRO calibration directory - * - * @param calibrationType The type of calibration data - * - * @return @b QString Path of the calibration directory - * - * @internal - * @history 2008-11-05 Jeannie Walldren - Original version - * @history 2016-08-16 Victor Silva - Added option for base calibration directory - */ -QString GetCalibrationDirectory(QString calibrationType) { - // Get the directory where the CISS calibration directories are. - PvlGroup &dataDir = Preference::Preferences().findGroup("DataDirectory"); - QString missionDir = (QString) dataDir["LRO"]; - if(calibrationType != "") { - calibrationType += "/"; - } - return missionDir + "/calibration/" + calibrationType; -} +void IsisMain() { + UserInterface &ui = Application::GetUserInterface(); + lrowaccal(ui); +} \ No newline at end of file From 2096549930d521d8f3bbb1aacc9e8ce3be532bad Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:18:43 -0700 Subject: [PATCH 03/19] Update formatting and NULL initialization --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 48 ++++++++++++----------- isis/src/lro/apps/lrowaccal/lrowaccal.h | 11 +++--- isis/src/lro/apps/lrowaccal/main.cpp | 7 ++-- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp index c23b954db6..a9fc5e23ee 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -1,7 +1,5 @@ #include "lrowaccal.h" -using namespace std; - #define POLAR_MODE_SAMPLES 1024 #define NO_POLAR_MODE_SAMPLES 704 #define BW_BANDS 1 @@ -72,7 +70,7 @@ namespace Isis { vector g_bands; - Buffer *g_darkCube1, *g_darkCube2, *g_flatCube, *g_specpixCube; + Buffer *g_darkCube1 = NULL, *g_darkCube2 = NULL, *g_flatCube = NULL, *g_specpixCube = NULL; // forward declared helper functions void ResetGlobals(); @@ -93,6 +91,10 @@ namespace Isis { void lrowaccal(UserInterface &ui) { ResetGlobals(); + Cube *icube = NULL; + icube = new Cube(); + icube->open(ui.GetCubeName("FROM")); + g_dark = ui.GetBoolean("DARK"); g_flatfield = ui.GetBoolean("FLATFIELD"); g_radiometric = ui.GetBoolean("RADIOMETRIC"); @@ -107,12 +109,10 @@ namespace Isis { QString specpixFile = ui.GetAsString("SPECIALPIXELSFILE"); QString tempFile = ui.GetAsString("TEMPERATUREFILE"); - Cube *icube = new Cube(); - icube->open(ui.GetCubeName("FROM")); - lrowaccal(icube, ui, darkFiles, flatFile, radFile, specpixFile, tempFile); delete icube; + icube = NULL; } /** @@ -188,7 +188,7 @@ namespace Isis { if (instModeId == "BW") darkFile += "_" + filter + "_Mode" + mode; darkFile += "_Offset" + offset + "_*C_*T_Dark.????.cub"; - GetDark (darkFile, temp, time, g_darkCube1, g_darkCube2, g_temp1, g_temp2, darkFiles[0], darkFiles[1]); + GetDark(darkFile, temp, time, g_darkCube1, g_darkCube2, g_temp1, g_temp2, darkFiles[0], darkFiles[1]); } else if (darkFiles.size() == 1) { CopyCubeIntoBuffer(darkFiles[0], g_darkCube1); @@ -199,10 +199,10 @@ namespace Isis { else { CopyCubeIntoBuffer(darkFiles[0], g_darkCube1); int index = darkFiles[0].lastIndexOf("_"); - g_temp1 = IString(darkFiles[0].mid( darkFiles[0].lastIndexOf("_", index-1), index)).ToDouble(); + g_temp1 = IString(darkFiles[0].mid(darkFiles[0].lastIndexOf("_", index-1), index)).ToDouble(); CopyCubeIntoBuffer(darkFiles[1], g_darkCube2); index = darkFiles[1].lastIndexOf("_"); - g_temp2 = IString(darkFiles[1].mid( darkFiles[1].lastIndexOf("_", index-1), index)).ToDouble(); + g_temp2 = IString(darkFiles[1].mid(darkFiles[1].lastIndexOf("_", index-1), index)).ToDouble(); } } @@ -248,7 +248,7 @@ namespace Isis { } try { - Camera *cam; + Camera *cam = NULL; cam = icube->camera(); iTime startTime((QString) inst["StartTime"]); cam->setTime(startTime); @@ -349,7 +349,8 @@ namespace Isis { g_exposure = inst["ExposureDuration"]; - Cube *ocube = p.SetOutputCube("TO"); + Cube *ocube = NULL; + ocube = p.SetOutputCube("TO"); p.ProcessCube(Calibrate, false); // Add an output group with the appropriate information @@ -386,7 +387,7 @@ namespace Isis { ocube->putGroup(calgrp); } - void ResetGlobals () { + void ResetGlobals() { g_iofResponsivity.clear(); g_radianceResponsivity.clear(); for (int b = 0; b < 7; b++){ @@ -418,7 +419,7 @@ namespace Isis { } // Calibrate each framelet - void Calibrate (Buffer &inCube, Buffer &outCube) { + void Calibrate(Buffer &inCube, Buffer &outCube) { int correctBand = -1; //If we are passed in a single band (img.cub+4) we need to pay special attention that we don't start with band1 if (inCube.BandDimension() == 1 && g_bands.size() == 1) @@ -443,7 +444,7 @@ namespace Isis { if (g_dark) { double tempFactor = (frameTemp - g_temp2) / (g_temp1-g_temp2); - for ( int b=0; b darkFiles; darkFiles.reserve(dir.count()); @@ -724,8 +726,8 @@ namespace Isis { file2.replace(timeIndex, 1, toString(time2)); file2.replace(tempIndex, 1, toString((int)temp2)); - CopyCubeIntoBuffer ( file1, data1 ); - CopyCubeIntoBuffer ( file2, data2 ); + CopyCubeIntoBuffer(file1, data1); + CopyCubeIntoBuffer(file2, data2); } void GetMask(QString &fileString, double temp, Buffer* &data) { @@ -738,7 +740,7 @@ namespace Isis { QString filter(basename); filter.append(".*"); - QDir dir( filename.path(), filter ); + QDir dir(filename.path(), filter); // create a regular expression to capture the temp and time from filenames QString regexPattern(basename); @@ -779,7 +781,7 @@ namespace Isis { index = fileString.indexOf("*"); fileString.replace(index, 1, toString((int)bestTemp)); - CopyCubeIntoBuffer ( fileString, data ); + CopyCubeIntoBuffer(fileString, data); } /** diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.h b/isis/src/lro/apps/lrowaccal/lrowaccal.h index b731e3a77c..5eb7d00a88 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.h +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.h @@ -1,16 +1,16 @@ #ifndef lrowaccal_h #define lrowaccal_h -#include "Cube.h" -#include "UserInterface.h" #include -#include + #include #include +#include #include "Brick.h" #include "Camera.h" #include "Constants.h" +#include "Cube.h" #include "CubeAttribute.h" #include "iTime.h" #include "Message.h" @@ -20,13 +20,12 @@ #include "PvlGroup.h" #include "SpecialPixel.h" #include "Statistics.h" +#include "UserInterface.h" -using namespace std; -using namespace Isis; namespace Isis { extern void lrowaccal(UserInterface &ui); - extern void lrowaccal(Cube *icube, UserInterface &ui, vector darkFiles, + extern void lrowaccal(Cube *icube, UserInterface &ui, std::vector darkFiles, QString flatFile, QString radFile, QString specpixFile, QString tempFile); } diff --git a/isis/src/lro/apps/lrowaccal/main.cpp b/isis/src/lro/apps/lrowaccal/main.cpp index e654a9761d..045b9bd918 100644 --- a/isis/src/lro/apps/lrowaccal/main.cpp +++ b/isis/src/lro/apps/lrowaccal/main.cpp @@ -1,12 +1,11 @@ #include "Isis.h" -//#include "Application.h" -//#include "UserInterface.h" #include "lrowaccal.h" using namespace Isis; + void IsisMain() { - UserInterface &ui = Application::GetUserInterface(); - lrowaccal(ui); + UserInterface &ui = Application::GetUserInterface(); + lrowaccal(ui); } \ No newline at end of file From fc4197522f5b26c49611b58f1b100578a0046e00 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Tue, 26 Apr 2022 16:39:04 -0700 Subject: [PATCH 04/19] Add NULL assignments after deletions --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp index a9fc5e23ee..d3605c962b 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -413,9 +413,13 @@ namespace Isis { g_numFrames = 0; delete g_darkCube1; + g_darkCube1 = NULL; delete g_darkCube2; + g_darkCube2 = NULL; delete g_flatCube; + g_flatCube = NULL; delete g_specpixCube; + g_specpixCube = NULL; } // Calibrate each framelet From f3cb0f1275780faa6fbe343a990d7f6958211295 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Fri, 29 Apr 2022 12:59:50 -0700 Subject: [PATCH 05/19] Add CubeAttributeOutput to output cube initialization --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp index d3605c962b..a5fbfd9c34 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -350,7 +350,7 @@ namespace Isis { g_exposure = inst["ExposureDuration"]; Cube *ocube = NULL; - ocube = p.SetOutputCube("TO"); + ocube = p.SetOutputCube(ui.GetCubeName("TO"), ui.GetOutputAttribute("TO")); p.ProcessCube(Calibrate, false); // Add an output group with the appropriate information From dc5a5c64cd2462463d209c3fa3b27c45037bd28d Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:13:14 -0700 Subject: [PATCH 06/19] Add functional test for lrowaccal units label addition and accompanying test cube --- isis/tests/FunctionalTestsLrowaccal.cpp | 40 ++++++++++++++++++ .../M1388981421CE.tmp.vis.even.reduced.cub | Bin 0 -> 141337 bytes 2 files changed, 40 insertions(+) create mode 100644 isis/tests/FunctionalTestsLrowaccal.cpp create mode 100644 isis/tests/data/lrowaccal/M1388981421CE.tmp.vis.even.reduced.cub diff --git a/isis/tests/FunctionalTestsLrowaccal.cpp b/isis/tests/FunctionalTestsLrowaccal.cpp new file mode 100644 index 0000000000..e2756d2b07 --- /dev/null +++ b/isis/tests/FunctionalTestsLrowaccal.cpp @@ -0,0 +1,40 @@ +#include "Fixtures.h" +#include "Pvl.h" +#include "PvlGroup.h" +#include "TestUtilities.h" + +#include "lrowaccal.h" + +#include "gtest/gtest.h" + +using namespace Isis; + +static QString APP_XML = FileName("$ISISROOT/bin/xml/lrowaccal.xml").expanded(); + +TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelExists) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QString outCubeFileName = tempDir.path() + "/outTemp.cub"; + QString testCubeFileName = "data/lrowaccal/M1388981421CE.tmp.vis.even.reduced.cub"; + + QVector args = {"from=" + testCubeFileName, + "to=" + outCubeFileName, + "radiometrictype=Radiance", + "radiometricfile=Default"}; + UserInterface options(APP_XML, args); + + try { + lrowaccal(options); + } + catch(IException &e) { + FAIL() << "Unable to open image cube: " << e.what() << std::endl; + } + + Cube outCube(outCubeFileName); + Pvl *outCubeLabel = outCube.label(); + + PvlGroup &radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); + + EXPECT_EQ(radiometryGroup["RadiometricType"].unit().toStdString(), "W/m2/sr/um"); +} diff --git a/isis/tests/data/lrowaccal/M1388981421CE.tmp.vis.even.reduced.cub b/isis/tests/data/lrowaccal/M1388981421CE.tmp.vis.even.reduced.cub new file mode 100644 index 0000000000000000000000000000000000000000..32d4337226334cfbd830cc6bfd5539ae53a75ad6 GIT binary patch literal 141337 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 KA}08v&i*G;iXQa< literal 0 HcmV?d00001 From 58eaa15e0a094ef4a05a7e88f3914b4f25068929 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Tue, 3 May 2022 14:55:57 -0700 Subject: [PATCH 07/19] Refactor lrowaccal to remove global variables and convert helper functions to lambdas with captures --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 957 +++++++++++----------- isis/src/lro/apps/lrowaccal/lrowaccal.h | 3 +- 2 files changed, 466 insertions(+), 494 deletions(-) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp index a5fbfd9c34..6348c338f5 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -16,85 +16,93 @@ using namespace Isis; namespace Isis { // globals - /** - * Structure for store list of available dark file temps/times. - */ - struct DarkFileInfo { - double temp; - int time; - - DarkFileInfo(double temp, int time) - { - this->temp = temp; - this->time = time; - } - }; /** - * @brief DarkFileInfo comparison object. + * @brief Calibrate a WAC cube. * - * Used for sorting DarkFileInfo objects. Sort first by difference from WAC temp, then difference from WAC time + * This is the programmatic interface to the lrowaccal application. + * + * @param ui the User Interface to parse the parameters from */ - struct DarkComp { - double wacTemp; - int wacTime; - - DarkComp(double wacTemp, int wacTime) - { - this->wacTemp = wacTemp; - this->wacTime = wacTime; - } - - // sort dark files by distance from wac temp, then distance from wac time - bool operator() (const DarkFileInfo &A, const DarkFileInfo &B) { - if (abs(wacTemp - A.temp) < abs(wacTemp - B.temp)) return true; - if (abs(wacTemp - A.temp) > abs(wacTemp - B.temp)) return false; - if (abs(wacTime - A.time) < abs(wacTime - B.time)) return true; - return false; - } - }; - - vector g_iofResponsivity; - vector g_radianceResponsivity; - double g_TempratureConstants[7][2]; - - bool g_dark = true, g_flatfield = true, g_radiometric = true, g_iof = true, g_specpix = true, g_temprature = true; - - double g_exposure; //!< Exposure duration - double g_solarDistance = 1.01; //!< average distance in [AU] - double g_startTemperature, g_endTemperature; - double g_temp1, g_temp2; - QString instModeId; - - int g_numFrames; - - vector g_bands; + void lrowaccal(UserInterface &ui) { + Cube *icube = NULL; + icube = new Cube(); + icube->open(ui.GetCubeName("FROM")); - Buffer *g_darkCube1 = NULL, *g_darkCube2 = NULL, *g_flatCube = NULL, *g_specpixCube = NULL; + lrowaccal(icube, ui); - // forward declared helper functions - void ResetGlobals(); - void Calibrate(Buffer &inCube, Buffer &outCube); - void CopyCubeIntoBuffer(QString &fileString, Buffer* &data); - double min(double a, double b); - QString GetCalibrationDirectory(QString calibrationType); - void GetDark(QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, double & temp1, double & temp2, QString & file1, QString & file2); - void GetMask(QString &fileString, double temp, Buffer* &data); + delete icube; + icube = NULL; + } /** - * @brief Calibrate a WAC cube. + * @brief Calibrate a WAC cube. * * This is the programmatic interface to the lrowaccal application. * + * @param cube input cube to be calibrated * @param ui the User Interface to parse the parameters from */ - void lrowaccal(UserInterface &ui) { - ResetGlobals(); + void lrowaccal(Cube *icube, UserInterface &ui) { + /** + * Structure for storing list of available dark file temps/times. + */ + struct DarkFileInfo { + double temp; + int time; + + DarkFileInfo(double temp, int time) + { + this->temp = temp; + this->time = time; + } + }; + + /** + * @brief DarkFileInfo comparison object. + * + * Used for sorting DarkFileInfo objects. Sort first by difference from WAC temp, then difference from WAC time + */ + struct DarkComp { + double wacTemp; + int wacTime; + + DarkComp(double wacTemp, int wacTime) + { + this->wacTemp = wacTemp; + this->wacTime = wacTime; + } - Cube *icube = NULL; - icube = new Cube(); - icube->open(ui.GetCubeName("FROM")); + // sort dark files by distance from wac temp, then distance from wac time + bool operator() (const DarkFileInfo &A, const DarkFileInfo &B) { + if (abs(wacTemp - A.temp) < abs(wacTemp - B.temp)) return true; + if (abs(wacTemp - A.temp) > abs(wacTemp - B.temp)) return false; + if (abs(wacTime - A.time) < abs(wacTime - B.time)) return true; + return false; + } + }; + + vector g_iofResponsivity; + vector g_radianceResponsivity; + double g_TempratureConstants[7][2]; + for (int b = 0; b < 7; b++){ + g_TempratureConstants[b][0] = 0; + g_TempratureConstants[b][1] = 0; + } + + bool g_dark = true, g_flatfield = true, g_radiometric = true, g_iof = true, g_specpix = true, g_temprature = true; + double g_exposure = 1.0; //!< Exposure duration + double g_solarDistance = 1.01; //!< average distance in [AU] + double g_startTemperature, g_endTemperature; + double g_temp1 = 0.0, g_temp2 = 0.0; + + int g_numFrames = 0; + + vector g_bands; + + Buffer *g_darkCube1 = NULL, *g_darkCube2 = NULL, *g_flatCube = NULL, *g_specpixCube = NULL; + g_dark = ui.GetBoolean("DARK"); g_flatfield = ui.GetBoolean("FLATFIELD"); g_radiometric = ui.GetBoolean("RADIOMETRIC"); @@ -109,21 +117,401 @@ namespace Isis { QString specpixFile = ui.GetAsString("SPECIALPIXELSFILE"); QString tempFile = ui.GetAsString("TEMPERATUREFILE"); - lrowaccal(icube, ui, darkFiles, flatFile, radFile, specpixFile, tempFile); + // + // Helper functions + // + auto min = [](double a, double b) -> double { + return (a < b) ? a : b; + }; + + auto CopyCubeIntoBuffer = [](QString &fileString, Buffer* &data) -> void { + 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 = NULL; + data = new Buffer(brick); + + fileString = filename.expanded(); + }; + + /** + * Finds 2 best dark files for WAC calibration. + * + * GetDark will find the 2 closest available dark file tempuratures matching the given file name + * pattern. Then find the dark file at each tempurature with the time closest to the WAC tempurature. + * If there is only one tempurature, it will pick the 2 closest times at that tempurature. + * + * + * @param fileString String pattern defining dark files to search (ie. lro/calibration/wac_darks/WAC_COLOR_Offset68_*C_*T_Dark.????.cub) + * @param temp Tempurature of WAC being calibrated + * @param time Time of WAC being calibrated + * @param data1 Buffer to hold dark file 1 cub data + * @param data2 Buffer to hold dark file 2 cub data + * @param temp1 Tempurature of dark file 1 + * @param temp2 Tempurature of dark file 2 + * @param file1 Filename of dark file 1 + * @param file2 Filename of dark file 2 + */ + auto GetDark = [CopyCubeIntoBuffer](QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, + double & temp1, double & temp2, QString & file1, QString & file2) -> void { + FileName filename(fileString); + QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" + + // create a regular expression to capture the temp and 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 darkFiles; + darkFiles.reserve(dir.count()); + + // Loop through all files in the dir that match our basename and extract time and temp + for (unsigned int indx=0; indx < dir.count(); indx++) { + // match against our regular expression + int pos = regex.indexIn(dir[indx]); + if (pos == -1) { + continue; // filename did not match basename regex (time or temp contain non-digit) + } - delete icube; - icube = NULL; - } + // Get a list of regex matches. Item 0 should be the full QString, item 1 + // is temp and item 2 is time. + QStringList texts = regex.capturedTexts(); + if (texts.size() < 3) { + continue; // could not find time and/or temp + } - /** - * @brief Calibrate a WAC cube. - * - * This is the programmatic interface to the lrowaccal application. - * - * @param cube input cube to be calibrated - * @param ui the User Interface to parse the parameters from - */ - void lrowaccal(Cube *icube, UserInterface &ui, vector darkFiles, QString flatFile, QString radFile, QString specpixFile, QString tempFile) { + // extract time/temp from regex texts + bool tempOK, timeOK; + double fileTemp = texts[1].toDouble(&tempOK); + int fileTime = texts[2].toInt(&timeOK); + if (!tempOK || !timeOK) { + continue; // time or temp was not a valid numeric value + } + + DarkFileInfo info(fileTemp, fileTime); + darkFiles.push_back(info); + } + + // we require at least 2 different dark files to interpolate/extrapolate + if (darkFiles.size() < 2) { + QString msg = "Not enough Dark files exist for these image options [" + basename + "]. Need at least 2 files with different temperatures\n"; + throw IException(IException::User, msg, _FILEINFO_); + } + + // sort the files by distance from wac temp and time + DarkComp darkComp(temp, (int)time); + sort(darkFiles.begin(), darkFiles.end(), darkComp); + + size_t temp1Index = 0; + size_t temp2Index; + + temp1 = darkFiles[temp1Index].temp; + + for (temp2Index = temp1Index+1; temp2Index < darkFiles.size(); temp2Index++) { + if (darkFiles[temp2Index].temp != temp1) { + break; + } + } + + if (temp2Index >= darkFiles.size()) { + temp2Index = 1; + } + + temp2 = darkFiles[temp2Index].temp; + + int time1 = darkFiles[temp1Index].time; + int time2 = darkFiles[temp2Index].time; + + int tempIndex = fileString.indexOf("*C"); + int timeIndex = fileString.indexOf("*T"); + + file1 = fileString; + file1.replace(timeIndex, 1, toString(time1)); + file1.replace(tempIndex, 1, toString((int)temp1)); + + file2 = fileString; + file2.replace(timeIndex, 1, toString(time2)); + file2.replace(tempIndex, 1, toString((int)temp2)); + + CopyCubeIntoBuffer(file1, data1); + CopyCubeIntoBuffer(file2, data2); + }; + + auto GetMask = [CopyCubeIntoBuffer](QString &fileString, double temp, Buffer* &data) { + FileName filename(fileString); + QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" + + unsigned int index = basename.indexOf("*"); + + // create a filter for the QDir to only load files matching our name + QString filter(basename); + filter.append(".*"); + + QDir dir(filename.path(), filter); + + // create a regular expression to capture the temp and time from filenames + QString regexPattern(basename); + regexPattern.replace("*", "([0-9\\.-]*)"); + QRegExp regex(regexPattern); + + double bestTemp = DBL_MAX; + for (unsigned int indx=0; indx < dir.count(); indx++) { + // match against our regular expression + int pos = regex.indexIn(dir[indx]); + if (pos == -1) { + continue; // filename did not match basename regex (temp contain non-digit) + } + + // Get a list of regex matches. Item 0 should be the full QString, item 1 is temp + QStringList texts = regex.capturedTexts(); + if (texts.size() < 2) { + continue; // could not find temp + } + + // extract time/temp from regex texts + bool tempOK; + double fileTemp = texts[1].toDouble(&tempOK); + if (!tempOK) { + continue; // temp was not a valid numeric value + } + + if (abs(temp - fileTemp) < abs(temp - bestTemp)) { + bestTemp = fileTemp; + } + } + + if (bestTemp == DBL_MAX) { + QString msg = "No files exist for these mask options [" + basename + "]"; + throw IException(IException::User, msg, _FILEINFO_); + } + + index = fileString.indexOf("*"); + fileString.replace(index, 1, toString((int)bestTemp)); + + CopyCubeIntoBuffer(fileString, data); + }; + + // Calibrate each framelet + auto Calibrate = [&](Buffer &inCube, Buffer &outCube) -> void { + int correctBand = -1; + //If we are passed in a single band (img.cub+4) we need to pay special attention that we don't start with band1 + if (inCube.BandDimension() == 1 && g_bands.size() == 1) + correctBand = g_bands.front(); + + int frameHeight = inCube.LineDimension(); + int frameSize = inCube.SampleDimension()*inCube.LineDimension(); + int frame = inCube.Line() / frameHeight; + + // Calculate a temperature factor for the current frame (this is done to avoid doing this for each pixel + // Used in dark and temprature correction + // frameTemp: + // + // (WAC end temp - WAC start temp) + // ------------------------------- * frame + WAC start temp + // (WAC num framelets) + double frameTemp = (g_endTemperature - g_startTemperature)/g_numFrames * frame + g_startTemperature; + + for (int i = 0; i < outCube.size(); i++) + outCube[i] = inCube[i]; + + if (g_dark) { + double tempFactor = (frameTemp - g_temp2) / (g_temp1-g_temp2); + + for (int b=0; bIndex(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, correctBand); + else + offset = g_darkCube1->Index(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, b+1); + + // We're bypassing Buffer::at for speed, so we need to make sure our + // index will not overrun the buffer + if(offset + frameSize > g_darkCube1->size()) { + QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 1)"; + throw IException(IException::Programmer, message, _FILEINFO_); + } + if(offset + frameSize > g_darkCube2->size()) { + QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 2)"; + throw IException(IException::Programmer, message, _FILEINFO_); + } + + for (int i = 0; i < frameSize; i++) { + double dark1Pixel = (*g_darkCube1)[offset + i]; + double dark2Pixel = (*g_darkCube2)[offset + i]; + double &outputPixel = outCube[i + b*frameSize]; + // Interpolate between the two darks with the current temperaturube1 + if (!IsSpecial(dark1Pixel) && !IsSpecial(dark2Pixel) && !IsSpecial(outputPixel)) { + if (g_temp1 != g_temp2) { + // Dark correction formula: + // + // (dark1Pixel - dark2Pixel) + // ------------------------- * (frameTemp - dark2Temp) + dark2Pixel + // (dark1Temp - dark2Temp) + // + // frameTemp: + // + // (WAC end temp - WAC start temp) + // ------------------------------- * frame + WAC start temp + // (WAC num framelets) + // + // tempFactor (calculated outside the loops for speed): + // + // (frameTemp - dark2Temp) + // ----------------------- + // (dark1Temp - dark2Temp) + // + outputPixel -= (dark1Pixel - dark2Pixel) * tempFactor + dark2Pixel; + } + else { + outputPixel -= dark1Pixel; + } + } + else { + outputPixel = Isis::Null; + } + } + } + } + + if (g_flatfield) { + for (int b=0; bIndex(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, correctBand); + else + offset = g_flatCube->Index(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, b+1); + + // We're bypassing Buffer::at for speed, so we need to make sure our + // index will not overrun the buffer + if(offset + frameSize > g_flatCube->size()) { + QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Flat-field cube)"; + throw IException(IException::Programmer, message, _FILEINFO_); + } + + int outFrameOffset = b*frameSize; + for (int i = 0; i < frameSize; i++) { + double flatPixel = (*g_flatCube)[offset + i]; + double &outputPixel = outCube[i + outFrameOffset]; + + if (flatPixel > 0.0 && !IsSpecial(flatPixel) && !IsSpecial(outputPixel)) + outputPixel *= flatPixel; // The flat-field data was inverted during load so we don't have to divide here. + else + outputPixel = Isis::Null; + } + } + } + + if (g_radiometric) { + for (int i = 0; i < outCube.size(); i++) { + if (IsSpecial(outCube[i])) + outCube[i] = Isis::Null; + else { + outCube[i] /= g_exposure; + if (g_iof) + outCube[i] *= pow(g_solarDistance, 2) / g_iofResponsivity[outCube.Band(i) - 1]; + else + outCube[i] /= g_radianceResponsivity[outCube.Band(i) - 1]; + } + } + } + + if (g_specpix) { + for (int b=0; bIndex(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, correctBand); + else + offset = g_specpixCube->Index(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, b+1); + + for (int i = 0; i < frameSize; i++) { + if (IsSpecial(g_specpixCube->at(offset + i))) + outCube[i+b*frameSize] = g_specpixCube->at(offset + i); + } + } + } + + if (g_temprature) { + for (int i = 0; i < outCube.size(); i++) { + if (IsSpecial(outCube[i])) + outCube[i] = Isis::Null; + else { + + // Temprature Correction Formula + // + // inputPixel + // --------------------- + // a*(frameTemp) + b + // + // Where: + // 'a' and 'b' are band dependant constants read in via a pvl file + // + // AND + // + // frameTemp: (Pre-Calculated as it is used in multiple places) + // + // (WAC end temp - WAC start temp) + // ------------------------------- * frame + WAC start temp + // (WAC num framelets) + // + // + // + if (correctBand != -1) + outCube[i] = outCube[i]/ (g_TempratureConstants[correctBand][0] * frameTemp + g_TempratureConstants[correctBand][1]); + else + outCube[i] = outCube[i]/ (g_TempratureConstants[outCube.Band(i)][0] * frameTemp + g_TempratureConstants[outCube.Band(i)][1]); + + } + } + } + }; + + /** + * This method returns a QString containing the path of an + * LRO calibration directory + * + * @param calibrationType The type of calibration data + * + * @return @b QString Path of the calibration directory + * + * @internal + * @history 2008-11-05 Jeannie Walldren - Original version + * @history 2016-08-16 Victor Silva - Added option for base calibration directory + */ + auto GetCalibrationDirectory = [](QString calibrationType) -> QString { + // Get the directory where the CISS calibration directories are. + PvlGroup &dataDir = Preference::Preferences().findGroup("DataDirectory"); + QString missionDir = (QString) dataDir["LRO"]; + if(calibrationType != "") { + calibrationType += "/"; + } + + return missionDir + "/calibration/" + calibrationType; + }; + + + // + // Start processing code + // ProcessByBrick p; p.SetInputCube(icube); @@ -385,32 +773,6 @@ namespace Isis { if (g_specpix) calgrp += PvlKeyword("SpecialPixelsFile", specpixFile); ocube->putGroup(calgrp); - } - - void ResetGlobals() { - g_iofResponsivity.clear(); - g_radianceResponsivity.clear(); - for (int b = 0; b < 7; b++){ - g_TempratureConstants[b][0] = 0; - g_TempratureConstants[b][1] = 0; - } - - g_dark = true; - g_flatfield = true; - g_radiometric = true; - g_iof = true; - g_specpix = true; - g_temprature = true; - - g_bands.clear(); - - g_exposure = 1.0; // Exposure duration - g_solarDistance = 1.01; // average distance in [AU] - - g_temp1 = 0.0; - g_temp2 = 0.0; - - g_numFrames = 0; delete g_darkCube1; g_darkCube1 = NULL; @@ -421,395 +783,6 @@ namespace Isis { delete g_specpixCube; g_specpixCube = NULL; } - - // Calibrate each framelet - void Calibrate(Buffer &inCube, Buffer &outCube) { - int correctBand = -1; - //If we are passed in a single band (img.cub+4) we need to pay special attention that we don't start with band1 - if (inCube.BandDimension() == 1 && g_bands.size() == 1) - correctBand = g_bands.front(); - - int frameHeight = inCube.LineDimension(); - int frameSize = inCube.SampleDimension()*inCube.LineDimension(); - int frame = inCube.Line() / frameHeight; - - // Calculate a temperature factor for the current frame (this is done to avoid doing this for each pixel - // Used in dark and temprature correction - // frameTemp: - // - // (WAC end temp - WAC start temp) - // ------------------------------- * frame + WAC start temp - // (WAC num framelets) - double frameTemp = (g_endTemperature - g_startTemperature)/g_numFrames * frame + g_startTemperature; - - for (int i = 0; i < outCube.size(); i++) - outCube[i] = inCube[i]; - - if (g_dark) { - double tempFactor = (frameTemp - g_temp2) / (g_temp1-g_temp2); - - for (int b=0; bIndex(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, correctBand); - else - offset = g_darkCube1->Index(1, frameHeight * (int) min(frame, g_darkCube1->LineDimension()/frameHeight - 1) + 1, b+1); - - // We're bypassing Buffer::at for speed, so we need to make sure our - // index will not overrun the buffer - if(offset + frameSize > g_darkCube1->size()) { - QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 1)"; - throw IException(IException::Programmer, message, _FILEINFO_); - } - if(offset + frameSize > g_darkCube2->size()) { - QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Dark cube 2)"; - throw IException(IException::Programmer, message, _FILEINFO_); - } - - for (int i = 0; i < frameSize; i++) { - double dark1Pixel = (*g_darkCube1)[offset + i]; - double dark2Pixel = (*g_darkCube2)[offset + i]; - double &outputPixel = outCube[i + b*frameSize]; - // Interpolate between the two darks with the current temperaturube1 - if (!IsSpecial(dark1Pixel) && !IsSpecial(dark2Pixel) && !IsSpecial(outputPixel)) { - if (g_temp1 != g_temp2) { - // Dark correction formula: - // - // (dark1Pixel - dark2Pixel) - // ------------------------- * (frameTemp - dark2Temp) + dark2Pixel - // (dark1Temp - dark2Temp) - // - // frameTemp: - // - // (WAC end temp - WAC start temp) - // ------------------------------- * frame + WAC start temp - // (WAC num framelets) - // - // tempFactor (calculated outside the loops for speed): - // - // (frameTemp - dark2Temp) - // ----------------------- - // (dark1Temp - dark2Temp) - // - outputPixel -= (dark1Pixel - dark2Pixel) * tempFactor + dark2Pixel; - } - else { - outputPixel -= dark1Pixel; - } - } - else { - outputPixel = Isis::Null; - } - } - } - } - - if (g_flatfield) { - for (int b=0; bIndex(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, correctBand); - else - offset = g_flatCube->Index(1, frameHeight * (int) min(frame, (g_flatCube->LineDimension()-1) / frameHeight)+1, b+1); - - // We're bypassing Buffer::at for speed, so we need to make sure our - // index will not overrun the buffer - if(offset + frameSize > g_flatCube->size()) { - QString message = Message::ArraySubscriptNotInRange(offset + frameSize) + " (Flat-field cube)"; - throw IException(IException::Programmer, message, _FILEINFO_); - } - - int outFrameOffset = b*frameSize; - for (int i = 0; i < frameSize; i++) { - double flatPixel = (*g_flatCube)[offset + i]; - double &outputPixel = outCube[i + outFrameOffset]; - - if (flatPixel > 0.0 && !IsSpecial(flatPixel) && !IsSpecial(outputPixel)) - outputPixel *= flatPixel; // The flat-field data was inverted during load so we don't have to divide here. - else - outputPixel = Isis::Null; - } - } - } - - if (g_radiometric) { - for (int i = 0; i < outCube.size(); i++) { - if (IsSpecial(outCube[i])) - outCube[i] = Isis::Null; - else { - outCube[i] /= g_exposure; - if (g_iof) - outCube[i] *= pow(g_solarDistance, 2) / g_iofResponsivity[outCube.Band(i) - 1]; - else - outCube[i] /= g_radianceResponsivity[outCube.Band(i) - 1]; - } - } - } - - if (g_specpix) { - for (int b=0; bIndex(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, correctBand); - else - offset = g_specpixCube->Index(1, frameHeight * (int) min(frame, (g_specpixCube->LineDimension()-1) / frameHeight)+1, b+1); - - for (int i = 0; i < frameSize; i++) { - if (IsSpecial(g_specpixCube->at(offset + i))) - outCube[i+b*frameSize] = g_specpixCube->at(offset + i); - } - } - } - - if (g_temprature) { - for (int i = 0; i < outCube.size(); i++) { - if (IsSpecial(outCube[i])) - outCube[i] = Isis::Null; - else { - - // Temprature Correction Formula - // - // inputPixel - // --------------------- - // a*(frameTemp) + b - // - // Where: - // 'a' and 'b' are band dependant constants read in via a pvl file - // - // AND - // - // frameTemp: (Pre-Calculated as it is used in multiple places) - // - // (WAC end temp - WAC start temp) - // ------------------------------- * frame + WAC start temp - // (WAC num framelets) - // - // - // - if (correctBand != -1) - outCube[i] = outCube[i]/ (g_TempratureConstants[correctBand][0] * frameTemp + g_TempratureConstants[correctBand][1]); - else - outCube[i] = outCube[i]/ (g_TempratureConstants[outCube.Band(i)][0] * frameTemp + g_TempratureConstants[outCube.Band(i)][1]); - - } - } - } - } - - void CopyCubeIntoBuffer(QString &fileString, Buffer* &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 = NULL; - data = new Buffer(brick); - - fileString = filename.expanded(); - } - - double min(double a, double b) { - if (a < b) - return a; - return b; - } - - /** - * Finds 2 best dark files for WAC calibration. - * - * GetDark will find the 2 closest available dark file tempuratures matching the given file name - * pattern. Then find the dark file at each tempurature with the time closest to the WAC tempurature. - * If there is only one tempurature, it will pick the 2 closest times at that tempurature. - * - * - * @param fileString String pattern defining dark files to search (ie. lro/calibration/wac_darks/WAC_COLOR_Offset68_*C_*T_Dark.????.cub) - * @param temp Tempurature of WAC being calibrated - * @param time Time of WAC being calibrated - * @param data1 Buffer to hold dark file 1 cub data - * @param data2 Buffer to hold dark file 2 cub data - * @param temp1 Tempurature of dark file 1 - * @param temp2 Tempurature of dark file 2 - * @param file1 Filename of dark file 1 - * @param file2 Filename of dark file 2 - */ - void GetDark(QString fileString, double temp, double time, Buffer* &data1, Buffer* &data2, double & temp1, double & temp2, QString & file1, QString & file2) { - FileName filename(fileString); - QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" - - // create a regular expression to capture the temp and 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 darkFiles; - darkFiles.reserve(dir.count()); - - // Loop through all files in the dir that match our basename and extract time and temp - for (unsigned int indx=0; indx < dir.count(); indx++) { - // match against our regular expression - int pos = regex.indexIn(dir[indx]); - if (pos == -1) { - continue; // filename did not match basename regex (time or temp contain non-digit) - } - - // Get a list of regex matches. Item 0 should be the full QString, item 1 - // is temp and item 2 is time. - QStringList texts = regex.capturedTexts(); - if (texts.size() < 3) { - continue; // could not find time and/or temp - } - - // extract time/temp from regex texts - bool tempOK, timeOK; - double fileTemp = texts[1].toDouble(&tempOK); - int fileTime = texts[2].toInt(&timeOK); - if (!tempOK || !timeOK) { - continue; // time or temp was not a valid numeric value - } - - DarkFileInfo info(fileTemp, fileTime); - darkFiles.push_back(info); - } - - // we require at least 2 different dark files to interpolate/extrapolate - if (darkFiles.size() < 2) { - QString msg = "Not enough Dark files exist for these image options [" + basename + "]. Need at least 2 files with different temperatures\n"; - throw IException(IException::User, msg, _FILEINFO_); - } - - // sort the files by distance from wac temp and time - DarkComp darkComp(temp, (int)time); - sort(darkFiles.begin(), darkFiles.end(), darkComp); - - size_t temp1Index = 0; - size_t temp2Index; - - temp1 = darkFiles[temp1Index].temp; - - for (temp2Index = temp1Index+1; temp2Index < darkFiles.size(); temp2Index++) { - if (darkFiles[temp2Index].temp != temp1) { - break; - } - } - - if (temp2Index >= darkFiles.size()) { - temp2Index = 1; - } - - temp2 = darkFiles[temp2Index].temp; - - int time1 = darkFiles[temp1Index].time; - int time2 = darkFiles[temp2Index].time; - - int tempIndex = fileString.indexOf("*C"); - int timeIndex = fileString.indexOf("*T"); - - file1 = fileString; - file1.replace(timeIndex, 1, toString(time1)); - file1.replace(tempIndex, 1, toString((int)temp1)); - - file2 = fileString; - file2.replace(timeIndex, 1, toString(time2)); - file2.replace(tempIndex, 1, toString((int)temp2)); - - CopyCubeIntoBuffer(file1, data1); - CopyCubeIntoBuffer(file2, data2); - } - - void GetMask(QString &fileString, double temp, Buffer* &data) { - FileName filename(fileString); - QString basename = FileName(filename.baseName()).baseName(); // We do it twice to remove the ".????.cub" - - unsigned int index = basename.indexOf("*"); - - // create a filter for the QDir to only load files matching our name - QString filter(basename); - filter.append(".*"); - - QDir dir(filename.path(), filter); - - // create a regular expression to capture the temp and time from filenames - QString regexPattern(basename); - regexPattern.replace("*", "([0-9\\.-]*)"); - QRegExp regex(regexPattern); - - double bestTemp = DBL_MAX; - for (unsigned int indx=0; indx < dir.count(); indx++) { - // match against our regular expression - int pos = regex.indexIn(dir[indx]); - if (pos == -1) { - continue; // filename did not match basename regex (temp contain non-digit) - } - - // Get a list of regex matches. Item 0 should be the full QString, item 1 is temp - QStringList texts = regex.capturedTexts(); - if (texts.size() < 2) { - continue; // could not find temp - } - - // extract time/temp from regex texts - bool tempOK; - double fileTemp = texts[1].toDouble(&tempOK); - if (!tempOK) { - continue; // temp was not a valid numeric value - } - - if (abs(temp - fileTemp) < abs(temp - bestTemp)) { - bestTemp = fileTemp; - } - } - - if (bestTemp == DBL_MAX) { - QString msg = "No files exist for these mask options [" + basename + "]"; - throw IException(IException::User, msg, _FILEINFO_); - } - - index = fileString.indexOf("*"); - fileString.replace(index, 1, toString((int)bestTemp)); - - CopyCubeIntoBuffer(fileString, data); - } - - /** - * This method returns a QString containing the path of an - * LRO calibration directory - * - * @param calibrationType The type of calibration data - * - * @return @b QString Path of the calibration directory - * - * @internal - * @history 2008-11-05 Jeannie Walldren - Original version - * @history 2016-08-16 Victor Silva - Added option for base calibration directory - */ - QString GetCalibrationDirectory(QString calibrationType) { - // Get the directory where the CISS calibration directories are. - PvlGroup &dataDir = Preference::Preferences().findGroup("DataDirectory"); - QString missionDir = (QString) dataDir["LRO"]; - if(calibrationType != "") { - calibrationType += "/"; - } - - return missionDir + "/calibration/" + calibrationType; - } } diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.h b/isis/src/lro/apps/lrowaccal/lrowaccal.h index 5eb7d00a88..11f5cc851e 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.h +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.h @@ -25,8 +25,7 @@ namespace Isis { extern void lrowaccal(UserInterface &ui); - extern void lrowaccal(Cube *icube, UserInterface &ui, std::vector darkFiles, - QString flatFile, QString radFile, QString specpixFile, QString tempFile); + extern void lrowaccal(Cube *icube, UserInterface &ui); } #endif \ No newline at end of file From 85584d29c6120de89f3dcff9b9a416753f34d30f Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Tue, 3 May 2022 15:03:48 -0700 Subject: [PATCH 08/19] Update history section of lrowaccal XML file with refactoring and output cube units label changes --- isis/src/lro/apps/lrowaccal/lrowaccal.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.xml b/isis/src/lro/apps/lrowaccal/lrowaccal.xml index 277307cec2..f111cad55d 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.xml +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.xml @@ -240,6 +240,12 @@ Added ability to get the sun distance from the camera. + + Refactored lrowaccal to be callable for testing and to remove global + variables and helper functions. Added units label "W/m2/sr/um" to + RadiometricType keyword in output cube label when RadiometricType is + AbsoluteRadiance. + From aa9f0244ff5f55c5a2de082aa9b3d947e3b81e9c Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Tue, 3 May 2022 15:17:13 -0700 Subject: [PATCH 09/19] Remove old lrowaccal Makefile tests --- isis/src/lro/apps/lrowaccal/tsts/Makefile | 4 ---- .../lro/apps/lrowaccal/tsts/wac-color/Makefile | 17 ----------------- .../lro/apps/lrowaccal/tsts/wac-mono/Makefile | 12 ------------ .../src/lro/apps/lrowaccal/tsts/wac-uv/Makefile | 12 ------------ .../lro/apps/lrowaccal/tsts/wac-vis/Makefile | 12 ------------ 5 files changed, 57 deletions(-) delete mode 100644 isis/src/lro/apps/lrowaccal/tsts/Makefile delete mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile delete mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile delete mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile delete mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile diff --git a/isis/src/lro/apps/lrowaccal/tsts/Makefile b/isis/src/lro/apps/lrowaccal/tsts/Makefile deleted file mode 100644 index 46d84c74c2..0000000000 --- a/isis/src/lro/apps/lrowaccal/tsts/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -BLANKS = "%-6s" -LENGTH = "%-40s" - -include $(ISISROOT)/make/isismake.tststree diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile deleted file mode 100644 index c3bd30efb1..0000000000 --- a/isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -APPNAME = lrowaccal -FILE=wac0000983c - -include $(ISISROOT)/make/isismake.tsts - -commands: - $(APPNAME) from=$(INPUT)/${FILE}.uv.odd.cub \ - to=$(OUTPUT)/${FILE}.uv.odd.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.uv.even.cub \ - to=$(OUTPUT)/${FILE}.uv.even.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.odd.cub \ - to=$(OUTPUT)/${FILE}.vis.odd.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub \ - to=$(OUTPUT)/${FILE}.vis.even.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub+4 \ - to=$(OUTPUT)/${FILE}.vis.even.cal.band4.cub > /dev/null; - diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile deleted file mode 100644 index a8da2b4483..0000000000 --- a/isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -APPNAME = lrowaccal -FILE=wac0002c120 - -include $(ISISROOT)/make/isismake.tsts - -commands: - $(APPNAME) from=$(INPUT)/${FILE}.vis.odd.cub \ - to=$(OUTPUT)/${FILE}.vis.odd.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub \ - to=$(OUTPUT)/${FILE}.vis.even.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.even.spice.cub \ - to=$(OUTPUT)/${FILE}.vis.even.spice.cal.cub > /dev/null; diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile deleted file mode 100644 index b0b9e9126d..0000000000 --- a/isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -APPNAME = lrowaccal -FILE=wac0001832b - -include $(ISISROOT)/make/isismake.tsts - -commands: - $(APPNAME) from=$(INPUT)/${FILE}.uv.odd.cub \ - to=$(OUTPUT)/${FILE}.uv.odd.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.uv.even.cub \ - to=$(OUTPUT)/${FILE}.uv.even.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.uv.even.cub+2 \ - to=$(OUTPUT)/${FILE}.uv.even.cal.band2.cub > /dev/null; diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile deleted file mode 100644 index 2d9d94a9df..0000000000 --- a/isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -APPNAME = lrowaccal -FILE=wac00002cf4 - -include $(ISISROOT)/make/isismake.tsts - -commands: - $(APPNAME) from=$(INPUT)/${FILE}.vis.odd.cub \ - to=$(OUTPUT)/${FILE}.vis.odd.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub \ - to=$(OUTPUT)/${FILE}.vis.even.cal.cub > /dev/null; - $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub+4 \ - to=$(OUTPUT)/${FILE}.vis.even.cal.band4.cub > /dev/null; From 5692de57a80208787a9f1456414040577e7ff5f9 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 11:19:57 -0700 Subject: [PATCH 10/19] Add lrowaccal functional test to ensure that the radiance units label is not inserted for RadiometricType IOF --- isis/tests/FunctionalTestsLrowaccal.cpp | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/isis/tests/FunctionalTestsLrowaccal.cpp b/isis/tests/FunctionalTestsLrowaccal.cpp index e2756d2b07..adaf28dbbd 100644 --- a/isis/tests/FunctionalTestsLrowaccal.cpp +++ b/isis/tests/FunctionalTestsLrowaccal.cpp @@ -38,3 +38,31 @@ TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelExists) { EXPECT_EQ(radiometryGroup["RadiometricType"].unit().toStdString(), "W/m2/sr/um"); } + +TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelNotForIOF) { + QTemporaryDir tempDir; + ASSERT_TRUE(tempDir.isValid()); + + QString outCubeFileName = tempDir.path() + "/outTemp.cub"; + QString testCubeFileName = "data/lrowaccal/M1388981421CE.tmp.vis.even.reduced.cub"; + + QVector args = {"from=" + testCubeFileName, + "to=" + outCubeFileName, + "radiometrictype=IOF", + "radiometricfile=Default"}; + UserInterface options(APP_XML, args); + + try { + lrowaccal(options); + } + catch(IException &e) { + FAIL() << "Unable to open image cube: " << e.what() << std::endl; + } + + Cube outCube(outCubeFileName); + Pvl *outCubeLabel = outCube.label(); + + PvlGroup &radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); + + EXPECT_NE(radiometryGroup["RadiometricType"].unit().toStdString(), "W/m2/sr/um"); +} From 322777560e8c8728eb35f58128f67f1d5df8da67 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 14:53:12 -0700 Subject: [PATCH 11/19] Add Cordell Michaud to the .zenodo.json document --- .zenodo.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.zenodo.json b/.zenodo.json index d00ff1ff8d..7b8521552b 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -304,6 +304,10 @@ "affiliation": "University of California, Los Angeles", "name": "Mehlman, Bob" }, + { + "affiliation": "Arizona State University", + "name": "Michaud, Cordell" + }, { "name": "Milazzo, Moses" }, From 954e59fd4f70bf951c1bbb70e09f0da8705a8b17 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 15:01:28 -0700 Subject: [PATCH 12/19] Add lrowaccal changes to CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0778b9ce2d..152e9b064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ release. ## [Unreleased] - 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) +- 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) ### Changed From cf9f055cfa3fed132330b668f408ff2717c4f13d Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 15:55:34 -0700 Subject: [PATCH 13/19] Clean up lrowaccal includes and leftover comment --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 21 +++++++++++++++++++-- isis/src/lro/apps/lrowaccal/lrowaccal.h | 18 ------------------ isis/src/lro/apps/lrowaccal/main.cpp | 1 + 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp index 6348c338f5..ac6cc1b8cc 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -1,3 +1,22 @@ +#include + +#include +#include +#include + +#include "Brick.h" +#include "Camera.h" +#include "Constants.h" +#include "CubeAttribute.h" +#include "iTime.h" +#include "Message.h" +#include "NaifStatus.h" +#include "Preference.h" +#include "ProcessByBrick.h" +#include "PvlGroup.h" +#include "SpecialPixel.h" +#include "Statistics.h" + #include "lrowaccal.h" #define POLAR_MODE_SAMPLES 1024 @@ -15,8 +34,6 @@ using namespace Isis; namespace Isis { - // globals - /** * @brief Calibrate a WAC cube. * diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.h b/isis/src/lro/apps/lrowaccal/lrowaccal.h index 11f5cc851e..e233d5d18c 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.h +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.h @@ -1,25 +1,7 @@ #ifndef lrowaccal_h #define lrowaccal_h -#include - -#include -#include -#include - -#include "Brick.h" -#include "Camera.h" -#include "Constants.h" #include "Cube.h" -#include "CubeAttribute.h" -#include "iTime.h" -#include "Message.h" -#include "NaifStatus.h" -#include "Preference.h" -#include "ProcessByBrick.h" -#include "PvlGroup.h" -#include "SpecialPixel.h" -#include "Statistics.h" #include "UserInterface.h" diff --git a/isis/src/lro/apps/lrowaccal/main.cpp b/isis/src/lro/apps/lrowaccal/main.cpp index 045b9bd918..3833b72f38 100644 --- a/isis/src/lro/apps/lrowaccal/main.cpp +++ b/isis/src/lro/apps/lrowaccal/main.cpp @@ -1,5 +1,6 @@ #include "Isis.h" +#include "Application.h" #include "lrowaccal.h" using namespace Isis; From 676efb92ac3e2bbc5e27a1a92b5d33a51acc3b1a Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 16:01:02 -0700 Subject: [PATCH 14/19] Re-add lrowaccal Makefile tests --- isis/src/lro/apps/lrowaccal/tsts/Makefile | 4 ++++ .../lro/apps/lrowaccal/tsts/wac-color/Makefile | 17 +++++++++++++++++ .../lro/apps/lrowaccal/tsts/wac-mono/Makefile | 12 ++++++++++++ .../src/lro/apps/lrowaccal/tsts/wac-uv/Makefile | 12 ++++++++++++ .../lro/apps/lrowaccal/tsts/wac-vis/Makefile | 12 ++++++++++++ 5 files changed, 57 insertions(+) create mode 100644 isis/src/lro/apps/lrowaccal/tsts/Makefile create mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile create mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile create mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile create mode 100644 isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile diff --git a/isis/src/lro/apps/lrowaccal/tsts/Makefile b/isis/src/lro/apps/lrowaccal/tsts/Makefile new file mode 100644 index 0000000000..46d84c74c2 --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/tsts/Makefile @@ -0,0 +1,4 @@ +BLANKS = "%-6s" +LENGTH = "%-40s" + +include $(ISISROOT)/make/isismake.tststree diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile new file mode 100644 index 0000000000..c3bd30efb1 --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/tsts/wac-color/Makefile @@ -0,0 +1,17 @@ +APPNAME = lrowaccal +FILE=wac0000983c + +include $(ISISROOT)/make/isismake.tsts + +commands: + $(APPNAME) from=$(INPUT)/${FILE}.uv.odd.cub \ + to=$(OUTPUT)/${FILE}.uv.odd.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.uv.even.cub \ + to=$(OUTPUT)/${FILE}.uv.even.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.odd.cub \ + to=$(OUTPUT)/${FILE}.vis.odd.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub \ + to=$(OUTPUT)/${FILE}.vis.even.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub+4 \ + to=$(OUTPUT)/${FILE}.vis.even.cal.band4.cub > /dev/null; + diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile new file mode 100644 index 0000000000..a8da2b4483 --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/tsts/wac-mono/Makefile @@ -0,0 +1,12 @@ +APPNAME = lrowaccal +FILE=wac0002c120 + +include $(ISISROOT)/make/isismake.tsts + +commands: + $(APPNAME) from=$(INPUT)/${FILE}.vis.odd.cub \ + to=$(OUTPUT)/${FILE}.vis.odd.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub \ + to=$(OUTPUT)/${FILE}.vis.even.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.even.spice.cub \ + to=$(OUTPUT)/${FILE}.vis.even.spice.cal.cub > /dev/null; diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile new file mode 100644 index 0000000000..b0b9e9126d --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/tsts/wac-uv/Makefile @@ -0,0 +1,12 @@ +APPNAME = lrowaccal +FILE=wac0001832b + +include $(ISISROOT)/make/isismake.tsts + +commands: + $(APPNAME) from=$(INPUT)/${FILE}.uv.odd.cub \ + to=$(OUTPUT)/${FILE}.uv.odd.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.uv.even.cub \ + to=$(OUTPUT)/${FILE}.uv.even.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.uv.even.cub+2 \ + to=$(OUTPUT)/${FILE}.uv.even.cal.band2.cub > /dev/null; diff --git a/isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile b/isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile new file mode 100644 index 0000000000..2d9d94a9df --- /dev/null +++ b/isis/src/lro/apps/lrowaccal/tsts/wac-vis/Makefile @@ -0,0 +1,12 @@ +APPNAME = lrowaccal +FILE=wac00002cf4 + +include $(ISISROOT)/make/isismake.tsts + +commands: + $(APPNAME) from=$(INPUT)/${FILE}.vis.odd.cub \ + to=$(OUTPUT)/${FILE}.vis.odd.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub \ + to=$(OUTPUT)/${FILE}.vis.even.cal.cub > /dev/null; + $(APPNAME) from=$(INPUT)/${FILE}.vis.even.cub+4 \ + to=$(OUTPUT)/${FILE}.vis.even.cal.band4.cub > /dev/null; From d91aa06955c40a67156f6f6f86a760c441e38fb9 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 16:03:36 -0700 Subject: [PATCH 15/19] Re-add disclaimer to top of lrowaccal's main.cpp file --- isis/src/lro/apps/lrowaccal/main.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/isis/src/lro/apps/lrowaccal/main.cpp b/isis/src/lro/apps/lrowaccal/main.cpp index 3833b72f38..f5d52c6734 100644 --- a/isis/src/lro/apps/lrowaccal/main.cpp +++ b/isis/src/lro/apps/lrowaccal/main.cpp @@ -1,3 +1,11 @@ +/** 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 "Application.h" From aaf23802cd525725958e70a5297ef84859ce7a91 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Wed, 4 May 2022 16:14:54 -0700 Subject: [PATCH 16/19] Add input attributes retrieval to lrowaccal.cpp --- isis/src/lro/apps/lrowaccal/lrowaccal.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp index ac6cc1b8cc..5439cde1f9 100644 --- a/isis/src/lro/apps/lrowaccal/lrowaccal.cpp +++ b/isis/src/lro/apps/lrowaccal/lrowaccal.cpp @@ -44,6 +44,10 @@ namespace Isis { void lrowaccal(UserInterface &ui) { Cube *icube = NULL; icube = new Cube(); + CubeAttributeInput inAtt = ui.GetInputAttribute("FROM"); + if (inAtt.bands().size() != 0) { + icube->setVirtualBands(inAtt.bands()); + } icube->open(ui.GetCubeName("FROM")); lrowaccal(icube, ui); From 4ca4f7acafb28cecb0383249cf842717bc507092 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Thu, 5 May 2022 11:27:26 -0700 Subject: [PATCH 17/19] Fix lrowaccal functional test error messages and add PVl group and existence checks --- isis/tests/FunctionalTestsLrowaccal.cpp | 38 +++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/isis/tests/FunctionalTestsLrowaccal.cpp b/isis/tests/FunctionalTestsLrowaccal.cpp index adaf28dbbd..28659bce46 100644 --- a/isis/tests/FunctionalTestsLrowaccal.cpp +++ b/isis/tests/FunctionalTestsLrowaccal.cpp @@ -28,15 +28,28 @@ TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelExists) { lrowaccal(options); } catch(IException &e) { - FAIL() << "Unable to open image cube: " << e.what() << std::endl; + FAIL() << "Call to lrowaccal failed, unable to calibrate cube: " << e.what() << std::endl; } Cube outCube(outCubeFileName); Pvl *outCubeLabel = outCube.label(); - PvlGroup &radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); + PvlGroup radiometryGroup; + try { + radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); + } + catch(IException &e) { + FAIL() << "Unable to find Radiometry group in output cube label: " << e.what() << std::endl; + } - EXPECT_EQ(radiometryGroup["RadiometricType"].unit().toStdString(), "W/m2/sr/um"); + PvlKeyword radiometricTypeKeyword; + try { + radiometricTypeKeyword = radiometryGroup.findKeyword("RadiometricType"); + } + catch(IException &e) { + FAIL() << "Unable to find RadiometricType keyword in Radiometry group of output cube label: " << e.what() << std::endl; + } + EXPECT_EQ(radiometricTypeKeyword.unit().toStdString(), "W/m2/sr/um"); } TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelNotForIOF) { @@ -56,13 +69,26 @@ TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelNotForIOF) { lrowaccal(options); } catch(IException &e) { - FAIL() << "Unable to open image cube: " << e.what() << std::endl; + FAIL() << "Call to lrowaccal failed, unable to calibrate cube: " << e.what() << std::endl; } Cube outCube(outCubeFileName); Pvl *outCubeLabel = outCube.label(); - PvlGroup &radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); + PvlGroup radiometryGroup; + try { + radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); + } + catch(IException &e) { + FAIL() << "Unable to find Radiometry group in output cube label: " << e.what() << std::endl; + } - EXPECT_NE(radiometryGroup["RadiometricType"].unit().toStdString(), "W/m2/sr/um"); + PvlKeyword radiometricTypeKeyword; + try { + radiometricTypeKeyword = radiometryGroup.findKeyword("RadiometricType"); + } + catch(IException &e) { + FAIL() << "Unable to find RadiometricType keyword in Radiometry group of output cube label: " << e.what() << std::endl; + } + EXPECT_NE(radiometricTypeKeyword.unit().toStdString(), "W/m2/sr/um"); } From 2e619f692c8b8a5fadc4e2b6a22d92f4781422e7 Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Thu, 5 May 2022 11:31:07 -0700 Subject: [PATCH 18/19] Move lrowaccal units change message to Changed sub-header in CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 152e9b064c..5df5937e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,9 +36,9 @@ release. ## [Unreleased] - 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) -- 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) ### 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) ### Added - Improved functionality of msi2isis and MsiCamera model to support new Eros dataset, including support for Gaskell's SUMSPICE files that adjust timing, pointing and spacecraft position ephemeris. [#4886](https://github.com/USGS-Astrogeology/ISIS3/issues/4886) From 48fd1fb94b6a27f93c9fc3f7f8c026e41237256e Mon Sep 17 00:00:00 2001 From: Cordell Michaud <10409047+michaudcordell@users.noreply.github.com> Date: Thu, 5 May 2022 12:03:42 -0700 Subject: [PATCH 19/19] Simplify lrowaccal functional test assertions --- isis/tests/FunctionalTestsLrowaccal.cpp | 42 ++++++------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/isis/tests/FunctionalTestsLrowaccal.cpp b/isis/tests/FunctionalTestsLrowaccal.cpp index 28659bce46..d93ebafce5 100644 --- a/isis/tests/FunctionalTestsLrowaccal.cpp +++ b/isis/tests/FunctionalTestsLrowaccal.cpp @@ -32,24 +32,13 @@ TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelExists) { } Cube outCube(outCubeFileName); - Pvl *outCubeLabel = outCube.label(); - PvlGroup radiometryGroup; - try { - radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); - } - catch(IException &e) { - FAIL() << "Unable to find Radiometry group in output cube label: " << e.what() << std::endl; - } + ASSERT_TRUE(outCube.hasGroup("Radiometry")); + PvlGroup &radiometry = outCube.group("Radiometry"); - PvlKeyword radiometricTypeKeyword; - try { - radiometricTypeKeyword = radiometryGroup.findKeyword("RadiometricType"); - } - catch(IException &e) { - FAIL() << "Unable to find RadiometricType keyword in Radiometry group of output cube label: " << e.what() << std::endl; - } - EXPECT_EQ(radiometricTypeKeyword.unit().toStdString(), "W/m2/sr/um"); + ASSERT_TRUE(radiometry.hasKeyword("RadiometricType")); + PvlKeyword &radiometricType = radiometry["RadiometricType"]; + ASSERT_EQ(radiometricType.unit().toStdString(), "W/m2/sr/um"); } TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelNotForIOF) { @@ -73,22 +62,11 @@ TEST(Lrowaccal, FunctionalTestLrowaccalRadianceUnitsLabelNotForIOF) { } Cube outCube(outCubeFileName); - Pvl *outCubeLabel = outCube.label(); - PvlGroup radiometryGroup; - try { - radiometryGroup = outCubeLabel->findGroup("Radiometry", Pvl::Traverse); - } - catch(IException &e) { - FAIL() << "Unable to find Radiometry group in output cube label: " << e.what() << std::endl; - } + ASSERT_TRUE(outCube.hasGroup("Radiometry")); + PvlGroup &radiometry = outCube.group("Radiometry"); - PvlKeyword radiometricTypeKeyword; - try { - radiometricTypeKeyword = radiometryGroup.findKeyword("RadiometricType"); - } - catch(IException &e) { - FAIL() << "Unable to find RadiometricType keyword in Radiometry group of output cube label: " << e.what() << std::endl; - } - EXPECT_NE(radiometricTypeKeyword.unit().toStdString(), "W/m2/sr/um"); + ASSERT_TRUE(radiometry.hasKeyword("RadiometricType")); + PvlKeyword &radiometricType = radiometry["RadiometricType"]; + ASSERT_NE(radiometricType.unit().toStdString(), "W/m2/sr/um"); }