Add support for OpenImageIO.
This commit is contained in:
parent
5b2a22b2c0
commit
a38138582e
5 changed files with 450 additions and 36 deletions
308
libs/oiio/oiio.cpp
Normal file
308
libs/oiio/oiio.cpp
Normal file
|
@ -0,0 +1,308 @@
|
|||
|
||||
enum ImageFormat {
|
||||
FORMAT_UNKNOWN = 0,
|
||||
FORMAT_BYTE, // 8-bit unsigned
|
||||
FORMAT_SHORT, // 16-bit unsigned
|
||||
FORMAT_HALF, // 16-bit float
|
||||
FORMAT_FLOAT, // 32-bit float
|
||||
MAX_ENUM = 0x7FFFFFFF
|
||||
};
|
||||
|
||||
// image_decoder.cpp
|
||||
#include <OpenImageIO/imageio.h>
|
||||
#include <OpenImageIO/filesystem.h>
|
||||
#include <OpenImageIO/imagebufalgo.h>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
using namespace OIIO;
|
||||
|
||||
static ImageFormat type_to_format(TypeDesc type) {
|
||||
switch (type.basetype) {
|
||||
case TypeDesc::UINT8: return FORMAT_BYTE;
|
||||
case TypeDesc::UINT16: return FORMAT_SHORT;
|
||||
case TypeDesc::HALF: return FORMAT_HALF;
|
||||
case TypeDesc::FLOAT: return FORMAT_FLOAT;
|
||||
default: return FORMAT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static const char* type_to_string(TypeDesc type) {
|
||||
switch (type.basetype) {
|
||||
case TypeDesc::UINT8: return "FORMAT_BYTE";
|
||||
case TypeDesc::UINT16: return "FORMAT_SHORT";
|
||||
case TypeDesc::HALF: return "FORMAT_HALF";
|
||||
case TypeDesc::FLOAT: return "FORMAT_FLOAT";
|
||||
default: return "FORMAT_UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static TypeDesc format_to_type(enum ImageFormat format) {
|
||||
switch (format) {
|
||||
case FORMAT_BYTE: return TypeDesc::UINT8; // 8-bit unsigned
|
||||
case FORMAT_SHORT: return TypeDesc::UINT16; // 16-bit unsigned
|
||||
case FORMAT_HALF: return TypeDesc::HALF; // 16-bit float
|
||||
case FORMAT_FLOAT: return TypeDesc::FLOAT; // 32-bit float
|
||||
default: return TypeDesc::UNKNOWN; // Unknown format
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int oiio_image_decode(const uint8_t* input, size_t input_len,
|
||||
uint8_t* output, size_t output_len,
|
||||
int32_t desired_channels, ImageFormat format, char* is_BGR, const char* file_name) {
|
||||
if (!input || !output || desired_channels < 1 || desired_channels > 4) {
|
||||
fprintf(stderr, "Invalid input parameters\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t* decoded = output;
|
||||
bool free_decoded = false;
|
||||
|
||||
try {
|
||||
auto mem_reader = Filesystem::IOMemReader(input, input_len);
|
||||
|
||||
auto in = ImageInput::open(file_name, nullptr, &mem_reader);
|
||||
if (!in) {
|
||||
fprintf(stderr, "Open failed: %s\n", geterror().c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImageSpec& spec = in->spec();
|
||||
*is_BGR = spec.channelnames[0] == "B"? 1:0;
|
||||
const size_t required = spec.width * spec.height * desired_channels;
|
||||
const TypeDesc ftype = format_to_type(format);
|
||||
if (output_len != required * ftype.size()) {
|
||||
fprintf(stderr, "Buffer size mismatch: Needed %zu, got %u\n",
|
||||
required * ftype.size(), output_len);
|
||||
in->close();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(spec.nchannels != desired_channels){
|
||||
// have a separate buffer for the decoded image, free later
|
||||
decoded = (uint8_t*)malloc(spec.image_bytes());
|
||||
free_decoded = true;
|
||||
}
|
||||
|
||||
if (!in->read_image(0, 0, 0, spec.nchannels, ftype, decoded)) {
|
||||
fprintf(stderr, "Read error: %s\n", in->geterror().c_str());
|
||||
in->close();
|
||||
if(free_decoded) free(decoded);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(spec.nchannels < desired_channels){
|
||||
// expand channels
|
||||
ImageSpec src_spec(spec.width, spec.height, spec.nchannels, ftype);
|
||||
ImageBuf src_buf(src_spec, decoded);
|
||||
ImageSpec dst_spec(spec.width, spec.height, desired_channels, ftype);
|
||||
ImageBuf dst_buf(dst_spec, output);
|
||||
|
||||
const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
// -1 below fills with the values above
|
||||
std::array<int, 4> channel_order = {-1, -1, -1, -1};
|
||||
for(int i=0; i<spec.nchannels; i++){
|
||||
channel_order[i] = i;
|
||||
}
|
||||
if(spec.nchannels == 1 && desired_channels >= 3){
|
||||
// single channel exception: make it greyscale RGB/RGBA
|
||||
channel_order = {0,0,0,-1};
|
||||
}
|
||||
|
||||
if (!ImageBufAlgo::channels(dst_buf, src_buf,
|
||||
desired_channels,
|
||||
channel_order, channel_values)) {
|
||||
fprintf(stderr, "Channel conversion failed: %s\n", dst_buf.geterror().c_str());
|
||||
in->close();
|
||||
if(free_decoded) free(decoded);
|
||||
return 0;
|
||||
}
|
||||
uint8_t* pixels = (uint8_t*)dst_buf.localpixels();
|
||||
if(pixels != output){
|
||||
// it turns out channels() always resets the buffer
|
||||
// so it's no longer wrapping output
|
||||
memcpy(output, pixels, dst_spec.image_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
in->close();
|
||||
if(free_decoded) free(decoded);
|
||||
return 1;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
fprintf(stderr, "Exception: %s\n", e.what());
|
||||
if(free_decoded) free(decoded);
|
||||
return 0;
|
||||
}
|
||||
catch (...) {
|
||||
fprintf(stderr, "Unknown exception occurred\n");
|
||||
if(free_decoded) free(decoded);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int oiio_image_get_attributes(const uint8_t* input, size_t input_len,
|
||||
int32_t* width, int32_t* height, int32_t* channels,
|
||||
enum ImageFormat* format, const char* file_name) {
|
||||
if (!input) {
|
||||
fprintf(stderr, "Null input buffer\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
auto mem_reader = Filesystem::IOMemReader(input, input_len);
|
||||
|
||||
auto in = ImageInput::open(file_name, nullptr, &mem_reader);
|
||||
if (!in) {
|
||||
fprintf(stderr, "Open failed: %s\n", geterror().c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImageSpec& spec = in->spec();
|
||||
if (width) *width = spec.width;
|
||||
if (height) *height = spec.height;
|
||||
if (channels) *channels = spec.nchannels;
|
||||
if (format) *format = type_to_format(spec.format);
|
||||
|
||||
in->close();
|
||||
return 1;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
fprintf(stderr, "Exception: %s\n", e.what());
|
||||
return 0;
|
||||
}
|
||||
catch (...) {
|
||||
fprintf(stderr, "Unknown exception occurred\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
|
||||
|
||||
// // image_to_ppm.c
|
||||
// #include <stdio.h>
|
||||
// #include <stdlib.h>
|
||||
|
||||
// int main(int argc, char** argv) {
|
||||
// if (argc != 2) {
|
||||
// fprintf(stderr, "Usage: %s <image-file>\n", argv[0]);
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// // Read input file
|
||||
// FILE* file = fopen(argv[1], "rb");
|
||||
// if (!file) {
|
||||
// perror("Failed to open file");
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// fseek(file, 0, SEEK_END);
|
||||
// long file_size = ftell(file);
|
||||
// fseek(file, 0, SEEK_SET);
|
||||
|
||||
// uint8_t* input = (uint8_t*)malloc(file_size);
|
||||
// if (!input) {
|
||||
// fclose(file);
|
||||
// fprintf(stderr, "Memory allocation failed\n");
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// if (fread(input, 1, file_size, file) != file_size) {
|
||||
// fclose(file);
|
||||
// free(input);
|
||||
// fprintf(stderr, "File read error\n");
|
||||
// return 1;
|
||||
// }
|
||||
// fclose(file);
|
||||
|
||||
// // Get image attributes
|
||||
// int width, height, channels;
|
||||
// enum ImageFormat format;
|
||||
// if (!image_get_attributes(input, file_size, &width, &height, &channels, &format)) {
|
||||
// free(input);
|
||||
// fprintf(stderr, "Unsupported image format\n");
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// // Allocate output buffer for 3-channel RGB
|
||||
// size_t output_size = width * height * 3;
|
||||
// uint8_t* pixels = (uint8_t*)malloc(output_size);
|
||||
// if (!pixels) {
|
||||
// free(input);
|
||||
// fprintf(stderr, "Output buffer allocation failed\n");
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// // Decode to 3 channels (RGB)
|
||||
// if (!image_decode(input, file_size, pixels, output_size, 3, format)) {
|
||||
// free(input);
|
||||
// free(pixels);
|
||||
// fprintf(stderr, "Image decoding failed\n");
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// free(input);
|
||||
|
||||
// // Output PPM header
|
||||
// printf("P6\n%d %d\n255\n", width, height);
|
||||
|
||||
// // Output raw pixel data
|
||||
// fwrite(pixels, 1, output_size, stdout);
|
||||
|
||||
// free(pixels);
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
bool convertRGBToRGBA(const std::string& filename,
|
||||
void* output_buffer,
|
||||
size_t buffer_length,
|
||||
std::string& error_msg) {
|
||||
// Load source image
|
||||
ImageBuf src_buf(filename);
|
||||
if (!src_buf.read()) {
|
||||
error_msg = "Failed to load image: " + src_buf.geterror();
|
||||
return false;
|
||||
}
|
||||
|
||||
const ImageSpec& src_spec = src_buf.spec();
|
||||
|
||||
// Verify source is RGB
|
||||
if (src_spec.nchannels != 3) {
|
||||
error_msg = "Image is not RGB (has " + std::to_string(src_spec.nchannels) + " channels)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate required buffer size
|
||||
TypeDesc data_type = src_spec.format;
|
||||
const size_t required_size = src_spec.width * src_spec.height * 4 * data_type.size();
|
||||
if (buffer_length < required_size) {
|
||||
error_msg = "Buffer too small. Required: " + std::to_string(required_size) +
|
||||
", provided: " + std::to_string(buffer_length);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare destination buffer wrapped in ImageBuf
|
||||
ImageSpec dst_spec(src_spec.width, src_spec.height, 4, data_type);
|
||||
ImageBuf dst_buf(dst_spec, output_buffer);
|
||||
|
||||
// Set up channel remapping with alpha=1.0
|
||||
const int channel_order[] = {0, 1, 2, -1}; // Source RGB, new alpha
|
||||
const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
|
||||
// Perform channel conversion
|
||||
if (!ImageBufAlgo::channels(dst_buf, src_buf,
|
||||
/* channel count */ 4,
|
||||
channel_order, channel_values)) {
|
||||
error_msg = "Channel conversion failed: " + dst_buf.geterror();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
75
libs/oiio/oiio.nim
Normal file
75
libs/oiio/oiio.nim
Normal file
|
@ -0,0 +1,75 @@
|
|||
|
||||
import std/strformat
|
||||
|
||||
{.compile("oiio.cpp","-std=c++14").}
|
||||
{.passL: "-lOpenImageIO -lOpenImageIO_Util".}
|
||||
|
||||
type OiioImageFormat* {.size: 4.} = enum
|
||||
FORMAT_UNKNOWN = 0
|
||||
FORMAT_BYTE # 8-bit unsigned
|
||||
FORMAT_SHORT # 16-bit unsigned
|
||||
FORMAT_HALF # 16-bit float
|
||||
FORMAT_FLOAT # 32-bit float
|
||||
|
||||
template size*(format: OiioImageFormat): int =
|
||||
case format:
|
||||
of FORMAT_BYTE: 1
|
||||
of FORMAT_SHORT: 2
|
||||
of FORMAT_HALF: 2
|
||||
of FORMAT_FLOAT: 4
|
||||
else: 0
|
||||
|
||||
proc decode(input: pointer, input_len: int,
|
||||
output: pointer, output_len: int,
|
||||
desired_channels: int32, format: OiioImageFormat, is_BGR: var byte,
|
||||
file_name: cstring): int32 {.importc:"oiio_image_decode", cdecl.}
|
||||
|
||||
proc get_attributes(input: pointer, input_len: int,
|
||||
width: var int32, height: var int32,
|
||||
channels: var int32, format: var OiioImageFormat,
|
||||
file_name: cstring): int32 {.importc:"oiio_image_get_attributes", cdecl.}
|
||||
|
||||
template has_bytes(p: pointer, bytes: string): bool =
|
||||
cmpMem(p, bytes.cstring, bytes.len) == 0
|
||||
|
||||
proc file_name_from_magic(input: openArray[byte]): string =
|
||||
if input.len < 10:
|
||||
return "image"
|
||||
var p = input[0].addr
|
||||
# OpenImageIO usually has no trouble detecting the most common formats,
|
||||
# however it seems we need to give it a file name for some, like EXR.
|
||||
if p.has_bytes "\x89PNG": return "image.png"
|
||||
if p.has_bytes "\xFF\xD8\xFF": return "image.jpg"
|
||||
if p.has_bytes "v/1\x01": return "image.exr"
|
||||
if p.has_bytes "#?RADIANCE": return "image.hdr"
|
||||
return "image"
|
||||
|
||||
proc oiioImageGetAttributes*(input: openArray[byte]): (int, int, int, OiioImageFormat) =
|
||||
# returns: width, height, channels, format
|
||||
doAssert input.len != 0, "input is empty"
|
||||
let file_name = file_name_from_magic(input)
|
||||
var w,h,c: int32
|
||||
var f: OiioImageFormat
|
||||
var res = get_attributes(input[0].addr, input.len, w,h,c,f, file_name.cstring)
|
||||
doAssert res != 0, "error getting image attibutes"
|
||||
doAssert f != FORMAT_UNKNOWN, "unknown image format"
|
||||
return (w.int, h.int, c.int, f)
|
||||
|
||||
proc oiioImageDecode*[T](input: openArray[byte], min_channels = 0): seq[T] =
|
||||
doAssert input.len != 0, "input is empty"
|
||||
let file_name = file_name_from_magic(input)
|
||||
var w,h,c: int32
|
||||
var f: OiioImageFormat
|
||||
var res = get_attributes(input[0].addr, input.len, w,h,c,f, file_name.cstring)
|
||||
doAssert res != 0, "error getting image attibutes"
|
||||
doAssert w != 0 and h != 0 and c != 0, "invalid image"
|
||||
let channels = max(min_channels.int32, c)
|
||||
# let size_in = w * h * c
|
||||
let size_out = w * h * channels
|
||||
result.setLen size_out * f.size
|
||||
var is_BGR: byte
|
||||
res = decode(input[0].addr, input.len,
|
||||
result[0].addr, size_out * f.size,
|
||||
channels, f, is_BGR, file_name.cstring)
|
||||
doAssert res != 0, "error decoding image"
|
||||
|
2
libs/packages/oiio.nim
Normal file
2
libs/packages/oiio.nim
Normal file
|
@ -0,0 +1,2 @@
|
|||
import ../oiio/oiio
|
||||
export oiio
|
|
@ -41,9 +41,12 @@ import vmath except Quat, quat
|
|||
import arr_ref
|
||||
import float16
|
||||
|
||||
import stb_image/read as stbi
|
||||
const myouConvertHdrToFloat16 {.booldefine.} = true
|
||||
const myouConvertHdrToFloat16 {.booldefine.} = not defined(myouUseOpenImageIO)
|
||||
|
||||
when defined(myouUseOpenImageIO):
|
||||
import oiio
|
||||
else:
|
||||
import stb_image/read as stbi
|
||||
when not defined(nimdoc):
|
||||
import tinyexr
|
||||
|
||||
|
@ -106,7 +109,19 @@ proc f32_to_f16(source: ptr UncheckedArray[float32], dest: ptr UncheckedArray[Fl
|
|||
dest[i] = source[i].tofloat16(clamp=true)
|
||||
|
||||
proc getDimensionsFormat*(p: pointer, len: int, min_channels=0): (int, int, TextureFormat) =
|
||||
when not defined(nimdoc):
|
||||
when defined(myouUseOpenImageIO):
|
||||
var (width, height, channels, fmt) = oiioImageGetAttributes(p.toOpenArrayByte(0, len-1))
|
||||
channels = max(min_channels, channels)
|
||||
when myouConvertHdrToFloat16:
|
||||
if fmt == FORMAT_FLOAT: fmt = FORMAT_HALF
|
||||
let format = case fmt:
|
||||
of FORMAT_BYTE: (R_u8.int - 1 + channels).TextureFormat
|
||||
of FORMAT_SHORT: (R_u16.int - 1 + channels).TextureFormat
|
||||
of FORMAT_HALF: (R_f16.int - 1 + channels).TextureFormat
|
||||
of FORMAT_FLOAT: (R_f32.int - 1 + channels).TextureFormat
|
||||
else: R_u8
|
||||
return (width, height, format)
|
||||
elif not defined(nimdoc):
|
||||
if isEXR(p, len):
|
||||
let dims = getEXRDimensions(p, len)
|
||||
return (dims[0], dims[1], RGBA_f16)
|
||||
|
@ -144,13 +159,25 @@ proc loadFileFromSlices*(tex: Texture, slices: seq[SliceMem[byte]],
|
|||
var out_buffer = newSliceMem[byte](layer_stride * tex.depth)
|
||||
var pos = 0
|
||||
for slice in slices:
|
||||
var pixels_ptr: pointer
|
||||
var pixels_len: int
|
||||
when defined(myouUseOpenImageIO):
|
||||
let image = oiioImageDecode[byte](slice.toOpenArrayByte, min_channels)
|
||||
pixels_ptr = image[0].addr
|
||||
pixels_len = image.len
|
||||
when myouConvertHdrToFloat16:
|
||||
if pixels_len == layer_stride * 2:
|
||||
f32_to_f16(
|
||||
cast[ptr UncheckedArray[float32]](pixels_ptr),
|
||||
cast[ptr UncheckedArray[Float16]](pixels_ptr),
|
||||
pixels_len div 4)
|
||||
pixels_len = pixels_len div 2
|
||||
# var flip = not flip
|
||||
else:
|
||||
var image: imagePixelData[byte]
|
||||
var image_16: imagePixelData[uint16]
|
||||
var image_f: imagePixelData[float32]
|
||||
var buffer: ArrRef[byte]
|
||||
# a reference to this pointer is kept with one of the vars above
|
||||
var pixels_ptr: pointer
|
||||
var pixels_len: int
|
||||
var flip = flip
|
||||
if isEXR(slice.data, slice.byte_len):
|
||||
let (width, height, pixels) = decodeEXR(slice.data, slice.byte_len)
|
||||
|
|
|
@ -430,8 +430,10 @@ proc newTexture*(engine: MyouEngine, name: string,
|
|||
self.engine.renderer.enqueue proc()=
|
||||
self.ensure_storage(existing_resource)
|
||||
if pixels != nil:
|
||||
doAssert pixels.byte_len == width * height * depth * format.stride,
|
||||
"Texture pixels have wrong size for the given dimensions and format"
|
||||
self.loadFromPixelsPointer(pixels.toPointer)
|
||||
else:
|
||||
elif existing_resource == nil:
|
||||
self.preallocate()
|
||||
when defined(myouUseRenderdoc):
|
||||
# Note: when we switch to arrays we'll have to store resolutions
|
||||
|
@ -530,7 +532,7 @@ proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: b
|
|||
echo getCurrentExceptionMsg()
|
||||
echo "Error loading texture " & file_name
|
||||
else:
|
||||
var (width, height, format) = getDimensionsFormat(res.orig_data.data, res.orig_data.byte_len, 3)
|
||||
var (width, height, format) = getDimensionsFormat(res.orig_data.toPointer, res.orig_data.byte_len, 3)
|
||||
if is_sRGB:
|
||||
if format in [RGB_u8, RGBA_u8]:
|
||||
format = format.to_sRGB
|
||||
|
|
Loading…
Reference in a new issue