Изменить размер изображения JPEG до указанного размера - PullRequest
5 голосов
/ 15 сентября 2011

Это функциональный код для уменьшения изображения до указанного меньшего размера.Но есть несколько вещей, которые не очень хороши:

  • это медленно
  • он может сделать несколько итераций, прежде чем получить масштабированное изображение
  • каждый раз, когда ему нужно определитьразмер он должен загрузить все изображение в memoryStream

Я хотел бы улучшить его.Может ли быть какой-то способ получить лучшую начальную оценку, чтобы исключить так много итераций?Я все об этом ошибаюсь?Мои причины для его создания - принять любое изображение неизвестного размера и масштабировать его до определенного размера.Это позволит лучше планировать потребности хранения.При масштабировании до определенной высоты / ширины размер изображения может сильно отличаться для наших нужд.

Вам понадобится ссылка на System.Drawing.

    //Scale down the image till it fits the given file size.
    public static Image ScaleDownToKb(Image img, long targetKilobytes, long quality)
    {
        //DateTime start = DateTime.Now;
        //DateTime end;

        float h, w;
        float halfFactor = 100; // halves itself each iteration
        float testPerc = 100;
        var direction = -1;
        long lastSize = 0;
        var iteration = 0;
        var origH = img.Height;
        var origW = img.Width;

        // if already below target, just return the image
        var size = GetImageFileSizeBytes(img, 250000, quality);
        if (size < targetKilobytes * 1024)
        {
            //end = DateTime.Now;
            //Console.WriteLine("================ DONE.  ITERATIONS: " + iteration + " " + end.Subtract(start));
            return img;
        }

        while (true)
        {
            iteration++;

            halfFactor /= 2;
            testPerc += halfFactor * direction;

            h = origH * testPerc / 100;
            w = origW * testPerc / 100;

            var test = ScaleImage(img, (int)w, (int)h);
            size = GetImageFileSizeBytes(test, 50000, quality);

            var byteTarg = targetKilobytes * 1024;
            //Console.WriteLine(iteration + ": " + halfFactor + "% (" + testPerc + ") " + size + " " + byteTarg);

            if ((Math.Abs(byteTarg - size) / (double)byteTarg) < .1  ||  size == lastSize  ||  iteration > 15 /* safety measure */)
            {
                //end = DateTime.Now;
                //Console.WriteLine("================ DONE.  ITERATIONS: " + iteration + " " + end.Subtract(start));
                return test;
            }

            if (size > targetKilobytes * 1024)
            {
                direction = -1;
            }
            else
            {
                direction = 1;
            }

            lastSize = size;
        }
    }

    public static long GetImageFileSizeBytes(Image image, int estimatedSize, long quality)
    {
        long jpegByteSize;
        using (var ms = new MemoryStream(estimatedSize))
        {
            SaveJpeg(image, ms, quality);
            jpegByteSize = ms.Length;
        }
        return jpegByteSize;
    }

    public static void SaveJpeg(Image image, MemoryStream ms, long quality)
    {
        ((Bitmap)image).Save(ms, FindEncoder(ImageFormat.Jpeg), GetEncoderParams(quality));
    }

    public static void SaveJpeg(Image image, string filename, long quality)
    {
        ((Bitmap)image).Save(filename, FindEncoder(ImageFormat.Jpeg), GetEncoderParams(quality));
    }

    public static ImageCodecInfo FindEncoder(ImageFormat format)
    {

        if (format == null)
            throw new ArgumentNullException("format");

        foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders())
        {
            if (codec.FormatID.Equals(format.Guid))
            {
                return codec;
            }
        }

        return null;
    }

    public static EncoderParameters GetEncoderParams(long quality)
    {
        System.Drawing.Imaging.Encoder encoder = System.Drawing.Imaging.Encoder.Quality;
        //Encoder encoder = new Encoder(ImageFormat.Jpeg.Guid);
        EncoderParameters eparams = new EncoderParameters(1);
        EncoderParameter eparam = new EncoderParameter(encoder, quality);
        eparams.Param[0] = eparam;
        return eparams;
    }

    //Scale an image to a given width and height.
    public static Image ScaleImage(Image img, int outW, int outH)
    {
        Bitmap outImg = new Bitmap(outW, outH, img.PixelFormat);
        outImg.SetResolution(img.HorizontalResolution, img.VerticalResolution);
        Graphics graphics = Graphics.FromImage(outImg);
        graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
        graphics.DrawImage(img, new Rectangle(0, 0, outW, outH), new Rectangle(0, 0, img.Width, img.Height), GraphicsUnit.Pixel);
        graphics.Dispose();

        return outImg;
    }

Вызов этого будетсоздайте 2-е изображение, близкое по размеру к запрошенному значению:

        var image = Image.FromFile(@"C:\Temp\test.jpg");
        var scaled = ScaleDownToKb(image, 250, 80);
        SaveJpeg(scaled, @"C:\Temp\test_REDUCED.jpg", 80);

Для этого конкретного примера:

  • оригинальный размер файла: 628 кБ
  • запрошеноразмер файла: 250 кБ
  • масштабированный размер файла: 238 кБ

Ответы [ 4 ]

1 голос
/ 15 сентября 2011

Я думаю, вы можете предположить линейный рост (и уменьшение) размера файла в зависимости от роста количества пикселей.Это означает, что, например, если у вас есть изображение размером 500x500 и 200 КБ, и вам нужно изображение размером 50 КБ, вам следует уменьшить размеры изображения до 250x250 (в 4 раза меньше пикселей).Я полагаю, что это должно получить желаемое изображение с одной итерацией большую часть времени.Но вы можете изменить это еще больше, введя некоторый процент риска (например, 10%) для снижения или что-то в этом роде.

0 голосов
/ 21 июля 2014

Моим решением этой проблемы было снижение качества до достижения желаемого размера. Ниже мое решение для потомков.

Примечание: это можно улучшить, если сделать какие-то догадки.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;

namespace PhotoShrinker
{
    class Program
    {
    /// <summary>
    /// Max photo size in bytes
    /// </summary>
    const long MAX_PHOTO_SIZE = 409600;

    static void Main(string[] args)
    {
        var photos = Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.jpg");

        foreach (var photo in photos)
        {
            var photoName = Path.GetFileNameWithoutExtension(photo);

            var fi = new FileInfo(photo);
            Console.WriteLine("Photo: " + photo);
            Console.WriteLine(fi.Length);

            if (fi.Length > MAX_PHOTO_SIZE)
            {
                using (var stream = DownscaleImage(Image.FromFile(photo)))
                {
                    using (var file = File.Create(photoName + "-smaller.jpg"))
                    {
                        stream.CopyTo(file);
                    }
                }
                Console.WriteLine("Done.");
            }
            Console.ReadLine();
        }

    }

    private static MemoryStream DownscaleImage(Image photo)
    {
        MemoryStream resizedPhotoStream = new MemoryStream();

        long resizedSize = 0;
        var quality = 93;
        //long lastSizeDifference = 0;
        do
        {
            resizedPhotoStream.SetLength(0);

            EncoderParameters eps = new EncoderParameters(1);
            eps.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality);
            ImageCodecInfo ici = GetEncoderInfo("image/jpeg");

            photo.Save(resizedPhotoStream, ici, eps);
            resizedSize = resizedPhotoStream.Length;

            //long sizeDifference = resizedSize - MAX_PHOTO_SIZE;
            //Console.WriteLine(resizedSize + "(" + sizeDifference + " " + (lastSizeDifference - sizeDifference) + ")");
            //lastSizeDifference = sizeDifference;
            quality--;

        } while (resizedSize > MAX_PHOTO_SIZE);

        resizedPhotoStream.Seek(0, SeekOrigin.Begin);

        return resizedPhotoStream;
    }

    private static ImageCodecInfo GetEncoderInfo(String mimeType)
    {
        int j;
        ImageCodecInfo[] encoders;
        encoders = ImageCodecInfo.GetImageEncoders();
        for (j = 0; j < encoders.Length; ++j)
        {
            if (encoders[j].MimeType == mimeType)
                return encoders[j];
        }
        return null;
    }
}
}
0 голосов
/ 15 сентября 2011

@ jbobbins: Я согласен с @xpda: если первая попытка изменить размер изображения до целевого размера слишком далеко от порога, вы можете повторить шаг еще раз или просто вернуться к своему предыдущему неэффективному алгоритму. Он будет сходиться намного быстрее, чем ваша текущая реализация. Все это должно быть выполнено в O (1) вместо O (log n), как вы делаете сейчас.

Вы можете сэмплировать некоторые коэффициенты сжатия JPEG и построить экспериментальную таблицу (я знаю, что она не будет идеальной, но достаточно близкой), которая даст вам очень хорошее приближение. Например ( взято из Википедии ):

Compression Ratio            Quality
     2.6:1                    100
      15:1                     50
      23:1                     25
      46:1                     10
0 голосов
/ 15 сентября 2011

Вместо медленного набора итераций для каждого изображения проведите тест с числом представительных изображений и получите разрешение, которое даст вам желаемый размер файла в среднем . Затем используйте это разрешение все время.

...