Files
unity/crnlib/crn_image_utils.cpp
T
Alexander Suvorov 3e12aff909 Fix miscellaneous compiler warnings
DXT Testing:

The modified algorithm has been tested on the Kodak test set using 64-bit build with default settings (running on Windows 10, i7-4790, 3.6GHz). All the decompressed test images are identical to the images being compressed and decompressed using original version of Crunch (revision ea9b8d8).

[Compressing Kodak set without mipmaps using DXT1 encoding]
Original: 1582222 bytes / 28.866 sec
Modified: 1468204 bytes / 11.858 sec
Improvement: 7.21% (compression ratio) / 58.92% (compression time)

[Compressing Kodak set with mipmaps using DXT1 encoding]
Original: 2065243 bytes / 36.878 sec
Modified: 1914805 bytes / 15.625 sec
Improvement: 7.28% (compression ratio) / 57.63% (compression time)

ETC Testing:

The modified algorithm has been tested on the Kodak test set using 64-bit build with default settings (running on Windows 10, i7-4790, 3.6GHz). The ETC1 quantization parameters have been selected in such a way, so that ETC1 compression gives approximately the same average Luma PSNR as the corresponding DXT1 compression (which is equal to 34.044 dB for the Kodak test set compressed without mipmaps using DXT1 encoding and default quality settings).

[Compressing Kodak set without mipmaps using ETC1 encoding]
Total size: 1607858 bytes
Total time: 17.181 sec
Average bitrate: 1.363 bpp
Average Luma PSNR: 34.050 dB
2017-09-11 13:52:21 +02:00

1218 lines
38 KiB
C++

// File: crn_image_utils.cpp
// See Copyright Notice and license at the end of inc/crnlib.h
#include "crn_core.h"
#include "crn_image_utils.h"
#include "crn_console.h"
#include "crn_resampler.h"
#include "crn_threaded_resampler.h"
#include "crn_strutils.h"
#include "crn_file_utils.h"
#include "crn_threading.h"
#include "crn_miniz.h"
#include "crn_jpge.h"
#include "crn_cfile_stream.h"
#include "crn_mipmapped_texture.h"
#include "crn_buffer_stream.h"
#define STBI_HEADER_FILE_ONLY
#include "crn_stb_image.cpp"
#include "crn_jpgd.h"
#include "crn_pixel_format.h"
namespace crnlib {
const float cInfinitePSNR = 999999.0f;
const uint CRNLIB_LARGEST_SUPPORTED_IMAGE_DIMENSION = 16384;
namespace image_utils {
bool read_from_stream_stb(data_stream_serializer& serializer, image_u8& img) {
uint8_vec buf;
if (!serializer.read_entire_file(buf))
return false;
int x = 0, y = 0, n = 0;
unsigned char* pData = stbi_load_from_memory(buf.get_ptr(), buf.size_in_bytes(), &x, &y, &n, 4);
if (!pData)
return false;
if ((x > (int)CRNLIB_LARGEST_SUPPORTED_IMAGE_DIMENSION) || (y > (int)CRNLIB_LARGEST_SUPPORTED_IMAGE_DIMENSION)) {
stbi_image_free(pData);
return false;
}
const bool has_alpha = ((n == 2) || (n == 4));
img.resize(x, y);
bool grayscale = true;
for (int py = 0; py < y; py++) {
const color_quad_u8* pSrc = reinterpret_cast<const color_quad_u8*>(pData) + (py * x);
color_quad_u8* pDst = img.get_scanline(py);
color_quad_u8* pDst_end = pDst + x;
while (pDst != pDst_end) {
color_quad_u8 c(*pSrc++);
if (!has_alpha)
c.a = 255;
if (!c.is_grayscale())
grayscale = false;
*pDst++ = c;
}
}
stbi_image_free(pData);
img.reset_comp_flags();
img.set_grayscale(grayscale);
img.set_component_valid(3, has_alpha);
return true;
}
bool read_from_stream_jpgd(data_stream_serializer& serializer, image_u8& img) {
uint8_vec buf;
if (!serializer.read_entire_file(buf))
return false;
int width = 0, height = 0, actual_comps = 0;
unsigned char* pSrc_img = jpgd::decompress_jpeg_image_from_memory(buf.get_ptr(), buf.size_in_bytes(), &width, &height, &actual_comps, 4);
if (!pSrc_img)
return false;
if (math::maximum(width, height) > (int)CRNLIB_LARGEST_SUPPORTED_IMAGE_DIMENSION) {
crnlib_free(pSrc_img);
return false;
}
if (!img.grant_ownership(reinterpret_cast<color_quad_u8*>(pSrc_img), width, height)) {
crnlib_free(pSrc_img);
return false;
}
img.reset_comp_flags();
img.set_grayscale(actual_comps == 1);
img.set_component_valid(3, false);
return true;
}
bool read_from_stream(image_u8& dest, data_stream_serializer& serializer, uint read_flags) {
if (read_flags > cReadFlagsAllFlags) {
CRNLIB_ASSERT(0);
return false;
}
if (!serializer.get_stream()) {
CRNLIB_ASSERT(0);
return false;
}
dynamic_string ext(serializer.get_name());
file_utils::get_extension(ext);
if ((ext == "jpg") || (ext == "jpeg")) {
// Use my jpeg decoder by default because it supports progressive jpeg's.
if ((read_flags & cReadFlagForceSTB) == 0) {
return image_utils::read_from_stream_jpgd(serializer, dest);
}
}
return image_utils::read_from_stream_stb(serializer, dest);
}
bool read_from_file(image_u8& dest, const char* pFilename, uint read_flags) {
if (read_flags > cReadFlagsAllFlags) {
CRNLIB_ASSERT(0);
return false;
}
cfile_stream file_stream;
if (!file_stream.open(pFilename))
return false;
data_stream_serializer serializer(file_stream);
return read_from_stream(dest, serializer, read_flags);
}
bool write_to_file(const char* pFilename, const image_u8& img, uint write_flags, int grayscale_comp_index) {
if ((grayscale_comp_index < -1) || (grayscale_comp_index > 3)) {
CRNLIB_ASSERT(0);
return false;
}
if (!img.get_width()) {
CRNLIB_ASSERT(0);
return false;
}
dynamic_string ext(pFilename);
bool is_jpeg = false;
if (file_utils::get_extension(ext)) {
is_jpeg = ((ext == "jpg") || (ext == "jpeg"));
if ((ext != "png") && (ext != "bmp") && (ext != "tga") && (!is_jpeg)) {
console::error("crnlib::image_utils::write_to_file: Can only write .BMP, .TGA, .PNG, or .JPG files!\n");
return false;
}
}
crnlib::vector<uint8> temp;
uint num_src_chans = 0;
const void* pSrc_img = NULL;
if (is_jpeg) {
write_flags |= cWriteFlagIgnoreAlpha;
}
if ((img.get_comp_flags() & pixel_format_helpers::cCompFlagGrayscale) || (write_flags & image_utils::cWriteFlagGrayscale)) {
CRNLIB_ASSERT(grayscale_comp_index < 4);
if (grayscale_comp_index > 3)
grayscale_comp_index = 3;
temp.resize(img.get_total_pixels());
for (uint y = 0; y < img.get_height(); y++) {
const color_quad_u8* pSrc = img.get_scanline(y);
const color_quad_u8* pSrc_end = pSrc + img.get_width();
uint8* pDst = &temp[y * img.get_width()];
if (img.get_comp_flags() & pixel_format_helpers::cCompFlagGrayscale) {
while (pSrc != pSrc_end)
*pDst++ = (*pSrc++)[1];
} else if (grayscale_comp_index < 0) {
while (pSrc != pSrc_end)
*pDst++ = static_cast<uint8>((*pSrc++).get_luma());
} else {
while (pSrc != pSrc_end)
*pDst++ = (*pSrc++)[grayscale_comp_index];
}
}
pSrc_img = &temp[0];
num_src_chans = 1;
} else if ((!img.is_component_valid(3)) || (write_flags & cWriteFlagIgnoreAlpha)) {
temp.resize(img.get_total_pixels() * 3);
for (uint y = 0; y < img.get_height(); y++) {
const color_quad_u8* pSrc = img.get_scanline(y);
const color_quad_u8* pSrc_end = pSrc + img.get_width();
uint8* pDst = &temp[y * img.get_width() * 3];
while (pSrc != pSrc_end) {
const color_quad_u8 c(*pSrc++);
pDst[0] = c.r;
pDst[1] = c.g;
pDst[2] = c.b;
pDst += 3;
}
}
num_src_chans = 3;
pSrc_img = &temp[0];
} else {
num_src_chans = 4;
pSrc_img = img.get_ptr();
}
bool success = false;
if (ext == "png") {
size_t png_image_size = 0;
void* pPNG_image_data = tdefl_write_image_to_png_file_in_memory(pSrc_img, img.get_width(), img.get_height(), num_src_chans, &png_image_size);
if (!pPNG_image_data)
return false;
success = file_utils::write_buf_to_file(pFilename, pPNG_image_data, png_image_size);
mz_free(pPNG_image_data);
} else if (is_jpeg) {
jpge::params params;
if (write_flags & cWriteFlagJPEGQualityLevelMask)
params.m_quality = math::clamp<uint>((write_flags & cWriteFlagJPEGQualityLevelMask) >> cWriteFlagJPEGQualityLevelShift, 1U, 100U);
params.m_two_pass_flag = (write_flags & cWriteFlagJPEGTwoPass) != 0;
params.m_no_chroma_discrim_flag = (write_flags & cWriteFlagJPEGNoChromaDiscrim) != 0;
if (write_flags & cWriteFlagJPEGH1V1)
params.m_subsampling = jpge::H1V1;
else if (write_flags & cWriteFlagJPEGH2V1)
params.m_subsampling = jpge::H2V1;
else if (write_flags & cWriteFlagJPEGH2V2)
params.m_subsampling = jpge::H2V2;
success = jpge::compress_image_to_jpeg_file(pFilename, img.get_width(), img.get_height(), num_src_chans, (const jpge::uint8*)pSrc_img, params);
} else {
success = ((ext == "bmp" ? stbi_write_bmp : stbi_write_tga)(pFilename, img.get_width(), img.get_height(), num_src_chans, pSrc_img) == CRNLIB_TRUE);
}
return success;
}
bool has_alpha(const image_u8& img) {
for (uint y = 0; y < img.get_height(); y++)
for (uint x = 0; x < img.get_width(); x++)
if (img(x, y).a < 255)
return true;
return false;
}
void renorm_normal_map(image_u8& img) {
for (uint y = 0; y < img.get_height(); y++) {
for (uint x = 0; x < img.get_width(); x++) {
color_quad_u8& c = img(x, y);
if ((c.r == 128) && (c.g == 128) && (c.b == 128))
continue;
vec3F v(c.r, c.g, c.b);
v *= 1.0f / 255.0f;
v *= 2.0f;
v -= vec3F(1.0f);
v.clamp(-1.0f, 1.0f);
float length = v.length();
if (length < .077f)
c.set(128, 128, 128, c.a);
else if (fabs(length - 1.0f) > .077f) {
if (length)
v /= length;
for (uint i = 0; i < 3; i++)
c[i] = static_cast<uint8>(math::clamp<float>(floor((v[i] + 1.0f) * .5f * 255.0f + .5f), 0.0f, 255.0f));
if ((c.r == 128) && (c.g == 128)) {
if (c.b < 128)
c.b = 0;
else
c.b = 255;
}
}
}
}
}
bool is_normal_map(const image_u8& img, const char* pFilename) {
float score = 0.0f;
uint num_invalid_pixels = 0;
// TODO: Derive better score from pixel mean, eigenvecs/vals
//crnlib::vector<vec3F> pixels;
for (uint y = 0; y < img.get_height(); y++) {
for (uint x = 0; x < img.get_width(); x++) {
const color_quad_u8& c = img(x, y);
if (c.b < 123) {
num_invalid_pixels++;
continue;
} else if ((c.r != 128) || (c.g != 128) || (c.b != 128)) {
vec3F v(c.r, c.g, c.b);
v -= vec3F(128.0f);
v /= vec3F(127.0f);
//pixels.push_back(v);
v.clamp(-1.0f, 1.0f);
float norm = v.norm();
if ((norm < 0.83f) || (norm > 1.29f))
num_invalid_pixels++;
}
}
}
score -= math::clamp(float(num_invalid_pixels) / (img.get_width() * img.get_height()) - .026f, 0.0f, 1.0f) * 5.0f;
if (pFilename) {
dynamic_string str(pFilename);
str.tolower();
if (str.contains("normal") || str.contains("local") || str.contains("nmap"))
score += 1.0f;
if (str.contains("diffuse") || str.contains("spec") || str.contains("gloss"))
score -= 1.0f;
}
return score >= 0.0f;
}
bool resample_single_thread(const image_u8& src, image_u8& dst, const resample_params& params) {
const uint src_width = src.get_width();
const uint src_height = src.get_height();
if (math::maximum(src_width, src_height) > CRNLIB_RESAMPLER_MAX_DIMENSION) {
printf("Image is too large!\n");
return EXIT_FAILURE;
}
const int cMaxComponents = 4;
if (((int)params.m_num_comps < 1) || ((int)params.m_num_comps > (int)cMaxComponents))
return false;
const uint dst_width = params.m_dst_width;
const uint dst_height = params.m_dst_height;
if ((math::minimum(dst_width, dst_height) < 1) || (math::maximum(dst_width, dst_height) > CRNLIB_RESAMPLER_MAX_DIMENSION)) {
printf("Image is too large!\n");
return EXIT_FAILURE;
}
if ((src_width == dst_width) && (src_height == dst_height)) {
dst = src;
return true;
}
dst.clear();
dst.resize(params.m_dst_width, params.m_dst_height);
// Partial gamma correction looks better on mips. Set to 1.0 to disable gamma correction.
const float source_gamma = params.m_source_gamma; //1.75f;
float srgb_to_linear[256];
if (params.m_srgb) {
for (int i = 0; i < 256; ++i)
srgb_to_linear[i] = (float)pow(i * 1.0f / 255.0f, source_gamma);
}
const int linear_to_srgb_table_size = 8192;
unsigned char linear_to_srgb[linear_to_srgb_table_size];
const float inv_linear_to_srgb_table_size = 1.0f / linear_to_srgb_table_size;
const float inv_source_gamma = 1.0f / source_gamma;
if (params.m_srgb) {
for (int i = 0; i < linear_to_srgb_table_size; ++i) {
int k = (int)(255.0f * pow(i * inv_linear_to_srgb_table_size, inv_source_gamma) + .5f);
if (k < 0)
k = 0;
else if (k > 255)
k = 255;
linear_to_srgb[i] = (unsigned char)k;
}
}
Resampler* resamplers[cMaxComponents];
crnlib::vector<float> samples[cMaxComponents];
resamplers[0] = crnlib_new<Resampler>(src_width, src_height, dst_width, dst_height,
params.m_wrapping ? Resampler::BOUNDARY_WRAP : Resampler::BOUNDARY_CLAMP, 0.0f, 1.0f,
params.m_pFilter, (Resampler::Contrib_List*)NULL, (Resampler::Contrib_List*)NULL, params.m_filter_scale, params.m_filter_scale);
samples[0].resize(src_width);
for (uint i = 1; i < params.m_num_comps; i++) {
resamplers[i] = crnlib_new<Resampler>(src_width, src_height, dst_width, dst_height,
params.m_wrapping ? Resampler::BOUNDARY_WRAP : Resampler::BOUNDARY_CLAMP, 0.0f, 1.0f,
params.m_pFilter, resamplers[0]->get_clist_x(), resamplers[0]->get_clist_y(), params.m_filter_scale, params.m_filter_scale);
samples[i].resize(src_width);
}
uint dst_y = 0;
for (uint src_y = 0; src_y < src_height; src_y++) {
const color_quad_u8* pSrc = src.get_scanline(src_y);
for (uint x = 0; x < src_width; x++) {
for (uint c = 0; c < params.m_num_comps; c++) {
const uint comp_index = params.m_first_comp + c;
const uint8 v = (*pSrc)[comp_index];
if (!params.m_srgb || (comp_index == 3))
samples[c][x] = v * (1.0f / 255.0f);
else
samples[c][x] = srgb_to_linear[v];
}
pSrc++;
}
for (uint c = 0; c < params.m_num_comps; c++) {
if (!resamplers[c]->put_line(&samples[c][0])) {
for (uint i = 0; i < params.m_num_comps; i++)
crnlib_delete(resamplers[i]);
return false;
}
}
for (;;) {
uint c;
for (c = 0; c < params.m_num_comps; c++) {
const uint comp_index = params.m_first_comp + c;
const float* pOutput_samples = resamplers[c]->get_line();
if (!pOutput_samples)
break;
const bool linear = !params.m_srgb || (comp_index == 3);
CRNLIB_ASSERT(dst_y < dst_height);
color_quad_u8* pDst = dst.get_scanline(dst_y);
for (uint x = 0; x < dst_width; x++) {
if (linear) {
int c = (int)(255.0f * pOutput_samples[x] + .5f);
if (c < 0)
c = 0;
else if (c > 255)
c = 255;
(*pDst)[comp_index] = (unsigned char)c;
} else {
int j = (int)(linear_to_srgb_table_size * pOutput_samples[x] + .5f);
if (j < 0)
j = 0;
else if (j >= linear_to_srgb_table_size)
j = linear_to_srgb_table_size - 1;
(*pDst)[comp_index] = linear_to_srgb[j];
}
pDst++;
}
}
if (c < params.m_num_comps)
break;
dst_y++;
}
}
for (uint i = 0; i < params.m_num_comps; i++)
crnlib_delete(resamplers[i]);
return true;
}
bool resample_multithreaded(const image_u8& src, image_u8& dst, const resample_params& params) {
const uint src_width = src.get_width();
const uint src_height = src.get_height();
if (math::maximum(src_width, src_height) > CRNLIB_RESAMPLER_MAX_DIMENSION) {
printf("Image is too large!\n");
return EXIT_FAILURE;
}
const int cMaxComponents = 4;
if (((int)params.m_num_comps < 1) || ((int)params.m_num_comps > (int)cMaxComponents))
return false;
const uint dst_width = params.m_dst_width;
const uint dst_height = params.m_dst_height;
if ((math::minimum(dst_width, dst_height) < 1) || (math::maximum(dst_width, dst_height) > CRNLIB_RESAMPLER_MAX_DIMENSION)) {
printf("Image is too large!\n");
return EXIT_FAILURE;
}
if ((src_width == dst_width) && (src_height == dst_height)) {
dst = src;
return true;
}
dst.clear();
// Partial gamma correction looks better on mips. Set to 1.0 to disable gamma correction.
const float source_gamma = params.m_source_gamma; //1.75f;
float srgb_to_linear[256];
if (params.m_srgb) {
for (int i = 0; i < 256; ++i)
srgb_to_linear[i] = (float)pow(i * 1.0f / 255.0f, source_gamma);
}
const int linear_to_srgb_table_size = 8192;
unsigned char linear_to_srgb[linear_to_srgb_table_size];
const float inv_linear_to_srgb_table_size = 1.0f / linear_to_srgb_table_size;
const float inv_source_gamma = 1.0f / source_gamma;
if (params.m_srgb) {
for (int i = 0; i < linear_to_srgb_table_size; ++i) {
int k = (int)(255.0f * pow(i * inv_linear_to_srgb_table_size, inv_source_gamma) + .5f);
if (k < 0)
k = 0;
else if (k > 255)
k = 255;
linear_to_srgb[i] = (unsigned char)k;
}
}
task_pool tp;
tp.init(g_number_of_processors - 1);
threaded_resampler resampler(tp);
threaded_resampler::params p;
p.m_src_width = src_width;
p.m_src_height = src_height;
p.m_dst_width = dst_width;
p.m_dst_height = dst_height;
p.m_sample_low = 0.0f;
p.m_sample_high = 1.0f;
p.m_boundary_op = params.m_wrapping ? Resampler::BOUNDARY_WRAP : Resampler::BOUNDARY_CLAMP;
p.m_Pfilter_name = params.m_pFilter;
p.m_filter_x_scale = params.m_filter_scale;
p.m_filter_y_scale = params.m_filter_scale;
uint resampler_comps = 4;
if (params.m_num_comps == 1) {
p.m_fmt = threaded_resampler::cPF_Y_F32;
resampler_comps = 1;
} else if (params.m_num_comps <= 3)
p.m_fmt = threaded_resampler::cPF_RGBX_F32;
else
p.m_fmt = threaded_resampler::cPF_RGBA_F32;
crnlib::vector<float> src_samples;
crnlib::vector<float> dst_samples;
if (!src_samples.try_resize(src_width * src_height * resampler_comps))
return false;
if (!dst_samples.try_resize(dst_width * dst_height * resampler_comps))
return false;
p.m_pSrc_pixels = src_samples.get_ptr();
p.m_src_pitch = src_width * resampler_comps * sizeof(float);
p.m_pDst_pixels = dst_samples.get_ptr();
p.m_dst_pitch = dst_width * resampler_comps * sizeof(float);
for (uint src_y = 0; src_y < src_height; src_y++) {
const color_quad_u8* pSrc = src.get_scanline(src_y);
float* pDst = src_samples.get_ptr() + src_width * resampler_comps * src_y;
for (uint x = 0; x < src_width; x++) {
for (uint c = 0; c < params.m_num_comps; c++) {
const uint comp_index = params.m_first_comp + c;
const uint8 v = (*pSrc)[comp_index];
if (!params.m_srgb || (comp_index == 3))
pDst[c] = v * (1.0f / 255.0f);
else
pDst[c] = srgb_to_linear[v];
}
pSrc++;
pDst += resampler_comps;
}
}
if (!resampler.resample(p))
return false;
src_samples.clear();
if (!dst.resize(params.m_dst_width, params.m_dst_height))
return false;
for (uint dst_y = 0; dst_y < dst_height; dst_y++) {
const float* pSrc = dst_samples.get_ptr() + dst_width * resampler_comps * dst_y;
color_quad_u8* pDst = dst.get_scanline(dst_y);
for (uint x = 0; x < dst_width; x++) {
color_quad_u8 dst(0, 0, 0, 255);
for (uint c = 0; c < params.m_num_comps; c++) {
const uint comp_index = params.m_first_comp + c;
const float v = pSrc[c];
if ((!params.m_srgb) || (comp_index == 3)) {
int c = static_cast<int>(255.0f * v + .5f);
if (c < 0)
c = 0;
else if (c > 255)
c = 255;
dst[comp_index] = (unsigned char)c;
} else {
int j = static_cast<int>(linear_to_srgb_table_size * v + .5f);
if (j < 0)
j = 0;
else if (j >= linear_to_srgb_table_size)
j = linear_to_srgb_table_size - 1;
dst[comp_index] = linear_to_srgb[j];
}
}
*pDst++ = dst;
pSrc += resampler_comps;
}
}
return true;
}
bool resample(const image_u8& src, image_u8& dst, const resample_params& params) {
if ((params.m_multithreaded) && (g_number_of_processors > 1))
return resample_multithreaded(src, dst, params);
else
return resample_single_thread(src, dst, params);
}
bool compute_delta(image_u8& dest, image_u8& a, image_u8& b, uint scale) {
if ((a.get_width() != b.get_width()) || (a.get_height() != b.get_height()))
return false;
dest.resize(a.get_width(), b.get_height());
for (uint y = 0; y < a.get_height(); y++) {
for (uint x = 0; x < a.get_width(); x++) {
const color_quad_u8& ca = a(x, y);
const color_quad_u8& cb = b(x, y);
color_quad_u8 cd;
for (uint c = 0; c < 4; c++) {
int d = (ca[c] - cb[c]) * scale + 128;
d = math::clamp(d, 0, 255);
cd[c] = static_cast<uint8>(d);
}
dest(x, y) = cd;
}
}
return true;
}
// FIXME: Totally hack-ass computation.
// Perhaps port http://www.lomont.org/Software/Misc/SSIM/SSIM.html?
double compute_block_ssim(uint t, const uint8* pX, const uint8* pY) {
double ave_x = 0.0f;
double ave_y = 0.0f;
for (uint i = 0; i < t; i++) {
ave_x += pX[i];
ave_y += pY[i];
}
ave_x /= t;
ave_y /= t;
double var_x = 0.0f;
double var_y = 0.0f;
for (uint i = 0; i < t; i++) {
var_x += math::square(pX[i] - ave_x);
var_y += math::square(pY[i] - ave_y);
}
var_x = sqrt(var_x / (t - 1));
var_y = sqrt(var_y / (t - 1));
double covar_xy = 0.0f;
for (uint i = 0; i < t; i++)
covar_xy += (pX[i] - ave_x) * (pY[i] - ave_y);
covar_xy /= (t - 1);
const double c1 = 6.5025; //(255*.01)^2
const double c2 = 58.5225; //(255*.03)^2
double n = (2.0f * ave_x * ave_y + c1) * (2.0f * covar_xy + c2);
double d = (ave_x * ave_x + ave_y * ave_y + c1) * (var_x * var_x + var_y * var_y + c2);
return n / d;
}
double compute_ssim(const image_u8& a, const image_u8& b, int channel_index) {
const uint N = 6;
uint8 sx[N * N], sy[N * N];
double total_ssim = 0.0f;
uint total_blocks = 0;
//image_u8 yimg((a.get_width() + N - 1) / N, (a.get_height() + N - 1) / N);
for (uint y = 0; y < a.get_height(); y += N) {
for (uint x = 0; x < a.get_width(); x += N) {
for (uint iy = 0; iy < N; iy++) {
for (uint ix = 0; ix < N; ix++) {
if (channel_index < 0)
sx[ix + iy * N] = (uint8)a.get_clamped(x + ix, y + iy).get_luma();
else
sx[ix + iy * N] = (uint8)a.get_clamped(x + ix, y + iy)[channel_index];
if (channel_index < 0)
sy[ix + iy * N] = (uint8)b.get_clamped(x + ix, y + iy).get_luma();
else
sy[ix + iy * N] = (uint8)b.get_clamped(x + ix, y + iy)[channel_index];
}
}
double ssim = compute_block_ssim(N * N, sx, sy);
total_ssim += ssim;
total_blocks++;
//uint ssim_c = (uint)math::clamp<double>(ssim * 127.0f + 128.0f, 0, 255);
//yimg(x / N, y / N).set(ssim_c, ssim_c, ssim_c, 255);
}
}
if (!total_blocks)
return 0.0f;
//save_to_file_stb_or_miniz("ssim.tga", yimg, cWriteFlagGrayscale);
return total_ssim / total_blocks;
}
void print_ssim(const image_u8& src_img, const image_u8& dst_img) {
(void)src_img;
(void)dst_img;
//double y_ssim = compute_ssim(src_img, dst_img, -1);
//console::printf("Luma MSSIM: %f, Scaled: %f", y_ssim, (y_ssim - .8f) / .2f);
//double r_ssim = compute_ssim(src_img, dst_img, 0);
//console::printf(" R MSSIM: %f", r_ssim);
//double g_ssim = compute_ssim(src_img, dst_img, 1);
//console::printf(" G MSSIM: %f", g_ssim);
//double b_ssim = compute_ssim(src_img, dst_img, 2);
//console::printf(" B MSSIM: %f", b_ssim);
}
void error_metrics::print(const char* pName) const {
if (mPeakSNR >= cInfinitePSNR)
console::printf("%s Error: Max: %3u, Mean: %3.3f, MSE: %3.3f, RMSE: %3.3f, PSNR: Infinite", pName, mMax, mMean, mMeanSquared, mRootMeanSquared);
else
console::printf("%s Error: Max: %3u, Mean: %3.3f, MSE: %3.3f, RMSE: %3.3f, PSNR: %3.3f", pName, mMax, mMean, mMeanSquared, mRootMeanSquared, mPeakSNR);
}
bool error_metrics::compute(const image_u8& a, const image_u8& b, uint first_channel, uint num_channels, bool average_component_error) {
//if ( (!a.get_width()) || (!b.get_height()) || (a.get_width() != b.get_width()) || (a.get_height() != b.get_height()) )
// return false;
const uint width = math::minimum(a.get_width(), b.get_width());
const uint height = math::minimum(a.get_height(), b.get_height());
CRNLIB_ASSERT((first_channel < 4U) && (first_channel + num_channels <= 4U));
// Histogram approach due to Charles Bloom.
double hist[256];
utils::zero_object(hist);
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
const color_quad_u8& ca = a(x, y);
const color_quad_u8& cb = b(x, y);
if (!num_channels)
hist[labs(ca.get_luma() - cb.get_luma())]++;
else {
for (uint c = 0; c < num_channels; c++)
hist[labs(ca[first_channel + c] - cb[first_channel + c])]++;
}
}
}
mMax = 0;
double sum = 0.0f, sum2 = 0.0f;
for (uint i = 0; i < 256; i++) {
if (!hist[i])
continue;
mMax = math::maximum(mMax, i);
double x = i * hist[i];
sum += x;
sum2 += i * x;
}
// See http://bmrc.berkeley.edu/courseware/cs294/fall97/assignment/psnr.html
double total_values = width * height;
if (average_component_error)
total_values *= math::clamp<uint>(num_channels, 1, 4);
mMean = math::clamp<double>(sum / total_values, 0.0f, 255.0f);
mMeanSquared = math::clamp<double>(sum2 / total_values, 0.0f, 255.0f * 255.0f);
mRootMeanSquared = sqrt(mMeanSquared);
if (!mRootMeanSquared)
mPeakSNR = cInfinitePSNR;
else
mPeakSNR = math::clamp<double>(log10(255.0f / mRootMeanSquared) * 20.0f, 0.0f, 500.0f);
return true;
}
void print_image_metrics(const image_u8& src_img, const image_u8& dst_img) {
if ((!src_img.get_width()) || (!dst_img.get_height()) || (src_img.get_width() != dst_img.get_width()) || (src_img.get_height() != dst_img.get_height()))
console::printf("print_image_metrics: Image resolutions don't match exactly (%ux%u) vs. (%ux%u)", src_img.get_width(), src_img.get_height(), dst_img.get_width(), dst_img.get_height());
image_utils::error_metrics error_metrics;
if (src_img.has_rgb() || dst_img.has_rgb()) {
error_metrics.compute(src_img, dst_img, 0, 3, false);
error_metrics.print("RGB Total ");
error_metrics.compute(src_img, dst_img, 0, 3, true);
error_metrics.print("RGB Average");
error_metrics.compute(src_img, dst_img, 0, 0);
error_metrics.print("Luma ");
error_metrics.compute(src_img, dst_img, 0, 1);
error_metrics.print("Red ");
error_metrics.compute(src_img, dst_img, 1, 1);
error_metrics.print("Green ");
error_metrics.compute(src_img, dst_img, 2, 1);
error_metrics.print("Blue ");
}
if (src_img.has_alpha() || dst_img.has_alpha()) {
error_metrics.compute(src_img, dst_img, 3, 1);
error_metrics.print("Alpha ");
}
}
static uint8 regen_z(uint x, uint y) {
float vx = math::clamp((x - 128.0f) * 1.0f / 127.0f, -1.0f, 1.0f);
float vy = math::clamp((y - 128.0f) * 1.0f / 127.0f, -1.0f, 1.0f);
float vz = sqrt(math::clamp(1.0f - vx * vx - vy * vy, 0.0f, 1.0f));
vz = vz * 127.0f + 128.0f;
if (vz < 128.0f)
vz -= .5f;
else
vz += .5f;
int ib = math::float_to_int(vz);
return static_cast<uint8>(math::clamp(ib, 0, 255));
}
void convert_image(image_u8& img, image_utils::conversion_type conv_type) {
switch (conv_type) {
case image_utils::cConversion_To_CCxY: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagAValid | pixel_format_helpers::cCompFlagLumaChroma));
break;
}
case image_utils::cConversion_From_CCxY: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid));
break;
}
case image_utils::cConversion_To_xGxR: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagAValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case image_utils::cConversion_From_xGxR: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case image_utils::cConversion_To_xGBR: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagAValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case image_utils::cConversion_To_AGBR: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagAValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case image_utils::cConversion_From_xGBR: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case image_utils::cConversion_From_AGBR: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagAValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case image_utils::cConversion_XY_to_XYZ: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagNormalMap));
break;
}
case cConversion_Y_To_A: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(img.get_comp_flags() | pixel_format_helpers::cCompFlagAValid));
break;
}
case cConversion_A_To_RGBA: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagAValid));
break;
}
case cConversion_Y_To_RGB: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(pixel_format_helpers::cCompFlagRValid | pixel_format_helpers::cCompFlagGValid | pixel_format_helpers::cCompFlagBValid | pixel_format_helpers::cCompFlagGrayscale | (img.has_alpha() ? pixel_format_helpers::cCompFlagAValid : 0)));
break;
}
case cConversion_To_Y: {
img.set_comp_flags(static_cast<pixel_format_helpers::component_flags>(img.get_comp_flags() | pixel_format_helpers::cCompFlagGrayscale));
break;
}
default: {
CRNLIB_ASSERT(false);
return;
}
}
for (uint y = 0; y < img.get_height(); y++) {
for (uint x = 0; x < img.get_width(); x++) {
color_quad_u8 src(img(x, y));
color_quad_u8 dst;
switch (conv_type) {
case image_utils::cConversion_To_CCxY: {
color::RGB_to_YCC(dst, src);
break;
}
case image_utils::cConversion_From_CCxY: {
color::YCC_to_RGB(dst, src);
break;
}
case image_utils::cConversion_To_xGxR: {
dst.r = 0;
dst.g = src.g;
dst.b = 0;
dst.a = src.r;
break;
}
case image_utils::cConversion_From_xGxR: {
dst.r = src.a;
dst.g = src.g;
// This is kinda iffy, we're assuming the image is a normal map here.
dst.b = regen_z(src.a, src.g);
dst.a = 255;
break;
}
case image_utils::cConversion_To_xGBR: {
dst.r = 0;
dst.g = src.g;
dst.b = src.b;
dst.a = src.r;
break;
}
case image_utils::cConversion_To_AGBR: {
dst.r = src.a;
dst.g = src.g;
dst.b = src.b;
dst.a = src.r;
break;
}
case image_utils::cConversion_From_xGBR: {
dst.r = src.a;
dst.g = src.g;
dst.b = src.b;
dst.a = 255;
break;
}
case image_utils::cConversion_From_AGBR: {
dst.r = src.a;
dst.g = src.g;
dst.b = src.b;
dst.a = src.r;
break;
}
case image_utils::cConversion_XY_to_XYZ: {
dst.r = src.r;
dst.g = src.g;
// This is kinda iffy, we're assuming the image is a normal map here.
dst.b = regen_z(src.r, src.g);
dst.a = 255;
break;
}
case image_utils::cConversion_Y_To_A: {
dst.r = src.r;
dst.g = src.g;
dst.b = src.b;
dst.a = static_cast<uint8>(src.get_luma());
break;
}
case image_utils::cConversion_Y_To_RGB: {
uint8 y = static_cast<uint8>(src.get_luma());
dst.r = y;
dst.g = y;
dst.b = y;
dst.a = src.a;
break;
}
case image_utils::cConversion_A_To_RGBA: {
dst.r = src.a;
dst.g = src.a;
dst.b = src.a;
dst.a = src.a;
break;
}
case image_utils::cConversion_To_Y: {
uint8 y = static_cast<uint8>(src.get_luma());
dst.r = y;
dst.g = y;
dst.b = y;
dst.a = src.a;
break;
}
default: {
CRNLIB_ASSERT(false);
dst = src;
break;
}
}
img(x, y) = dst;
}
}
}
image_utils::conversion_type get_conversion_type(bool cooking, pixel_format fmt) {
image_utils::conversion_type conv_type = image_utils::cConversion_Invalid;
if (cooking) {
switch (fmt) {
case PIXEL_FMT_DXT5_CCxY: {
conv_type = image_utils::cConversion_To_CCxY;
break;
}
case PIXEL_FMT_DXT5_xGxR: {
conv_type = image_utils::cConversion_To_xGxR;
break;
}
case PIXEL_FMT_DXT5_xGBR: {
conv_type = image_utils::cConversion_To_xGBR;
break;
}
case PIXEL_FMT_DXT5_AGBR: {
conv_type = image_utils::cConversion_To_AGBR;
break;
}
default:
break;
}
} else {
switch (fmt) {
case PIXEL_FMT_3DC:
case PIXEL_FMT_DXN: {
conv_type = image_utils::cConversion_XY_to_XYZ;
break;
}
case PIXEL_FMT_DXT5_CCxY: {
conv_type = image_utils::cConversion_From_CCxY;
break;
}
case PIXEL_FMT_DXT5_xGxR: {
conv_type = image_utils::cConversion_From_xGxR;
break;
}
case PIXEL_FMT_DXT5_xGBR: {
conv_type = image_utils::cConversion_From_xGBR;
break;
}
case PIXEL_FMT_DXT5_AGBR: {
conv_type = image_utils::cConversion_From_AGBR;
break;
}
default:
break;
}
}
return conv_type;
}
image_utils::conversion_type get_image_conversion_type_from_crn_format(crn_format fmt) {
switch (fmt) {
case cCRNFmtDXT5_CCxY:
return image_utils::cConversion_To_CCxY;
case cCRNFmtDXT5_xGxR:
return image_utils::cConversion_To_xGxR;
case cCRNFmtDXT5_xGBR:
return image_utils::cConversion_To_xGBR;
case cCRNFmtDXT5_AGBR:
return image_utils::cConversion_To_AGBR;
default:
break;
}
return image_utils::cConversion_Invalid;
}
double compute_std_dev(uint n, const color_quad_u8* pPixels, uint first_channel, uint num_channels) {
if (!n)
return 0.0f;
double sum = 0.0f;
double sum2 = 0.0f;
for (uint i = 0; i < n; i++) {
const color_quad_u8& cp = pPixels[i];
if (!num_channels) {
uint l = cp.get_luma();
sum += l;
sum2 += l * l;
} else {
for (uint c = 0; c < num_channels; c++) {
uint l = cp[first_channel + c];
sum += l;
sum2 += l * l;
}
}
}
double w = math::maximum(1U, num_channels) * n;
sum /= w;
sum2 /= w;
double var = sum2 - sum * sum;
var = math::maximum<double>(var, 0.0f);
return sqrt(var);
}
uint8* read_image_from_memory(const uint8* pImage, int nSize, int* pWidth, int* pHeight, int* pActualComps, int req_comps, const char* pFilename) {
*pWidth = 0;
*pHeight = 0;
*pActualComps = 0;
if ((req_comps < 1) || (req_comps > 4))
return NULL;
mipmapped_texture tex;
buffer_stream buf_stream(pImage, nSize);
buf_stream.set_name(pFilename);
data_stream_serializer serializer(buf_stream);
if (!tex.read_from_stream(serializer))
return NULL;
if (tex.is_packed()) {
if (!tex.unpack_from_dxt(true))
return NULL;
}
image_u8 img;
image_u8* pImg = tex.get_level_image(0, 0, img);
if (!pImg)
return NULL;
*pWidth = tex.get_width();
*pHeight = tex.get_height();
if (pImg->has_alpha())
*pActualComps = 4;
else if (pImg->is_grayscale())
*pActualComps = 1;
else
*pActualComps = 3;
uint8* pDst = NULL;
if (req_comps == 4) {
pDst = (uint8*)malloc(tex.get_total_pixels() * sizeof(uint32));
uint8* pSrc = (uint8*)pImg->get_ptr();
memcpy(pDst, pSrc, tex.get_total_pixels() * sizeof(uint32));
} else {
image_u8 luma_img;
if (req_comps == 1) {
luma_img = *pImg;
luma_img.convert_to_grayscale();
pImg = &luma_img;
}
pixel_packer packer(req_comps, 8);
uint32 n;
pDst = image_utils::pack_image(*pImg, packer, n);
}
return pDst;
}
} // namespace image_utils
} // namespace crnlib