private static DataRectangle <double> Blur(DataRectangle <double> values, int maxRadius)
 {
     return(values.Transform((value, point) =>
     {
         var valuesInArea = new List <double>();
         for (var x = -maxRadius; x <= maxRadius; x++)
         {
             for (var y = -maxRadius; y <= maxRadius; y++)
             {
                 var newPoint = new Point(point.X + x, point.Y + y);
                 if ((newPoint.X < 0) || (newPoint.Y < 0) ||
                     (newPoint.X >= values.Width) || (newPoint.Y >= values.Height))
                 {
                     continue;
                 }
                 valuesInArea.Add(values[newPoint.X, newPoint.Y]);
             }
         }
         return valuesInArea.Average();
     }));
 }
        private static IEnumerable <int> GetBarLengthsFromBarcodeSlice(DataRectangle <bool> barcodeDetails, int sliceY)
        {
            if ((sliceY < 0) || (sliceY >= barcodeDetails.Height))
            {
                throw new ArgumentOutOfRangeException(nameof(sliceY));
            }

            // Take the horizontal slice of the data
            var values = new List <bool>();

            for (var x = 0; x < barcodeDetails.Width; x++)
            {
                values.Add(barcodeDetails[x, sliceY]);
            }

            // Split the slice into bars - we only care about how long each segment is when they
            // alternate, not whether they're dark bars or light bars
            var segments = new List <Tuple <bool, int> >();

            foreach (var value in values)
            {
                if ((segments.Count == 0) || (segments[^ 1].Item1 != value))
        /// <summary>
        /// This will return values in the range 0-255 (inclusive)
        /// </summary>
        // Based on http://stackoverflow.com/a/4748383/3813189
        public static DataRectangle <double> GetGreyscale(this Bitmap image)
        {
            var values = new double[image.Width, image.Height];
            var data   = image.LockBits(
                new Rectangle(0, 0, image.Width, image.Height),
                ImageLockMode.ReadOnly,
                PixelFormat.Format24bppRgb
                );

            try
            {
                var pixelData = new Byte[data.Stride];
                for (var lineIndex = 0; lineIndex < data.Height; lineIndex++)
                {
                    Marshal.Copy(
                        source: data.Scan0 + (lineIndex * data.Stride),
                        destination: pixelData,
                        startIndex: 0,
                        length: data.Stride
                        );
                    for (var pixelOffset = 0; pixelOffset < data.Width; pixelOffset++)
                    {
                        // Note: PixelFormat.Format24bppRgb means the data is stored in memory as BGR
                        const int PixelWidth = 3;
                        var       r          = pixelData[pixelOffset * PixelWidth + 2];
                        var       g          = pixelData[pixelOffset * PixelWidth + 1];
                        var       b          = pixelData[pixelOffset * PixelWidth];
                        values[pixelOffset, lineIndex] = (0.2989 * r) + (0.5870 * g) + (0.1140 * b);
                    }
                }
            }
            finally
            {
                image.UnlockBits(data);
            }
            return(DataRectangle.For(values));
        }
        private static string?TryToReadBarcodeValueFromSingleLine(DataRectangle <bool> barcodeDetails, int sliceY)
        {
            if ((sliceY < 0) || (sliceY >= barcodeDetails.Height))
            {
                throw new ArgumentOutOfRangeException(nameof(sliceY));
            }

            var lengths = GetBarLengthsFromBarcodeSlice(barcodeDetails, sliceY).ToArray();

            if (lengths.Length < 57)
            {
                // As explained, we'd like 60 bars (which would include the final guard region) but we
                // can still make an attempt with 57 (but no fewer)
                // - There will often be another section of blank content after the barcode that we ignore
                // - If we don't want to validate the final guard region then we can work with a barcode
                //   image where some of the end is cut off, so long as the data for the 12 digits is
                //   there (this will be the case where there are only 57 lengths)
                return(null);
            }

            var offset = 0;
            var extractedNumericValues = new List <int>();

            for (var i = 0; i < 14; i++)
            {
                if (i == 0)
                {
                    // This should be the first guard region and it should be a pattern of three single-
                    // width bars
                    offset += 3;
                }
                else if (i == 7)
                {
                    // This should be the guard region in the middle of the barcode and it should be a
                    // pattern of five single-width bars
                    offset += 5;
                }
                else
                {
                    var value = TryToGetValueForLengths(
                        lengths[offset],
                        lengths[offset + 1],
                        lengths[offset + 2],
                        lengths[offset + 3]
                        );
                    if (value is null)
                    {
                        return(null);
                    }
                    extractedNumericValues.Add(value.Value);
                    offset += 4;
                }
            }

            // Calculate what the checksum should be based upon the first 11 numbers and ensure that
            // the 12th matches it
            if (extractedNumericValues.Last() != CalculateChecksum(extractedNumericValues.Take(11)))
            {
                return(null);
            }

            return(string.Join("", extractedNumericValues));
        }