web-dev-qa-db-fra.com

Changer la teinte d'une couleur RVB

J'essaie d'écrire une fonction pour décaler la teinte d'une couleur RVB. Plus précisément, je l'utilise dans une application iOS, mais le calcul est universel.

Le graphique ci-dessous montre comment les valeurs R, G et B changent en fonction de la teinte.

Graph of RGB values across hues

En regardant cela, il semble qu'il soit relativement simple d'écrire une fonction permettant de décaler la teinte sans effectuer de conversions désagréables vers un format de couleur différent, ce qui introduirait davantage d'erreurs (ce qui pourrait poser problème si vous continuez d'appliquer de petits décalages à une couleur). , et je suppose que cela coûterait plus cher en calcul.

Voici ce que j'ai jusqu'ici quel type de travail. Cela fonctionne parfaitement si vous changez de jaune pur ou de cyan ou de magenta, mais sinon, il devient un peu squiffant à certains endroits.

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

Je sais que vous pouvez le faire avec UIColors et modifier la teinte avec quelque chose comme ceci:

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

mais cela ne me passionne pas, car cela ne fonctionne que dans iOS 5, et entre allouer un certain nombre d'objets de couleur et convertir de RVB en HSB, puis inversement, cela semble excessif.

Je pourrais finir par utiliser une table de correspondance ou pré-calculer les couleurs dans mon application, mais je suis vraiment curieux de savoir s'il existe un moyen de faire fonctionner mon code. Merci!

31
Anthony Mattox

Edit par commentaire modifié "sont tous" en "peut être linéairement approché par".
Edit 2 ajout de décalages.


Essentiellement, les étapes que vous souhaitez sont

RBG->HSV->Update hue->RGB

Étant donné que ces peuvent être approchées par transformations matricielles linéaires (c’est-à-dire qu’elles sont associatives), vous pouvez les exécuter en une seule étape sans conversion ni perte de précision. Vous ne faites que multiplier les matrices de transformation les unes avec les autres et les utiliser pour transformer vos couleurs.

Il y a une rapide étape par étape ici http://beesbuzz.biz/code/hsv_color_transforms.php

Voici le code C++ (avec la saturation et la transformation de valeur supprimées):

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;
}
15
Jacob Eggers

L'espace colorimétrique RVB décrit un cube. Il est possible de faire pivoter ce cube autour de l'axe diagonal de (0,0,0) à (255,255,255) pour effectuer un changement de teinte. Notez que certains résultats se situeront en dehors de la plage 0 à 255 et devront être coupés.

J'ai enfin eu l'occasion de coder cet algorithme. C'est en Python, mais il devrait être facile de traduire dans la langue de votre choix. La formule de rotation 3D provient de http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle

Edit: Si vous avez vu le code que j'ai posté précédemment, veuillez l'ignorer. J'étais tellement impatient de trouver une formule pour la rotation que j'ai converti une solution à base de matrice en une formule, ne réalisant pas que la matrice était toujours la meilleure forme. J'ai toujours simplifié le calcul de la matrice en utilisant la constante sqrt (1/3) pour les valeurs de vecteur unité d'axe, mais ceci est beaucoup plus proche de la référence et plus simple dans le calcul par pixel 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)

Voici quelques résultats de ce qui précède:

Hue rotation example

Vous pouvez trouver une implémentation différente de la même idée sur http://www.graficaobscura.com/matrix/index.html

36
Mark Ransom

J'ai été déçu par la plupart des réponses que j'ai trouvées ici, certaines étaient imparfaites et fondamentalement fausses. J'ai fini par passer plus de 3 heures à essayer de comprendre cela. La réponse de Mark Ransom est correcte, mais je souhaite proposer une solution complète en C vérifiée également par MATLAB. J'ai testé cela à fond, et voici le code 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;
}

REMARQUE: La matrice de rotation ne dépend que de la teinte (fHue). Ainsi, une fois que vous avez calculé matrix[3][3], vous pouvez le réutiliser pour chaque pixel de l'image subissant la même transformation de teinte! Cela améliorera considérablement l’efficacité. Voici un code MATLAB qui vérifie les résultats:

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

Voici un exemple d’entrée/sortie reproductible par les deux codes:

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

Ainsi, le RVB d'entrée de [86,52,30] a été converti en [36,43,88] en utilisant une teinte de 210.

6
MasterHD

Fondamentalement, il y a deux options:

  1. Convertir RVB -> HSV, changer de teinte, convertir HSV -> RGB
  2. Changer la teinte directement avec une transformation linéaire

Je ne suis pas vraiment sûr de la mise en œuvre de 2, mais vous devrez créer une matrice de transformation et filtrer l'image à travers cette matrice. Cependant, cela recoloriera l'image au lieu de ne changer que la teinte. Si cela vous convient, alors cela pourrait être une option, mais sinon, une conversion ne peut être évitée.

Modifier

Une petite recherche montre ceci , ce qui confirme mes pensées. Pour résumer: La conversion de RVB à HSV devrait être préférée si un résultat exact est souhaité. La modification de l’image RVB d’origine par une transformation linéaire conduit également à un résultat, mais cela ternit l’image. La différence s’explique comme suit: La conversion de RVB à HSV n’est pas linéaire, alors que la transformation est linéaire.

3
Sebastian Dressler

Le message est vieux et l'affiche originale cherchait du code ios. Cependant, j'ai été envoyé ici via une recherche de code visuel simple. J'ai donc converti le code de Mark en un module 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

Je mets des termes communs dans de (longues) variables, mais sinon c'est une conversion simple - qui a bien fonctionné pour mes besoins.

En passant, j’ai essayé de laisser Mark un vote positif pour son excellent code, mais je n’avais pas moi-même assez de votes pour le rendre visible (indice, indice).

2
Dave P.

Implémentation Javascript (basé sur le PHP de Vladimir ci-dessus)

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));
}
1
Matt Blackstone

Il semble plus logique de se convertir au HSV. Sass fournit des aides de couleurs incroyables. C'est en Ruby, mais cela pourrait être utile. 

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

0
Scott Messinger

Scott .... pas exactement. L'algo semble fonctionner de la même manière que dans HSL/HSV, mais plus rapidement. De plus, si vous multipliez simplement les 3 premiers éléments du tableau par le facteur pour le gris, vous ajoutez/diminuez la luma.

Exemple ... Les niveaux de gris de Rec709 ont ces valeurs [GrayRedFactor_Rec709: R $ 0.212671 GrayGreenFactor_Rec709: R $ 0.715160 GrayBlueFactor_Rec709: R $ 0.072169]

Lorsque vous multipliez Self.matrix [x] [x] avec le correspondant GreyFactor, vous diminuez la luminance sans toucher à la saturation 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]

Et le contraire est également vrai. Si vous divisez au lieu de multiplier, la luminosité est considérablement accrue.

De ce que j’essaie de tester, ces algorithmes peuvent être un excellent substitut pour HSL, tant que vous n’avez pas besoin de saturation, bien sûr.

Essayez de faire ceci ... faites pivoter la teinte à seulement 1 degré (Juste pour forcer l’algo à fonctionner correctement tout en conservant la même sensibilité de perception de l’image), et multipliez par ces facteurs.

0

Pour tous ceux qui ont besoin du décalage de teinte décrit ci-dessus (non corrigé par les gamma) en tant que shader HLSL Pixel paramétré (je le fais ensemble pour une application WPF et je pensais pouvoir le partager)

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

Code excellent, mais je me demande s’il peut être plus rapide si vous n’utilisez simplement pas self.matrix [2] [0], self.matrix [2] [1], self.matrix [2] [1]

Par conséquent, set_hue_rotation peut être écrit simplement comme suit:

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

De plus, l’algo de Mark produit des résultats plus précis.

Par exemple, si vous faites pivoter la teinte sur 180º à l'aide de l'espace de couleurs HSV, l'image peut donner une couleur de ton rougeâtre.

Mais sur l’algo de Mark, l’image est correctement pivotée. Les tons de skins, par exemple (Teinte = 17, Sat = 170, L = 160 sur PSP), deviennent correctement bleus et ont une teinte d'environ 144 sur PSP, et toutes les autres couleurs de l'image sont correctement pivotées.

L'algo a un sens puisque Hue n'est rien de plus, rien d'autre qu'une fonction logarithmique d'un arctan de rouge, vert, bleu tel que défini par cette formule:

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

Implémentation 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));
    }
}
0
Vladimir Klubov