Skip to content

Commit e53ac26

Browse files
committed
support arbitrary channel names in exr
1 parent 0c156a7 commit e53ac26

7 files changed

Lines changed: 148 additions & 73 deletions

File tree

Core/imageio.cpp

Lines changed: 82 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,41 @@ struct ExrChannelLayout {
5959
int idxG = -1;
6060
int idxB = -1;
6161
int idxA = -1;
62-
int idxY = -1;
62+
63+
std::map<std::string, int> customChannels = {};
6364

6465
int CountChannels() const {
6566
return
6667
(idxR >= 0 ? 1 : 0) +
6768
(idxG >= 0 ? 1 : 0) +
6869
(idxB >= 0 ? 1 : 0) +
6970
(idxA >= 0 ? 1 : 0) +
70-
(idxY >= 0 ? 1 : 0);
71+
customChannels.size();
72+
}
73+
74+
std::string NameOf(int idx) const {
75+
int rgbaOffset =
76+
(idxR >= 0 ? 1 : 0) +
77+
(idxG >= 0 ? 1 : 0) +
78+
(idxB >= 0 ? 1 : 0) +
79+
(idxA >= 0 ? 1 : 0);
80+
81+
if (idx < rgbaOffset) {
82+
char* fixed = (char*)alloca(4);
83+
int offset = 0;
84+
if (idxR >= 0) fixed[offset++] = 'R';
85+
if (idxG >= 0) fixed[offset++] = 'G';
86+
if (idxB >= 0) fixed[offset++] = 'B';
87+
if (idxA >= 0) fixed[offset++] = 'A';
88+
89+
return std::string { fixed[idx] };
90+
}
91+
92+
int i = idx - rgbaOffset;
93+
auto it = customChannels.cbegin();
94+
for (; it != customChannels.cend(); ++it)
95+
if (i-- == 0) break;
96+
return it->first;
7197
}
7298
};
7399

@@ -156,48 +182,43 @@ int CacheExrImage(const char* filename) {
156182
}
157183

158184
// Read the number of channels in each layer
159-
int freeChannels = 0;
160185
for (int chan = 0; chan < result.header.num_channels; ++chan) {
161-
// Extract the layer name by assuming a channel name of the form "layername.R", "layername.B" and so on
162-
size_t len = strlen(result.header.channels[chan].name);
163-
164-
std::string layerName;
165-
if (len <= 2) // Plain channels without a layer will be merged into an unnamed default layer (e.g., "R", "G", and "B")
186+
// Extract the layer name by assuming a channel name of the form "layername.channelname"
187+
std::string name = result.header.channels[chan].name;
188+
size_t len = name.size();
189+
190+
size_t idxSep = name.rfind('.');
191+
std::string layerName, chanName;
192+
if (idxSep == name.npos) {
193+
// Plain channels without a layer will be merged into an unnamed default layer (e.g., "R", "G", and "B")
166194
layerName = "";
167-
else if (result.header.channels[chan].name[len - 2] != '.') // Keep the full name as the layer name
168-
layerName = std::string(result.header.channels[chan].name, len);
169-
else // Remove channel name from the layer name (e.g., .R / .G / .B)
170-
layerName = std::string(result.header.channels[chan].name, len - 2);
171-
172-
char chanName = result.header.channels[chan].name[len - 1];
195+
chanName = name;
196+
} else {
197+
// Remove channel name from the layer name
198+
layerName = name.substr(0, idxSep);
199+
chanName = name.substr(idxSep + 1);
200+
}
173201

174202
// Update the channel layout info
175203
auto iter = result.channelsPerLayer.find(layerName);
176204
if (iter == result.channelsPerLayer.end()) {
177205
result.channelsPerLayer[layerName] = ExrChannelLayout();
178206
result.layerNames.emplace_back(layerName);
179207
}
180-
181208
auto& layout = result.channelsPerLayer[layerName];
182209

183-
switch (chanName) {
184-
case 'R':
210+
// Handle known channel names R G B and A so we can reorder them
211+
// All other channel names will be alphabethically sorted
212+
if (chanName == "R")
185213
layout.idxR = chan;
186-
break;
187-
case 'G':
214+
else if (chanName == "G")
188215
layout.idxG = chan;
189-
break;
190-
case 'B':
216+
else if (chanName == "B")
191217
layout.idxB = chan;
192-
break;
193-
case 'A':
218+
else if (chanName == "A")
194219
layout.idxA = chan;
195-
break;
196-
case 'Y':
197-
default:
198-
layout.idxY = chan;
199-
break;
200-
}
220+
else if (!layout.customChannels.emplace(chanName, chan).second)
221+
std::cerr << "Duplicate channel '" << chanName << "' in layer '" << layerName << "' ignored." << std::endl;
201222
}
202223

203224
cacheMutex.lock();
@@ -234,44 +255,24 @@ bool CopyCachedExrLayer(int id, std::string layerName, float* out) {
234255
int numChannels = layerInfo.CountChannels();
235256

236257
auto swizzle = [numChannels, &layerInfo, &layerName](unsigned char** images, int srcIdx, int dstIdx, float* out) {
237-
if (numChannels == 1) { // Y
238-
assert(layerInfo.idxY >= 0);
239-
auto chanImg = (float*)images[layerInfo.idxY];
240-
out[dstIdx + 0] = chanImg[srcIdx];
241-
} else if (numChannels == 3) { // RGB
242-
assert(layerInfo.idxR >= 0);
243-
auto chanImg = (float*)images[layerInfo.idxR];
244-
out[dstIdx + 0] = chanImg[srcIdx];
245-
246-
assert(layerInfo.idxG >= 0);
247-
chanImg = (float*)images[layerInfo.idxG];
248-
out[dstIdx + 1] = chanImg[srcIdx];
249-
250-
assert(layerInfo.idxB >= 0);
251-
chanImg = (float*)images[layerInfo.idxB];
252-
out[dstIdx + 2] = chanImg[srcIdx];
253-
} else if (numChannels == 4) { // RGBA
254-
assert(layerInfo.idxR >= 0);
255-
auto chanImg = (float*)images[layerInfo.idxR];
256-
out[dstIdx + 0] = chanImg[srcIdx];
257-
258-
assert(layerInfo.idxG >= 0);
259-
chanImg = (float*)images[layerInfo.idxG];
260-
out[dstIdx + 1] = chanImg[srcIdx];
261-
262-
assert(layerInfo.idxB >= 0);
263-
chanImg = (float*)images[layerInfo.idxB];
264-
out[dstIdx + 2] = chanImg[srcIdx];
265-
266-
assert(layerInfo.idxA >= 0);
267-
chanImg = (float*)images[layerInfo.idxA];
268-
out[dstIdx + 3] = chanImg[srcIdx];
269-
} else {
270-
std::cerr << "ERROR while reading .exr layer " << layerName << ": Images with "
271-
<< numChannels << " channels are currently not supported. " << std::endl;
272-
cacheMutex.unlock();
273-
return false;
274-
}
258+
int offset = 0;
259+
auto add = [&](int idx) {
260+
auto chanImg = (float*)images[idx];
261+
out[dstIdx + offset] = chanImg[srcIdx];
262+
offset++;
263+
};
264+
265+
if (layerInfo.idxR >= 0)
266+
add(layerInfo.idxR);
267+
if (layerInfo.idxG >= 0)
268+
add(layerInfo.idxG);
269+
if (layerInfo.idxB >= 0)
270+
add(layerInfo.idxB);
271+
if (layerInfo.idxA >= 0)
272+
add(layerInfo.idxA);
273+
for (auto it = layerInfo.customChannels.begin(); it != layerInfo.customChannels.end(); ++it)
274+
add(it->second);
275+
275276
return true;
276277
};
277278

@@ -896,6 +897,20 @@ SIIO_API void GetExrLayerName(int id, int layerIdx, char* out) {
896897
cacheMutex.unlock();
897898
}
898899

900+
SIIO_API int GetExrChannelNameLen(int id, const char* layerName, int channelIdx) {
901+
cacheMutex.lock();
902+
const auto& chanName = exrImages[id].channelsPerLayer[layerName].NameOf(channelIdx);
903+
cacheMutex.unlock();
904+
return chanName.size();
905+
}
906+
907+
SIIO_API void GetExrChannelName(int id, const char* layerName, int channelIdx, char* out) {
908+
cacheMutex.lock();
909+
const auto& chanName = exrImages[id].channelsPerLayer[layerName].NameOf(channelIdx);
910+
strcpy(out, chanName.c_str());
911+
cacheMutex.unlock();
912+
}
913+
899914
SIIO_API bool CopyCachedLayer(int id, const char* name, float* out) {
900915
return CopyCachedExrLayer(id, name, out);
901916
}
@@ -960,7 +975,6 @@ SIIO_API int GetExrLayerNames(const char* filename, char*** names) {
960975
}
961976

962977
// Read the number of channels in each layer
963-
int freeChannels = 0;
964978
std::unordered_set<std::string> layerNames;
965979
for (int chan = 0; chan < header.num_channels; ++chan) {
966980
// Extract the layer name by assuming a channel name of the form "layername.R", "layername.B" and so on

PyTest/test_io.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,21 @@ def test_write_layers_read_all(self):
204204
], dtype=np.float32)
205205

206206
sio.write_layered_exr("layered.exr", {"": img, "albedo": other})
207-
layers = sio.read_layered_exr("layered.exr")
207+
channel_names = {}
208+
layers = sio.read_layered_exr("layered.exr", channel_names)
208209

209210
self.assertTrue("" in layers)
210211
self.assertTrue("albedo" in layers)
211212

213+
self.assertEqual(len(channel_names[""]), 3)
214+
self.assertEqual(len(channel_names["albedo"]), 3)
215+
self.assertEqual(channel_names["albedo"][0], "R")
216+
self.assertEqual(channel_names["albedo"][1], "G")
217+
self.assertEqual(channel_names["albedo"][2], "B")
218+
self.assertEqual(channel_names[""][0], "R")
219+
self.assertEqual(channel_names[""][1], "G")
220+
self.assertEqual(channel_names[""][2], "B")
221+
212222
self.assertEqual(other[0,0,0], layers["albedo"][0,0,0])
213223
self.assertEqual(other[0,0,1], layers["albedo"][0,0,1])
214224
self.assertEqual(other[0,0,2], layers["albedo"][0,0,2])
@@ -248,4 +258,4 @@ def test_write_layers_read_all(self):
248258
os.remove("layered.exr")
249259

250260
if __name__ == "__main__":
251-
unittest.main()
261+
unittest.main()

PyWrapper/simpleimageio/image.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
_get_layer_name.argtypes = [c_int, c_int, POINTER(c_char)]
4646
_get_layer_name.restype = None
4747

48+
_get_channel_name_len = corelib.core.GetExrChannelNameLen
49+
_get_channel_name_len.argtypes = [c_int, c_char_p, c_int]
50+
_get_channel_name_len.restype = c_int
51+
52+
_get_channel_name = corelib.core.GetExrChannelName
53+
_get_channel_name.argtypes = [c_int, c_char_p, c_int, POINTER(c_char)]
54+
_get_channel_name.restype = None
55+
4856
_copy_layer = corelib.core.CopyCachedLayer
4957
_copy_layer.argtypes = [c_int, c_char_p, POINTER(c_float)]
5058
_copy_layer.restype = None
@@ -69,7 +77,14 @@ def read(filename: str):
6977

7078
return buffer
7179

72-
def read_layered_exr(filename: str):
80+
def read_layered_exr(filename: str, layout = None):
81+
'''
82+
Reads all layers from an .exr file
83+
84+
Arguments:
85+
filename -- the file to load
86+
layout -- optional dictionary that will be filled with the per-layer channel name configurations
87+
'''
7388
w = c_int()
7489
h = c_int()
7590
c = c_int()
@@ -92,6 +107,16 @@ def read_layered_exr(filename: str):
92107

93108
_copy_layer(idx, name, buffer.ctypes.data_as(POINTER(c_float)))
94109

110+
# Read the channel names
111+
if layout is not None:
112+
channel_names = []
113+
for c in range(num_chans):
114+
nc = _get_channel_name_len(idx, name, c)
115+
cstr_buf = create_string_buffer(nc + 1)
116+
_get_channel_name(idx, name, c, cstr_buf);
117+
channel_names.append(cstr_buf.value.decode('utf-8'))
118+
layout[name.decode('utf-8')] = channel_names
119+
95120
layers[name.decode('utf-8')] = buffer
96121

97122
_delete_image(idx)
@@ -142,4 +167,4 @@ def base64_jpg(img, quality = 80):
142167
mem = corelib.invoke(_write_to_mem, img, ".jpg".encode('utf-8'), quality, byref(numbytes))
143168
b64 = base64.b64encode(bytearray(mem[:numbytes.value]))
144169
_free_mem(mem)
145-
return b64
170+
return b64

SimpleImageIO.Integration/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626

2727
if (args.Contains("falsecolor") || args.Length == 0) {
2828
TestFalseColor.TestGradient();
29-
}
29+
}

SimpleImageIO/Image.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ public unsafe class Image : IDisposable {
2323
/// </summary>
2424
public int NumChannels { get; protected set; }
2525

26+
/// <summary>
27+
/// Names of each channel in this image (e.g., "R" for the red color channel)
28+
/// If null or empty, then a default interpretation (RGBA order) is assumed
29+
/// </summary>
30+
public string[] ChannelNames { get; protected set; }
31+
2632
/// <summary>
2733
/// Pointer to the native memory containing the image data
2834
/// </summary>
@@ -77,6 +83,7 @@ public Image(int w, int h, int numChannels) {
7783
Width = w;
7884
Height = h;
7985
this.NumChannels = numChannels;
86+
this.ChannelNames = new string[numChannels];
8087
Alloc();
8188

8289
// Zero out the values to avoid undefined contents
@@ -140,6 +147,7 @@ public static void Move(Image src, Image dest) {
140147
dest.Width = src.Width;
141148
dest.Height = src.Height;
142149
dest.NumChannels = src.NumChannels;
150+
dest.ChannelNames = src.ChannelNames;
143151
}
144152

145153
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -366,6 +374,7 @@ protected void LoadFromFile(string filename) {
366374
Width = w;
367375
Height = h;
368376
NumChannels = n;
377+
ChannelNames = new string[n];
369378
if (id < 0 || Width <= 0 || Height <= 0)
370379
throw new IOException($"ERROR: Could not load image file '{filename}'");
371380

@@ -441,6 +450,7 @@ protected void Zoom(Image other, int scale) {
441450
Width = other.Width * scale;
442451
Height = other.Height * scale;
443452
NumChannels = other.NumChannels;
453+
ChannelNames = other.ChannelNames;
444454
Alloc();
445455

446456
SimpleImageIOCore.ZoomWithNearestInterp(other.DataPointer, NumChannels * other.Width, DataPointer,
@@ -468,6 +478,7 @@ public static bool Matches(Image a, Image b)
468478
result.Width = a.Width;
469479
result.Height = a.Height;
470480
result.NumChannels = a.NumChannels;
481+
result.ChannelNames = a.ChannelNames;
471482
result.Alloc();
472483
Parallel.For(0, a.Height, row => {
473484
for (int col = 0; col < a.Width; ++col) {
@@ -492,6 +503,7 @@ public static bool Matches(Image a, Image b)
492503
result.Width = a.Width;
493504
result.Height = a.Height;
494505
result.NumChannels = a.NumChannels;
506+
result.ChannelNames = a.ChannelNames;
495507
result.Alloc();
496508
Parallel.For(0, a.Height, row => {
497509
for (int col = 0; col < a.Width; ++col) {

SimpleImageIO/Layers.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ public static Dictionary<string, Image> LoadFromFile(string filename) {
4141
else
4242
layers[name] = new(width, height, numChans);
4343

44+
// Read the channel names
45+
for (int c = 0; c < numChans; ++c) {
46+
int clen = SimpleImageIOCore.GetExrChannelNameLen(id, name, c);
47+
StringBuilder chanNameBuilder = new(clen);
48+
SimpleImageIOCore.GetExrChannelName(id, name, c, chanNameBuilder);
49+
layers[name].ChannelNames[c] = chanNameBuilder.ToString();
50+
}
51+
4452
SimpleImageIOCore.CopyCachedLayer(id, name, layers[name].DataPointer);
4553
}
4654

SimpleImageIO/SimpleImageIOCore.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public static extern int CacheImage(out int width, out int height, out int numCh
1616
[DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
1717
public static extern int GetExrLayerChannelCount(int id, string name);
1818

19+
[DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
20+
public static extern int GetExrChannelNameLen(int id, string layerName, int channelIdx);
21+
22+
[DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
23+
public static extern int GetExrChannelName(int id, string layerName, int channelIdx, StringBuilder name);
24+
1925
[DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
2026
public static extern int GetExrLayerNameLen(int id, int layerIdx);
2127

0 commit comments

Comments
 (0)