summaryrefslogtreecommitdiff
path: root/src/ColorTransformation.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/ColorTransformation.vala')
-rw-r--r--src/ColorTransformation.vala1519
1 files changed, 1519 insertions, 0 deletions
diff --git a/src/ColorTransformation.vala b/src/ColorTransformation.vala
new file mode 100644
index 0000000..74e4cf2
--- /dev/null
+++ b/src/ColorTransformation.vala
@@ -0,0 +1,1519 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public struct RGBAnalyticPixel {
+ public float red;
+ public float green;
+ public float blue;
+
+ private const float INV_255 = 1.0f / 255.0f;
+
+ public RGBAnalyticPixel() {
+ red = 0.0f;
+ green = 0.0f;
+ blue = 0.0f;
+ }
+
+ public RGBAnalyticPixel.from_components(float red, float green,
+ float blue) {
+ this.red = red.clamp(0.0f, 1.0f);
+ this.green = green.clamp(0.0f, 1.0f);
+ this.blue = blue.clamp(0.0f, 1.0f);
+ }
+
+ public RGBAnalyticPixel.from_quantized_components(uchar red_quantized,
+ uchar green_quantized, uchar blue_quantized) {
+ this.red = ((float) red_quantized) * INV_255;
+ this.green = ((float) green_quantized) * INV_255;
+ this.blue = ((float) blue_quantized) * INV_255;
+ }
+
+ public RGBAnalyticPixel.from_hsv(HSVAnalyticPixel hsv_pixel) {
+ RGBAnalyticPixel from_hsv = hsv_pixel.to_rgb();
+ red = from_hsv.red;
+ green = from_hsv.green;
+ blue = from_hsv.blue;
+ }
+
+ public uchar quantized_red() {
+ return (uchar)(red * 255.0f);
+ }
+
+ public uchar quantized_green() {
+ return (uchar)(green * 255.0f);
+ }
+
+ public uchar quantized_blue() {
+ return (uchar)(blue * 255.0f);
+ }
+
+ public bool equals(RGBAnalyticPixel? rhs) {
+ return ((red == rhs.red) && (green == rhs.green) && (blue == rhs.blue));
+ }
+
+ public uint hash_code() {
+ return (((uint)(red * 255.0f)) << 16) + (((uint)(green * 255.0f)) << 8) +
+ ((uint)(blue * 255.0f));
+ }
+
+ public HSVAnalyticPixel to_hsv() {
+ return HSVAnalyticPixel.from_rgb(this);
+ }
+}
+
+public struct HSVAnalyticPixel {
+ public float hue;
+ public float saturation;
+ public float light_value;
+
+ private const float INV_255 = 1.0f / 255.0f;
+
+ public HSVAnalyticPixel() {
+ hue = 0.0f;
+ saturation = 0.0f;
+ light_value = 0.0f;
+ }
+
+ public HSVAnalyticPixel.from_components(float hue, float saturation,
+ float light_value) {
+ this.hue = hue.clamp(0.0f, 1.0f);
+ this.saturation = saturation.clamp(0.0f, 1.0f);
+ this.light_value = light_value.clamp(0.0f, 1.0f);
+ }
+
+ public HSVAnalyticPixel.from_quantized_components(uchar hue_quantized,
+ uchar saturation_quantized, uchar light_value_quantized) {
+ this.hue = ((float) hue_quantized) * INV_255;
+ this.saturation = ((float) saturation_quantized) * INV_255;
+ this.light_value = ((float) light_value_quantized) * INV_255;
+ }
+
+ public HSVAnalyticPixel.from_rgb(RGBAnalyticPixel p) {
+ float max_component = float.max(float.max(p.red, p.green), p.blue);
+ float min_component = float.min(float.min(p.red, p.green), p.blue);
+
+ light_value = max_component;
+ saturation = (max_component != 0.0f) ? ((max_component - min_component) /
+ max_component) : 0.0f;
+
+ if (saturation == 0.0f) {
+ hue = 0.0f; /* hue is undefined in the zero saturation case */
+ } else {
+ float delta = max_component - min_component;
+ if (p.red == max_component) {
+ hue = (p.green - p.blue) / delta;
+ } else if (p.green == max_component) {
+ hue = 2.0f + ((p.blue - p.red) / delta);
+ } else if (p.blue == max_component) {
+ hue = 4.0f + ((p.red - p.green) / delta);
+ }
+
+ hue *= 60.0f;
+ if (hue < 0.0f)
+ hue += 360.0f;
+
+ hue /= 360.0f; /* normalize hue */
+ }
+
+ hue = hue.clamp(0.0f, 1.0f);
+ saturation = saturation.clamp(0.0f, 1.0f);
+ light_value = light_value.clamp(0.0f, 1.0f);
+ }
+
+ public RGBAnalyticPixel to_rgb() {
+ RGBAnalyticPixel result = RGBAnalyticPixel();
+
+ if (saturation == 0.0f) {
+ result.red = light_value;
+ result.green = light_value;
+ result.blue = light_value;
+ } else {
+ float hue_denorm = hue * 360.0f;
+ if (hue_denorm == 360.0f)
+ hue_denorm = 0.0f;
+
+ float hue_hexant = hue_denorm / 60.0f;
+
+ int hexant_i_part = (int) hue_hexant;
+
+ float hexant_f_part = hue_hexant - ((float) hexant_i_part);
+
+ /* the p, q, and t quantities from section 13.3 of Foley, et. al. */
+ float p = light_value * (1.0f - saturation);
+ float q = light_value * (1.0f - (saturation * hexant_f_part));
+ float t = light_value * (1.0f - (saturation * (1.0f - hexant_f_part)));
+ switch (hexant_i_part) {
+ /* the (r, g, b) components of the output pixel are computed
+ from the light_value, p, q, and t quantities differently
+ depending on which "hexant" (1/6 of a full rotation) of the
+ HSV color cone the hue lies in. For example, if the hue lies
+ in the yellow hexant, the dominant channels in the output
+ are red and green, so we map relatively more of the light_value
+ into these colors than if, say, the hue were to lie in the
+ cyan hexant. See chapter 13 of Foley, et. al. for more
+ information. */
+ case 0:
+ result.red = light_value;
+ result.green = t;
+ result.blue = p;
+ break;
+
+ case 1:
+ result.red = q;
+ result.green = light_value;
+ result.blue = p;
+ break;
+
+ case 2:
+ result.red = p;
+ result.green = light_value;
+ result.blue = t;
+ break;
+
+ case 3:
+ result.red = p;
+ result.green = q;
+ result.blue = light_value;
+ break;
+
+ case 4:
+ result.red = t;
+ result.green = p;
+ result.blue = light_value;
+ break;
+
+ case 5:
+ result.red = light_value;
+ result.green = p;
+ result.blue = q;
+ break;
+
+ default:
+ error("bad color hexant in HSV-to-RGB conversion");
+ }
+ }
+
+ return result;
+ }
+
+ public bool equals(ref HSVAnalyticPixel rhs) {
+ return ((hue == rhs.hue) && (saturation == rhs.saturation) &&
+ (light_value == rhs.light_value));
+ }
+
+ public uint hash_code() {
+ return (((uint)(hue * 255.0f)) << 16) + (((uint)(saturation * 255.0f)) << 8) +
+ ((uint)(light_value * 255.0f));
+ }
+}
+
+public enum CompositionMode {
+ NONE,
+ RGB_MATRIX
+}
+
+public enum PixelFormat {
+ RGB,
+ HSV
+}
+
+public enum PixelTransformationType {
+ TONE_EXPANSION,
+ SHADOWS,
+ HIGHLIGHTS,
+ TEMPERATURE,
+ TINT,
+ SATURATION,
+ EXPOSURE
+}
+
+public class PixelTransformationBundle {
+ private static PixelTransformationBundle? copied_color_adjustments = null;
+
+ private Gee.HashMap<int, PixelTransformation> map = new Gee.HashMap<int, PixelTransformation>(
+ Gee.Functions.get_hash_func_for(typeof(int)), Gee.Functions.get_equal_func_for(typeof(int)));
+
+ public PixelTransformationBundle() {
+ }
+
+ public static PixelTransformationBundle? get_copied_color_adjustments() {
+ return copied_color_adjustments;
+ }
+
+ public static void set_copied_color_adjustments(PixelTransformationBundle adjustments) {
+ copied_color_adjustments = adjustments;
+ }
+
+ public static bool has_copied_color_adjustments() {
+ return copied_color_adjustments != null;
+ }
+
+ public void set(PixelTransformation transformation) {
+ map.set((int) transformation.get_transformation_type(), transformation);
+ }
+
+ public void set_to_identity() {
+ set(new ExpansionTransformation.from_extrema(0, 255));
+ set(new ShadowDetailTransformation(0.0f));
+ set(new HighlightDetailTransformation(0.0f));
+ set(new TemperatureTransformation(0.0f));
+ set(new TintTransformation(0.0f));
+ set(new SaturationTransformation(0.0f));
+ set(new ExposureTransformation(0.0f));
+ }
+
+ public void load(KeyValueMap store) {
+ string expansion_params_encoded = store.get_string("expansion", "-");
+ if (expansion_params_encoded == "-")
+ set(new ExpansionTransformation.from_extrema(0, 255));
+ else
+ set(new ExpansionTransformation.from_string(expansion_params_encoded));
+
+ set(new ShadowDetailTransformation(store.get_float("shadows", 0.0f)));
+ set(new HighlightDetailTransformation(store.get_float("highlights", 0.0f)));
+ set(new TemperatureTransformation(store.get_float("temperature", 0.0f)));
+ set(new TintTransformation(store.get_float("tint", 0.0f)));
+ set(new SaturationTransformation(store.get_float("saturation", 0.0f)));
+ set(new ExposureTransformation(store.get_float("exposure", 0.0f)));
+ }
+
+ public KeyValueMap save(string group) {
+ KeyValueMap store = new KeyValueMap(group);
+
+ ExpansionTransformation? new_expansion_trans =
+ (ExpansionTransformation) get_transformation(PixelTransformationType.TONE_EXPANSION);
+ assert(new_expansion_trans != null);
+ store.set_string("expansion", new_expansion_trans.to_string());
+
+ ShadowDetailTransformation? new_shadows_trans =
+ (ShadowDetailTransformation) get_transformation(PixelTransformationType.SHADOWS);
+ assert(new_shadows_trans != null);
+ store.set_float("shadows", new_shadows_trans.get_parameter());
+
+ HighlightDetailTransformation? new_highlight_trans =
+ (HighlightDetailTransformation) get_transformation(PixelTransformationType.HIGHLIGHTS);
+ assert(new_highlight_trans != null);
+ store.set_float("highlights", new_highlight_trans.get_parameter());
+
+ TemperatureTransformation? new_temp_trans =
+ (TemperatureTransformation) get_transformation(PixelTransformationType.TEMPERATURE);
+ assert(new_temp_trans != null);
+ store.set_float("temperature", new_temp_trans.get_parameter());
+
+ TintTransformation? new_tint_trans =
+ (TintTransformation) get_transformation(PixelTransformationType.TINT);
+ assert(new_tint_trans != null);
+ store.set_float("tint", new_tint_trans.get_parameter());
+
+ SaturationTransformation? new_sat_trans =
+ (SaturationTransformation) get_transformation(PixelTransformationType.SATURATION);
+ assert(new_sat_trans != null);
+ store.set_float("saturation", new_sat_trans.get_parameter());
+
+ ExposureTransformation? new_exposure_trans =
+ (ExposureTransformation) get_transformation(PixelTransformationType.EXPOSURE);
+ assert(new_exposure_trans != null);
+ store.set_float("exposure", new_exposure_trans.get_parameter());
+
+ return store;
+ }
+
+ public int get_count() {
+ return map.size;
+ }
+
+ public PixelTransformation? get_transformation(PixelTransformationType type) {
+ return map.get((int) type);
+ }
+
+ public Gee.Iterable<PixelTransformation> get_transformations() {
+ return map.values;
+ }
+
+ public bool is_identity() {
+ foreach (PixelTransformation adjustment in get_transformations()) {
+ if (!adjustment.is_identity())
+ return false;
+ }
+
+ return true;
+ }
+
+ public PixelTransformer generate_transformer() {
+ PixelTransformer transformer = new PixelTransformer();
+ foreach (PixelTransformation transformation in get_transformations())
+ transformer.attach_transformation(transformation);
+
+ return transformer;
+ }
+
+ public PixelTransformationBundle copy() {
+ PixelTransformationBundle bundle = new PixelTransformationBundle();
+ foreach (PixelTransformation transformation in get_transformations())
+ bundle.set(transformation);
+
+ return bundle;
+ }
+}
+
+public abstract class PixelTransformation {
+ private PixelTransformationType type;
+
+ public PixelTransformation(PixelTransformationType type) {
+ this.type = type;
+ }
+
+ public PixelTransformationType get_transformation_type() {
+ return type;
+ }
+
+ public abstract PixelFormat get_preferred_format();
+
+ public virtual CompositionMode get_composition_mode() {
+ return CompositionMode.NONE;
+ }
+
+ public virtual void compose_with(PixelTransformation other) {
+ error("PixelTransformation: compose_with( ): this type of pixel " +
+ "transformation doesn't support composition.");
+ }
+
+ public virtual bool is_identity() {
+ return true;
+ }
+
+ public virtual HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel p) {
+ return p;
+ }
+
+ public virtual RGBAnalyticPixel transform_pixel_rgb(RGBAnalyticPixel p) {
+ return p;
+ }
+
+ public virtual string to_string() {
+ return "PixelTransformation";
+ }
+
+ public abstract PixelTransformation copy();
+}
+
+public class RGBTransformation : PixelTransformation {
+ /* matrix entries are stored in row-major order; by default, the matrix formed
+ by matrix_entries is the 4x4 identity matrix */
+ protected float[] matrix_entries;
+
+ protected const int MATRIX_SIZE = 16;
+
+ protected bool identity = true;
+
+ public RGBTransformation(PixelTransformationType type) {
+ base(type);
+
+ // Can't initialize these in their member declarations because of a valac bug that
+ // I've been unable to produce a minimal test case for to report (JN). May be
+ // related to this bug:
+ // https://bugzilla.gnome.org/show_bug.cgi?id=570821
+ matrix_entries = {
+ 1.0f, 0.0f, 0.0f, 0.0f,
+ 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.0f, 0.0f, 1.0f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f };
+ }
+
+ public override PixelFormat get_preferred_format() {
+ return PixelFormat.RGB;
+ }
+
+ public override CompositionMode get_composition_mode() {
+ return CompositionMode.RGB_MATRIX;
+ }
+
+ public override void compose_with(PixelTransformation other) {
+ if (other.get_composition_mode() != CompositionMode.RGB_MATRIX)
+ error("RGBTransformation: compose_with( ): 'other' transformation " +
+ "does not support RGB_MATRIX composition mode");
+
+ RGBTransformation transform = (RGBTransformation) other;
+
+ float[] result_matrix_entries = new float[16];
+
+ /* row 0 */
+ result_matrix_entries[0] =
+ (transform.matrix_entries[0] * matrix_entries[0]) +
+ (transform.matrix_entries[1] * matrix_entries[4]) +
+ (transform.matrix_entries[2] * matrix_entries[8]) +
+ (transform.matrix_entries[3] * matrix_entries[12]);
+
+ result_matrix_entries[1] =
+ (transform.matrix_entries[0] * matrix_entries[1]) +
+ (transform.matrix_entries[1] * matrix_entries[5]) +
+ (transform.matrix_entries[2] * matrix_entries[9]) +
+ (transform.matrix_entries[3] * matrix_entries[13]);
+
+ result_matrix_entries[2] =
+ (transform.matrix_entries[0] * matrix_entries[2]) +
+ (transform.matrix_entries[1] * matrix_entries[6]) +
+ (transform.matrix_entries[2] * matrix_entries[10]) +
+ (transform.matrix_entries[3] * matrix_entries[14]);
+
+ result_matrix_entries[3] =
+ (transform.matrix_entries[0] * matrix_entries[3]) +
+ (transform.matrix_entries[1] * matrix_entries[7]) +
+ (transform.matrix_entries[2] * matrix_entries[11]) +
+ (transform.matrix_entries[3] * matrix_entries[15]);
+
+ /* row 1 */
+ result_matrix_entries[4] =
+ (transform.matrix_entries[4] * matrix_entries[0]) +
+ (transform.matrix_entries[5] * matrix_entries[4]) +
+ (transform.matrix_entries[6] * matrix_entries[8]) +
+ (transform.matrix_entries[7] * matrix_entries[12]);
+
+ result_matrix_entries[5] =
+ (transform.matrix_entries[4] * matrix_entries[1]) +
+ (transform.matrix_entries[5] * matrix_entries[5]) +
+ (transform.matrix_entries[6] * matrix_entries[9]) +
+ (transform.matrix_entries[7] * matrix_entries[13]);
+
+ result_matrix_entries[6] =
+ (transform.matrix_entries[4] * matrix_entries[2]) +
+ (transform.matrix_entries[5] * matrix_entries[6]) +
+ (transform.matrix_entries[6] * matrix_entries[10]) +
+ (transform.matrix_entries[7] * matrix_entries[14]);
+
+ result_matrix_entries[7] =
+ (transform.matrix_entries[4] * matrix_entries[3]) +
+ (transform.matrix_entries[5] * matrix_entries[7]) +
+ (transform.matrix_entries[6] * matrix_entries[11]) +
+ (transform.matrix_entries[7] * matrix_entries[15]);
+
+ /* row 2 */
+ result_matrix_entries[8] =
+ (transform.matrix_entries[8] * matrix_entries[0]) +
+ (transform.matrix_entries[9] * matrix_entries[4]) +
+ (transform.matrix_entries[10] * matrix_entries[8]) +
+ (transform.matrix_entries[11] * matrix_entries[12]);
+
+ result_matrix_entries[9] =
+ (transform.matrix_entries[8] * matrix_entries[1]) +
+ (transform.matrix_entries[9] * matrix_entries[5]) +
+ (transform.matrix_entries[10] * matrix_entries[9]) +
+ (transform.matrix_entries[11] * matrix_entries[13]);
+
+ result_matrix_entries[10] =
+ (transform.matrix_entries[8] * matrix_entries[2]) +
+ (transform.matrix_entries[9] * matrix_entries[6]) +
+ (transform.matrix_entries[10] * matrix_entries[10]) +
+ (transform.matrix_entries[11] * matrix_entries[14]);
+
+ result_matrix_entries[11] =
+ (transform.matrix_entries[8] * matrix_entries[3]) +
+ (transform.matrix_entries[9] * matrix_entries[7]) +
+ (transform.matrix_entries[10] * matrix_entries[11]) +
+ (transform.matrix_entries[11] * matrix_entries[15]);
+
+ /* row 3 */
+ result_matrix_entries[12] =
+ (transform.matrix_entries[12] * matrix_entries[0]) +
+ (transform.matrix_entries[13] * matrix_entries[4]) +
+ (transform.matrix_entries[14] * matrix_entries[8]) +
+ (transform.matrix_entries[15] * matrix_entries[12]);
+
+ result_matrix_entries[13] =
+ (transform.matrix_entries[12] * matrix_entries[1]) +
+ (transform.matrix_entries[13] * matrix_entries[5]) +
+ (transform.matrix_entries[14] * matrix_entries[9]) +
+ (transform.matrix_entries[15] * matrix_entries[13]);
+
+ result_matrix_entries[14] =
+ (transform.matrix_entries[12] * matrix_entries[2]) +
+ (transform.matrix_entries[13] * matrix_entries[6]) +
+ (transform.matrix_entries[14] * matrix_entries[10]) +
+ (transform.matrix_entries[15] * matrix_entries[14]);
+
+ result_matrix_entries[15] =
+ (transform.matrix_entries[12] * matrix_entries[3]) +
+ (transform.matrix_entries[13] * matrix_entries[7]) +
+ (transform.matrix_entries[14] * matrix_entries[11]) +
+ (transform.matrix_entries[15] * matrix_entries[15]);
+
+ for (int i = 0; i < MATRIX_SIZE; i++)
+ matrix_entries[i] = result_matrix_entries[i];
+
+ identity = (identity && transform.identity);
+ }
+
+ public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel p) {
+ return (transform_pixel_rgb(p.to_rgb())).to_hsv();
+ }
+
+ public override RGBAnalyticPixel transform_pixel_rgb(RGBAnalyticPixel p) {
+ float red_out = (p.red * matrix_entries[0]) +
+ (p.green * matrix_entries[1]) +
+ (p.blue * matrix_entries[2]) +
+ matrix_entries[3];
+ red_out = red_out.clamp(0.0f, 1.0f);
+
+ float green_out = (p.red * matrix_entries[4]) +
+ (p.green * matrix_entries[5]) +
+ (p.blue * matrix_entries[6]) +
+ matrix_entries[7];
+ green_out = green_out.clamp(0.0f, 1.0f);
+
+ float blue_out = (p.red * matrix_entries[8]) +
+ (p.green * matrix_entries[9]) +
+ (p.blue * matrix_entries[10]) +
+ matrix_entries[11];
+ blue_out = blue_out.clamp(0.0f, 1.0f);
+
+ return RGBAnalyticPixel.from_components(red_out, green_out, blue_out);
+ }
+
+ public override bool is_identity() {
+ return identity;
+ }
+
+ public override PixelTransformation copy() {
+ RGBTransformation result = new RGBTransformation(get_transformation_type());
+
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ result.matrix_entries[i] = matrix_entries[i];
+ }
+
+ return result;
+ }
+}
+
+public abstract class HSVTransformation : PixelTransformation {
+ public HSVTransformation(PixelTransformationType type) {
+ base(type);
+ }
+
+ public override PixelFormat get_preferred_format() {
+ return PixelFormat.HSV;
+ }
+
+ public override RGBAnalyticPixel transform_pixel_rgb(RGBAnalyticPixel p) {
+ return (transform_pixel_hsv(p.to_hsv())).to_rgb();
+ }
+}
+
+public class TintTransformation : RGBTransformation {
+ private const float INTENSITY_FACTOR = 0.25f;
+ public const float MIN_PARAMETER = -16.0f;
+ public const float MAX_PARAMETER = 16.0f;
+
+ private float parameter;
+
+ public TintTransformation(float client_param) {
+ base(PixelTransformationType.TINT);
+
+ parameter = client_param.clamp(MIN_PARAMETER, MAX_PARAMETER);
+
+ if (parameter != 0.0f) {
+ float adjusted_param = parameter / MAX_PARAMETER;
+ adjusted_param *= INTENSITY_FACTOR;
+
+ matrix_entries[11] -= (adjusted_param / 2);
+ matrix_entries[7] += adjusted_param;
+ matrix_entries[3] -= (adjusted_param / 2);
+
+ identity = false;
+ }
+ }
+
+ public float get_parameter() {
+ return parameter;
+ }
+}
+
+public class TemperatureTransformation : RGBTransformation {
+ private const float INTENSITY_FACTOR = 0.33f;
+ public const float MIN_PARAMETER = -16.0f;
+ public const float MAX_PARAMETER = 16.0f;
+
+ private float parameter;
+
+ public TemperatureTransformation(float client_parameter) {
+ base(PixelTransformationType.TEMPERATURE);
+
+ parameter = client_parameter.clamp(MIN_PARAMETER, MAX_PARAMETER);
+
+ if (parameter != 0.0f) {
+ float adjusted_param = parameter / MAX_PARAMETER;
+ adjusted_param *= INTENSITY_FACTOR;
+
+ matrix_entries[11] -= adjusted_param;
+ matrix_entries[7] += (adjusted_param / 2);
+ matrix_entries[3] += (adjusted_param / 2);
+
+ identity = false;
+ }
+ }
+
+ public float get_parameter() {
+ return parameter;
+ }
+}
+
+public class SaturationTransformation : RGBTransformation {
+ public const float MIN_PARAMETER = -16.0f;
+ public const float MAX_PARAMETER = 16.0f;
+
+ private float parameter;
+
+ public SaturationTransformation(float client_parameter) {
+ base(PixelTransformationType.SATURATION);
+
+ parameter = client_parameter.clamp(MIN_PARAMETER, MAX_PARAMETER);
+
+ if (parameter != 0.0f) {
+ float adjusted_param = parameter / MAX_PARAMETER;
+ adjusted_param += 1.0f;
+
+ float one_third = 0.3333333f;
+
+ matrix_entries[0] = ((1.0f - adjusted_param) * one_third) +
+ adjusted_param;
+ matrix_entries[1] = (1.0f - adjusted_param) * one_third;
+ matrix_entries[2] = (1.0f - adjusted_param) * one_third;
+
+ matrix_entries[4] = (1.0f - adjusted_param) * one_third;
+ matrix_entries[5] = ((1.0f - adjusted_param) * one_third) +
+ adjusted_param;
+ matrix_entries[6] = (1.0f - adjusted_param) * one_third;
+
+ matrix_entries[8] = (1.0f - adjusted_param) * one_third;
+ matrix_entries[9] = (1.0f - adjusted_param) * one_third;
+ matrix_entries[10] = ((1.0f - adjusted_param) * one_third) +
+ adjusted_param;
+
+ identity = false;
+ }
+ }
+
+ public float get_parameter() {
+ return parameter;
+ }
+}
+
+public class ExposureTransformation : RGBTransformation {
+ public const float MIN_PARAMETER = -16.0f;
+ public const float MAX_PARAMETER = 16.0f;
+
+ float parameter;
+
+ public ExposureTransformation(float client_parameter) {
+ base(PixelTransformationType.EXPOSURE);
+
+ parameter = client_parameter.clamp(MIN_PARAMETER, MAX_PARAMETER);
+
+ if (parameter != 0.0f) {
+
+ float adjusted_param = ((parameter + 16.0f) / 32.0f) + 0.5f;
+
+ matrix_entries[0] = adjusted_param;
+ matrix_entries[5] = adjusted_param;
+ matrix_entries[10] = adjusted_param;
+
+ identity = false;
+ }
+ }
+
+ public float get_parameter() {
+ return parameter;
+ }
+}
+
+public class PixelTransformer {
+ private Gee.ArrayList<PixelTransformation> transformations =
+ new Gee.ArrayList<PixelTransformation>();
+ private PixelTransformation[] optimized_transformations = null;
+ private int optimized_slots_used = 0;
+
+ public PixelTransformer() {
+ }
+
+ public PixelTransformer copy() {
+ PixelTransformer clone = new PixelTransformer();
+
+ foreach (PixelTransformation transformation in transformations)
+ clone.transformations.add(transformation);
+
+ return clone;
+ }
+
+ private void build_optimized_transformations() {
+ optimized_transformations = new PixelTransformation[transformations.size];
+
+ PixelTransformation pre_trans = null;
+ optimized_slots_used = 0;
+ for (int i = 0; i < transformations.size; i++) {
+ PixelTransformation trans = transformations.get(i);
+
+ if (trans.is_identity())
+ continue;
+
+ PixelTransformation this_trans = null;
+ if (trans.get_composition_mode() == CompositionMode.NONE)
+ this_trans = trans;
+ else
+ this_trans = trans.copy();
+
+ if ((pre_trans != null) && (this_trans.get_composition_mode() != CompositionMode.NONE)
+ && (this_trans.get_composition_mode() == pre_trans.get_composition_mode())) {
+ pre_trans.compose_with(this_trans);
+ } else {
+ optimized_transformations[optimized_slots_used++] = this_trans;
+ pre_trans = this_trans;
+ }
+ }
+ }
+
+ private RGBAnalyticPixel apply_transformations(RGBAnalyticPixel p) {
+ PixelFormat current_format = PixelFormat.RGB;
+ RGBAnalyticPixel p_rgb = p;
+ HSVAnalyticPixel p_hsv = HSVAnalyticPixel();
+
+ for (int i = 0; i < optimized_slots_used; i++) {
+ PixelTransformation trans = optimized_transformations[i];
+ if (trans.get_preferred_format() == PixelFormat.RGB) {
+ if (current_format == PixelFormat.HSV) {
+ p_rgb = p_hsv.to_rgb();
+ current_format = PixelFormat.RGB;
+ }
+ p_rgb = trans.transform_pixel_rgb(p_rgb);
+ } else {
+ if (current_format == PixelFormat.RGB) {
+ p_hsv = p_rgb.to_hsv();
+ current_format = PixelFormat.HSV;
+ }
+ p_hsv = trans.transform_pixel_hsv(p_hsv);
+ }
+ }
+
+ if (current_format == PixelFormat.HSV)
+ p_rgb = p_hsv.to_rgb();
+
+ return p_rgb;
+ }
+
+ /* NOTE: this method allows the same transformation to be added multiple
+ times. There's nothing wrong with this behavior as of today,
+ but it may be a policy that we want to change in the future */
+ public void attach_transformation(PixelTransformation trans) {
+ transformations.add(trans);
+ optimized_transformations = null;
+ }
+
+ /* NOTE: if a transformation has been added multiple times, only the first
+ instance of it will be removed */
+ public void detach_transformation(PixelTransformation victim) {
+ transformations.remove(victim);
+ optimized_transformations = null;
+ }
+
+ /* NOTE: if a transformation has been added multiple times, only the first
+ instance of it will be replaced with 'new' */
+ public void replace_transformation(PixelTransformation old_trans,
+ PixelTransformation new_trans) {
+ for (int i = 0; i < transformations.size; i++) {
+ if (transformations.get(i) == old_trans) {
+ transformations.set(i, new_trans);
+
+ optimized_transformations = null;
+ return;
+ }
+ }
+ error("PixelTransformer: replace_transformation( ): old_trans is not present in " +
+ "transformation collection");
+ }
+
+ public void transform_pixbuf(Gdk.Pixbuf pixbuf, Cancellable? cancellable = null) {
+ transform_to_other_pixbuf(pixbuf, pixbuf, cancellable);
+ }
+
+ public void transform_from_fp(ref float[] fp_pixel_cache, Gdk.Pixbuf dest) {
+ if (optimized_transformations == null)
+ build_optimized_transformations();
+
+ int dest_width = dest.get_width();
+ int dest_height = dest.get_height();
+ int dest_num_channels = dest.get_n_channels();
+ int dest_rowstride = dest.get_rowstride();
+ unowned uchar[] dest_pixels = dest.get_pixels();
+
+ int cache_pixel_ticker = 0;
+
+ for (int j = 0; j < dest_height; j++) {
+ int row_start_index = j * dest_rowstride;
+ int row_end_index = row_start_index + (dest_width * dest_num_channels);
+ for (int i = row_start_index; i < row_end_index; i += dest_num_channels) {
+ RGBAnalyticPixel pixel = RGBAnalyticPixel.from_components(
+ fp_pixel_cache[cache_pixel_ticker],
+ fp_pixel_cache[cache_pixel_ticker + 1],
+ fp_pixel_cache[cache_pixel_ticker + 2]);
+
+ cache_pixel_ticker += 3;
+
+ pixel = apply_transformations(pixel);
+
+ dest_pixels[i] = (uchar) (pixel.red * 255.0f);
+ dest_pixels[i + 1] = (uchar) (pixel.green * 255.0f);
+ dest_pixels[i + 2] = (uchar) (pixel.blue * 255.0f);
+ }
+ }
+ }
+
+ public void transform_to_other_pixbuf(Gdk.Pixbuf source, Gdk.Pixbuf dest,
+ Cancellable? cancellable = null) {
+ if (source.width != dest.width)
+ error("PixelTransformer: source and destination pixbufs must have the same width");
+
+ if (source.height != dest.height)
+ error("PixelTransformer: source and destination pixbufs must have the same height");
+
+ if (source.n_channels != dest.n_channels)
+ error("PixelTransformer: source and destination pixbufs must have the same number " +
+ "of channels");
+
+ if (optimized_transformations == null)
+ build_optimized_transformations();
+
+ int n_channels = source.get_n_channels();
+ int rowstride = source.get_rowstride();
+ int width = source.get_width();
+ int height = source.get_height();
+ int rowbytes = n_channels * width;
+ unowned uchar[] source_pixels = source.get_pixels();
+ unowned uchar[] dest_pixels = dest.get_pixels();
+ for (int j = 0; j < height; j++) {
+ int row_start_index = j * rowstride;
+ int row_end_index = row_start_index + rowbytes;
+ for (int i = row_start_index; i < row_end_index; i += n_channels) {
+ RGBAnalyticPixel current_pixel = RGBAnalyticPixel.from_quantized_components(
+ source_pixels[i], source_pixels[i + 1], source_pixels[i + 2]);
+
+ current_pixel = apply_transformations(current_pixel);
+
+ dest_pixels[i] = current_pixel.quantized_red();
+ dest_pixels[i + 1] = current_pixel.quantized_green();
+ dest_pixels[i + 2] = current_pixel.quantized_blue();
+ }
+
+ if ((cancellable != null) && (cancellable.is_cancelled())) {
+ return;
+ }
+ }
+ }
+}
+
+class RGBHistogram {
+ private const uchar MARKED_BACKGROUND = 30;
+ private const uchar MARKED_FOREGROUND = 210;
+ private const uchar UNMARKED_BACKGROUND = 120;
+
+ public const int GRAPHIC_WIDTH = 256;
+ public const int GRAPHIC_HEIGHT = 100;
+
+ private int[] red_counts = new int[256];
+ private int[] green_counts = new int[256];
+ private int[] blue_counts = new int[256];
+ private int[] qualitative_red_counts = null;
+ private int[] qualitative_green_counts = null;
+ private int[] qualitative_blue_counts = null;
+ private Gdk.Pixbuf graphic = null;
+
+ public RGBHistogram(Gdk.Pixbuf pixbuf) {
+ int sample_bytes = pixbuf.get_bits_per_sample() / 8;
+ int pixel_bytes = sample_bytes * pixbuf.get_n_channels();
+ int row_length_bytes = pixel_bytes * pixbuf.width;
+
+ unowned uchar[] pixel_data = pixbuf.get_pixels();
+
+ for (int y = 0; y < pixbuf.height; y++) {
+ int row_start_offset = y * pixbuf.rowstride;
+
+ int r_offset = row_start_offset;
+ int g_offset = row_start_offset + sample_bytes;
+ int b_offset = row_start_offset + sample_bytes + sample_bytes;
+
+ while (b_offset < (row_start_offset + row_length_bytes)) {
+ red_counts[pixel_data[r_offset]] += 1;
+ green_counts[pixel_data[g_offset]] += 1;
+ blue_counts[pixel_data[b_offset]] += 1;
+
+ r_offset += pixel_bytes;
+ g_offset += pixel_bytes;
+ b_offset += pixel_bytes;
+ }
+ }
+ }
+
+ private int correct_snap_to_quantization(int[] buckets, int i) {
+ assert(buckets.length == 256);
+ assert((i >= 0) && (i <= 255));
+
+ if (i == 0) {
+ if (buckets[i] > 0)
+ if (buckets[i + 1] > 0)
+ if (buckets[i] > (2 * buckets[i + 1]))
+ return buckets[i + 1];
+ } else if (i == 255) {
+ if (buckets[i] > 0)
+ if (buckets[i - 1] > 0)
+ if (buckets[i] > (2 * buckets[i - 1]))
+ return buckets[i - 1];
+ } else {
+ if (buckets[i] > 0)
+ if (buckets[i] > ((buckets[i - 1] + buckets[i + 1]) / 2))
+ return (buckets[i - 1] + buckets[i + 1]) / 2;
+ }
+
+ return buckets[i];
+ }
+
+ private int correct_snap_from_quantization(int[] buckets, int i) {
+ assert(buckets.length == 256);
+ assert((i >= 0) && (i <= 255));
+
+ if (i == 0) {
+ return buckets[i];
+ } else if (i == 255) {
+ return buckets[i];
+ } else {
+ if (buckets[i] == 0)
+ if (buckets[i - 1] > 0)
+ if (buckets[i + 1] > 0)
+ return (buckets[i - 1] + buckets[i + 1]) / 2;
+ }
+
+ return buckets[i];
+ }
+
+ private void smooth_extrema(ref int[] count_data) {
+ assert(count_data.length == 256);
+
+ /* the blocks of code below are unrolled loops that replace values at the extrema
+ (buckets 0-4 and 251-255, inclusive) of the histogram with a weighted
+ average of their neighbors. This mitigates quantization and pooling artifacts */
+
+ count_data[0] = (5 * count_data[0] + 3 * count_data[1] + 2 * count_data[2]) /
+ 10;
+ count_data[1] = (3 * count_data[0] + 5 * count_data[1] + 3 * count_data[2] +
+ 2 * count_data[3]) / 13;
+ count_data[2] = (2 * count_data[0] + 3 * count_data[1] + 5 * count_data[2] +
+ 3 * count_data[3] + 2 * count_data[4]) / 15;
+ count_data[3] = (2 * count_data[1] + 3 * count_data[2] + 5 * count_data[3] +
+ 3 * count_data[4] + 2 * count_data[5]) / 15;
+ count_data[4] = (2 * count_data[2] + 3 * count_data[3] + 5 * count_data[4] +
+ 3 * count_data[5] + 2 * count_data[6]) / 15;
+
+ count_data[255] = (5 * count_data[255] + 3 * count_data[254] + 2 * count_data[253]) /
+ 10;
+ count_data[254] = (3 * count_data[255] + 5 * count_data[254] + 3 * count_data[253] +
+ 2 * count_data[252]) / 13;
+ count_data[253] = (2 * count_data[255] + 3 * count_data[254] + 5 * count_data[253] +
+ 3 * count_data[252] + 2 * count_data[251]) / 15;
+ count_data[252] = (2 * count_data[254] + 3 * count_data[253] + 5 * count_data[252] +
+ 3 * count_data[251] + 2 * count_data[250]) / 15;
+ count_data[251] = (2 * count_data[253] + 3 * count_data[252] + 5 * count_data[251] +
+ 3 * count_data[250] + 2 * count_data[249]) / 15;
+ }
+
+ private void prepare_qualitative_counts() {
+ if ((qualitative_red_counts != null) && (qualitative_green_counts != null) &&
+ (qualitative_blue_counts != null))
+ return;
+
+ qualitative_red_counts = new int[256];
+ qualitative_green_counts = new int[256];
+ qualitative_blue_counts = new int[256];
+
+ int[] temp_red_counts = new int[256];
+ int[] temp_green_counts = new int[256];
+ int[] temp_blue_counts = new int[256];
+
+ /* Remove snap-away-from-value quantization artifacts from the qualitative
+ histogram. While these are indeed present in the underlying data as a
+ consequence of sampling, transformation, and reconstruction, they lead
+ to an unpleasant looking histogram, so we detect and eliminate them here */
+ for (int i = 0; i < 256; i++) {
+ qualitative_red_counts[i] =
+ correct_snap_from_quantization(red_counts, i);
+ qualitative_green_counts[i] =
+ correct_snap_from_quantization(green_counts, i);
+ qualitative_blue_counts[i] =
+ correct_snap_from_quantization(blue_counts, i);
+ }
+
+ for (int i = 0; i < 256; i++) {
+ temp_red_counts[i] = qualitative_red_counts[i];
+ temp_green_counts[i] = qualitative_green_counts[i];
+ temp_blue_counts[i] = qualitative_blue_counts[i];
+ }
+
+ /* Remove snap-to-value quantization artifacts from the qualitative
+ histogram */
+ for (int i = 0; i < 256; i++) {
+ qualitative_red_counts[i] =
+ correct_snap_to_quantization(temp_red_counts, i);
+ qualitative_green_counts[i] =
+ correct_snap_to_quantization(temp_green_counts, i);
+ qualitative_blue_counts[i] =
+ correct_snap_to_quantization(temp_blue_counts, i);
+ }
+
+ /* constrain the peaks in the qualitative histogram so that no peak can be more
+ than 8 times higher than the mean height of the entire image */
+ int mean_qual_count = 0;
+ for (int i = 0; i < 256; i++) {
+ mean_qual_count += (qualitative_red_counts[i] + qualitative_green_counts[i] +
+ qualitative_blue_counts[i]);
+ }
+ mean_qual_count /= (256 * 3);
+ int constrained_max_qual_count = 8 * mean_qual_count;
+ for (int i = 0; i < 256; i++) {
+ if (qualitative_red_counts[i] > constrained_max_qual_count)
+ qualitative_red_counts[i] = constrained_max_qual_count;
+
+ if (qualitative_green_counts[i] > constrained_max_qual_count)
+ qualitative_green_counts[i] = constrained_max_qual_count;
+
+ if (qualitative_blue_counts[i] > constrained_max_qual_count)
+ qualitative_blue_counts[i] = constrained_max_qual_count;
+ }
+
+ smooth_extrema(ref qualitative_red_counts);
+ smooth_extrema(ref qualitative_green_counts);
+ smooth_extrema(ref qualitative_blue_counts);
+ }
+
+ public Gdk.Pixbuf get_graphic() {
+ if (graphic == null) {
+ prepare_qualitative_counts();
+ int max_count = 0;
+ for (int i = 0; i < 256; i++) {
+ if (qualitative_red_counts[i] > max_count)
+ max_count = qualitative_red_counts[i];
+ if (qualitative_green_counts[i] > max_count)
+ max_count = qualitative_green_counts[i];
+ if (qualitative_blue_counts[i] > max_count)
+ max_count = qualitative_blue_counts[i];
+ }
+
+ graphic = new Gdk.Pixbuf(Gdk.Colorspace.RGB, false, 8,
+ GRAPHIC_WIDTH, GRAPHIC_HEIGHT);
+
+ int rowstride = graphic.rowstride;
+ int sample_bytes = graphic.get_bits_per_sample() / 8;
+ int pixel_bytes = sample_bytes * graphic.get_n_channels();
+
+ double scale_bar = 0.98 * ((double) GRAPHIC_HEIGHT) /
+ ((double) max_count);
+
+ unowned uchar[] pixel_data = graphic.get_pixels();
+
+ /* detect pathological case of bilevel black & white images -- in this case, draw
+ a blank histogram and return it to the caller */
+ if (max_count == 0) {
+ for (int i = 0; i < (pixel_bytes * graphic.width * graphic.height); i++) {
+ pixel_data[i] = UNMARKED_BACKGROUND;
+ }
+ return graphic;
+ }
+
+ for (int x = 0; x < 256; x++) {
+ int red_bar_height = (int)(((double) qualitative_red_counts[x]) *
+ scale_bar);
+ int green_bar_height = (int)(((double) qualitative_green_counts[x]) *
+ scale_bar);
+ int blue_bar_height = (int)(((double) qualitative_blue_counts[x]) *
+ scale_bar);
+
+ int max_bar_height = int.max(int.max(red_bar_height,
+ green_bar_height), blue_bar_height);
+
+ int y = GRAPHIC_HEIGHT - 1;
+ int pixel_index = (x * pixel_bytes) + (y * rowstride);
+ for ( ; y >= (GRAPHIC_HEIGHT - max_bar_height); y--) {
+ pixel_data[pixel_index] = MARKED_BACKGROUND;
+ pixel_data[pixel_index + 1] = MARKED_BACKGROUND;
+ pixel_data[pixel_index + 2] = MARKED_BACKGROUND;
+
+ if (y >= (GRAPHIC_HEIGHT - red_bar_height - 1))
+ pixel_data[pixel_index] = MARKED_FOREGROUND;
+ if (y >= (GRAPHIC_HEIGHT - green_bar_height - 1))
+ pixel_data[pixel_index + 1] = MARKED_FOREGROUND;
+ if (y >= (GRAPHIC_HEIGHT - blue_bar_height - 1))
+ pixel_data[pixel_index + 2] = MARKED_FOREGROUND;
+
+ pixel_index -= rowstride;
+ }
+
+ for ( ; y >= 0; y--) {
+ pixel_data[pixel_index] = UNMARKED_BACKGROUND;
+ pixel_data[pixel_index + 1] = UNMARKED_BACKGROUND;
+ pixel_data[pixel_index + 2] = UNMARKED_BACKGROUND;
+
+ pixel_index -= rowstride;
+ }
+ }
+ }
+
+ return graphic;
+ }
+}
+
+public class IntensityHistogram {
+ private int[] counts = new int[256];
+ private float[] probabilities = new float[256];
+ private float[] cumulative_probabilities = new float[256];
+
+ public IntensityHistogram(Gdk.Pixbuf pixbuf) {
+ int n_channels = pixbuf.get_n_channels();
+ int rowstride = pixbuf.get_rowstride();
+ int width = pixbuf.get_width();
+ int height = pixbuf.get_height();
+ int rowbytes = n_channels * width;
+ unowned uchar[] pixels = pixbuf.get_pixels();
+ for (int j = 0; j < height; j++) {
+ int row_start_index = j * rowstride;
+ int row_end_index = row_start_index + rowbytes;
+ for (int i = row_start_index; i < row_end_index; i += n_channels) {
+ RGBAnalyticPixel pix_rgb = RGBAnalyticPixel.from_quantized_components(
+ pixels[i], pixels[i + 1], pixels[i + 2]);
+ HSVAnalyticPixel pix_hsi = HSVAnalyticPixel.from_rgb(pix_rgb);
+ int quantized_light_value = (int)(pix_hsi.light_value * 255.0f);
+ counts[quantized_light_value] += 1;
+ }
+ }
+
+ float pixel_count = (float)(pixbuf.width * pixbuf.height);
+ float accumulator = 0.0f;
+ for (int i = 0; i < 256; i++) {
+ probabilities[i] = ((float) counts[i]) / pixel_count;
+ accumulator += probabilities[i];
+ cumulative_probabilities[i] = accumulator;
+ }
+ }
+
+ public float get_cumulative_probability(int level) {
+ // clamp out-of-range pixels to prevent crashing.
+ level = level.clamp(0, 255);
+ return cumulative_probabilities[level];
+ }
+}
+
+public class ExpansionTransformation : HSVTransformation {
+ private float[] remap_table = null;
+ private const float LOW_DISCARD_MASS = 0.02f;
+ private const float HIGH_DISCARD_MASS = 0.02f;
+
+ private int low_kink;
+ private int high_kink;
+
+ public ExpansionTransformation(IntensityHistogram histogram) {
+ base(PixelTransformationType.TONE_EXPANSION);
+
+ remap_table = new float[256];
+
+ float LOW_KINK_MASS = LOW_DISCARD_MASS;
+ low_kink = 0;
+ while (histogram.get_cumulative_probability(low_kink) < LOW_KINK_MASS)
+ low_kink++;
+
+ float HIGH_KINK_MASS = 1.0f - HIGH_DISCARD_MASS;
+ high_kink = 255;
+ while ((histogram.get_cumulative_probability(high_kink) > HIGH_KINK_MASS) && (high_kink > 0))
+ high_kink--;
+
+ build_remap_table();
+ }
+
+ public ExpansionTransformation.from_extrema(int black_point, int white_point) {
+ base(PixelTransformationType.TONE_EXPANSION);
+
+ white_point = white_point.clamp(0, 255);
+ black_point = black_point.clamp(0, 255);
+
+ if (black_point == white_point) {
+ if (black_point == 0)
+ white_point = 1;
+ else if (white_point == 255)
+ black_point = 254;
+ else
+ black_point = white_point - 1;
+ }
+
+ low_kink = black_point;
+ high_kink = white_point;
+
+ build_remap_table();
+ }
+
+ public ExpansionTransformation.from_string(string encoded_transformation) {
+ base(PixelTransformationType.TONE_EXPANSION);
+
+ encoded_transformation.canon("0123456789. ", ' ');
+ encoded_transformation.chug();
+ encoded_transformation.chomp();
+
+ int num_captured = encoded_transformation.scanf("%d %d", &low_kink,
+ &high_kink);
+
+ assert(num_captured == 2);
+
+ build_remap_table();
+ }
+
+ private void build_remap_table() {
+ if (remap_table == null)
+ remap_table = new float[256];
+
+ float low_kink_f = ((float) low_kink) / 255.0f;
+ float high_kink_f = ((float) high_kink) / 255.0f;
+
+ float slope = 1.0f / (high_kink_f - low_kink_f);
+ float intercept = -(low_kink_f / (high_kink_f - low_kink_f));
+
+ int i = 0;
+ for ( ; i <= low_kink; i++)
+ remap_table[i] = 0.0f;
+
+ for ( ; i < high_kink; i++)
+ remap_table[i] = slope * (((float) i) / 255.0f) + intercept;
+
+ for ( ; i < 256; i++)
+ remap_table[i] = 1.0f;
+ }
+
+ public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel pixel) {
+ int remap_index = (int)(pixel.light_value * 255.0f);
+
+ HSVAnalyticPixel result = pixel;
+ result.light_value = remap_table[remap_index];
+
+ result.light_value = result.light_value.clamp(0.0f, 1.0f);
+
+ return result;
+ }
+
+ public override string to_string() {
+ return "{ %d, %d }".printf(low_kink, high_kink);
+ }
+
+ public int get_white_point() {
+ return high_kink;
+ }
+
+ public int get_black_point() {
+ return low_kink;
+ }
+
+ public override bool is_identity() {
+ return ((low_kink == 0) && (high_kink == 255));
+ }
+
+ public override PixelTransformation copy() {
+ return new ExpansionTransformation.from_extrema(low_kink, high_kink);
+ }
+}
+
+public class ShadowDetailTransformation : HSVTransformation {
+ private const float MAX_EFFECT_SHIFT = 0.5f;
+ private const float MIN_TONAL_WIDTH = 0.1f;
+ private const float MAX_TONAL_WIDTH = 1.0f;
+ private const float TONAL_WIDTH = 1.0f;
+
+ private float intensity = 0.0f;
+ private float[] remap_table = null;
+
+ public const float MIN_PARAMETER = 0.0f;
+ public const float MAX_PARAMETER = 32.0f;
+
+ public ShadowDetailTransformation(float user_intensity) {
+ base(PixelTransformationType.SHADOWS);
+
+ intensity = user_intensity;
+ float intensity_adj = (intensity / MAX_PARAMETER).clamp(0.0f, 1.0f);
+
+ float effect_shift = MAX_EFFECT_SHIFT * intensity_adj;
+ HermiteGammaApproximationFunction func =
+ new HermiteGammaApproximationFunction(TONAL_WIDTH);
+
+ remap_table = new float[256];
+ for (int i = 0; i < 256; i++) {
+ float x = ((float) i) / 255.0f;
+ float weight = func.evaluate(x);
+ remap_table[i] = (weight * (x + effect_shift)) + ((1.0f - weight) * x);
+ }
+ }
+
+ public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel pixel) {
+ HSVAnalyticPixel result = pixel;
+ result.light_value = (remap_table[(int)(pixel.light_value * 255.0f)]).clamp(0.0f, 1.0f);
+ return result;
+ }
+
+ public override PixelTransformation copy() {
+ return new ShadowDetailTransformation(intensity);
+ }
+
+ public override bool is_identity() {
+ return (intensity == 0.0f);
+ }
+
+ public float get_parameter() {
+ return intensity;
+ }
+}
+
+public class HermiteGammaApproximationFunction {
+ private float x_scale = 1.0f;
+ private float nonzero_interval_upper = 1.0f;
+
+ public HermiteGammaApproximationFunction(float user_interval_upper) {
+ nonzero_interval_upper = user_interval_upper.clamp(0.1f, 1.0f);
+ x_scale = 1.0f / nonzero_interval_upper;
+ }
+
+ public float evaluate(float x) {
+ if (x < 0.0f)
+ return 0.0f;
+ else if (x > nonzero_interval_upper)
+ return 0.0f;
+ else {
+ float indep_var = x_scale * x;
+
+ float dep_var = 6.0f * ((indep_var * indep_var * indep_var) -
+ (2.0f * (indep_var * indep_var)) + (indep_var));
+
+ return dep_var.clamp(0.0f, 1.0f);
+ }
+ }
+}
+
+public class HighlightDetailTransformation : HSVTransformation {
+ private const float MAX_EFFECT_SHIFT = 0.5f;
+ private const float MIN_TONAL_WIDTH = 0.1f;
+ private const float MAX_TONAL_WIDTH = 1.0f;
+ private const float TONAL_WIDTH = 1.0f;
+
+ private float intensity = 0.0f;
+ private float[] remap_table = null;
+
+ public const float MIN_PARAMETER = -32.0f;
+ public const float MAX_PARAMETER = 0.0f;
+
+ public HighlightDetailTransformation(float user_intensity) {
+ base(PixelTransformationType.HIGHLIGHTS);
+
+ intensity = user_intensity;
+ float intensity_adj = (intensity / MIN_PARAMETER).clamp(0.0f, 1.0f);
+
+ float effect_shift = MAX_EFFECT_SHIFT * intensity_adj;
+ HermiteGammaApproximationFunction func =
+ new HermiteGammaApproximationFunction(TONAL_WIDTH);
+
+ remap_table = new float[256];
+ for (int i = 0; i < 256; i++) {
+ float x = ((float) i) / 255.0f;
+ float weight = func.evaluate(1.0f - x);
+ remap_table[i] = (weight * (x - effect_shift)) + ((1.0f - weight) * x);
+ }
+ }
+
+ public override HSVAnalyticPixel transform_pixel_hsv(HSVAnalyticPixel pixel) {
+ HSVAnalyticPixel result = pixel;
+ result.light_value = (remap_table[(int)(pixel.light_value * 255.0f)]).clamp(0.0f, 1.0f);
+ return result;
+ }
+
+ public override PixelTransformation copy() {
+ return new HighlightDetailTransformation(intensity);
+ }
+
+ public override bool is_identity() {
+ return (intensity == 0.0f);
+ }
+
+ public float get_parameter() {
+ return intensity;
+ }
+}
+
+namespace AutoEnhance {
+ const int SHADOW_DETECT_MIN_INTENSITY = 8;
+ const int SHADOW_DETECT_MAX_INTENSITY = 100;
+ const int SHADOW_DETECT_INTENSITY_RANGE = SHADOW_DETECT_MAX_INTENSITY -
+ SHADOW_DETECT_MIN_INTENSITY;
+ const float SHADOW_MODE_HIGH_DISCARD_MASS = 0.02f;
+ const float SHADOW_AGGRESSIVENESS_MUL = 0.4f;
+ const int EMPIRICAL_DARK = 30;
+
+public PixelTransformationBundle create_auto_enhance_adjustments(Gdk.Pixbuf pixbuf) {
+ PixelTransformationBundle adjustments = new PixelTransformationBundle();
+
+ IntensityHistogram analysis_histogram = new IntensityHistogram(pixbuf);
+ /* compute the percentage of pixels in the image that fall into the shadow range --
+ this measures "of the pixels in the image, how many of them are in shadow?" */
+ float pct_in_range =
+ 100.0f *(analysis_histogram.get_cumulative_probability(SHADOW_DETECT_MAX_INTENSITY) -
+ analysis_histogram.get_cumulative_probability(SHADOW_DETECT_MIN_INTENSITY));
+
+ /* compute the mean intensity of the pixels that are in the shadow range -- this measures
+ "of those pixels that are in shadow, just how dark are they?" */
+ float shadow_range_mean_prob_val =
+ (analysis_histogram.get_cumulative_probability(SHADOW_DETECT_MIN_INTENSITY) +
+ analysis_histogram.get_cumulative_probability(SHADOW_DETECT_MAX_INTENSITY)) * 0.5f;
+ int shadow_mean_intensity = SHADOW_DETECT_MIN_INTENSITY;
+ for ( ; shadow_mean_intensity <= SHADOW_DETECT_MAX_INTENSITY; shadow_mean_intensity++) {
+ if (analysis_histogram.get_cumulative_probability(shadow_mean_intensity) >= shadow_range_mean_prob_val)
+ break;
+ }
+
+ /* if more than 40 percent of the pixels in the image are in the shadow detection range,
+ or if the mean intensity within the shadow range is less than 50 (an empirically
+ determined threshold below which pixels appear very dark), regardless of the
+ percent of pixels in it, then perform shadow detail enhancement. Otherwise,
+ skip shadow detail enhancement and perform a traditional contrast expansion */
+ if ((pct_in_range > 40.0f) || (pct_in_range > 20.0f) && (shadow_mean_intensity < EMPIRICAL_DARK)) {
+ float shadow_trans_effect_size = ((((float) SHADOW_DETECT_MAX_INTENSITY) -
+ ((float) shadow_mean_intensity)) / ((float) SHADOW_DETECT_INTENSITY_RANGE)) *
+ ShadowDetailTransformation.MAX_PARAMETER;
+
+ shadow_trans_effect_size *= SHADOW_AGGRESSIVENESS_MUL;
+
+ adjustments.set(new ShadowDetailTransformation(shadow_trans_effect_size));
+
+ /* if shadow detail expansion is being performed, we still perform contrast expansion,
+ but only on the top end */
+ int discard_point = 255;
+ for ( ; discard_point > -1; discard_point--) {
+ if ((1.0f - analysis_histogram.get_cumulative_probability(discard_point)) >
+ SHADOW_MODE_HIGH_DISCARD_MASS)
+ break;
+ }
+
+ adjustments.set(new ExpansionTransformation.from_extrema(0, discard_point));
+ }
+ else {
+ adjustments.set(new ExpansionTransformation(analysis_histogram));
+ adjustments.set(new ShadowDetailTransformation(0));
+ }
+ /* zero out any existing color transformations as these may conflict with
+ auto-enhancement */
+ adjustments.set(new HighlightDetailTransformation(0.0f));
+ adjustments.set(new TemperatureTransformation(0.0f));
+ adjustments.set(new TintTransformation(0.0f));
+ adjustments.set(new ExposureTransformation(0.0f));
+ adjustments.set(new SaturationTransformation(0.0f));
+
+ return adjustments;
+}
+}
+