/// *********************************************************************************************************
/// © 2014 www.jakemdrew.com All rights reserved.
/// This source code is licensed under The GNU General Public License (GPLv3):
/// http://opensource.org/licenses/gpl-3.0.html
/// *********************************************************************************************************
/// *********************************************************************************************************
/// ImagesDistanceMatrix - MapReduce An Image Distance Matrix in Parallel.
///
/// Created By - Jake Drew
/// Version - 1.0, 06/23/2014
/// *********************************************************************************************************
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ImageClustering
{
///
/// This class hold all information for a single pairwise distance match within the matrix.
///
public class PairwiseMatch
{
public int rowIndex;
public int colIndex;
public RgbProjection projectionR;
public RgbProjection projectionC;
public PairwiseMatch(int row, int column,RgbProjection pR, RgbProjection pC)
{
rowIndex = row;
colIndex = column;
projectionR = pR;
projectionC = pC;
}
public PairwiseMatch(int row, int column)
{
rowIndex = row;
colIndex = column;
}
}
///
/// Creates a pairwise distance matrix from an input directory of images.
///
public class ImagesDistanceMatrix
{
public string[] filePaths;
public double[][] matrixRows;
//We save the RGB projection features for each image since they are used multiple times
public ConcurrentDictionary imageFeatures = new ConcurrentDictionary();
BlockingCollection MatrixMatches = new BlockingCollection(new ConcurrentQueue());
BlockingCollection ImageFeatures = new BlockingCollection(new ConcurrentQueue());
public void createImageDistanceMatrix(string inputDirLoc, string outputFileLoc)
{
File.Delete(outputFileLoc);
//get all of the image files in a directory
filePaths = Directory.GetFiles(inputDirLoc,"*.png");
matrixRows = new double[filePaths.Length][];
//initialize matrix array
for (int i = 1; i < matrixRows.Length; i++)
{
matrixRows[i] = new double[i];
}
//Produce / map pairwise matches in a background thread.
Task.Run(() => createPairwiseMatches(inputDirLoc));
//Consume /Reduce pairwise matches into matrix distances at the same time.
calculateDistances(outputFileLoc);
//We are down with all of the features at this point.
imageFeatures.Clear();
//Write the distance matrix to a csv
createMatrix(outputFileLoc, matrixRows);
}
///
/// Rapidly generate all required matrix matches in the background for processing.
/// (this allows a much more balanced execution of Parallel.ForEach on all matches)
/// Yeild return passes each match to parallel foreach in createPairwiseMatches()
/// as they are produced.
///
///
private IEnumerable generateMatches()
{
for (int r = 1; r < filePaths.Length; r++)
{
//for each column less than the current row
for (int c = 0; c < r; c++)
{
yield return new PairwiseMatch(r, c);
}
}
}
///
/// Parallel asynchronous background process to create all pairwise matches for a
/// given matrix, extracting image features one time (as required).
///
///
public void createPairwiseMatches(string inputDirLoc)
{
Parallel.ForEach(generateMatches(), match =>
{
match.projectionR = getImageFeatures(filePaths[match.rowIndex]);
match.projectionC = getImageFeatures(filePaths[match.colIndex]);
MatrixMatches.Add(match);
});
MatrixMatches.CompleteAdding();
}
///
/// Asynchronous background process to extract rgb projections (when needed)
/// and calculate similarity between images in parallel.
///
public void calculateDistances(string outputFileLoc)
{
Parallel.ForEach(MatrixMatches.GetConsumingEnumerable(), matrixMatch =>
{
//Calculate similarity between two images and save it to the correct matrix row.
double similarity = RgbProjector.CalculateSimilarity(matrixMatch.projectionR, matrixMatch.projectionC);
double distance = 1 - similarity;
matrixRows[matrixMatch.rowIndex][matrixMatch.colIndex] = distance;
});
}
public void createMatrix(string outputFileLoc, double[][] matrixRows)
{
StringBuilder matrix = new StringBuilder();
//Create header and 1st row
matrix.AppendLine(createHeaderRow());
int rowIndex = 0;
foreach (var row in matrixRows)
{
//matrix.Append("\"" + Path.GetFileNameWithoutExtension(filePaths[rowIndex]) + "\"");
matrix.Append("\"" + filePaths[rowIndex] + "\"");
if (row != null)
{
foreach (var rowValue in row)
{
matrix.Append(",\"" + rowValue + "\"");
}
matrixRows[rowIndex] = null;
}
rowIndex++;
matrix.Append("\r\n");
//write output in blocks of 2500 characters.
if (matrix.Length >= 2500)
{
File.AppendAllText(outputFileLoc, matrix.ToString());
matrix.Clear();
}
}
//write any remaining text to the output file.
File.AppendAllText(outputFileLoc,matrix.ToString());
}
public string createHeaderRow()
{
StringBuilder row = new StringBuilder();
row.Append("www.jakemdrew.com");
foreach (var filePath in filePaths)
{
//row.Append("," + "\"" + Path.GetFileNameWithoutExtension(filePath) + "\"");
row.Append("," + "\"" + filePath + "\"");
}
return row.ToString();
}
public RgbProjection getImageFeatures(string imagePath)
{
RgbProjection imageProjections;
if (imageFeatures.TryGetValue(imagePath.GetHashCode(), out imageProjections))
{
return imageProjections;
}
else
{
using (var bitmap = RgbProjector.ResizeBitmap(new Bitmap(imagePath), 1000, 1000))
{
imageProjections = RgbProjector.GetRgbProjections(bitmap);
imageFeatures.TryAdd(imagePath.GetHashCode(), imageProjections);
}
return imageProjections;
}
}
}
}