Как обработать гамма-коррекцию и цвета в файле PNG? - PullRequest
3 голосов
/ 14 июня 2019

Я работаю над программой для рисования изображений, и я действительно запутался в цветовых пространствах.Чем больше я читаю о гамме, тем меньше знаю ( это не сильно помогло).

Внутри программа рисования будет сохранять изображения как 8-битный sRGB, а затем преобразовывать в 16-битовый линейный для операций компоновки и фильтрации.Возможно, мне следует сохранить 16-разрядный линейный код и затем преобразовать его в 8-разрядный sRGB при экспорте?Я хотел бы получить несколько советов по этому вопросу.В настоящее время я не совсем уверен, в каком цветовом пространстве пиксели RGB интерпретируются как Qt!

Насколько я понимаю, sRGB близок к цветовому пространству, используемому большинством мониторов, поэтому преобразование из sRGB в цветовое пространство монитора нене сильно меняйте данные изображения.Глядя на данные sRGB, как если бы они находились в правильном цветовом пространстве, вероятно, будет довольно близко.

Цели

Программа рисования отображает изображения sRGB.Я хочу сохранить эти изображения sRGB в файл PNG.Когда я открываю этот файл PNG (с предварительным просмотром на Mac) на том же компьютере, на котором было создано изображение, он должен выглядеть точно так же, как то, что видит художник в программе для рисования, и иметь те же значения цвета (проверено с помощью Digital Colorметр).Разные мониторы и разные операционные системы используют разные цветовые пространства.Эти цветовые пространства могут не соответствовать цветовому пространству системы, используемой для создания изображения.Когда я открываю PNG на другом мониторе или даже на другом компьютере, изображение должно выглядеть максимально похожим на оригинал, но, вероятно, иметь другие цветовые значения.

Эксперименты

Кажется, что программа рисованияправильно отображать изображения (я думаю).Проблема в PNG.Я использую Qt и сохраняю изображение с помощью QImage::save.Я хочу использовать libPNG, если мне нужно больше контроля.

Для тестирования я рисую изображение 5x5 со значениями цвета 0 63 127 191 255 для красного и зеленого.

Скриншоты программы рисования

Когда я сэмплирую изображение, отображаемое программой для рисования, с помощью Digital Color Meter, значения пикселей остаются неизменными.Пиксель в 3,3, выбранный с помощью DCM, равен 191 191 0, как и должно быть.Существует четкий контраст между каждым из пикселей.

Когда я делаю скриншот, значения пикселей в файле скриншота отличаются.Пиксель в 3,3, выбранный с помощью DCM при просмотре в режиме предварительного просмотра, равен 192 191 0.Пиксель в 3,3, сохраненный в файле, равен 140 126 4.Я должен отметить, что файл снимка экрана содержит фрагмент sRGB с целью восприятия рендеринга.

Когда я обрезаю снимок экрана до изображения 5x5 с помощью предварительного просмотра, блок sRGB заменяется на gAMA иcHRM чанки, которые соответствуют sRGB (я использовал 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 при просмотре в режиме предварительного просмотра.

Ниже приведен обрезанный снимок экрана (он очень маленький!).

Cropped screenshot

Кажется, что лучший способ сохранить чертеж - это сделать скриншот, но даже он не идеален.

Тестовые программы

Программа 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 output

Программа 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 output

Я действительно рад, что 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 при открытии с предварительным просмотром и дискретизации с помощью цифрового цветометра. Звучит как простая задача, но это определенно не так. В тестовой программе libpng есть некоторый закомментированный код, который я пробовал. Ни один из них не дает желаемого результата. Что действительно расстраивает, так это то, что Chrome и Preview дают разные результаты. Я понятия не имею, что является правильным или наиболее близким к правильному или что означает «правильный».

Чем больше я читаю об этом, тем больше я думаю, что должен просто согласиться на "О, хорошо, это заметно неправильно, но я думаю, что это достаточно хорошо * вздох *" .

Просмотр экспериментов

Я написал две одинаковые программы для просмотра PNG. Они оба дают желаемый результат (выборка с DCM возвращает 0 63 127 191 255).

Qt Viewer

#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 Viewer

#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 - своего рода хлопот. Мое приложение предназначено для создания спрайтов и текстур для игр, поэтому, если оно работает с API рендеринга SDL2 и OpenGL, тогда, я думаю, все в порядке. Я еще не проводил никаких экспериментов с внешними мониторами. Помещение фрагментов sRGB, gAMA и cHRM в PNG не влияет на вывод любого из средств просмотра. Я не уверен, хорошо это или плохо. На первый взгляд, похоже, что моя проблема только что исчезла. Я все еще хотел бы, чтобы кто-нибудь объяснил мои наблюдения.

Утилита ColorSync

Я обнаружил новый инструмент, и теперь я думаю, что знаю, что происходит ...

enter image description here

...