Забавы с пиксельным шейдером. Цветовые плоскости CMYK

В прошлый раз мы посмотрели разложение изображения по цветовым плоскостям в модели RGB. Сейчас попробуем выполнить подобное разложение для цветовой модели CMYK.

Модель CMYK довольно сильно отличается от модели RGB, так что давайте посмотрим на описание модели, которое предлагает википедия.

Четырёхцветная автотипия (CMYK: Cyan, Magenta, Yellow, Key color) — субтрактивная схема формирования цвета, используемая прежде всего в полиграфии для стандартной триадной печати. Схема CMYK, как правило, обладает (сравнительно с RGB) небольшим цветовым охватом.

По-русски эти цвета часто называют так: голубой, пурпурный, жёлтый, хотя в профессиональной среде часто подразумевают cyan, magenta и yellow (о значении K см. далее). Печать четырьмя красками, соответствующими CMYK, также называют печатью триадными красками.

Цвет в CMYK зависит не только от спектральных характеристик красителей и от способа их нанесения, но и их количества, характеристик бумаги и других факторов. Фактически, цифры CMYK являются лишь набором аппаратных данных для фотонаборного автомата или CTP и не определяют цвет однозначно.

Так, исторически в разных странах сложилось несколько стандартизованных процессов офсетной печати. Сегодня это американский, европейский и японский стандарты для мелованной и немелованной бумаг. Именно для этих процессов разработаны стандартизованные бумаги и краски (например, стандарты ECI). Для них же созданы соответствующие цветовые модели
CMYK, которые используются в процессах цветоделения. Однако, многие типографии, в которых работают специалисты с достаточной квалификацией (или способные на время пригласить такого специалиста), нередко создают профиль, описывающий печатный процесс конкретной печатной машины с конкретной бумагой. Этот профиль они предоставляют своим заказчикам.

Значение K в аббревиатуре CMYK

В CMYK используются четыре цвета, первые три в аббревиатуре названы по первой букве цвета, а в качестве четвёртого используется чёрный. Одна из версий утверждает, что K — сокращение от англ.
blacK. Согласно этой версии, при выводе полиграфических плёнок на них одной буквой указывался цвет, которому они принадлежат. Чёрный не стали обозначать B, чтобы не путать с B (англ.
blue) из модели RGB, а стали обозначать K (по последней букве). Профессиональные цветокорректоры работают с десятью каналами RGBCMYKLab, используя доступные цветовые пространства. Поэтому при обозначении CMYK как CMYB фраза «манипуляция с каналом B» требовала бы уточнения «манипуляция с каналом B из CMYB», что было бы неудобно.

Согласно другому варианту, K является сокращением от слова ключевой
англ.
Key, скелетный: в англоязычных странах термином key plate обозначается печатная форма для чёрной краски.

Третий вариант говорит о немецком происхождении К — нем.
Kontur. Эта версия подтверждается ещё и тем, что многие старые монтажники так и называют соответствующую плёнку — контур, контурная. Тем более, что в технологии печати чёрный и вправду как бы оконтуривает изображение.

Четвёртый вариант это сокращение от слова Kobalt (темно-серый).

Почему CMYK называют субтрактивной моделью

Так как модель CMYK применяют в основном в полиграфии при цветной печати, а бумага и прочие печатные материалы являются поверхностями, отражающими свет, удобнее считать, какое количество света отразилось от той или иной поверхности, нежели сколько поглотилось. Таким образом, если вычесть из белого три первичных цвета, RGB, мы получим тройку дополнительных цветов CMY. «Субтрактивный» означает «вычитаемый» — из белого вычитаются первичные цвета.

Почему в CMYK четыре цвета, а в RGB три

Несмотря на то, что чёрный цвет можно получать смешением в равной пропорции пурпурного, голубого и жёлтого красителей, по ряду причин (чистота цвета, переувлажнение бумаги и др.) такой подход обычно неудовлетворителен. Основные причины использования дополнительного чёрного пигмента таковы:

  • На практике в силу неидеальности красителей и погрешностей в пропорциях компонентов смешение реальных пурпурного, голубого и жёлтого цветов даёт скорее грязно-коричневый или грязно-серый цвет; триадные краски не дают той глубины и насыщенности, которая достигается использованием настоящего чёрного. Так как чистота и насыщенность чёрного цвета, а также стабильность оттенка нейтральных (серых) областей чрезвычайно важны в печатном процессе, был введён ещё один цвет.
  • При выводе мелких чёрных деталей изображения или текста без использования чёрного пигмента возрастает риск неприводки (недостаточно точное совпадение точек нанесения) пурпурного, голубого и жёлтого цветов. Увеличение же точности печатающего аппарата требует неадекватных затрат.
  • Смешение 100 % пурпурного, голубого и жёлтого пигментов в одной точке в случае струйной печати существенно смачивает бумагу, деформирует её и увеличивает время просушки. Аналогичные проблемы с так называемой суммой красок возникают и в офсетной печати. В зависимости от типа материала и технологии печати ограничение по сумме красок может быть ниже 300 % (например, в газетной печати типичное ограничение 260—280 %), что делает технически невозможным синтез насыщенного чёрного из трёх стопроцентных компонентов триады.
  • Чёрный пигмент (в качестве которого, как правило, используется сажа) существенно дешевле остальных трёх.

Числовые значения в CMYK и их преобразование

Каждое из чисел, определяющее цвет в CMYK, представляет собой процент краски данного цвета, составляющей цветовую комбинацию, а точнее, размер точки растра, выводимой на фотонаборном аппарате на плёнке данного цвета (или прямо на печатной форме в случае с CTP). Например, для получения цвета «хаки» следует смешать 30 % голубой краски, 45 % пурпурной краски, 80 % жёлтой краски и 5 % чёрной. Это можно обозначить следующим образом: (30,45,80,5). Иногда пользуются таким обозначением: C30M45Y80K5.

Важно отметить, что числовое значение краски в CMYK не может само по себе описать цвет. Цифры — лишь набор аппаратных данных, используемых в печатном процессе для формирования изображения. На практике реальный цвет будет обусловлен не только размером точки растра на фотовыводе, соответствующем числам в подготовленном к печати файле, но и реалиями конкретного печатного процесса: растискиванием, на которое могут влиять такие факторы, как состояние печатной машины, качество бумаги, влажность в цеху; условиями просмотра отпечатка (спектральными характеристиками источника освещения) и другими.

Для получения представления о цвете, заданном в цветовой модели CMYK, применяют цветовые профили, которые связывают значения аппаратных данных с реальным цветом, выраженным, как правило, в цветовых моделях XYZ или LAB. Наибольшее применение в наши дни нашли ICC-профили.

Теперь нам осталось только добавить нужные техники в шейдер.

float4 PSCyan(float2 TexCoords : TEXCOORD0) : COLOR0

{

        float4 color1 = tex2D(ColorSampler, TexCoords);

 

        float computedC = 0;

        float computedM = 0;

        float computedY = 0;

        float computedK = 0;

 

         if (color1.r==0 && color1.g==0 && color1.b==0) {

                computedK = 1;

                return float4(0,0,0,1);

         }

 

        computedC = 1 - (color1.r);

        computedM = 1 - (color1.g);

        computedY = 1 - (color1.b);

 

         float minCMY = min(computedC,

              min(computedM,computedY));

 computedC = (computedC - minCMY) / (1 - minCMY) ;

 computedM = (computedM - minCMY) / (1 - minCMY) ;

 computedY = (computedY - minCMY) / (1 - minCMY) ;

 computedK = minCMY;

 

        color1.r = 0;

        color1.g = computedC-computedK;

        color1.b = computedC-computedK;

 

        return color1;

}

 

float4 PSMagenta(float2 TexCoords : TEXCOORD0) : COLOR0

{

        float4 color1 = tex2D(ColorSampler, TexCoords);

 

        float computedC = 0;

        float computedM = 0;

        float computedY = 0;

        float computedK = 0;

 

         if (color1.r==0 && color1.g==0 && color1.b==0) {

                computedK = 1;

                return float4(0,0,0,1);

         }

 

        computedC = 1 - (color1.r);

        computedM = 1 - (color1.g);

        computedY = 1 - (color1.b);

 

        

        

         float minCMY = min(computedC,

              min(computedM,computedY));

 computedC = (computedC - minCMY) / (1 - minCMY) ;

 computedM = (computedM - minCMY) / (1 - minCMY) ;

 computedY = (computedY - minCMY) / (1 - minCMY) ;

 computedK = minCMY;

 

        color1.r = computedM-computedK;

        color1.g = 0;

        color1.b = computedM-computedK;

 

        return color1;

}

 

float4 PSYellow(float2 TexCoords : TEXCOORD0) : COLOR0

{

        float4 color1 = tex2D(ColorSampler, TexCoords);

 

        float computedC = 0;

        float computedM = 0;

        float computedY = 0;

        float computedK = 0;

 

         if (color1.r==0 && color1.g==0 && color1.b==0) {

                computedK = 1;

                return float4(0,0,0,1);

         }

 

 

        computedC = 1 - (color1.r);

        computedM = 1 - (color1.g);

        computedY = 1 - (color1.b);

 

        

         float minCMY = min(computedC,

              min(computedM,computedY));

 computedC = (computedC - minCMY) / (1 - minCMY) ;

 computedM = (computedM - minCMY) / (1 - minCMY) ;

 computedY = (computedY - minCMY) / (1 - minCMY) ;

 computedK = minCMY;

 

        color1.r = computedY - computedK;

        color1.g = computedY - computedK;

        color1.b = 0;

 

        return color1;

}

 

float4 PSBlack(float2 TexCoords : TEXCOORD0) : COLOR0

{

        float4 color1 = tex2D(ColorSampler, TexCoords);

 

        float computedC = 0;

        float computedM = 0;

        float computedY = 0;

        float computedK = 0;

 

         if (color1.r==0 && color1.g==0 && color1.b==0) {

                computedK = 1;

                return float4(0,0,0,1);

         }

 

 

        computedC = 1 - (color1.r);

        computedM = 1 - (color1.g);

        computedY = 1 - (color1.b);

 

        

         float minCMY = min(computedC,

              min(computedM,computedY));

 computedC = (computedC - minCMY) / (1 - minCMY) ;

 computedM = (computedM - minCMY) / (1 - minCMY) ;

 computedY = (computedY - minCMY) / (1 - minCMY) ;

 computedK = minCMY;

 

        color1.r = 1 - computedK;

        color1.g = 1 - computedK;

        color1.b = 1 - computedK;

 

        return color1;

}

 

 

technique Cyan

{

    pass Pass1

    {

        PixelShader = compile ps_2_0 PSCyan();

    }

}

 

technique Magenta

{

    pass Pass1

    {

        PixelShader = compile ps_2_0 PSMagenta();

    }

}

 

technique Yellow

{

    pass Pass1

    {

        PixelShader = compile ps_2_0 PSYellow();

    }

}

 

technique Black

{

    pass Pass1

    {

        PixelShader = compile ps_2_0 PSBlack();

    }

}

 


Собственно, результаты работы шейдеров достаточно очевидны. Мы разложили исходное изображение по 4 цветовым плоскостям: Голубой, Пурпурной, Желтой и Черной.

На Желтой плоскости ярче всего выглядят желтые цветы, на Пурпурной фиолетовые и т.д.

Исходный код:

Game1.cs

 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
namespace PixelShaderFun
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
 
        Texture2D background;
        Texture2D background2;
        Texture2D background3;
        Effect effect;
 
        int currentTehnique;
        KeyboardState oldKs;
 
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
 
            graphics.PreferredBackBufferWidth = 640;
            graphics.PreferredBackBufferHeight = 480;
        }
 
        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
 
            base.Initialize();
        }
 
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
 
            // TODO: use this.Content to load your game content here
            background = Content.Load<Texture2D>("tulips");
            background2 = Content.Load<Texture2D>("koala");
            background3 = Content.Load<Texture2D>("flowers");
 
            effect = Content.Load<Effect>("pixelEffect");
        }
 
        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }
 
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
 
            // TODO: Add your update logic here
            KeyboardState ks = Keyboard.GetState();
            if (ks.IsKeyDown(Keys.Space) && oldKs.IsKeyUp(Keys.Space))
            {
                currentTehnique = (currentTehnique + 1) % effect.Techniques.Count;
                effect.CurrentTechnique = effect.Techniques[currentTehnique];
            }
 
            oldKs = ks;
            Window.Title = effect.CurrentTechnique.Name;
            base.Update(gameTime);
        }
 
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);
 
            // TODO: Add your drawing code here            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, effect);
            spriteBatch.Draw(background3, new Rectangle(0, 0, 640, 480), Color.White);
            spriteBatch.End();
 
            base.Draw(gameTime);
        }
    }
}

 


pixelEffect.fx

 
sampler  ColorSampler  : register(s0);

float4 donothing(float2 TexCoords : TEXCOORD0) : COLOR0
{
          return tex2D(ColorSampler, TexCoords);
}
 
float4 PSRed(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.g = 0;
          color1.b = 0;
 
          return color1;
}
 
float4 PSGreen(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.r = 0;
          color1.b = 0;
 
          return color1;
}
 
float4 PSBlue(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.g = 0;
          color1.r = 0;
 
          return color1;
}
 
 
technique DoNothing
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 donothing();
    }
}
 
technique Red
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSRed();
    }
}
technique Green
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSGreen();
    }
}
technique Blue
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSBlue();
    }
}
 
 
float4 PSNoRed(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.r = 0;
 
          return color1;
}
 
float4 PSNoGreen(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.g = 0;
 
          return color1;
}
 
float4 PSNoBlue(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          color1.b = 0;
 
          return color1;
}
 
technique NoRed
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSNoRed();
    }
}
technique NoGreen
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSNoGreen();
    }
}
technique NoBlue
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSNoBlue();
    }
}
 
 
float4 PSCyan(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          float computedC = 0;
          float computedM = 0;
          float computedY = 0;
          float computedK = 0;
 
           if (color1.r==0 && color1.g==0 && color1.b==0) {
                    computedK = 1;
                    return float4(0,0,0,1);
           }
 
          computedC = 1 - (color1.r);
          computedM = 1 - (color1.g);
          computedY = 1 - (color1.b);
 
           float minCMY = min(computedC,
              min(computedM,computedY));
 computedC = (computedC - minCMY) / (1 - minCMY) ;
 computedM = (computedM - minCMY) / (1 - minCMY) ;
 computedY = (computedY - minCMY) / (1 - minCMY) ;
 computedK = minCMY;
 
          color1.r = 0;
          color1.g = computedC-computedK;
          color1.b = computedC-computedK;
 
          return color1;
}
 
float4 PSMagenta(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          float computedC = 0;
          float computedM = 0;
          float computedY = 0;
          float computedK = 0;
 
           if (color1.r==0 && color1.g==0 && color1.b==0) {
                    computedK = 1;
                    return float4(0,0,0,1);
           }
 
          computedC = 1 - (color1.r);
          computedM = 1 - (color1.g);
          computedY = 1 - (color1.b);
 
          
          
           float minCMY = min(computedC,
              min(computedM,computedY));
 computedC = (computedC - minCMY) / (1 - minCMY) ;
 computedM = (computedM - minCMY) / (1 - minCMY) ;
 computedY = (computedY - minCMY) / (1 - minCMY) ;
 computedK = minCMY;
 
          color1.r = computedM-computedK;
          color1.g = 0;
          color1.b = computedM-computedK;
 
          return color1;
}
 
float4 PSYellow(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          float computedC = 0;
          float computedM = 0;
          float computedY = 0;
          float computedK = 0;
 
           if (color1.r==0 && color1.g==0 && color1.b==0) {
                    computedK = 1;
                    return float4(0,0,0,1);
           }
 
 
          computedC = 1 - (color1.r);
          computedM = 1 - (color1.g);
          computedY = 1 - (color1.b);
 
          
           float minCMY = min(computedC,
              min(computedM,computedY));
 computedC = (computedC - minCMY) / (1 - minCMY) ;
 computedM = (computedM - minCMY) / (1 - minCMY) ;
 computedY = (computedY - minCMY) / (1 - minCMY) ;
 computedK = minCMY;
 
          color1.r = computedY - computedK;
          color1.g = computedY - computedK;
          color1.b = 0;
 
          return color1;
}
 
float4 PSBlack(float2 TexCoords : TEXCOORD0) : COLOR0
{
          float4 color1 = tex2D(ColorSampler, TexCoords);
 
          float computedC = 0;
          float computedM = 0;
          float computedY = 0;
          float computedK = 0;
 
           if (color1.r==0 && color1.g==0 && color1.b==0) {
                    computedK = 1;
                    return float4(0,0,0,1);
           }
 
 
          computedC = 1 - (color1.r);
          computedM = 1 - (color1.g);
          computedY = 1 - (color1.b);
 
          
           float minCMY = min(computedC,
              min(computedM,computedY));
 computedC = (computedC - minCMY) / (1 - minCMY) ;
 computedM = (computedM - minCMY) / (1 - minCMY) ;
 computedY = (computedY - minCMY) / (1 - minCMY) ;
 computedK = minCMY;
 
          color1.r = 1 - computedK;
          color1.g = 1 - computedK;
          color1.b = 1 - computedK;
 
          return color1;
}
 
 
technique Cyan
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSCyan();
    }
}
 
technique Magenta
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSMagenta();
    }
}
 
technique Yellow
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSYellow();
    }
}
 
technique Black
{
    pass Pass1
    {
        PixelShader = compile ps_2_0 PSBlack();
    }
}

 


					
Реклама
Запись опубликована в рубрике Компьютерная графика с метками . Добавьте в закладки постоянную ссылку.

2 комментария на «Забавы с пиксельным шейдером. Цветовые плоскости CMYK»

  1. Klk:

    Мне кажется, нагляднее было бы голубой, жёлтый и фиолетовый выводить на белом фоне, а не на чёрном.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s