Exemple #1
0
        static public FloatImage FromByteImage(ByteImage inputImage)
        {
            byte[]      inputImageData = inputImage.imageData;
            int         width = inputImage.width, height = inputImage.height, stride = inputImage.stride;
            PixelFormat pixelFormat = inputImage.pixelFormat;

            float[] output     = new float[inputImageData.Length];
            int     strideHere = 0;
            int     xX4;
            int     offsetHere;

            for (int y = 0; y < height; y++)
            {
                strideHere = stride * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    xX4        = x * 4;
                    offsetHere = strideHere + xX4;

                    output[offsetHere]     = (float)inputImageData[offsetHere];
                    output[offsetHere + 1] = (float)inputImageData[offsetHere + 1];
                    output[offsetHere + 2] = (float)inputImageData[offsetHere + 2];
                    output[offsetHere + 3] = (float)inputImageData[offsetHere + 3];
                }
            }

            return(new FloatImage(output, stride, width, height, pixelFormat));
        }
        public Vector3Image(ByteImage inputImage)
        {
            byte[] inputImageData = inputImage.imageData;
            width          = inputImage.width;
            height         = inputImage.height;
            originalStride = inputImage.stride;
            PixelFormat pixelFormat = inputImage.pixelFormat;

            imageData = new Vector3[width * height];
            int strideHere = 0;
            int pixelOffset;
            int offsetHere;

            int pixelMultiplier = 3;

            if (inputImage.pixelFormat == PixelFormat.Format32bppArgb)
            {
                pixelMultiplier = 4;
            }

            for (int y = 0; y < height; y++)
            {
                strideHere = originalStride * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    pixelOffset = x * pixelMultiplier;
                    offsetHere  = strideHere + pixelOffset;

                    imageData[y * width + x].X = inputImageData[offsetHere];
                    imageData[y * width + x].Y = inputImageData[offsetHere + 1];
                    imageData[y * width + x].Z = inputImageData[offsetHere + 2];
                }
            }
        }
        // Very simple and inadequate greyscale conversion, but it'll do
        static public ByteImage ToGreyscale(ByteImage inputImage, bool alpha100 = true)
        {
            byte[] inputImageData = inputImage.imageData;
            int    width          = inputImage.width;
            int    height         = inputImage.height;
            int    stride         = inputImage.stride;

            byte[] output     = new byte[inputImageData.Length];
            int    strideHere = 0;
            int    xX4;
            int    offsetHere;

            byte tmpLuma;

            for (int y = 0; y < height; y++)
            {
                strideHere = stride * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    xX4        = x * 4;
                    offsetHere = strideHere + xX4;

                    tmpLuma = (byte)(inputImageData[offsetHere] * 0.11 + 0.59 * inputImageData[offsetHere + 1] + 0.3 * inputImageData[offsetHere + 2]);

                    output[offsetHere]     = tmpLuma;
                    output[offsetHere + 1] = tmpLuma;
                    output[offsetHere + 2] = tmpLuma;
                    output[offsetHere + 3] = alpha100 ? (byte)255 : inputImageData[offsetHere + 3];
                }
            }

            return(new ByteImage(output, stride, width, height, inputImage.pixelFormat));
        }
        public static Bitmap ByteArrayToBitmap(ByteImage byteImage)
        {
            Bitmap    myBitmap = new Bitmap(byteImage.width, byteImage.height, byteImage.pixelFormat);
            Rectangle rect     = new Rectangle(0, 0, myBitmap.Width, myBitmap.Height);

            System.Drawing.Imaging.BitmapData bmpData =
                myBitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
                                  myBitmap.PixelFormat);

            bmpData.Stride = byteImage.stride;

            IntPtr ptr = bmpData.Scan0;

            System.Runtime.InteropServices.Marshal.Copy(byteImage.imageData, 0, ptr, byteImage.imageData.Length);

            myBitmap.UnlockBits(bmpData);
            return(myBitmap);
        }
Exemple #5
0
        private void drawStats(int maxIndex)
        {
            int imageWidth  = (int)stats_img_container.ActualWidth;
            int imageHeight = (int)stats_img_container.ActualHeight;

            if (imageWidth < 5 || imageHeight < 5)
            {
                return;                                    // avoid crashes and shit
            }
            // We flip imageHeight and imageWidth because it's more efficient to work on rows than on columns. We later rotate the image into the proper position
            ByteImage statsImage = Helpers.BitmapToByteArray(new Bitmap(imageHeight, imageWidth, System.Drawing.Imaging.PixelFormat.Format24bppRgb));

            int stride = statsImage.stride;
            int strideHere;

            double verticalScaleFactorFps = highestFPS == 0 ? 0: (imageHeight - 1) / highestFPS;
            double verticalScaleFactorSmallestDifference = highestSmallestDifference == 0 ? 1: (imageHeight - 1) / highestSmallestDifference;

            double indiziPerPixel          = (double)maxIndex / (double)imageWidth;
            int    indiziPerPixelRounded   = (int)Math.Ceiling(indiziPerPixel);
            int    pixelValueAddedPerValue = (int)Math.Max(1, Math.Ceiling(255.0 / indiziPerPixel));

            float[] currentColumnValuesFps = new float[indiziPerPixelRounded];
            float[] currentColumnValuesSmallestDifference = new float[indiziPerPixelRounded];

            double averageFpsForColumnCounter;
            double averageSmallestDifferenceForColumnCounter;

            int    rangeStart = 0;
            int    yPosition;
            double yPositionUnrounded;

            // Remember, x/y in the actual Bitmap data are flipped! Hence having the outer for be X makes more sense
            for (int x = 0; x < imageWidth; x++)
            {
                strideHere = x * stride;
                rangeStart = (int)Math.Floor((indiziPerPixel * (double)x));

                Array.Copy(fps, rangeStart, currentColumnValuesFps, 0, Math.Min(indiziPerPixelRounded, fps.Length - rangeStart));
                Array.Sort(currentColumnValuesFps);
                Array.Copy(smallestDifferences, rangeStart, currentColumnValuesSmallestDifference, 0, Math.Min(indiziPerPixelRounded, smallestDifferences.Length - rangeStart));
                Array.Sort(currentColumnValuesSmallestDifference);

                averageFpsForColumnCounter = 0;
                averageSmallestDifferenceForColumnCounter = 0;
                for (int i = 0; i < indiziPerPixelRounded; i++) // fps
                {
                    yPositionUnrounded          = currentColumnValuesFps[i] * verticalScaleFactorFps;
                    yPosition                   = (int)yPositionUnrounded;
                    averageFpsForColumnCounter += yPositionUnrounded;
                    statsImage.imageData[strideHere + yPosition * 3 + 2] = statsImage.imageData[strideHere + yPosition * 3 + 2] == 255 ? (byte)255 : (byte)Math.Min(255, statsImage.imageData[strideHere + yPosition * 3 + 2] + (byte)pixelValueAddedPerValue);
                }
                yPosition = averageFpsForColumnCounter == 0 ? 0: (int)(averageFpsForColumnCounter / (double)indiziPerPixelRounded);
                statsImage.imageData[strideHere + yPosition * 3 + 2] = 255;
                for (int i = 0; i < indiziPerPixelRounded; i++) // Smallest differences
                {
                    yPositionUnrounded = currentColumnValuesSmallestDifference[i] * verticalScaleFactorSmallestDifference;
                    yPosition          = (int)yPositionUnrounded;
                    averageSmallestDifferenceForColumnCounter           += yPositionUnrounded;
                    statsImage.imageData[strideHere + yPosition * 3 + 1] = statsImage.imageData[strideHere + yPosition * 3 + 1] == 255 ? (byte)255 : (byte)Math.Min(255, statsImage.imageData[strideHere + yPosition * 3 + 1] + (byte)pixelValueAddedPerValue);
                }
                yPosition = averageSmallestDifferenceForColumnCounter == 0 ? 0 : (int)(averageSmallestDifferenceForColumnCounter / (double)indiziPerPixelRounded);
                statsImage.imageData[strideHere + yPosition * 3 + 1] = 255;
                for (int y = 0; y < imageHeight; y++) // Apply gamma to make it more visible
                {
                    statsImage.imageData[strideHere + y * 3 + 1] = (byte)Math.Min(255, 255 * Math.Pow(((double)statsImage.imageData[strideHere + y * 3 + 1] / 255), 1.0 / 3.0));
                    statsImage.imageData[strideHere + y * 3 + 2] = (byte)Math.Min(255, 255 * Math.Pow(((double)statsImage.imageData[strideHere + y * 3 + 2] / 255), 1.0 / 3.0));
                }
            }
            Bitmap statsImageBitmap = Helpers.ByteArrayToBitmap(statsImage);

            statsImageBitmap.RotateFlip(RotateFlipType.Rotate270FlipNone);

            int padding = 2;

            if (imageWidth > 200 && imageHeight > 100)
            {
                Graphics g = Graphics.FromImage(statsImageBitmap);
                g.SmoothingMode     = SmoothingMode.AntiAlias;
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.PixelOffsetMode   = PixelOffsetMode.HighQuality;
                g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
                string fpsString = "Max FPS: " + highestFPS;
                string lowestDifferenceString = "Biggest difference: " + highestSmallestDifference;

                SizeF fpsStringSize = g.MeasureString(fpsString, new Font("Tahoma", 8));
                SizeF lowestDifferenceStringSize = g.MeasureString(lowestDifferenceString, new Font("Tahoma", 8));
                if ((fpsStringSize.Width + padding) < imageWidth && (fpsStringSize.Height + padding) < imageHeight)
                {
                    RectangleF rectf = new RectangleF(padding, padding, fpsStringSize.Width, fpsStringSize.Height);
                    g.DrawString(fpsString, new Font("Tahoma", 8), System.Drawing.Brushes.Red, rectf);
                }
                if ((lowestDifferenceStringSize.Width + padding) < imageWidth && (fpsStringSize.Height + padding * 2 + lowestDifferenceStringSize.Height) < imageHeight)
                {
                    RectangleF rectf = new RectangleF(padding, lowestDifferenceStringSize.Height + padding * 2, lowestDifferenceStringSize.Width, lowestDifferenceStringSize.Height);
                    g.DrawString(lowestDifferenceString, new Font("Tahoma", 8), System.Drawing.Brushes.Green, rectf);
                }

                g.Flush();
            }


            stats_img.Source = Helpers.BitmapToImageSource(statsImageBitmap);
        }
        // Simple 1D Greyscale regrade of one image to another. Basically just to match brightness, that's all.
        static public FloatImage Regrade1DSimpleLUT(ByteImage testImage, ByteImage referenceImage)
        {
            byte[]  testImageData      = testImage.imageData;
            byte[]  referenceImageData = referenceImage.imageData;
            float[] output             = new float[testImage.Length];
            // Build histogram
            AverageData1D[] FactorLUT = new AverageData1D[256];

            int width      = testImage.width;
            int height     = testImage.height;
            int strideTest = testImage.stride;
            int strideRef  = referenceImage.stride;

            int strideHereTest, strideHereRef = 0;
            int xX4;
            int offsetHereTest, offsetHereRef;

            float testLuma, refLuma;
            float hereFactor;

            for (int y = 0; y < height; y++)
            {
                strideHereTest = strideTest * y;
                strideHereRef  = strideRef * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    xX4            = x * 4;
                    offsetHereTest = strideHereTest + xX4;
                    offsetHereRef  = strideHereRef + xX4;

                    testLuma = testImageData[offsetHereTest] * 0.11f + 0.59f * testImageData[offsetHereTest + 1] + 0.3f * testImageData[offsetHereTest + 2];
                    refLuma  = referenceImageData[offsetHereRef] * 0.11f + 0.59f * referenceImageData[offsetHereRef + 1] + 0.3f * referenceImageData[offsetHereRef + 2];

                    FactorLUT[(int)testLuma].color += refLuma / testLuma;
                    FactorLUT[(int)testLuma].divisor++;


                    /*output[offsetHereTest] = tmpLuma;
                     * output[offsetHereTest + 1] = tmpLuma;
                     * output[offsetHereTest + 2] = tmpLuma;
                     * output[offsetHereTest + 3] = testImageData[offsetHere + 3];*/
                }
            }

            // Evaluate histogram
            // No interpolation bc it only has to apply to this one image.
            for (int i = 0; i < 256; i++)
            {
                if (FactorLUT[i].divisor != 0)
                {
                    FactorLUT[i].color = FactorLUT[i].color / FactorLUT[i].divisor;
                }
            }

            // Apply histogram
            for (int y = 0; y < height; y++)
            {
                strideHereTest = strideTest * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    xX4            = x * 4;
                    offsetHereTest = strideHereTest + xX4;

                    testLuma = testImageData[offsetHereTest] * 0.11f + 0.59f * testImageData[offsetHereTest + 1] + 0.3f * testImageData[offsetHereTest + 2];


                    hereFactor = FactorLUT[(int)testLuma].color;

                    output[offsetHereTest]     = testImageData[offsetHereTest] * hereFactor;
                    output[offsetHereTest + 1] = testImageData[offsetHereTest + 1] * hereFactor;
                    output[offsetHereTest + 2] = testImageData[offsetHereTest + 2] * hereFactor;
                    output[offsetHereTest + 3] = testImageData[offsetHereTest + 3];
                }
            }

            return(new FloatImage(output, strideTest, width, height, testImage.pixelFormat));
        }
        // Simple 1D Greyscale regrade of one image to another. Basically just to match brightness, that's all.
        static public FloatImage Regrade1DHistogram(ByteImage testImage, ByteImage referenceImage, int percentileSubdivisions = 100, int smoothradius = 20, float smoothIntensity = 1f)
        {
            byte[]  testImageData      = testImage.imageData;
            byte[]  referenceImageData = referenceImage.imageData;
            float[] output             = new float[testImage.Length];

            // Work in 16 bit for more precision in the percentile thing.
            int sixteenBitCount = 256 * 256;
            int sixteenBitMax   = sixteenBitCount - 1;

            // Build histogram
            int[] histogramTest = new int[sixteenBitCount];
            int[] histogramRef  = new int[sixteenBitCount];

            int width      = testImage.width;
            int height     = testImage.height;
            int strideTest = testImage.stride;
            int strideRef  = referenceImage.stride;

            int strideHereTest, strideHereRef = 0;
            int xX4;
            int offsetHereTest, offsetHereRef;

            int   testLuma, refLuma;
            float hereFactor;

            // 1. BUILD HISTOGRAMS
            for (int y = 0; y < height; y++)
            {
                strideHereTest = strideTest * y;
                strideHereRef  = strideRef * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    xX4            = x * 4;
                    offsetHereTest = strideHereTest + xX4;
                    offsetHereRef  = strideHereRef + xX4;

                    // Add 1 to avoid division by zero issues, subtract later again.
                    testLuma = 1 + (int)Math.Max(0, Math.Min(sixteenBitMax, (((int)testImageData[offsetHereTest] << 8) * 0.11f + 0.59f * ((int)testImageData[offsetHereTest + 1] << 8) + 0.3f * ((int)testImageData[offsetHereTest + 2] << 8))));
                    refLuma  = 1 + (int)Math.Max(0, Math.Min(sixteenBitMax, (((int)referenceImageData[offsetHereRef] << 8) * 0.11f + 0.59f * ((int)referenceImageData[offsetHereRef + 1] << 8) + 0.3f * ((int)referenceImageData[offsetHereRef + 2] << 8))));

                    histogramTest[testLuma]++;
                    histogramRef[refLuma]++;

                    /*output[offsetHereTest] = tmpLuma;
                     * output[offsetHereTest + 1] = tmpLuma;
                     * output[offsetHereTest + 2] = tmpLuma;
                     * output[offsetHereTest + 3] = testImageData[offsetHere + 3];*/
                }
            }

            // Info: The subdivision count is the amount of "boxes" I divide the brightness spectrum into. But these arrays do not represent the boxes, but rather the splits between the boxes, so it's -1. The splits define exactly WHERE the boxes end.
            FloatIssetable[] percentilesTest = new FloatIssetable[percentileSubdivisions - 1];
            FloatIssetable[] percentilesRef  = new FloatIssetable[percentileSubdivisions - 1];

            float onePercentile = width * height / percentileSubdivisions; // This is a bit messy I guess but it should work out.


            // 2. BUILD PERCENTILE ARRAYS
            // Fill percentile-arrays, basically saying at which brightness a certain percentile starts, by the amount of pixels that have a certain brightness
            int   countTest = 0;
            int   currentPercentileTest = 0, lastPercentileTest = 0;
            float lastTestPercentileJumpValue = 0;
            int   countRef = 0;
            int   currentPercentileRef = 0, lastPercentileRef = 0;
            float lastRefPercentileJumpValue = 0;

            for (int i = 0; i < sixteenBitCount; i++)
            {
                countTest            += histogramTest[i];
                currentPercentileTest = (int)Math.Min(percentileSubdivisions - 1, Math.Floor((double)countTest / onePercentile));

                if (currentPercentileTest != lastPercentileTest)
                {
                    percentilesTest[currentPercentileTest - 1].value = i;
                    percentilesTest[currentPercentileTest - 1].isSet = true;

                    // Fill holes, if there are any.
                    for (int a = lastPercentileTest + 1; a < currentPercentileTest; a++)
                    {
                        percentilesTest[a - 1].value = lastTestPercentileJumpValue + (i - lastTestPercentileJumpValue) * ((a - lastPercentileTest) / ((float)currentPercentileTest - lastPercentileTest));
                        percentilesTest[a - 1].isSet = true;
                    }
                    lastTestPercentileJumpValue = i;
                }

                lastPercentileTest = currentPercentileTest;

                countRef            += histogramRef[i];
                currentPercentileRef = (int)Math.Min(percentileSubdivisions - 1, Math.Floor((double)countRef / onePercentile));

                if (currentPercentileRef != lastPercentileRef)
                {
                    percentilesRef[currentPercentileRef - 1].value = i;
                    percentilesRef[currentPercentileRef - 1].isSet = true;

                    // Fill holes, if there are any.
                    for (int a = lastPercentileRef + 1; a < currentPercentileRef; a++)
                    {
                        //percentilesRef[a - 1].value = lastRefPercentileJumpValue + (i - lastRefPercentileJumpValue) * (currentPercentileRef - lastPercentileRef) / a;
                        percentilesRef[a - 1].value = lastRefPercentileJumpValue + (i - lastRefPercentileJumpValue) * ((a - currentPercentileRef) / ((float)currentPercentileRef - lastPercentileRef));
                        percentilesRef[a - 1].isSet = true;
                    }
                    lastRefPercentileJumpValue = i;
                }

                lastPercentileRef = currentPercentileRef;
            }



            // 3. BUILD LUT FROM PERCENTILE ARRAYS
            FloatIssetable[] factorsLUT = new FloatIssetable[256];
            for (int i = 0; i < percentilesTest.Length; i++)
            {
                factorsLUT[(int)Math.Floor(percentilesTest[i].value / 256)].value = percentilesTest[i].value == 0 ? 0 : percentilesRef[i].value / percentilesTest[i].value;
                factorsLUT[(int)Math.Floor(percentilesTest[i].value / 256)].isSet = true;
            }

            // 4. FILL HOLES IN LUT
            bool  lastExists = false, nextExists = false;
            int   lastExisting = 0, nextExisting = 0;
            float linearInterpolationFactor = 0;

            for (int i = 0; i < 256; i++)
            {
                if (factorsLUT[i].isSet == false)
                {
                    // Find next set value, if it exisst
                    nextExists = false;
                    for (int a = i + 1; a < 256; a++)
                    {
                        if (factorsLUT[a].isSet)
                        {
                            nextExists   = true;
                            nextExisting = a;
                            break;
                        }
                    }

                    if (nextExists && lastExists)
                    {
                        linearInterpolationFactor = ((float)i - lastExisting) / (nextExisting - lastExisting);
                        factorsLUT[i].value       = factorsLUT[lastExisting].value + linearInterpolationFactor * (factorsLUT[nextExisting].value - factorsLUT[lastExisting].value);
                        factorsLUT[i].isSet       = true;

                        lastExists   = true;
                        lastExisting = i;
                    }
                    else if (lastExists)
                    {
                        factorsLUT[i].value = factorsLUT[lastExisting].value;
                        factorsLUT[i].isSet = true;

                        lastExists   = true;
                        lastExisting = i;
                    }
                    else if (nextExists)
                    {
                        factorsLUT[i].value = factorsLUT[nextExisting].value;
                        factorsLUT[i].isSet = true;

                        lastExists   = true;
                        lastExisting = i;
                    }
                    else if (!nextExists && !lastExists)
                    {
                        // Kinda impossible but ok
                        // I think we're kinda fucced then. Let's just assume this never happens
                    }
                }
                else
                {
                    // Do nothing, all good
                    lastExists   = true;
                    lastExisting = i;
                }
            }


            // 5. SMOOTH LUT
            // Simple 1D "Box" blur. So, a line blur?
            // TODO find a way to protect blacks
            if (smoothIntensity > 0)
            {
                float[] elevation              = new float[factorsLUT.Length - 1];
                float[] elevationSmoothed      = new float[factorsLUT.Length - 1];
                float   totalElevation         = 0;
                float   totalElevationSmoothed = 0;
                float   blackPoint             = float.PositiveInfinity;
                float   whitePoint             = 0;
                float   startPoint             = factorsLUT[0].value;


                if (factorsLUT[0].value > whitePoint)
                {
                    whitePoint = factorsLUT[0].value;
                }

                if (factorsLUT[0].value < blackPoint)
                {
                    blackPoint = factorsLUT[0].value;
                }


                float blackPointSmoothed = blackPoint;
                float whitePointSmoothed = whitePoint;

                // We are not smoothing the raw data but the rise.
                for (int i = 0; i < elevation.Length; i++)
                {
                    elevation[i]    = factorsLUT[i + 1].value - factorsLUT[i].value;
                    totalElevation += elevation[i];
                    if (factorsLUT[i + 1].value > whitePoint)
                    {
                        whitePoint = factorsLUT[i + 1].value;
                    }

                    if (factorsLUT[i + 1].value < blackPoint)
                    {
                        blackPoint = factorsLUT[i + 1].value;
                    }
                }

                double averageValue            = 0;
                double averageDivisor          = 0;
                float  invertedSmoothIntensity = 1 - smoothIntensity;

                float currentRealValueWithSmoothing = factorsLUT[0].value;

                FloatIssetable[] factorsLUTSmoothed = new FloatIssetable[factorsLUT.Length];
                for (int i = 0; i < 255; i++)
                {
                    if (i == 0)
                    {
                        // let's say radius 5
                        // goes up to 4, so: 0 1 2 3 4 5
                        for (int a = 0; a <= smoothradius; a++)
                        {
                            averageValue   += elevation[a];
                            averageDivisor += 1;
                        }
                    }
                    else
                    {
                        // i == 5
                        // 0 1 2 3 4 5
                        if (i <= smoothradius)
                        {
                            averageValue += elevation[0];
                            averageDivisor++;
                        }
                        else
                        {
                            // i == 6 : 0 1 2 3 4 5 6
                            // i-5 = 1. i-5-1 = 0
                            averageValue -= elevation[i - (smoothradius - 1)];
                            averageDivisor--;
                        }
                        // i + 5 = 254 : i == 249: 249 250 251 252 253 254
                        if (i + smoothradius < 255)
                        {
                            averageValue += elevation[i + smoothradius];
                            averageDivisor++;
                        }
                        else
                        {
                            averageValue -= elevation[254];
                            averageDivisor--;
                        }
                    }

                    elevationSmoothed[i]           = (float)(invertedSmoothIntensity * elevation[i] + smoothIntensity * (averageValue / averageDivisor));
                    totalElevationSmoothed        += elevationSmoothed[i];
                    currentRealValueWithSmoothing += elevationSmoothed[i];
                    if (currentRealValueWithSmoothing > whitePointSmoothed)
                    {
                        whitePointSmoothed = currentRealValueWithSmoothing;
                    }

                    if (currentRealValueWithSmoothing < blackPointSmoothed)
                    {
                        blackPointSmoothed = currentRealValueWithSmoothing;
                    }
                }

                // Need a last pass to make sure blacks and whites are still the same, thus:
                float scaleFactor = (whitePointSmoothed - blackPointSmoothed) / (whitePoint - blackPoint);
                for (int i = 0; i < 255; i++)
                {
                    elevationSmoothed[i] /= scaleFactor;
                }

                // Transfer back to FactorsLUT
                for (int i = 0; i < 255; i++)
                {
                    factorsLUT[i + 1].value = factorsLUT[i].value + elevationSmoothed[i];
                }
            }

            // 6. APPLY LUT
            for (int y = 0; y < height; y++)
            {
                strideHereTest = strideTest * y;
                for (int x = 0; x < width; x++) // 4 bc RGBA
                {
                    xX4            = x * 4;
                    offsetHereTest = strideHereTest + xX4;

                    testLuma = (int)Math.Max(0, Math.Min(sixteenBitMax, (((float)testImageData[offsetHereTest]) * 0.11f + 0.59f * ((float)testImageData[offsetHereTest + 1]) + 0.3f * ((float)testImageData[offsetHereTest + 2]))));

                    hereFactor = factorsLUT[(int)testLuma].value;

                    output[offsetHereTest]     = (float)testImageData[offsetHereTest] * hereFactor;
                    output[offsetHereTest + 1] = (float)testImageData[offsetHereTest + 1] * hereFactor;
                    output[offsetHereTest + 2] = (float)testImageData[offsetHereTest + 2] * hereFactor;
                    output[offsetHereTest + 3] = (float)testImageData[offsetHereTest + 3];
                }
            }


            return(new FloatImage(output, strideTest, width, height, testImage.pixelFormat));
        }