private void InsertMinMax(ImageUtility.float3 v, ImageUtility.float3[] _Min, ImageUtility.float3[] _Max, int _Length) { if (v.y < _Min[_Length - 1].y) { // We're sure we're good enough for that array! for (int i = 0; i < _Length; i++) { if (v.y < _Min[i].y) { // Insert here if (i != _Length - 1) { Array.Copy(_Min, i, _Min, i + 1, _Length - i - 1); } _Min[i] = v; break; } } } if (v.y > _Max[_Length - 1].y) { // We're sure we're good enough for that array! for (int i = 0; i < _Length; i++) { if (v.y > _Max[i].y) { // Insert here if (i != _Length - 1) { Array.Copy(_Max, i, _Max, i + 1, _Length - i - 1); } _Max[i] = v; break; } } } }
/// <summary> /// Calibrates a raw luminance value with spatial luminance correction /// </summary> /// <param name="_U">The U coordinate in the image (U=X/Width)</param> /// <param name="_V">The V coordinate in the image (V=Y/Height)</param> /// <param name="_Luminance">The uncalibrated value</param> /// <returns>The calibrated reflectance value</returns> /// <remarks>Typically, you start from a RAW XYZ value that you convert to xyY, pass it to this method /// and replace it into your orignal xyY, convert back to XYZ and voilà !</remarks> public ImageUtility.float3 CalibrateWithSpatialCorrection(float _U, float _V, ImageUtility.float3 _xyY) { if (m_RootNode == null) { throw new Exception("Calibration grid hasn't been built: did you provide a valid database path? Does the path contain camera calibration data?"); } if (m_InterpolatedCalibration == null) { throw new Exception("Calibration grid hasn't been prepared for calibration: did you call the PrepareCalibrationFor() method?"); } float CorrectionFactor = m_WhiteReflectanceCorrectionFactor; CorrectionFactor *= GetSpatialLuminanceCorrectionFactor(_U, _V); // Apply spatial correction float Reflectance = m_InterpolatedCalibration.Calibrate(CorrectionFactor * _xyY.z); _xyY.z = Reflectance; if (m_DoWhiteBalance) { ImageUtility.float3 XYZ = ImageUtility.ColorProfile.xyY2XYZ(_xyY); XYZ *= m_WhiteBalanceXYZ; _xyY = ImageUtility.ColorProfile.XYZ2xyY(XYZ); } return(_xyY); }
/// <summary> /// Computes the statistical mean of the values /// </summary> /// <param name="_Values"></param> /// <returns></returns> private ImageUtility.float3 ComputeMean(ImageUtility.float3[] _Values) { ImageUtility.float3 AvgXYZ = new ImageUtility.float3(0, 0, 0); for (int i = 0; i < _Values.Length; i++) { AvgXYZ += _Values[i]; } AvgXYZ = (1.0f / _Values.Length) * AvgXYZ; return(AvgXYZ); }
/// <summary> /// Builds a swatch bitmap /// </summary> /// <param name="_Width"></param> /// <param name="_Height"></param> /// <param name="_xyY"></param> /// <returns></returns> private ImageUtility.Bitmap BuildSwatch(int _Width, int _Height, ImageUtility.float3 _xyY) { ImageUtility.Bitmap Result = new ImageUtility.Bitmap(_Width, _Height, new ImageUtility.ColorProfile(ImageUtility.ColorProfile.STANDARD_PROFILE.sRGB)); ImageUtility.float4 XYZ = new ImageUtility.float4(ImageUtility.ColorProfile.xyY2XYZ(_xyY), 1.0f); for (int Y = 0; Y < _Height; Y++) { for (int X = 0; X < _Width; X++) { Result.ContentXYZ[X, Y] = XYZ; } } return(Result); }
/// <summary> /// Computes the statistical mode of the values /// </summary> /// <param name="_Values"></param> /// <returns></returns> private ImageUtility.float3 ComputeMode(ImageUtility.float3[] _Values) { int COUNT = _Values.Length; // The idea is simply to discretize the set of values and count the ones that are the most represented float Start = _Values[0].y; float End = _Values[_Values.Length - 1].y; float IntervalLength = 0.0f, Normalizer = 0.0f; if (Math.Abs(End - Start) > 1e-6f) { IntervalLength = (End - Start) / COUNT; Normalizer = 1.0f / IntervalLength; } // Fill bins int[] Bins = new int[COUNT]; ImageUtility.float3[] BinsSum = new ImageUtility.float3[COUNT]; for (int i = 0; i < _Values.Length; i++) { int BinIndex = Math.Min(COUNT - 1, (int)Math.Floor((_Values[i].y - Start) * Normalizer)); Bins[BinIndex]++; BinsSum[BinIndex] += _Values[i]; } // Find the one that contains most values (yep, that's the mode!) int MaxValuesCount = 0; int MaxValuesBinIndex = -1; for (int BinIndex = 0; BinIndex < COUNT; BinIndex++) { if (Bins[BinIndex] > MaxValuesCount) { // More filled up bin discovered! MaxValuesCount = Bins[BinIndex]; MaxValuesBinIndex = BinIndex; } } ImageUtility.float3 ModeXYZ = (1.0f / MaxValuesCount) * BinsSum[MaxValuesBinIndex]; return(ModeXYZ); }
/// <summary> /// Computes the average color within a rectangle in UV space /// </summary> /// <param name="_TopLeft">The top left corner (in UV space) of the rectangle to sample</param> /// <param name="_BottomRight">The bottom right corner (in UV space) of the rectangle to sample</param> /// <returns>The average xyY color</returns> public ImageUtility.float3 ComputeAverageSwatchColor(ImageUtility.float2 _TopLeft, ImageUtility.float2 _BottomRight) { // Average xyY values in the specified rectangle int X0 = Math.Max(0, Math.Min(m_Texture.Width - 1, (int)Math.Floor(_TopLeft.x * m_Texture.Width))); int Y0 = Math.Max(0, Math.Min(m_Texture.Height - 1, (int)Math.Floor(_TopLeft.y * m_Texture.Height))); int X1 = Math.Min(m_Texture.Width, Math.Max(X0 + 1, (int)Math.Floor(_BottomRight.x * m_Texture.Width))); int Y1 = Math.Min(m_Texture.Height, Math.Max(Y0 + 1, (int)Math.Floor(_BottomRight.y * m_Texture.Height))); int W = X1 - X0; int H = Y1 - Y0; ImageUtility.float4 AverageXYZ = new ImageUtility.float4(0, 0, 0, 0); for (int Y = Y0; Y < Y1; Y++) { for (int X = X0; X < X1; X++) { ImageUtility.float4 XYZ = m_Texture.ContentXYZ[X, Y]; AverageXYZ += XYZ; } } AverageXYZ = (1.0f / (W * H)) * AverageXYZ; ImageUtility.float3 xyY = ImageUtility.ColorProfile.XYZ2xyY((ImageUtility.float3)AverageXYZ); return(xyY); }
/// <summary> /// Integrates the luminance of the pixels in a given circle selected by the user /// </summary> /// <param name="_X"></param> /// <param name="_Y"></param> /// <param name="_Radius"></param> /// <param name="_PercentOfBlackValues"></param> /// <param name="_HasSaturatedValues">Returns the percentage of encountered saturated values. 0 is okay, more means the probe shouldn't be used</param> /// <returns></returns> private ImageUtility.float3 IntegrateLuminance( float _X, float _Y, float _Radius, out ImageUtility.float3 _MinXYZ, out ImageUtility.float3 _MaxXYZ, out float _PercentOfBlackValues, out float _PercentOfSaturatedValues ) { float Radius = Math.Max( 1, _Radius * m_BitmapXYZ.Width ); float CenterX = _X * m_BitmapXYZ.Width; float CenterY = _Y * m_BitmapXYZ.Height; float SqRadius = Radius*Radius; int X0 = Math.Max( 0, Math.Min( m_BitmapXYZ.Width-1, (int) Math.Floor( CenterX - Radius ) ) ); int X1 = Math.Max( 0, Math.Min( m_BitmapXYZ.Width-1, (int) Math.Ceiling( CenterX + Radius ) ) ); int Y0 = Math.Max( 0, Math.Min( m_BitmapXYZ.Height-1, (int) Math.Floor( CenterY - Radius ) ) ); int Y1 = Math.Max( 0, Math.Min( m_BitmapXYZ.Height-1, (int) Math.Ceiling( CenterY + Radius ) ) ); const float SMOOTHSTEP_MAX_RADIUS = 0.2f; // We reach max weight 1 at 20% of the border of the circle bool ApplySpatialCorrection = checkBoxSpatialLuminanceCorrection.Checked; int TotalBlackValuesCount = 0; int TotalSaturatedValuesCount = 0; int TotalValuesCount = 0; ImageUtility.float3 SumXYZ = new ImageUtility.float3( 0, 0, 0 ); _MinXYZ = new ImageUtility.float3( 0, +float.MaxValue, 0 ); _MaxXYZ = new ImageUtility.float3( 0, 0, 0 ); float SumWeights = 0.0f; for ( int Y=Y0; Y < Y1; Y++ ) for ( int X=X0; X < X1; X++ ) { float SqR = (X-CenterX)*(X-CenterX) + (Y-CenterY)*(Y-CenterY); if ( SqR > SqRadius ) continue; float r = (float) Math.Sqrt( SqR ) / Radius; // Nomalized radius // float Weight = Math.Min( 1.0f, floatTrackbarControl1.Value * (float) Math.Exp( -floatTrackbarControl2.Value * r ) ); float x = Math.Max( 0.0f, Math.Min( 1.0f, (1.0f - r) / SMOOTHSTEP_MAX_RADIUS ) ); float Weight = x*x*(3.0f - 2.0f*x); //DEBUG m_BitmapXYZ.ContentXYZ[X,Y].y = Weight; ImageUtility.float3 XYZ = (ImageUtility.float3) m_BitmapXYZ.ContentXYZ[X,Y]; if ( ApplySpatialCorrection ) { ImageUtility.float3 xyY = ImageUtility.ColorProfile.XYZ2xyY( XYZ ); xyY.z *= m_CalibrationDatabase.GetSpatialLuminanceCorrectionFactor( (float) X / m_BitmapXYZ.Width, (float) Y / m_BitmapXYZ.Height ); XYZ = ImageUtility.ColorProfile.xyY2XYZ( xyY ); } if ( XYZ.y < 0.001f ) TotalBlackValuesCount++; // Warning! if ( XYZ.y > 0.999f ) TotalSaturatedValuesCount++; // Warning! TotalValuesCount++; if ( XYZ.y < _MinXYZ.y ) _MinXYZ = XYZ; if ( XYZ.y > _MaxXYZ.y ) _MaxXYZ = XYZ; SumXYZ += Weight * XYZ; SumWeights += Weight; } //DEBUG RebuildImage(); _PercentOfBlackValues = (float) TotalBlackValuesCount / TotalValuesCount; _PercentOfSaturatedValues = (float) TotalSaturatedValuesCount / TotalValuesCount; SumXYZ = (1.0f/SumWeights) * SumXYZ; return SumXYZ; }
private void buttonWhiteRefTest3_Click( object sender, EventArgs e ) { int W = DEFAULT_WHITE_REFERENCE_IMAGE_SIZE; int H = DEFAULT_WHITE_REFERENCE_IMAGE_SIZE; ImageUtility.Bitmap WhiteRef = new ImageUtility.Bitmap( W, H, m_sRGBProfile ); ImageUtility.float3 xyY = new ImageUtility.float3( m_sRGBProfile.Chromas.W.x, m_sRGBProfile.Chromas.W.y, 0.0f ); for ( int Y=0; Y < H; Y++ ) { float V = (float) Y / H; for ( int X=0; X < W; X++ ) { float U = (float) X / W; xyY.z = Math.Min( 1.0f, 1.0f - 0.5f * V ); ImageUtility.float4 XYZ = new ImageUtility.float4( ImageUtility.ColorProfile.xyY2XYZ( xyY ), 1.0f ); WhiteRef.ContentXYZ[X,Y] = XYZ; } } // Assign to the database m_CalibrationDatabase.WhiteReferenceImage = WhiteRef; UpdateWhiteReferenceImageUI(); }
private void buttonResetWhiteReflectance_Click( object sender, EventArgs e ) { m_CalibrationDatabase.WhiteReflectanceReference = new ImageUtility.float3( 0, 0, -1 ); // Will reset the factor // Clear cached white reference data m_WhiteReflectanceReference = m_CalibrationDatabase.WhiteReflectanceReference; m_WhiteReflectanceISOSpeed = -1; m_WhiteReflectanceShutterSpeed = -1; m_WhiteReflectanceAperture = -1; SetRegKey( "ReloadWhiteReflectanceOnStartup", "false" ); // Prevent auto load on startup UpdateWhiteReflectanceUI(); }
////////////////////////////////////////////////////////////////////////// // White Reflectance Reference // private void buttonPickWhiteReflectance_Click( object sender, EventArgs e ) { if ( m_BitmapXYZ == null ) { // No image loaded you moron! MessageBox( "Can't pick white reflectance as no image is currently loaded!", MessageBoxButtons.OK, MessageBoxIcon.Exclamation ); return; } outputPanel.StartCalibrationTargetPicking( ( ImageUtility.float2 _Center, float _Radius ) => { ImageUtility.float3 MinXYZ, MaxXYZ; float BlackValues, SaturatedValues; ImageUtility.float3 AverageXYZ = IntegrateLuminance( _Center.x, _Center.y, _Radius, out MinXYZ, out MaxXYZ, out BlackValues, out SaturatedValues ); if ( BlackValues > BLACK_VALUES_TOLERANCE ) { MessageBox( "This probe has more than 5% luminance values that are too dark, it cannot be used as a white reference!", MessageBoxButtons.OK, MessageBoxIcon.Warning ); return; } if ( SaturatedValues > SATURATED_VALUES_TOLERANCE ) { if ( MessageBox( "This probe has more than 5% luminance values that are saturated, it SHOULD NOT be used as a white reference!\r\n\r\nDo you want to use it anyway?", MessageBoxButtons.YesNo, MessageBoxIcon.Warning ) != DialogResult.Yes ) return; } // Scale the luminance based on the user-supplied white target // ImageUtility.float3 WhiteRefxyY = ImageUtility.ColorProfile.XYZ2xyY( MaxXYZ ); // Use Max otherwise we can get luminances higher than 99%! ImageUtility.float3 WhiteRefxyY = ImageUtility.ColorProfile.XYZ2xyY( AverageXYZ ); // NO! Use Average because we want a white probe to return exactly the same result as when we sampled it at calibration time!!!! WhiteRefxyY.z *= 99.0f / floatTrackbarControlTargetWhiteReflectance.Value; m_CalibrationDatabase.WhiteReflectanceReference = WhiteRefxyY; // Cache white reference data m_WhiteReflectanceReference = m_CalibrationDatabase.WhiteReflectanceReference; m_WhiteReflectanceISOSpeed = m_CalibrationDatabase.PreparedForISOSpeed; m_WhiteReflectanceShutterSpeed = m_CalibrationDatabase.PreparedForShutterSpeed; m_WhiteReflectanceAperture = m_CalibrationDatabase.PreparedForAperture; UpdateWhiteReflectanceUI(); } ); }
/// <summary> /// Computes the statistical mean of the values /// </summary> /// <param name="_Values"></param> /// <returns></returns> private ImageUtility.float3 ComputeMean( ImageUtility.float3[] _Values ) { ImageUtility.float3 AvgXYZ = new ImageUtility.float3( 0, 0, 0 ); for ( int i=0; i < _Values.Length; i++ ) AvgXYZ += _Values[i]; AvgXYZ = (1.0f / _Values.Length) * AvgXYZ; return AvgXYZ; }
public ImageUtility.float3 m_xyY; // Last picked xyY color #endregion Fields #region Methods public virtual void UpdateSwatchColor() { m_RGB = (ImageUtility.float3) m_Owner.m_sRGBProfile.XYZ2RGB( new ImageUtility.float4( ImageUtility.ColorProfile.xyY2XYZ( m_xyY ), 1.0f ) ); Color C = Color.FromArgb( Math.Max( 0, Math.Min( 255, (int) (m_RGB.x * 255.0f) ) ), Math.Max( 0, Math.Min( 255, (int) (m_RGB.y * 255.0f) ) ), Math.Max( 0, Math.Min( 255, (int) (m_RGB.z * 255.0f) ) ) ); m_Panel.BackColor = C; m_Panel.ForeColor = m_xyY.z > 1.0f ? Color.Red : Color.Black; }
public ImageUtility.Bitmap Texture; // The bitmap generated from the swatch color public virtual void Save(CalibratedTexture _Owner, XmlElement _SwatchElement) { ImageUtility.float4 XYZ = new ImageUtility.float4(ImageUtility.ColorProfile.xyY2XYZ(xyY), 1.0f); ImageUtility.float3 RGB = (ImageUtility.float3)Texture.Profile.XYZ2RGB(XYZ); _Owner.SetAttribute(_SwatchElement, "xyY", xyY.ToString()).SetAttribute("RGB", RGB.ToString()); }
/// <summary> /// Captures the calibrated texture /// </summary> /// <param name="_Source">The source image to capture</param> /// <param name="_Database">Database to perform proper calibration</param> /// <param name="_Parms">Parameters for the capture</param> public void Capture(ImageUtility.Bitmap _Source, CameraCalibrationDatabase _Database, CaptureParms _Parms) { if (_Source == null) { throw new Exception("Invalid source bitmap to build texture from!"); } if (_Database == null) { throw new Exception("Invalid calibration database found in parameters!"); } if (_Parms == null) { throw new Exception("Invalid calibration parameters!"); } if (m_SwatchWidth <= 0 || m_SwatchHeight <= 0) { throw new Exception("Invalid swatch size! Must be > 0!"); } // Save parameters as they're associated to this texture m_CaptureParameters = _Parms; m_WhiteReflectanceReference = _Database.WhiteReflectanceReference; m_WhiteReflectanceCorrectionFactor = _Database.WhiteReflectanceCorrectionFactor; m_SpatialCorrectionEnabled = _Database.WhiteReferenceImage != null; ////////////////////////////////////////////////////////////////////////// // Setup the database to find the most appropriate calibration data for our image infos _Database.PrepareCalibrationFor(_Parms.ISOSpeed, _Parms.ShutterSpeed, _Parms.Aperture); ////////////////////////////////////////////////////////////////////////// // Build target texture ImageUtility.float4 AvgXYZ = new ImageUtility.float4(0, 0, 0, 0); //DEBUG // float MinLuminance_Raw = float.MaxValue; // float MaxLuminance_Raw = -float.MaxValue; const int EXTREME_VALUES_COUNT = 100; ImageUtility.float3[] ArrayMin = new ImageUtility.float3[EXTREME_VALUES_COUNT]; ImageUtility.float3[] ArrayMax = new ImageUtility.float3[EXTREME_VALUES_COUNT]; for (int i = 0; i < EXTREME_VALUES_COUNT; i++) { ArrayMin[i] = new ImageUtility.float3(0, 1, 0); ArrayMax[i] = new ImageUtility.float3(0, 0, 0); } if (_Parms.CropSource) { float fImageWidth = 2.0f * _Parms.CropRectangleHalfSize.x * _Source.Height; float fImageHeight = 2.0f * _Parms.CropRectangleHalfSize.y * _Source.Height; int W = (int)Math.Floor(fImageWidth); int H = (int)Math.Floor(fImageHeight); ImageUtility.float2 AxisX = new ImageUtility.float2((float)Math.Cos(_Parms.CropRectangleRotation), -(float)Math.Sin(_Parms.CropRectangleRotation)); ImageUtility.float2 AxisY = new ImageUtility.float2((float)Math.Sin(_Parms.CropRectangleRotation), (float)Math.Cos(_Parms.CropRectangleRotation)); ImageUtility.float2 TopLeftCorner = new ImageUtility.float2(0.5f * (_Source.Width - _Source.Height) + _Parms.CropRectangleCenter.x * _Source.Height, _Source.Height * _Parms.CropRectangleCenter.y) + _Source.Height * (-_Parms.CropRectangleHalfSize.x * AxisX - _Parms.CropRectangleHalfSize.y * AxisY); m_Texture = new ImageUtility.Bitmap(W, H, new ImageUtility.ColorProfile(ImageUtility.ColorProfile.STANDARD_PROFILE.sRGB)); ImageUtility.float4 XYZ; ImageUtility.float3 ShortXYZ; ImageUtility.float3 xyY; ImageUtility.float2 CurrentScanlinePixel = TopLeftCorner + 0.5f * (fImageWidth - W) * AxisX + 0.5f * (fImageHeight - H) * AxisY; if (Math.Abs(_Parms.CropRectangleRotation) < 1e-6f) { // Use integer pixels to avoid attenuated values due to bilinear filtering CurrentScanlinePixel.x = (float)Math.Floor(CurrentScanlinePixel.x); CurrentScanlinePixel.y = (float)Math.Floor(CurrentScanlinePixel.y); } for (int Y = 0; Y < H; Y++) { ImageUtility.float2 CurrentPixel = CurrentScanlinePixel; for (int X = 0; X < W; X++) { float U = CurrentPixel.x / _Source.Width; float V = CurrentPixel.y / _Source.Height; XYZ = _Source.BilinearSample(CurrentPixel.x, CurrentPixel.y); //DEBUG // float L = XYZ.y * _Database.GetSpatialLuminanceCorrectionFactor( U, V ); // if ( L < MinLuminance_Raw ) // MinLuminance_Raw = L; // if ( L > MaxLuminance_Raw ) // MaxLuminance_Raw = L; //DEBUG xyY = ImageUtility.ColorProfile.XYZ2xyY((ImageUtility.float3)XYZ); xyY = _Database.CalibrateWithSpatialCorrection(U, V, xyY); // Apply luminance calibration ShortXYZ = ImageUtility.ColorProfile.xyY2XYZ(xyY); XYZ = new ImageUtility.float4(ShortXYZ, XYZ.w); m_Texture.ContentXYZ[X, Y] = XYZ; // Update min/max/avg values InsertMinMax(ShortXYZ, ArrayMin, ArrayMax, EXTREME_VALUES_COUNT); AvgXYZ += XYZ; CurrentPixel += AxisX; } CurrentScanlinePixel += AxisY; } } else { // Simple texture copy, with luminance calibration m_Texture = new ImageUtility.Bitmap(_Source.Width, _Source.Height, new ImageUtility.ColorProfile(ImageUtility.ColorProfile.STANDARD_PROFILE.sRGB)); ImageUtility.float4 XYZ; ImageUtility.float3 ShortXYZ; ImageUtility.float3 xyY; int W = m_Texture.Width; int H = m_Texture.Height; int X0 = 0; int X1 = W; int Y0 = 0; int Y1 = H; //DEBUG // X0 = 1088; Y0 = 764; // X1 = X0 + 1100; Y1 = Y0 + 632; for (int Y = Y0; Y < Y1; Y++) { float V = (float)Y / H; for (int X = X0; X < X1; X++) { float U = (float)X / W; XYZ = _Source.ContentXYZ[X, Y]; //DEBUG // float L = XYZ.y * _Database.GetSpatialLuminanceCorrectionFactor( U, V ); // if ( L < MinLuminance_Raw ) // MinLuminance_Raw = L; // if ( L > MaxLuminance_Raw ) // MaxLuminance_Raw = L; //DEBUG xyY = ImageUtility.ColorProfile.XYZ2xyY((ImageUtility.float3)XYZ); xyY = _Database.CalibrateWithSpatialCorrection(U, V, xyY); // Apply luminance calibration ShortXYZ = ImageUtility.ColorProfile.xyY2XYZ(xyY); XYZ = new ImageUtility.float4(ShortXYZ, XYZ.w); m_Texture.ContentXYZ[X, Y] = XYZ; // Update min/max/avg values InsertMinMax(ShortXYZ, ArrayMin, ArrayMax, EXTREME_VALUES_COUNT); AvgXYZ += XYZ; } } } // Normalize average swatch color float Normalizer = 1.0f / (m_Texture.Width * m_Texture.Height); ImageUtility.float3 avgxyY = ImageUtility.ColorProfile.XYZ2xyY(Normalizer * ((ImageUtility.float3)AvgXYZ)); m_SwatchAvg.xyY = avgxyY; // Compute min & max using statistical norm ImageUtility.float3 BestXYZ_Min; ImageUtility.float3 BestXYZ_Max; if (_Parms.UseModeInsteadOfMean) { // Use mode BestXYZ_Min = ComputeMode(ArrayMin); BestXYZ_Max = ComputeMode(ArrayMax); } else { // Use mean BestXYZ_Min = ComputeMean(ArrayMin); BestXYZ_Max = ComputeMean(ArrayMax); } m_SwatchMin.xyY = ImageUtility.ColorProfile.XYZ2xyY(BestXYZ_Min); m_SwatchMax.xyY = ImageUtility.ColorProfile.XYZ2xyY(BestXYZ_Max); m_SwatchMin.Texture = BuildSwatch(m_SwatchWidth, m_SwatchHeight, m_SwatchMin.xyY); m_SwatchMax.Texture = BuildSwatch(m_SwatchWidth, m_SwatchHeight, m_SwatchMax.xyY); m_SwatchAvg.Texture = BuildSwatch(m_SwatchWidth, m_SwatchHeight, m_SwatchAvg.xyY); // Rebuild custom swatches foreach (CustomSwatch CS in m_CustomSwatches) { CS.Texture = BuildSwatch(m_SwatchWidth, m_SwatchHeight, CS.xyY); } ////////////////////////////////////////////////////////////////////////// // Feed some purely informational shot infos to the main texture, probably won't be saved anyway... m_Texture.HasValidShotInfo = true; m_Texture.ISOSpeed = _Parms.ISOSpeed; m_Texture.ShutterSpeed = _Parms.ShutterSpeed; m_Texture.Aperture = _Parms.Aperture; }
/// <summary> /// Loads the white reflectance reference from a file /// </summary> /// <param name="_WhiteReflectanceFileName"></param> /// <param name="_AlertIfFailed"></param> private void LoadWhiteReflectanceFile( System.IO.FileInfo _FileName, bool _AlertIfFailed ) { try { // Load System.Xml.XmlDocument Doc = new System.Xml.XmlDocument(); Doc.Load( _FileName.FullName ); System.Xml.XmlElement Root = Doc["WhiteReflectance"]; if ( Root == null ) throw new Exception( "Couldn't find expected root element \"WhiteReflectance\"! Is this a white ref file?" ); m_WhiteReflectanceReference = new ImageUtility.float3( 0, 0, -1 ); // Invalid m_WhiteReflectanceReference = ImageUtility.float3.Parse( Root.GetAttribute( "Value" ) ); m_WhiteReflectanceISOSpeed = floatTrackbarControlISOSpeed.Value; float.TryParse( Root.GetAttribute( "ISOSpeed" ), out m_WhiteReflectanceISOSpeed ); m_WhiteReflectanceShutterSpeed = floatTrackbarControlShutterSpeed.Value; float.TryParse( Root.GetAttribute( "ShutterSpeed" ), out m_WhiteReflectanceShutterSpeed ); m_WhiteReflectanceAperture = floatTrackbarControlAperture.Value; float.TryParse( Root.GetAttribute( "Aperture" ), out m_WhiteReflectanceAperture ); m_CalibrationDatabase.WhiteReflectanceReference = m_WhiteReflectanceReference; UpdateWhiteReflectanceUI(); } catch ( Exception _e ) { if ( _AlertIfFailed ) MessageBox( "An error occurred while loading white reflectance file \"" + _FileName.FullName + "\":\r\n\r\n", _e ); } }
public ImageUtility.float2 m_LocationTopLeft = new ImageUtility.float2( -1, -1 ); // Last picked location #endregion Fields #region Methods public override void UpdateSwatchColor() { if ( !m_CheckBox.Checked || m_Owner.m_Texture == null ) { m_CheckBox.Checked = false; m_xyY = m_RGB = new ImageUtility.float3( 0, 0, 0 ); m_Panel.BackColor = Color.DimGray; return; } // Re-capture m_xyY = m_Owner.m_Texture.ComputeAverageSwatchColor( m_LocationTopLeft, m_LocationBottomRight ); base.UpdateSwatchColor(); }
void TestChromaRanges() { ImageUtility.ColorProfile profile = new ImageUtility.ColorProfile(ImageUtility.ColorProfile.STANDARD_PROFILE.sRGB ); ImageUtility.float3 tempFloat3 = new ImageUtility.float3( 0, 0, 0 ); ImageUtility.float4 tempFloat4 = new ImageUtility.float4( 0, 0, 0, 1 ); float Ygo, Cg, Co; ranges_t[] ranges = new ranges_t[4]; for ( int lumaIndex=0; lumaIndex < ranges.Length; lumaIndex++ ) { ranges_t range = new ranges_t(); ranges[lumaIndex] = range; float L = (1+lumaIndex) / 255.0f; for ( int R=0; R < 256; R++ ) { for ( int G=0; G < 256; G++ ) { for ( int B=0; B < 256; B++ ) { tempFloat4.x = L * R; tempFloat4.y = L * G; tempFloat4.z = L * B; // Convert to YCoCg // Ygo = 0.25f * tempFloat4.x + 0.5f * tempFloat4.y + 0.25f * tempFloat4.z; // Cg = -0.25f * tempFloat4.x + 0.5f * tempFloat4.y - 0.25f * tempFloat4.z; // Co = 0.50f * tempFloat4.x + 0.0f * tempFloat4.y - 0.50f * tempFloat4.z; RGB2YCoCg( tempFloat4.x, tempFloat4.y, tempFloat4.z, out Ygo, out Co, out Cg ); YCoCg2RGB( Ygo, Co, Cg, out tempFloat3.x, out tempFloat3.y, out tempFloat3.z ); if ( Math.Abs( tempFloat3.x - tempFloat4.x ) > 1e-6 ) throw new Exception( "RHA!" ); if ( Math.Abs( tempFloat3.y - tempFloat4.y ) > 1e-6 ) throw new Exception( "RHA!" ); if ( Math.Abs( tempFloat3.z - tempFloat4.z ) > 1e-6 ) throw new Exception( "RHA!" ); // Convert to xyY ImageUtility.float4 XYZ = profile.RGB2XYZ( tempFloat4 ); tempFloat3.x = XYZ.x; tempFloat3.y = XYZ.y; tempFloat3.z = XYZ.z; ImageUtility.float3 xyY = ImageUtility.ColorProfile.XYZ2xyY( tempFloat3 ); // Update ranges range.Ygo_min = Math.Min( range.Ygo_min, Ygo ); range.Ygo_max = Math.Max( range.Ygo_max, Ygo ); range.Cg_min = Math.Min( range.Cg_min, Cg ); range.Cg_max = Math.Max( range.Cg_max, Cg ); range.Co_min = Math.Min( range.Co_min, Co ); range.Co_max = Math.Max( range.Co_max, Co ); range.Y_min = Math.Min( range.Y_min, xyY.z ); range.Y_max = Math.Max( range.Y_max, xyY.z ); range.x_min = Math.Min( range.x_min, xyY.x ); range.x_max = Math.Max( range.x_max, xyY.x ); range.y_min = Math.Min( range.y_min, xyY.y ); range.y_max = Math.Max( range.y_max, xyY.y ); } } } } }
/// <summary> /// Captures the calibrated texture /// </summary> /// <param name="_Source">The source image to capture</param> /// <param name="_Database">Database to perform proper calibration</param> /// <param name="_Parms">Parameters for the capture</param> public void Capture( ImageUtility.Bitmap _Source, CameraCalibrationDatabase _Database, CaptureParms _Parms ) { if ( _Source == null ) throw new Exception( "Invalid source bitmap to build texture from!" ); if ( _Database == null ) throw new Exception( "Invalid calibration database found in parameters!" ); if ( _Parms == null ) throw new Exception( "Invalid calibration parameters!" ); if ( m_SwatchWidth <= 0 || m_SwatchHeight <= 0 ) throw new Exception( "Invalid swatch size! Must be > 0!" ); // Save parameters as they're associated to this texture m_CaptureParameters = _Parms; m_WhiteReflectanceReference = _Database.WhiteReflectanceReference; m_WhiteReflectanceCorrectionFactor = _Database.WhiteReflectanceCorrectionFactor; m_SpatialCorrectionEnabled = _Database.WhiteReferenceImage != null; ////////////////////////////////////////////////////////////////////////// // Setup the database to find the most appropriate calibration data for our image infos _Database.PrepareCalibrationFor( _Parms.ISOSpeed, _Parms.ShutterSpeed, _Parms.Aperture ); ////////////////////////////////////////////////////////////////////////// // Build target texture ImageUtility.float4 AvgXYZ = new ImageUtility.float4( 0, 0, 0, 0 ); //DEBUG // float MinLuminance_Raw = float.MaxValue; // float MaxLuminance_Raw = -float.MaxValue; const int EXTREME_VALUES_COUNT = 100; ImageUtility.float3[] ArrayMin = new ImageUtility.float3[EXTREME_VALUES_COUNT]; ImageUtility.float3[] ArrayMax = new ImageUtility.float3[EXTREME_VALUES_COUNT]; for ( int i=0; i < EXTREME_VALUES_COUNT; i++ ) { ArrayMin[i] = new ImageUtility.float3( 0, 1, 0 ); ArrayMax[i] = new ImageUtility.float3( 0, 0, 0 ); } if ( _Parms.CropSource ) { float fImageWidth = 2.0f * _Parms.CropRectangleHalfSize.x * _Source.Height; float fImageHeight = 2.0f * _Parms.CropRectangleHalfSize.y * _Source.Height; int W = (int) Math.Floor( fImageWidth ); int H = (int) Math.Floor( fImageHeight ); ImageUtility.float2 AxisX = new ImageUtility.float2( (float) Math.Cos( _Parms.CropRectangleRotation ), -(float) Math.Sin( _Parms.CropRectangleRotation ) ); ImageUtility.float2 AxisY = new ImageUtility.float2( (float) Math.Sin( _Parms.CropRectangleRotation ), (float) Math.Cos( _Parms.CropRectangleRotation ) ); ImageUtility.float2 TopLeftCorner = new ImageUtility.float2( 0.5f * (_Source.Width - _Source.Height) + _Parms.CropRectangleCenter.x * _Source.Height, _Source.Height * _Parms.CropRectangleCenter.y ) + _Source.Height * (-_Parms.CropRectangleHalfSize.x * AxisX - _Parms.CropRectangleHalfSize.y * AxisY); m_Texture = new ImageUtility.Bitmap( W, H, new ImageUtility.ColorProfile( ImageUtility.ColorProfile.STANDARD_PROFILE.sRGB ) ); ImageUtility.float4 XYZ; ImageUtility.float3 ShortXYZ; ImageUtility.float3 xyY; ImageUtility.float2 CurrentScanlinePixel = TopLeftCorner + 0.5f * (fImageWidth - W) * AxisX + 0.5f * (fImageHeight - H) * AxisY; if ( Math.Abs( _Parms.CropRectangleRotation ) < 1e-6f ) { // Use integer pixels to avoid attenuated values due to bilinear filtering CurrentScanlinePixel.x = (float) Math.Floor( CurrentScanlinePixel.x ); CurrentScanlinePixel.y = (float) Math.Floor( CurrentScanlinePixel.y ); } for ( int Y=0; Y < H; Y++ ) { ImageUtility.float2 CurrentPixel = CurrentScanlinePixel; for ( int X=0; X < W; X++ ) { float U = CurrentPixel.x / _Source.Width; float V = CurrentPixel.y / _Source.Height; XYZ = _Source.BilinearSample( CurrentPixel.x, CurrentPixel.y ); //DEBUG // float L = XYZ.y * _Database.GetSpatialLuminanceCorrectionFactor( U, V ); // if ( L < MinLuminance_Raw ) // MinLuminance_Raw = L; // if ( L > MaxLuminance_Raw ) // MaxLuminance_Raw = L; //DEBUG xyY = ImageUtility.ColorProfile.XYZ2xyY( (ImageUtility.float3) XYZ ); xyY = _Database.CalibrateWithSpatialCorrection( U, V, xyY ); // Apply luminance calibration ShortXYZ = ImageUtility.ColorProfile.xyY2XYZ( xyY ); XYZ = new ImageUtility.float4( ShortXYZ, XYZ.w ); m_Texture.ContentXYZ[X,Y] = XYZ; // Update min/max/avg values InsertMinMax( ShortXYZ, ArrayMin, ArrayMax, EXTREME_VALUES_COUNT ); AvgXYZ += XYZ; CurrentPixel += AxisX; } CurrentScanlinePixel += AxisY; } } else { // Simple texture copy, with luminance calibration m_Texture = new ImageUtility.Bitmap( _Source.Width, _Source.Height, new ImageUtility.ColorProfile( ImageUtility.ColorProfile.STANDARD_PROFILE.sRGB ) ); ImageUtility.float4 XYZ; ImageUtility.float3 ShortXYZ; ImageUtility.float3 xyY; int W = m_Texture.Width; int H = m_Texture.Height; int X0 = 0; int X1 = W; int Y0 = 0; int Y1 = H; //DEBUG // X0 = 1088; Y0 = 764; // X1 = X0 + 1100; Y1 = Y0 + 632; for ( int Y=Y0; Y < Y1; Y++ ) { float V = (float) Y / H; for ( int X=X0; X < X1; X++ ) { float U = (float) X / W; XYZ = _Source.ContentXYZ[X,Y]; //DEBUG // float L = XYZ.y * _Database.GetSpatialLuminanceCorrectionFactor( U, V ); // if ( L < MinLuminance_Raw ) // MinLuminance_Raw = L; // if ( L > MaxLuminance_Raw ) // MaxLuminance_Raw = L; //DEBUG xyY = ImageUtility.ColorProfile.XYZ2xyY( (ImageUtility.float3) XYZ ); xyY = _Database.CalibrateWithSpatialCorrection( U, V, xyY ); // Apply luminance calibration ShortXYZ = ImageUtility.ColorProfile.xyY2XYZ( xyY ); XYZ = new ImageUtility.float4( ShortXYZ, XYZ.w ); m_Texture.ContentXYZ[X,Y] = XYZ; // Update min/max/avg values InsertMinMax( ShortXYZ, ArrayMin, ArrayMax, EXTREME_VALUES_COUNT ); AvgXYZ += XYZ; } } } // Normalize average swatch color float Normalizer = 1.0f / (m_Texture.Width*m_Texture.Height); ImageUtility.float3 avgxyY = ImageUtility.ColorProfile.XYZ2xyY( Normalizer * ((ImageUtility.float3) AvgXYZ) ); m_SwatchAvg.xyY = avgxyY; // Compute min & max using statistical norm ImageUtility.float3 BestXYZ_Min; ImageUtility.float3 BestXYZ_Max; if ( _Parms.UseModeInsteadOfMean ) { // Use mode BestXYZ_Min = ComputeMode( ArrayMin ); BestXYZ_Max = ComputeMode( ArrayMax ); } else { // Use mean BestXYZ_Min = ComputeMean( ArrayMin ); BestXYZ_Max = ComputeMean( ArrayMax ); } m_SwatchMin.xyY = ImageUtility.ColorProfile.XYZ2xyY( BestXYZ_Min ); m_SwatchMax.xyY = ImageUtility.ColorProfile.XYZ2xyY( BestXYZ_Max ); m_SwatchMin.Texture = BuildSwatch( m_SwatchWidth, m_SwatchHeight, m_SwatchMin.xyY ); m_SwatchMax.Texture = BuildSwatch( m_SwatchWidth, m_SwatchHeight, m_SwatchMax.xyY ); m_SwatchAvg.Texture = BuildSwatch( m_SwatchWidth, m_SwatchHeight, m_SwatchAvg.xyY ); // Rebuild custom swatches foreach ( CustomSwatch CS in m_CustomSwatches ) CS.Texture = BuildSwatch( m_SwatchWidth, m_SwatchHeight, CS.xyY ); ////////////////////////////////////////////////////////////////////////// // Feed some purely informational shot infos to the main texture, probably won't be saved anyway... m_Texture.HasValidShotInfo = true; m_Texture.ISOSpeed = _Parms.ISOSpeed; m_Texture.ShutterSpeed = _Parms.ShutterSpeed; m_Texture.Aperture = _Parms.Aperture; }
/// <summary> /// Prepares the interpolated calibration table to process the pixels in an image shot with the specified shot infos /// </summary> /// <param name="_ISOSpeed"></param> /// <param name="_ShutterSpeed"></param> /// <param name="_Aperture"></param> public void PrepareCalibrationFor( float _ISOSpeed, float _ShutterSpeed, float _Aperture ) { if ( m_RootNode == null ) throw new Exception( "Calibration grid hasn't been built: did you provide a valid database path? Does the path contain camera calibration data?" ); if ( IsPreparedFor( _ISOSpeed, _ShutterSpeed, _Aperture ) ) return; // Already prepared! ////////////////////////////////////////////////////////////////////////// // Find the 8 nodes encompassing our values // I'm making the delicate assumption that, although the starting node is chosen on the // condition its EV values are strictly inferior to the target we're looking for, all // neighbor nodes should satisfy the condition they're properly placed. // // This is true for the direct neighbors +X, +Y, +Z that are immediately above target values // but for example, neighbor (+X +Y) may have a very bad aperture value (Z) that may be // above the target aperture... // // Let's hope the user won't provide too fancy calibrations... // (anyway, interpolants are clamped in [0,1] so there's no risk of overshooting) // ImageUtility.float3 EV; GridNode.Convert2EV( _ISOSpeed, _ShutterSpeed, _Aperture, out EV.x, out EV.y, out EV.z ); // Find the start node GridNode StartNode = FindStartNode( EV.x, EV.y, EV.z ); m_InterpolationStartNode = StartNode; // Build the 8 grid nodes from it GridNode[,,] Grid = new GridNode[2,2,2]; Grid[0,0,0] = StartNode; Grid[1,0,0] = StartNode.m_Neighbors[0][1] != null ? StartNode.m_Neighbors[0][1] : StartNode; // +X Grid[0,1,0] = StartNode.m_Neighbors[1][1] != null ? StartNode.m_Neighbors[1][1] : StartNode; // +Y Grid[0,0,1] = StartNode.m_Neighbors[2][1] != null ? StartNode.m_Neighbors[2][1] : StartNode; // +Z Grid[1,1,0] = Grid[1,0,0].m_Neighbors[1][1] != null ? Grid[1,0,0].m_Neighbors[1][1] : Grid[1,0,0]; // +X +Y Grid[0,1,1] = Grid[0,1,0].m_Neighbors[2][1] != null ? Grid[0,1,0].m_Neighbors[2][1] : Grid[0,1,0]; // +Y +Z Grid[1,0,1] = Grid[0,0,1].m_Neighbors[0][1] != null ? Grid[0,0,1].m_Neighbors[0][1] : Grid[0,0,1]; // +X +Z Grid[1,1,1] = Grid[1,1,0].m_Neighbors[2][1] != null ? Grid[1,1,0].m_Neighbors[2][1] : Grid[1,1,0]; // +X +Y +Z ////////////////////////////////////////////////////////////////////////// // Create the successive interpolants for trilinear interpolation // // Assume we interpolate on X first (ISO speed), so we need 4 distinct values ImageUtility.float4 tX = new ImageUtility.float4( Math.Max( 0.0f, Math.Min( 1.0f, (EV.x - Grid[0,0,0].m_EV_ISOSpeed) / Math.Max( 1e-6f, Grid[1,0,0].m_EV_ISOSpeed - Grid[0,0,0].m_EV_ISOSpeed) ) ), // Y=0 Z=0 Math.Max( 0.0f, Math.Min( 1.0f, (EV.x - Grid[0,1,0].m_EV_ISOSpeed) / Math.Max( 1e-6f, Grid[1,1,0].m_EV_ISOSpeed - Grid[0,1,0].m_EV_ISOSpeed) ) ), // Y=1 Z=0 Math.Max( 0.0f, Math.Min( 1.0f, (EV.x - Grid[0,0,1].m_EV_ISOSpeed) / Math.Max( 1e-6f, Grid[1,0,1].m_EV_ISOSpeed - Grid[0,0,1].m_EV_ISOSpeed) ) ), // Y=0 Z=1 Math.Max( 0.0f, Math.Min( 1.0f, (EV.x - Grid[0,1,1].m_EV_ISOSpeed) / Math.Max( 1e-6f, Grid[1,1,1].m_EV_ISOSpeed - Grid[0,1,1].m_EV_ISOSpeed) ) ) // Y=1 Z=1 ); ImageUtility.float4 rX = new ImageUtility.float4( 1.0f - tX.x, 1.0f - tX.y, 1.0f - tX.z, 1.0f - tX.w ); // Compute the 4 interpolated shutter speeds & apertures ImageUtility.float4 ShutterSpeedsX = new ImageUtility.float4( rX.x * Grid[0,0,0].m_EV_ShutterSpeed + tX.x * Grid[1,0,0].m_EV_ShutterSpeed, // Y=0 Z=0 rX.y * Grid[0,1,0].m_EV_ShutterSpeed + tX.y * Grid[1,1,0].m_EV_ShutterSpeed, // Y=1 Z=0 rX.z * Grid[0,0,1].m_EV_ShutterSpeed + tX.z * Grid[1,0,1].m_EV_ShutterSpeed, // Y=0 Z=1 rX.w * Grid[0,1,1].m_EV_ShutterSpeed + tX.w * Grid[1,1,1].m_EV_ShutterSpeed // Y=1 Z=1 ); ImageUtility.float4 AperturesX = new ImageUtility.float4( rX.x * Grid[0,0,0].m_EV_Aperture + tX.x * Grid[1,0,0].m_EV_Aperture, // Y=0 Z=0 rX.y * Grid[0,1,0].m_EV_Aperture + tX.y * Grid[1,1,0].m_EV_Aperture, // Y=1 Z=0 rX.z * Grid[0,0,1].m_EV_Aperture + tX.z * Grid[1,0,1].m_EV_Aperture, // Y=0 Z=1 rX.w * Grid[0,1,1].m_EV_Aperture + tX.w * Grid[1,1,1].m_EV_Aperture // Y=1 Z=1 ); // Next we interpolate on Y (Shutter speed), so we need 2 distinct values ImageUtility.float2 tY = new ImageUtility.float2( Math.Max( 0.0f, Math.Min( 1.0f, (EV.y - ShutterSpeedsX.x) / Math.Max( 1e-6f, ShutterSpeedsX.y - ShutterSpeedsX.x) ) ), // Z=0 Math.Max( 0.0f, Math.Min( 1.0f, (EV.y - ShutterSpeedsX.z) / Math.Max( 1e-6f, ShutterSpeedsX.w - ShutterSpeedsX.z) ) ) // Z=1 ); ImageUtility.float2 rY = new ImageUtility.float2( 1.0f - tY.x, 1.0f - tY.y ); // Compute the 2 apertures ImageUtility.float2 AperturesY = new ImageUtility.float2( rY.x * AperturesX.x + tY.x * AperturesX.y, rY.y * AperturesX.z + tY.y * AperturesX.w ); // Finally, we interpolate on Z (Aperture), we need only 1 single value float tZ = Math.Max( 0.0f, Math.Min( 1.0f, (EV.z - AperturesY.x) / Math.Max( 1e-6f, AperturesY.y - AperturesY.x) ) ); float rZ = 1.0f - tZ; ////////////////////////////////////////////////////////////////////////// // Create the special camera calibration that is the result of the interpolation of the 8 nearest ones in the grid m_InterpolatedCalibration = new CameraCalibration(); m_InterpolatedCalibration.m_CameraShotInfos.m_ISOSpeed = _ISOSpeed; m_InterpolatedCalibration.m_CameraShotInfos.m_ShutterSpeed = _ShutterSpeed; m_InterpolatedCalibration.m_CameraShotInfos.m_Aperture = _Aperture; for ( int ProbeIndex=0; ProbeIndex < REQUIRED_PROBES_COUNT; ProbeIndex++ ) { CameraCalibration.Probe TargetProbe = m_InterpolatedCalibration.m_Reflectances[ProbeIndex]; float L000 = Grid[0,0,0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L100 = Grid[1,0,0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L010 = Grid[0,1,0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L110 = Grid[1,1,0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L001 = Grid[0,0,1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L101 = Grid[1,0,1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L011 = Grid[0,1,1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L111 = Grid[1,1,1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; // Interpolate on X (ISO speed) float L00 = rX.x * L000 + tX.x * L100; float L10 = rX.y * L010 + tX.y * L110; float L01 = rX.z * L001 + tX.z * L101; float L11 = rX.w * L011 + tX.w * L111; // Interpolate on Y (shutter speed) float L0 = rY.x * L00 + tY.x * L10; float L1 = rY.y * L01 + tY.y * L11; // Interpolate on Z (aperture) float L = rZ * L0 + tZ * L1; TargetProbe.m_IsAvailable = true; TargetProbe.m_LuminanceMeasured = L; } // Fill missing values m_InterpolatedCalibration.UpdateAllLuminances(); // Reset white reflectance reference because it was set for another setup WhiteReflectanceReference = new ImageUtility.float3( 0, 0, -1 ); }
/// <summary> /// Computes the statistical mode of the values /// </summary> /// <param name="_Values"></param> /// <returns></returns> private ImageUtility.float3 ComputeMode( ImageUtility.float3[] _Values ) { int COUNT = _Values.Length; // The idea is simply to discretize the set of values and count the ones that are the most represented float Start = _Values[0].y; float End = _Values[_Values.Length-1].y; float IntervalLength = 0.0f, Normalizer = 0.0f; if ( Math.Abs( End - Start ) > 1e-6f ) { IntervalLength = (End - Start) / COUNT; Normalizer = 1.0f / IntervalLength; } // Fill bins int[] Bins = new int[COUNT]; ImageUtility.float3[] BinsSum = new ImageUtility.float3[COUNT]; for ( int i=0; i < _Values.Length; i++ ) { int BinIndex = Math.Min( COUNT-1, (int) Math.Floor( (_Values[i].y - Start) * Normalizer ) ); Bins[BinIndex]++; BinsSum[BinIndex] += _Values[i]; } // Find the one that contains most values (yep, that's the mode!) int MaxValuesCount = 0; int MaxValuesBinIndex = -1; for ( int BinIndex=0; BinIndex < COUNT; BinIndex++ ) if ( Bins[BinIndex] > MaxValuesCount ) { // More filled up bin discovered! MaxValuesCount = Bins[BinIndex]; MaxValuesBinIndex = BinIndex; } ImageUtility.float3 ModeXYZ = (1.0f / MaxValuesCount) * BinsSum[MaxValuesBinIndex]; return ModeXYZ; }
/// <summary> /// Prepares the interpolated calibration table to process the pixels in an image shot with the specified shot infos /// </summary> /// <param name="_ISOSpeed"></param> /// <param name="_ShutterSpeed"></param> /// <param name="_Aperture"></param> public void PrepareCalibrationFor(float _ISOSpeed, float _ShutterSpeed, float _Aperture) { if (m_RootNode == null) { throw new Exception("Calibration grid hasn't been built: did you provide a valid database path? Does the path contain camera calibration data?"); } if (IsPreparedFor(_ISOSpeed, _ShutterSpeed, _Aperture)) { return; // Already prepared! } ////////////////////////////////////////////////////////////////////////// // Find the 8 nodes encompassing our values // I'm making the delicate assumption that, although the starting node is chosen on the // condition its EV values are strictly inferior to the target we're looking for, all // neighbor nodes should satisfy the condition they're properly placed. // // This is true for the direct neighbors +X, +Y, +Z that are immediately above target values // but for example, neighbor (+X +Y) may have a very bad aperture value (Z) that may be // above the target aperture... // // Let's hope the user won't provide too fancy calibrations... // (anyway, interpolants are clamped in [0,1] so there's no risk of overshooting) // ImageUtility.float3 EV; GridNode.Convert2EV(_ISOSpeed, _ShutterSpeed, _Aperture, out EV.x, out EV.y, out EV.z); // Find the start node GridNode StartNode = FindStartNode(EV.x, EV.y, EV.z); m_InterpolationStartNode = StartNode; // Build the 8 grid nodes from it GridNode[,,] Grid = new GridNode[2, 2, 2]; Grid[0, 0, 0] = StartNode; Grid[1, 0, 0] = StartNode.m_Neighbors[0][1] != null ? StartNode.m_Neighbors[0][1] : StartNode; // +X Grid[0, 1, 0] = StartNode.m_Neighbors[1][1] != null ? StartNode.m_Neighbors[1][1] : StartNode; // +Y Grid[0, 0, 1] = StartNode.m_Neighbors[2][1] != null ? StartNode.m_Neighbors[2][1] : StartNode; // +Z Grid[1, 1, 0] = Grid[1, 0, 0].m_Neighbors[1][1] != null ? Grid[1, 0, 0].m_Neighbors[1][1] : Grid[1, 0, 0]; // +X +Y Grid[0, 1, 1] = Grid[0, 1, 0].m_Neighbors[2][1] != null ? Grid[0, 1, 0].m_Neighbors[2][1] : Grid[0, 1, 0]; // +Y +Z Grid[1, 0, 1] = Grid[0, 0, 1].m_Neighbors[0][1] != null ? Grid[0, 0, 1].m_Neighbors[0][1] : Grid[0, 0, 1]; // +X +Z Grid[1, 1, 1] = Grid[1, 1, 0].m_Neighbors[2][1] != null ? Grid[1, 1, 0].m_Neighbors[2][1] : Grid[1, 1, 0]; // +X +Y +Z ////////////////////////////////////////////////////////////////////////// // Create the successive interpolants for trilinear interpolation // // Assume we interpolate on X first (ISO speed), so we need 4 distinct values ImageUtility.float4 tX = new ImageUtility.float4( Math.Max(0.0f, Math.Min(1.0f, (EV.x - Grid[0, 0, 0].m_EV_ISOSpeed) / Math.Max(1e-6f, Grid[1, 0, 0].m_EV_ISOSpeed - Grid[0, 0, 0].m_EV_ISOSpeed))), // Y=0 Z=0 Math.Max(0.0f, Math.Min(1.0f, (EV.x - Grid[0, 1, 0].m_EV_ISOSpeed) / Math.Max(1e-6f, Grid[1, 1, 0].m_EV_ISOSpeed - Grid[0, 1, 0].m_EV_ISOSpeed))), // Y=1 Z=0 Math.Max(0.0f, Math.Min(1.0f, (EV.x - Grid[0, 0, 1].m_EV_ISOSpeed) / Math.Max(1e-6f, Grid[1, 0, 1].m_EV_ISOSpeed - Grid[0, 0, 1].m_EV_ISOSpeed))), // Y=0 Z=1 Math.Max(0.0f, Math.Min(1.0f, (EV.x - Grid[0, 1, 1].m_EV_ISOSpeed) / Math.Max(1e-6f, Grid[1, 1, 1].m_EV_ISOSpeed - Grid[0, 1, 1].m_EV_ISOSpeed))) // Y=1 Z=1 ); ImageUtility.float4 rX = new ImageUtility.float4(1.0f - tX.x, 1.0f - tX.y, 1.0f - tX.z, 1.0f - tX.w); // Compute the 4 interpolated shutter speeds & apertures ImageUtility.float4 ShutterSpeedsX = new ImageUtility.float4( rX.x * Grid[0, 0, 0].m_EV_ShutterSpeed + tX.x * Grid[1, 0, 0].m_EV_ShutterSpeed, // Y=0 Z=0 rX.y * Grid[0, 1, 0].m_EV_ShutterSpeed + tX.y * Grid[1, 1, 0].m_EV_ShutterSpeed, // Y=1 Z=0 rX.z * Grid[0, 0, 1].m_EV_ShutterSpeed + tX.z * Grid[1, 0, 1].m_EV_ShutterSpeed, // Y=0 Z=1 rX.w * Grid[0, 1, 1].m_EV_ShutterSpeed + tX.w * Grid[1, 1, 1].m_EV_ShutterSpeed // Y=1 Z=1 ); ImageUtility.float4 AperturesX = new ImageUtility.float4( rX.x * Grid[0, 0, 0].m_EV_Aperture + tX.x * Grid[1, 0, 0].m_EV_Aperture, // Y=0 Z=0 rX.y * Grid[0, 1, 0].m_EV_Aperture + tX.y * Grid[1, 1, 0].m_EV_Aperture, // Y=1 Z=0 rX.z * Grid[0, 0, 1].m_EV_Aperture + tX.z * Grid[1, 0, 1].m_EV_Aperture, // Y=0 Z=1 rX.w * Grid[0, 1, 1].m_EV_Aperture + tX.w * Grid[1, 1, 1].m_EV_Aperture // Y=1 Z=1 ); // Next we interpolate on Y (Shutter speed), so we need 2 distinct values ImageUtility.float2 tY = new ImageUtility.float2( Math.Max(0.0f, Math.Min(1.0f, (EV.y - ShutterSpeedsX.x) / Math.Max(1e-6f, ShutterSpeedsX.y - ShutterSpeedsX.x))), // Z=0 Math.Max(0.0f, Math.Min(1.0f, (EV.y - ShutterSpeedsX.z) / Math.Max(1e-6f, ShutterSpeedsX.w - ShutterSpeedsX.z))) // Z=1 ); ImageUtility.float2 rY = new ImageUtility.float2(1.0f - tY.x, 1.0f - tY.y); // Compute the 2 apertures ImageUtility.float2 AperturesY = new ImageUtility.float2( rY.x * AperturesX.x + tY.x * AperturesX.y, rY.y * AperturesX.z + tY.y * AperturesX.w ); // Finally, we interpolate on Z (Aperture), we need only 1 single value float tZ = Math.Max(0.0f, Math.Min(1.0f, (EV.z - AperturesY.x) / Math.Max(1e-6f, AperturesY.y - AperturesY.x))); float rZ = 1.0f - tZ; ////////////////////////////////////////////////////////////////////////// // Create the special camera calibration that is the result of the interpolation of the 8 nearest ones in the grid m_InterpolatedCalibration = new CameraCalibration(); m_InterpolatedCalibration.m_CameraShotInfos.m_ISOSpeed = _ISOSpeed; m_InterpolatedCalibration.m_CameraShotInfos.m_ShutterSpeed = _ShutterSpeed; m_InterpolatedCalibration.m_CameraShotInfos.m_Aperture = _Aperture; for (int ProbeIndex = 0; ProbeIndex < REQUIRED_PROBES_COUNT; ProbeIndex++) { CameraCalibration.Probe TargetProbe = m_InterpolatedCalibration.m_Reflectances[ProbeIndex]; float L000 = Grid[0, 0, 0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L100 = Grid[1, 0, 0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L010 = Grid[0, 1, 0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L110 = Grid[1, 1, 0].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L001 = Grid[0, 0, 1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L101 = Grid[1, 0, 1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L011 = Grid[0, 1, 1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; float L111 = Grid[1, 1, 1].m_CameraCalibration.m_Reflectances[ProbeIndex].m_LuminanceMeasured; // Interpolate on X (ISO speed) float L00 = rX.x * L000 + tX.x * L100; float L10 = rX.y * L010 + tX.y * L110; float L01 = rX.z * L001 + tX.z * L101; float L11 = rX.w * L011 + tX.w * L111; // Interpolate on Y (shutter speed) float L0 = rY.x * L00 + tY.x * L10; float L1 = rY.y * L01 + tY.y * L11; // Interpolate on Z (aperture) float L = rZ * L0 + tZ * L1; TargetProbe.m_IsAvailable = true; TargetProbe.m_LuminanceMeasured = L; } // Fill missing values m_InterpolatedCalibration.UpdateAllLuminances(); // Reset white reflectance reference because it was set for another setup WhiteReflectanceReference = new ImageUtility.float3(0, 0, -1); }