Dateisortierung - nur etwas umständlicher

Fernaless

Schon lange hier
Hallo,

ich habe ein kleines Problem: Auf einer externen Festplatte habe ich einen Ordner mit rund 800 Bildern. Davon sind etliche doppelt und dreifach vorhanden.:D Diese sehen exake gleich aus, haben nur verschiedene Namen. Nun zu meiner Frage: Gibt es irgendeine Möglichkeit, diese durch ein Programm (könnte ich auch selbst schreiben) so zu Sortieren, dass im Hauptordner nur jedes Bild einmal vorkommt und die duplikate entweder in einen anderen Ordner verschoben oder gelöscht werden?

Schonmal im voraus danke für die Hilfe.
 
Keine Ahnung, ob es das gibt, und wenn, müsste ja jedes Bild mit jedem verglichen werden, eine äußerst aufwändige Sache.
Aber suche mal in den Software-Vorstellungen mit dem Suchbegriff "Doppelte Dateien", eventuell ist da etwas mit dabei.
 
Wenn es wirklich exakt die gleichen Bilder sind könntest du für jedes Bild eine Prüfsumme, z.B. md5, berechnen. Wenn du dann eine Prüfsumme öfters hast, könntest du je nachdem mit den zugehörigen Bildern weiter verfahren wie du willst
 
Wie gesagt, sie sind exakt dieselben, allerdings unterschiedliche Namen, die allerdings auch ähnlich sind.
 
Also bei C# könnte ich die Bilder als Objekt der Klasse Image einzulesen und dann so zu vergleichen, da wird dann ein hash-Wert berechnet, mit dem dann verglichen wird. Ich weiß leider nicht, wie man das mit dem Hash-Wert manuell anstellt, aber ich denke mal, dass der auf Basis der Pixel berechnet wird, also wenn wirklich jedes Pixel gleich ist, sollte das eigentlich funktionieren, aber ich teste es gleich mal, hab grad nix zu tun. ^^

Wenn ich Recht habe, wäre es auch nicht weiter schwer, das gleich in ein kleines Programm zu verwenden, aber damit wirklich nichts doppelt ist, dann müsste jedes Bild miteinander verglichen werden. Entweder ich würde dann immer extra das Bild einlesen lassen, wenn es verglichen werden soll, oder ich lasse alle Bilder am Anfang einlesen und die liegen dann im Arbeitsspeicher, während sie verglichen werden. Das ist dann halt die Frage, was sinnvoller ist, denn 800 Bilder sind schon eine ganz ansehnliche Zahl.


Edit: Funktioniert nicht, aber ich bin der Sache auf der Spur und probiere mal, ob ich das mit sl0rp Vorschlag lösen kann.
 
Zuletzt bearbeitet:
Ich denke dann wäre der Ansatz über die Prüfsumme recht einfach. Der Name der Datei fließt nicht in die Prüfsummenberechnung mit ein, somit würdest du also die identischen Dateien heraus finden. Ich weiss nicht in welcher Programmiersprache du dich auskennst, aber ich denke bei den meisten sind diverse Funktionen für die Prüfsummenberechnung schon implementiert. Also müsstest du nur noch die Prüfsummen vergleichen und dann die entsprechenden Dateien verschieben oder löschen.
 
@sl0rp:
C# ^^
Und die Methode zum Vergleichen hab ich bereits und sie funktioniert auch, allerdings nicht selber geschrieben. ^^
Dort wird der SHA256-Hash berechnet und dann verglichen, funktioniert 1A.

Grad bin ich dabei, eine weitere Methode zu schreiben, die Dublikate findet um dann verschiedene Vergleichs-Methoden zu testen und die schnellste zu nehmen.
 
Wenn es schon Programme gibt dafür (was ich mir auch schon gedacht habe) super. Doch normalerweise würde ich sowas selbst Programmieren. Kann aber nur ganz grob Java, und kompliziertere Programmiersprachen werden nicht umbedingt drankommen in der Ausbildung/Berufsschule, da ich eine Ausbildung als Fachinformatiker für Systemintegration mache.;)
 
Meine erste Idee für den Vergleich wäre mit einer HashMap gewesen. Ich hätte die Prüfsumme als Key genommen und den Dateinamen als Wert. Dann in einer Schleife:
Prüfsumme von Datei berechnen
überprüfen ob Key bereits in HashMap vorhanden ist
wenn nein: Key/Wert in HashMap einfügen
wenn ja: aktuelle Datei verschieben/löschen
 
@Fernaless:

Kompliziertere Programmiersprachen als Java? Mehr als Java brauchst du eigentlich nicht. ^^
Denke ich zumindest, ich kenne von Java nur den winzigen Schul-Umfang, hab dann halt C# gelernt.

Aber in C# ist es eigentlich ziemlich klein, da wird es in Java auch nicht so viel sein. Wenn da dann Bibliotheken fehlen, findet sich bestimmt das Eine oder Andere im Netz.


@sl0rp:

Kann ich das als Äquivalent zu der HashMap von Java betrachten?
Hat das dann irgendeinen bestimmten Sinn, dass es nach den Hash-Werten sortiert wird?
Ich würde sonst einfach ein ganz einfaches Dictionary nehmen, welches auch Schlüssel-Wert-Paare bereit stellt. Die Schlüssel sind eindeutig, aber werden nicht sortiert.


Dann wäre das wirklich total einfach, deine Herangehensweise :D
Ich wollte jetzt erst einmal eine Methode bauen, die die Bilder dann sortiert und die optisch gleichen Bilder alle gruppiert, damit ich testen kann, ob der Vergleich auch bei größeren Mengen zuverlässig arbeitet.
 
Hm kenne mich mit C# nicht wirklich aus. Nach kurzer Suche würde ich aber sagen, dass eher die HashMap dem Dictionary entspricht, und die HashTable der HashTable ;)

Joa, man muss sich ja nicht immer alles verkomplizieren :D
 
Hab mal eine Methode geschrieben, die genau das tut:

PHP:
        public static string[] DistinctPictures(string[] files)
        {
            var PictureHashList = new byte[files.Length][];
            for (int i = 0; i < files.Length; i++)
                using (Image img = Image.FromFile(files[i]))
                    PictureHashList[i] = new SHA256Managed().ComputeHash(
                        (Byte[])new ImageConverter().ConvertTo(
                        img, typeof(Byte[])));
            var dictionary = new Dictionary<byte[], string>();
            for (int i = 0; i < PictureHashList.Length; i++)
                if (!new Func<bool>(() =>
                {
                    foreach (var item in dictionary.Keys.ToList())
                        if (PictureHashList[i].SequenceEqual(item))
                            return true;
                    return false;
                }).Invoke())
                    dictionary.Add(PictureHashList[i], files[i]);
            return dictionary.Values.ToArray();
        }

Ist jetzt ziemlich kompakt geschrieben, einfach nur, weil ich kompakt geschriebene Methoden mag. Da hab ich besseren Überblick über alle Methoden. ^^


Ich habs für 40 Bilder getestet. Das Resultat war, wie erwartet, zehn Dateien.
Fernaless, wenn du mir OK gibst, schreib ich noch ein kleines Konsolen-Programm, was dein Problem löst und die Bilder, die nicht doppelt sind, in einen extra Ordner kopiert.


verborgener Text:

PHP:
        public static string[] DistinctPictures(string[] files)
        {
            // Eine Liste, die die Hash-Werte der Bilder gleich auf nimmt,
            // um die Bilder nicht so lange im Speicher zu lassen
            List<byte[]> PictureHashList = new List<byte[]>(files.Length);
            // Durchläuft das Array der Datei-Pfade
            for (int i = 0; i < files.Length; i++)
            {
                // Image.FrimFile(string) liest aus einer Bild-Datei die Daten für ein Image-Objekt
                // Dieses Bild wird als Ressource behandelt, using sorgt dafür,
                // dass der Speicher nachträglich wieder frei gegeben wird
                using (Image img = Image.FromFile(files[i]))
                {
                    // Das Bild wird als byte-Array gespeichert, aus dem dann der SHA256-Hash-Wert berechnet werden kann
                    ImageConverter imgConverter = new ImageConverter();
                    Byte[] ImgBytes = (Byte[])imgConverter.ConvertTo(img, typeof(Byte[]));

                    // Hier wird der SHA256-Hash-Wert berechnet und in die Liste von Oben gelegt
                    SHA256Managed sha = new SHA256Managed();
                    PictureHashList[i] = sha.ComputeHash(ImgBytes);
                } // Dort wo der using-Block endet, wird auch das Bild via Dispose() aus dem Speicher gelöscht
            }

            // Um sicher zu gehen, dass wirklich jeder SHA256-Hash-Wert nur einmal vorhanden ist
            // muss ich alle Anderen aus sortieren, deher lege ich die Werte zusammen mit dem Datei-Pfad
            // in ein Dictionary
            Dictionary<byte[], string> dictionary = new Dictionary<byte[], string>();
            for (int i = 0; i < PictureHashList.Count; i++)
            {
                // Die Liste der Keys benötige ich um eine Alternative zu ContainsKey(byte[]) zu bauen,
                // da diese Methode nicht sequenziell vergleicht
                List<byte[]> Keys = dictionary.Keys.ToList();
                if (!ContainsByteArrayKey(Keys, PictureHashList[i]))
                {
                    // Wenn der neue SHA256-Hash-Wert nicht gefunden wurde, kann er in das Dictionary aufgenommen werden
                    // Gleichzeitig wird noch der Datei-Pfad eingefügt,
                    // damit der im Nachhinein in einem Array zurück gegeben werden kann
                    dictionary.Add(PictureHashList[i], files[i]);
                }
            }
            // Die Dateipfade der nicht doppelten Bilder werden zurück gegeben
            return dictionary.Values.ToArray();
        }

        // Diese Methode stellt die Alternative zu ContainsKey(byte[]) dar,
        // sie benötigt eine Liste der Schlüssel und den neuen Schlüssel, der darin gesucht wird
        // Diese Methode hab ich in der kurzen Fassung als Lambda-Ausdruck geschrieben und
        // mit einem Delegaten gestartet um keine zweite Methode zu benötigen
        private static bool ContainsByteArrayKey(List<byte[]> Keys, byte[] NewKey)
        {
            foreach (var item in Keys)
            {
                // Wenn beim Durchlaufen der Schlüssel nur einmal der neue Schlüssel gefunden wird true zurück gegeben
                // SequenceEqual(byte[]) vergleicht die einzelnen Elemente der Arrays
                if (NewKey.SequenceEqual(item))
                {
                    return true;
                }
            }
            return false;
        }
 
Du hast mein OK. Einzige Bedingung: Ich bekomme ein Strucktogramm und (wenn du willst) auch das Programm samt Quellcode, ich will schließlich auch noch etwas draus lernen für Programmiersprachen. :D
 
Auch noch Ansprüche hier :D


Naja, nagut, aber ein Strucktogramm? Das dauert dann noch :D
Am PC werde ich das dann wohl nie schaffen zu machen. Ich druck mir den Quelltext einfach aus und mach das in der Schule, dann hab ich da was sinnvolles zu tun. ^^


Zum Programm:
Den Kern, also die DistinctPictures-Methode hab ich ja schon erklärt und dort liegt auch der schwerste Punkt. Dort hab ich allerdings etwas ergänzt, nämlich den try-catch-Block, der Fehler beim Einlesen in ein Image ab fangen soll, da eine Datei ja auch mal kein Bild sein muss. Dann schreibe ich in den Fehler-Text den Datei-Pfad und die Fehler-Nachricht, die mit gegeben wurde und das ganze wird dann am Ende in die error.txt geschrieben. Da wird auch immer ein Fehler-Auftauchen, denn er will die Tools.exe auch als Bild einlesen, was natürlich nicht funktioniert, aber das ist ja nicht weiter wild, soll jetzt ja nur dein Problem zuverlässig lösen und das tut es einwandfrei. Zumindest bei mir :D

In der Main-Methode passiert eigentlich kaum etwas, aber ich hab sie dennoch nochmal kommentiert.


verborgener Text:
PHP:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Drawing;
using System.IO;

namespace Tools
{
    class Program
    {
        static void Main(string[] args)
        {
            // Empfängt den Pfad des Ordners, in dem die exe-Datei liegt
            string WorkPath = Environment.CurrentDirectory;

            // Hier suche ich einen in jedem Fall nicht vorhandenenen Ordner-Namen
            string ResultPath = WorkPath + "~";
            int i = 1;
            // Die Schleife läuft so lange weiter, bis der abgeänderte Pfad nicht existiert.
            // Damit die Änderung sich auch ändert, wird eine int-Variable hoch gezählt
            while (Directory.Exists(ResultPath + i.ToString())) i++;
            // Wenn die Schleife nun endet, speicher ich den neuen Pfad ab und erstelle den Ordner.
            ResultPath += i.ToString();
            Directory.CreateDirectory(ResultPath);

            // Ruft alle Datei-Pfad im angegebenen Ordner ab
            string[] files = Directory.GetFiles(WorkPath);

            // Hier kommen die ausgemisteten Dateien an, ohne Duplikate
            string[] Results = DistinctPictures(files);
            // Die Liste der resultierenden Datei-Pfade wird durchlaufen und jede Datei kopiert.
            // Der erste Parameter gibt die Quelle, der Zweite das Ziel an
            // im zweiten Parameter nehme ich dann den neuen, leeren Ordner-Pfad und hänge auf eine sichere Weise
            // Den Datei-Namen an. Dafür gibt es die Methode Path.Combine, die das fehlerfrei erledigt
            foreach (var item in Results)
                File.Copy(item, Path.Combine(ResultPath, Path.GetFileName(item)));

        }

        static string[] DistinctPictures(string[] files)
        {
            // Der Fehler-Text, der in die error.txt geschrieben wird
            string ErrorString = "";
            var PictureHashList = new byte[files.Length][];
            for (int i = 0; i < files.Length; i++)
                // Der try-Block wird in jedem Fall ausgeführt
                try
                {
                    using (Image img = Image.FromFile(files[i]))
                        PictureHashList[i] = new SHA256Managed().ComputeHash(
                            (Byte[])new ImageConverter().ConvertTo(
                            img, typeof(Byte[])));
                }
                // Der catch-Block wird immer dann ausgeführt, wenn im try-Block ein Laufzeitfehler auf tritt
                catch (Exception ex)
                {
                    // Exception ist die Basis-Klasse für alle Fehler-Klassen.
                    // Dort kann man verschiedene Typen verwenden und so z.B.
                    // für die Ableitung "FileNotFoundException" eine gesonderte Fehlerbehandlung einbauen,
                    // Wenn eine Datei nicht gefunden wurde
                    // Ich hab hier einfach nur Exception genommen, um den Absturz des Programmes in jedem Fall zu verhindern.
                    ErrorString += "Error:";
                    ErrorString += "  \r\nDatei:  " + files[i];
                    ErrorString += "  \r\nFehler: " + ex.Message;
                    ErrorString += "\r\n" + new string('-', 20);
                }
            // Hier schreibe ich die error.txt
            // Ein FileStream ist Stream, der geschlossen werden muss, daher verwende ich hier using
            // Der Parameter "FileMode.Create" gibt an, dass die Datei entweder erstellt oder überschrieben wird
            if (ErrorString != "")
                using (FileStream file = new FileStream("error.txt", FileMode.Create))
                // Da der FileStream nur Bytes schreiben kann, brauche ich einen StreamWriter,
                // der für mich in den FileStream schreibt. Auch der muss danach wieder geschlossen werden,
                // daher auch hier wieder using
                    using (StreamWriter writer = new StreamWriter(file))
                        writer.Write(ErrorString);



            var dictionary = new Dictionary<byte[], string>();
            for (int i = 0; i < PictureHashList.Length; i++)
                if (PictureHashList[i] != null)
                    if (!new Func<bool>(() =>
                    {
                        foreach (var item in dictionary.Keys.ToList())
                            if (PictureHashList[i].SequenceEqual(item))
                                return true;
                        return false;
                    }).Invoke())
                        dictionary.Add(PictureHashList[i], files[i]);
            return dictionary.Values.ToArray();
        }
    }
}


PS: Kennt jemand eine Seite, wo ich Quell-Code hinterlegen und dann von dort verlinken kann, bloß mit einer anständigen Formatierung, wie sie auch farblich für C# üblich ist? Das mit PHP-Tags zu machen ist irgendwie nicht das Wahre. ^^

PS²: Kann sein, dass er meckert, von wegen die error.txt wird von einem anderen Prozess verwendet. Das ist der Grund, warum das jetzt so ewig gedauert hat, ich hab diesen blöden Fehler nicht weg gekriegt und wusste auch nicht, welcher Prozess das sein sollte. Irgendwann hab ich dann nochmal compiliert und gestartet, dann ging es, aber woran das nun lag, weiß ich nicht.

PS³: Mein Test lief vier Sekunden, wenn ich schnell genug auf die Uhr geschaut hab. ^^
Die Maße sind schon recht groß, das geht von 1500x1000 bis 2700x2700, grob geschätzt, aber alle JPG.
Ein weiterer Test mit den gleichen zehn Bildern, nur diesmal 200 davon, ergab 16 Sekunden, nach Augenmaß.
 

Anhänge

  • Tools.rar
    3,5 KB · Aufrufe: 183
Zuletzt bearbeitet:
Abgesehen davon, dass ich es toll finde, dass der Code geposted wurde (Danke! Direkt kopiert) verwende ich zur suche von Doubletten und auch von ähnlichen Bildern die eingebaute Suche bei XNView mit einstellbarer Ähnlichkeitssuche. Ist zwar relativ langsam, wenn man nicht auf genau gleiche Dateien unabhängig von der Bezeichnung geht (da verwendet XNView auch den hash), aber manchmal die einzige Art, viele ähnliche Bilder in einem Ordner zu versammeln, ohne sich selbst die Augen verbiegen zu müssen ...

Funktioniert auch rekursiv über mehrere Ordner.
 
Das ist alles auch nicht weiter DAS Problem. ^^

Hab überlegt, ob ich mir da auch was suche, habs dann aber raus gelassen, weil Fernaless geschrieben hat, die Bilder seien exakt gleich.

Wenn ihr mir etwas Zeit gebt, kann ich auch mal schauen, ob ich ein umfangreicheres Programm dafür baue und das dann auch mehrere Ordner durchsucht, das wärde in der Zeile
PHP:
string[] files = Directory.GetFiles(OrdnerPfad)
nur so geändert werden müssen:
PHP:
string[] files = Directory.GetFiles(OrdnerPfad, "*.*", SearchOption.AllDirectories)
Der erste Parameter ist immer noch der Pfad des Ordners, der Zweite enthält eine Suchzeichenfolge, die ich hier mit "*.*" bezeichne, da der Stern für alle Zeichen steht und so alle Datei-Namen und Endungen akzeptiert werden. Der Dritte Parameter ist ein Wert des Enums SearchOption, welches zwei Konstanten bereit stellt:
  • TopDirectoryOnly -> Schließt nur das aktuelle Verzeichnis in einem Suchvorgang ein.
  • AllDirectories -> Schließt das aktuelle Verzeichnis und alle Unterverzeichnisse in einem Suchvorgang ein.Diese Option schließt Analysepunkte wie umschlossenes Laufwerk und symbolische Links in der Suche.


Ich würde dann auch eine einfache grafische Oberfläche bauen, aber das kostet dann wieder mehr Zeit.
Auch den Quell-Code würde ich einfügen, damit hab ich kein Problem ^^


Edit:
Hab da jetzt eine Bibliothek gefunden, die da scheinbar ganz gute Funktionen für den ganzen Breiten Themen-Bereich zur Verfügung stellt.
Ich würde zwar versuchen, das alleine zu schreiben, aber je mehr ich darüber lese, um so mehr Respekt bekomme ich auch vor dem Umfang und der Komplexität der Aufgabe :D
Schöne Diskussion dazu: http://www.mycsharp.de/wbb2/thread.php?threadid=100695

Dort hab ich auch den Code her, den ich dann für diese Methode verwendet habe:
PHP:
static Bitmap verkleinern(Bitmap image, int width = 25, int hight = 25)
{
      Bitmap bmpOutput = new Bitmap (width, hight, PixelFormat.Format24bppRgb);
      Graphics gOutput = Graphics.FromImage (bmpOutput);
      Rectangle rectOutput = new Rectangle (0, 0, bmpOutput.Width, bmpOutput.Height);

      ImageAttributes ia = new ImageAttributes();
      ia.SetWrapMode (WrapMode.TileFlipXY);

      gOutput.InterpolationMode = InterpolationMode.HighQualityBilinear;
      gOutput.PixelOffsetMode = PixelOffsetMode.Half;
      gOutput.CompositingMode = CompositingMode.SourceCopy;

      gOutput.DrawImage (image, rectOutput, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, ia);
      bmpOutput.Save ("output.bmp", ImageFormat.Bmp);
      
      return bmpOutput;
}

Die Methode verkleinert einfach ein Bild, das brauch ich dann für die Bestimmung, ob das Bild zu einem Anderen ähnlich ist. Da bin ich aber noch am knobeln und außerdem brauch ich dafür noch einen Grenz-Wert, der dann angibt, wie verschieden ein Bild zum Anderen sein darf, die Frage ist nur, wo dieser Wert liegt.
Ich schau erst mal, ob ich die Methode hin kriege und würde euch dann bitten, mir beim Ermitteln dieses Wertes zu helfen, dass dabei ein paar Werte heraus kommen, die ich dann ja in ein Enum einbauen kann. Also dann z.B. die Konstanten: NichtÄhnlich, Ähnlich, SehrÄhnlich, Gleich
Oder irgendwie so
 
Zuletzt bearbeitet:
Oben