2011-09-15 16 views
5

Questo è un codice funzionale per ridurre un'immagine fino a una dimensione più piccola specificata. Ma ha diverse cose che non sono buone:Ridimensiona l'immagine JPEG alla dimensione specificata

  • è lento
  • può fare diverse iterazioni prima di ottenere l'immagine in scala
  • ogni volta che deve determinare la dimensione che deve caricare l'intera immagine in un memoryStream

Mi piacerebbe migliorarlo. Ci può essere un modo per ottenere una stima iniziale migliore che precluda così tante iterazioni? Sto sbagliando tutto questo? Le mie ragioni per crearlo è accettare qualsiasi immagine di dimensioni sconosciute e ridimensionarla a una certa dimensione. Ciò consentirà una migliore pianificazione delle esigenze di archiviazione. Quando riduci a una certa altezza/larghezza, la dimensione dell'immagine può variare troppo per le nostre esigenze.

Avrete bisogno di un riferimento 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; 
    } 

chiamata a questo creerà una seconda immagine che si avvicina per dimensioni al valore richiesto:

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

Per questo esempio specifico:

  • dimensione del file originale: 628 kB
  • dimensione file richiesta: 250 kB
  • dimensioni file ridotte: 238 kB

risposta

0

Invece di fare una serie lenta di iterazioni per ogni immagine, fare una prova con un numero immagini rappresentative e ottenere una risoluzione che vi darà la dimensione del file desiderato in media. Quindi usa sempre quella risoluzione.

+0

Ho fatto qualcosa di simile al tuo suggerimento. Funziona, ma ci sono dei momenti in cui, ad esempio, la dimensione del target era 250 kB e l'immagine in scala era di 440 kB (orig size 628 kB) perché c'erano più dettagli nell'immagine o qualcosa del genere. Questo è troppo errore. Grazie per il suggerimento. – jbobbins

1

Penso che si possa assumere una crescita lineare (e ridotta) delle dimensioni del file in base alla crescita del conteggio dei pixel. Se, ad esempio, hai un'immagine 500x500 da 200 kb e hai bisogno di un'immagine da 50 kb, dovresti ridurre le dimensioni dell'immagine a 250 x 250 (4 volte meno pixel). Credo che questo dovrebbe farti un'immagine desiderata con una sola ripetizione la maggior parte del tempo. Ma puoi migliorare ulteriormente, introducendo una percentuale di rischio (come il 10%) per ridurre il rapporto o qualcosa del genere.

0

@jbobbins: sono d'accordo con @xpda, se il primo tentativo di ridimensionare l'immagine rispetto alla dimensione di destinazione è troppo lontano dalla soglia, è possibile ripetere il passaggio ancora una volta o semplicemente tornare al precedente algoritmo inefficiente . Convergerà molto più velocemente rispetto all'attuale implementazione. L'intera operazione dovrebbe essere eseguita in O (1) anziché in O (log n), come si fa ora.

È possibile campionare alcuni rapporti di compressione JPEG e creare un tavolo dalla sperimentazione (so che non sarà perfetto, ma abbastanza vicino) che vi darà una buona approssimazione. Per esempio (taken from Wikipedia):

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

grazie Icarus. Ci proverò. – jbobbins

+0

Con le mie scarse abilità matematiche non sono sicuro da dove cominciare. Gradirei qualsiasi aiuto che potresti dare in questo lavoro matematicamente. Grazie! – jbobbins

+0

quindi ci sono 2 cose da fare, giusto? 1) ottieni il rapporto di compressione per Q80 (valore di qualità che sto usando nel mio esempio). Non sono sicuro di cosa tu stia facendo matematica per estrapolarlo. 2) (eseguendo il ciclo? Math?) In base al valore di compressione ottieni W e H che genereranno la dimensione del byte di destinazione per un bitmap a 24 bpp – jbobbins

0

La mia soluzione a questo problema è stato quello di ridurre la qualità fino a raggiungere le dimensioni desiderate. Di seguito la mia soluzione per i posteri.

NB: Questo potrebbe essere migliorato facendo una specie di congettura.

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; 
    } 
} 
} 
Problemi correlati