在图片中查找/替换颜色(即重新着色)
Posted
技术标签:
【中文标题】在图片中查找/替换颜色(即重新着色)【英文标题】:Find / replace colors in (i.e. recolor) a picture 【发布时间】:2016-02-02 11:27:34 【问题描述】:我正在尝试重新创建 Microsoft 在从 Office 2003 过渡到 2007 时不幸终止的“重新着色图片”对话框。这对于替换图片中的颜色非常有用(有关对话框的完整说明,请参阅 http://www.indezine.com/products/powerpoint/learn/picturesandvisuals/recolor-pictures-ppt2003.html)。
我最感兴趣的是为 元文件 格式(EMF 或 WMF)的图像执行此操作,根据我的经验,这些格式的颜色往往比其他图片格式少。下图是从 Excel 粘贴到 PowerPoint 中的增强图元文件图片的示例,它似乎只包含 6 种颜色:
如果我能够使用上图所示的旧版 Office 对话框,我会在“原始”列的左侧看到我的 6 种颜色,并且我可以轻松地将蓝色字体(和边框)颜色更改为黑色。这里的问题是,如果我使用GetPixel()
以编程方式清点图像中的颜色,由于字体的抗锯齿,我会得到几十种颜色,并且向用户显示所有这些重新着色选项是不切实际的(其中将有效地要求用户手动重新创建适当的抗锯齿效果)。下面代码的 sn-p 说明了我是如何尝试对颜色进行清点的:
Dim listColors as New List(Of Color)
Dim shp as PowerPoint.Shape = [a metafile picture in PowerPoint]
Dim strTemp as String = Path.Combine(Environ("temp"), "temp_img.emf")
shp.Export(strTemp, PowerPoint.PpShapeFormat.ppShapeFormatEMF, 0, 0)
Using bmp As New Bitmap(strTemp)
For x As Integer = 0 To bmp.Width - 1
For y As Integer = 0 To bmp.Height - 1
listColors.Add(bmp.GetPixel(x, y))
Next
Next
End Using
我看到元文件有一个可选的Palette property,我认为它可以提供答案,但是当我尝试访问它时会抛出异常,所以这是一个死胡同。我还看到元文件图像有headers,但我无法从互联网上有限的文档中破译如何处理它们,我什至不确定这会让我得到正确的答案(即 6 种颜色) .
总而言之,问题的第 1 部分是如何清点(即识别)上图中的 6 种“核心”颜色,第 2 部分是如何将这 6 种颜色中的一种替换为另一种。 VB.NET 解决方案是首选,虽然如果不太复杂,我可能会翻译 C# 代码。
如果需要,您可以在https://www.dropbox.com/s/n03ys3dh9pcd0xu/temp_img.emf?dl=0 下载上图的 EMF 版本。
编辑:明确地说,我对“计算”上图中的六种“核心”颜色不感兴趣。我相信,也许是错误的,这六种颜色是图像的明确属性,我的第一个目标是弄清楚如何访问它们。实际上,如果您在 PowerPoint 中简单地对图元文件图片进行两次解组,您可以循环遍历生成的形状以获得这六种颜色。这将解决问题的第 1 部分,尽管它看起来有点草率,仅适用于元文件(实际上可能没问题),我怀疑这就是旧版重新着色图片对话框的工作方式。为了解决第 2 部分,我可以在交换颜色后重新组合元文件图片形状,但同样,这看起来很草率,并且以不同于预期的方式修改了图片。那么,如何在 [元文件] 图片中显式检索/修改/设置这些“核心”颜色?
【问题讨论】:
我认为问题在于抗锯齿字体,您要么必须在获取像素之前更改图像,要么全部获取像素,要么将所有相似颜色选择到字典中,以主颜色为键然后当颜色改变时,改变其他的 如果您使用屏幕观察镜并仔细观察屏幕字体,您会在它们周围看到一个多色光晕,即使它们是黑色的.. @TaW 我相信这就是 OP 的问题所在,或者至少是其中的一部分 确实,这就是我赞成您的评论的原因;我也同意你的建议,如何解决它,尽管对于 excel 导入,人们也可以看看实际上可以从那里得到哪些颜色.. ;-) 【参考方案1】:所以我快速尝试了实现我的一个想法,即使用颜色字典等等。代码目前还不能正常工作,但我想我在这里展示它,以便您可以快速了解它的工作原理并从那里进行开发。
using (Bitmap bitmap = new Bitmap(@"InputPath"))
Dictionary<Color, List<Color>> colourDictionary = new Dictionary<Color, List<Color>>();
int nTolerance = 60;
int nBytesPerPixel = Bitmap.GetPixelFormatSize(bitmap.PixelFormat) / 8;
System.Drawing.Imaging.BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);
try
int nByteCount = bitmapData.Stride * bitmap.Height;
byte[] _baPixels = new byte[nByteCount];
System.Runtime.InteropServices.Marshal.Copy(bitmapData.Scan0, _baPixels, 0, _baPixels.Length);
int _nStride = bitmapData.Stride;
for (int h = 0; h < bitmap.Height; h++)
int nCurrentLine = h * _nStride;
for (int w = 0; w < (bitmap.Width * nBytesPerPixel); w += nBytesPerPixel)
int nBlue = _baPixels[nCurrentLine + w];
int nGreen = _baPixels[nCurrentLine + w + 1];
int nRed = _baPixels[nCurrentLine + w + 2];
if (colourDictionary.Keys.Count > 0)
Color[] caNearbyColours = colourDictionary.Keys.Select(c => c)
.Where(c => (int)c.B <= (nBlue + nTolerance) && (int)c.B >= (nBlue - nTolerance)
&& (int)c.G <= (nGreen + nTolerance) && (int)c.G >= (nGreen - nTolerance)
&& (int)c.R <= (nRed + nTolerance) && (int)c.R >= (nRed - nTolerance)).ToArray();
if (caNearbyColours.Length > 0)
if (!colourDictionary[caNearbyColours.FirstOrDefault()].Any(c => c.R == nRed && c.G == nGreen && c.B == nBlue))
colourDictionary[caNearbyColours.FirstOrDefault()].Add(Color.FromArgb(255, nRed, nGreen, nBlue));
else
colourDictionary.Add(Color.FromArgb(255, nRed, nGreen, nBlue), new List<Color>());
else
colourDictionary.Add(Color.FromArgb(255, nRed, nGreen, nBlue), new List<Color>());
finally
bitmap.UnlockBits(bitmapData);
using (Bitmap colourBitmap = new Bitmap(bitmap.Width, bitmap.Height, bitmap.PixelFormat))
using (Graphics g = Graphics.FromImage(colourBitmap))
for (int h = 0; h < colourBitmap.Height; h++)
for (int w = 0; w < colourBitmap.Width; w++)
Color colour = bitmap.GetPixel(w, h);
if (!colourDictionary.ContainsKey(colour))
Color keyColour = colourDictionary.Keys.FirstOrDefault(k => colourDictionary[k].Any(v => v == colour));
colourBitmap.SetPixel(w, h, keyColour);
else
colourBitmap.SetPixel(w, h, colour);
colourBitmap.Save(@"OutputPath", System.Drawing.Imaging.ImageFormat.Png);
请注意顶部如何使用Lockbits
以获得更好的性能。这可以很容易地转移到底部。另请注意,锁定位代码设置为适用于每像素字节数为 3 或 4 的图像。
现在看看代码是如何工作的:
首先循环遍历初始图像并寻找颜色。如果已经找到颜色,它会跳过它,但是如果颜色不在字典中,它将添加它,并且如果颜色在字典键中具有相似的颜色(也需要更改以查看值),它将添加它符合它的价值观。
然后循环遍历输出图像中的像素,根据字典中的键设置它们,从而创建一个“阻塞”图像。
现在正如我所说,它还不能正常工作,所以这里需要进行一些改进:
如上所述,检查值中的颜色 在底部代码中添加锁定位以获得更好的性能 调整nTolerance
值以获得更好的结果
跟踪颜色计数并在循环结束时将键设置为计数最多的键
当然还有其他我没想到的东西
【讨论】:
@MacG 希望这有助于您开始解决问题 谢谢。我知道 LockBits 是 GetPixel() 获取颜色的更快替代方案。但是,LockBits 仍然会返回图像中的所有颜色,包括用于抗锯齿的颜色,对吧?为了解决这个问题,我知道您正在使用容差将颜色“存储”到更少的存储桶中,但这对于始终获得正确的“核心”颜色(在本例中为 6)似乎不可靠。此外,即使容差概念可以正确清点第 1 部分中的 6 种颜色,我认为在替换第 2 部分中的颜色时也会产生其他问题,例如低质量或没有抗锯齿。 @MacG 当然,使用这种方法你会失去质量和抗锯齿效果。我认为这是一个可以帮助您开始思考的想法。但是,尽量减少容差桶误报的方法是让主色成为出现最多的颜色。您可以通过使用 Graphics 并设置平滑质量来保持抗锯齿效果,而不是使用 SetPixel。以上是关于在图片中查找/替换颜色(即重新着色)的主要内容,如果未能解决你的问题,请参考以下文章