/src/apps/ocioconvert/main.cpp
C++ | 699 lines | 578 code | 96 blank | 25 comment | 87 complexity | 0ee7fb62e83220117e0592d252a5ae5e MD5 | raw file
Possible License(s): BSD-3-Clause
1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright Contributors to the OpenColorIO Project.
3
4#include <chrono>
5#include <cstdlib>
6#include <iostream>
7#include <sstream>
8#include <utility>
9#include <vector>
10
11#include <OpenColorIO/OpenColorIO.h>
12namespace OCIO = OCIO_NAMESPACE;
13
14#include <OpenImageIO/imageio.h>
15#include <OpenImageIO/typedesc.h>
16#if (OIIO_VERSION < 10100)
17namespace OIIO = OIIO_NAMESPACE;
18#endif
19
20#include "apputils/argparse.h"
21
22#ifdef OCIO_GPU_ENABLED
23#include "oglapp.h"
24#endif // OCIO_GPU_ENABLED
25
26#include "oiiohelpers.h"
27#include "OpenEXR/half.h"
28
29
30// Array of non OpenColorIO arguments.
31static std::vector<std::string> args;
32
33
34// Fill 'args' array with OpenColorIO arguments.
35static int
36parse_end_args(int argc, const char *argv[])
37{
38 while(argc>0)
39 {
40 args.push_back(argv[0]);
41 argc--;
42 argv++;
43 }
44
45 return 0;
46}
47
48bool ParseNameValuePair(std::string& name, std::string& value,
49 const std::string& input);
50
51bool StringToFloat(float * fval, const char * str);
52
53bool StringToInt(int * ival, const char * str);
54
55bool StringToVector(std::vector<int> * ivector, const char * str);
56
57int main(int argc, const char **argv)
58{
59 ArgParse ap;
60
61 std::vector<std::string> floatAttrs;
62 std::vector<std::string> intAttrs;
63 std::vector<std::string> stringAttrs;
64 std::string keepChannels;
65 bool croptofull = false;
66 bool usegpu = false;
67 bool usegpuLegacy = false;
68 bool outputgpuInfo = false;
69 bool verbose = false;
70 bool useLut = false;
71 bool useDisplayView = false;
72
73 ap.options("ocioconvert -- apply colorspace transform to an image \n\n"
74 "usage: ocioconvert [options] inputimage inputcolorspace outputimage outputcolorspace\n"
75 " or: ocioconvert [options] --lut lutfile inputimage outputimage\n"
76 " or: ocioconvert [options] --view inputimage inputcolorspace outputimage displayname viewname\n\n",
77 "%*", parse_end_args, "",
78 "<SEPARATOR>", "Options:",
79 "--lut", &useLut, "Convert using a LUT rather than a config file",
80 "--view", &useDisplayView, "Convert to a (display,view) pair rather than to "
81 "an output color space",
82 "--gpu", &usegpu, "Use GPU color processing instead of CPU (CPU is the default)",
83 "--gpulegacy", &usegpuLegacy, "Use the legacy (i.e. baked) GPU color processing "
84 "instead of the CPU one (--gpu is ignored)",
85 "--gpuinfo", &outputgpuInfo, "Output the OCIO shader program",
86 "--v", &verbose, "Display general information",
87 "<SEPARATOR>", "\nOpenImageIO options:",
88 "--float-attribute %L", &floatAttrs, "\"name=float\" pair defining OIIO float attribute "
89 "for outputimage",
90 "--int-attribute %L", &intAttrs, "\"name=int\" pair defining OIIO int attribute "
91 "for outputimage",
92 "--string-attribute %L", &stringAttrs, "\"name=string\" pair defining OIIO string attribute "
93 "for outputimage",
94 "--croptofull", &croptofull, "Crop or pad to make pixel data region match the "
95 "\"full\" region",
96 "--ch %s", &keepChannels, "Select channels (e.g., \"2,3,4\")",
97 NULL
98 );
99 if (ap.parse (argc, argv) < 0)
100 {
101 std::cerr << ap.geterror() << std::endl;
102 ap.usage ();
103 exit(1);
104 }
105
106#ifndef OCIO_GPU_ENABLED
107 if (usegpu || outputgpuInfo || usegpuLegacy)
108 {
109 std::cerr << "Compiled without OpenGL support, GPU options are not available.";
110 std::cerr << std::endl;
111 exit(1);
112 }
113#endif // OCIO_GPU_ENABLED
114
115 const char * inputimage = nullptr;
116 const char * inputcolorspace = nullptr;
117 const char * outputimage = nullptr;
118 const char * outputcolorspace = nullptr;
119 const char * lutFile = nullptr;
120 const char * display = nullptr;
121 const char * view = nullptr;
122
123 if (!useLut && !useDisplayView)
124 {
125 if (args.size() != 4)
126 {
127 std::cerr << "ERROR: Expecting 4 arguments, found " << args.size() << std::endl;
128 ap.usage();
129 exit(1);
130 }
131 inputimage = args[0].c_str();
132 inputcolorspace = args[1].c_str();
133 outputimage = args[2].c_str();
134 outputcolorspace = args[3].c_str();
135 }
136 else if (useLut && useDisplayView)
137 {
138 std::cerr << "ERROR: Options lut & view can't be used at the same time." << std::endl;
139 ap.usage();
140 exit(1);
141 }
142 else if (useLut)
143 {
144 if (args.size() != 3)
145 {
146 std::cerr << "ERROR: Expecting 3 arguments for --lut option, found "
147 << args.size() << std::endl;
148 ap.usage();
149 exit(1);
150 }
151 lutFile = args[0].c_str();
152 inputimage = args[1].c_str();
153 outputimage = args[2].c_str();
154 }
155 else if (useDisplayView)
156 {
157 if (args.size() != 5)
158 {
159 std::cerr << "ERROR: Expecting 5 arguments for --view option, found "
160 << args.size() << std::endl;
161 ap.usage();
162 exit(1);
163 }
164 inputimage = args[0].c_str();
165 inputcolorspace = args[1].c_str();
166 outputimage = args[2].c_str();
167 display = args[3].c_str();
168 view = args[4].c_str();
169 }
170
171 if(verbose)
172 {
173 std::cout << std::endl;
174 std::cout << "OIIO Version: " << OIIO_VERSION_STRING << std::endl;
175 std::cout << "OCIO Version: " << OCIO::GetVersion() << std::endl;
176 const char * env = getenv("OCIO");
177 if(env && *env)
178 {
179 try
180 {
181 std::cout << std::endl;
182 std::cout << "OCIO Configuration: '" << env << "'" << std::endl;
183 OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig();
184 std::cout << "OCIO search_path: " << config->getSearchPath() << std::endl;
185 }
186 catch (const OCIO::Exception & e)
187 {
188 std::cout << "ERROR loading config file: " << e.what() << std::endl;
189 exit(1);
190 }
191 catch(...)
192 {
193
194 std::cerr << "ERROR loading the config file: '" << env << "'";
195 exit(1);
196 }
197 }
198 }
199
200 if (usegpuLegacy)
201 {
202 std::cout << std::endl;
203 std::cout << "Using legacy OCIO v1 GPU color processing." << std::endl;
204 }
205 else if (usegpu)
206 {
207 std::cout << std::endl;
208 std::cout << "Using GPU color processing." << std::endl;
209 }
210
211 OIIO::ImageSpec spec;
212 OCIO::ImgBuffer img;
213 int imgwidth = 0;
214 int imgheight = 0;
215 int components = 0;
216
217 // Load the image.
218 std::cout << std::endl;
219 std::cout << "Loading " << inputimage << std::endl;
220 try
221 {
222#if OIIO_VERSION < 10903
223 OIIO::ImageInput* f = OIIO::ImageInput::create(inputimage);
224#else
225 auto f = OIIO::ImageInput::create(inputimage);
226#endif
227 if(!f)
228 {
229 std::cerr << "ERROR: Could not create image input." << std::endl;
230 exit(1);
231 }
232
233 f->open(inputimage, spec);
234
235 std::string error = f->geterror();
236 if(!error.empty())
237 {
238 std::cerr << "ERROR: Could not load image: " << error << std::endl;
239 exit(1);
240 }
241
242 OCIO::PrintImageSpec(spec, verbose);
243
244 imgwidth = spec.width;
245 imgheight = spec.height;
246 components = spec.nchannels;
247
248 if (usegpu || usegpuLegacy)
249 {
250 spec.format = OIIO::TypeDesc::FLOAT;
251 img.allocate(spec);
252
253 const bool ok = f->read_image(spec.format, img.getBuffer());
254 if(!ok)
255 {
256 std::cerr << "ERROR: Reading \"" << inputimage << "\" failed with: "
257 << f->geterror() << std::endl;
258 exit(1);
259 }
260
261 if(croptofull)
262 {
263 std::cerr << "ERROR: Crop disabled in GPU mode" << std::endl;
264 exit(1);
265 }
266 }
267 else
268 {
269 img.allocate(spec);
270
271 const bool ok = f->read_image(spec.format, img.getBuffer());
272 if(!ok)
273 {
274 std::cerr << "ERROR: Reading \"" << inputimage << "\" failed with: "
275 << f->geterror() << std::endl;
276 exit(1);
277 }
278 }
279
280#if OIIO_VERSION < 10903
281 OIIO::ImageInput::destroy(f);
282#endif
283
284 std::vector<int> kchannels;
285 //parse --ch argument.
286 if (keepChannels != "" && !StringToVector(&kchannels, keepChannels.c_str()))
287 {
288 std::cerr << "ERROR: --ch: '" << keepChannels
289 << "' should be comma-seperated integers" << std::endl;
290 exit(1);
291 }
292
293 //if kchannels not specified, then keep all channels.
294 if (kchannels.size() == 0)
295 {
296 kchannels.resize(components);
297 for (int channel=0; channel < components; channel++)
298 {
299 kchannels[channel] = channel;
300 }
301 }
302
303 if (croptofull)
304 {
305 imgwidth = spec.full_width;
306 imgheight = spec.full_height;
307
308 std::cout << "cropping to " << imgwidth
309 << "x" << imgheight << std::endl;
310 }
311
312 if (croptofull || (int)kchannels.size() < spec.nchannels)
313 {
314 // Redefine the spec so it matches the new bounding box.
315 OIIO::ImageSpec croppedSpec = spec;
316
317 croppedSpec.x = 0;
318 croppedSpec.y = 0;
319 croppedSpec.height = imgheight;
320 croppedSpec.width = imgwidth;
321 croppedSpec.nchannels = (int)(kchannels.size());
322
323 OCIO::ImgBuffer croppedImg(croppedSpec);
324
325 void * croppedBuf = croppedImg.getBuffer();
326 void * imgBuf = img.getBuffer();
327
328 // crop down bounding box and ditch all but n channels.
329 // img is a flattened 3 dimensional matrix heightxwidthxchannels.
330 // fill croppedimg with only the needed pixels.
331 for (int y=0 ; y < spec.height ; y++)
332 {
333 for (int x=0 ; x < spec.width; x++)
334 {
335 for (int k=0; k < (int)kchannels.size(); k++)
336 {
337 int channel = kchannels[k];
338 int current_pixel_y = y + spec.y;
339 int current_pixel_x = x + spec.x;
340
341 if (current_pixel_y >= 0 &&
342 current_pixel_x >= 0 &&
343 current_pixel_y < imgheight &&
344 current_pixel_x < imgwidth)
345 {
346 const size_t imgIdx = (y * spec.width * components)
347 + (x * components) + channel;
348
349 const size_t cropIdx = (current_pixel_y * imgwidth * kchannels.size())
350 + (current_pixel_x * kchannels.size())
351 + channel;
352
353 if(spec.format==OIIO::TypeDesc::FLOAT)
354 {
355 ((float*)croppedBuf)[cropIdx] = ((float*)imgBuf)[imgIdx];
356 }
357 else if(spec.format==OIIO::TypeDesc::HALF)
358 {
359 ((half*)croppedBuf)[cropIdx] = ((half*)imgBuf)[imgIdx];
360 }
361 else if(spec.format==OIIO::TypeDesc::UINT16)
362 {
363 ((uint16_t*)croppedBuf)[cropIdx] = ((uint16_t*)imgBuf)[imgIdx];
364 }
365 else if(spec.format==OIIO::TypeDesc::UINT8)
366 {
367 ((uint8_t*)croppedBuf)[cropIdx] = ((uint8_t*)imgBuf)[imgIdx];
368 }
369 else
370 {
371 std::cerr << "ERROR: Unsupported image type: "
372 << spec.format << std::endl;
373 exit(1);
374 }
375 }
376 }
377 }
378 }
379
380 components = (int)(kchannels.size());
381
382 img = std::move(croppedImg);
383 }
384 }
385 catch(...)
386 {
387 std::cerr << "ERROR: Loading file failed" << std::endl;
388 exit(1);
389 }
390
391#ifdef OCIO_GPU_ENABLED
392 // Initialize GPU.
393
394 OCIO::OglAppRcPtr oglApp;
395 if (usegpu || usegpuLegacy)
396 {
397 OCIO::OglApp::Components comp = OCIO::OglApp::COMPONENTS_RGBA;
398 if (components == 4)
399 {
400 comp = OCIO::OglApp::COMPONENTS_RGBA;
401 }
402 else if (components == 3)
403 {
404 comp = OCIO::OglApp::COMPONENTS_RGB;
405 }
406 else
407 {
408 std::cerr << "Cannot convert image with " << components << " components." << std::endl;
409 exit(1);
410 }
411
412 try
413 {
414 oglApp = std::make_shared<OCIO::OglApp>("ocioconvert", 256, 20);
415 }
416 catch (const OCIO::Exception & e)
417 {
418 std::cerr << std::endl << e.what() << std::endl;
419 exit(1);
420 }
421
422 if (outputgpuInfo)
423 {
424 oglApp->printGLInfo();
425 }
426
427 oglApp->initImage(imgwidth, imgheight, comp, (float *)img.getBuffer());
428
429 oglApp->createGLBuffers();
430 }
431#endif // OCIO_GPU_ENABLED
432
433 // Process the image.
434 try
435 {
436 // Load the current config.
437 OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig();
438
439 // Get the processor.
440 OCIO::ConstProcessorRcPtr processor;
441
442 try
443 {
444 if (useLut)
445 {
446 // Create the OCIO processor for the specified transform.
447 OCIO::FileTransformRcPtr t = OCIO::FileTransform::Create();
448 t->setSrc(lutFile);
449 t->setInterpolation(OCIO::INTERP_BEST);
450
451 processor = config->getProcessor(t);
452 }
453 else if (useDisplayView)
454 {
455 OCIO::DisplayTransformRcPtr t = OCIO::DisplayTransform::Create();
456 t->setInputColorSpaceName(inputcolorspace);
457 t->setDisplay(display);
458 t->setView(view);
459 processor = config->getProcessor(t);
460 }
461 else
462 {
463 processor = config->getProcessor(inputcolorspace, outputcolorspace);
464 }
465 }
466 catch (const OCIO::Exception & e)
467 {
468 std::cout << "ERROR: OCIO failed with: " << e.what() << std::endl;
469 exit(1);
470 }
471 catch (...)
472 {
473 std::cout << "ERROR: Creating processor unknown failure" << std::endl;
474 exit(1);
475 }
476
477#ifdef OCIO_GPU_ENABLED
478 if (usegpu || usegpuLegacy)
479 {
480 // Get the GPU shader program from the processor and set oglApp to use it.
481 OCIO::GpuShaderDescRcPtr shaderDesc;
482 if (usegpuLegacy)
483 {
484 shaderDesc = OCIO::GpuShaderDesc::CreateLegacyShaderDesc(32);
485 }
486 else
487 {
488 shaderDesc = OCIO::GpuShaderDesc::CreateShaderDesc();
489 }
490 shaderDesc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_1_3);
491 processor->getDefaultGPUProcessor()->extractGpuShaderInfo(shaderDesc);
492 oglApp->setShader(shaderDesc);
493
494 oglApp->reshape(imgwidth, imgheight);
495 oglApp->redisplay();
496 oglApp->readImage((float *)img.getBuffer());
497 }
498 else
499#endif // OCIO_GPU_ENABLED
500 {
501 const OCIO::BitDepth bitDepth = OCIO::GetBitDepth(spec);
502
503 OCIO::ConstCPUProcessorRcPtr cpuProcessor
504 = processor->getOptimizedCPUProcessor(bitDepth, bitDepth,
505 OCIO::OPTIMIZATION_DEFAULT);
506
507 const std::chrono::high_resolution_clock::time_point start
508 = std::chrono::high_resolution_clock::now();
509
510 OCIO::ImageDescRcPtr imgDesc = OCIO::CreateImageDesc(spec, img);
511 cpuProcessor->apply(*imgDesc);
512
513 if(verbose)
514 {
515 const std::chrono::high_resolution_clock::time_point end
516 = std::chrono::high_resolution_clock::now();
517
518 std::chrono::duration<float, std::milli> duration = end - start;
519
520 std::cout << std::endl;
521 std::cout << "CPU processing took: "
522 << duration.count()
523 << " ms" << std::endl;
524 }
525 }
526 }
527 catch(OCIO::Exception & exception)
528 {
529 std::cerr << "ERROR: OCIO failed with: " << exception.what() << std::endl;
530 exit(1);
531 }
532 catch(...)
533 {
534 std::cerr << "ERROR: Unknown error processing the image" << std::endl;
535 exit(1);
536 }
537
538 //
539 // set the provided OpenImageIO attributes.
540 //
541 bool parseerror = false;
542 for(unsigned int i=0; i<floatAttrs.size(); ++i)
543 {
544 std::string name, value;
545 float fval = 0.0f;
546
547 if(!ParseNameValuePair(name, value, floatAttrs[i]) ||
548 !StringToFloat(&fval,value.c_str()))
549 {
550 std::cerr << "ERROR: Attribute string '" << floatAttrs[i]
551 << "' should be in the form name=floatvalue" << std::endl;
552 parseerror = true;
553 continue;
554 }
555
556 spec.attribute(name, fval);
557 }
558
559 for(unsigned int i=0; i<intAttrs.size(); ++i)
560 {
561 std::string name, value;
562 int ival = 0;
563 if(!ParseNameValuePair(name, value, intAttrs[i]) ||
564 !StringToInt(&ival,value.c_str()))
565 {
566 std::cerr << "ERROR: Attribute string '" << intAttrs[i]
567 << "' should be in the form name=intvalue" << std::endl;
568 parseerror = true;
569 continue;
570 }
571
572 spec.attribute(name, ival);
573 }
574
575 for(unsigned int i=0; i<stringAttrs.size(); ++i)
576 {
577 std::string name, value;
578 if(!ParseNameValuePair(name, value, stringAttrs[i]))
579 {
580 std::cerr << "ERROR: Attribute string '" << stringAttrs[i]
581 << "' should be in the form name=value" << std::endl;
582 parseerror = true;
583 continue;
584 }
585
586 spec.attribute(name, value);
587 }
588
589 if(parseerror)
590 {
591 exit(1);
592 }
593
594 // Write out the result.
595 try
596 {
597#if OIIO_VERSION < 10903
598 OIIO::ImageOutput* f = OIIO::ImageOutput::create(outputimage);
599#else
600 auto f = OIIO::ImageOutput::create(outputimage);
601#endif
602 if(!f)
603 {
604 std::cerr << "ERROR: Could not create output input" << std::endl;
605 exit(1);
606 }
607
608 f->open(outputimage, spec);
609
610 if(!f->write_image(spec.format, img.getBuffer()))
611 {
612 std::cerr << "ERROR: Writing \"" << outputimage << "\" failed with: "
613 << f->geterror() << std::endl;
614 exit(1);
615 }
616
617 f->close();
618#if OIIO_VERSION < 10903
619 OIIO::ImageOutput::destroy(f);
620#endif
621 }
622 catch(...)
623 {
624 std::cerr << "ERROR: Writing file \"" << outputimage << "\"" << std::endl;
625 exit(1);
626 }
627
628 std::cout << std::endl;
629 std::cout << "Wrote " << outputimage << std::endl;
630
631 return 0;
632}
633
634
635// Parse name=value parts.
636// return true on success.
637
638bool ParseNameValuePair(std::string& name,
639 std::string& value,
640 const std::string& input)
641{
642 // split string into name=value.
643 size_t pos = input.find('=');
644 if(pos==std::string::npos) return false;
645
646 name = input.substr(0,pos);
647 value = input.substr(pos+1);
648 return true;
649}
650
651// return true on success.
652bool StringToFloat(float * fval, const char * str)
653{
654 if(!str) return false;
655
656 std::istringstream inputStringstream(str);
657 float x;
658 if(!(inputStringstream >> x))
659 {
660 return false;
661 }
662
663 if(fval) *fval = x;
664 return true;
665}
666
667bool StringToInt(int * ival, const char * str)
668{
669 if(!str) return false;
670
671 std::istringstream inputStringstream(str);
672 int x;
673 if(!(inputStringstream >> x))
674 {
675 return false;
676 }
677
678 if(ival) *ival = x;
679 return true;
680}
681
682bool StringToVector(std::vector<int> * ivector, const char * str)
683{
684 std::stringstream ss(str);
685 int i;
686 while (ss >> i)
687 {
688 ivector->push_back(i);
689 if (ss.peek() == ',')
690 {
691 ss.ignore();
692 }
693 }
694 return ivector->size() != 0;
695}
696
697
698
699