Сдвиг оттенка цвета RGB - PullRequest
33 голосов
/ 14 декабря 2011

Я пытаюсь написать функцию для смещения оттенка цвета RGB. В частности, я использую его в приложении для iOS, но математика универсальна.

График ниже показывает, как значения R, G и B изменяются относительно оттенка.

Graph of RGB values across hues

Глядя на это, кажется, что было бы относительно просто написать функцию для смещения оттенка без каких-либо неприятных преобразований в другой цветовой формат, который привел бы к большей ошибке (что могло бы стать проблемой, если продолжать применять небольшие сдвиги к цвет), и я подозреваю, что будет в вычислительном отношении дороже.

Вот что у меня есть, какие работы. Он отлично работает, если вы переходите от чистого желтого, голубого или пурпурного, но в некоторых местах он становится немного шатким.

Color4f ShiftHue(Color4f c, float d) {
    if (d==0) {
        return c;
    }
    while (d<0) {
        d+=1;
    }

    d *= 3;

    float original[] = {c.red, c.green, c.blue};
    float returned[] = {c.red, c.green, c.blue};

    // big shifts
    for (int i=0; i<3; i++) {
        returned[i] = original[(i+((int) d))%3];
    }
    d -= (float) ((int) d);
    original[0] = returned[0];
    original[1] = returned[1];
    original[2] = returned[2];

    float lower = MIN(MIN(c.red, c.green), c.blue);
    float upper = MAX(MAX(c.red, c.green), c.blue);

    float spread = upper - lower;
    float shift  = spread * d * 2;

    // little shift
    for (int i = 0; i < 3; ++i) {
        // if middle value
        if (original[(i+2)%3]==upper && original[(i+1)%3]==lower) {
            returned[i] -= shift;
            if (returned[i]<lower) {
                returned[(i+1)%3] += lower - returned[i];
                returned[i]=lower;
            } else
                if (returned[i]>upper) {
                    returned[(i+2)%3] -= returned[i] - upper;
                    returned[i]=upper;
                }
            break;
        }
    }

    return Color4fMake(returned[0], returned[1], returned[2], c.alpha);
}

Я знаю, что вы можете сделать это с помощью UIColors и изменить цвет на что-то вроде этого:

CGFloat hue;
CGFloat sat;
CGFloat bri;
[[UIColor colorWithRed:parent.color.red green:parent.color.green blue:parent.color.blue alpha:1] getHue:&hue saturation:&sat brightness:&bri alpha:nil];
hue -= .03;
if (hue<0) {
    hue+=1;
}
UIColor *tempColor = [UIColor colorWithHue:hue saturation:sat brightness:bri alpha:1];
const float* components= CGColorGetComponents(tempColor.CGColor);
color = Color4fMake(components[0], components[1], components[2], 1);

но я не в восторге от этого, так как он работает только в iOS 5, и между выделением ряда цветных объектов и преобразованием из RGB в HSB, а затем обратно это кажется довольно излишним.

Я мог бы в конечном итоге использовать справочную таблицу или предварительно рассчитать цвета в моем приложении, но мне действительно любопытно, есть ли способ заставить мой код работать. Спасибо!

Ответы [ 13 ]

38 голосов
/ 14 декабря 2011

Цветовое пространство RGB описывает куб. Можно повернуть этот куб вокруг диагональной оси от (0,0,0) до (255,255,255), чтобы изменить оттенок. Обратите внимание, что некоторые результаты будут выходить за пределы диапазона от 0 до 255. Их нужно будет обрезать.

Я наконец-то получил шанс закодировать этот алгоритм. Это на Python, но его должно быть легко перевести на язык по вашему выбору. Формула для трехмерного вращения пришла от http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

Редактировать: Если вы видели код, который я разместил ранее, пожалуйста, игнорируйте его. Я так хотел найти формулу для вращения, что я преобразовал решение на основе матрицы в формулу, не понимая, что матрица была лучшей формой с самого начала. Я все еще упростил вычисление матрицы, используя константу sqrt (1/3) для значений вектора единицы оси, но это намного ближе по духу к эталону и проще в расчете на пиксель apply.

from math import sqrt,cos,sin,radians

def clamp(v):
    if v < 0:
        return 0
    if v > 255:
        return 255
    return int(v + 0.5)

class RGBRotate(object):
    def __init__(self):
        self.matrix = [[1,0,0],[0,1,0],[0,0,1]]

    def set_hue_rotation(self, degrees):
        cosA = cos(radians(degrees))
        sinA = sin(radians(degrees))
        self.matrix[0][0] = cosA + (1.0 - cosA) / 3.0
        self.matrix[0][1] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
        self.matrix[0][2] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
        self.matrix[1][0] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
        self.matrix[1][1] = cosA + 1./3.*(1.0 - cosA)
        self.matrix[1][2] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
        self.matrix[2][0] = 1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA
        self.matrix[2][1] = 1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA
        self.matrix[2][2] = cosA + 1./3. * (1.0 - cosA)

    def apply(self, r, g, b):
        rx = r * self.matrix[0][0] + g * self.matrix[0][1] + b * self.matrix[0][2]
        gx = r * self.matrix[1][0] + g * self.matrix[1][1] + b * self.matrix[1][2]
        bx = r * self.matrix[2][0] + g * self.matrix[2][1] + b * self.matrix[2][2]
        return clamp(rx), clamp(gx), clamp(bx)

Вот некоторые результаты из вышеперечисленного:

Hue rotation example

Вы можете найти другую реализацию той же идеи в http://www.graficaobscura.com/matrix/index.html

16 голосов
/ 14 декабря 2011

Изменить за комментарий изменен "все" на "может быть линейно аппроксимирован".
Редактировать 2 , добавляя смещения.


По сути, вы хотите шаги

RBG->HSV->Update hue->RGB

Поскольку эти могут быть аппроксимированы линейными матричными преобразованиями (т.е. они являются ассоциативными), вы можете выполнить это за один шаг без какого-либо неприятного преобразования или потери точности. Вы просто умножаете матрицы преобразования друг на друга и используете их для преобразования ваших цветов.

Здесь есть быстрый шаг за шагом http://beesbuzz.biz/code/hsv_color_transforms.php

Вот код C ++ (с удаленными преобразованиями насыщенности и значения):

Color TransformH(
    const Color &in,  // color to transform
    float H
)
{
  float U = cos(H*M_PI/180);
  float W = sin(H*M_PI/180);

  Color ret;
  ret.r = (.299+.701*U+.168*W)*in.r
    + (.587-.587*U+.330*W)*in.g
    + (.114-.114*U-.497*W)*in.b;
  ret.g = (.299-.299*U-.328*W)*in.r
    + (.587+.413*U+.035*W)*in.g
    + (.114-.114*U+.292*W)*in.b;
  ret.b = (.299-.3*U+1.25*W)*in.r
    + (.587-.588*U-1.05*W)*in.g
    + (.114+.886*U-.203*W)*in.b;
  return ret;
}
6 голосов
/ 27 мая 2015

Я был разочарован большинством ответов, которые я нашел здесь, некоторые были ошибочными и в основном категорически неправильными. Я потратил более 3 часов, пытаясь понять это. Ответ Марка Рэнсома правильный, но я хочу предложить полное решение C, которое также проверено с помощью MATLAB. Я проверил это тщательно, и вот код C:

#include <math.h>
typedef unsigned char BYTE; //define an "integer" that only stores 0-255 value

typedef struct _CRGB //Define a struct to store the 3 color values
{
    BYTE r;
    BYTE g;
    BYTE b;
}CRGB;

BYTE clamp(float v) //define a function to bound and round the input float value to 0-255
{
    if (v < 0)
        return 0;
    if (v > 255)
        return 255;
    return (BYTE)v;
}

CRGB TransformH(const CRGB &in, const float fHue)
{
    CRGB out;
    const float cosA = cos(fHue*3.14159265f/180); //convert degrees to radians
    const float sinA = sin(fHue*3.14159265f/180); //convert degrees to radians
    //calculate the rotation matrix, only depends on Hue
    float matrix[3][3] = {{cosA + (1.0f - cosA) / 3.0f, 1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA, 1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA},
        {1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA, cosA + 1.0f/3.0f*(1.0f - cosA), 1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA},
        {1.0f/3.0f * (1.0f - cosA) - sqrtf(1.0f/3.0f) * sinA, 1.0f/3.0f * (1.0f - cosA) + sqrtf(1.0f/3.0f) * sinA, cosA + 1.0f/3.0f * (1.0f - cosA)}};
    //Use the rotation matrix to convert the RGB directly
    out.r = clamp(in.r*matrix[0][0] + in.g*matrix[0][1] + in.b*matrix[0][2]);
    out.g = clamp(in.r*matrix[1][0] + in.g*matrix[1][1] + in.b*matrix[1][2]);
    out.b = clamp(in.r*matrix[2][0] + in.g*matrix[2][1] + in.b*matrix[2][2]);
    return out;
}

ПРИМЕЧАНИЕ. Матрица вращения зависит только от оттенка (fHue), поэтому после вычисления matrix[3][3] вы можете повторно использовать для каждого пикселя в изображение, которое подвергается тому же преобразованию оттенка! Это значительно повысит эффективность. Вот код MATLAB, который проверяет результаты:

function out = TransformH(r,g,b,H)
    cosA = cos(H * pi/180);
    sinA = sin(H * pi/180);

    matrix = [cosA + (1-cosA)/3, 1/3 * (1 - cosA) - sqrt(1/3) * sinA, 1/3 * (1 - cosA) + sqrt(1/3) * sinA;
          1/3 * (1 - cosA) + sqrt(1/3) * sinA, cosA + 1/3*(1 - cosA), 1/3 * (1 - cosA) - sqrt(1/3) * sinA;
          1/3 * (1 - cosA) - sqrt(1/3) * sinA, 1/3 * (1 - cosA) + sqrt(1/3) * sinA, cosA + 1/3 * (1 - cosA)];

    in = [r, g, b]';
    out = round(matrix*in);
end

Вот пример ввода / вывода, который воспроизводился обоими кодами:

TransformH(86,52,30,210)
ans =
    36
    43
    88

Таким образом, входной RGB [86,52,30] был преобразован в [36,43,88] с использованием оттенка 210.

3 голосов
/ 14 декабря 2011

В основном есть два варианта:

  1. Преобразование RGB -> HSV, изменение оттенка, преобразование HSV -> RGB
  2. Изменение оттенка напрямую с помощью линейного преобразования

Я не совсем уверен, как реализовать 2, но в основном вам придется создать матрицу преобразования и отфильтровать изображение через эту матрицу.Однако это изменит цвет изображения вместо изменения только оттенка.Если это хорошо для вас, тогда это может быть вариант, но если нет, то избежать конверсии невозможно.

Редактировать

Небольшое исследование показывает это , что подтверждает мои мысли.Подводя итог: преобразование из RGB в HSV должно быть предпочтительным, если требуется точный результат.Изменение исходного изображения RGB с помощью линейного преобразования также приводит к результату, но это скорее окрашивает изображение.Разница объясняется следующим образом: преобразование из RGB в HSV является нелинейным, тогда как преобразование является линейным.

2 голосов
/ 13 февраля 2019

Реализация JavaScript (на основе вышеприведенного Владимирского PHP)

const deg = Math.PI / 180;

function rotateRGBHue(r, g, b, hue) {
  const cosA = Math.cos(hue * deg);
  const sinA = Math.sin(hue * deg);
  const neo = [
    cosA + (1 - cosA) / 3,
    (1 - cosA) / 3 - Math.sqrt(1 / 3) * sinA,
    (1 - cosA) / 3 + Math.sqrt(1 / 3) * sinA,
  ];
  const result = [
    r * neo[0] + g * neo[1] + b * neo[2],
    r * neo[2] + g * neo[0] + b * neo[1],
    r * neo[1] + g * neo[2] + b * neo[0],
  ];
  return result.map(x => uint8(x));
}

function uint8(value) {
  return 0 > value ? 0 : (255 < value ? 255 : Math.round(value));
}
2 голосов
/ 08 февраля 2016

Сообщение старое, и оригинальный постер искал код ios - однако меня послали сюда через поиск кода для базовых визуальных элементов, поэтому для всех, кто, как я, я преобразовал код Марка в модуль vb .net

Public Module HueAndTry    
    Public Function ClampIt(ByVal v As Double) As Integer    
        Return CInt(Math.Max(0F, Math.Min(v + 0.5, 255.0F)))    
    End Function    
    Public Function DegreesToRadians(ByVal degrees As Double) As Double    
        Return degrees * Math.PI / 180    
    End Function    
    Public Function RadiansToDegrees(ByVal radians As Double) As Double    
        Return radians * 180 / Math.PI    
    End Function    
    Public Sub HueConvert(ByRef rgb() As Integer, ByVal degrees As Double)
        Dim selfMatrix(,) As Double = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}
        Dim cosA As Double = Math.Cos(DegreesToRadians(degrees))
        Dim sinA As Double = Math.Sin(DegreesToRadians(degrees))
        Dim sqrtOneThirdTimesSin As Double = Math.Sqrt(1.0 / 3.0) * sinA
        Dim oneThirdTimesOneSubCos As Double = 1.0 / 3.0 * (1.0 - cosA)
        selfMatrix(0, 0) = cosA + (1.0 - cosA) / 3.0
        selfMatrix(0, 1) = oneThirdTimesOneSubCos - sqrtOneThirdTimesSin
        selfMatrix(0, 2) = oneThirdTimesOneSubCos + sqrtOneThirdTimesSin
        selfMatrix(1, 0) = selfMatrix(0, 2)
        selfMatrix(1, 1) = cosA + oneThirdTimesOneSubCos
        selfMatrix(1, 2) = selfMatrix(0, 1)
        selfMatrix(2, 0) = selfMatrix(0, 1)
        selfMatrix(2, 1) = selfMatrix(0, 2)
        selfMatrix(2, 2) = cosA + oneThirdTimesOneSubCos
        Dim rx As Double = rgb(0) * selfMatrix(0, 0) + rgb(1) * selfMatrix(0, 1) + rgb(2) * selfMatrix(0, 2)
        Dim gx As Double = rgb(0) * selfMatrix(1, 0) + rgb(1) * selfMatrix(1, 1) + rgb(2) * selfMatrix(1, 2)
        Dim bx As Double = rgb(0) * selfMatrix(2, 0) + rgb(1) * selfMatrix(2, 1) + rgb(2) * selfMatrix(2, 2)
        rgb(0) = ClampIt(rx)
        rgb(1) = ClampIt(gx)
        rgb(2) = ClampIt(bx)
    End Sub
End Module

Я помещаю общие термины в (длинные) переменные, но в противном случае это простое преобразование - сработало для моих нужд.

Кстати, я пытался оставить Марка голос за его отличный код, но у меня не было достаточно голосов, чтобы он был виден (Подсказка, Подсказка).

1 голос
/ 07 июня 2019

версия WebGL:

vec3 hueShift(vec3 col, float shift){
    vec3 m = vec3(cos(shift), -sin(shift) * .57735, 0);
    m = vec3(m.xy, -m.y) + (1. - m.x) * .33333;
    return mat3(m, m.zxy, m.yzx) * col;
}
1 голос
/ 14 декабря 2011

Кажется, преобразование в HSV имеет больше смысла.Sass предоставляет несколько удивительных помощников цвета.Это в рубине, но это может быть полезным.

http://sass -lang.com / документы / yardoc / Sass / Script / Functions.html

0 голосов
/ 23 июля 2018

Для тех, кому необходим описанный выше (без гамма-коррекции) сдвиг оттенка в качестве параметризованного пиксельного шейдера HLSL (я прошел через это вместе для приложения WPF и подумал, что могу даже просто поделиться им):

    sampler2D implicitInput : register(s0);
    float factor : register(c0);

    float4 main(float2 uv : TEXCOORD) : COLOR
    {
            float4 color = tex2D(implicitInput, uv);

            float h = 360 * factor;          //Hue
            float s = 1;                     //Saturation
            float v = 1;                     //Value
            float M_PI = 3.14159265359;

            float vsu = v * s*cos(h*M_PI / 180);
            float vsw = v * s*sin(h*M_PI / 180);

            float4 result;
            result.r = (.299*v + .701*vsu + .168*vsw)*color.r
                            + (.587*v - .587*vsu + .330*vsw)*color.g
                            + (.114*v - .114*vsu - .497*vsw)*color.b;
            result.g = (.299*v - .299*vsu - .328*vsw)*color.r
                            + (.587*v + .413*vsu + .035*vsw)*color.g
                            + (.114*v - .114*vsu + .292*vsw)*color.b;
            result.b = (.299*v - .300*vsu + 1.25*vsw)*color.r
                            + (.587*v - .588*vsu - 1.05*vsw)*color.g
                            + (.114*v + .886*vsu - .203*vsw)*color.b;;
            result.a = color.a;

            return result;
    }
0 голосов
/ 06 марта 2018

Реализация PHP:

class Hue
{
    public function convert(int $r, int $g, int $b, int $hue)
    {
        $cosA = cos($hue * pi() / 180);
        $sinA = sin($hue * pi() / 180);

        $neo = [
            $cosA + (1 - $cosA) / 3,
            (1 - $cosA) / 3 - sqrt(1 / 3) * $sinA,
            (1 - $cosA) / 3 + sqrt(1 / 3) * $sinA,
        ];

        $result = [
            $r * $neo[0] + $g * $neo[1] + $b * $neo[2],
            $r * $neo[2] + $g * $neo[0] + $b * $neo[1],
            $r * $neo[1] + $g * $neo[2] + $b * $neo[0],
        ];

        return array_map([$this, 'crop'], $result);
    }

    private function crop(float $value)
    {
        return 0 > $value ? 0 : (255 < $value ? 255 : (int)round($value));
    }
}
...