我如何处理PNG文件中的伽玛校正和颜色?

问题描述 投票:3回答:1

我正在开发一个图像绘制程序,我对色彩空间感到非常困惑。我越了解伽玛,就越了解(this并没有太大帮助)。

内部,绘图程序会将图像存储为8位sRGB,然后转换为16位线性以进行合成和过滤操作。也许我应该存储16位线性,然后在导出时转换为8位sRGB?我很乐意为此提供一些建议。当前,我不太确定Qt将RGB像素解释为哪种颜色空间!

据我所知,sRGB接近大多数显示器使用的色彩空间,因此从sRGB转换为显示器色彩空间不会对图像数据造成太大改变。以正确的色彩空间查看sRGB数据可能非常接近。

目标

绘图程序显示sRGB图像。我想将这些sRGB图像保存到PNG文件。当我在创建图像的同一台计算机上打开此PNG文件(在Mac上为Preview)时,它的外观应与画家在绘图程序中看到的外观完全相同,并且具有相同的颜色值(已通过Digital Color检查)仪表)。不同的监视器和不同的操作系统使用不同的色彩空间。这些色彩空间可能不“适合”用于创建图像的系统的色彩空间。当我在其他显示器甚至计算机上打开PNG时,图像应看起来与原始图像尽可能相似,但可能具有不同的颜色值。

实验

绘图程序似乎正确显示了图像(我认为)。问题是PNG。我正在使用Qt,并使用QImage::save保存图像。如果需要更多控制,我愿意使用libPNG。

为了测试,我绘制了一个5x5图像,红色和绿色的颜色值为0 63 127 191 255

绘图程序屏幕截图

当我使用数字色度计对绘图程序渲染的图像进行采样时,像素值保持不变。用DCM采样的3,3处的像素应为191 191 0。每个像素之间有明显的对比度。

[当我拍摄屏幕快照时,屏幕快照文件中的像素值不同。在“预览”中查看时,用DCM采样的3,3处的像素为192 191 0。文件中存储的3,3处的像素为140 126 4。我应该注意,屏幕截图文件有一个sRGB块,带有渲染意图。

[当我使用预览将屏幕截图裁剪为5x5图像时,sRGB块被替换为与sRGB对应的gAMAcHRM块(我使用了pngcheck)。

pngcheck

两个版本在文件中存储的像素值相同。在“预览”中查看时,使用DCM采样时它们的像素值也相同。

下面是裁剪后的屏幕截图(非常小!)。

gAMA 0.45455 cHRM White x = 0.3127 y = 0.329, Red x = 0.64 y = 0.33 Green x = 0.3 y = 0.6, Blue x = 0.15 y = 0.06

保存图形的最佳方法似乎是截取屏幕快照,但即使那样也不完美。

测试程序

Qt程序

Cropped screenshot

该程序产生与libpng程序相同的输出,只是Qt添加了#include <QtGui/qimage.h> int main() { QImage image{5, 5, QImage::Format_RGB32}; const int values[5] = {0, 63, 127, 191, 255}; for (int r = 0; r != 5; ++r) { for (int g = 0; g != 5; ++g) { image.setPixel(g, r, qRgb(values[r], values[g], 0)); } } image.save("qt.png"); } 块。输出看起来与所需的输出相似,但是像素之间的对比度较小,并且像素值明显不同。

pHYs

Libpng程序

Qt output

正如我在上一节中所说,输出类似于所需的输出,但是对比度降低。在“预览”中查看时,用DCM采样的#include <cmath> #include <iostream> #include <libpng16/png.h> png_byte srgb_lut[256]; void initLut(const double exponent) { for (int i = 0; i != 256; ++i) { srgb_lut[i] = std::round(std::pow(i / 255.0, exponent) * 255.0); } } int main() { std::FILE *file = std::fopen("libpng.png", "wb"); if (!file) { std::cerr << "Failed to open file\n"; return 1; } png_structp pngPtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!pngPtr) { std::fclose(file); std::cout << "Failed to initialize png write struct\n"; return 1; } png_infop infoPtr = png_create_info_struct(pngPtr); if (!infoPtr) { png_destroy_write_struct(&pngPtr, nullptr); std::fclose(file); std::cout << "Failed to initialize png info struct\n"; return 1; } if (setjmp(png_jmpbuf(pngPtr))) { png_destroy_write_struct(&pngPtr, &infoPtr); std::fclose(file); std::cout << "Failed to set jmp buf\n"; return 1; } png_init_io(pngPtr, file); png_set_IHDR( pngPtr, infoPtr, 5, 5, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT ); //png_set_gAMA_fixed(pngPtr, infoPtr, 100000); //png_set_sRGB_gAMA_and_cHRM(pngPtr, infoPtr, PNG_sRGB_INTENT_PERCEPTUAL); //png_set_sRGB(pngPtr, infoPtr, PNG_sRGB_INTENT_PERCEPTUAL); //initLut(2.2); //initLut(1.0/2.2); initLut(1.0); png_bytep rows[5]; png_color imageData[5][5]; const png_byte values[5] = {0, 63, 127, 191, 255}; for (int r = 0; r != 5; ++r) { for (int g = 0; g != 5; ++g) { imageData[r][g] = {srgb_lut[values[r]], srgb_lut[values[g]], 0}; } rows[r] = reinterpret_cast<png_bytep>(&imageData[r][0]); } png_set_rows(pngPtr, infoPtr, rows); png_write_png(pngPtr, infoPtr, PNG_TRANSFORM_IDENTITY, nullptr); png_destroy_write_struct(&pngPtr, &infoPtr); std::fclose(file); } 处的像素为3,3,相距很远。

186 198 0

我很高兴Qt产生与libpng相同的输出,即使输出不是我想要的。这意味着我可以根据需要切换到libpng。

示例程序

此程序从PNG采样像素。我很确定它不会进行任何色彩空间转换,只是给我存储在文件中的值。

Libpng output

我真正想做的事

我需要修改libpng测试程序,以便在使用Preview打开并使用Digital Color Meter进行采样时,像素值为#include <iostream> #include <libpng16/png.h> int main(int argc, char **argv) { if (argc != 4) { std::cout << "sample <file> <x> <y>\n"; return 1; } const int x = std::atoi(argv[2]); const int y = std::atoi(argv[3]); if (x < 0 || y < 0) { std::cerr << "Coordinates out of range\n"; return 1; } std::FILE *file = std::fopen(argv[1], "rb"); if (!file) { std::cerr << "Failed to open file\n"; return 1; } png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!pngPtr) { std::fclose(file); std::cerr << "Failed to initialize read struct\n"; return 1; } png_infop infoPtr = png_create_info_struct(pngPtr); if (!infoPtr) { png_destroy_read_struct(&pngPtr, nullptr, nullptr); std::fclose(file); std::cerr << "Failed to initialize info struct\n"; return 1; } if (setjmp(png_jmpbuf(pngPtr))) { // Pssh, who needs exceptions anyway? png_destroy_read_struct(&pngPtr, &infoPtr, nullptr); std::fclose(file); std::cerr << "Failed to set jmp buf\n"; return 1; } png_init_io(pngPtr, file); // Does this prevent libpng from changing the color values? png_set_gamma(pngPtr, PNG_GAMMA_LINEAR, PNG_GAMMA_LINEAR); png_read_png(pngPtr, infoPtr, PNG_TRANSFORM_STRIP_ALPHA, nullptr); png_bytepp rows = png_get_rows(pngPtr, infoPtr); const int width = png_get_image_width(pngPtr, infoPtr); const int height = png_get_image_height(pngPtr, infoPtr); if (x >= width || y >= height) { // Pssh, who needs RAII anyway? png_destroy_read_struct(&pngPtr, &infoPtr, nullptr); std::fclose(file); std::cerr << "Coordinates out of range\n"; return 1; } png_bytep row = rows[y]; for (int c = 0; c != 3; ++c) { std::cout << static_cast<int>(row[x * 3 + c]) << ' '; } std::cout << '\n'; png_destroy_read_struct(&pngPtr, &infoPtr, nullptr); std::fclose(file); } 。听起来像一个简单的任务,但绝对不是。我尝试过的东西在libpng测试程序中有一些注释的代码。它们都不产生期望的输出。真正令人沮丧的是,Chrome和Preview产生了不同的结果。我不知道哪个是正确的或最接近正确的,甚至都不是什么“正确”的意思。

我读得越多,我就越应该满足于“”哦,这是明显的错误,但我想它足够*叹气*“]]。]]

查看实验

我编写了两个相同的程序来查看PNG。它们都产生所需的输出(使用DCM采样返回0 63 127 191 255)。

Qt查看器
0 63 127 191 255

SDL2 Libpng查看器

#include <iostream>
#include <QtWidgets/qlabel.h>
#include <QtWidgets/qmainwindow.h>
#include <QtWidgets/qapplication.h>

int main(int argc, char **argv) {
  if (argc != 2) {
    std::cerr << "qt_render <file>\n";
    return EXIT_FAILURE;
  }
  QImage image{argv[1]};
  if (image.isNull()) {
    std::cerr << "Failed to load image\n";
    return EXIT_FAILURE;
  }
  image = image.scaled(image.size() * 64);

  QApplication app{argc, argv};
  QMainWindow window;
  window.setWindowTitle(argv[1]);
  window.setFixedSize(image.size());
  QLabel label{&window};
  QPixmap pixmap;
  if (!pixmap.convertFromImage(image)) {
    std::cerr << "Failed to convert surface to texture\n";
    return EXIT_FAILURE;
  }
  label.setPixmap(pixmap);
  label.setFixedSize(image.size());
  window.show();

  return app.exec();
}

我很想编写一个SDL2,OpenGL,Libpng查看器,但是OpenGL有点麻烦。我的应用程序旨在为游戏制作精灵和纹理,因此,如果它与SDL2渲染API和OpenGL兼容,那么我想一切都很好。我尚未使用外部显示器进行任何实验。将#include <iostream> #include <SDL2/SDL.h> #include <libpng16/png.h> template <typename... Args> [[noreturn]] void fatalError(Args &&... args) { (std::cerr << ... << args) << '\n'; throw std::exception{}; } void checkErr(const int errorCode) { if (errorCode != 0) { fatalError("Error: ", SDL_GetError()); } } template <typename T> T *checkNull(T *ptr) { if (ptr == nullptr) { fatalError("Error: ", SDL_GetError()); } else { return ptr; } } struct FileCloser { void operator()(std::FILE *file) const noexcept { std::fclose(file); } }; using File = std::unique_ptr<std::FILE, FileCloser>; File openFile(const char *path, const char *mode) { std::FILE *file = std::fopen(path, mode); if (!file) { fatalError("Failed to open file"); } else { return File{file}; } } struct WindowDestroyer { void operator()(SDL_Window *window) const noexcept { SDL_DestroyWindow(window); } }; using Window = std::unique_ptr<SDL_Window, WindowDestroyer>; struct SurfaceDestroyer { void operator()(SDL_Surface *surface) const noexcept { SDL_FreeSurface(surface); } }; using Surface = std::unique_ptr<SDL_Surface, SurfaceDestroyer>; struct TextureDestroyer { void operator()(SDL_Texture *texture) const noexcept { SDL_DestroyTexture(texture); } }; using Texture = std::unique_ptr<SDL_Texture, TextureDestroyer>; struct RendererDestroyer { void operator()(SDL_Renderer *renderer) const noexcept { SDL_DestroyRenderer(renderer); } }; using Renderer = std::unique_ptr<SDL_Renderer, RendererDestroyer>; class SurfaceLock { public: explicit SurfaceLock(SDL_Surface *surface) : surface{surface} { SDL_LockSurface(surface); } ~SurfaceLock() { SDL_UnlockSurface(surface); } private: SDL_Surface *surface; }; Surface createSurface(png_structp pngPtr, png_infop infoPtr) { const png_bytepp rows = png_get_rows(pngPtr, infoPtr); const int width = png_get_image_width(pngPtr, infoPtr); const int height = png_get_image_height(pngPtr, infoPtr); Surface surface = Surface{checkNull(SDL_CreateRGBSurfaceWithFormat( 0, width, height, 24, SDL_PIXELFORMAT_RGB24 ))}; { SurfaceLock lock{surface.get()}; for (int y = 0; y != height; ++y) { uint8_t *dst = static_cast<uint8_t *>(surface->pixels); dst += y * surface->pitch; std::memcpy(dst, rows[y], width * 3); } } return surface; } void doMain(int argc, char **argv) { if (argc != 2) { fatalError("sdl_render <file>"); } File file = openFile(argv[1], "rb"); png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!pngPtr) { fatalError("Failed to initialize read struct\n"); } png_infop infoPtr = png_create_info_struct(pngPtr); if (!infoPtr) { png_destroy_read_struct(&pngPtr, nullptr, nullptr); fatalError("Failed to initialize info struct\n"); } if (setjmp(png_jmpbuf(pngPtr))) { png_destroy_read_struct(&pngPtr, &infoPtr, nullptr); fatalError("Failed to set jmp buf"); } png_init_io(pngPtr, file.get()); png_read_png(pngPtr, infoPtr, PNG_TRANSFORM_STRIP_ALPHA, nullptr); Surface surface = createSurface(pngPtr, infoPtr); png_destroy_read_struct(&pngPtr, &infoPtr, nullptr); checkErr(SDL_Init(SDL_INIT_VIDEO)); std::atexit(SDL_Quit); Window window = Window{checkNull(SDL_CreateWindow( argv[1], SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, surface->w * 64, surface->h * 64, 0 ))}; Renderer renderer = Renderer{checkNull(SDL_CreateRenderer( window.get(), -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC ))}; Texture texture = Texture{checkNull(SDL_CreateTextureFromSurface( renderer.get(), surface.get() ))}; surface.reset(); while (true) { SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { return; } } SDL_RenderCopy(renderer.get(), texture.get(), nullptr, nullptr); SDL_RenderPresent(renderer.get()); } } int main(int argc, char **argv) { try { doMain(argc, argv); } catch (...) { return EXIT_FAILURE; } return EXIT_SUCCESS; } sRGBgAMA块放入PNG对任何一个查看器的输出都没有任何影响。我不确定这是好是坏。从表面上看,我的问题似乎已经消失了。 我仍然希望有人能解释我的看法。

ColorSync实用程序

我发现了一个新工具,现在我想我知道发生了什么事...

cHRM

我正在开发一个图像绘制程序,我对色彩空间感到非常困惑。我对gamma的了解越多,对我的了解就越少(这没有多大帮助)。在内部,绘图程序将...

c++ qt colors png libpng
1个回答
0
投票

这可以用enter image description here来完成。

仅供参考,不同的照片编辑程序对PNG元数据的管理方式也不同,因此请注意这一点。例如,Mac的Preview应用会自动将sRGB IEC61966-2.1 color profile

© www.soinside.com 2019 - 2024. All rights reserved.