1 /**
2 * @file StitchingExecutor.cpp
3 * @brief implementation of CommandQueue creating for stitching engine
4 *
5 * @author T. Modes
6 */
7 
8 /*  This is free software; you can redistribute it and/or
9 *  modify it under the terms of the GNU General Public
10 *  License as published by the Free Software Foundation; either
11 *  version 2 of the License, or (at your option) any later version.
12 *
13 *  This software is distributed in the hope that it will be useful,
14 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 *  Lesser General Public License for more details.
17 *
18 *  You should have received a copy of the GNU General Public
19 *  License along with this software. If not, see
20 *  <http://www.gnu.org/licenses/>.
21 *
22 */
23 
24 #include "base_wx/platform.h"
25 #include "StitchingExecutor.h"
26 #include <list>
27 #include <wx/config.h>
28 #include <wx/translation.h>
29 #include <wx/arrstr.h>
30 #include <wx/filefn.h>
31 #include <wx/txtstrm.h>
32 #include <wx/wfstream.h>
33 #include "hugin_utils/utils.h"
34 #include "hugin_base/panotools/PanoToolsUtils.h"
35 #include "hugin_base/panodata/PanoramaOptions.h"
36 #include "hugin_base/algorithms/basic/LayerStacks.h"
37 #include "base_wx/wxPlatform.h"
38 #include "base_wx/LensTools.h"
39 #include "hugin/config_defaults.h"
40 
41 namespace HuginQueue
42 {
43     namespace detail
44     {
45         // contains some helper functions
46         /** returns an array of filenames with numbers */
GetNumberedFilename(const wxString & prefix,const wxString & postfix,const HuginBase::UIntSet & img)47         wxArrayString GetNumberedFilename(const wxString& prefix, const wxString& postfix, const HuginBase::UIntSet& img)
48         {
49             wxArrayString filenames;
50             for (HuginBase::UIntSet::const_iterator it = img.begin(); it != img.end(); ++it)
51             {
52                 filenames.Add(wxString::Format(wxT("%s%04u%s"), prefix.c_str(), *it, postfix.c_str()));
53             };
54             return filenames;
55         };
56 
57         /** append all strings from input array to output array */
AddToArray(const wxArrayString & input,wxArrayString & output)58         void AddToArray(const wxArrayString& input, wxArrayString& output)
59         {
60             for (size_t i = 0; i < input.size(); ++i)
61             {
62                 output.Add(input[i]);
63             };
64         };
65 
66         /** generate the final argfile
67             @return full name of generated argfile
68         */
GenerateFinalArgfile(const HuginBase::Panorama & pano,const wxString & projectName,const wxConfigBase * config,const HuginBase::UIntSet & images,const double exifToolVersion)69         wxString GenerateFinalArgfile(const HuginBase::Panorama & pano, const wxString& projectName, const wxConfigBase* config, const HuginBase::UIntSet& images, const double exifToolVersion)
70         {
71             wxString argfileInput = config->Read(wxT("/output/FinalArgfile"), wxEmptyString);
72             const bool generateGPanoTags = (config->Read(wxT("/output/writeGPano"), HUGIN_EXIFTOOL_CREATE_GPANO) == 1l) && (exifToolVersion >= 9.09);
73             pano_projection_features proj;
74             const HuginBase::PanoramaOptions &opts = pano.getOptions();
75             const bool readProjectionName = panoProjectionFeaturesQuery(opts.getProjection(), &proj) != 0;
76             // build placeholder map
77             struct Placeholder
78             {
79                 wxString placeholder;
80                 wxString value;
81                 Placeholder(const wxString& holder, const wxString& newValue)
82                 {
83                     placeholder = holder;
84                     value = newValue;
85                 };
86             };
87             std::list<Placeholder> placeholders;
88 #ifdef _WIN32
89             const wxString linebreak(wxT("&#xd;&#xa;"));
90 #else
91             const wxString linebreak(wxT("&#xa;"));
92 #endif
93             if (readProjectionName)
94             {
95                 // %projectionNumber have to be processed before %projection, otherwise it does not work
96                 placeholders.push_back(Placeholder(wxT("%projectionNumber"), wxString::Format(wxT("%d"), opts.getProjection())));
97                 placeholders.push_back(Placeholder(wxT("%projection"), wxString(proj.name, wxConvLocal)));
98             };
99             // fill in some placeholders
100             placeholders.push_back(Placeholder(wxT("%hfov"), wxString::Format(wxT("%.0f"), opts.getHFOV())));
101             placeholders.push_back(Placeholder(wxT("%vfov"), wxString::Format(wxT("%.0f"), opts.getVFOV())));
102             placeholders.push_back(Placeholder(wxT("%ev"), wxString::Format(wxT("%.2f"), opts.outputExposureValue)));
103             placeholders.push_back(Placeholder(wxT("%nrImages"), wxString::Format(wxT("%lu"), (unsigned long)images.size())));
104             placeholders.push_back(Placeholder(wxT("%nrAllImages"), wxString::Format(wxT("%lu"), (unsigned long)pano.getNrOfImages())));
105             placeholders.push_back(Placeholder(wxT("%fullwidth"), wxString::Format(wxT("%u"), opts.getWidth())));
106             placeholders.push_back(Placeholder(wxT("%fullheight"), wxString::Format(wxT("%u"), opts.getHeight())));
107             placeholders.push_back(Placeholder(wxT("%width"), wxString::Format(wxT("%d"), opts.getROI().width())));
108             placeholders.push_back(Placeholder(wxT("%height"), wxString::Format(wxT("%d"), opts.getROI().height())));
109             wxFileName projectFilename(projectName);
110             placeholders.push_back(Placeholder(wxT("%projectname"), projectFilename.GetFullName()));
111             // now open the final argfile
112             wxFileName tempArgfileFinal(wxFileName::CreateTempFileName(GetConfigTempDir(config) + wxT("he")));
113             wxFFileOutputStream outputStream(tempArgfileFinal.GetFullPath());
114             wxTextOutputStream outputFile(outputStream);
115             // write argfile
116             outputFile << wxT("-Software=Hugin ") << wxString(hugin_utils::GetHuginVersion().c_str(), wxConvLocal) << endl;
117             outputFile << wxT("-E") << endl;
118             outputFile << wxT("-UserComment<${UserComment}") << linebreak;
119             if (readProjectionName)
120             {
121                 outputFile << wxT("Projection: ") << wxString(proj.name, wxConvLocal) << wxT(" (") << opts.getProjection() << wxT(")") << linebreak;
122             };
123             outputFile << wxT("FOV: ") << wxString::Format(wxT("%.0f"), opts.getHFOV()) << wxT(" x ") << wxString::Format(wxT("%.0f"), opts.getVFOV()) << linebreak;
124             outputFile << wxT("Ev: ") << wxString::Format(wxT("%.2f"), opts.outputExposureValue) << endl;
125             outputFile << wxT("-f") << endl;
126             if (generateGPanoTags)
127             {
128                 //GPano tags are only indented for equirectangular images
129                 if (opts.getProjection() == HuginBase::PanoramaOptions::EQUIRECTANGULAR)
130                 {
131                     const vigra::Rect2D roi = opts.getROI();
132                     int left = roi.left();
133                     int top = roi.top();
134                     int width = roi.width();
135                     int height = roi.height();
136 
137                     int fullWidth = opts.getWidth();
138                     if (opts.getHFOV()<360)
139                     {
140                         fullWidth = static_cast<int>(opts.getWidth() * 360.0 / opts.getHFOV());
141                         left += (fullWidth - opts.getWidth()) / 2;
142                     };
143                     int fullHeight = opts.getHeight();
144                     if (opts.getVFOV()<180)
145                     {
146                         fullHeight = static_cast<int>(opts.getHeight() * 180.0 / opts.getVFOV());
147                         top += (fullHeight - opts.getHeight()) / 2;
148                     };
149                     outputFile << wxT("-UsePanoramaViewer=True") << endl;
150                     outputFile << wxT("-StitchingSoftware=Hugin") << endl;
151                     outputFile << wxT("-ProjectionType=equirectangular") << endl;
152                     outputFile << wxT("-CroppedAreaLeftPixels=") << left << endl;
153                     outputFile << wxT("-CroppedAreaTopPixels=") << top << endl;
154                     outputFile << wxT("-CroppedAreaImageWidthPixels=") << width << endl;
155                     outputFile << wxT("-CroppedAreaImageHeightPixels=") << height << endl;
156                     outputFile << wxT("-FullPanoWidthPixels=") << fullWidth << endl;
157                     outputFile << wxT("-FullPanoHeightPixels=") << fullHeight << endl;
158                     outputFile << wxT("-SourcePhotosCount=") << static_cast<wxUint32>(pano.getNrOfImages()) << endl;
159                 };
160             };
161             // now open the input file and append it
162             if (!argfileInput.IsEmpty())
163             {
164                 if (wxFileExists(argfileInput))
165                 {
166                     wxFileInputStream inputFileStream(argfileInput);
167                     wxTextInputStream input(inputFileStream);
168                     while (inputFileStream.IsOk() && !inputFileStream.Eof())
169                     {
170                         wxString line = input.ReadLine();
171                         // replace all placeholders
172                         for (auto variable : placeholders)
173                         {
174                             line.Replace(variable.placeholder, variable.value, true);
175                         };
176                         // now append to existing argfile
177                         outputFile << line << endl;
178                     };
179                 };
180             };
181             return tempArgfileFinal.GetFullPath();
182         };
183 
PrintDetailInfo(const HuginBase::Panorama & pano,const HuginBase::PanoramaOptions & opts,const HuginBase::UIntSet & allActiveImages,const wxString & prefix,const wxString & bindir,wxConfigBase * config,double & exiftoolVersion)184         wxString PrintDetailInfo(const HuginBase::Panorama& pano, const HuginBase::PanoramaOptions& opts, const HuginBase::UIntSet& allActiveImages, const wxString& prefix, const wxString& bindir, wxConfigBase* config, double& exiftoolVersion)
185         {
186             wxString output;
187             const wxString wxEndl(wxT("\n"));
188             output
189                 << wxT("============================================") << wxEndl
190                 << _("Stitching panorama...") << wxEndl
191                 << wxT("============================================") << wxEndl
192                 << wxEndl
193                 << _("Platform:") << wxT(" ") << wxGetOsDescription() << wxEndl
194                 << _("Version:") << wxT(" ") << wxString(hugin_utils::GetHuginVersion().c_str(), wxConvLocal) << wxEndl
195                 << _("Working directory:") << wxT(" ") << wxFileName::GetCwd() << wxEndl
196                 << _("Output prefix:") << wxT(" ") << prefix << wxEndl
197                 << wxEndl;
198             if (opts.outputLDRBlended || opts.outputLDRExposureBlended || opts.outputLDRExposureLayersFused ||
199                 opts.outputHDRBlended || opts.outputLDRExposureLayers)
200             {
201                 switch (opts.blendMode)
202                 {
203                     case HuginBase::PanoramaOptions::ENBLEND_BLEND:
204                         {
205                             wxArrayString version;
206                             if (wxExecute(wxEscapeFilename(GetExternalProgram(config, bindir, wxT("enblend"))) + wxT(" --version"), version, wxEXEC_SYNC) == 0l)
207                             {
208                                 output << _("Blender:") << wxT(" ") << version[0] << wxEndl;
209                             }
210                             else
211                             {
212                                 output << _("Blender:") << wxT(" ") << _("Unknown blender (enblend --version failed)") << wxEndl;
213                             };
214                         };
215                         break;
216                     case HuginBase::PanoramaOptions::INTERNAL_BLEND:
217                     default:  // switch to internal blender for all other cases, not exposed in GUI
218                         output << _("Blender:") << wxT(" ") << _("internal") << wxEndl;
219                         break;
220                 };
221             };
222             if (opts.outputLDRExposureBlended || opts.outputLDRExposureLayersFused || opts.outputLDRStacks )
223             {
224                 wxArrayString version;
225                 if (wxExecute(wxEscapeFilename(GetExternalProgram(config, bindir, wxT("enfuse"))) + wxT(" --version"), version, wxEXEC_SYNC) == 0l)
226                 {
227                     output << _("Exposure fusion:") << wxT(" ") << version[0] << wxEndl;
228                 }
229                 else
230                 {
231                     output << _("Exposure fusion:") << wxT(" ") << _("Unknown exposure fusion (enfuse --version failed)") << wxEndl;
232                 };
233             };
234             if (config->Read(wxT("/output/useExiftool"), HUGIN_USE_EXIFTOOL) == 1l)
235             {
236                 wxArrayString version;
237                 if (wxExecute(wxEscapeFilename(GetExternalProgram(config, bindir, wxT("exiftool"))) + wxT(" -ver"), version, wxEXEC_SYNC) == 0l)
238                 {
239                     output << _("ExifTool version:") << wxT(" ") << version[0] << wxEndl;
240                     version[0].ToCDouble(&exiftoolVersion);
241                 }
242                 else
243                 {
244                     output << _("ExifTool:") << wxT(" ") << _("FAILED") << wxEndl;
245                     exiftoolVersion = 1;
246                 };
247             };
248             output
249                 << wxEndl
250                 << _("Number of active images:") << wxT(" ") << allActiveImages.size() << wxEndl
251                 << wxString::Format(_("Output exposure value: %.1f"), opts.outputExposureValue) << wxEndl
252                 << wxString::Format(_("Canvas size: %dx%d"), opts.getSize().width(), opts.getSize().height()) << wxEndl
253                 << wxString::Format(_("ROI: (%d, %d) - (%d, %d)"), opts.getROI().left(), opts.getROI().top(), opts.getROI().right(), opts.getROI().bottom()) << wxT(" ") << wxEndl
254                 << wxString::Format(_("FOV: %.0fx%.0f"), opts.getHFOV(), opts.getVFOV()) << wxEndl;
255             pano_projection_features proj;
256             const bool readProjectionName = panoProjectionFeaturesQuery(opts.getProjection(), &proj) != 0;
257             if (readProjectionName)
258             {
259                 output
260                     << _("Projection:") << wxT(" ") << wxGetTranslation(wxString(proj.name, wxConvLocal))
261                     << wxT("(") << opts.getProjection() << wxT(")") << wxEndl;
262             }
263             else
264             {
265                 output
266                     << _("Projection:") << wxT(" ") << opts.getProjection() << wxEndl;
267             };
268             output
269                 << _("Using GPU for remapping:") << wxT(" ") << (opts.remapUsingGPU ? _("true") : _("false")) << wxEndl
270                 << wxEndl;
271             if (opts.outputLDRBlended || opts.outputLDRExposureBlended || opts.outputLDRExposureLayersFused || opts.outputHDRBlended)
272             {
273                 output << _("Panorama Outputs:") << wxEndl;
274                 if (opts.outputLDRBlended)
275                 {
276                     output << wxT("* ") << _("Exposure corrected, low dynamic range") << wxEndl;
277                 };
278                 if (opts.outputLDRExposureBlended)
279                 {
280                     output << wxT("* ") << _("Exposure fused from stacks") << wxEndl;
281                 };
282                 if (opts.outputLDRExposureLayersFused)
283                 {
284                     output << wxT("* ") << _("Exposure fused from any arrangement") << wxEndl;
285                 };
286                 if (opts.outputHDRBlended)
287                 {
288                     output << wxT("* ") << _("High dynamic range") << wxEndl;
289                 };
290                 output << wxEndl;
291             };
292             if (opts.outputLDRLayers || opts.outputLDRExposureRemapped || opts.outputHDRLayers)
293             {
294                 output << _("Remapped Images:") << wxEndl;
295                 if (opts.outputLDRBlended)
296                 {
297                     output << wxT("* ") << _("Exposure corrected, low dynamic range") << wxEndl;
298                 };
299                 if (opts.outputLDRExposureRemapped)
300                 {
301                     output << wxT("* ") << _("No exposure correction, low dynamic range") << wxEndl;
302                 };
303                 if (opts.outputHDRLayers)
304                 {
305                     output << wxT("* ") << _("High dynamic range") << wxEndl;
306                 };
307                 output << wxEndl;
308             };
309             if (opts.outputLDRStacks || opts.outputHDRStacks)
310             {
311                 output << _("Combined stacks:") << wxEndl;
312                 if (opts.outputLDRStacks)
313                 {
314                     output << wxT("* ") << _("Exposure fused stacks") << wxEndl;
315                 };
316                 if (opts.outputHDRStacks)
317                 {
318                     output << wxT("* ") << _("High dynamic range") << wxEndl;
319                 };
320                 output << wxEndl;
321             };
322             if (opts.outputLDRExposureLayers)
323             {
324                 output << _("Layers:") << wxEndl
325                     << wxT("* ") << _("Blended layers of similar exposure, without exposure correction") << wxEndl
326                     << wxEndl;
327             };
328             const HuginBase::SrcPanoImage img = pano.getImage(*allActiveImages.begin());
329             output << _("First input image") << wxEndl
330                 << _("Number:") << wxT(" ") << *allActiveImages.begin() << wxEndl
331                 << _("Filename:") << wxT(" ") << img.getFilename() << wxEndl
332                 << wxString::Format(_("Size: %dx%d"), img.getWidth(), img.getHeight()) << wxEndl
333                 << _("Projection:") << wxT(" ") << getProjectionString(img) << wxEndl
334                 << _("Response type:") << wxT(" ") << getResponseString(img) << wxEndl
335                 << wxString::Format(_("HFOV: %.0f"), img.getHFOV()) << wxEndl
336                 << wxString::Format(_("Exposure value: %.1f"), img.getExposureValue()) << wxEndl
337                 << wxEndl;
338             return output;
339         };
340 
341         /** build quoted filename list for verdandi */
GetQuotedFilenamesStringForVerdandi(const wxArrayString & files,const HuginBase::Panorama & pano,const HuginBase::UIntSetVector & stacks,const int referenceImage,const bool hardSeam)342         wxString GetQuotedFilenamesStringForVerdandi(const wxArrayString& files, const HuginBase::Panorama& pano, const HuginBase::UIntSetVector& stacks, const int referenceImage, const bool hardSeam)
343         {
344             // if output is hard seam we keep the order
345             if (hardSeam)
346             {
347                 return GetQuotedFilenamesString(files);
348             };
349             // user wants a blended seam, we need to figure out the correct order
350             int refImage = 0;
351             // first build a subpano which contains only one image per stack of the original pano
352             HuginBase::UIntSet stackImgs;
353             for (size_t i = 0; i < stacks.size(); ++i)
354             {
355                 if (set_contains(stacks[i], referenceImage))
356                 {
357                     refImage = i;
358                 };
359                 stackImgs.insert(*(stacks[i].begin()));
360             };
361             // now create the subpano, don't forget to delete at end
362             HuginBase::PanoramaData* subpano = pano.getNewSubset(stackImgs);
363             HuginBase::UIntSet subpanoImgs;
364             fill_set(subpanoImgs, 0, stackImgs.size() - 1);
365             // find the blend order
366             HuginBase::UIntVector blendOrder = HuginBase::getEstimatedBlendingOrder(*subpano, subpanoImgs, refImage);
367             delete subpano;
368             // now build the string in the correct order
369             wxString s;
370             for (size_t i = 0; i < blendOrder.size();++i)
371             {
372                 s.Append(wxEscapeFilename(files[blendOrder[i]]) + wxT(" "));
373             };
374             return s;
375         };
376     } // namespace detail
377 
GetStitchingCommandQueue(const HuginBase::Panorama & pano,const wxString & ExePath,const wxString & project,const wxString & prefix,wxString & statusText,wxArrayString & outputFiles,wxArrayString & tempFilesDelete,std::ostream & errStream)378     CommandQueue* GetStitchingCommandQueue(const HuginBase::Panorama & pano, const wxString& ExePath, const wxString& project, const wxString& prefix, wxString& statusText, wxArrayString& outputFiles, wxArrayString& tempFilesDelete, std::ostream& errStream)
379     {
380         CommandQueue* commands = new CommandQueue;
381         const HuginBase::UIntSet allActiveImages = getImagesinROI(pano, pano.getActiveImages());
382         if (allActiveImages.empty())
383         {
384             errStream << "ERROR: No active images in ROI. Nothing to do." << std::endl;
385             return commands;
386         }
387         std::vector<HuginBase::UIntSet> stacks;
388 
389         // check options, not all are currently supported
390         HuginBase::PanoramaOptions opts = pano.getOptions();
391         wxConfigBase* config = wxConfigBase::Get();
392         opts.remapUsingGPU = config->Read(wxT("/Nona/UseGPU"), HUGIN_NONA_USEGPU) == 1;
393         if (opts.remapper != HuginBase::PanoramaOptions::NONA)
394         {
395             errStream << "ERROR: Only nona remappper is supported by hugin_executor." << std::endl;
396             return commands;
397         };
398         if (opts.blendMode != HuginBase::PanoramaOptions::ENBLEND_BLEND && opts.blendMode != HuginBase::PanoramaOptions::INTERNAL_BLEND)
399         {
400             errStream << "ERROR: Only enblend and internal remappper are currently supported by hugin_executor." << std::endl;
401             return commands;
402         };
403         if (opts.hdrMergeMode != HuginBase::PanoramaOptions::HDRMERGE_AVERAGE)
404         {
405             errStream << "ERROR: Only hdr merger HDRMERGE_AVERAGE is currently supported by hugin_executor." << std::endl;
406             return commands;
407         };
408         double exiftoolVersion;
409         statusText=detail::PrintDetailInfo(pano, opts, allActiveImages, prefix, ExePath, config, exiftoolVersion);
410         // prepare some often needed variables
411         const wxString quotedProject(wxEscapeFilename(project));
412         // prepare nona arguments
413         wxString nonaArgs(wxT("-v "));
414         wxString enLayersCompressionArgs;
415         if (!opts.outputLayersCompression.empty())
416         {
417             nonaArgs.Append(wxT("-z ") + opts.outputLayersCompression + wxT(" "));
418             enLayersCompressionArgs.Append(wxT(" --compression=") + opts.outputLayersCompression + wxT(" "));
419         }
420         else
421         {
422             if (opts.outputImageType == "jpg")
423             {
424                 nonaArgs.Append(wxT("-z LZW "));
425             }
426         };
427         if (opts.remapUsingGPU)
428         {
429             nonaArgs.Append(wxT("-g "));
430         };
431         // prepare enblend arguments
432         wxString enblendArgs;
433         if (opts.blendMode == HuginBase::PanoramaOptions::ENBLEND_BLEND)
434         {
435             enblendArgs.Append(opts.enblendOptions);
436             if ((opts.getHFOV() == 360.0) && (opts.getWidth()==opts.getROI().width()))
437             {
438                 enblendArgs.Append(wxT(" -w"));
439             };
440             const vigra::Rect2D roi (opts.getROI());
441             if (roi.top() != 0 || roi.left() != 0)
442             {
443                 enblendArgs << wxT(" -f") << roi.width() << wxT("x") << roi.height() << wxT("+") << roi.left() << wxT("+") << roi.top();
444             }
445             else
446             {
447                 enblendArgs << wxT(" -f") << roi.width() << wxT("x") << roi.height();
448             };
449             enblendArgs.Append(wxT(" "));
450         };
451         // prepare internal blending arguments
452         wxString verdandiArgs;
453         if (opts.blendMode == HuginBase::PanoramaOptions::INTERNAL_BLEND)
454         {
455             verdandiArgs.Append(opts.verdandiOptions);
456             if ((opts.getHFOV() == 360.0) && (opts.getWidth() == opts.getROI().width()))
457             {
458                 verdandiArgs.Append(wxT(" -w"));
459             };
460         };
461         // prepare the compression switches
462         wxString finalCompressionArgs;
463         if (opts.outputImageType == "tif" && !opts.outputImageTypeCompression.empty())
464         {
465             finalCompressionArgs << wxT(" --compression=") << opts.outputImageTypeCompression;
466         }
467         else
468         {
469             if (opts.outputImageType == "jpg")
470             {
471                 finalCompressionArgs << wxT(" --compression=") << opts.quality;
472             };
473         };
474         finalCompressionArgs.Append(wxT(" "));
475         // prepare enfuse arguments
476         wxString enfuseArgs(opts.enfuseOptions + wxT(" "));
477         if ((opts.getHFOV() == 360.0) && (opts.getWidth() == opts.getROI().width()))
478         {
479             enfuseArgs.Append(wxT(" -w"));
480         };
481         const vigra::Rect2D roi (opts.getROI());
482         if (roi.top() != 0 || roi.left() != 0)
483         {
484             enfuseArgs << wxT(" -f") << roi.width() << wxT("x") << roi.height() << wxT("+") << roi.left() << wxT("+") << roi.top();
485         }
486         else
487         {
488             enfuseArgs << wxT(" -f") << roi.width() << wxT("x") << roi.height();
489         };
490         enfuseArgs.Append(wxT(" "));
491 
492         // prepare exiftool args
493         const bool copyMetadata = config->Read(wxT("/output/useExiftool"), HUGIN_USE_EXIFTOOL) == 1l;
494         wxString exiftoolArgs;
495         wxString exiftoolArgsFinal;
496         if (copyMetadata)
497         {
498             exiftoolArgs = wxT("-overwrite_original -TagsFromFile ");
499             exiftoolArgs.Append(wxEscapeFilename(wxString(pano.getImage(0).getFilename().c_str(), HUGIN_CONV_FILENAME)));
500             // required tags, can not be overwritten
501             exiftoolArgs.Append(wxT(" -WhitePoint -ColorSpace"));
502             wxString exiftoolArgfile = config->Read(wxT("/output/CopyArgfile"), wxEmptyString);
503             if (exiftoolArgfile.IsEmpty())
504             {
505                 exiftoolArgfile = wxString(std::string(hugin_utils::GetDataDir() + "hugin_exiftool_copy.arg").c_str(), HUGIN_CONV_FILENAME);
506             };
507             wxFileName argfile(exiftoolArgfile);
508             argfile.Normalize();
509             exiftoolArgs.Append(wxT(" -@ ") + wxEscapeFilename(argfile.GetFullPath()) + wxT(" "));
510             wxString finalArgfile = detail::GenerateFinalArgfile(pano, project, config, allActiveImages, exiftoolVersion);
511             if (!finalArgfile.IsEmpty())
512             {
513                 exiftoolArgsFinal.Append(wxT(" -@ ") + wxEscapeFilename(finalArgfile) + wxT(" "));
514                 tempFilesDelete.Add(finalArgfile);
515             };
516         };
517         wxArrayString filesForFullExiftool;
518         wxArrayString filesForCopyTagsExiftool;
519 
520         // normal output
521         if (opts.outputLDRBlended || opts.outputLDRLayers)
522         {
523             const wxArrayString remappedImages(detail::GetNumberedFilename(prefix, wxT(".tif"), allActiveImages));
524             const wxString finalFilename(prefix + wxT(".") + opts.outputImageType);
525             if (opts.blendMode == HuginBase::PanoramaOptions::INTERNAL_BLEND && opts.outputLDRBlended)
526             {
527                 wxString finalNonaArgs(wxT("-v -r ldr "));
528                 if (opts.remapUsingGPU)
529                 {
530                     finalNonaArgs.Append(wxT("-g "));
531                 }
532                 if (!opts.verdandiOptions.empty())
533                 {
534                     finalNonaArgs.Append(opts.verdandiOptions);
535                     finalNonaArgs.Append(wxT(" "));
536                 };
537                 if (opts.outputImageType == "tif")
538                 {
539                     finalNonaArgs.Append(wxT("-m TIFF "));
540                     if (!opts.outputImageTypeCompression.empty())
541                     {
542                         finalNonaArgs.Append(wxT("-z ") + opts.outputImageTypeCompression + wxT(" "));
543                     };
544                 }
545                 else
546                 {
547                     if (opts.outputImageType == "jpg")
548                     {
549                         finalNonaArgs.Append(wxT("-m JPEG -z "));
550                         finalNonaArgs << opts.quality << wxT(" ");
551                     }
552                     else
553                     {
554                         if (opts.outputImageType == "png")
555                         {
556                             finalNonaArgs.Append(wxT("-m PNG "));
557                         }
558                         else
559                         {
560                             errStream << "ERROR: Invalid output image type found." << std::endl;
561                             return commands;
562                         };
563                     };
564                 };
565                 if (opts.outputLDRLayers)
566                 {
567                     finalNonaArgs.Append(wxT("--save-intermediate-images "));
568                     detail::AddToArray(remappedImages, outputFiles);
569                 }
570                 finalNonaArgs.Append(wxT("-o ") + wxEscapeFilename(prefix) + wxT(" ") + quotedProject);
571                 commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("nona")),
572                     finalNonaArgs, _("Remapping and blending LDR images...")));
573                 outputFiles.Add(finalFilename);
574                 if (copyMetadata)
575                 {
576                     filesForFullExiftool.Add(finalFilename);
577                 };
578             }
579             else
580             {
581                 commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("nona")),
582                     nonaArgs + wxT("-r ldr -m TIFF_m -o ") + wxEscapeFilename(prefix) + wxT(" ") + quotedProject,
583                     _("Remapping LDR images...")));
584                 detail::AddToArray(remappedImages, outputFiles);
585                 if (opts.outputLDRBlended)
586                 {
587                     if (opts.blendMode == HuginBase::PanoramaOptions::ENBLEND_BLEND)
588                     {
589                         wxString finalEnblendArgs(enblendArgs + finalCompressionArgs);
590                         finalEnblendArgs.Append(wxT(" -o ") + wxEscapeFilename(finalFilename) + wxT(" -- "));
591                         commands->push_back(new NormalCommand(
592                             GetExternalProgram(config, ExePath, wxT("enblend")),
593                             finalEnblendArgs + wxT(" ") + GetQuotedFilenamesString(remappedImages),
594                             _("Blending images..."))
595                         );
596                         outputFiles.Add(finalFilename);
597                         if (copyMetadata)
598                         {
599                             filesForFullExiftool.Add(finalFilename);
600                         };
601                     };
602                     if (!opts.outputLDRLayers)
603                     {
604                         detail::AddToArray(remappedImages, tempFilesDelete);
605                     };
606                 };
607             };
608         };
609         // exposure fusion output
610         if (opts.outputLDRExposureRemapped || opts.outputLDRStacks || opts.outputLDRExposureLayers ||
611             opts.outputLDRExposureBlended || opts.outputLDRExposureLayersFused)
612         {
613             const wxArrayString remappedImages = detail::GetNumberedFilename(prefix + wxT("_exposure_layers_"), wxT(".tif"), allActiveImages);
614             std::vector<HuginBase::UIntSet> exposureLayers;
615             wxArrayString exposureLayersFiles;
616             if (opts.outputLDRExposureLayers || opts.outputLDRExposureLayersFused)
617             {
618                 exposureLayers = getExposureLayers(pano, allActiveImages, opts);
619             };
620             if (opts.blendMode == HuginBase::PanoramaOptions::INTERNAL_BLEND &&
621                 (opts.outputLDRExposureLayers || opts.outputLDRExposureLayersFused))
622             {
623                 // directly export exposure layers by nona
624                 wxString finalNonaArgs(nonaArgs);
625                 if (!opts.verdandiOptions.empty())
626                 {
627                     finalNonaArgs.Append(opts.verdandiOptions);
628                     finalNonaArgs.Append(wxT(" "));
629                 };
630                 finalNonaArgs.append(wxT("-r ldr --create-exposure-layers --ignore-exposure -o ") + wxEscapeFilename(prefix + wxT("_exposure_")));
631                 if (opts.outputLDRExposureRemapped || opts.outputLDRStacks || opts.outputLDRExposureBlended)
632                 {
633                     finalNonaArgs.append(wxT(" --save-intermediate-images --intermediate-suffix=layers_"));
634                     detail::AddToArray(remappedImages, outputFiles);
635                     if (!opts.outputLDRExposureRemapped)
636                     {
637                         detail::AddToArray(remappedImages, tempFilesDelete);
638                     }
639                 };
640                 finalNonaArgs.append(wxT(" "));
641                 finalNonaArgs.append(quotedProject);
642                 commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("nona")),
643                     finalNonaArgs, _("Remapping LDR images and blending exposure layers...")));
644                 HuginBase::UIntSet exposureLayersNumber;
645                 fill_set(exposureLayersNumber, 0, exposureLayers.size() - 1);
646                 exposureLayersFiles = detail::GetNumberedFilename(prefix + wxT("_exposure_"), wxT(".tif"), exposureLayersNumber);
647                 detail::AddToArray(exposureLayersFiles, outputFiles);
648                 if (!opts.outputLDRExposureLayers)
649                 {
650                     detail::AddToArray(exposureLayersFiles, tempFilesDelete);
651                 };
652                 if (copyMetadata && opts.outputLDRExposureLayers)
653                 {
654                     detail::AddToArray(exposureLayersFiles, filesForCopyTagsExiftool);
655                 };
656             }
657             else
658             {
659                 commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("nona")),
660                     nonaArgs + wxT("-r ldr -m TIFF_m --ignore-exposure -o ") + wxEscapeFilename(prefix + wxT("_exposure_layers_")) + wxT(" ") + quotedProject,
661                     _("Remapping LDR images without exposure correction...")));
662                 detail::AddToArray(remappedImages, outputFiles);
663                 if (!opts.outputLDRExposureRemapped)
664                 {
665                     detail::AddToArray(remappedImages, tempFilesDelete);
666                 };
667                 if (opts.outputLDRExposureLayers || opts.outputLDRExposureLayersFused)
668                 {
669                     // blending exposure layers, then fusing
670                     // fuse all exposure layers
671                     for (unsigned exposureLayer = 0; exposureLayer < exposureLayers.size(); ++exposureLayer)
672                     {
673                         const wxArrayString exposureLayersImgs = detail::GetNumberedFilename(prefix + wxT("_exposure_layers_"), wxT(".tif"), exposureLayers[exposureLayer]);
674                         const wxString exposureLayerImgName = wxString::Format(wxT("%s_exposure_%04u%s"), prefix.c_str(), exposureLayer, wxT(".tif"));
675                         exposureLayersFiles.Add(exposureLayerImgName);
676                         outputFiles.Add(exposureLayerImgName);
677                         // variant with internal blender is handled before, so we need only enblend
678                         commands->push_back(new NormalCommand(
679                             GetExternalProgram(config, ExePath, wxT("enblend")),
680                             enblendArgs + enLayersCompressionArgs + wxT(" -o ") + wxEscapeFilename(exposureLayerImgName) + wxT(" -- ") + GetQuotedFilenamesString(exposureLayersImgs),
681                             wxString::Format(_("Blending exposure layer %u..."), exposureLayer))
682                         );
683                         if (copyMetadata && opts.outputLDRExposureLayers)
684                         {
685                             filesForCopyTagsExiftool.Add(exposureLayerImgName);
686                         };
687                         if (!opts.outputLDRExposureLayers)
688                         {
689                             tempFilesDelete.Add(exposureLayerImgName);
690                         };
691                     };
692                 };
693             };
694             if (opts.outputLDRExposureLayersFused)
695             {
696                 wxString finalEnfuseArgs(enfuseArgs + finalCompressionArgs);
697                 const wxString fusedExposureLayersFilename(prefix + wxT("_blended_fused.") + opts.outputImageType);
698                 finalEnfuseArgs.Append(wxT(" -o ") + wxEscapeFilename(fusedExposureLayersFilename) + wxT(" -- "));
699                 commands->push_back(new NormalCommand(
700                     GetExternalProgram(config, ExePath, wxT("enfuse")),
701                     finalEnfuseArgs + wxT(" ")+GetQuotedFilenamesString(exposureLayersFiles),
702                     _("Fusing all exposure layers..."))
703                 );
704                 outputFiles.Add(fusedExposureLayersFilename);
705                 if (copyMetadata)
706                 {
707                     filesForFullExiftool.Add(fusedExposureLayersFilename);
708                 };
709             };
710             if (opts.outputLDRStacks || opts.outputLDRExposureBlended)
711             {
712                 // fusing stacks, then blending
713                 stacks = getHDRStacks(pano, allActiveImages, opts);
714                 wxArrayString stackedImages;
715                 // fuse all stacks
716                 for (unsigned stackNr = 0; stackNr < stacks.size(); ++stackNr)
717                 {
718                     const wxArrayString stackImgs = detail::GetNumberedFilename(prefix + wxT("_exposure_layers_"), wxT(".tif"), stacks[stackNr]);
719                     const wxString stackImgName = wxString::Format(wxT("%s_stack_ldr_%04u%s"), prefix.c_str(), stackNr, wxT(".tif"));
720                     outputFiles.Add(stackImgName);
721                     stackedImages.Add(stackImgName);
722                     commands->push_back(new NormalCommand(
723                         GetExternalProgram(config, ExePath, wxT("enfuse")),
724                         enfuseArgs + enLayersCompressionArgs + wxT(" -o ") + wxEscapeFilename(stackImgName) + wxT(" -- ") + GetQuotedFilenamesString(stackImgs),
725                         wxString::Format(_("Fusing stack number %u..."), stackNr))
726                     );
727                     if (copyMetadata && opts.outputLDRStacks)
728                     {
729                         filesForCopyTagsExiftool.Add(stackImgName);
730                     };
731                     if (!opts.outputLDRStacks)
732                     {
733                         tempFilesDelete.Add(stackImgName);
734                     };
735                 };
736                 if (opts.outputLDRExposureBlended)
737                 {
738                     const wxString fusedStacksFilename(prefix + wxT("_fused.") + opts.outputImageType);
739                     switch (opts.blendMode)
740                     {
741                     case HuginBase::PanoramaOptions::ENBLEND_BLEND:
742                         {
743                             wxString finalEnblendArgs(enblendArgs + finalCompressionArgs);
744                             finalEnblendArgs.Append(wxT(" -o ") + wxEscapeFilename(fusedStacksFilename) + wxT(" -- "));
745                             commands->push_back(new NormalCommand(
746                                 GetExternalProgram(config, ExePath, wxT("enblend")),
747                                 finalEnblendArgs+wxT(" ")+GetQuotedFilenamesString(stackedImages),
748                                 _("Blending all stacks..."))
749                             );
750                         };
751                         break;
752                     case HuginBase::PanoramaOptions::INTERNAL_BLEND:
753                     default:  // switch to internal blender for all other cases, not exposed in GUI
754                         {
755                             wxString finalVerdandiArgs(verdandiArgs + finalCompressionArgs);
756                             finalVerdandiArgs.Append(wxT(" -o ") + wxEscapeFilename(fusedStacksFilename));
757                             finalVerdandiArgs.Append(wxT(" -- ") + detail::GetQuotedFilenamesStringForVerdandi(stackedImages, pano, stacks, opts.colorReferenceImage, opts.verdandiOptions.find("--seam=blend") == std::string::npos));
758                             commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("verdandi")),
759                                 finalVerdandiArgs, _("Blending all stacks...")));
760                         };
761                         break;
762                     };
763                     outputFiles.Add(fusedStacksFilename);
764                     if (copyMetadata)
765                     {
766                         filesForFullExiftool.Add(fusedStacksFilename);
767                     };
768                 };
769             };
770         };
771         // hdr output
772         if (opts.outputHDRLayers || opts.outputHDRStacks || opts.outputHDRBlended)
773         {
774             commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("nona")),
775                 nonaArgs + wxT("-r hdr -m EXR_m  -o ") + wxEscapeFilename(prefix + wxT("_hdr_")) + wxT(" ") + quotedProject,
776                 _("Remapping HDR images...")));
777             const wxArrayString remappedHDR = detail::GetNumberedFilename(prefix + wxT("_hdr_"), wxT(".exr"), allActiveImages);
778             const wxArrayString remappedHDRComp = detail::GetNumberedFilename(prefix + wxT("_hdr_"), wxT("_gray.pgm"), allActiveImages);
779             detail::AddToArray(remappedHDR, outputFiles);
780             detail::AddToArray(remappedHDRComp, outputFiles);
781             if (opts.outputHDRStacks || opts.outputHDRBlended)
782             {
783                 // merging stacks, then blending
784                 if (stacks.empty())
785                 {
786                     stacks = getHDRStacks(pano, allActiveImages, opts);
787                 };
788                 wxArrayString stackedImages;
789                 // merge all stacks
790                 for (unsigned stackNr = 0; stackNr < stacks.size(); ++stackNr)
791                 {
792                     const wxArrayString stackImgs = detail::GetNumberedFilename(prefix + wxT("_hdr_"), wxT(".exr"), stacks[stackNr]);
793                     const wxString stackImgName = wxString::Format(wxT("%s_stack_hdr_%04u%s"), prefix.c_str(), stackNr, wxT(".exr"));
794                     stackedImages.Add(stackImgName);
795                     outputFiles.Add(stackImgName);
796                     commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("hugin_hdrmerge")),
797                         opts.hdrmergeOptions + wxT(" -o ") + wxEscapeFilename(stackImgName) + wxT(" -- ") + GetQuotedFilenamesString(stackImgs),
798                         wxString::Format(_("Merging HDR stack number %u..."), stackNr)));
799                     if (!opts.outputHDRStacks)
800                     {
801                         tempFilesDelete.Add(stackImgName);
802                     };
803                 };
804                 if (opts.outputHDRBlended)
805                 {
806                     const wxString mergedStacksFilename(prefix + wxT("_hdr.") + opts.outputImageTypeHDR);
807                     wxString finalBlendArgs(wxT(" -o ") + wxEscapeFilename(mergedStacksFilename) + wxT(" -- "));
808                     switch (opts.blendMode)
809                     {
810                         case HuginBase::PanoramaOptions::ENBLEND_BLEND:
811                             commands->push_back(new NormalCommand(
812                                 GetExternalProgram(config, ExePath, wxT("enblend")),
813                                 enblendArgs + finalBlendArgs + wxT(" ") + GetQuotedFilenamesString(stackedImages),
814                                     _("Blending HDR stacks..."))
815                             );
816                             break;
817                         case HuginBase::PanoramaOptions::INTERNAL_BLEND:
818                         default:  // switch to internal blender for all other cases, not exposed in GUI
819                             commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("verdandi")),
820                                 verdandiArgs + finalBlendArgs + detail::GetQuotedFilenamesStringForVerdandi(stackedImages, pano, stacks, opts.colorReferenceImage, opts.verdandiOptions.find("--seam=blend") == std::string::npos),
821                                 _("Blending HDR stacks...")));
822                             break;
823                     };
824                     outputFiles.Add(mergedStacksFilename);
825                     if (copyMetadata)
826                     {
827                         filesForFullExiftool.Add(mergedStacksFilename);
828                     };
829                 };
830             };
831             if (!opts.outputHDRLayers)
832             {
833                 detail::AddToArray(remappedHDR, tempFilesDelete);
834                 detail::AddToArray(remappedHDRComp, tempFilesDelete);
835             };
836         };
837         // update metadata
838         if (!filesForCopyTagsExiftool.IsEmpty())
839         {
840             commands->push_back(new OptionalCommand(GetExternalProgram(config, ExePath, wxT("exiftool")),
841                 exiftoolArgs + GetQuotedFilenamesString(filesForCopyTagsExiftool),
842                 _("Updating metadata...")));
843         };
844         if (!filesForFullExiftool.IsEmpty())
845         {
846             commands->push_back(new OptionalCommand(GetExternalProgram(config, ExePath, wxT("exiftool")),
847                 exiftoolArgs + exiftoolArgsFinal + GetQuotedFilenamesString(filesForFullExiftool),
848                 _("Updating metadata...")));
849         };
850         return commands;
851     };
852 
853     // now the user defined stitching engine
854     // we read the settings from an ini file and construct our CommandQueue
855     namespace detail
856     {
857         // add program with argment to queue
858         // do some checks on the way
859         // if an error occurs or the input is not valid, the queue is cleared and the function returns false
AddBlenderCommand(CommandQueue * queue,const wxString & ExePath,const wxString & prog,const int & stepNr,const wxString & arguments,const wxString & description,std::ostream & errStream)860         bool AddBlenderCommand(CommandQueue* queue, const wxString& ExePath, const wxString& prog,
861             const int& stepNr, const wxString& arguments, const wxString& description, std::ostream& errStream)
862         {
863             if (prog.IsEmpty())
864             {
865                 errStream << "ERROR: Step " << stepNr << " has no program name specified." << std::endl;
866                 CleanQueue(queue);
867                 return false;
868             }
869             // check program name
870             // get full path for some internal commands
871             wxString program;
872             if (prog.CmpNoCase(wxT("verdandi")) == 0)
873             {
874                 program = GetInternalProgram(ExePath, wxT("verdandi"));
875             }
876             else
877             {
878                 if (prog.CmpNoCase(wxT("hugin_hdrmerge")) == 0)
879                 {
880                     program = GetInternalProgram(ExePath, wxT("hugin_hdrmerge"));
881                 }
882                 else
883                 {
884                     program = prog;
885                 }
886             };
887             // now add to queue
888             queue->push_back(new NormalCommand(program, arguments, description));
889             return true;
890         };
891 
892         // replace the %prefix% placeholder, with optional postfix
893         // the string is modified in place
894         // return true on success, false if there are errors
ReplacePrefixPlaceholder(wxString & args,const wxString prefix)895         bool ReplacePrefixPlaceholder(wxString& args, const wxString prefix)
896         {
897             int prefixPos = args.Find("%prefix");
898             while (prefixPos != wxNOT_FOUND)
899             {
900                 const wxString nextChar = args.Mid(prefixPos + 7, 1);
901                 if (nextChar == "%")
902                 {
903                     args.Replace("%prefix%", wxEscapeFilename(prefix), true);
904                 }
905                 else
906                 {
907                     if (nextChar == ",")
908                     {
909                         const int closingPercent = args.Mid(prefixPos + 8).Find("%");
910                         if (closingPercent < 2)
911                         {
912                             return false;
913                         };
914                         wxString postfix = args.Mid(prefixPos + 8, closingPercent);
915                         args.Replace("%prefix," + postfix + "%", wxEscapeFilename(prefix + postfix), true);
916                     }
917                     else
918                     {
919                         return false;
920                     };
921                 };
922                 prefixPos = args.Find("%prefix");
923             };
924             return true;
925         };
926 
927         // replace the %with% or %height% placeholder
ReplaceWidthHeightPlaceHolder(wxString & args,const wxString name,int value)928         bool ReplaceWidthHeightPlaceHolder(wxString& args, const wxString name, int value)
929         {
930             int pos = args.Find("%" + name);
931             while (pos != wxNOT_FOUND)
932             {
933                 const wxString nextChar = args.Mid(pos + 1 + name.Len(), 1);
934                 if (nextChar == "%")
935                 {
936                     args.Replace("%" + name + "%", wxString::Format("%d", value), true);
937                 }
938                 else
939                 {
940                     if (nextChar == "*")
941                     {
942                         const int closingPercent = args.Mid(pos + 2 + name.Len()).Find("%");
943                         if (closingPercent < 2)
944                         {
945                             return false;
946                         };
947                         wxString factorString = args.Mid(pos + 2 + name.Len(), closingPercent);
948                         double factor;
949                         if (!factorString.ToCDouble(&factor))
950                         {
951                             return false;
952                         };
953                         args.Replace("%" + name + "*" + factorString + "%", wxString::Format("%d", hugin_utils::roundi(factor*value)), true);
954                     }
955                     else
956                     {
957                         return false;
958                     };
959                 };
960                 pos = args.Find("%" + name);
961             };
962             return true;
963         };
964 
965     }
966 
GetStitchingCommandQueueUserOutput(const HuginBase::Panorama & pano,const wxString & ExePath,const wxString & project,const wxString & prefix,const wxString & outputSettings,wxString & statusText,wxArrayString & outputFiles,wxArrayString & tempFilesDelete,std::ostream & errStream)967     CommandQueue* GetStitchingCommandQueueUserOutput(const HuginBase::Panorama & pano, const wxString& ExePath, const wxString& project, const wxString& prefix, const wxString& outputSettings, wxString& statusText, wxArrayString& outputFiles, wxArrayString& tempFilesDelete, std::ostream& errStream)
968     {
969         CommandQueue* commands = new CommandQueue;
970         const HuginBase::UIntSet allActiveImages = getImagesinROI(pano, pano.getActiveImages());
971         if (allActiveImages.empty())
972         {
973             errStream << "ERROR: No active images in ROI. Nothing to do." << std::endl;
974             return commands;
975         }
976         wxFileInputStream input(outputSettings);
977         if (!input.IsOk())
978         {
979             errStream << "ERROR: Can not open file \"" << outputSettings.mb_str(wxConvLocal) << "\"." << std::endl;
980             return commands;
981         }
982         wxFileConfig settings(input);
983         long stepCount;
984         settings.Read(wxT("/General/StepCount"), &stepCount, 0);
985         if (stepCount == 0)
986         {
987             errStream << "ERROR: User-setting does not define any output steps." << std::endl;
988             return commands;
989         }
990         const wxString desc = GetSettingStringTranslated(&settings, wxT("/General/Description"), wxEmptyString);
991         if (desc.IsEmpty())
992         {
993             statusText = wxString::Format(_("Stitching using \"%s\""), outputSettings.c_str());
994         }
995         else
996         {
997             statusText = wxString::Format(_("Stitching using \"%s\""), desc.c_str());
998         };
999         wxString intermediateImageType = GetSettingString(&settings, wxT("/General/IntermediateImageType"), wxT(".tif"));
1000         // add point if missing
1001         if (intermediateImageType.Left(1).Cmp(wxT("."))!=0)
1002         {
1003             intermediateImageType.Prepend(wxT("."));
1004         }
1005         // prepare some often needed variables/strings
1006         const HuginBase::PanoramaOptions opts = pano.getOptions();
1007         const bool needsWrapSwitch = (opts.getHFOV() == 360.0) && (opts.getWidth() == opts.getROI().width());
1008         const vigra::Rect2D roi(opts.getROI());
1009         wxString sizeString;
1010         if (roi.top() != 0 || roi.left() != 0)
1011         {
1012             sizeString << roi.width() << wxT("x") << roi.height() << wxT("+") << roi.left() << wxT("+") << roi.top();
1013         }
1014         else
1015         {
1016             sizeString << roi.width() << wxT("x") << roi.height();
1017         };
1018         const wxArrayString remappedImages(detail::GetNumberedFilename(prefix, intermediateImageType, allActiveImages));
1019         wxArrayString inputImages;
1020         for (auto& i : allActiveImages)
1021         {
1022             inputImages.Add(wxString(pano.getImage(i).getFilename().c_str(), HUGIN_CONV_FILENAME));
1023         };
1024 
1025         std::vector<HuginBase::UIntSet> exposureLayers;
1026         wxArrayString exposureLayersFiles;
1027 
1028         std::vector<HuginBase::UIntSet> stacks;
1029         wxArrayString stacksFiles;
1030 
1031         // now iterate all steps
1032         for (size_t i = 0; i < stepCount; ++i)
1033         {
1034             wxString stepString(wxT("/Step"));
1035             stepString << i;
1036             if (!settings.HasGroup(stepString))
1037             {
1038                 errStream << "ERROR: Output specifies " << stepCount << " steps, but step " << i << " is missing in configuration." << std::endl;
1039                 CleanQueue(commands);
1040                 return commands;
1041             }
1042             settings.SetPath(stepString);
1043             const wxString stepType=GetSettingString(&settings, wxT("Type"));
1044             if (stepType.IsEmpty())
1045             {
1046                 errStream << "ERROR: \"" << stepString.mb_str(wxConvLocal) << "\" has no type defined." << std::endl;
1047                 CleanQueue(commands);
1048                 return commands;
1049             };
1050             wxString args = GetSettingString(&settings, wxT("Arguments"));
1051             if (args.IsEmpty())
1052             {
1053                 errStream << "ERROR: Step " << i << " has no arguments given." << std::endl;
1054                 CleanQueue(commands);
1055                 return commands;
1056             }
1057             const wxString description = GetSettingStringTranslated(&settings, wxT("Description"));
1058             if (stepType.CmpNoCase(wxT("remap")) == 0)
1059             {
1060                 // build nona command
1061                 const bool outputLayers = (settings.Read(wxT("OutputExposureLayers"), 0l) == 1l);
1062                 if (outputLayers)
1063                 {
1064                     args.Append(wxT(" --create-exposure-layers -o ") + wxEscapeFilename(prefix + wxT("_layer")));
1065                 }
1066                 else
1067                 {
1068                     args.Append(wxT(" -o ") + wxEscapeFilename(prefix));
1069                 };
1070                 args.Append(wxT(" ") + wxEscapeFilename(project));
1071                 commands->push_back(new NormalCommand(GetInternalProgram(ExePath, wxT("nona")),
1072                     args, description));
1073                 if (outputLayers)
1074                 {
1075                     if (exposureLayers.empty())
1076                     {
1077                         exposureLayers = getExposureLayers(pano, allActiveImages, opts);
1078                         HuginBase::UIntSet exposureLayersNumber;
1079                         fill_set(exposureLayersNumber, 0, exposureLayers.size() - 1);
1080                         exposureLayersFiles = detail::GetNumberedFilename(prefix + wxT("_layer"), intermediateImageType, exposureLayersNumber);
1081                     };
1082                     detail::AddToArray(exposureLayersFiles, outputFiles);
1083                     if (settings.Read(wxT("Keep"), 0l) == 0l)
1084                     {
1085                         detail::AddToArray(exposureLayersFiles, tempFilesDelete);
1086                     };
1087                 }
1088                 else
1089                 {
1090                     const wxArrayString remappedHDRComp = detail::GetNumberedFilename(prefix, wxT("_gray.pgm"), allActiveImages);
1091                     const bool hdrOutput = args.MakeLower().Find(wxT("-r hdr")) != wxNOT_FOUND;
1092                     detail::AddToArray(remappedImages, outputFiles);
1093                     if (hdrOutput)
1094                     {
1095                         detail::AddToArray(remappedHDRComp, outputFiles);
1096                     };
1097                     if (settings.Read(wxT("Keep"), 0l) == 0l)
1098                     {
1099                         detail::AddToArray(remappedImages, tempFilesDelete);
1100                         if (hdrOutput)
1101                         {
1102                             detail::AddToArray(remappedHDRComp, tempFilesDelete);
1103                         };
1104                     };
1105                 };
1106             }
1107             else
1108             {
1109                 if (stepType.CmpNoCase(wxT("merge")) == 0)
1110                 {
1111                     // build a merge command
1112                     wxString resultFile = GetSettingString(&settings, wxT("Result"));
1113                     if (resultFile.IsEmpty())
1114                     {
1115                         errStream << "ERROR: Step " << i << " has no result file specified." << std::endl;
1116                         CleanQueue(commands);
1117                         return commands;
1118                     };
1119                     resultFile.Replace(wxT("%prefix%"), prefix, true);
1120                     if (args.Replace(wxT("%result%"), wxEscapeFilename(resultFile), true) == 0)
1121                     {
1122                         errStream << "ERROR: Step " << i << " has missing %result% placeholder in arguments." << std::endl;
1123                         CleanQueue(commands);
1124                         return commands;
1125                     };
1126                     const wxString BlenderInput = GetSettingString(&settings, wxT("Input"), wxT("all"));
1127                     // set the input images depending on the input
1128                     if (BlenderInput.CmpNoCase(wxT("all")) == 0)
1129                     {
1130                         if (args.Replace(wxT("%input%"), GetQuotedFilenamesString(remappedImages), true) == 0)
1131                         {
1132                             errStream << "ERROR: Step " << i << " has missing %input% placeholder in arguments." << std::endl;
1133                             CleanQueue(commands);
1134                             return commands;
1135                         };
1136                     }
1137                     else
1138                     {
1139                         if (BlenderInput.CmpNoCase(wxT("stacks")) == 0)
1140                         {
1141                             if (stacks.empty())
1142                             {
1143                                 stacks= HuginBase::getHDRStacks(pano, allActiveImages, opts);
1144                                 HuginBase::UIntSet stackNumbers;
1145                                 fill_set(stackNumbers, 0, stacks.size() - 1);
1146                                 stacksFiles = detail::GetNumberedFilename(prefix + wxT("_stack"), intermediateImageType, stackNumbers);
1147                             };
1148                             if (args.Replace(wxT("%input%"), GetQuotedFilenamesString(stacksFiles), true) == 0)
1149                             {
1150                                 errStream << "ERROR: Step " << i << " has missing %input% placeholder in arguments." << std::endl;
1151                                 CleanQueue(commands);
1152                                 return commands;
1153                             };
1154                         }
1155                         else
1156                         {
1157                             if (BlenderInput.CmpNoCase(wxT("layers")) == 0)
1158                             {
1159                                 if (exposureLayers.empty())
1160                                 {
1161                                     exposureLayers = getExposureLayers(pano, allActiveImages, opts);
1162                                     HuginBase::UIntSet exposureLayersNumber;
1163                                     fill_set(exposureLayersNumber, 0, exposureLayers.size() - 1);
1164                                     exposureLayersFiles = detail::GetNumberedFilename(prefix + wxT("_layer"), intermediateImageType, exposureLayersNumber);
1165                                 };
1166                                 if (args.Replace(wxT("%input%"), GetQuotedFilenamesString(exposureLayersFiles), true) == 0)
1167                                 {
1168                                     errStream << "ERROR: Step " << i << " has missing %input% placeholder in arguments." << std::endl;
1169                                     CleanQueue(commands);
1170                                     return commands;
1171                                 };
1172                             }
1173                             else
1174                             {
1175                                 errStream << "ERROR: Step " << i << " has invalid input type: \"" << BlenderInput.mb_str(wxConvLocal) << "\"." << std::endl;
1176                                 CleanQueue(commands);
1177                                 return commands;
1178                             };
1179                         };
1180                     };
1181                     args.Replace(wxT("%size%"), sizeString, true);
1182                     wxString wrapSwitch = GetSettingString(&settings, wxT("WrapArgument"));
1183                     if (needsWrapSwitch && !wrapSwitch.IsEmpty())
1184                     {
1185                         args.Prepend(wrapSwitch + wxT(" "));
1186                     }
1187                     if (!detail::AddBlenderCommand(commands, ExePath, GetSettingString(&settings, wxT("Program")), i,
1188                         args, description, errStream))
1189                     {
1190                         return commands;
1191                     };
1192                     outputFiles.Add(resultFile);
1193                     if (settings.Read(wxT("Keep"), 1l) == 0l)
1194                     {
1195                         tempFilesDelete.Add(resultFile);
1196                     };
1197                 }
1198                 else
1199                 {
1200                     if (stepType.CmpNoCase(wxT("stack")) == 0)
1201                     {
1202                         // build command for each stack
1203                         if (stacks.empty())
1204                         {
1205                             stacks = HuginBase::getHDRStacks(pano, allActiveImages, opts);
1206                             HuginBase::UIntSet stackNumbers;
1207                             fill_set(stackNumbers, 0, stacks.size() - 1);
1208                             stacksFiles = detail::GetNumberedFilename(prefix + wxT("_stack"), intermediateImageType, stackNumbers);
1209                         };
1210                         const bool clean = (settings.Read(wxT("Keep"), 0l) == 0l);
1211                         args.Replace(wxT("%size%"), sizeString, true);
1212                         // now iterate each stack
1213                         for (size_t stackNr = 0; stackNr < stacks.size(); ++stackNr)
1214                         {
1215                             wxString finalArgs(args);
1216                             wxArrayString remappedStackImages = detail::GetNumberedFilename(prefix, intermediateImageType, stacks[stackNr]);
1217                             if (finalArgs.Replace(wxT("%input%"), GetQuotedFilenamesString(remappedStackImages), true) == 0)
1218                             {
1219                                 errStream << "ERROR: Step " << i << " has missing %input% placeholder in arguments." << std::endl;
1220                                 CleanQueue(commands);
1221                                 return commands;
1222                             };
1223                             if (finalArgs.Replace(wxT("%output%"), wxEscapeFilename(stacksFiles[stackNr]), true) == 0)
1224                             {
1225                                 errStream << "ERROR: Step " << i << " has missing %output% placeholder in arguments." << std::endl;
1226                                 CleanQueue(commands);
1227                                 return commands;
1228                             };
1229                             if (!detail::AddBlenderCommand(commands, ExePath, GetSettingString(&settings, wxT("Program")), i,
1230                                 finalArgs, description, errStream))
1231                             {
1232                                 return commands;
1233                             };
1234                             outputFiles.Add(stacksFiles[stackNr]);
1235                             if (clean)
1236                             {
1237                                 tempFilesDelete.Add(stacksFiles[stackNr]);
1238                             };
1239                         }
1240                     }
1241                     else
1242                     {
1243                         if (stepType.CmpNoCase(wxT("layer")) == 0)
1244                         {
1245                             // build command for each exposure layer
1246                             if (exposureLayers.empty())
1247                             {
1248                                 exposureLayers = HuginBase::getExposureLayers(pano, allActiveImages, opts);
1249                                 HuginBase::UIntSet exposureLayersNumber;
1250                                 fill_set(exposureLayersNumber, 0, exposureLayers.size() - 1);
1251                                 exposureLayersFiles = detail::GetNumberedFilename(prefix + wxT("_layer"), intermediateImageType, exposureLayersNumber);
1252                             };
1253                             const bool clean = (settings.Read(wxT("Keep"), 0l) == 0l);
1254                             args.Replace(wxT("%size%"), sizeString, true);
1255                             // iterate all exposure layers
1256                             for (size_t exposureLayerNr = 0; exposureLayerNr < exposureLayers.size(); ++exposureLayerNr)
1257                             {
1258                                 wxString finalArgs(args);
1259                                 wxArrayString remappedLayerImages = detail::GetNumberedFilename(prefix, intermediateImageType, exposureLayers[exposureLayerNr]);
1260                                 if (finalArgs.Replace(wxT("%input%"), GetQuotedFilenamesString(remappedLayerImages), true) == 0)
1261                                 {
1262                                     errStream << "ERROR: Step " << i << " has missing %input% placeholder in arguments." << std::endl;
1263                                     CleanQueue(commands);
1264                                     return commands;
1265                                 };
1266                                 if (finalArgs.Replace(wxT("%output%"), wxEscapeFilename(exposureLayersFiles[exposureLayerNr]), true) == 0)
1267                                 {
1268                                     errStream << "ERROR: Step " << i << " has missing %output% placeholder in arguments." << std::endl;
1269                                     CleanQueue(commands);
1270                                     return commands;
1271                                 };
1272                                 if (!detail::AddBlenderCommand(commands, ExePath, GetSettingString(&settings, wxT("Program")), i,
1273                                     finalArgs, description, errStream))
1274                                 {
1275                                     return commands;
1276                                 };
1277                                 outputFiles.Add(exposureLayersFiles[exposureLayerNr]);
1278                                 if (clean)
1279                                 {
1280                                     tempFilesDelete.Add(exposureLayersFiles[exposureLayerNr]);
1281                                 };
1282                             }
1283                         }
1284                         else
1285                         {
1286                             if (stepType.CmpNoCase(wxT("modify")) == 0)
1287                             {
1288                                 // build a modify command
1289                                 wxString inputFiles = GetSettingString(&settings, wxT("File"));
1290                                 if (inputFiles.IsEmpty())
1291                                 {
1292                                     errStream << "ERROR: Step " << i << " has no input/output file specified." << std::endl;
1293                                     CleanQueue(commands);
1294                                     return commands;
1295                                 };
1296                                 if (args.Find(wxT("%file%")) == wxNOT_FOUND)
1297                                 {
1298                                     errStream << "ERROR: Step " << i << " has missing %file% placeholder in arguments." << std::endl;
1299                                     CleanQueue(commands);
1300                                     return commands;
1301                                 };
1302                                 args.Replace(wxT("%project%"), wxEscapeFilename(project), true);
1303                                 if (!detail::ReplacePrefixPlaceholder(args, prefix))
1304                                 {
1305                                     errStream << "ERROR: Step " << i << " has invalid %prefix% placeholder in arguments." << std::endl;
1306                                     CleanQueue(commands);
1307                                     return commands;
1308                                 };
1309                                 if (!detail::ReplaceWidthHeightPlaceHolder(args, "width", opts.getWidth()))
1310                                 {
1311                                     errStream << "ERROR: Step " << i << " has invalid %width% placeholder in arguments." << std::endl;
1312                                     CleanQueue(commands);
1313                                     return commands;
1314                                 }
1315                                 if (!detail::ReplaceWidthHeightPlaceHolder(args, "height", opts.getHeight()))
1316                                 {
1317                                     errStream << "ERROR: Step " << i << " has invalid %height% placeholder in arguments." << std::endl;
1318                                     CleanQueue(commands);
1319                                     return commands;
1320                                 }
1321                                 const wxString progName = GetSettingString(&settings, wxT("Program"));
1322                                 if (progName.IsEmpty())
1323                                 {
1324                                     errStream << "ERROR: Step " << i << " has no program name specified." << std::endl;
1325                                     CleanQueue(commands);
1326                                     return commands;
1327                                 };
1328 #ifdef __WXMAC__
1329                                 // check if program can be found in bundle
1330                                 const wxString prog = GetExternalProgram(wxConfig::Get(), ExePath, progName);
1331 #elif defined __WXMSW__
1332 
1333                                 const wxString prog = MSWGetProgname(ExePath, progName);
1334 #else
1335                                 const wxString prog = progName;
1336 #endif
1337                                 if (inputFiles.CmpNoCase(wxT("all")) == 0)
1338                                 {
1339                                     for (size_t imgNr = 0; imgNr < remappedImages.size(); ++imgNr)
1340                                     {
1341                                         wxString finalArgs(args);
1342                                         finalArgs.Replace(wxT("%file%"), wxEscapeFilename(remappedImages[imgNr]), true);
1343                                         finalArgs.Replace(wxT("%sourceimage%"), wxEscapeFilename(inputImages[imgNr]), true);
1344                                         commands->push_back(new NormalCommand(prog, finalArgs, description));
1345                                     };
1346                                 }
1347                                 else
1348                                 {
1349                                     if (inputFiles.CmpNoCase(wxT("stacks")) == 0)
1350                                     {
1351                                         if (stacks.empty())
1352                                         {
1353                                             errStream << "ERROR: Step " << i << " requests to modify stacks, but no stack was created before." << std::endl;
1354                                             CleanQueue(commands);
1355                                             return commands;
1356                                         };
1357                                         for (size_t stackNr = 0; stackNr < stacksFiles.size(); ++stackNr)
1358                                         {
1359                                             wxString finalArgs(args);
1360                                             finalArgs.Replace(wxT("%file%"), wxEscapeFilename(stacksFiles[stackNr]), true);
1361                                             commands->push_back(new NormalCommand(prog, finalArgs, description));
1362                                         };
1363                                     }
1364                                     else
1365                                     {
1366                                         if (inputFiles.CmpNoCase(wxT("layers")) == 0)
1367                                         {
1368                                             if (exposureLayers.empty())
1369                                             {
1370                                                 errStream << "ERROR: Step " << i << " requests to modify exposure layers, but no exposure layer was created before." << std::endl;
1371                                                 CleanQueue(commands);
1372                                                 return commands;
1373                                             };
1374                                             for (size_t layerNr = 0; layerNr < exposureLayersFiles.size(); ++layerNr)
1375                                             {
1376                                                 wxString finalArgs(args);
1377                                                 finalArgs.Replace(wxT("%file%"), wxEscapeFilename(exposureLayersFiles[layerNr]), true);
1378                                                 commands->push_back(new NormalCommand(prog, finalArgs, description));
1379                                             };
1380                                         }
1381                                         else
1382                                         {
1383                                             inputFiles.Replace(wxT("%prefix%"), prefix, true);
1384                                             args.Replace(wxT("%file%"), wxEscapeFilename(inputFiles), true);
1385                                             commands->push_back(new NormalCommand(prog , args, description));
1386                                         };
1387                                     };
1388                                 };
1389                             }
1390                             else
1391                             {
1392                                 if (stepType.CmpNoCase(wxT("exiftool")) == 0)
1393                                 {
1394                                     wxString resultFile = GetSettingString(&settings, wxT("Result"));
1395                                     if (resultFile.IsEmpty())
1396                                     {
1397                                         errStream << "ERROR: Step " << i << " has no result file specified." << std::endl;
1398                                         CleanQueue(commands);
1399                                         return commands;
1400                                     };
1401                                     resultFile.Replace(wxT("%prefix%"), prefix, true);
1402                                     if (args.Replace(wxT("%result%"), wxEscapeFilename(resultFile), true) == 0)
1403                                     {
1404                                         errStream << "ERROR: Step " << i << " has missing %result% placeholder in arguments." << std::endl;
1405                                         CleanQueue(commands);
1406                                         return commands;
1407                                     };
1408                                     args.Replace(wxT("%image0%"), wxEscapeFilename(wxString(pano.getImage(0).getFilename().c_str(), HUGIN_CONV_FILENAME)), true);
1409                                     commands->push_back(new OptionalCommand(GetExternalProgram(wxConfigBase::Get(), ExePath, wxT("exiftool")),
1410                                         args, description));
1411                                 }
1412                                 else
1413                                 {
1414                                     errStream << "ERROR: Step " << i << " has unknown Type \"" << stepType.mb_str(wxConvLocal) << "\"." << std::endl;
1415                                     CleanQueue(commands);
1416                                     return commands;
1417                                 };
1418                             };
1419                         };
1420                     };
1421                 };
1422             };
1423         };
1424         return commands;
1425     }
1426 
1427     /** return a wxString with all files in files quoted */
GetQuotedFilenamesString(const wxArrayString & files)1428     wxString GetQuotedFilenamesString(const wxArrayString& files)
1429     {
1430         wxString s;
1431         for (size_t i = 0; i < files.size(); ++i)
1432         {
1433             s.Append(wxEscapeFilename(files[i]) + wxT(" "));
1434         };
1435         return s;
1436     };
1437 
1438 }; // namespace
1439