颜色处理

原创
2021/07/21 21:21
阅读数 285

最近有个朋友要出书,请我帮忙。其中涉及到图片颜色的处理,花了一天写了个程序,结果发现根本用不上,直接用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,然后删除对应的颜色通道即可,这个程序没有用上。不过也不算白弄,起码又学习了几个知识点,和大家分享。

 

最后,上面的完整程序可在下面地址下载:

https://download.csdn.net/download/u014559935/20416142

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部