2011-09-15 26 views
5

Este es un código funcional para reducir una imagen a un tamaño específico más pequeño. Pero tiene varias cosas que no son buenas:Cambiar el tamaño de la imagen JPEG al tamaño especificado

  • es lento
  • que puede hacer varias iteraciones antes de obtener la imagen escalada
  • cada vez que tiene que determinar el tamaño que tiene que cargar toda la imagen en un memoryStream

Me gustaría mejorarlo. ¿Puede haber alguna forma de obtener una mejor estimación inicial para evitar tantas iteraciones? ¿Me estoy equivocando? Mis motivos para crearlo es aceptar cualquier imagen de tamaño desconocido y escalarla a un cierto tamaño. Esto permitirá una mejor planificación de las necesidades de almacenamiento. Cuando se escala a una cierta altura/ancho, el tamaño de la imagen puede variar demasiado para nuestras necesidades.

Necesitará hacer una referencia a 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; 
    } 

Al llamar a este creará una segunda imagen que está cerca en tamaño al valor solicitado:

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

Para este ejemplo específico:

  • tamaño del archivo original: 628 kB
  • Tamaño de archivo solicitado: 250 kB
  • Tamaño del archivo a escala: 238 kB

Respuesta

0

En vez de hacer un juego lento de iteraciones para cada imagen, realice una prueba con un número de imágenes representativas y conseguir una resolución que le dará el tamaño de archivo deseado en promedio . Entonces usa esa resolución todo el tiempo.

+0

Hice algo similar a su sugerencia. Funciona de cierta manera, pero luego hay ocasiones en las que, por ejemplo, el tamaño objetivo era 250 kB y la imagen escalada tenía 440 kB (tamaño original 628 kB) porque había más detalles en la imagen o algo así. Eso es demasiado error. Gracias por la sugerencia. – jbobbins

1

Creo que puede suponer un crecimiento lineal (y una reducción) del tamaño del archivo en función del crecimiento del recuento de píxeles. Es decir, si, por ejemplo, tiene una imagen 500x500 de 200 kb y necesita una imagen de 50 kb, debe reducir las dimensiones de la imagen a 250x250 (4 veces menos píxeles). Creo que esto debería obtener una imagen deseada con una iteración la mayor parte del tiempo. Pero puede ajustar esto aún más, al introducir un porcentaje de riesgo (como 10%) para reducir la proporción o algo así.

0

@jbobbins: si el primer intento de redimensionar la imagen al tamaño objetivo está demasiado lejos del umbral, puede repetir el paso una vez más o simplemente recurrir a su algoritmo ineficiente anterior . Va a converger mucho más rápido que su implementación actual. Todo debe ejecutarse en O (1) en lugar de O (log n), como lo está haciendo ahora.

Puede probar algunas relaciones de compresión JPEG y construir una tabla de experimentación (sé que no será perfecto, pero lo suficientemente cerca) que le dará una muy buena aproximación. Por ejemplo (taken from Wikipedia):

Compression Ratio   Quality 
    2.6:1     100 
     15:1      50 
     23:1      25 
     46:1      10 
+0

gracias Icarus. Voy a intentar esto. – jbobbins

+0

Con mis pobres habilidades matemáticas no estoy seguro por dónde empezar. Agradecería cualquier ayuda que pudieran dar allí para resolver esto matemáticamente. ¡Gracias! – jbobbins

+0

, de modo que hay 2 cosas que hacer, ¿no? 1) obtener la relación de compresión para Q80 (valor de calidad que estoy usando en mi ejemplo). No estoy seguro de qué matemática hagas para extrapolar eso. 2) (mediante el bucle? Matemática?) Basado en el valor de compresión, obtenga el W y el H que darán como resultado el tamaño del byte objetivo para un mapa de bits de 24 bpp – jbobbins

0

Mi solución a este problema era reducir la calidad hasta que se alcanzó el tamaño deseado. A continuación está mi solución para la posteridad.

NB: Esto podría mejorarse haciendo algún tipo de conjetura.

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; 
    } 
} 
} 
Cuestiones relacionadas