Pobieranie wymiarów obrazu bez czytania całego pliku

104

Czy istnieje tani sposób na uzyskanie wymiarów obrazu (jpg, png, ...)? Najlepiej byłoby to osiągnąć używając tylko standardowej biblioteki klas (ze względu na ograniczenia hostingowe). Wiem, że powinno być stosunkowo łatwo odczytać nagłówek obrazu i sam go przeanalizować, ale wygląda na to, że coś takiego powinno już tam być. Sprawdziłem również, że następujący fragment kodu odczytuje cały obraz (którego nie chcę):

using System;
using System.Drawing;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Image img = new Bitmap("test.png");
            System.Console.WriteLine(img.Width + " x " + img.Height);
        }
    }
}
Jan Zich
źródło
Byłoby pomocne, gdybyś był bardziej szczegółowy w odpowiednim pytaniu. Tagi powiedziały mi .net i c #, a chcesz biblioteki standardowej, ale jakie są te ograniczenia hostingu, o których wspomniałeś?
mądry
Jeśli masz dostęp do przestrzeni nazw System.Windows.Media.Imaging (w WPF), zobacz to pytanie SO: stackoverflow.com/questions/784734/ ...
Charlie

Odpowiedzi:

106

Najlepszym rozwiązaniem jest znalezienie dobrze sprawdzonej biblioteki. Jednak powiedziałeś, że jest to trudne, więc oto jakiś podejrzany, w dużej mierze nieprzetestowany kod, który powinien działać w sporej liczbie przypadków:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;

namespace ImageDimensions
{
    public static class ImageHelper
    {
        const string errorMessage = "Could not recognize image format.";

        private static Dictionary<byte[], Func<BinaryReader, Size>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, Size>>()
        {
            { new byte[]{ 0x42, 0x4D }, DecodeBitmap},
            { new byte[]{ 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },
            { new byte[]{ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },
            { new byte[]{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
            { new byte[]{ 0xff, 0xd8 }, DecodeJfif },
        };

        /// <summary>
        /// Gets the dimensions of an image.
        /// </summary>
        /// <param name="path">The path of the image to get the dimensions of.</param>
        /// <returns>The dimensions of the specified image.</returns>
        /// <exception cref="ArgumentException">The image was of an unrecognized format.</exception>
        public static Size GetDimensions(string path)
        {
            using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)))
            {
                try
                {
                    return GetDimensions(binaryReader);
                }
                catch (ArgumentException e)
                {
                    if (e.Message.StartsWith(errorMessage))
                    {
                        throw new ArgumentException(errorMessage, "path", e);
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the dimensions of an image.
        /// </summary>
        /// <param name="path">The path of the image to get the dimensions of.</param>
        /// <returns>The dimensions of the specified image.</returns>
        /// <exception cref="ArgumentException">The image was of an unrecognized format.</exception>    
        public static Size GetDimensions(BinaryReader binaryReader)
        {
            int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;

            byte[] magicBytes = new byte[maxMagicBytesLength];

            for (int i = 0; i < maxMagicBytesLength; i += 1)
            {
                magicBytes[i] = binaryReader.ReadByte();

                foreach(var kvPair in imageFormatDecoders)
                {
                    if (magicBytes.StartsWith(kvPair.Key))
                    {
                        return kvPair.Value(binaryReader);
                    }
                }
            }

            throw new ArgumentException(errorMessage, "binaryReader");
        }

        private static bool StartsWith(this byte[] thisBytes, byte[] thatBytes)
        {
            for(int i = 0; i < thatBytes.Length; i+= 1)
            {
                if (thisBytes[i] != thatBytes[i])
                {
                    return false;
                }
            }
            return true;
        }

        private static short ReadLittleEndianInt16(this BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(short)];
            for (int i = 0; i < sizeof(short); i += 1)
            {
                bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt16(bytes, 0);
        }

        private static int ReadLittleEndianInt32(this BinaryReader binaryReader)
        {
            byte[] bytes = new byte[sizeof(int)];
            for (int i = 0; i < sizeof(int); i += 1)
            {
                bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
            }
            return BitConverter.ToInt32(bytes, 0);
        }

        private static Size DecodeBitmap(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(16);
            int width = binaryReader.ReadInt32();
            int height = binaryReader.ReadInt32();
            return new Size(width, height);
        }

        private static Size DecodeGif(BinaryReader binaryReader)
        {
            int width = binaryReader.ReadInt16();
            int height = binaryReader.ReadInt16();
            return new Size(width, height);
        }

        private static Size DecodePng(BinaryReader binaryReader)
        {
            binaryReader.ReadBytes(8);
            int width = binaryReader.ReadLittleEndianInt32();
            int height = binaryReader.ReadLittleEndianInt32();
            return new Size(width, height);
        }

        private static Size DecodeJfif(BinaryReader binaryReader)
        {
            while (binaryReader.ReadByte() == 0xff)
            {
                byte marker = binaryReader.ReadByte();
                short chunkLength = binaryReader.ReadLittleEndianInt16();

                if (marker == 0xc0)
                {
                    binaryReader.ReadByte();

                    int height = binaryReader.ReadLittleEndianInt16();
                    int width = binaryReader.ReadLittleEndianInt16();
                    return new Size(width, height);
                }

                binaryReader.ReadBytes(chunkLength - 2);
            }

            throw new ArgumentException(errorMessage);
        }
    }
}

Miejmy nadzieję, że kod jest dość oczywisty. Aby dodać nowy format pliku, dodajesz go do imageFormatDecodersklucza będącego tablicą „magicznych bitów”, które pojawiają się na początku każdego pliku o podanym formacie, a wartością jest funkcja wyodrębniająca rozmiar ze strumienia. Większość formatów jest dość prosta, jedynym prawdziwym smrodem jest jpeg.

ICR
źródło
6
Zgoda, JPEG jest do bani. Przy okazji - uwaga dla osób, które chcą używać tego kodu w przyszłości: jest to rzeczywiście niesprawdzone. Przeszedłem przez to z dobrym grzebieniem i oto co znalazłem: format BMP ma inną (starożytną) odmianę nagłówka, w której wymiary są 16-bitowe; wysokość plus może być ujemna (wtedy upuść znak). Jeśli chodzi o JPEG - 0xC0 to nie jedyny nagłówek. Zasadniczo wszystkie od 0xC0 do 0xCF z wyjątkiem 0xC4 i 0xCC są prawidłowymi nagłówkami (można je łatwo pobrać w plikach JPG z przeplotem). Aby było zabawniej, wysokość może wynosić 0 i zostać określona później w bloku 0xDC. Zobacz w3.org/Graphics/JPEG/itu-t81.pdf
Vilx-
Poprawiono powyższą metodę DecodeJfif, aby rozszerzyć oryginalny (marker == 0xC0) czek, aby akceptował również 0xC1 i 0xC2. Te inne nagłówki początku ramki SOF1 i SOF2 kodują szerokość / wysokość w tych samych pozycjach bajtów. SOF2 jest dość powszechny.
Ryan Barton
4
Standardowe ostrzeżenie: nigdy nie powinieneś pisać, throw e;ale po prostu throw;zamiast tego. Twoje komentarze do dokumentu XML dotyczące drugiego dokumentu GetDimensionsrównież wyświetlają się pathzamiastbinaryReader
Eregrith
1
Wydaje się również, że ten kod nie akceptuje plików JPEG zakodowanych w formacie EXIF ​​/ TIFF, który jest wysyłany przez wiele aparatów cyfrowych. Obsługuje tylko JFIF.
cwills
2
System.Drawing.Image.FromStream (stream, false, false) poda wymiary bez ładowania całego obrazu i działa na każdym obrazie, który można załadować .Net. Dlaczego to niechlujne i niekompletne rozwiązanie ma tak wiele głosów poparcia, jest nie do zrozumienia.
dynamichael
25
using (FileStream file = new FileStream(this.ImageFileName, FileMode.Open, FileAccess.Read))
{
    using (Image tif = Image.FromStream(stream: file, 
                                        useEmbeddedColorManagement: false,
                                        validateImageData: false))
    {
        float width = tif.PhysicalDimension.Width;
        float height = tif.PhysicalDimension.Height;
        float hresolution = tif.HorizontalResolution;
        float vresolution = tif.VerticalResolution;
     }
}

validateImageDatazestaw do falsezapobiega GDI + wykonywanie kosztownych analizy danych obrazu, co poważnie skraca czas ładowania. To pytanie rzuca więcej światła na ten temat.

Koray
źródło
1
Użyłem twojego rozwiązania jako ostatniego zasobu zmieszanego z rozwiązaniem ICR powyżej. Miałem problemy z JPEG i rozwiązałem to.
Zorkind,
2
Niedawno wypróbowałem to w projekcie, w którym musiałem zapytać o rozmiar 2000+ obrazów (głównie jpg i png, bardzo mieszane rozmiary) i było to rzeczywiście znacznie szybsze niż w tradycyjny sposób new Bitmap().
AeonOfTime
1
Najlepsza odpowiedź. Szybko, czysto i skutecznie.
dynamichael
1
Ta funkcja doskonale sprawdza się w oknach. ale nie działa na Linuksie, nadal będzie czytać cały plik na Linuksie. (.net core 2.2)
zhengchun
21

Czy próbowałeś użyć klas WPF Imaging? System.Windows.Media.Imaging.BitmapDecoderitp.?

Uważam, że podjęto pewne wysiłki, aby upewnić się, że te kodeki odczytują tylko podzbiór pliku w celu określenia informacji nagłówka. Warto to sprawdzić.

Frank Krueger
źródło
Dziękuję Ci. Wydaje się to rozsądne, ale mój hosting ma .NET 2.
Jan Zich
1
Doskonała odpowiedź. Jeśli możesz uzyskać odniesienie do PresentationCore w swoim projekcie, to jest droga do zrobienia.
ojrac
W moich testach jednostkowych te klasy nie działają lepiej niż GDI ... nadal wymagają ~ 32K do odczytu wymiarów JPEG.
Nariman
Aby uzyskać wymiary obrazu OP, w jaki sposób używasz BitmapDecoder?
Chuck Savage,
1
Zobacz to pytanie SO: stackoverflow.com/questions/784734/…
Charlie,
12

Szukałem czegoś podobnego kilka miesięcy wcześniej. Chciałem przeczytać typ, wersję, wysokość i szerokość obrazu GIF, ale nie mogłem znaleźć nic przydatnego w Internecie.

Na szczęście w przypadku GIF-a wszystkie wymagane informacje były w pierwszych 10 bajtach:

Type: Bytes 0-2
Version: Bytes 3-5
Height: Bytes 6-7
Width: Bytes 8-9

PNG są nieco bardziej złożone (szerokość i wysokość to 4 bajty):

Width: Bytes 16-19
Height: Bytes 20-23

Jak wspomniano powyżej, wotsit to dobra strona ze szczegółowymi specyfikacjami dotyczącymi formatów obrazów i danych, chociaż specyfikacje PNG w pnglib są znacznie bardziej szczegółowe. Uważam jednak, że wpis w Wikipedii dotyczący formatów PNG i GIF jest najlepszym miejscem do rozpoczęcia.

Oto mój oryginalny kod do sprawdzania GIF-ów, spasowałem też coś dla PNG:

using System;
using System.IO;
using System.Text;

public class ImageSizeTest
{
    public static void Main()
    {
        byte[] bytes = new byte[10];

        string gifFile = @"D:\Personal\Images&Pics\iProduct.gif";
        using (FileStream fs = File.OpenRead(gifFile))
        {
            fs.Read(bytes, 0, 10); // type (3 bytes), version (3 bytes), width (2 bytes), height (2 bytes)
        }
        displayGifInfo(bytes);

        string pngFile = @"D:\Personal\Images&Pics\WaveletsGamma.png";
        using (FileStream fs = File.OpenRead(pngFile))
        {
            fs.Seek(16, SeekOrigin.Begin); // jump to the 16th byte where width and height information is stored
            fs.Read(bytes, 0, 8); // width (4 bytes), height (4 bytes)
        }
        displayPngInfo(bytes);
    }

    public static void displayGifInfo(byte[] bytes)
    {
        string type = Encoding.ASCII.GetString(bytes, 0, 3);
        string version = Encoding.ASCII.GetString(bytes, 3, 3);

        int width = bytes[6] | bytes[7] << 8; // byte 6 and 7 contain the width but in network byte order so byte 7 has to be left-shifted 8 places and bit-masked to byte 6
        int height = bytes[8] | bytes[9] << 8; // same for height

        Console.WriteLine("GIF\nType: {0}\nVersion: {1}\nWidth: {2}\nHeight: {3}\n", type, version, width, height);
    }

    public static void displayPngInfo(byte[] bytes)
    {
        int width = 0, height = 0;

        for (int i = 0; i <= 3; i++)
        {
            width = bytes[i] | width << 8;
            height = bytes[i + 4] | height << 8;            
        }

        Console.WriteLine("PNG\nWidth: {0}\nHeight: {1}\n", width, height);  
    }
}
Abbas
źródło
8

Na podstawie dotychczasowych odpowiedzi i dodatkowych wyszukiwań wydaje się, że w bibliotece klas .NET 2 nie ma dla niej funkcjonalności. Postanowiłem więc napisać własną. Oto bardzo przybliżona wersja tego. W tej chwili potrzebowałem go tylko do JPG. Tak więc uzupełnia odpowiedź zamieszczoną przez Abbasa.

Nie ma sprawdzania błędów ani żadnej innej weryfikacji, ale obecnie potrzebuję go do ograniczonego zadania i ostatecznie można go łatwo dodać. Przetestowałem to na kilku obrazach i zwykle nie czyta więcej niż 6K z obrazu. Myślę, że to zależy od ilości danych EXIF.

using System;
using System.IO;

namespace Test
{

    class Program
    {

        static bool GetJpegDimension(
            string fileName,
            out int width,
            out int height)
        {

            width = height = 0;
            bool found = false;
            bool eof = false;

            FileStream stream = new FileStream(
                fileName,
                FileMode.Open,
                FileAccess.Read);

            BinaryReader reader = new BinaryReader(stream);

            while (!found || eof)
            {

                // read 0xFF and the type
                reader.ReadByte();
                byte type = reader.ReadByte();

                // get length
                int len = 0;
                switch (type)
                {
                    // start and end of the image
                    case 0xD8: 
                    case 0xD9: 
                        len = 0;
                        break;

                    // restart interval
                    case 0xDD: 
                        len = 2;
                        break;

                    // the next two bytes is the length
                    default: 
                        int lenHi = reader.ReadByte();
                        int lenLo = reader.ReadByte();
                        len = (lenHi << 8 | lenLo) - 2;
                        break;
                }

                // EOF?
                if (type == 0xD9)
                    eof = true;

                // process the data
                if (len > 0)
                {

                    // read the data
                    byte[] data = reader.ReadBytes(len);

                    // this is what we are looking for
                    if (type == 0xC0)
                    {
                        width = data[1] << 8 | data[2];
                        height = data[3] << 8 | data[4];
                        found = true;
                    }

                }

            }

            reader.Close();
            stream.Close();

            return found;

        }

        static void Main(string[] args)
        {
            foreach (string file in Directory.GetFiles(args[0]))
            {
                int w, h;
                GetJpegDimension(file, out w, out h);
                System.Console.WriteLine(file + ": " + w + " x " + h);
            }
        }

    }
}
Jan Zich
źródło
Kiedy próbuję, szerokość i wysokość są odwrócone.
Jason Sturges
@JasonSturges Być może będziesz musiał wziąć pod uwagę tag Exif Orientation.
Andrew Morton
3

Zrobiłem to dla pliku PNG

  var buff = new byte[32];
        using (var d =  File.OpenRead(file))
        {            
            d.Read(buff, 0, 32);
        }
        const int wOff = 16;
        const int hOff = 20;            
        var Widht =BitConverter.ToInt32(new[] {buff[wOff + 3], buff[wOff + 2], buff[wOff + 1], buff[wOff + 0],},0);
        var Height =BitConverter.ToInt32(new[] {buff[hOff + 3], buff[hOff + 2], buff[hOff + 1], buff[hOff + 0],},0);
Danny D.
źródło
1

Tak, absolutnie możesz to zrobić, a kod zależy od formatu pliku. Pracuję dla dostawcy obrazowania ( Atalasoft ), a nasz produkt zapewnia GetImageInfo () dla każdego kodeka, który robi minimum, aby znaleźć wymiary i inne łatwe do uzyskania dane.

Jeśli chcesz stworzyć własny, proponuję zacząć od wotsit.org , który zawiera szczegółowe specyfikacje dla prawie wszystkich formatów obrazów i zobaczysz, jak zidentyfikować plik, a także gdzie można znaleźć informacje.

Jeśli nie przeszkadza Ci praca w C, możesz skorzystać z darmowego jpeglib, aby uzyskać te informacje. Założę się, że można to zrobić z bibliotekami .NET, ale nie wiem jak.

Lou Franco
źródło
czy można bezpiecznie założyć, że używanie new AtalaImage(filepath).Widthpowoduje coś podobnego?
drzaus
1
Pierwsza (AtalaImage) czyta cały obraz - druga (GetImageInfo) odczytuje minimalne metadane, aby uzyskać elementy obiektu informacji o obrazie.
Lou Franco
0

Zaktualizowana odpowiedź ICR na obsługę progresywnych jPegów i WebP :)

internal static class ImageHelper
{
    const string errorMessage = "Could not recognise image format.";

    private static Dictionary<byte[], Func<BinaryReader, Size>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, Size>>()
    {
        { new byte[] { 0x42, 0x4D }, DecodeBitmap },
        { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },
        { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },
        { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
        { new byte[] { 0xff, 0xd8 }, DecodeJfif },
        { new byte[] { 0x52, 0x49, 0x46, 0x46 }, DecodeWebP },
    };

    /// <summary>        
    /// Gets the dimensions of an image.        
    /// </summary>        
    /// <param name="path">The path of the image to get the dimensions of.</param>        
    /// <returns>The dimensions of the specified image.</returns>        
    /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>            
    public static Size GetDimensions(BinaryReader binaryReader)
    {
        int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;
        byte[] magicBytes = new byte[maxMagicBytesLength];
        for(int i = 0; i < maxMagicBytesLength; i += 1)
        {
            magicBytes[i] = binaryReader.ReadByte();
            foreach(var kvPair in imageFormatDecoders)
            {
                if(StartsWith(magicBytes, kvPair.Key))
                {
                    return kvPair.Value(binaryReader);
                }
            }
        }

        throw new ArgumentException(errorMessage, "binaryReader");
    }

    private static bool StartsWith(byte[] thisBytes, byte[] thatBytes)
    {
        for(int i = 0; i < thatBytes.Length; i += 1)
        {
            if(thisBytes[i] != thatBytes[i])
            {
                return false;
            }
        }

        return true;
    }

    private static short ReadLittleEndianInt16(BinaryReader binaryReader)
    {
        byte[] bytes = new byte[sizeof(short)];

        for(int i = 0; i < sizeof(short); i += 1)
        {
            bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte();
        }
        return BitConverter.ToInt16(bytes, 0);
    }

    private static int ReadLittleEndianInt32(BinaryReader binaryReader)
    {
        byte[] bytes = new byte[sizeof(int)];
        for(int i = 0; i < sizeof(int); i += 1)
        {
            bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
        }
        return BitConverter.ToInt32(bytes, 0);
    }

    private static Size DecodeBitmap(BinaryReader binaryReader)
    {
        binaryReader.ReadBytes(16);
        int width = binaryReader.ReadInt32();
        int height = binaryReader.ReadInt32();
        return new Size(width, height);
    }

    private static Size DecodeGif(BinaryReader binaryReader)
    {
        int width = binaryReader.ReadInt16();
        int height = binaryReader.ReadInt16();
        return new Size(width, height);
    }

    private static Size DecodePng(BinaryReader binaryReader)
    {
        binaryReader.ReadBytes(8);
        int width = ReadLittleEndianInt32(binaryReader);
        int height = ReadLittleEndianInt32(binaryReader);
        return new Size(width, height);
    }

    private static Size DecodeJfif(BinaryReader binaryReader)
    {
        while(binaryReader.ReadByte() == 0xff)
        {
            byte marker = binaryReader.ReadByte();
            short chunkLength = ReadLittleEndianInt16(binaryReader);
            if(marker == 0xc0 || marker == 0xc2) // c2: progressive
            {
                binaryReader.ReadByte();
                int height = ReadLittleEndianInt16(binaryReader);
                int width = ReadLittleEndianInt16(binaryReader);
                return new Size(width, height);
            }

            if(chunkLength < 0)
            {
                ushort uchunkLength = (ushort)chunkLength;
                binaryReader.ReadBytes(uchunkLength - 2);
            }
            else
            {
                binaryReader.ReadBytes(chunkLength - 2);
            }
        }

        throw new ArgumentException(errorMessage);
    }

    private static Size DecodeWebP(BinaryReader binaryReader)
    {
        binaryReader.ReadUInt32(); // Size
        binaryReader.ReadBytes(15); // WEBP, VP8 + more
        binaryReader.ReadBytes(3); // SYNC

        var width = binaryReader.ReadUInt16() & 0b00_11111111111111; // 14 bits width
        var height = binaryReader.ReadUInt16() & 0b00_11111111111111; // 14 bits height

        return new Size(width, height);
    }

}
huk
źródło
-1

Będzie to zależeć od formatu pliku. Zwykle podają to we wczesnych bajtach pliku. Zwykle weźmie to pod uwagę dobra implementacja do odczytywania obrazów. Nie mogę jednak wskazać ci jednego dla .NET.

Kevin Conner
źródło