Cambiar el tono de un color RGB

13 minutos de lectura

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.

Gráfico de valores RGB en tonalidades

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:

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

avatar de usuario
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.

Finalmente tuve la oportunidad de codificar este algoritmo. Está en Python, pero debería ser fácil de traducir al idioma de su elección. La fórmula para la rotación 3D vino 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 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)

Aquí hay algunos resultados de lo anterior:

Ejemplo de rotación de tono

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

  • 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

avatar de usuario
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.

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

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

avatar de usuario
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));
}

avatar de usuario
sebastian dressler

Básicamente hay dos opciones:

  1. Convertir RGB -> HSV, cambiar tono, convertir HSV -> RGB
  2. 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

avatar de usuario
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;)

    – Sebastián Dressler

    14 de diciembre de 2011 a las 19:23

avatar de usuario
artur ilkaev

Versión 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;
}

¿Ha sido útil esta solución?

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
Privacidad