Tono de cambio de un color RGB

Intento escribir una función para cambiar el tono de un color RGB. Específicamente lo estoy usando en una aplicación de iOS, pero la matemática es universal.

El siguiente gráfico muestra cómo cambian los valores R, G y B con respecto al matiz.

Gráfico de valores RGB entre tonalidades

Al ver eso, parece que debería ser relativamente simple escribir una función para cambiar el tono sin hacer ninguna conversión desagradable a un formato de color diferente que introduciría más errores (lo que podría ser un problema si continúa aplicando pequeños cambios a un color) , y sospecho que sería más costoso desde el punto de vista computacional.

Esto es lo que tengo hasta ahora, que funciona. Funciona perfectamente si está cambiando de amarillo puro o cian o magenta, pero de lo contrario se pone un poco pálido en algunos lugares.

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]upper) { returned[(i+2)%3] -= returned[i] - upper; returned[i]=upper; } break; } } return Color4fMake(returned[0], returned[1], returned[2], c.alpha); } 

Sé que puedes hacer esto con UIColors y cambiar el tono con algo como esto:

 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); 

pero no estoy loco por eso, ya que solo funciona en iOS 5, y entre la asignación de varios objetos de color y la conversión de RGB a HSB y viceversa, parece bastante exagerado.

Podría terminar usando una tabla de búsqueda o precalcular los colores en mi aplicación, pero realmente tengo curiosidad de saber si hay alguna manera de hacer que mi código funcione. ¡Gracias!

Editar por comentario cambiado “son todos” a “se puede aproximar linealmente por”.
Editar 2 agregar compensaciones.


Esencialmente, los pasos que deseas son

 RBG->HSV->Update hue->RGB 

Dado que estos se pueden aproximar mediante transformaciones de matriz lineal (es decir, son asociativos), puede realizarlo en un solo paso sin ninguna conversión desagradable o pérdida de precisión. Simplemente multiplique las matrices de transformación entre sí, y use eso para transformar sus colores.

Hay un paso a paso rápido aquí http://beesbuzz.biz/code/hsv_color_transforms.php

Aquí está el código de C ++ (con las transformaciones de saturación y valor eliminadas):

 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; } 

El espacio de color RGB describe un cubo. Es posible girar este cubo alrededor del eje diagonal desde (0,0,0) a (255,255,255) para efectuar un cambio de tono. Tenga en cuenta que algunos de los resultados se encontrarán fuera del rango de 0 a 255 y deberán recortarse.

Finalmente tuve la oportunidad de codificar este algoritmo. Está en Python, pero debería ser fácil traducirlo al idioma que elijas. La fórmula para la rotación 3D provino de http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

Editar: si vio el código que publiqué anteriormente, ignórelo. Estaba tan ansioso por encontrar una fórmula para la rotación que convertí una solución basada en matriz en una fórmula, sin darme cuenta de que la matriz era la mejor forma desde el principio. Todavía simplifiqué el cálculo de la matriz utilizando la constante sqrt (1/3) para los valores de vector de unidad de eje, pero esto es mucho más cercano en espíritu a la referencia y también más simple en el cálculo por píxel.

 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) 

Aquí hay algunos resultados de lo anterior:

Ejemplo de rotación de tonos

Puede encontrar una implementación diferente de la misma idea en http://www.graficaobscura.com/matrix/index.html

Me decepcionó la mayoría de las respuestas que encontré aquí, algunas fueron defectuosas y, básicamente, completamente mal. Terminé pasando más de 3 horas tratando de resolver esto. La respuesta de Mark Ransom es correcta, pero quiero ofrecer una solución C completa que también se verifica con MATLAB. He probado esto a fondo, y aquí está el código C:

 #include  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; } 

NOTA: La matriz de rotación solo depende del tono ( fHue ), por lo que una vez que haya calculado la matrix[3][3] , puede reutilizarla para cada píxel de la imagen que esté experimentando la misma transformación de matiz. Esto mejorará la eficiencia drásticamente. Aquí hay un código de MATLAB que verifica los resultados:

 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 

Aquí hay una entrada / salida de muestra reproducible por ambos códigos:

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

Entonces la entrada RGB de [86,52,30] se convirtió a [36,43,88] usando un tono de 210 .

Básicamente hay dos opciones:

  1. Convierta RGB -> HSV, cambie el tono, convierta HSV -> RGB
  2. Cambia el tono directamente con una transformación lineal

No estoy seguro de cómo implementar 2, pero básicamente tendrás que crear una matriz de transformación y filtrar la imagen a través de esta matriz. Sin embargo, esto volverá a colorear la imagen en lugar de cambiar solo el tono. Si esto está bien para usted, entonces esta podría ser una opción, pero si no se puede evitar una conversión.

Editar

Una pequeña investigación muestra esto , lo que confirma mis pensamientos. Para resumir: se debe preferir la conversión de RGB a HSV, si se desea un resultado exacto. Modificar la imagen RGB original mediante una transformación lineal también conduce a un resultado, pero esto matiza la imagen. La diferencia se explica de la siguiente manera: La conversión de RGB a HSV no es lineal, mientras que la transformación es lineal.

La publicación es antigua y el póster original buscaba el código ios; sin embargo, me enviaron aquí a través de una búsqueda de código visual básico, así que para todos los que me gusta, convertí el código de Mark a un módulo 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 

Puse términos comunes en variables (largas), pero de lo contrario es una conversión directa, funcionó bien para mis necesidades.

Por cierto, traté de dejar a Mark un voto favorable por su excelente código, pero no tuve suficientes votos para permitir que sea visible (Sugerencia, Sugerencia).

Parece que convertir a HSV tiene más sentido. Sass ofrece algunos increíbles ayudantes de color. Está en Ruby, pero podría ser útil.

http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html

Código excelente, pero me pregunto si puede ser más rápido si simplemente no usa self.matrix [2] [0], self.matrix [2] [1], self.matrix [2] [1]

Por lo tanto, set_hue_rotation se puede escribir simplemente como:

 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] = self.matrix[0][2] <---Not sure, if this is the right code, but i think you got the idea self.matrix[1][1] = self.matrix[0][0] self.matrix[1][2] = self.matrix[0][1] 

Scott … no exactamente. El algo parece funcionar igual que en HSL / HSV, pero más rápido. Además, si simplemente multiplicas los primeros 3 elementos de la matriz con el factor de gris, agregas / reduces luma.

Ejemplo … Escala de grises de Rec709 tiene esos valores [GrayRedFactor_Rec709: R $ 0.212671 GrayGreenFactor_Rec709: R $ 0.715160 GrayBlueFactor_Rec709: R $ 0.072169]

Cuando multiplique self.matrix [x] [x] con el corresponsal de GreyFactor, disminuirá la luminancia sin tocar la saturación Ex:

 def set_hue_rotation(self, degrees): cosA = cos(radians(degrees)) sinA = sin(radians(degrees)) self.matrix[0][0] = (cosA + (1.0 - cosA) / 3.0) * 0.212671 self.matrix[0][1] = (1./3. * (1.0 - cosA) - sqrt(1./3.) * sinA) * 0.715160 self.matrix[0][2] = (1./3. * (1.0 - cosA) + sqrt(1./3.) * sinA) * 0.072169 self.matrix[1][0] = self.matrix[0][2] <---Not sure, if this is the right code, but i think you got the idea self.matrix[1][1] = self.matrix[0][0] self.matrix[1][2] = self.matrix[0][1] 

Y lo opuesto también es cierto. Si divides en lugar de multiplicar, la luminosidad aumenta dramáticamente.

Por lo que estoy probando, estos algoritmos pueden ser un reemplazo maravilloso para HSL, siempre que no necesites saturación, por supuesto.

Intente hacer esto ... rote el tono a solo 1 grado (solo para forzar el funcionamiento del algoritmo mientras mantiene la misma sensibilidad de percepción de la imagen) y multiplique por esos factores.

Además, el algo de Mark produce resultados más precisos.

Por ejemplo, si gira el tono a 180º utilizando el espacio de color HSV, la imagen puede dar como resultado un tono de color rojizo.

Pero en el algo de Mark, la imagen gira correctamente. Los tonos de máscaras, por ejemplo (Hue = 17, Sat = 170, L = 160 en PSP) se convierten correctamente en azul, que tienen Hue alrededor de 144 en PSP, y todos los otros colores de la imagen giran correctamente.

El algo tiene sentido ya que Hue no es nada más, nada más que una función de Logaritmo de un arctano de Rojo, Verde, Azul según lo define esta fórmula:

 Hue = arctan((logR-logG)/(logR-logG+2*LogB)) 

Implementación de 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)); } } 

Para cualquiera que necesite el cambio de matiz descrito anteriormente (gamma-no corregido) como un sombreador HLSL Pixel parametrizado (lo cruzo para una aplicación WPF y pensé que incluso podría simplemente compartirlo):

  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; }