How to adjust Hue Saturation and Brightness of an image or widget in Flutter?
Asked Answered
R

5

8

In my Flutter app, I have an image and three sliders, one for Hue, one for Saturation, and one for Brightness, and I'm trying to figure out how to use the ColorFiltered widget to make these adjustments, but I can't figure out what to put in for the ColorFilter.matrix.

My code looks something like this:

ColorFiltered(
  colorFilter: ColorFilter.matrix(
    // What goes here?
  ),
  child: Container(
    decoration: BoxDecoration(
      image: DecorationImage(
        fit: BoxFit.cover,
        image: NetworkImage(myImageUrl),
      )
    )
  )
)

Does anyone know how to generate a color filter matrix based on HSV values?

Rabblerouser answered 2/11, 2020 at 3:54 Comment(0)
D
1

I've added BananaNeil's answer into the Themed package.

Use the provided ChangeColors widget to change the brightness, saturation and hue of any widget, including images, like this:

ChangeColors(
   hue: 0.55,
   brightness: 0.2,
   saturation: 0.1,
   child: Image.asset('myImage.png'),
);

Parameters are:

brightness

  • Negative value will make it darker (-1 is darkest).
  • Positive value will make it lighter (1 is the maximum, but you can go above it).
  • 0.0 is unchanged.

saturation

  • Negative value will make it less saturated (-1 is greyscale).
  • Positive value will make it more saturated (1 is the maximum, but you can go above it).
  • 0.0 is unchanged.

hue

  • From -1.0 to 1.0 (Note: 1.0 wraps into -1.0, such as 1.2 is the same as -0.8).
  • 0.0 is unchanged. Adding or subtracting multiples of 2.0 also keeps it unchanged.

Please note: The difference from the ChangeColors widget to BananaNeil's answer is that the widget is a proper StatelessWidget, code is null-safe, it fixes the limits of saturation so that you don't get weird effects, and the documentation was added explaining the parameters. If you don't want to add a package, just copy the code from GitHub.

Depilatory answered 23/11, 2021 at 20:47 Comment(2)
SO COOL! Thank you so much! πŸ™ – Rabblerouser
I did not double check this, but I'm going to mark it as the accepted answer because it seems to do what I did, but made the implementation much easier. – Rabblerouser
R
22

To solve this issue, I built an ImageFilter widget, which can be used like this:

ImageFilter(
  hue: 0.1,
  brightness: -0.6,
  saturation: 0.8,
  child: Container(
    decoration: BoxDecoration(
      image: DecorationImage(
        fit: BoxFit.cover,
        image: NetworkImage(imageUrl),
      ),
    )
  )
)

It takes percentage inputs in decimal form between -1 and 1.

Which uses 3 layers of the ColorFiltered widget:

Widget ImageFilter({brightness, saturation, hue, child}) {
  return ColorFiltered(
    colorFilter: ColorFilter.matrix(
      ColorFilterGenerator.brightnessAdjustMatrix(
        value: brightness,
      )
    ),
    child: ColorFiltered(
      colorFilter: ColorFilter.matrix(
        ColorFilterGenerator.saturationAdjustMatrix(
          value: saturation,
        )
      ),
      child: ColorFiltered(
        colorFilter: ColorFilter.matrix(
          ColorFilterGenerator.hueAdjustMatrix(
            value: hue,
          )
        ),
        child: child,
      )
    )
  );
}

To generate the filter matrix, I used help from this answer to a similar question for android: https://mcmap.net/q/49854/-understanding-the-use-of-colormatrix-and-colormatrixcolorfilter-to-modify-a-drawable-39-s-hue and created a ColorFilterGenerator that works in flutter:

import 'dart:math';

class ColorFilterGenerator {
    static List<double> hueAdjustMatrix({double value}) {
      value = value * pi;

      if (value == 0)
        return [
          1,0,0,0,0,
          0,1,0,0,0,
          0,0,1,0,0,
          0,0,0,1,0,
        ];

      double cosVal = cos(value);
      double sinVal = sin(value);
      double lumR = 0.213;
      double lumG = 0.715;
      double lumB = 0.072;

      return List<double>.from(<double>[
        (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)), (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)), (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)), 0, 0, (lumR + (cosVal * (-lumR))) + (sinVal * 0.143), (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14), (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)), 0, 0, (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))), (lumG + (cosVal * (-lumG))) + (sinVal * lumG), (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB), 0, 0, 0, 0, 0, 1, 0,
      ]).map((i) => i.toDouble()).toList();
    }

    static List<double> brightnessAdjustMatrix({double value}}) {
      if (value <= 0)
        value = value * 255;
      else value = value * 100

      if (value == 0)
        return [
          1,0,0,0,0,
          0,1,0,0,0,
          0,0,1,0,0,
          0,0,0,1,0,
        ];

      return List<double>.from(<double>[
        1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0
      ]).map((i) => i.toDouble()).toList();
    }

    static List<double> saturationAdjustMatrix({double value}) {
      value = value * 100;

      if (value == 0)
        return [
          1,0,0,0,0,
          0,1,0,0,0,
          0,0,1,0,0,
          0,0,0,1,0,
        ];

      double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble();
      double lumR = 0.3086;
      double lumG = 0.6094;
      double lumB = 0.082;

      return List<double>.from(<double>[
        (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x),
        0, 0,
        lumR * (1 - x),
        (lumG * (1 - x)) + x,
        lumB * (1 - x),
        0, 0,
        lumR * (1 - x),
        lumG * (1 - x),
        (lumB * (1 - x)) + x,
        0, 0, 0, 0, 0, 1, 0,
      ]).map((i) => i.toDouble()).toList();
    }
}

I would bet that there is a way to concatenate the hue/saturation/brightness matrices (like was done in the android question I mentioned above), and only use 1 color filtered matrix (which would likely be more efficient), but this worked for my case.

Rabblerouser answered 10/11, 2020 at 5:25 Comment(7)
Thanks for sharing your work around, it is working good. Is it possible to share contrast adjust too? At the Android question I can see that someone explained HSBC adjustment and I hope you could help me complete the code for contrast part. Thanks anyway. – Egocentric
@Egocentric - if you post a new stackoverflow question, and you tag me in a comment, I'll help you out! – Rabblerouser
Thank you @Rabblerouser , I have already figured it out by myself. I can share the result if needed. – Egocentric
Probably a good thing to do for the community! I'd recommend putting it into a new Q/A though. – Rabblerouser
@Egocentric how did you add contrast? can you share it? – Chaille
@Rabblerouser can you help me on how to add Contrast to an image? here's my question: #69544833 – Chaille
@Chaille please find your answer here: #69575642 – Egocentric
C
1

We have to include a color matrix in this. Some examples are:

static const ColorFilter sepia = ColorFilter.matrix(<double>[
 0.393, 0.769, 0.189, 0, 0,
 0.349, 0.686, 0.168, 0, 0,
 0.272, 0.534, 0.131, 0, 0,
 0,     0,     0,     1, 0]);

static const ColorFilter greyscale = ColorFilter.matrix(<double>[
 0.2126, 0.7152, 0.0722, 0, 0,
 0.2126, 0.7152, 0.0722, 0, 0,
 0.2126, 0.7152, 0.0722, 0, 0,
 0,      0,      0,      1, 0]);

Just copy the matrix above to get a sepia or greyscale filter.

Conversant answered 17/7, 2021 at 11:2 Comment(0)
D
1

I've added BananaNeil's answer into the Themed package.

Use the provided ChangeColors widget to change the brightness, saturation and hue of any widget, including images, like this:

ChangeColors(
   hue: 0.55,
   brightness: 0.2,
   saturation: 0.1,
   child: Image.asset('myImage.png'),
);

Parameters are:

brightness

  • Negative value will make it darker (-1 is darkest).
  • Positive value will make it lighter (1 is the maximum, but you can go above it).
  • 0.0 is unchanged.

saturation

  • Negative value will make it less saturated (-1 is greyscale).
  • Positive value will make it more saturated (1 is the maximum, but you can go above it).
  • 0.0 is unchanged.

hue

  • From -1.0 to 1.0 (Note: 1.0 wraps into -1.0, such as 1.2 is the same as -0.8).
  • 0.0 is unchanged. Adding or subtracting multiples of 2.0 also keeps it unchanged.

Please note: The difference from the ChangeColors widget to BananaNeil's answer is that the widget is a proper StatelessWidget, code is null-safe, it fixes the limits of saturation so that you don't get weird effects, and the documentation was added explaining the parameters. If you don't want to add a package, just copy the code from GitHub.

Depilatory answered 23/11, 2021 at 20:47 Comment(2)
SO COOL! Thank you so much! πŸ™ – Rabblerouser
I did not double check this, but I'm going to mark it as the accepted answer because it seems to do what I did, but made the implementation much easier. – Rabblerouser
M
1

For those who looking for Contrast and Invert Color HSBCI, I added and reorder the filter based on BananaNeil's answer:

class ColorFilterGenerator {
  static List<double> hueAdjustMatrix({required double value}) {
    value = value * pi;

    if (value == 0)
      return [
        1,0,0,0,0,
        0,1,0,0,0,
        0,0,1,0,0,
        0,0,0,1,0,
      ];

    double cosVal = cos(value);
    double sinVal = sin(value);
    double lumR = 0.213;
    double lumG = 0.715;
    double lumB = 0.072;

    return List<double>.from(<double>[
      (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)), (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)), (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)), 0, 0, (lumR + (cosVal * (-lumR))) + (sinVal * 0.143), (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14), (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)), 0, 0, (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))), (lumG + (cosVal * (-lumG))) + (sinVal * lumG), (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB), 0, 0, 0, 0, 0, 1, 0,
    ]).map((i) => i.toDouble()).toList();
  }

  static List<double> brightnessAdjustMatrix({required double value}) {
        if (value <= 0) value = value * 255;
        else value = value * 100;
        if (value == 0)
        return [
          1,0,0,0,0,
          0,1,0,0,0,
          0,0,1,0,0,
          0,0,0,1,0,
        ];
        return List<double>.from(<double>[
        1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0
        ]).map((i) => i.toDouble()).toList();
  }

static List<double> saturationAdjustMatrix({required double value}) {
    value = value * 100;

    if (value == 0)
    return [
    1,0,0,0,0,
    0,1,0,0,0,
    0,0,1,0,0,
    0,0,0,1,0,
    ];

    double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble();
    double lumR = 0.3086;
    double lumG = 0.6094;
    double lumB = 0.082;

    return List<double>.from(<double>[
    (lumR * (1 - x)) + x, lumG * (1 - x), lumB * (1 - x),
    0, 0,
    lumR * (1 - x),
    (lumG * (1 - x)) + x,
    lumB * (1 - x),
    0, 0,
    lumR * (1 - x),
    lumG * (1 - x),
    (lumB * (1 - x)) + x,
    0, 0, 0, 0, 0, 1, 0,
    ]).map((i) => i.toDouble()).toList();
}

  static List<double> invertAdjustMatrix({required double value}) {
    if (value == 0)
      return [
        1,0,0,0,0,
        0,1,0,0,0,
        0,0,1,0,0,
        0,0,0,1,0,
      ];
    return List<double>.from(<double>[
      -value, 0, 0, value, 0,
      0, -value, 0, value, 0,
      0, 0, -value, value, 0,
      0, 0, 0, 1, 0
    ]).map((i) => i.toDouble()).toList();
  }

  static List<double> contrastAdjustMatrix({required double value}) {
    if (value == 0)
      return [
        1,0,0,0,0,
        0,1,0,0,0,
        0,0,1,0,0,
        0,0,0,1,0,
      ];

    return List<double>.from(<double>[
      Pow(2,value), 0, 0, 0.5-value/2, 0,
      0, Pow(2,value), 0, 0.5-value/2, 0,
      0, 0, Pow(2,value), 0.5-value/2, 0,
      0, 0, 0, 1, 0
    ]).map((i) => i.toDouble()).toList();
  }

}

And the Widget ImageFilter:

Widget ImageFilter({saturation, hue, contrast, brightness, invert,  child}) {
    return
        ColorFiltered(colorFilter: ColorFilter.matrix(ColorFilterGenerator.saturationAdjustMatrix(value: saturation,)),
          child: ColorFiltered(colorFilter: ColorFilter.matrix(ColorFilterGenerator.hueAdjustMatrix(value: hue,)),
            child: ColorFiltered(colorFilter: ColorFilter.matrix(ColorFilterGenerator.contrastAdjustMatrix(value: contrast,)),
              child: ColorFiltered(colorFilter: ColorFilter.matrix(ColorFilterGenerator.brightnessAdjustMatrix(value: brightness,)),
                child: ColorFiltered(colorFilter: ColorFilter.matrix(ColorFilterGenerator.invertAdjustMatrix(value: invert,)),
                  child: child,
              )
            )
          )
        )
      );
  }

The priority execute is following: Invert > Brightness > Contrast > Hue > Saturation

This order help easy predict result and protect color signal.

Using:

ImageFilter(
    hue: 0.5,
    saturation: -0.5,
    brightness: 0.2,
    contrast: 0.5,
    invert: 1.0,

    child: yourWidget,
);

Invert

  • Value from 0 to 1, between 0 and 1 will have affect on contrast too.

Contrast

  • Value from -1 to 1

  • There are several contrast scale system. In my case is reduce half the range of each channel (*0.5) when value is equal -1, and double the range of each channel (*2) when value equal 1. The pivot point is centered at 0.5.

Malvie answered 8/5 at 23:56 Comment(0)
M
0

I've further tweaked BananaNeil & Marcelo Glasberg answers, combining the 3 matrices into 1 while also adding support for adjusting a child's contrast, thereby improving performance.

ColorFiltered(
  colorFilter: ColorFilters.matrix(brightness: -0.5, saturation: 0.5),
  child: Image.network('https://upload.wikimedia.org/wikipedia/commons/c/c6/500_x_500_SMPTE_Color_Bars.png'),
);

It is available as part of the Stevia package.

Alternatively, if you don't want to add the package, you can copy the source code.

Magazine answered 27/8, 2023 at 9:0 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.