【问题标题】:How do I handle gamma correction and colors in a PNG file?如何处理 PNG 文件中的伽马校正和颜色?
【发布时间】:2019-10-28 16:09:22
【问题描述】:

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

在内部,绘图程序会将图像存储为 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 表示红色和绿色。

绘图程序截图

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

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

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

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

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

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

正如我在上一节中所说,输出与所需输出相似,但对比度有所降低。在预览中查看时,使用 DCM 采样的3,3 处的像素是186 198 0,这相差甚远。

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

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

查看实验

我编写了两个相同的程序来查看 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 一起使用,那么我想一切都很好。我还没有用外接显示器做过任何实验。将sRGBgAMAcHRM 块放入PNG 不会对任一查看器的输出产生任何影响。我不确定这是好是坏。从表面上看,我的问题似乎已经消失了。 我仍然希望有人解释我的观察结果。

色彩同步实用程序

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

【问题讨论】:

  • 我不确定您对 Chrome 或 Preview 的信任程度。我父亲的爱好是在 PC 上进行数码摄影和后期处理。 (我负责技术支持 - 叹息。)有一天我们注意到 JPEG 图像在 DxO 与 Windows 资源管理器预览中看起来略有不同。 (他使用 Windows 图像视图进行 dia 显示,我怀疑预览和图像查看器共享相同的代码。)我口吃的解释:对 gamma corr 的不同考虑。值。
  • ...不过,我不确定这是否只是一种视错觉。 DxO 有一个深灰色的用户界面(我认为这是有充分理由的),其中预览遵循默认的 Windows 配色方案,即在这种情况下为白底黑字。 TLDR:色彩正确的呈现在我看来就像一个黑魔法(我读到的大多数东西都支持这种感觉)。所以,你的最后一个选项(你最后一句话)在我看来值得考虑...... ;-)

标签: c++ qt colors png libpng


【解决方案1】:

这可以通过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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-12-04
    • 1970-01-01
    • 2013-12-16
    • 1970-01-01
    • 2022-12-10
    • 2021-08-14
    • 1970-01-01
    • 2011-09-28
    相关资源
    最近更新 更多