Skip to content

Commit ca95735

Browse files
lgritz1div0
authored andcommitted
fix(oiiotool): Overhaul and fix bugs in mixed-channel propogation (AcademySoftwareFoundation#4127)
Remember that ImageBuf only stores one pixel data type per buffer, but of course they may have differing-per-channel types in image files. So how does oiiotool know what to write? There is a series of heuristics: -d is for explicit user control, if not, then try to deduce it from the input files. A bedrock precept is that `oiiotool in.exr -o out.exr` ought to write the same channel types that it read in. The logic to implement this with the various image operations and possible overrides can be hard to get right. Indeed, I did not get it right. It made mistakes even in simple cases like the above. And the code was complicated and hard to understand, as well. This PR rewrites a couple functions that implement this chunk of this logic, both correcting several errors as well as (I hope) being clearer and better commented so that we can more easily reason about it and modify it in the future. I also added some additional tests to testsuite/perchannel to verify some of the cases that were previously failing. Signed-off-by: Larry Gritz <[email protected]> Signed-off-by: Peter Kovář <[email protected]>
1 parent 35b2f1c commit ca95735

File tree

3 files changed

+91
-51
lines changed

3 files changed

+91
-51
lines changed

src/oiiotool/oiiotool.cpp

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ Oiiotool::remember_input_channelformats(ImageRecRef img)
443443
if (input_channelformats[subchname] == "")
444444
input_channelformats[subchname] = chtypename;
445445
} else {
446-
if (input_channelformats[chname] != "")
446+
if (input_channelformats[chname] == "")
447447
input_channelformats[chname] = chtypename;
448448
}
449449
}
@@ -764,39 +764,23 @@ set_output_dataformat(ImageSpec& spec, TypeDesc format,
764764
std::map<std::string, std::string>& channelformats,
765765
int bitdepth)
766766
{
767-
// Account for default requested format
768767
if (format != TypeUnknown)
769768
spec.format = format;
770-
if (bitdepth)
771-
spec.attribute("oiio:BitsPerSample", bitdepth);
772-
else
773-
spec.erase_attribute("oiio:BitsPerSample");
774-
775-
// See if there's a recommended format for this subimage
776-
std::string subimagename = spec["oiio:subimagename"];
777-
if (format == TypeUnknown && subimagename.size()) {
778-
auto key = Strutil::fmt::format("{}.*", subimagename);
779-
if (channelformats[key] != "")
780-
spec.format = TypeDesc(channelformats[key]);
781-
}
782-
783-
// Honor any per-channel requests
784-
if (channelformats.size()) {
785-
spec.channelformats.clear();
786-
spec.channelformats.resize(spec.nchannels, spec.format);
769+
spec.channelformats.resize(spec.nchannels, spec.format);
770+
if (!channelformats.empty()) {
771+
std::string subimagename = spec["oiio:subimagename"];
787772
for (int c = 0; c < spec.nchannels; ++c) {
788773
std::string chname = spec.channel_name(c);
789774
auto subchname = Strutil::fmt::format("{}.{}", subimagename,
790775
chname);
791-
if (channelformats[subchname] != "" && subimagename.size())
792-
spec.channelformats[c] = TypeDesc(channelformats[subchname]);
776+
TypeDesc chtype = spec.channelformat(c);
777+
if (subimagename.size() && channelformats[subchname] != "")
778+
chtype = TypeDesc(channelformats[subchname]);
793779
else if (channelformats[chname] != "")
794-
spec.channelformats[c] = TypeDesc(channelformats[chname]);
795-
else
796-
spec.channelformats[c] = spec.format;
780+
chtype = TypeDesc(channelformats[chname]);
781+
if (chtype != TypeUnknown)
782+
spec.channelformats[c] = chtype;
797783
}
798-
} else {
799-
spec.channelformats.clear();
800784
}
801785

802786
// Eliminate the per-channel formats if they are all the same.
@@ -809,6 +793,11 @@ set_output_dataformat(ImageSpec& spec, TypeDesc format,
809793
spec.channelformats.clear();
810794
}
811795
}
796+
797+
if (bitdepth)
798+
spec.attribute("oiio:BitsPerSample", bitdepth);
799+
else
800+
spec.erase_attribute("oiio:BitsPerSample");
812801
}
813802

814803

@@ -835,37 +824,71 @@ adjust_output_options(string_view filename, ImageSpec& spec,
835824
// format as the input, or else she would have said so).
836825
// * Otherwise, just write the buffer's format, regardless of how it got
837826
// that way.
838-
TypeDesc requested_output_dataformat = ot.output_dataformat;
839-
auto requested_output_channelformats = ot.output_channelformats;
827+
828+
// spec is what we're going to use for output.
829+
830+
// Accumulating results here
831+
TypeDesc requested_output_dataformat;
832+
std::map<std::string, std::string> requested_channelformats;
833+
int requested_output_bits = 0;
834+
835+
if (was_direct_read && nativespec) {
836+
// If the image we're outputting is an unmodified direct read of a
837+
// file, assume that we'll default to outputting the same channel
838+
// formats it started in.
839+
requested_output_dataformat = nativespec->format;
840+
for (int c = 0; c < nativespec->nchannels; ++c) {
841+
requested_channelformats[nativespec->channel_name(c)]
842+
= nativespec->channelformat(c).c_str();
843+
}
844+
requested_output_bits = nativespec->get_int_attribute(
845+
"oiio:BitsPerSample");
846+
} else if (ot.input_dataformat != TypeUnknown) {
847+
// If the image we're outputting is a computed or modified image, not
848+
// a direct read, then assume that the FIRST image we read in provides
849+
// a template for the output we want (if we ever read an image).
850+
requested_output_dataformat = ot.input_dataformat;
851+
requested_channelformats = ot.input_channelformats;
852+
requested_output_bits = ot.input_bitspersample;
853+
}
854+
855+
// Any "global" format requests set by -d override the above.
856+
if (ot.output_dataformat != TypeUnknown) {
857+
// `-d type` clears the board and imposes the request
858+
requested_output_dataformat = ot.output_dataformat;
859+
requested_channelformats.clear();
860+
spec.channelformats.clear();
861+
if (ot.output_bitspersample)
862+
requested_output_bits = ot.output_bitspersample;
863+
}
864+
if (!ot.output_channelformats.empty()) {
865+
// `-d chan=type` overrides the format for a specific channel
866+
for (auto&& c : ot.output_channelformats)
867+
requested_channelformats[c.first] = c.second;
868+
}
869+
870+
// Any override options on the -o command itself take precedence over
871+
// everything else.
840872
if (fileoptions.contains("type")) {
841873
requested_output_dataformat.fromstring(fileoptions.get_string("type"));
842-
requested_output_channelformats.clear();
874+
requested_channelformats.clear();
875+
spec.channelformats.clear();
843876
} else if (fileoptions.contains("datatype")) {
844877
requested_output_dataformat.fromstring(
845878
fileoptions.get_string("datatype"));
846-
requested_output_channelformats.clear();
847-
}
848-
int requested_output_bits = fileoptions.get_int("bits",
849-
ot.output_bitspersample);
850-
851-
if (requested_output_dataformat != TypeUnknown) {
852-
// Requested an explicit override of datatype
853-
set_output_dataformat(spec, requested_output_dataformat,
854-
requested_output_channelformats,
855-
requested_output_bits);
856-
} else if (was_direct_read && nativespec) {
857-
// Do nothing -- use the file's native data format
858-
spec.channelformats = nativespec->channelformats;
859-
set_output_dataformat(spec, nativespec->format,
860-
requested_output_channelformats,
861-
(*nativespec)["oiio:BitsPerSample"].get<int>());
862-
} else if (ot.input_dataformat != TypeUnknown) {
863-
auto mergedlist = ot.input_channelformats;
864-
for (auto& c : requested_output_channelformats)
865-
mergedlist[c.first] = c.second;
866-
set_output_dataformat(spec, ot.input_dataformat, mergedlist,
867-
ot.input_bitspersample);
879+
requested_channelformats.clear();
880+
spec.channelformats.clear();
868881
}
882+
requested_output_bits = fileoptions.get_int("bits", requested_output_bits);
883+
884+
// At this point, the trio of "requested" variable reflect any global or
885+
// command requests to override the logic of what was found in the input
886+
// files.
887+
888+
// Set the types in the spec
889+
set_output_dataformat(spec, requested_output_dataformat,
890+
requested_channelformats, requested_output_bits);
891+
869892

870893
// Tiling strategy:
871894
// * If a specific request was made for tiled or scanline output, honor

testsuite/perchannel/ref/out.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ Comparing "../openexr-images/Tiles/Spirals.exr" and "spiral1.exr"
66
PASS
77
Comparing "../openexr-images/Tiles/Spirals.exr" and "spiral2.exr"
88
PASS
9+
hfhf.exr : 64 x 64, 4 channel, half/float/half/float openexr
10+
SHA-1: 6FECD5769C727E137B7580AE3B1823B06EE6F9D9
11+
hfhf_copy.exr : 64 x 64, 4 channel, half/float/half/float openexr
12+
SHA-1: 6FECD5769C727E137B7580AE3B1823B06EE6F9D9
13+
hfhf_mod.exr : 64 x 64, 4 channel, half/float/half/float openexr
14+
SHA-1: 6FECD5769C727E137B7580AE3B1823B06EE6F9D9

testsuite/perchannel/run.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@
3333
command += (oiio_app("oiiotool") + image + " -o spiral2.exr ;\n")
3434
command += diff_command (image, "spiral2.exr")
3535

36+
# Create a file with per-channel data types, make sure that works
37+
command += oiiotool("--create 64x64 4 --d R=half,G=float,B=half,A=float -o hfhf.exr")
38+
command += info_command("hfhf.exr", verbose=False)
39+
40+
# Make sure that read/write unmodified will preserve the channel types
41+
command += oiiotool("hfhf.exr -o hfhf_copy.exr")
42+
command += info_command("hfhf_copy.exr", verbose=False)
43+
44+
# Make sure that read/modify/write preserves the channel types of the input
45+
command += oiiotool("hfhf.exr -mulc 0.5,0.5,0.5,0.5 -o hfhf_mod.exr")
46+
command += info_command("hfhf_mod.exr", verbose=False)
3647

3748
#print "Running this command:\n" + command + "\n"
3849

0 commit comments

Comments
 (0)