#pragma once #include "AftrConfig.h" #include #include #include #include #include "Tex.h" namespace Aftr { //SLN BLUF - read/write jpg/png files: //use write_image_png(...) to write a png to disk //use load_image_from_file(...) to read a "common image file" like png or jpg (the engine uses this to load textures) // // read/write grayscale files: //use write_image_pgm(...) for gray scale 8 or 16-bit depth. Use std::uint8_t as the template param for 8 bit // and use std::uint16_t for 16 bit depth -- like for monochrome images acquired from cameras //use load_image_from_file_cimg(...) to load the grayscale images. This can also do a lot more if ImageMagick is //installed, but I haven't added the simple helper stubs yet. //Provides a move-only RAII wrapper around the raw stbi image loader to avoid mem leaks and ensure, at compile //time, that no extra copies are made (move only) //This image format is always loaded as R RG RGB RGBA in interleaved pixel format. //The STBI loader is good for reading images, not as good for writing them -- it only writes 8 bit channel //imagery and is not that fast. It also does not support writing grayscale pgm. So CImg is also used //depending on what is desired to be written / read. //If you are reading and writing a normal colored image for a texture, use //load_image_from_file() and write_image_png() //AftrBurner also wraps CImg library to support pgm files and any other format // that ImageMagick supports (but you have to install Image Magick before CImg can process them). //If you are saving monochrome imagery, its a lot faster to use //load_image_from_file_cimg() and write_image_pgm() //See C:\repos\aburn\engine\src\aftr\gtest\AftrUtil_image_test.cpp and the tests for examples //reading and writing images... grayscale_timing_cimg_pnm //Must use the method load_image_from_file(...) to create an Aftr_stb_image class Aftr_stb_image; using Aftr_stb_imagePtr = std::shared_ptr; //The Aftr_stb_image class wraps the STBI image header only library since it has a C malloc'd internal //loading system. class Aftr_stb_image { public: //A friend so it can access the private c'tor friend Aftr_stb_imagePtr load_image_from_file( std::string const& fileName, bool flipVertically, int desired_NumChannels ); Aftr_stb_image() = delete; ~Aftr_stb_image(); Aftr_stb_image( Aftr_stb_image const& toCopy ) = default; Aftr_stb_image( Aftr_stb_image&& toMove ) noexcept = default; Aftr_stb_image& operator=( Aftr_stb_image const& ) = default; Aftr_stb_image& operator=( Aftr_stb_image&& toMoveAssign ) noexcept = default; int w = -1; int h = -1; int numComp = 0; //owned by this instance, but allocated via C (malloc), must use stbi_image_free to deallocate. //Hence this class exists to RAII allocate and free the memory appropriately. unsigned char* data = nullptr; private: //Private c'tor so only factory function can create this and return a shared_ptr. Aftr_stb_image( int wPix, int hPix, int numColorChannels, unsigned char* data ); }; template class ImageWrapper; //forward declare so toString free function is visible to ImageWrapper using ImageWrapper_uint8t = ImageWrapper< std::uint8_t >; //each color channel is 8bits using ImageWrapper_uint16t = ImageWrapper< std::uint16_t >; //each color channel is 16bits using ImageWrapper_uint32t = ImageWrapper< std::uint32_t >; //each color channel is 32bits using ImageWrapper_f32t = ImageWrapper< float >; //each color channel is 32 bit float //Free functions to print info about an ImageWrapper std::string toString( ImageWrapper_uint8t const& img ); std::string toString( ImageWrapper_uint16t const& img ); std::string toString( ImageWrapper_uint32t const& img ); std::string toString( ImageWrapper_f32t const& img ); //Stores only the meta data of an ImageWrapper. Helpful if one doesn't want to store an actual //ImageWrapper copy which will keep the shared_ptr to the pixel data alive. struct ImageWrapperInfo { int32_t w{ 0 }; int32_t h{ 0 }; int32_t numComponents{ 0 }; int32_t bytesPerComponent{ 0 }; bool isInterleavedComp{ true }; std::uint32_t getSizeBytes() const noexcept { return w * h * numComponents * bytesPerComponent; } std::string toString() const; auto operator<=>( const ImageWrapperInfo& ) const = default; //enable compairison on meta data }; //The ImageWrapper class generically wraps data loaded from some other image loading library like // CImg. Eventually the Aftr_stb_image will be replaced with this. //Most common values for BYTES_PER_COMPONENT are // A component is a single part of an individual pixel. For example if one pixel // has RED,GREEN,BLUE, and ALPHA. The component is the underlying TYPE used to store each R,G,B, and A. // So if the component was std::uint8_t, then each pixel would be RGBA8, that is four 1 byte values // RED is std::uint8_t //std::uint8_t, std::uint16_t, std::uint32_t, std::float or std::double //if unsure, you probably mean std::uint8_t template< typename PIXEL_COMPONENT_TYPE = std::uint8_t > class ImageWrapper { public: //template< typename > //should be same type as PIXEL_COMPONENT_TYPE but gcc doesn't allow template shadowing //friend std::expected< ImageWrapper< PIXEL_COMPONENT_TYPE >, std::string > load_image_from_file_cimg( std::string const& fileName ); //factory friend function to access private c'tor using value_type = PIXEL_COMPONENT_TYPE; //The type of the size for each component of each pixel std::uint32_t getSizeBytes() const noexcept { return w * h * numComponents * bytesPerComponent; } std::int32_t w{ 0 }; // Width in pixels std::int32_t h{ 0 }; //height in pixels std::int32_t numComponents{ 0 }; // number of color channels per pixel std::int32_t bytesPerComponent{ sizeof(PIXEL_COMPONENT_TYPE) }; //size of each channel, in bytes -- this is the template param in CImg bool isInterleavedComp{ true }; //true if components are interlaved, ie RGB RGB RGB. If false, the components are planar RRR GGG BBB std::shared_ptr< PIXEL_COMPONENT_TYPE[] > dat = nullptr; ImageWrapper() = delete; ~ImageWrapper() = default; ImageWrapper( ImageWrapper const& toCopy ) = default; ImageWrapper( ImageWrapper&& toMove ) noexcept = default; ImageWrapper& operator=( ImageWrapper const& ) = default; ImageWrapper& operator=( ImageWrapper&& toMoveAssign ) noexcept = default; explicit ImageWrapper( std::int32_t w, std::int32_t h, std::int32_t numComp, std::shared_ptr< PIXEL_COMPONENT_TYPE[] > dat, bool isInterleavedComp = true ) : w( w ), h( h ), numComponents( numComp ), bytesPerComponent( sizeof( PIXEL_COMPONENT_TYPE ) ), isInterleavedComp( isInterleavedComp ), dat( dat ) {} //explicit ImageWrapper( cimg_library::CImg&& takeOwnership ); //use the Aftr::toString free function to toString an ImageWrapper std::string toString() const { return Aftr::toString( *this ); } ImageWrapperInfo getMetaInfo() const noexcept { return ImageWrapperInfo{ w, h, numComponents, bytesPerComponent, isInterleavedComp }; } }; //---------------- //STB Functions -- these use the header only STB library to read and write images. Good for reading // typical texture files for 3D Models. // //Factory function that creates a shared_ptr to a Aftr_stb_image. The shared_ptr is reference counted, copy-able //and easily placed in std::function captures for execution on different threads. // By default, OpenGL Textures like 0,0 to be lower left instead of upper left, so the bool flips the image // vertically. // numChannelsInOutputImage parameter, if non-zero, will force the output image // to have that many channels in the returned image. // To query an image about its internals before loading it, use stbi_info* Aftr_stb_imagePtr load_image_from_file( std::string const& fileName, bool flipVertically = true, int numChannelsInOutputImage = 0 ); //Writes in passed data to filename. No data is owned/freed by this function. //Because this uses stb_write, it ONLY supports 8 bits per channel (1 byte). Use CImg if need larger bytes per channel / component. std::expected write_image_grayscale_png( std::string const& filename, int width, int height, unsigned char const* const data_nonOwningPtr, int numChannels = 1 ); std::expected write_image_png( std::string const& filename, Aftr_stb_image const& img ); //Uses STB to write the a bmp. This assumes each color channel is 8 bit std::expected write_image_bmp( std::string const& filename, Aftr::ImageWrapper_uint8t const& img ); /// Convert a 3 channel RGB image to a grayscale luminance image with one color channel. If input not already interleaved, this method fails. /// Returns a single channel ImageWrapper if successful or an error string if failed. std::expected< ImageWrapper_uint8t, std::string > resizeRGB_stb( ImageWrapper_uint8t const& rgb_img, int widthPix, int heightPix ); // End of STB Functions //---------------- //---------------- //Image Conversion Functions template< typename PIXEL_COMPONENT_TYPE > std::optional create_Texture_from_Image( ImageWrapper const& img ); // Assumes in_out_Tex is the same size, same number of components, same bytes per component as img. Essentially, // in_out_Tex must be memory-wise identical to to img. This can be done by first calling create_Texture_from_image(...) // and then subsequently calling this function using the Tex returned from create_Texture_from_image. template< typename PIXEL_COMPONENT_TYPE > bool update_Existing_Texture_from_Image( Tex& existing_tex_to_update_pix_data, ImageWrapper const& img ); //End of Image Conversion Functions //---------------- #ifdef AFTR_CONFIG_USE_CIMG //---------------- //CImg Functions // The last parameter, bool layout_ImgData_as_interleaved, details how the pixel color data is layed out in memory. // Typically, OpenGL, OpenCV, and other image libraries store the pixel data as RGB, RGB, RGB (interleaves // Red, then green, then blue) per pixel. // But CIMG stores pixel data as planar data, ie, RRRRRR..., GGGGGG..., BBBBBB.... (all red pixels, then all green, // then all blue). If saving a ImageWrapper that is loaded as planar pixel data, then pass in false, // otherwise, use true. template< typename PIXEL_COMPONENT_TYPE = std::uint8_t > std::expected< ImageWrapper< PIXEL_COMPONENT_TYPE >, std::string > load_image_from_file_cimg( std::string const& fileName, bool layout_ImgData_as_interleaved = true ); //Automatically appends the .pgm to the end of fileName parameters. template< typename PIXEL_COMPONENT_TYPE = std::uint8_t > std::expected< bool, std::string > write_image_pgm( std::string const& fileName, ImageWrapper const& img ); // Saves image as a bmp. Can be either color or grayscale. Grayscale only supported by write_image_bmp_cimg. // CImg saving can be slower than STB image. If this causes a performance problem, consider Aftr::write_image_bmp. // However, STB can only write 8-bit channels, ie, RGB8. // The last parameter, bool isImgData_interleaved_rgb, details how the pixel color data is layed out in memory. // Typically, OpenGL, OpenCV, and other image libraries store the pixel data as RGB, RGB, RGB per pixel. But CIMG // stores pixel data as planar data, ie, RRRRRR..., GGGGGG..., BBBBBB.... If saving a ImageWrapper that // is populated from Cimg, pass in false, otherwise, use true. template< typename PIXEL_COMPONENT_TYPE = std::uint8_t > std::expected< bool, std::string > write_image_bmp_cimg( std::string const& fileName, ImageWrapper const& img ); /// Convert a 3 channel RGB image to a grayscale luminance image with one color channel. /// Returns a single channel ImageWrapper if successful or an error string if failed. template< typename PIXEL_COMPONENT_TYPE = std::uint8_t > std::expected< ImageWrapper, std::string > convertRGBtoLuminance( ImageWrapper const& rgb_img ); // End of CImg Functions, preprocessor may remove based on AFTR_CONFIG_USE_CIMG //---------------- #endif } //namespace Aftr