最近有个朋友要出书,请我帮忙。其中涉及到图片颜色的处理,花了一天写了个程序,结果发现根本用不上,直接用PS就行。不过还是学到一些东西,记录一下。
一开始朋友告诉我,书里图片的颜色太多,印刷成本高。要是能只用两种颜色印刷,就可以把成本降下来。于是我想当然认为是某种聚类算法,把RGB颜色全部聚类到两种颜色(R1, G1, B1)和(R2, G2, B2)。最简单的想法是在颜色空间定义“颜色距离”,把某个距离内的颜色全部用一种颜色代替即可,其核心在于如何计算两种颜色的“距离”。一开始采用欧式距离,尝试了带A和不带A的情况,结果效果都不太理想,于是查了一下,发现有加权欧式距离,试了一下,效果还可以。于是写了第一版程序如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading.Tasks;
namespace ColorManage
{
class JPG
{
Bitmap m_img;
double m_step;
int m_typeNum;
public int Width
{
get { return m_img.Width; }
}
public int Height
{
get { return m_img.Height; }
}
public JPG()
{
}
public JPG(int typeNum)
{
m_typeNum = typeNum;
m_step = ColorDistance(Color.Black, Color.White) / typeNum;
}
public bool Load(string path)
{
Image img = Image.FromFile(path);
m_img = new Bitmap(img);
return true;
}
public bool Save(string path, ImageFormat format)
{
m_img.Save(path, format);
return true;
}
public void Process()
{
// 1. 读入所有坐标点颜色并按颜色分类
List<List<Coordinate>> data = DistributeColor(m_img);
// 2. 计算每种颜色类型的平均颜色
List<Color> bin = Average(data);
// 3. 把每一类的颜色都用平均颜色代替
m_img = ReplaceColor(m_img, data, bin);
}
// 将图中所有颜色用colorList中的颜色代替
// 若图中颜色和colorList中颜色色差小于dist,则用colorList中对应颜色代替
// 否则用白色代替,并返回被设置为白色的点数
public int ChangeColor(List<Color> colorList, double dist = 50.0)
{
int count = 0;
for (var x = 0; x < m_img.Width; x++)
{
for (var y = 0; y < m_img.Height; y++)
{
Color pixelColor = m_img.GetPixel(x, y);
for (var i = 0; i < colorList.Count; i++)
{
double distance = ColorDistance(pixelColor, colorList[i]);
if (distance < dist)
{
m_img.SetPixel(x, y, colorList[i]);
break;
}
if (i == colorList.Count - 1)
{
m_img.SetPixel(x, y, Color.White);
count++;
}
}
}
}
return count;
}
// 对颜色聚类,将所有颜色分为m_typeNum个组
private List<List<Coordinate>> DistributeColor(Bitmap imgObj)
{
List<List<Coordinate>> data = new List<List<Coordinate>>();
for (int type = 0; type < m_typeNum; type++)
{
data.Add(new List<Coordinate>());
}
for (var x = 0; x < imgObj.Width; x++)
{
for (var j = 0; j < imgObj.Height; j++)
{
//获取像素的颜色
Color pixelColor = imgObj.GetPixel(x, j);
//double color = Math.Sqrt(//pixelColor.A * pixelColor.A +
// pixelColor.R * pixelColor.R + pixelColor.G * pixelColor.G
// + pixelColor.B * pixelColor.B);
double color = ColorDistance(pixelColor, Color.Black);
for (int type = 0; type < m_typeNum; type++ )
{
if (color >= type*m_step && color < (type+1)*m_step) // 注意白色不算在内
{
Coordinate xyc = new Coordinate(x, j, pixelColor);
data[type].Add(xyc);
break;
}
}
}
}
return data;
}
// 计算每组的平均颜色
private List<Color> Average(List<List<Coordinate>> data)
{
List<Color> mean = new List<Color>();
for (int i = 0; i < data.Count; i++)
{
double a = 0;
double r = 0;
double g = 0;
double b = 0;
for (int j = 0; j < data[i].Count; j++)
{
double olda = a;
double oldr = r;
double oldg = g;
double oldb = b;
a = olda + (data[i][j].color.A - olda) / (j + 1);
r = oldr + (data[i][j].color.R - oldr) / (j + 1);
g = oldg + (data[i][j].color.G - oldg) / (j + 1);
b = oldb + (data[i][j].color.B - oldb) / (j + 1);
}
mean.Add(Color.FromArgb((int)a, (int)r, (int)g, (int)b));
}
return mean;
}
// 将每组点的颜色都用平均值代替
private Bitmap ReplaceColor(Bitmap imgObj, List<List<Coordinate>> data, List<Color> color)
{
for (int i = 0; i < data.Count; i++ )
{
for (int j = 0; j < data[i].Count; j++)
{
imgObj.SetPixel(data[i][j].x, data[i][j].y, color[i]);
}
}
return imgObj;
}
// 计算颜色之间的距离,采用加权欧式距离
private double ColorDistance(Color a, Color b)
{
double rmean = (a.R + b.R ) / 2.0;
double R = a.R - b.R;
double G = a.G - b.G;
double B = a.B - b.B;
return Math.Sqrt((2+rmean/256)*(R*R)+4*(G*G)+(2+(255-rmean)/256)*(B*B));
}
}
}
其详细说明可参考如下链接:
https://www.compuphase.com/cmetric.htm
结果后来和朋友进一步沟通,发现并不是这样。而是计算机显示用RGB坐标,出版社印刷用CMYK坐标。所谓的两种颜色,指的是在CMYK里只用到两种原色,类似于RGB里只用R和G或者只用G和B。CMYK指的是青色-Cyan,洋红-Magenta,黄色-Yellow,黑色-Black。ARGB方便用于表示发射光谱,而CMYK方便用于表征吸收光谱,所以略有不同。找了半天,发现二者转换关系可表示如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Shapes;
namespace ColorManage
{
class Coordinate
{
public int x;
public int y;
public System.Drawing.Color color;
public Coordinate(int xx, int yy, System.Drawing.Color cc)
{
x = xx;
y = yy;
color = cc;
}
}
class CMYK
{
public byte C;
public byte M;
public byte Y;
public byte K;
public CMYK(byte c, byte m, byte y, byte k)
{
C = c;
M = m;
Y = y;
K = k;
}
// RGB转CMYK
public CMYK(byte R, byte G, byte B)
{
int tmp = R > G ? R : G;
int max = tmp > B ? tmp : B;
double tmp1 = (1 - max / 255.0);
C = Convert.ToByte(100 * (1 - tmp1 - R / 255.0) / (1 - tmp1));
M = Convert.ToByte(100 * (1 - tmp1 - G / 255.0) / (1 - tmp1));
Y = Convert.ToByte(100 * (1 - tmp1 - B / 255.0) / (1 - tmp1));
K = Convert.ToByte(100 * tmp1);
}
// CMYK转RGB
public Color ToRGB()
{
byte R = Convert.ToByte(255.0 * (100 - C) * (100 - K) / 10000.0);
byte G = Convert.ToByte(255.0 * (100 - M) * (100 - K) / 10000.0);
byte B = Convert.ToByte(255.0 * (100 - Y) * (100 - K) / 10000.0);
return Color.FromRgb(R, G, B);
}
public static Color ToRGB(byte c, byte m, byte y, byte k)
{
byte R = Convert.ToByte(255.0 * (100 - c) * (100 - k) / 10000.0);
byte G = Convert.ToByte(255.0 * (100 - m) * (100 - k) / 10000.0);
byte B = Convert.ToByte(255.0 * (100 - y) * (100 - k) / 10000.0);
return Color.FromRgb(R, G, B);
}
}
}
最后写了个界面,把这两部分代码强行放在一起,凑合能用。
<Window x:Class="ColorManage.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="450" Width="675">
<Grid>
<Button Content="自动配色" Margin="0,0,10,10" Click="Button_Click" Height="19" VerticalAlignment="Bottom" HorizontalAlignment="Right" Width="75"/>
<TextBox Name="textbox1" Margin="0,0,90,10" TextWrapping="Wrap" Text="" HorizontalAlignment="Right" Width="120" Height="23" VerticalAlignment="Bottom">
<TextBox.Resources>
<VisualBrush x:Key="HintText" TileMode="None" Opacity="0.5" Stretch="None" AlignmentX="Left">
<VisualBrush.Visual>
<TextBlock FontStyle="Italic" Text="请输入颜色数"/>
</VisualBrush.Visual>
</VisualBrush>
</TextBox.Resources>
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource HintText}"/>
</Trigger>
<Trigger Property="Text" Value="">
<Setter Property="Background" Value="{StaticResource HintText}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<Image Name="img1" Margin="10,10,249,10" Stretch="UniformToFill" MouseMove="img1_MouseMove" MouseDown="img1_MouseDown"/>
<Button Content="载入图像" Margin="0,10,135,0" VerticalAlignment="Top" HorizontalAlignment="Right" Width="75" Click="Button_Click_1"/>
<TextBlock Margin="0,40,193,0" TextWrapping="Wrap" Text="x:" VerticalAlignment="Top" RenderTransformOrigin="0.471,-0.459" HorizontalAlignment="Right" Width="17"/>
<TextBlock Name="xx" HorizontalAlignment="Right" Margin="0,40,133,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top"/>
<TextBlock Margin="0,40,97,0" TextWrapping="Wrap" Text="y:" VerticalAlignment="Top" RenderTransformOrigin="0.471,-0.459" HorizontalAlignment="Right" Width="17"/>
<TextBlock Name="yy" HorizontalAlignment="Right" Margin="0,40,37,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top"/>
<TextBlock HorizontalAlignment="Right" Margin="0,70,155,0" TextWrapping="Wrap" Text="颜色:" VerticalAlignment="Top"/>
<Rectangle Name="color1" Fill="#FFF4F4F5" HorizontalAlignment="Right" Height="23" Margin="0,65,52,0" Stroke="Black" VerticalAlignment="Top" Width="77"/>
<ListBox Name="listbox1" Margin="0,232,15,65" HorizontalAlignment="Right" Width="200"/>
<TextBlock Margin="0,133,185,0" TextWrapping="Wrap" Text="C:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Cvalue" HorizontalAlignment="Right" Height="15" Margin="0,132,126,0" TextWrapping="Wrap" Text="100" VerticalAlignment="Top" Width="52"/>
<TextBlock Margin="0,152,185,0" TextWrapping="Wrap" Text="M:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Mvalue" HorizontalAlignment="Right" Height="15" Margin="0,151,126,0" TextWrapping="Wrap" Text="100" VerticalAlignment="Top" Width="52"/>
<TextBlock Margin="0,173,185,0" TextWrapping="Wrap" Text="Y:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Yvalue" HorizontalAlignment="Right" Height="15" Margin="0,172,126,0" TextWrapping="Wrap" Text="100" VerticalAlignment="Top" Width="52"/>
<TextBlock Margin="0,192,185,0" TextWrapping="Wrap" Text="K:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Kvalue" HorizontalAlignment="Right" Height="15" Margin="0,191,126,0" TextWrapping="Wrap" Text="100" VerticalAlignment="Top" Width="52"/>
<TextBlock Margin="0,133,80,0" TextWrapping="Wrap" Text="R:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Rvalue" HorizontalAlignment="Right" Height="15" Margin="0,132,21,0" TextWrapping="Wrap" Text="255" VerticalAlignment="Top" Width="52"/>
<TextBlock Margin="0,152,80,0" TextWrapping="Wrap" Text="G:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Gvalue" HorizontalAlignment="Right" Height="15" Margin="0,151,21,0" TextWrapping="Wrap" Text="255" VerticalAlignment="Top" Width="52"/>
<TextBlock Margin="0,173,80,0" TextWrapping="Wrap" Text="B:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Bvalue" HorizontalAlignment="Right" Height="15" Margin="0,172,21,0" TextWrapping="Wrap" Text="255" VerticalAlignment="Top" Width="52"/>
<Button Content="添 加" Margin="0,201,21,0" VerticalAlignment="Top" HorizontalAlignment="Right" Width="75" Click="Button_Click_3"/>
<TextBlock Margin="0,100,196,0" TextWrapping="Wrap" Text="R:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Rview" HorizontalAlignment="Right" Height="15" Margin="0,100,156,0" TextWrapping="Wrap" Text="255" VerticalAlignment="Top" Width="38"/>
<TextBlock Margin="0,100,133,0" TextWrapping="Wrap" Text="G:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Gview" HorizontalAlignment="Right" Height="15" Margin="0,100,92,0" TextWrapping="Wrap" Text="255" VerticalAlignment="Top" Width="38"/>
<TextBlock Margin="0,100,68,0" TextWrapping="Wrap" Text="B:" VerticalAlignment="Top" RenderTransformOrigin="0.5,1.64" HorizontalAlignment="Right" Width="18"/>
<TextBox Name="Bview" HorizontalAlignment="Right" Height="15" Margin="0,100,28,0" TextWrapping="Wrap" Text="255" VerticalAlignment="Top" Width="38"/>
<Button Content="配 色" Margin="0,10,25,0" VerticalAlignment="Top" HorizontalAlignment="Right" Width="75" Click="Button_Click_2"/>
</Grid>
</Window>
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ColorManage
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
int typeNum = 3;
if(!int.TryParse(textbox1.Text, out typeNum))
{
MessageBox.Show("请输入正确的颜色数");
}
JPG pic = new JPG(typeNum);
pic.Load("test.jpg");
pic.Process();
pic.Save("test1.jpg", ImageFormat.Jpeg);
}
private BitmapImage m_bitmapImg;
public string Path
{
get
{
if (!string.IsNullOrEmpty(m_picPath))
{
return m_picPath;
}
else
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.Title = "选择图片文件";
//openFileDialog.Filter = "jpg文件|*.jpg";
openFileDialog.FileName = string.Empty;
openFileDialog.FilterIndex = 1;
openFileDialog.Multiselect = false;
openFileDialog.RestoreDirectory = true;
openFileDialog.DefaultExt = "jpg";
if (openFileDialog.ShowDialog() == false)
{
MessageBox.Show("please report the error to the developer.");
}
return openFileDialog.FileName;
}
}
}
private string m_picPath = null;
private void Button_Click_1(object sender, RoutedEventArgs e)
{
m_bitmapImg = new BitmapImage(new Uri(Path));
img1.Source = m_bitmapImg;
}
private void img1_MouseMove(object sender, MouseEventArgs e)
{
Point p = e.GetPosition(img1);
xx.Text = p.X.ToString();
yy.Text = p.Y.ToString();
SolidColorBrush mySolidColorBrush = GetPixelColor(p);
color1.Fill = mySolidColorBrush;
}
private SolidColorBrush GetPixelColor(Point point)
{
byte[] data = new byte[4*m_bitmapImg.PixelWidth*m_bitmapImg.PixelHeight];
m_bitmapImg.CopyPixels(data, 4 * m_bitmapImg.PixelWidth, 0);
int x = (int)(point.X / img1.ActualWidth * m_bitmapImg.PixelWidth);
int y = (int)(point.Y / img1.ActualHeight * m_bitmapImg.PixelHeight);
byte b = data[4*(y * m_bitmapImg.PixelWidth + x)];
byte g = data[4 * (y * m_bitmapImg.PixelWidth + x) + 1];
byte r = data[4 * (y * m_bitmapImg.PixelWidth + x) + 2];
byte a = data[4 * (y * m_bitmapImg.PixelWidth + x) + 3];
m_color = Color.FromRgb(r, g, b);
Rview.Text = r.ToString();
Gview.Text = g.ToString();
Bview.Text = b.ToString();
return new SolidColorBrush(m_color);
}
private Color m_color;
private void img1_MouseDown(object sender, MouseButtonEventArgs e)
{
ListBoxItem item = new ListBoxItem();
item.Background = new SolidColorBrush(m_color);
CMYK cmyk = new CMYK(m_color.R, m_color.G, m_color.B);
string info = "R: " + m_color.R +
" G: " + m_color.G + " B: " + m_color.B +
";\tK: " + cmyk.K + " C: " + cmyk.C +
" M: " + cmyk.M + " Y: " + cmyk.Y;
item.Content = info;
item.Tag = m_color;
listbox1.Items.Add(item);
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
JPG pic = new JPG();
pic.Load(Path);
List<System.Drawing.Color> list = new List<System.Drawing.Color>();
for (int i = 0; i < listbox1.Items.Count; i++ )
{
ListBoxItem item = listbox1.Items[i] as ListBoxItem;
Color c = (Color)item.Tag;
System.Drawing.Color color = System.Drawing.Color.FromArgb(c.A, c.R, c.G, c.B);
list.Add(color);
}
list.Add(System.Drawing.Color.White);
double count = pic.ChangeColor(list, 250);
string info = "count: " + count.ToString() + "\nwidth: " +
pic.Width.ToString() + "\nheight: " + pic.Height.ToString()
+ "\nratio: " + (count / (pic.Width * pic.Height)).ToString();
MessageBox.Show(info);
pic.Save("test1.bmp", ImageFormat.Bmp);
}
private void Button_Click_3(object sender, RoutedEventArgs e)
{
ListBoxItem item = new ListBoxItem();
byte c, m, y, k, r, g, b;
CMYK cmyk;
if (byte.TryParse(Cvalue.Text, out c) && byte.TryParse(Mvalue.Text, out m) &&
byte.TryParse(Yvalue.Text, out y) && byte.TryParse(Kvalue.Text, out k))
{
cmyk = new CMYK(c, m, y, k);
m_color = cmyk.ToRGB();
}
else if (byte.TryParse(Rvalue.Text, out r) &&
byte.TryParse(Gvalue.Text, out g) && byte.TryParse(Bvalue.Text, out b))
{
cmyk = new CMYK(r, g, b);
m_color = Color.FromRgb(r, g, b);
}
else
{
MessageBox.Show("Please input the CMYK value or RGB value for the color.");
return;
}
item.Background = new SolidColorBrush(m_color);
string info = "R: " + m_color.R +
" G: " + m_color.G + " B: " + m_color.B +
";\tK: " + cmyk.K + " C: " + cmyk.C +
" M: " + cmyk.M + " Y: " + cmyk.Y;
item.Content = info;
item.Tag = m_color;
listbox1.Items.Add(item);
}
}
}
运行效果如下:
不过后来发现,其实用Photoshop很容易就能把RGB转为CMYK,然后删除对应的颜色通道即可,这个程序没有用上。不过也不算白弄,起码又学习了几个知识点,和大家分享。
最后,上面的完整程序可在下面地址下载: