Estoy tratando de escribir una función para cambiar el tono de un color RGB. Específicamente, lo estoy usando en una aplicación de iOS, pero las matemáticas son universales.
El siguiente gráfico muestra cómo cambian los valores R, G y B con respecto al tono.
Mirando eso, parece que debería ser relativamente simple escribir una función para cambiar el tono sin hacer conversiones desagradables 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 computacionalmente.
Esto es lo que tengo hasta ahora que tipo de obras. Funciona perfectamente si está cambiando de amarillo puro, cian o magenta, pero de lo contrario se vuelve un poco esponjoso 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]<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);
}
Sé que puedes hacer esto con UIColors y cambiar el tono con algo como esto:
pero no estoy loco por eso, ya que solo funciona en iOS 5, y entre la asignación de una cantidad de objetos de color y la conversión de RGB a HSB y luego de regreso, parece bastante exagerado.
Podría terminar usando una tabla de búsqueda o precalcular los colores en mi aplicación, pero tengo mucha curiosidad por saber si hay una manera de hacer que mi código funcione. ¡Gracias!
No he leído su código, pero según ese gráfico, ¿no necesitará transformar su color RGB en HSV para saber dónde se encuentra en ese gráfico, para que pueda saber cómo moverse?
–Oliver Charlesworth
14 de diciembre de 2011 a las 16:24
marca rescate
El espacio de color RGB describe un cubo. Es posible rotar este cubo alrededor del eje diagonal de (0,0,0) a (255,255,255) para efectuar un cambio de tono. Tenga en cuenta que algunos de los resultados estarán fuera del rango de 0 a 255 y deberán recortarse.
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 una matriz en una fórmula, sin darme cuenta de que la matriz era la mejor forma todo el tiempo. Todavía simplifiqué el cálculo de la matriz usando la constante sqrt (1/3) para los valores del vector unitario del eje, pero esto es mucho más cercano en espíritu a la referencia y más simple en el cálculo por píxel. apply así como.
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)
Me ahorró varias horas. ¡¿Cómo es que esto no tiene más votos a favor?!
– Escher
10 de diciembre de 2015 a las 10:33
@Escher por muchas razones: 1. Me tomó días desarrollar completamente la respuesta. 2. No es algo que mucha gente necesite hacer. 3. La solución obvia de convertir a un espacio de color con un componente de tono es simple y funciona lo suficientemente bien para muchas personas.
– Mark Ransom
10 de diciembre de 2015 a las 13:14
@AlicanC Cuando escribí el código pensé que sería obvio que apply necesitaría ser llamado por píxel, y set_hue_rotation solo se usaría para la configuración. Creo que estaba equivocado.
– Mark Ransom
19 de enero de 2016 a las 23:52
Encuentro que esta solución funciona mejor que la que encontré en SO. muy buena rotación de tonalidades
– Andrea Bogazzi
6 de agosto de 2017 a las 12:36
@Attila, la diagonal de un cubo de 256x256x256 es más larga que 256, por lo que cuando lo gira, esas esquinas simplemente sobresalen. Es porque RGB describe un cubo y no una esfera.
– Mark Ransom
24 de abril de 2020 a las 16:44
jacob eggers
Editar por comentario cambió “son todos” a “puede ser aproximado linealmente por”. Editar 2 añadiendo compensaciones.
Esencialmente, los pasos que desea son
RBG->HSV->Update hue->RGB
Dado que estos se puede aproximar por Transformaciones de matriz lineal (es decir, son asociativas), puede realizarlas en un solo paso sin ninguna conversión desagradable o pérdida de precisión. Simplemente multiplica las matrices de transformación entre sí y usa eso para transformar tus colores.
Aquí está el código 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;
}
Como autor original de la página a la que se vinculó, me gustaría señalar que RGB->HSV y HSV->RGB no son transformaciones de matriz lineal. Lo que realmente está haciendo ese código es transformar RGB->YIQ (que es un equivalente lineal de HSV) y rotar a través del plano IQ. Tampoco genera los resultados que la gente espera a veces. Sin embargo, tratar de explicar eso y por qué HSV es un concepto de color ridículo para empezar no encajará en este cuadro de comentarios. 🙂
– esponjoso
9 de febrero de 2012 a las 7:31
No puedo reproducir los resultados correctos con su método, sin embargo, la respuesta de Mark Ransom funcionó muy bien. He aquí un ejemplo: Entrada ([R,G,B],H) = ([86,52,30]210) y la salida es [-31,15,2] por su método, y [36,43,88] con la de Mark. No creo que el error de redondeo pueda explicar esta diferencia drástica, algo anda mal.
– Master HD
27 mayo 2015 a las 15:09
@MasterHD Parece que olvidé las compensaciones, pero sigo teniendo una diferencia [28,75,62]así que no estoy seguro de qué está mal ahora.
–Jacob Eggers
27 mayo 2015 a las 22:44
@mcd escribió: “hay un signo cambiado en la última línea de cálculos.+ (.114-.886*U-.203*W)*in.b;debiera ser+ (.114+.886*U-.203*W)*in.b;“. (El OP no tiene suficiente reputación para comentar).
– Nisse Engström
2 de junio de 2015 a las 17:18
@fluffy Sé que esto fue hace años, pero ¿es posible que pueda actualizar su código con dígitos más significativos? Estoy ejecutando su código en Haxe (no en C++) y obtengo algunos resultados extraños. Estoy seguro de que estoy perdiendo algo de precisión, y me pregunto si los dígitos más significativos ayudarían.
– cenizas999
21 de febrero de 2016 a las 14:48
Maestro HD
Me decepcionó la mayoría de las respuestas que encontré aquí, algunas eran defectuosas y básicamente incorrectas. 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 esté verificada con MATLAB. He probado esto a fondo, y aquí está el código 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;
}
NOTA: La matriz de rotación solo depende del Hue (fHue), por lo que una vez que haya calculado matrix[3][3]puede reutilizar ¡para cada píxel de la imagen que está experimentando la misma transformación de tono! 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 que ambos códigos pueden reproducir:
TransformH(86,52,30,210)
ans =
36
43
88
Así que la entrada RGB de [86,52,30] fue convertido a [36,43,88] usando un tono de 210.
Oye, gracias. Sin embargo, descubrí que si haces esto en un bucle, los colores eventualmente se oscurecen, por lo que debes agregar un poco de brillo. Probablemente alguna pérdida de redondeo. por ejemplo, lo hice: float bright = 1.01; int r = (flotante)fg.r * brillante; si (r > 255) { r = 255; } fg.r = r; int g = (float)fg.g * brillante; si (g > 255) { g = 255; } fg.g = g; int b = (flotante)fg.b * brillante; si (b > 255) { b = 255; } fg.b = b;
– Goblinhack
15 de noviembre de 2020 a las 16:42
Implementación de Javascript (basada en el PHP de Vladimir anterior)
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));
}
Cambia el tono directamente con una transformación lineal
No estoy muy 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, no se puede evitar una conversión.
Editar
Una pequeña investigación muestra esta, lo que confirma mis pensamientos. Para resumir: se debe preferir la conversión de RGB a HSV, si se desea un resultado exacto. La modificación de la imagen RGB original mediante una transformación lineal también conduce a un resultado, pero esto tiñe más bien 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.
Hizo una edición. Lo haré directamente la próxima vez, sin recordatorio;)
– Sebastián Dressler
14 de diciembre de 2011 a las 19:23
david p
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, por lo que para todos aquellos como yo, convertí el código de Mark en 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 por lo demás es una conversión sencilla: funcionó bien para mis necesidades.
Por cierto, traté de darle a Mark un voto a favor por su excelente código, pero yo mismo no tuve suficientes votos para permitir que sea visible (Pista, Pista).
Hizo una edición. Lo haré directamente la próxima vez, sin recordatorio;)
Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos.
Configurar y más información
No he leído su código, pero según ese gráfico, ¿no necesitará transformar su color RGB en HSV para saber dónde se encuentra en ese gráfico, para que pueda saber cómo moverse?
–Oliver Charlesworth
14 de diciembre de 2011 a las 16:24