Я хочу создать изображение с размытием Флойда-Стейнберга, используя определенный цвет в качестве основы. В конце процесса дизеринга пиксели, соответствующие входному цвету, будут установлены на черный цвет, в противном случае — на белый (или наоборот).
Как примеры изображений ниже
В примере изображения выше используются следующие цвета (RGB):
(255, 255, 255) белый
(0, 215, 225) светло-синий
(0, 120, 240) средний_синий
(0, 0, 120) тёмно-синий
(0, 0, 0) черный
Это из https://www.lesswrong.com/posts/a2v5Syk6gJs7HnRoq/computational-thread-art
Я попытался воспроизвести это сглаживание. Но мне не удалось добиться таких же результатов. Пример моего результата.
Цвет светло-синий:
Черный цвет :
Вот код того, что у меня есть на данный момент.
#include <opencv2/opencv.hpp>
cv::Vec3b FindClosestPaletteColor_(cv::Vec3b Color, const std::vector<cv::Vec3b>& Palette) {
int MinID = 0;
cv::Vec3b Difference = Color - Palette[0];
float MinDistance = cv::norm(Difference);
for (size_t i = 1; i < Palette.size(); i++) {
Difference = Color - Palette[i];
float Distance = cv::norm(Difference);
if (Distance < MinDistance) {
MinDistance = Distance;
MinID = i;
}
}
return Palette[MinID];
}
cv::Mat FloydSteinbergDithering_(cv::Mat InputImage, const std::vector<cv::Vec3b>& ColorPalette) {
cv::Mat Image = InputImage.clone();
cv::Mat ResultImage = Image.clone();
for (int i = 0; i < Image.rows; i++) {
for (int j = 0; j < Image.cols; j++) {
cv::Vec3b CurrentColor = Image.at<cv::Vec3b>(i, j);
cv::Vec3b NewPixelColor = FindClosestPaletteColor_(CurrentColor, ColorPalette);
ResultImage.at<cv::Vec3b>(i, j) = NewPixelColor;
for (int k = 0; k < 3; k++) {
int QuantError = (int)CurrentColor[k] - NewPixelColor[k];
if (j + 1 < Image.cols)
Image.at<cv::Vec3b>(i, j + 1)[k] = cv::saturate_cast<uchar>(Image.at<cv::Vec3b>(i, j + 1)[k] + (7 * QuantError) / 16);
if (i + 1 < Image.rows && j - 1 >= 0)
Image.at<cv::Vec3b>(i + 1, j - 1)[k] = cv::saturate_cast<uchar>(Image.at<cv::Vec3b>(i + 1, j - 1)[k] + (3 * QuantError) / 16);
if (i + 1 < Image.rows)
Image.at<cv::Vec3b>(i + 1, j)[k] = cv::saturate_cast<uchar>(Image.at<cv::Vec3b>(i + 1, j)[k] + (5 * QuantError) / 16);
if (i + 1 < Image.rows && j + 1 < Image.cols)
Image.at<cv::Vec3b>(i + 1, j + 1)[k] = cv::saturate_cast<uchar>(Image.at<cv::Vec3b>(i + 1, j + 1)[k] + (1 * QuantError) / 16);
}
}
}
return ResultImage;
}
int main()
{
cv::Mat InputImage = cv::imread("TestImage.jpg");
if (InputImage.empty()) {
std::cerr << "Error: Unable to load input image!" << std::endl;
return -1;
}
//I have used white as secondary color. But want it to only have 1 color as input in the final version. So it can work with any color
/*Black*/std::vector<cv::Vec3b> ColorPalette = { cv::Vec3b(0, 0, 0), cv::Vec3b(255, 255, 255) };
///*light Blue*/std::vector<cv::Vec3b> ColorPalette = { cv::Vec3b(225, 215, 0), cv::Vec3b(255, 255, 255) };
cv::Mat ResultImage = FloydSteinbergDithering_(InputImage, ColorPalette);
cv::imwrite("output_image.jpg", ResultImage);
cv::cvtColor(ResultImage, ResultImage, cv::COLOR_BGR2GRAY);
cv::bitwise_not(ResultImage, ResultImage);
cv::imwrite("GrayScaledImage.jpg", ResultImage);
cv::imshow("Result Image", ResultImage);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
Введите изображение
дело в диффузии ошибок в сочетании с палитрой. в каждом пикселе вы находите ближайшее значение палитры, а затем увеличиваете ошибку. любые проблемы, которые у вас могут возникнуть, могут быть связаны с добровольным программированием на C++. вы можете полностью использовать Python с numba, чтобы сделать это быстрее.
если вам нужна маска изображения для каждой записи палитры, вам необходимо сохранить изображение индексов палитры во время распространения. если вы сохраняете только полученные цвета, вам придется снова искать запись палитры для каждого пикселя.
Вы используете только черно-белое сглаживание, и, очевидно, вы получите большую синюю ошибку, распространяющуюся бесконечно. Я думаю, вам нужно применить дизеринг ко всем этим цветам, а затем на втором этапе выделить пиксели каждого цвета.
Единственная странная вещь, которую я вижу в вашем коде сглаживания, это то, что вы делаете две копии входного изображения: одну используете для распространения ошибок, а другую — в качестве выходного изображения. Вы можете сделать и то, и другое в одной копии.
Спасибо всем за ответы :) Думаю, я понимаю, о чем вы говорите. Посмотрим, смогу ли я это создать. :)
о, а еще, поскольку вы отсекаете свои ошибки, это все исказит. вам нужно работать с int16, чтобы он НЕ обрезался/насыщался. вам также необходимо, чтобы палитра содержала ВСЕ цвета, которые вы планируете использовать, а не только два.
@ChristophRackwitz У меня все работает. Опубликовано как ответ. Но не могли бы вы просмотреть код и посмотреть, видите ли вы что-то, что следует сделать по-другому? И спасибо всем, что нашли время ответить. :)
Так является ли цель иметь только один цвет в каждой позиции пикселя или их комбинацию? Может ли пиксель быть темно-синим и средне-синим одновременно?
Если вы также хотите понять и узнать больше о дизеринге Флойда Стейнберга. javidx9 снял на YouTube хорошее видео, объясняющее эту тему. https://thewikihow.com/video_lseR6ZguBNY
Рабочее решение.
cv::Scalar FindNearestColor(const cv::Scalar& Color, const std::vector<cv::Scalar>& Palettes)
{
double MinimumDistance = DBL_MAX;
cv::Scalar NearestColor;
for (const auto& CurrentColor : Palettes)
{
double Distance = norm(Color - CurrentColor);
if (Distance < MinimumDistance)
{
MinimumDistance = Distance;
NearestColor = CurrentColor;
}
}
return NearestColor;
}
void FloydSteinbergDithering(cv::Mat& Image, const std::vector<cv::Scalar>& Palettes, optional<vector<cv::Mat>>* Masks = nullptr)
{
std::vector<cv::Mat> TempMasks;
if (Masks != nullptr && Masks->has_value())
{
TempMasks.resize(Palettes.size());
for (size_t i = 0; i < Palettes.size(); ++i)
{
TempMasks[i] = cv::Mat::zeros(Image.size(), CV_8UC1);
}
}
for (int y = 0; y < Image.rows; ++y)
{
for (int x = 0; x < Image.cols; ++x)
{
cv::Scalar CurrentColor(Image.at<cv::Vec3b>(y, x));
cv::Scalar NewColor = FindNearestColor(CurrentColor, Palettes);
Image.at<cv::Vec3b>(y, x) = cv::Vec3b(NewColor[0], NewColor[1], NewColor[2]);
if (Masks != nullptr && Masks->has_value())
{
for (size_t i = 0; i < Palettes.size(); ++i)
{
if (NewColor == Palettes[i])
{
TempMasks[i].at<uchar>(y, x) = 255;
}
}
}
cv::Scalar QuantizationError = CurrentColor - NewColor;
auto AddQuantizationError = [&](int y, int x, double Factor)
{
if (x >= 0 && x < Image.cols && y >= 0 && y < Image.rows)
{
cv::Scalar Pixel(Image.at<cv::Vec3b>(y, x));
for (int i = 0; i < 3; ++i)
{
Pixel[i] += QuantizationError[i] * Factor;
Pixel[i] = clamp(Pixel[i], 0.0, 255.0);
}
Image.at<cv::Vec3b>(y, x) = cv::Vec3b(Pixel[0], Pixel[1], Pixel[2]);
}
};
AddQuantizationError(y, x + 1, 7.0 / 16);
AddQuantizationError(y + 1, x - 1, 3.0 / 16);
AddQuantizationError(y + 1, x, 5.0 / 16);
AddQuantizationError(y + 1, x + 1, 1.0 / 16);
}
}
if (Masks != nullptr && Masks->has_value())
{
Masks->emplace(std::move(TempMasks));
}
}
int main()
{
vector<cv::Scalar> ColorPalette =
{
cv::Scalar(0, 0, 0), // Black
cv::Scalar(120, 0, 0), // Dark Blue
cv::Scalar(240, 120, 0), // Mid Blue
cv::Scalar(225, 215, 0), // Light Blue
cv::Scalar(255, 255, 255) // White
};
cv::Mat Image = cv::imread("Image.jpg", cv::IMREAD_COLOR);
if (Image.empty())
{
std::cerr << "Error: Could not open or find the image!" << std::endl;
return -1;
}
// Optional vector to store masks
optional<vector<cv::Mat>> Masks = vector<cv::Mat>();
FloydSteinbergDithering(Image, ColorPalette, &Masks);
if (Masks.has_value())
{
for (size_t k = 0; k < Masks->size(); ++k)
{
std::string filename = "Mask_Color_" + std::to_string(k) + ".png";
if (!cv::imwrite(filename, Masks->at(k)))
{
std::cerr << "Error writing mask to file: " << filename << std::endl;
}
}
}
cv::imshow("Dithered Image", Image);
cv::imwrite("Dithered_Image.jpg", Image);
cv::waitKey(0);
return 0;
}
Вы получите лучшие результаты, если не будете ограничивать значения пикселей при распространении ошибки. Зажимание означает, что вы стираете информацию. Поэтому вам нужно преобразовать изображение в более крупный тип (например, 16-битное целое число со знаком или число с плавающей запятой, что всегда удобнее). Выполняя этот приведение, вы можете одновременно добавить рамку толщиной в 1 пиксель к левой, правой и нижней сторонам изображения. Используя эту границу, вы можете удалить тесты на доступ за пределами границ, что значительно ускорит работу программы.
Кроме того, ваш FindNearestColor()
должен возвращать индекс палитры, а не цвет. Потому что сразу после этого вы ищете индекс, снова перебирая палитру, что кажется глупым.
Если я не сделаю это «clamp(Pixel[i], 0.0, 255.0);» Я получаю некоторые артефакты Werd? Скаляр использует двойной? Так разве это не должно быть правильно?
Image
, в котором вы пишете новые значения, — это uint8 (если я не пропущу приведение, я его не вижу). Поэтому, если вы не зафиксируете свои значения, запись их в изображение приведет к переполнению (что приводит к переносу, 256 будет записано как 0). Это явно плохо, лучше зажать, чем перелить. Но если вы приведете свое изображение к int16 или к float, распространяемая ошибка не должна переполняться, и вам больше не понадобится приведение.
я попробовал преобразовать полное изображение в 16 бит. Но я вернулся к этому, так как получал те же странные артефакты, если не зажимал. Но я также думаю, что добавление зажима было странным. Но при просмотре видео и кода из видео на YouTube, опубликованного javidx9. Он также использовал зажим. Итак, я думал, что так и должно было быть, даже если я думаю, что описание алгоритма не имеет смысла?
«Кто-то еще тоже использовал зажим» — не совсем хороший аргумент. Это не значит, что возможность разместить видео на YouTube не является хорошим фактором для понимания алгоритмов обработки изображений. Какие странные артефакты вы получили?
Я изменил ваш код так, чтобы он делал OriginalImage.convertTo(Image, CV_16S);
вверху FloydSteinbergDithering()
(при этом первый входной аргумент был переименован в OriginalImage
, а Image.convertTo(OriginalImage, CV_8U);
в конце этой функции. Для этого потребовалось изменить все cv::Vec3b
на cv::Vec3s
. Затем я мог бы просто удалить строку clamp
. Есть Я признаю, что разница в результате не такая уж большая, но это должно быть более правильно.
@CrisLuengo Именно это я и сделал, прежде чем получил приведенный выше код. Но если вы попробуете рендерить только черно-белое изображение. Вы получаете артефакты :/
Верно, это потому, что вы не можете сглаживать цветное изображение, используя только черно-белое. ‼Я уже это комментировала .
@CrisLuengo Что-то не так, если ты не зажимаешь. При использовании 5 цветов изображение выглядит одинаково. Но при четырех цветах или меньше зажатое изображение выглядит лучше и не имеет странных артефактов. Вот изображение с 4 цветами без зажима = imgur.com/wB8EsIn . То же самое с зажимом = imgur.com/Gixq9hJ . А если мы используем 3 цвета. Без зажима = imgur.com/61N2VwA , с зажимом = imgur.com/o6FI078.
Я записал свои мысли о вашей проблеме с артефактами в качестве ответа, потому что комментарий — не лучшее место для этого. У меня нет для вас кода, может быть, со временем у меня будет время его написать.
Я добавил простой пример, демонстрирующий чрезмерное распространение ошибок и его эффект.
Вот мои мысли о сглаживании цветов и зажиме.
При дизеринге мы создаём точки разных цветов, на расстоянии эти цвета смешиваются и мы воспринимаем патч как среднее из цветов точек. Одним из ключевых выводов здесь является то, что вы не можете усреднить два цвета, чтобы получить третий цвет, если только этот третий цвет не находится на линии между двумя цветами в цветовом пространстве (при условии линейного RGB, третий цвет будет где-то на прямой линии, в других случаях цветовых пространствах линия не обязательно должна быть прямой). При дизеринге трех цветов мы имеем треугольник цветов на 2D-плоскости, который можно получить путем их смешивания. В общем, выпуклая оболочка набора цветов (палитры), используемая для размывания, представляет все цвета, которые мы можем создать, смешивая цвета в палитре.
Если входное изображение имеет цвета за пределами этой выпуклой оболочки, то эти цвета невозможно воспроизвести. Процесс сглаживания приведет к ошибке, которая будет распространяться бесконечно, создавая артефакты, которые могут сохраняться в течение длительного времени. Представьте себе два цвета, смешанные с серым и черным. Изображение имеет белую область. Алгоритм сглаживания заполнит эту область серым цветом и аккумулирует разницу между этим серым и белым, в результате чего область справа и снизу от этого объекта будет намного ярче, чем должна быть, пока разница не будет учтена. .
То же самое произошло бы с любым другим цветом за пределами выпуклой оболочки палитры.
Если палитра не охватывает все цвета изображения, если в изображении есть цвета, которые не могут быть образованы комбинациями этих цветов, нам необходимо избавиться от этих цветов перед применением сглаживания.
Обходным решением является ограничение накопленной ошибки. Это позволяет избежать слишком большого распространения накопленной ошибки и предотвращает появление огромных артефактов из-за непредставимых цветов. Но он также изменит цвета, которые можно представить, и поэтому не сможет создать идеальные цвета.
Лучшее решение, конечно, — обеспечить, чтобы палитра, которую мы используем для дизеринга, всегда охватывала всю гамму, используемую в изображении. Вместо того, чтобы выбирать цвета, которые часто встречаются на изображении, мы выбираем цвета, которые образуют крайности цветов изображения. Включение насыщенного красного, желтого, зеленого, голубого, синего и пурпурного, а также чистого белого и черного цвета позволит добиться этого для любого изображения RGB.
Если выбор цветов фиксирован, как в данном случае, то лучшим решением будет предварительная обработка входного изображения для удаления цветов за пределами выпуклой оболочки палитры и замены их цветами внутри. Это не сложный процесс, но чтобы все сделать правильно, нужно немного подумать.
Зажим — очень простое и дешевое решение, но оно неоптимально.
Быстрый пример. Я использую изображение с измененными цветами, чтобы сделать их ярче, чтобы эффект был более заметен.
Давайте сначала размываем, используя только черный, белый, красный, зеленый и синий. Обратите внимание, что вы не можете получить ярко-желтый цвет, используя эти цвета. Если вы усредните красный и зеленый, вы получите что-то более похожее на оранжевый или коричневый, чем на желтый. В процессе сглаживания добавляется белый цвет, чтобы попытаться сделать объекты ярче, но он по-прежнему накапливает большую ошибку, которая распространяется вниз и вправо, изменяя темные буквы и темные линии:
Если мы добавим в палитру чистый желтый, ошибки не накапливаются, желтый цвет на изображении может быть образован путем смешивания цветов из нашей палитры:
Когда мы сглаживаем без использования желтого цвета и ограничиваем ошибку так, чтобы она находилась в диапазоне [0 255] для каждого канала, ошибка не накапливается так сильно и поэтому не распространяется так далеко, но все же она все еще существует, и мы все еще можем наблюдать, как диагональные линии в левом верхнем и правом нижнем углу знака, а также буквы кажутся тоньше, чем должны быть, и имеют зеленый ореол. В правом нижнем углу знака также есть яркая линия:
Если бы мы обрезали значения входных пикселей так, чтобы они находились внутри выпуклой оболочки палитры, не было бы накопления ошибок.
Я давно научился следить за тем, чтобы моя палитра всегда включала 8 углов цветового куба.
Спасибо за этот хороший ответ и спасибо, что нашли время помочь :) Продолжайте в том же духе :)
Мне нравится олень. Однако вы забыли задать вопрос.