diff --git a/CHANGELOG.md b/CHANGELOG.md index d906e8e933..4040916a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ ctest FunctionalTestJigsawApollo to validate this output. [#5710](https://github - Added CSM State output capability for jigsaw. [#5609](https://github.com/DOI-USGS/ISIS3/issues/5609) - Added ShowDeprecated option to show or hide warnings in IsisPreferences. [#5611](https://github.com/DOI-USGS/ISIS3/issues/5611) - Added SpiceQL integration to replace cspice calls. [#5545](https://github.com/DOI-USGS/ISIS3/pull/5545) +- Added initial `eisstitch` app [#5591](https://github.com/DOI-USGS/ISIS3/pull/5591) ### Changed - Removed Arm dependency on xalan-c, as it does not build for now on conda-forge. This requires turning off doc building on Arm. Also changed some variables to avoid name clashes on Arm with clang 16. [#5802](https://github.com/DOI-USGS/ISIS3/pull/5802) diff --git a/isis/src/clipper/apps/eisstitch/assets/IN/eis_cubes.lis b/isis/src/clipper/apps/eisstitch/assets/IN/eis_cubes.lis new file mode 100644 index 0000000000..0e003ac720 --- /dev/null +++ b/isis/src/clipper/apps/eisstitch/assets/IN/eis_cubes.lis @@ -0,0 +1,3 @@ +/Path/to/eis_images/capture_1.cub +/Path/to/eis_images/capture_2.cub +/Path/to/eis_images/capture_3.cub \ No newline at end of file diff --git a/isis/src/clipper/apps/eisstitch/assets/images/capture_1.jpg b/isis/src/clipper/apps/eisstitch/assets/images/capture_1.jpg new file mode 100644 index 0000000000..1c30e48f22 Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/images/capture_1.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/images/capture_2.jpg b/isis/src/clipper/apps/eisstitch/assets/images/capture_2.jpg new file mode 100644 index 0000000000..5de01dfd8d Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/images/capture_2.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/images/capture_3.jpg b/isis/src/clipper/apps/eisstitch/assets/images/capture_3.jpg new file mode 100644 index 0000000000..191022fc78 Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/images/capture_3.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/images/eis_stitched_cube.jpg b/isis/src/clipper/apps/eisstitch/assets/images/eis_stitched_cube.jpg new file mode 100644 index 0000000000..9f8e4a72f2 Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/images/eis_stitched_cube.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_1_thumb.jpg b/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_1_thumb.jpg new file mode 100644 index 0000000000..750d19f97a Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_1_thumb.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_2_thumb.jpg b/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_2_thumb.jpg new file mode 100644 index 0000000000..6b8aebb4d1 Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_2_thumb.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_3_thumb.jpg b/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_3_thumb.jpg new file mode 100644 index 0000000000..53ab2f0a2c Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/thumbs/capture_3_thumb.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/assets/thumbs/eis_stitched_cube_thumb.jpg b/isis/src/clipper/apps/eisstitch/assets/thumbs/eis_stitched_cube_thumb.jpg new file mode 100644 index 0000000000..6b305c549e Binary files /dev/null and b/isis/src/clipper/apps/eisstitch/assets/thumbs/eis_stitched_cube_thumb.jpg differ diff --git a/isis/src/clipper/apps/eisstitch/eisstitch.cpp b/isis/src/clipper/apps/eisstitch/eisstitch.cpp new file mode 100644 index 0000000000..610492776d --- /dev/null +++ b/isis/src/clipper/apps/eisstitch/eisstitch.cpp @@ -0,0 +1,321 @@ +/** 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 "Application.h" +#include "Cube.h" + +#include "FileName.h" +#include "FileList.h" +#include "IException.h" +#include "iTime.h" + +#include "CubeAttribute.h" +#include "ProcessByBrick.h" +#include "Pvl.h" +#include "PvlKeyword.h" +#include "Table.h" +#include "TableField.h" +#include "TableRecord.h" +#include "UserInterface.h" + +#include +#include +#include +#include + +#include "eisstitch.h" +#include +#include +#include +#include + +using namespace std; + +// This will eventually change to account for the pushbroom +// framelets but is just a linescanner for now +struct eisTiming { + double start; + int line_start; + int lines; + double exposureDuration; + // Should be the start + (lineCounts * exposureTimes) + double stop; + + eisTiming(double start, int line_start, int lines, double exposureDuration) : + start(start), line_start(line_start), lines(lines), exposureDuration(exposureDuration) + { + stop = start + (lines * exposureDuration); + } +}; + +bool compareByStartTime(const eisTiming &time1, const eisTiming &time2) { + return time1.start < time2.start; +} + +namespace Isis { + + void eisstitch(UserInterface &ui) { + // Get the list of names of input cubes to stitch together + FileList fileList; + ui = Application::GetUserInterface(); + try { + fileList.read(ui.GetFileName("FROMLIST")); + } + catch(IException &e) { + QString msg = "Unable to read [" + ui.GetFileName("FROMLIST") + "]"; + IException(e, IException::User, msg, _FILEINFO_); + } + if (fileList.size() < 1) { + QString msg = "The list file[" + ui.GetFileName("FROMLIST") + + " does not contain any filenames"; + throw IException(IException::User, msg, _FILEINFO_); + } + + std::vector eisTimes = {}; + QString filterName = ""; + int naifFrameCode = 0; + int detectorOffset = -1; + + for (FileName file : fileList) { + Pvl inputCubeLabel = Pvl(file.expanded()); + + PvlGroup instGroup; + try { + instGroup = inputCubeLabel.findGroup("Instrument", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find instrument group in [" + file.name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + + if (instGroup.findKeyword("SpacecraftName")[0] != "Europa Clipper" && + !instGroup.findKeyword("InstrumentId")[0].contains("EIS")) { + QString msg = "Cube " + file.name() + " is not a Clipper EIS image. Either update the " + "image or remove it so the other images can be stitched."; + throw IException(IException::User, msg, _FILEINFO_); + } + + if (detectorOffset == -1) { + detectorOffset = instGroup.findKeyword("DetectorOffset"); + } + else { + if (detectorOffset != toInt(instGroup.findKeyword("DetectorOffset"))) { + QString msg = "DetectorOffset [" + instGroup.findKeyword("DetectorOffset")[0] + "] from image [" + file.name() + "] does " + "not match recorded detector offset " + toString(detectorOffset); + throw IException(IException::User, msg, _FILEINFO_); + } + } + + PvlGroup kernelGroup; + try { + kernelGroup = inputCubeLabel.findGroup("Kernels", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find kernels group in [" + file.name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + + if (naifFrameCode == 0) { + naifFrameCode = kernelGroup.findKeyword("NaifFrameCode"); + } + else { + if (naifFrameCode != toInt(kernelGroup.findKeyword("NaifFrameCode"))) { + QString msg = "NaifFrameCode [" + kernelGroup.findKeyword("NaifFrameCode")[0] + "] from image [" + file.name() + "] does " + "not match recorded frame code " + toString(naifFrameCode); + throw IException(IException::User, msg, _FILEINFO_); + } + } + + PvlGroup bandBinGroup; + try { + bandBinGroup = inputCubeLabel.findGroup("BandBin", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find kernels group in [" + file.name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + + if (filterName == "") { + filterName = bandBinGroup.findKeyword("FilterName")[0]; + } + else { + if (filterName != bandBinGroup.findKeyword("FilterName")[0]) { + QString msg = "FilterName [" + kernelGroup.findKeyword("FilterName")[0] + "] from image [" + file.name() + "] does " + "not match recorded filter name " + filterName; + throw IException(IException::User, msg, _FILEINFO_); + } + } + + PvlGroup dimGroup; + try { + dimGroup = inputCubeLabel.findGroup("Dimensions", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find instrument group in [" + file.name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + Table timesTable("LineScanTimes", file.expanded()); + + if(timesTable.Records() <= 0) { + QString msg = "Table [LineScanTimes] in ["; + msg += file.expanded() + "] must not be empty"; + throw IException(IException::Unknown, msg, _FILEINFO_); + } + + for(int i = 0; i < timesTable.Records(); i++) { + double startTime = (double)timesTable[i][0]; + int line_start = timesTable[i][2]; + int lines = int(dimGroup.findKeyword("Lines")) - (line_start - 1); + double exposureDuration = timesTable[i][1]; + eisTimes.push_back(eisTiming(startTime, line_start, lines, exposureDuration)); + } + } + + // Likely need to re-order the cubes in the fileList if the eisTimes + // are moved around + // sort(eisTimes.begin(), eisTimes.end(), compareByStartTime); + + // Check for overlap + for (int i = 0; i < eisTimes.size() - 1; i++) { + if (eisTimes[i].stop > eisTimes[i + 1].start) { + QString msg = "Image " + QString(i + 1) + " and " + QString(i + 2) + " in the image list have " + + "overlapping times."; + throw IException(IException::User, msg, _FILEINFO_); + } + } + + // Assume the images don't know about each other + TableField ephTimeField("EphemerisTime", TableField::Double); + TableField expTimeField("ExposureTime", TableField::Double); + TableField lineStartField("LineStart", TableField::Integer); + + TableRecord timesRecord; + timesRecord += ephTimeField; + timesRecord += expTimeField; + timesRecord += lineStartField; + + Table timesTable("LineScanTimes", timesRecord); + + int lineCount = 1; + for (int i = 0; i < eisTimes.size(); i++) { + timesRecord[0] = eisTimes[i].start; + timesRecord[1] = eisTimes[i].exposureDuration; + timesRecord[2] = lineCount; + lineCount += eisTimes[i].lines; + timesTable += timesRecord; + + if (i < eisTimes.size() - 1) { + double imageTotalTime = eisTimes[i].start + (eisTimes[i].exposureDuration * eisTimes[i].lines); + if ( std::abs(imageTotalTime - eisTimes[i + 1].start) > std::numeric_limits::epsilon()) { + double remainingTime = eisTimes[i + 1].start - imageTotalTime; + int blankLines = int(remainingTime / eisTimes[i].exposureDuration); + lineCount += blankLines; + } + } + } + + Pvl firstCubeLabel(fileList[0].expanded()); + PvlGroup dimGroup; + try { + dimGroup = firstCubeLabel.findGroup("Dimensions", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find instrument group in [" + fileList[0].name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + int outLines = lineCount - 1; + int outSamples = dimGroup["Samples"]; + + ProcessByBrick process; + process.PropagateLabels(false); + process.PropagateTables(false); + process.PropagatePolygons(false); + process.PropagateOriginalLabel(false); + process.SetInputCube(fileList[0].expanded(), CubeAttributeInput()); + + QString outCubeFile = ui.GetCubeName("TO"); + Cube *outCube = process.SetOutputCube(outCubeFile, CubeAttributeOutput(outCubeFile), + outSamples, outLines, 1); + process.ClearInputCubes(); + Brick cubeBrick = Brick(outCube->sampleCount(), 1, 1, outCube->pixelType()); + + int writeLine = 1; + + for (int i = 0; i < fileList.size(); i++) { + + Cube *inputCube = process.SetInputCube(fileList[i].expanded(), CubeAttributeInput()); + + for (int line = 1; line < inputCube->lineCount() + 1; line++) { + cubeBrick.SetBasePosition(1, line, 1); + inputCube->read(cubeBrick); + cubeBrick.SetBasePosition(1, writeLine, 1); + outCube->write(cubeBrick); + writeLine++; + } + + if (i < fileList.size() - 1) { + int expectedLines = (int)timesTable[i + 1][2] - 1; + while(writeLine != expectedLines) { + writeLine++; + } + } + + process.ClearInputCubes(); + } + + // Still need to write the Time table and other label data + outCube->write(timesTable); + + Pvl *outLabel = outCube->label(); + PvlObject &outputIsisObject = outLabel->findObject("IsisCube"); + + Pvl inputCubeLabel = Pvl(fileList[0].expanded()); + PvlGroup inputInstGroup; + try { + inputInstGroup = inputCubeLabel.findGroup("Instrument", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find instrument group in [" + fileList[0].name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + + PvlGroup instGroup("Instrument"); + instGroup.addKeyword(PvlKeyword("SpacecraftName", inputInstGroup["SpacecraftName"])); + instGroup.addKeyword(PvlKeyword("InstrumentId", inputInstGroup["InstrumentId"])); + instGroup.addKeyword(PvlKeyword("TargetName", inputInstGroup["TargetName"])); + instGroup.addKeyword(PvlKeyword("StartTime", inputInstGroup["StartTime"])); + instGroup.addKeyword(PvlKeyword("DetectorOffset", inputInstGroup["DetectorOffset"])); + outputIsisObject.addGroup(instGroup); + + + PvlGroup inputKernelsGroup; + try { + inputKernelsGroup = inputCubeLabel.findGroup("Kernels", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find instrument group in [" + fileList[0].name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + + PvlGroup kernelsGroup("Kernels"); + kernelsGroup.addKeyword(PvlKeyword("NaifFrameCode", inputKernelsGroup["NaifFrameCode"])); + outputIsisObject.addGroup(kernelsGroup); + + PvlGroup bandBinGroup; + try { + bandBinGroup = inputCubeLabel.findGroup("BandBin", PvlObject::FindOptions::Traverse); + } + catch(IException &e) { + QString msg = "Unable to find instrument group in [" + fileList[0].name() + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + outputIsisObject.addGroup(bandBinGroup); + + process.EndProcess(); + } +} diff --git a/isis/src/clipper/apps/eisstitch/eisstitch.h b/isis/src/clipper/apps/eisstitch/eisstitch.h new file mode 100644 index 0000000000..f5d2efec23 --- /dev/null +++ b/isis/src/clipper/apps/eisstitch/eisstitch.h @@ -0,0 +1,18 @@ +#ifndef eis2isis_h +#define eis2isis_h + +/** 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 "UserInterface.h" + +namespace Isis { + extern void eisstitch(UserInterface &ui); +} + +#endif diff --git a/isis/src/clipper/apps/eisstitch/eisstitch.xml b/isis/src/clipper/apps/eisstitch/eisstitch.xml new file mode 100644 index 0000000000..f2ce00390e --- /dev/null +++ b/isis/src/clipper/apps/eisstitch/eisstitch.xml @@ -0,0 +1,131 @@ + + + + + + Combine Clipper EIS files into a single ISIS cube + + +

+ This program takes a series of Clipper EIS cubes and combines them into a single unified cube with a unified ephemeris time table + as well as a unified camera. +

+ +

+ The program requires that the list of cubes provided be Clipper EIS cubes and that the cubes are in chronological order. +

+
+ + + + Original version. + + + + + Clipper + + + + + + + filename + input + + List of cubes to stitch together + + + The name of a file containing a list of Clipper EIS images in chronological + order to be stitched together. + + + * + + + + + cube + output + + Output ISIS cube file for the stitched image + + + The output cube file that will contain the EIS stitched image in ISIS format. + + + *.cub + + + + + + + + + Demonstrats how to use eisstitch + + This example demonstrates the basic workings of eisstitch and + how to properly format the input information for stitching. + + + + fromlist=eis_cubes.lis to=eis_stitched_cube.cub + + + This is the command line used to create the stitched output cube. The FROMLIST + needs to be a newline delimited list of EIS Clipper cubes to stitch together. + + + + + First chronological capture from an EIS image set + + This is the first image in a series of images taken by the EIS Clipper + WAC/NAC camera. + + + + + Second chronological capture from an EIS image set + + This is the second image in a series of images taken by the EIS Clipper + WAC/NAC camera. + + + + + Third chronological capture from an EIS image set + + This is the third image in a series of images taken by the EIS Clipper + WAC/NAC camera. + + + + + + + + FROMLIST + + Input list file of cubes, where each line is the full path or relative + path for each cube to stitch + + FROMLIST + + + + + + Output image for eisstitch + + The sitched output from the three images above + + + TO + + + + + +
diff --git a/isis/src/clipper/apps/eisstitch/main.cpp b/isis/src/clipper/apps/eisstitch/main.cpp new file mode 100644 index 0000000000..3c6b610dab --- /dev/null +++ b/isis/src/clipper/apps/eisstitch/main.cpp @@ -0,0 +1,19 @@ +/** 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 "UserInterface.h" +#include "eisstitch.h" + +using namespace std; +using namespace Isis; + +void IsisMain() { + UserInterface &ui = Application::GetUserInterface(); + eisstitch(ui); +}