Skip to content

Add DataView Type Register (supporting loading in-memory image and other in-memory custom types) #3263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ML;
using Microsoft.ML.Data;

namespace Samples.Dynamic
{
class CustomMappingWithInMemoryCustomType
{
static public void Example()
{
var mlContext = new MLContext();
// Build in-memory data.
var tribe = new List<AlienHero>() { new AlienHero("ML.NET", 2, 1000, 2000, 3000, 4000, 5000, 6000, 7000) };

// Build a ML.NET pipeline and make prediction.
var tribeDataView = mlContext.Data.LoadFromEnumerable(tribe);
var pipeline = mlContext.Transforms.CustomMapping(AlienFusionProcess.GetMapping(), contractName: null);
var model = pipeline.Fit(tribeDataView);
var tribeTransformed = model.Transform(tribeDataView);

// Print out prediction produced by the model.
var firstAlien = mlContext.Data.CreateEnumerable<SuperAlienHero>(tribeTransformed, false).First();
Console.WriteLine($"We got a super alien with name {firstAlien.Name}, age {firstAlien.Merged.Age}, " +
$"height {firstAlien.Merged.Height}, weight {firstAlien.Merged.Weight}, and {firstAlien.Merged.HandCount} hands.");

// Expected output:
// We got a super alien with name Super Unknown, age 4002, height 6000, weight 8000, and 10000 hands.

// Create a prediction engine and print out its prediction.
var engine = mlContext.Model.CreatePredictionEngine<AlienHero, SuperAlienHero>(model);
var alien = new AlienHero("TEN.LM", 1, 2, 3, 4, 5, 6, 7, 8);
var superAlien = engine.Predict(alien);
Console.Write($"We got a super alien with name {superAlien.Name}, age {superAlien.Merged.Age}, " +
$"height {superAlien.Merged.Height}, weight {superAlien.Merged.Weight}, and {superAlien.Merged.HandCount} hands.");

// Expected output:
// We got a super alien with name Super Unknown, age 6, height 8, weight 10, and 12 hands.
}

// A custom type which ML.NET doesn't know yet. Its value will be loaded as a DataView column in this test.
private class AlienBody
{
public int Age { get; set; }
public float Height { get; set; }
public float Weight { get; set; }
public int HandCount { get; set; }

public AlienBody(int age, float height, float weight, int handCount)
{
Age = age;
Height = height;
Weight = weight;
HandCount = handCount;
}
}

// DataViewTypeAttribute applied to class AlienBody members.
private sealed class AlienTypeAttributeAttribute : DataViewTypeAttribute
{
public int RaceId { get; }

// Create an DataViewTypeAttribute> from raceId to a AlienBody.
public AlienTypeAttributeAttribute(int raceId)
{
RaceId = raceId;
}

// A function implicitly invoked by ML.NET when processing a custom type.
// It binds a DataViewType to a custom type plus its attributes.
public override void Register()
{
DataViewTypeManager.Register(new DataViewAlienBodyType(RaceId), typeof(AlienBody), new[] { this });
}

public override bool Equals(DataViewTypeAttribute other)
{
if (other is AlienTypeAttributeAttribute)
return RaceId == ((AlienTypeAttributeAttribute)other).RaceId;
return false;
}

public override int GetHashCode() => RaceId.GetHashCode();
}

// A custom class with a type which ML.NET doesn't know yet. Its value will be loaded as a DataView row in this test.
// It will be the input of AlienFusionProcess.MergeBody(AlienHero, SuperAlienHero).
//
// The members One> and Two" would be mapped to different types inside ML.NET type system because they
// have different AlienTypeAttributeAttribute's. For example, the column type of One would be DataViewAlienBodyType
// with RaceId=100.
// </summary>
private class AlienHero
{
public string Name { get; set; }

[AlienTypeAttribute(100)]
public AlienBody One { get; set; }

[AlienTypeAttribute(200)]
public AlienBody Two { get; set; }

public AlienHero()
{
Name = "Unknown";
One = new AlienBody(0, 0, 0, 0);
Two = new AlienBody(0, 0, 0, 0);
}

public AlienHero(string name,
int age, float height, float weight, int handCount,
int anotherAge, float anotherHeight, float anotherWeight, int anotherHandCount)
{
Name = "Unknown";
One = new AlienBody(age, height, weight, handCount);
Two = new AlienBody(anotherAge, anotherHeight, anotherWeight, anotherHandCount);
}
}

// Type of AlienBody in ML.NET's type system.
// It usually shows up as DataViewSchema.Column.Type among IDataView.Schema.
private class DataViewAlienBodyType : StructuredDataViewType
{
public int RaceId { get; }

public DataViewAlienBodyType(int id) : base(typeof(AlienBody))
{
RaceId = id;
}

public override bool Equals(DataViewType other)
{
if (other is DataViewAlienBodyType otherAlien)
return otherAlien.RaceId == RaceId;
return false;
}

public override int GetHashCode()
{
return RaceId.GetHashCode();
}
}

// The output type of processing AlienHero using AlienFusionProcess.MergeBody(AlienHero, SuperAlienHero).
private class SuperAlienHero
{
public string Name { get; set; }

[AlienTypeAttribute(007)]
public AlienBody Merged { get; set; }

public SuperAlienHero()
{
Name = "Unknown";
Merged = new AlienBody(0, 0, 0, 0);
}
}

// The implementation of custom mapping is MergeBody. It accepts AlienHero and produces SuperAlienHero.
private class AlienFusionProcess
{
public static void MergeBody(AlienHero input, SuperAlienHero output)
{
output.Name = "Super " + input.Name;
output.Merged.Age = input.One.Age + input.Two.Age;
output.Merged.Height = input.One.Height + input.Two.Height;
output.Merged.Weight = input.One.Weight + input.Two.Weight;
output.Merged.HandCount = input.One.HandCount + input.Two.HandCount;
}

public static Action<AlienHero, SuperAlienHero> GetMapping()
{
return MergeBody;
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Drawing;
using Microsoft.ML;
using Microsoft.ML.Transforms.Image;

namespace Samples.Dynamic
{
class ConvertToGrayScaleInMemory
{
static public void Example()
{
var mlContext = new MLContext();
// Create an image list.
var images = new[] { new ImageDataPoint(2, 3, Color.Blue), new ImageDataPoint(2, 3, Color.Red) };

// Convert the list of data points to an IDataView object, which is consumable by ML.NET API.
var data = mlContext.Data.LoadFromEnumerable(images);

// Convert image to gray scale.
var pipeline = mlContext.Transforms.ConvertToGrayscale("GrayImage", "Image");

// Fit the model.
var model = pipeline.Fit(data);

// Test path: image files -> IDataView -> Enumerable of Bitmaps.
var transformedData = model.Transform(data);

// Load images in DataView back to Enumerable.
var transformedDataPoints = mlContext.Data.CreateEnumerable<ImageDataPoint>(transformedData, false);

// Print out input and output pixels.
foreach (var dataPoint in transformedDataPoints)
{
var image = dataPoint.Image;
var grayImage = dataPoint.GrayImage;
for (int x = 0; x < grayImage.Width; ++x)
{
for (int y = 0; y < grayImage.Height; ++y)
{
var pixel = image.GetPixel(x, y);
var grayPixel = grayImage.GetPixel(x, y);
Console.WriteLine($"The original pixel is {pixel} and its pixel in gray is {grayPixel}");
}
}
}

// Expected output:
// The original pixel is Color[A = 255, R = 0, G = 0, B = 255] and its pixel in gray is Color[A = 255, R = 28, G = 28, B = 28]
// The original pixel is Color[A = 255, R = 0, G = 0, B = 255] and its pixel in gray is Color[A = 255, R = 28, G = 28, B = 28]
// The original pixel is Color[A = 255, R = 0, G = 0, B = 255] and its pixel in gray is Color[A = 255, R = 28, G = 28, B = 28]
// The original pixel is Color[A = 255, R = 0, G = 0, B = 255] and its pixel in gray is Color[A = 255, R = 28, G = 28, B = 28]
// The original pixel is Color[A = 255, R = 0, G = 0, B = 255] and its pixel in gray is Color[A = 255, R = 28, G = 28, B = 28]
// The original pixel is Color[A = 255, R = 0, G = 0, B = 255] and its pixel in gray is Color[A = 255, R = 28, G = 28, B = 28]
// The original pixel is Color[A = 255, R = 255, G = 0, B = 0] and its pixel in gray is Color[A = 255, R = 77, G = 77, B = 77]
// The original pixel is Color[A = 255, R = 255, G = 0, B = 0] and its pixel in gray is Color[A = 255, R = 77, G = 77, B = 77]
// The original pixel is Color[A = 255, R = 255, G = 0, B = 0] and its pixel in gray is Color[A = 255, R = 77, G = 77, B = 77]
// The original pixel is Color[A = 255, R = 255, G = 0, B = 0] and its pixel in gray is Color[A = 255, R = 77, G = 77, B = 77]
// The original pixel is Color[A = 255, R = 255, G = 0, B = 0] and its pixel in gray is Color[A = 255, R = 77, G = 77, B = 77]
// The original pixel is Color[A = 255, R = 255, G = 0, B = 0] and its pixel in gray is Color[A = 255, R = 77, G = 77, B = 77]
}

private class ImageDataPoint
{
[ImageType(3, 4)]
public Bitmap Image { get; set; }

[ImageType(3, 4)]
public Bitmap GrayImage { get; set; }

public ImageDataPoint()
{
Image = null;
GrayImage = null;
}

public ImageDataPoint(int width, int height, Color color)
{
Image = new Bitmap(width, height);
for (int i = 0; i < width; ++i)
for (int j = 0; j < height; ++j)
Image.SetPixel(i, j, color);
}
}
}
}
Loading