首页 > 解决方案 > 如何处理 PNG 文件中的伽马校正和颜色?

问题描述

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

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

据我了解,sRGB 接近大多数显示器使用的色彩空间,因此从 sRGB 转换为显示器色彩空间不会对图像数据产生太大影响。将 sRGB 数据视为在正确的色彩空间中可能会非常接近。

目标

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

实验

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

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

绘图程序截图

当我用数字测色仪对绘图程序渲染的图像进行采样时,像素值没有变化。3,3使用 DCM 采样的像素191 191 0应如此。每个像素之间有明显的对比。

当我截图时,截图文件中的像素值是不同的。3,3在预览中查看时使用 DCM 采样的像素为192 191 0. 3,3存储在文件中的像素是140 126 4. 我应该注意到屏幕截图文件有一个sRGB带有渲染意图感知的块。

当我使用预览将屏幕截图裁剪为 5x5 图像时,该sRGB块被替换为对应于 sRGB的gAMA和块(我使用)。cHRMpngcheck

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

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

下面是裁剪后的屏幕截图(它真的很小!)。

裁剪的屏幕截图

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

测试程序

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");
}

该程序产生与 libpng 程序相同的输出,只是 Qt 添加了一个pHYs块。输出看起来与所需输出相似,但像素之间的对比度较小,并且像素值明显偏离。

Qt 输出

Libpng程序

#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在预览中查看时使用 DCM 采样的像素186 198 0是偏离的。

Libpng 输出

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

示例程序

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

#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 测试程序,以便在0 63 127 191 255使用 Preview 打开并使用 Digital Color Meter 采样时像素值。听起来像是一项简单的任务,但绝对不是。我尝试过的东西的 libpng 测试程序中有一些注释代码。它们都没有产生所需的输出。真正令人沮丧的是 Chrome 和 Preview 会产生不同的结果。我不知道哪个是正确的或最接近正确的,或者“正确”甚至意味着什么。

我读得越多,我就越觉得我应该满足于“哦,好吧,这显然是错误的,但我想这已经足够了 *sigh*”

查看实验

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

Qt 查看器

#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 Libpng 查看器

#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;
}

我很想写一个 SDL2、OpenGL、Libpng 查看器来确定,但 OpenGL 有点麻烦。我的应用程序旨在为游戏制作精灵和纹理,所以如果它与 SDL2 渲染 API 和 OpenGL 一起使用,那么我想一切都很好。我还没有用外接显示器做过任何实验。将sRGB,gAMAcHRM块放入 PNG 对任一查看器的输出都没有任何影响。我不确定这是好是坏。从表面上看,我的问题似乎已经消失了。我仍然希望有人解释我的观察。

颜色同步实用程序

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

在此处输入图像描述

标签: c++qtcolorspnglibpng

解决方案


这可以通过ImageMagick来完成。

仅供参考,不同的照片编辑程序以不同的方式管理 PNG 元数据,因此请注意这一点。例如,Mac 的 Preview 应用程序会自动将 sRGB IEC61966-2.1颜色配置文件附加到没有现有颜色配置文件的已编辑 PNG 文件。此行为不仅会更改颜色,还会影响文件大小。

为了进行图像文件比较,我使用以下 ImageMagick 命令(针对每个文件):

magick identify -verbose insert/image/filepath/here.png

此外,如果您想去除所有 PNG 元数据,请使用以下 ImageMagick 命令:

convert -strip insert/original/image/filepath/here.png insert/NEW/image/filepath/here2.png

推荐阅读