基于指定颜色的Floyd-Steinberg抖动灰度图像生成问题求助
问题说明
我希望基于指定颜色生成Floyd-Steinberg抖动图像,抖动处理完成后,匹配输入颜色的像素设为黑色,其余设为白色(或反之),目标效果参考使用以下RGB色值的示例:(255,255,255)白色、(0,215,225)浅蓝、(0,120,240)中蓝、(0,0,120)深蓝、(0,0,0)黑色。
我尝试复现该效果但未成功,当前实现的效果如下:
- 浅蓝效果:

- 黑色效果:

输入图像:
当前实现代码如下:
#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; }
问题分析与修复
当前代码存在三个核心问题:
- 颜色空间错误:OpenCV默认用BGR存储图像,设置浅蓝调色板时直接用
cv::Vec3b(225,215,0)会导致颜色匹配逻辑混乱,因为目标色是RGB格式的(0,215,225)。 - 后处理逻辑偏离需求:先做双色调抖动再转灰度取反,和“匹配指定色的像素设为黑/白”的目标逻辑不符。
- 调色板设计不合理:目标是基于单指定色生成抖动再映射到黑白,而非直接用指定色和白色做抖动。
修改后的代码
#include <opencv2/opencv.hpp> #include <cmath> // 计算当前像素与目标颜色的相似度(RGB空间) float ColorSimilarity(const cv::Vec3b& pixel, const cv::Vec3b& targetRGB) { // 转换OpenCV的BGR到RGB cv::Vec3b pixelRGB(pixel[2], pixel[1], pixel[0]); int dr = pixelRGB[0] - targetRGB[0]; int dg = pixelRGB[1] - targetRGB[1]; int db = pixelRGB[2] - targetRGB[2]; // 返回欧氏距离的倒数(值越大越相似) return 1.0f / (1.0f + sqrt(dr*dr + dg*dg + db*db)); } cv::Mat FloydSteinbergDitheringForTargetColor(cv::Mat inputImage, const cv::Vec3b& targetRGB) { cv::Mat image = inputImage.clone(); cv::Mat result = cv::Mat::zeros(image.size(), CV_8UC1); // 最终黑白图 for (int i = 0; i < image.rows; ++i) { for (int j = 0; j < image.cols; ++j) { cv::Vec3b currentBGR = image.at<cv::Vec3b>(i, j); // 计算相似度,判断是否设为黑色(匹配目标色)或白色 float similarity = ColorSimilarity(currentBGR, targetRGB); uchar newPixel = (similarity > 0.5f) ? 0 : 255; result.at<uchar>(i, j) = newPixel; // 基于灰度亮度计算误差,用于抖动扩散 cv::Vec3b currentRGB(currentBGR[2], currentBGR[1], currentBGR[0]); uchar currentGray = cv::saturate_cast<uchar>(0.299*currentRGB[0] + 0.587*currentRGB[1] + 0.114*currentRGB[2]); uchar newGray = newPixel; int error = currentGray - newGray; // Floyd-Steinberg误差扩散(统一修改三通道为灰度值,简化计算) if (j + 1 < image.cols) { uchar adjusted = cv::saturate_cast<uchar>(image.at<cv::Vec3b>(i, j+1)[0] + (error * 7) / 16); image.at<cv::Vec3b>(i, j+1) = cv::Vec3b(adjusted, adjusted, adjusted); } if (i + 1 < image.rows && j - 1 >= 0) { uchar adjusted = cv::saturate_cast<uchar>(image.at<cv::Vec3b>(i+1, j-1)[0] + (error * 3) / 16); image.at<cv::Vec3b>(i+1, j-1) = cv::Vec3b(adjusted, adjusted, adjusted); } if (i + 1 < image.rows) { uchar adjusted = cv::saturate_cast<uchar>(image.at<cv::Vec3b>(i+1, j)[0] + (error * 5) / 16); image.at<cv::Vec3b>(i+1, j) = cv::Vec3b(adjusted, adjusted, adjusted); } if (i + 1 < image.rows && j + 1 < image.cols) { uchar adjusted = cv::saturate_cast<uchar>(image.at<cv::Vec3b>(i+1, j+1)[0] + (error * 1) / 16); image.at<cv::Vec3b>(i+1, j+1) = cv::Vec3b(adjusted, adjusted, adjusted); } } } return result; } int main() { cv::Mat inputImage = cv::imread("TestImage.jpg"); if (inputImage.empty()) { std::cerr << "Error: Unable to load input image!" << std::endl; return -1; } // 设置目标颜色(RGB格式),比如浅蓝(0,215,225) cv::Vec3b targetRGB(0, 215, 225); // 或者黑色(0,0,0) // cv::Vec3b targetRGB(0, 0, 0); cv::Mat resultImage = FloydSteinbergDitheringForTargetColor(inputImage, targetRGB); // 保存黑白抖动结果 cv::imwrite("output_blackwhite_dither.jpg", resultImage); // 如需反转效果(匹配色为白,其余为黑),取消注释 // cv::bitwise_not(resultImage, resultImage); // cv::imwrite("output_inverted.jpg", resultImage); cv::imshow("Result", resultImage); cv::waitKey(0); cv::destroyAllWindows(); return 0; }
关键修改说明
- 颜色空间校正:明确将OpenCV的BGR格式转换为RGB后计算颜色相似度,避免匹配错误。
- 单目标色逻辑:直接基于目标色的相似度生成黑白二值图,完全符合“匹配指定色设为黑/白”的需求。
- 误差扩散优化:基于灰度亮度计算抖动误差,简化通道操作的同时保证抖动效果自然。
- 可切换效果:通过
bitwise_not可以快速切换“匹配色为黑”或“匹配色为白”的两种模式。
内容的提问来源于stack exchange,提问作者Smile
相关产品推荐
相关产品推荐

