diff options
Diffstat (limited to 'src/ColorTransformation.vala')
-rw-r--r-- | src/ColorTransformation.vala | 1519 |
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; +} +} + |