Accueil > WPF > [WPF] : Fast thumbnails image generation (+ rotation)

[WPF] : Fast thumbnails image generation (+ rotation)

Si vous manipulez des images dans une application WPF vous avez déjà du être confrontés à des problèmes de performances et d’empreinte mémoire importante…
Je partage, dans ce post, mon retour sur expérience dans le domaine !
Voici une solution pour générer rapidement et à la volée des minaitures de vos images tout en ayant une application restant stable lors de la génération des dites miniatures.

FastThumbnails

Tout d’abord nous allons créer un ViewModel représentant une image :


public class PhotoViewModel : INotifyPropertyChanged
    {
        #region Membres
        private BitmapSource _thumbnail = null;
        /// <summary>
        /// INotofyPropertyChanged event
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        private ExifOrientations orientation = ExifOrientations.Normal;
        #endregion

        #region Ctors
        /// <summary>
        /// Crée une instance du view model permettant d'obtenir un thumbnail d'une image
        /// </summary>
        /// <param name="mediaUrl">Url de l'image à manipuler</param>
        public PhotoViewModel(String mediaUrl)
        {
            MediaUrl = mediaUrl;
        }
        #endregion

        #region Propriétées

        /// <summary>
        /// Détermine l'url de l'image à manipuler
        /// </summary>
        public String MediaUrl
        {
            get;
            private set;
        }

        /// <summary>
        /// Détermine si la génération de la miniature à échoué
        /// </summary>
        public Boolean IsFailed
        {
            get;
            private set;
        }

        /// <summary>
        /// Obtient le thumbnail de l'image source
        /// </summary>
        public BitmapSource Thumbnail
        {
            get
            {
                if ((_thumbnail == null) && !IsFailed)
                    _thumbnail = GetThumbnail();
                return _thumbnail;
            }
        }
        #endregion

        #region Méthodes
        /// <summary>
        /// Libère les ressources utilisées
        /// </summary>
        public void Dispose()
        {
            _thumbnail = null;
        }

        /// <summary>
        /// Obtient le thumnail de l'image source (incluant la rotation)
        /// </summary>
        /// <returns></returns>
        private BitmapSource GetThumbnail()
        {
            BitmapSource ret = null;
            BitmapMetadata meta = null;
            double angle = 0;

            try
            {
                //tentative de creation du thumbnail via Bitmap frame : très rapide et très peu couteux en mémoire !
                BitmapFrame frame = BitmapFrame.Create(
                    new Uri(MediaUrl),
                    BitmapCreateOptions.DelayCreation,
                    BitmapCacheOption.None);

                if (frame.Thumbnail == null) //echec, on tente avec BitmapImage (plus lent et couteux en mémoire)
                {
                    BitmapImage image = new BitmapImage();
                    image.DecodePixelHeight = 90; //on limite à 90px de hauteur
                    image.BeginInit();
                    image.UriSource = new Uri(MediaUrl);
                    image.CacheOption = BitmapCacheOption.None;
                    image.CreateOptions = BitmapCreateOptions.DelayCreation; //important pour les performances
                    image.EndInit();

                    if (image.CanFreeze) //pour éviter les memory leak
                        image.Freeze();

                    ret = image;
                }
                else
                {
                    //récupération des métas de l'image
                    meta = frame.Metadata as BitmapMetadata;
                    ret = frame.Thumbnail;
                }

                if ((meta != null) && (ret != null)) //si on a des meta, tentative de récupération de l'orientation
                {
                    if (meta.GetQuery("/app1/ifd/{ushort=274}") != null)
                    {
                        orientation = (ExifOrientations)Enum.Parse(typeof(ExifOrientations), meta.GetQuery("/app1/ifd/{ushort=274}").ToString());
                    }

                    switch (orientation)
                    {
                        case ExifOrientations.Rotate90:
                            angle = -90;
                            break;
                        case ExifOrientations.Rotate180:
                            angle = 180;
                            break;
                        case ExifOrientations.Rotate270:
                            angle = 90;
                            break;
                    }

                    if (angle != 0) //on doit effectuer une rotation de l'image
                    {
                        ret = new TransformedBitmap(ret.Clone(), new RotateTransform(angle));
                        ret.Freeze();
                    }
                }
            }
            catch (Exception ex)
            {
            }

            return ret;
        }
        #endregion

        #region Handlers
        public void OnPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }

Ensuite créez un nouveau fichier code pour y mettre l’énumération des données Exif d’orientation :


public enum ExifOrientations
    {
        None = 0,
        Normal = 1,
        HorizontalFlip = 2,
        Rotate180 = 3,
        VerticalFlip = 4,
        Transpose = 5,
        Rotate270 = 6,
        Transverse = 7,
        Rotate90 = 8
    }

Ca y est nous y sommes presque !, créons une fenêtre WPF :


<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Background="White">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button Content="Parcourrir..." Click="Button_Click"/>
            <Button Content="Clear" Click="Button_Click_1"/>
        </StackPanel>

        <ItemsControl x:Name="UploadList"
                 Grid.Row="1"
                      ItemsSource="{Binding}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 Background="Transparent"
                 ScrollViewer.IsDeferredScrollingEnabled="True">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel IsItemsHost="True"/>

                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid Margin="5" >
                        <Rectangle Fill="White" >
                            <Rectangle.BitmapEffect>
                                <DropShadowBitmapEffect Opacity="0.5" ShadowDepth="0" />
                            </Rectangle.BitmapEffect>
                        </Rectangle>
                        <Border Background="Gray" BorderBrush="White" BorderThickness="5" Height="90">
                            <Image Stretch="Uniform" Source="{Binding Path=Thumbnail, Mode=OneTime, IsAsync=True}"/>
                        </Border>
                    </Grid>

                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

Et voici le code behind :


using System;
using System.Windows;
using Microsoft.Win32;
using System.Collections.ObjectModel;

namespace WpfApplication1
{
    /// <summary>
    /// Logique d'interaction pour Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        ObservableCollection<PhotoViewModel> liste = new ObservableCollection<PhotoViewModel>();

        public Window1()
        {
            InitializeComponent();

            DataContext = liste;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog dlg = new OpenFileDialog();
            dlg.Multiselect = true;
            dlg.Filter = "Fichiers image|*.jpg";
            dlg.ShowDialog(this);

            foreach (String s in dlg.FileNames)
            {
                liste.Add(new PhotoViewModel(s));
            }
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            foreach (PhotoViewModel m in liste)
                m.Dispose();

            liste.Clear();

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
}

Facile et rapide !
Code source de l’application : WPFFastThumbnails.zip (renommez -zip.doc en .zip)

Publicités
Catégories :WPF
  1. c#-fan
    10 avril 2010 à 19 h 22

    Merci beaucoup j’ai cherchez long temps pour une soultions comme sela. Cest tres vite.

  2. slouge
    10 avril 2010 à 19 h 34

    🙂

  3. 7 mai 2011 à 21 h 02

    Bonjour, c’est tres instructif, mais je ne comprend pas exactement les 2 lignes :

    094
    if (image.CanFreeze) //pour éviter les memory leak
    095
    image.Freeze();

  4. Stéphane LOUGE
    8 mai 2011 à 7 h 42

    Bonjour et merci !
    tout d’abord un lien msdn expliquant les objets Freezable : http://msdn.microsoft.com/en-us/library/ms750509.aspx

    Ces deux lignes que vous mentionnez sont importantes dans plusieurs cas :
    – le CanFreeze permet de savoir si l’objet est Freezable afin de pouvoir appeler la méthode Freeze() sans quelle ne déclenche une exception mentionnant que l’objet de peut pas être « Freezé »
    – Permet d’accéder au BitmapSource depuis un autre thread (exemple si vous faites la création de tout celà dans un thread autre que celui de l’UI)
    – Image.Source n’est pas thread safe il faut donc passer par un invoke ou un objet freezable qui peut être partagé entre les threads.
    – rend BitmapSource non modifiable : améliore les performances et ne garde plus les références au fichier et url de l’image qui peuvent créer des memory leak (fuites mémoire).

    🙂

  5. 8 mai 2011 à 10 h 14

    ca c’est de la réponse !
    j’ai juste pas du tout le niveau en .net ( on programme en delphi en ce moment, et on galere pour trouver un code qui puisse faire marcher le WIC en mode asynchro comme tu l’exploites avec la ligne :

    image.CreateOptions = BitmapCreateOptions.DelayCreation

    est ce que l’équivalent de ton code serait faisable en pure com ?

  6. Stéphane LOUGE
    8 mai 2011 à 19 h 14

    As-tu un petit projet d’exemple qui pourrait me montrer ce que tu veux faire en condition réelle ?

  7. fedos
    18 octobre 2011 à 8 h 57

    Tout d’abord, merci pour ce code qui marche très bien.

    J’ai juste un petit problème. Sur mes photos, les miniatures ont des bandes noires en haut et en bas. Je ne comprends pas pourquoi. Est-ce que tu aurais une idée ?

  8. Stéphane LOUGE
    23 octobre 2011 à 20 h 01

    Merci 🙂
    En effet cela correspond à la miniature embarquée dans les clichés d’appareils photo numériques type reflex ou ayant un format 3/2.
    Je regarderai ce que fait le framework quand j’aurai 5 minutes pour voir d’où cela vient !
    Je te conseille de faire en « background » la méthode plus lente afin d’obtenir une version sans ces éventuelles bordures noires !

  9. 18 février 2014 à 5 h 30

    I test it and left running while on lunch, and when I got back I saw:
    Managed Debugging Assistant ‘ContextSwitchDeadlock’ has detected a problem in ‘C:\Test\wpffastthumbnails-zip1\WPFFastThumbnails\bin\Debug\WpfApplication10.vshost.exe’.

    Additional Information: The CLR has been unable to transition from COM context 0x218d080 to COM context 0x218d2d0 for 60 seconds. The thread that owns the destination context/apartment is most likely either doing a non pumping wait or processing a very long running operation without pumping Windows messages. This situation generally has a negative performance impact and may even lead to the

  1. No trackbacks yet.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

w

Connexion à %s

%d blogueurs aiment cette page :