From ba6f06765ec2cd1ba85d0fdfa1d8384244690a64 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 2 Jul 2018 16:22:16 -0700 Subject: [PATCH 01/16] first iteration --- Microsoft.ML.sln | 7 + .../ImageLoaderTransform.cs | 166 ++++++ .../ImagePixelExtractorTransform.cs | 541 ++++++++++++++++++ .../ImageResizerTransform.cs | 308 ++++++++++ src/Microsoft.ML.ImageAnalytics/ImageType.cs | 52 ++ .../Microsoft.ML.ImageAnalytics.csproj | 18 + 6 files changed, 1092 insertions(+) create mode 100644 src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs create mode 100644 src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs create mode 100644 src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs create mode 100644 src/Microsoft.ML.ImageAnalytics/ImageType.cs create mode 100644 src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln index 63a15c977c..8848ad9c7f 100644 --- a/Microsoft.ML.sln +++ b/Microsoft.ML.sln @@ -90,6 +90,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.LightGBM", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.Ensemble", "src\Microsoft.ML.Ensemble\Microsoft.ML.Ensemble.csproj", "{DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.ML.ImageAnalytics", "src\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.csproj", "{80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -204,6 +206,10 @@ Global {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}.Release|Any CPU.Build.0 = Release|Any CPU + {80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,6 +247,7 @@ Global {3DEB504D-7A07-48CE-91A2-8047461CB3D4} = {AED9C836-31E3-4F3F-8ABC-929555D3F3C4} {001F3B4E-FBE4-4001-AFD2-A6A989CD1C25} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27} = {09EADF06-BE25-4228-AB53-95AE3E15B530} + {80FBE6ED-F29B-4FDC-A2A7-137A5577DD00} = {09EADF06-BE25-4228-AB53-95AE3E15B530} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D} diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs new file mode 100644 index 0000000000..b9cbba027e --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Drawing; +using System.Text; +using Microsoft.ML.Runtime.ImageAnalytics; +using Microsoft.ML.Runtime; +using Microsoft.ML.Runtime.CommandLine; +using Microsoft.ML.Runtime.Data; +using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.Internal.Utilities; +using Microsoft.ML.Runtime.Model; + + +[assembly: LoadableClass(ImageLoaderTransform.Summary, typeof(ImageLoaderTransform), typeof(ImageLoaderTransform.Arguments), typeof(SignatureDataTransform), + ImageLoaderTransform.UserName, "ImageLoaderTransform", "ImageLoader")] + +[assembly: LoadableClass(ImageLoaderTransform.Summary, typeof(ImageLoaderTransform), null, typeof(SignatureLoadDataTransform), + ImageLoaderTransform.UserName, ImageLoaderTransform.LoaderSignature)] + +namespace Microsoft.ML.Runtime.Data +{ + // REVIEW: Rewrite as LambdaTransform to simplify. + public sealed class ImageLoaderTransform : OneToOneTransformBase + { + public sealed class Column : OneToOneColumn + { + public static Column Parse(string str) + { + Contracts.AssertNonEmpty(str); + + var res = new Column(); + if (res.TryParse(str)) + return res; + return null; + } + + public bool TryUnparse(StringBuilder sb) + { + Contracts.AssertValue(sb); + return TryUnparseCore(sb); + } + } + + public sealed class Arguments : TransformInputBase + { + [Argument(ArgumentType.Multiple | ArgumentType.Required, HelpText = "New column definition(s) (optional form: name:src)", + ShortName = "col", SortOrder = 1)] + public Column[] Column; + } + + internal const string Summary = "Loads an image from a file."; + internal const string UserName = "Image Loader Transform"; + public const string LoaderSignature = "ImageLoaderTransform"; + + private static VersionInfo GetVersionInfo() + { + return new VersionInfo( + modelSignature: "IMGLOADT", + verWrittenCur: 0x00010001, // Initial + verReadableCur: 0x00010001, + verWeCanReadBack: 0x00010001, + loaderSignature: LoaderSignature); + } + + private readonly ImageType _type; + + private const string RegistrationName = "ImageLoader"; + + /// + /// Public constructor corresponding to SignatureDataTransform. + /// + public ImageLoaderTransform(IHostEnvironment env, Arguments args, IDataView input) + : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, TestIsText) + { + Host.AssertNonEmpty(Infos); + Host.Assert(Infos.Length == Utils.Size(args.Column)); + _type = new ImageType(); + Metadata.Seal(); + } + + private ImageLoaderTransform(IHost host, ModelLoadContext ctx, IDataView input) + : base(host, ctx, input, TestIsText) + { + Host.AssertValue(ctx); + + // *** Binary format *** + // + _type = new ImageType(); + Metadata.Seal(); + } + + public static ImageLoaderTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) + { + Contracts.CheckValue(env, nameof(env)); + var h = env.Register(RegistrationName); + h.CheckValue(ctx, nameof(ctx)); + h.CheckValue(input, nameof(input)); + ctx.CheckAtModel(GetVersionInfo()); + return h.Apply("Loading Model", ch => new ImageLoaderTransform(h, ctx, input)); + } + + public override void Save(ModelSaveContext ctx) + { + Host.CheckValue(ctx, nameof(ctx)); + ctx.CheckAtModel(); + ctx.SetVersionInfo(GetVersionInfo()); + + // *** Binary format *** + // + SaveBase(ctx); + } + + protected override ColumnType GetColumnTypeCore(int iinfo) + { + Host.Check(0 <= iinfo && iinfo < Infos.Length); + return _type; + } + + protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) + { + Host.AssertValueOrNull(ch); + Host.AssertValue(input); + Host.Assert(0 <= iinfo && iinfo < Infos.Length); + disposer = null; + + var getSrc = GetSrcGetter(input, iinfo); + DvText src = default(DvText); + ValueGetter del = + (ref Bitmap dst) => + { + if (dst != null) + { + dst.Dispose(); + dst = null; + } + + getSrc(ref src); + + if (src.Length > 0) + { + // Catch exceptions and pass null through. Should also log failures... + try + { + dst = new Bitmap(filename: src.ToString(), useIcm: false); + } + catch (Exception e) + { + // REVIEW shonk: We catch everything since the documentation for new Bitmap(string) + // appears to be incorrect. When the file isn't found, it throws an ArgumentException, + // while the documentation says FileNotFoundException. Not sure what it will throw + // in other cases, like corrupted file, etc. + + // REVIEW shonk: Log failures. + ch.Info(e.Message); + ch.Info(e.StackTrace); + dst = null; + } + } + }; + return del; + } + } +} diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs new file mode 100644 index 0000000000..771052813d --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -0,0 +1,541 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Drawing; +using System.Text; +using Microsoft.ML.Runtime; +using Microsoft.ML.Runtime.CommandLine; +using Microsoft.ML.Runtime.Data; +using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.Internal.Utilities; +using Microsoft.ML.Runtime.Model; +using Microsoft.ML.Runtime.ImageAnalytics; + +[assembly: LoadableClass(ImagePixelExtractorTransform.Summary, typeof(ImagePixelExtractorTransform), typeof(ImagePixelExtractorTransform.Arguments), typeof(SignatureDataTransform), + ImagePixelExtractorTransform.UserName, "ImagePixelExtractorTransform", "ImagePixelExtractor")] + +[assembly: LoadableClass(ImagePixelExtractorTransform.Summary, typeof(ImagePixelExtractorTransform), null, typeof(SignatureLoadDataTransform), + ImagePixelExtractorTransform.UserName, ImagePixelExtractorTransform.LoaderSignature)] + +namespace Microsoft.ML.Runtime.Data +{ + // REVIEW coeseanu: Rewrite as LambdaTransform to simplify. + public sealed class ImagePixelExtractorTransform : OneToOneTransformBase + { + public class Column : OneToOneColumn + { + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use alpha channel", ShortName = "alpha")] + public bool? UseAlpha; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use red channel", ShortName = "red")] + public bool? UseRed; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use green channel", ShortName = "green")] + public bool? UseGreen; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use blue channel", ShortName = "blue")] + public bool? UseBlue; + + // REVIEW anro: Consider turning this into an enum that allows for pixel, line, or planar interleaving. + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to separate each channel or interleave in ARGB order", ShortName = "interleave")] + public bool? InterleaveArgb; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to convert to floating point", ShortName = "conv")] + public bool? Convert; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Offset (pre-scale)")] + public Single? Offset; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Scale factor")] + public Single? Scale; + + public static Column Parse(string str) + { + Contracts.AssertNonEmpty(str); + + var res = new Column(); + if (res.TryParse(str)) + return res; + return null; + } + + public bool TryUnparse(StringBuilder sb) + { + Contracts.AssertValue(sb); + if (UseAlpha != null || UseRed != null || UseGreen != null || UseBlue != null || Convert != null || + Offset != null || Scale != null || InterleaveArgb != null) + { + return false; + } + return TryUnparseCore(sb); + } + } + + public class Arguments : TransformInputBase + { + [Argument(ArgumentType.Multiple | ArgumentType.Required, HelpText = "New column definition(s) (optional form: name:src)", ShortName = "col", SortOrder = 1)] + public Column[] Column; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use alpha channel", ShortName = "alpha")] + public bool UseAlpha = false; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use red channel", ShortName = "red")] + public bool UseRed = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use green channel", ShortName = "green")] + public bool UseGreen = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use blue channel", ShortName = "blue")] + public bool UseBlue = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to separate each channel or interleave in ARGB order", ShortName = "interleave")] + public bool InterleaveArgb = false; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to convert to floating point", ShortName = "conv")] + public bool Convert = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Offset (pre-scale)")] + public Single? Offset; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Scale factor")] + public Single? Scale; + } + + /// + /// Which color channels are extracted. Note that these values are serialized so should not be modified. + /// + [Flags] + private enum ColorBits : byte + { + Alpha = 0x01, + Red = 0x02, + Green = 0x04, + Blue = 0x08, + + All = Alpha | Red | Green | Blue + } + + private sealed class ColInfoEx + { + public readonly ColorBits Colors; + public readonly byte Planes; + + public readonly bool Convert; + public readonly Single Offset; + public readonly Single Scale; + public readonly bool Interleave; + + public bool Alpha { get { return (Colors & ColorBits.Alpha) != 0; } } + public bool Red { get { return (Colors & ColorBits.Red) != 0; } } + public bool Green { get { return (Colors & ColorBits.Green) != 0; } } + public bool Blue { get { return (Colors & ColorBits.Blue) != 0; } } + + public ColInfoEx(Column item, Arguments args) + { + if (item.UseAlpha ?? args.UseAlpha) { Colors |= ColorBits.Alpha; Planes++; } + if (item.UseRed ?? args.UseRed) { Colors |= ColorBits.Red; Planes++; } + if (item.UseGreen ?? args.UseGreen) { Colors |= ColorBits.Green; Planes++; } + if (item.UseBlue ?? args.UseBlue) { Colors |= ColorBits.Blue; Planes++; } + Contracts.CheckUserArg(Planes > 0, nameof(item.UseRed), "Need to use at least one color plane"); + + Interleave = item.InterleaveArgb ?? args.InterleaveArgb; + + Convert = item.Convert ?? args.Convert; + if (!Convert) + { + Offset = 0; + Scale = 1; + } + else + { + Offset = item.Offset ?? args.Offset ?? 0; + Scale = item.Scale ?? args.Scale ?? 1; + Contracts.CheckUserArg(FloatUtils.IsFinite(Offset), nameof(item.Offset)); + Contracts.CheckUserArg(FloatUtils.IsFiniteNonZero(Scale), nameof(item.Scale)); + } + } + + public ColInfoEx(ModelLoadContext ctx) + { + Contracts.AssertValue(ctx); + + // *** Binary format *** + // byte: colors + // byte: convert + // Float: offset + // Float: scale + // byte: separateChannels + Colors = (ColorBits)ctx.Reader.ReadByte(); + Contracts.CheckDecode(Colors != 0); + Contracts.CheckDecode((Colors & ColorBits.All) == Colors); + + // Count the planes. + int planes = (int)Colors; + planes = (planes & 0x05) + ((planes >> 1) & 0x05); + planes = (planes & 0x03) + ((planes >> 2) & 0x03); + Planes = (byte)planes; + Contracts.Assert(0 < Planes & Planes <= 4); + + Convert = ctx.Reader.ReadBoolByte(); + Offset = ctx.Reader.ReadFloat(); + Contracts.CheckDecode(FloatUtils.IsFinite(Offset)); + Scale = ctx.Reader.ReadFloat(); + Contracts.CheckDecode(FloatUtils.IsFiniteNonZero(Scale)); + Contracts.CheckDecode(Convert || Offset == 0 && Scale == 1); + Interleave = ctx.Reader.ReadBoolByte(); + } + + public void Save(ModelSaveContext ctx) + { + Contracts.AssertValue(ctx); + +#if DEBUG + // This code is used in deserialization - assert that it matches what we computed above. + int planes = (int)Colors; + planes = (planes & 0x05) + ((planes >> 1) & 0x05); + planes = (planes & 0x03) + ((planes >> 2) & 0x03); + Contracts.Assert(planes == Planes); +#endif + + // *** Binary format *** + // byte: colors + // byte: convert + // Float: offset + // Float: scale + // byte: separateChannels + Contracts.Assert(Colors != 0); + Contracts.Assert((Colors & ColorBits.All) == Colors); + ctx.Writer.Write((byte)Colors); + ctx.Writer.WriteBoolByte(Convert); + Contracts.Assert(FloatUtils.IsFinite(Offset)); + ctx.Writer.Write(Offset); + Contracts.Assert(FloatUtils.IsFiniteNonZero(Scale)); + Contracts.Assert(Convert || Offset == 0 && Scale == 1); + ctx.Writer.Write(Scale); + ctx.Writer.WriteBoolByte(Interleave); + } + } + + internal const string Summary = "Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point."; + internal const string UserName = "Image Pixel Extractor Transform"; + public const string LoaderSignature = "ImagePixelExtractor"; + private static VersionInfo GetVersionInfo() + { + return new VersionInfo( + modelSignature: "IMGPXEXT", + verWrittenCur: 0x00010001, // Initial + verReadableCur: 0x00010001, + verWeCanReadBack: 0x00010001, + loaderSignature: LoaderSignature); + } + + private const string RegistrationName = "ImagePixelExtractor"; + + private readonly ColInfoEx[] _exes; + private readonly VectorType[] _types; + + /// + /// Public constructor corresponding to SignatureDataTransform. + /// + public ImagePixelExtractorTransform(IHostEnvironment env, Arguments args, IDataView input) + : base(env, RegistrationName, Contracts.CheckRef(args, nameof(args)).Column, input, + t => t is ImageType ? null : "Expected Image type") + { + Host.AssertNonEmpty(Infos); + Host.Assert(Infos.Length == Utils.Size(args.Column)); + + _exes = new ColInfoEx[Infos.Length]; + for (int i = 0; i < _exes.Length; i++) + { + var item = args.Column[i]; + _exes[i] = new ColInfoEx(item, args); + } + + _types = ConstructTypes(true); + } + + private ImagePixelExtractorTransform(IHost host, ModelLoadContext ctx, IDataView input) + : base(host, ctx, input, t => t is ImageType ? null : "Expected Image type") + { + Host.AssertValue(ctx); + + // *** Binary format *** + // + // + // foreach added column + // ColInfoEx + Host.AssertNonEmpty(Infos); + _exes = new ColInfoEx[Infos.Length]; + for (int i = 0; i < _exes.Length; i++) + _exes[i] = new ColInfoEx(ctx); + + _types = ConstructTypes(false); + } + + public static ImagePixelExtractorTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) + { + Contracts.CheckValue(env, nameof(env)); + var h = env.Register(RegistrationName); + h.CheckValue(ctx, nameof(ctx)); + h.CheckValue(input, nameof(input)); + ctx.CheckAtModel(GetVersionInfo()); + + return h.Apply("Loading Model", + ch => + { + // *** Binary format *** + // int: sizeof(Float) + // + int cbFloat = ctx.Reader.ReadInt32(); + ch.CheckDecode(cbFloat == sizeof(Single)); + return new ImagePixelExtractorTransform(h, ctx, input); + }); + } + + public override void Save(ModelSaveContext ctx) + { + Host.CheckValue(ctx, nameof(ctx)); + ctx.CheckAtModel(); + ctx.SetVersionInfo(GetVersionInfo()); + + // *** Binary format *** + // int: sizeof(Float) + // + // foreach added column + // ColInfoEx + ctx.Writer.Write(sizeof(Single)); + SaveBase(ctx); + + Host.Assert(_exes.Length == Infos.Length); + for (int i = 0; i < _exes.Length; i++) + _exes[i].Save(ctx); + } + + private VectorType[] ConstructTypes(bool user) + { + var types = new VectorType[Infos.Length]; + for (int i = 0; i < Infos.Length; i++) + { + var info = Infos[i]; + var ex = _exes[i]; + Host.Assert(ex.Planes > 0); + + var type = Source.Schema.GetColumnType(info.Source) as ImageType; + Host.Assert(type != null); + if (type.Height <= 0 || type.Width <= 0) + { + // REVIEW shonk: Could support this case by making the destination column be variable sized. + // However, there's no mechanism to communicate the dimensions through with the pixel data. + string name = Source.Schema.GetColumnName(info.Source); + throw user ? + Host.ExceptUserArg(nameof(Arguments.Column), "Column '{0}' does not have known size", name) : + Host.Except("Column '{0}' does not have known size", name); + } + int height = type.Height; + int width = type.Width; + Host.Assert(height > 0); + Host.Assert(width > 0); + Host.Assert((long)height * width <= int.MaxValue / 4); + + if (ex.Interleave) + types[i] = new VectorType(ex.Convert ? NumberType.Float : NumberType.U1, height, width, ex.Planes); + else + types[i] = new VectorType(ex.Convert ? NumberType.Float : NumberType.U1, ex.Planes, height, width); + } + Metadata.Seal(); + return types; + } + + protected override ColumnType GetColumnTypeCore(int iinfo) + { + Host.Assert(0 <= iinfo & iinfo < Infos.Length); + return _types[iinfo]; + } + + protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) + { + Host.AssertValueOrNull(ch); + Host.AssertValue(input); + Host.Assert(0 <= iinfo && iinfo < Infos.Length); + + if (_exes[iinfo].Convert) + return GetGetterCore(input, iinfo, out disposer); + return GetGetterCore(input, iinfo, out disposer); + } + + private ValueGetter> GetGetterCore(IRow input, int iinfo, out Action disposer) + { + var type = _types[iinfo]; + Host.Assert(type.DimCount == 3); + + var ex = _exes[iinfo]; + + int planes = ex.Interleave ? type.GetDim(2) : type.GetDim(0); + int height = ex.Interleave ? type.GetDim(0) : type.GetDim(1); + int width = ex.Interleave ? type.GetDim(1) : type.GetDim(2); + + int size = type.ValueCount; + Host.Assert(size > 0); + Host.Assert(size == planes * height * width); + int cpix = height * width; + + var getSrc = GetSrcGetter(input, iinfo); + var src = default(Bitmap); + + disposer = + () => + { + if (src != null) + { + src.Dispose(); + src = null; + } + }; + + return + (ref VBuffer dst) => + { + getSrc(ref src); + Contracts.AssertValueOrNull(src); + + if (src == null) + { + dst = new VBuffer(size, 0, dst.Values, dst.Indices); + return; + } + + Host.Check(src.PixelFormat== System.Drawing.Imaging.PixelFormat.Canonical); + Host.Check(src.Height == height && src.Width == width); + + var values = dst.Values; + if (Utils.Size(values) < size) + values = new TValue[size]; + + Single offset = ex.Offset; + Single scale = ex.Scale; + Host.Assert(scale != 0); + + var vf = values as Single[]; + var vb = values as byte[]; + Host.Assert(vf != null || vb != null); + bool needScale = offset != 0 || scale != 1; + Host.Assert(!needScale || vf != null); + + bool a = ex.Alpha; + bool r = ex.Red; + bool g = ex.Green; + bool b = ex.Blue; + + int h = height; + int w = width; + + if (ex.Interleave) + { + int idst = 0; + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; x++) + { + var pb = src.GetPixel(y, x); + if (vb != null) + { + if (a) { vb[idst++] = (byte)0; } + if (r) { vb[idst++] = pb.R; } + if (g) { vb[idst++] = pb.G; } + if (b) { vb[idst++] = pb.B; } + } + else if (!needScale) + { + if (a) { vf[idst++] = 0.0f; } + if (r) { vf[idst++] = pb.R; } + if (g) { vf[idst++] = pb.G; } + if (b) { vf[idst++] = pb.B; } + } + else + { + if (a) { vf[idst++] = 0.0f; } + if (r) { vf[idst++] = (pb.R - offset) * scale; } + if (g) { vf[idst++] = (pb.B - offset) * scale; } + if (b) { vf[idst++] = (pb.G - offset) * scale; } + } + } + } + + Host.Assert(idst == size); + } + else + { + int idstMin = 0; + if (ex.Alpha) + { + // The image only has rgb but we need to supply alpha as well, so fake it up, + // assuming that it is 0xFF. + if (vf != null) + { + Single v = (0xFF - offset) * scale; + for (int i = 0; i < cpix; i++) + vf[i] = v; + } + else + { + for (int i = 0; i < cpix; i++) + vb[i] = 0xFF; + } + idstMin = cpix; + + // We've preprocessed alpha, avoid it in the + // scan operation below. + a = false; + } + + for (int y = 0; y < h; ++y) + { + int idstBase = idstMin + y * w; + + // Note that the bytes are in order BGR[A]. We arrange the layers in order ARGB. + if (vb != null) + { + for (int x = 0; x < w; x++, idstBase++) + { + var pb = src.GetPixel(y, x); + int idst = idstBase; + if (a) { vb[idst] = pb.A; idst += cpix; } + if (r) { vb[idst] = pb.R; idst += cpix; } + if (g) { vb[idst] = pb.G; idst += cpix; } + if (b) { vb[idst] = pb.B; idst += cpix; } + } + } + else if (!needScale) + { + for (int x = 0; x < w; x++, idstBase++) + { + var pb = src.GetPixel(y, x); + int idst = idstBase; + if (a) { vf[idst] = pb.A; idst += cpix; } + if (r) { vf[idst] = pb.R; idst += cpix; } + if (g) { vf[idst] = pb.G; idst += cpix; } + if (b) { vf[idst] = pb.B; idst += cpix; } + } + } + else + { + for (int x = 0; x < w; x++, idstBase++) + { + var pb = src.GetPixel(y, x); + int idst = idstBase; + if (a) { vf[idst] = (pb.A - offset) * scale; idst += cpix; } + if (r) { vf[idst] = (pb.R - offset) * scale; idst += cpix; } + if (g) { vf[idst] = (pb.G - offset) * scale; idst += cpix; } + if (b) { vf[idst] = (pb.B - offset) * scale; idst += cpix; } + } + } + } + } + + dst = new VBuffer(size, values, dst.Indices); + }; + } + } +} diff --git a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs new file mode 100644 index 0000000000..c43d936f58 --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs @@ -0,0 +1,308 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Drawing; +using System.Text; +using Microsoft.ML.Runtime; +using Microsoft.ML.Runtime.CommandLine; +using Microsoft.ML.Runtime.Data; +using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.Internal.Internallearn; +using Microsoft.ML.Runtime.Internal.Utilities; +using Microsoft.ML.Runtime.Model; +using Microsoft.ML.Runtime.ImageAnalytics; + + +[assembly: LoadableClass(ImageResizerTransform.Summary, typeof(ImageResizerTransform), typeof(ImageResizerTransform.Arguments), typeof(SignatureDataTransform), + ImageResizerTransform.UserName, "ImageResizerTransform", "ImageResizer")] + +[assembly: LoadableClass(ImageResizerTransform.Summary, typeof(ImageResizerTransform), null, typeof(SignatureLoadDataTransform), + ImageResizerTransform.UserName, ImageResizerTransform.LoaderSignature)] + +namespace Microsoft.ML.Runtime.Data +{ + // REVIEW coeseanu: Rewrite as LambdaTransform to simplify. + public sealed class ImageResizerTransform : OneToOneTransformBase + { + public enum ResizingKind : byte + { + [TGUI(Label = "Isotropic with Padding")] + IsoPad = 0, + + [TGUI(Label = "Isotropic with Cropping")] + IsoCrop = 1, + + [TGUI(Label = "Anisotropic")] + Aniso = 2, + } + + public sealed class Column : OneToOneColumn + { + [Argument(ArgumentType.AtMostOnce, HelpText = "Width of the resized image", ShortName = "width")] + public int? ImageWidth; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Height of the resized image", ShortName = "height")] + public int? ImageHeight; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Resizing method", ShortName = "scale")] + public ResizingKind? Resizing; + + public static Column Parse(string str) + { + Contracts.AssertNonEmpty(str); + + var res = new Column(); + if (res.TryParse(str)) + return res; + return null; + } + + public bool TryUnparse(StringBuilder sb) + { + Contracts.AssertValue(sb); + if (ImageWidth != null || ImageHeight != null || Resizing != null) + return false; + return TryUnparseCore(sb); + } + } + + public class Arguments : TransformInputBase + { + [Argument(ArgumentType.Multiple | ArgumentType.Required, HelpText = "New column definition(s) (optional form: name:src)", ShortName = "col", SortOrder = 1)] + public Column[] Column; + + [Argument(ArgumentType.Required, HelpText = "Resized width of the image", ShortName = "width")] + public int ImageWidth; + + [Argument(ArgumentType.Required, HelpText = "Resized height of the image", ShortName = "height")] + public int ImageHeight; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Resizing method", ShortName = "scale")] + public ResizingKind Resizing = ResizingKind.IsoCrop; + } + + /// + /// Extra information for each column (in addition to ColumnInfo). + /// + private sealed class ColInfoEx + { + public readonly int Width; + public readonly int Height; + public readonly ResizingKind Scale; + public readonly ColumnType Type; + + public ColInfoEx(int width, int height, ResizingKind scale) + { + Contracts.CheckUserArg(width > 0, nameof(Column.ImageWidth)); + Contracts.CheckUserArg(height > 0, nameof(Column.ImageHeight)); + Contracts.CheckUserArg(Enum.IsDefined(typeof(ResizingKind), scale), nameof(Column.Resizing)); + + Width = width; + Height = height; + Scale = scale; + Type = new ImageType(Height, Width); + } + } + + internal const string Summary = "Scales an image to specified dimensions using one of the three scale types: isotropic with padding, " + + "isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image."; + + internal const string UserName = "Image Resizer Transform"; + public const string LoaderSignature = "ImageScalerTransform"; + private static VersionInfo GetVersionInfo() + { + return new VersionInfo( + modelSignature: "IMGSCALF", + verWrittenCur: 0x00010001, // Initial + verReadableCur: 0x00010001, + verWeCanReadBack: 0x00010001, + loaderSignature: LoaderSignature); + } + + private const string RegistrationName = "ImageScaler"; + + // This is parallel to Infos. + private readonly ColInfoEx[] _exes; + + /// + /// Public constructor corresponding to SignatureDataTransform. + /// + public ImageResizerTransform(IHostEnvironment env, Arguments args, IDataView input) + : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") + { + Host.AssertNonEmpty(Infos); + Host.Assert(Infos.Length == Utils.Size(args.Column)); + + _exes = new ColInfoEx[Infos.Length]; + for (int i = 0; i < _exes.Length; i++) + { + var item = args.Column[i]; + _exes[i] = new ColInfoEx( + item.ImageWidth ?? args.ImageWidth, + item.ImageHeight ?? args.ImageHeight, + item.Resizing ?? args.Resizing); + } + Metadata.Seal(); + } + + private ImageResizerTransform(IHost host, ModelLoadContext ctx, IDataView input) + : base(host, ctx, input, t => t is ImageType ? null : "Expected Image type") + { + Host.AssertValue(ctx); + + // *** Binary format *** + // + // + // for each added column + // int: width + // int: height + // byte: scaling kind + Host.AssertNonEmpty(Infos); + + _exes = new ColInfoEx[Infos.Length]; + for (int i = 0; i < _exes.Length; i++) + { + int width = ctx.Reader.ReadInt32(); + Host.CheckDecode(width > 0); + int height = ctx.Reader.ReadInt32(); + Host.CheckDecode(height > 0); + var scale = (ResizingKind)ctx.Reader.ReadByte(); + Host.CheckDecode(Enum.IsDefined(typeof(ResizingKind), scale)); + _exes[i] = new ColInfoEx(width, height, scale); + } + Metadata.Seal(); + } + + public static ImageResizerTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) + { + Contracts.CheckValue(env, nameof(env)); + var h = env.Register(RegistrationName); + h.CheckValue(ctx, nameof(ctx)); + h.CheckValue(input, nameof(input)); + ctx.CheckAtModel(GetVersionInfo()); + return h.Apply("Loading Model", + ch => + { + // *** Binary format *** + // int: sizeof(Float) + // + int cbFloat = ctx.Reader.ReadInt32(); + ch.CheckDecode(cbFloat == sizeof(Single)); + return new ImageResizerTransform(h, ctx, input); + }); + } + + public override void Save(ModelSaveContext ctx) + { + Host.CheckValue(ctx, nameof(ctx)); + ctx.CheckAtModel(); + ctx.SetVersionInfo(GetVersionInfo()); + + // *** Binary format *** + // int: sizeof(Float) + // + // for each added column + // int: width + // int: height + // byte: scaling kind + ctx.Writer.Write(sizeof(Single)); + SaveBase(ctx); + + Host.Assert(_exes.Length == Infos.Length); + for (int i = 0; i < _exes.Length; i++) + { + var ex = _exes[i]; + ctx.Writer.Write(ex.Width); + ctx.Writer.Write(ex.Height); + Host.Assert((ResizingKind)(byte)ex.Scale == ex.Scale); + ctx.Writer.Write((byte)ex.Scale); + } + } + + protected override ColumnType GetColumnTypeCore(int iinfo) + { + Host.Check(0 <= iinfo && iinfo < Infos.Length); + return _exes[iinfo].Type; + } + + protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) + { + Host.AssertValue(ch, "ch"); + Host.AssertValue(input); + Host.Assert(0 <= iinfo && iinfo < Infos.Length); + + var src = default(Bitmap); + var getSrc = GetSrcGetter(input, iinfo); + var ex = _exes[iinfo]; + + disposer = + () => + { + if (src != null) + { + src.Dispose(); + src = null; + } + }; + + ValueGetter del = + (ref Image dst) => + { + if (dst != null) + dst.Dispose(); + + getSrc(ref src); + if (src == null || src.Height <= 0 || src.Width <= 0) + return; + + int x = 0; + int y = 0; + int w = ex.Width; + int h = ex.Height; + bool pad = ex.Scale == ResizingKind.IsoPad; + if (ex.Scale == ResizingKind.IsoPad || ex.Scale == ResizingKind.IsoCrop) + { + long wh = (long)src.Height * ex.Height; + long hw = (long)src.Width * ex.Width; + + if (pad == (wh > hw)) + { + h = checked((int)(hw / src.Height)); + y = (ex.Height - h) / 2; + } + else + { + w = checked((int)(wh / src.Width)); + x = (ex.Width - w) / 2; + } + + // If we're not padding, the rectangle should fill everything. + Host.Assert(pad || x <= 0 && y <= 0 && + h >= ex.Height && w >= ex.Width); + } + // Draw the image. + var srcRectangle = new Rectangle(0, 0, src.Width, src.Height); + if (pad) + { + using (var g = Graphics.FromImage(src)) + { + g.DrawImage(dst, srcRectangle, new Rectangle(x, y, w, h), GraphicsUnit.Pixel); + } + } + else + { + using (var g = Graphics.FromImage(src)) + { + g.DrawImage(dst, srcRectangle, new Rectangle(-x, -y, ex.Width, ex.Height), GraphicsUnit.Pixel); + } + } + + Host.Assert(dst.Width == ex.Width && dst.Height == ex.Height); + }; + + return del; + } + } +} diff --git a/src/Microsoft.ML.ImageAnalytics/ImageType.cs b/src/Microsoft.ML.ImageAnalytics/ImageType.cs new file mode 100644 index 0000000000..75826ca435 --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/ImageType.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Drawing; +using Microsoft.ML.Runtime.Data; + +namespace Microsoft.ML.Runtime.ImageAnalytics +{ + public sealed class ImageType: StructuredType + { + public readonly int Height; + public readonly int Width; + public ImageType(int height, int width) + : base(typeof(Bitmap)) + { + Contracts.CheckParam(height > 0, nameof(height)); + Contracts.CheckParam(width > 0, nameof(width)); + Contracts.CheckParam((long)height * width <= int.MaxValue / 4, nameof(height), "height * width is too large"); + Height = height; + Width = width; + } + + public ImageType() + : base(typeof(Image)) + { + } + + public override bool Equals(ColumnType other) + { + if (other == this) + return true; + var tmp = other as ImageType; + if (tmp == null) + return false; + if (Height != tmp.Height) + return false; + if (Width != tmp.Width) + return false; + return true; + } + + public override string ToString() + { + if (Height == 0 && Width == 0) + return "Picture"; + return string.Format("Picture<{0}, {1}>", Height, Width); + } + } + + +} diff --git a/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj b/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj new file mode 100644 index 0000000000..81011c5276 --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + Microsoft.ML.Runtime.ImageAnalytics + Microsoft.ML.Runtime.ImageAnalytics + + + + + + + + + + + + From ffa7f12d362eee1b35ffa18bba53eb4ff832bc19 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Wed, 11 Jul 2018 18:51:08 -0700 Subject: [PATCH 02/16] clean code add VectorToImage transform add first test --- .../ImageLoaderTransform.cs | 18 +- .../ImagePixelExtractorTransform.cs | 8 +- .../ImageResizerTransform.cs | 137 ++++-- .../VectorToImageTransform.cs | 413 ++++++++++++++++++ test/Microsoft.ML.Tests/ImagesTests.cs | 77 ++++ .../Microsoft.ML.Tests.csproj | 1 + test/data/images/banana.jpg | Bin 0 -> 30719 bytes test/data/images/hotdog.jpg | Bin 0 -> 64523 bytes test/data/images/images.tsv | 3 + test/data/images/tomato.jpg | Bin 0 -> 46523 bytes 10 files changed, 612 insertions(+), 45 deletions(-) create mode 100644 src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs create mode 100644 test/Microsoft.ML.Tests/ImagesTests.cs create mode 100644 test/data/images/banana.jpg create mode 100644 test/data/images/hotdog.jpg create mode 100644 test/data/images/images.tsv create mode 100644 test/data/images/tomato.jpg diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index b9cbba027e..6ba6639d9e 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -4,6 +4,7 @@ using System; using System.Drawing; +using System.IO; using System.Text; using Microsoft.ML.Runtime.ImageAnalytics; using Microsoft.ML.Runtime; @@ -13,7 +14,6 @@ using Microsoft.ML.Runtime.Internal.Utilities; using Microsoft.ML.Runtime.Model; - [assembly: LoadableClass(ImageLoaderTransform.Summary, typeof(ImageLoaderTransform), typeof(ImageLoaderTransform.Arguments), typeof(SignatureDataTransform), ImageLoaderTransform.UserName, "ImageLoaderTransform", "ImageLoader")] @@ -49,6 +49,9 @@ public sealed class Arguments : TransformInputBase [Argument(ArgumentType.Multiple | ArgumentType.Required, HelpText = "New column definition(s) (optional form: name:src)", ShortName = "col", SortOrder = 1)] public Column[] Column; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Image folder", ShortName = "folder")] + public string ImageFolder; } internal const string Summary = "Loads an image from a file."; @@ -66,6 +69,7 @@ private static VersionInfo GetVersionInfo() } private readonly ImageType _type; + private string _imageFolder; private const string RegistrationName = "ImageLoader"; @@ -76,6 +80,7 @@ public ImageLoaderTransform(IHostEnvironment env, Arguments args, IDataView inpu : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, TestIsText) { Host.AssertNonEmpty(Infos); + _imageFolder = args.ImageFolder; Host.Assert(Infos.Length == Utils.Size(args.Column)); _type = new ImageType(); Metadata.Seal(); @@ -88,6 +93,7 @@ private ImageLoaderTransform(IHost host, ModelLoadContext ctx, IDataView input) // *** Binary format *** // + _imageFolder = ctx.Reader.ReadString(); _type = new ImageType(); Metadata.Seal(); } @@ -110,6 +116,7 @@ public override void Save(ModelSaveContext ctx) // *** Binary format *** // + ctx.Writer.Write(_imageFolder); SaveBase(ctx); } @@ -144,16 +151,19 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou // Catch exceptions and pass null through. Should also log failures... try { - dst = new Bitmap(filename: src.ToString(), useIcm: false); + string path = src.ToString(); + if (!string.IsNullOrWhiteSpace(_imageFolder)) + path = Path.Combine(_imageFolder, path); + dst = new Bitmap(filename: path, useIcm: false); } catch (Exception e) { - // REVIEW shonk: We catch everything since the documentation for new Bitmap(string) + // REVIEW: We catch everything since the documentation for new Bitmap(string) // appears to be incorrect. When the file isn't found, it throws an ArgumentException, // while the documentation says FileNotFoundException. Not sure what it will throw // in other cases, like corrupted file, etc. - // REVIEW shonk: Log failures. + // REVIEW : Log failures. ch.Info(e.Message); ch.Info(e.StackTrace); dst = null; diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs index 771052813d..2f82350afe 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -21,7 +21,7 @@ namespace Microsoft.ML.Runtime.Data { - // REVIEW coeseanu: Rewrite as LambdaTransform to simplify. + // REVIEW: Rewrite as LambdaTransform to simplify. public sealed class ImagePixelExtractorTransform : OneToOneTransformBase { public class Column : OneToOneColumn @@ -38,7 +38,7 @@ public class Column : OneToOneColumn [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use blue channel", ShortName = "blue")] public bool? UseBlue; - // REVIEW anro: Consider turning this into an enum that allows for pixel, line, or planar interleaving. + // REVIEW: Consider turning this into an enum that allows for pixel, line, or planar interleaving. [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to separate each channel or interleave in ARGB order", ShortName = "interleave")] public bool? InterleaveArgb; @@ -326,7 +326,7 @@ private VectorType[] ConstructTypes(bool user) Host.Assert(type != null); if (type.Height <= 0 || type.Width <= 0) { - // REVIEW shonk: Could support this case by making the destination column be variable sized. + // REVIEW: Could support this case by making the destination column be variable sized. // However, there's no mechanism to communicate the dimensions through with the pixel data. string name = Source.Schema.GetColumnName(info.Source); throw user ? @@ -406,7 +406,7 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo return; } - Host.Check(src.PixelFormat== System.Drawing.Imaging.PixelFormat.Canonical); + Host.Check(src.PixelFormat== System.Drawing.Imaging.PixelFormat.Format32bppArgb); Host.Check(src.Height == height && src.Width == width); var values = dst.Values; diff --git a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs index c43d936f58..c6e5f9f079 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs @@ -23,7 +23,7 @@ namespace Microsoft.ML.Runtime.Data { - // REVIEW coeseanu: Rewrite as LambdaTransform to simplify. + // REVIEW: Rewrite as LambdaTransform to simplify. public sealed class ImageResizerTransform : OneToOneTransformBase { public enum ResizingKind : byte @@ -32,10 +32,16 @@ public enum ResizingKind : byte IsoPad = 0, [TGUI(Label = "Isotropic with Cropping")] - IsoCrop = 1, + IsoCrop = 1 + } - [TGUI(Label = "Anisotropic")] - Aniso = 2, + public enum Anchor : byte + { + Right = 0, + Left = 1, + Top = 2, + Bottom = 3, + Center = 4 } public sealed class Column : OneToOneColumn @@ -49,6 +55,9 @@ public sealed class Column : OneToOneColumn [Argument(ArgumentType.AtMostOnce, HelpText = "Resizing method", ShortName = "scale")] public ResizingKind? Resizing; + [Argument(ArgumentType.AtMostOnce, HelpText = "Anchor for cropping", ShortName = "anchor")] + public Anchor? CropAnchor; + public static Column Parse(string str) { Contracts.AssertNonEmpty(str); @@ -62,7 +71,7 @@ public static Column Parse(string str) public bool TryUnparse(StringBuilder sb) { Contracts.AssertValue(sb); - if (ImageWidth != null || ImageHeight != null || Resizing != null) + if (ImageWidth != null || ImageHeight != null || Resizing != null || CropAnchor != null) return false; return TryUnparseCore(sb); } @@ -81,6 +90,9 @@ public class Arguments : TransformInputBase [Argument(ArgumentType.AtMostOnce, HelpText = "Resizing method", ShortName = "scale")] public ResizingKind Resizing = ResizingKind.IsoCrop; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Anchor for cropping", ShortName = "anchor")] + public Anchor CropAnchor = Anchor.Center; } /// @@ -91,17 +103,20 @@ private sealed class ColInfoEx public readonly int Width; public readonly int Height; public readonly ResizingKind Scale; + public readonly Anchor Anchor; public readonly ColumnType Type; - public ColInfoEx(int width, int height, ResizingKind scale) + public ColInfoEx(int width, int height, ResizingKind scale, Anchor anchor) { Contracts.CheckUserArg(width > 0, nameof(Column.ImageWidth)); Contracts.CheckUserArg(height > 0, nameof(Column.ImageHeight)); Contracts.CheckUserArg(Enum.IsDefined(typeof(ResizingKind), scale), nameof(Column.Resizing)); + Contracts.CheckUserArg(Enum.IsDefined(typeof(Anchor), anchor), nameof(Column.CropAnchor)); Width = width; Height = height; Scale = scale; + Anchor = anchor; Type = new ImageType(Height, Width); } } @@ -142,7 +157,8 @@ public ImageResizerTransform(IHostEnvironment env, Arguments args, IDataView inp _exes[i] = new ColInfoEx( item.ImageWidth ?? args.ImageWidth, item.ImageHeight ?? args.ImageHeight, - item.Resizing ?? args.Resizing); + item.Resizing ?? args.Resizing, + item.CropAnchor ?? args.CropAnchor); } Metadata.Seal(); } @@ -170,7 +186,9 @@ private ImageResizerTransform(IHost host, ModelLoadContext ctx, IDataView input) Host.CheckDecode(height > 0); var scale = (ResizingKind)ctx.Reader.ReadByte(); Host.CheckDecode(Enum.IsDefined(typeof(ResizingKind), scale)); - _exes[i] = new ColInfoEx(width, height, scale); + var anchor = (Anchor)ctx.Reader.ReadByte(); + Host.CheckDecode(Enum.IsDefined(typeof(Anchor), anchor)); + _exes[i] = new ColInfoEx(width, height, scale, anchor); } Metadata.Seal(); } @@ -218,6 +236,8 @@ public override void Save(ModelSaveContext ctx) ctx.Writer.Write(ex.Height); Host.Assert((ResizingKind)(byte)ex.Scale == ex.Scale); ctx.Writer.Write((byte)ex.Scale); + Host.Assert((Anchor)(byte)ex.Anchor == ex.Anchor); + ctx.Writer.Write((byte)ex.Anchor); } } @@ -247,8 +267,8 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou } }; - ValueGetter del = - (ref Image dst) => + ValueGetter del = + (ref Bitmap dst) => { if (dst != null) dst.Dispose(); @@ -256,49 +276,92 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou getSrc(ref src); if (src == null || src.Height <= 0 || src.Width <= 0) return; - - int x = 0; - int y = 0; - int w = ex.Width; - int h = ex.Height; - bool pad = ex.Scale == ResizingKind.IsoPad; - if (ex.Scale == ResizingKind.IsoPad || ex.Scale == ResizingKind.IsoCrop) + if (src.Height == ex.Height && src.Width == ex.Width) { - long wh = (long)src.Height * ex.Height; - long hw = (long)src.Width * ex.Width; + dst = src; + return; + } - if (pad == (wh > hw)) + int sourceWidth = src.Width; + int sourceHeight = src.Height; + int sourceX = 0; + int sourceY = 0; + int destX = 0; + int destY = 0; + int destWidth = 0; + int destHeight = 0; + float aspect = 0; + float widthAspect = 0; + float heightAspect = 0; + + widthAspect = (float)ex.Width / sourceWidth; + heightAspect = (float)ex.Height / sourceHeight; + + if (ex.Scale == ResizingKind.IsoPad) + { + widthAspect = (float)ex.Width / sourceWidth; + heightAspect = (float)ex.Height / sourceHeight; + if (heightAspect < widthAspect) { - h = checked((int)(hw / src.Height)); - y = (ex.Height - h) / 2; + aspect = heightAspect; + destX = (int)((ex.Width - (sourceWidth * aspect)) / 2); } else { - w = checked((int)(wh / src.Width)); - x = (ex.Width - w) / 2; + aspect = widthAspect; + destY = (int)((ex.Height - (sourceHeight * aspect)) / 2); } - // If we're not padding, the rectangle should fill everything. - Host.Assert(pad || x <= 0 && y <= 0 && - h >= ex.Height && w >= ex.Width); + destWidth = (int)(sourceWidth * aspect); + destHeight = (int)(sourceHeight * aspect); } - // Draw the image. - var srcRectangle = new Rectangle(0, 0, src.Width, src.Height); - if (pad) + else { - using (var g = Graphics.FromImage(src)) + if (heightAspect < widthAspect) { - g.DrawImage(dst, srcRectangle, new Rectangle(x, y, w, h), GraphicsUnit.Pixel); + aspect = widthAspect; + switch (ex.Anchor) + { + case Anchor.Top: + destY = 0; + break; + case Anchor.Bottom: + destY = (int)(ex.Height - (sourceHeight * aspect)); + break; + default: + destY = (int)((ex.Height - (sourceHeight * aspect)) / 2); + break; + } } - } - else - { - using (var g = Graphics.FromImage(src)) + else { - g.DrawImage(dst, srcRectangle, new Rectangle(-x, -y, ex.Width, ex.Height), GraphicsUnit.Pixel); + aspect = heightAspect; + switch (ex.Anchor) + { + case Anchor.Left: + destX = 0; + break; + case Anchor.Right: + destX = (int)(ex.Width - (sourceWidth * aspect)); + break; + default: + destX = (int)((ex.Width - (sourceWidth * aspect)) / 2); + break; + } } + + destWidth = (int)(sourceWidth * aspect); + destHeight = (int)(sourceHeight * aspect); } + dst = new Bitmap(ex.Width, ex.Height); + dst.SetResolution(src.VerticalResolution, src.VerticalResolution); + var srcRectangle = new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight); + var destRectangle = new Rectangle(destX, destY, destWidth, destHeight); + using (var g = Graphics.FromImage(dst)) + { + g.DrawImage(src, destRectangle, srcRectangle, GraphicsUnit.Pixel); + } Host.Assert(dst.Width == ex.Width && dst.Height == ex.Height); }; diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs new file mode 100644 index 0000000000..c20f7b3786 --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -0,0 +1,413 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using Microsoft.ML.Runtime; +using Microsoft.ML.Runtime.CommandLine; +using Microsoft.ML.Runtime.Data; +using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.ImageAnalytics; +using Microsoft.ML.Runtime.Internal.Utilities; +using Microsoft.ML.Runtime.Model; +using System; +using System.Drawing; +using System.Text; + +[assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), typeof(VectorToImageTransform.Arguments), typeof(SignatureDataTransform), + ImagePixelExtractorTransform.UserName, "ImagePixelExtractorTransform", "ImagePixelExtractor")] + +[assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), null, typeof(SignatureLoadDataTransform), + VectorToImageTransform.UserName, VectorToImageTransform.LoaderSignature)] + +namespace Microsoft.ML.Runtime.Data +{ + // REVIEW: Rewrite as LambdaTransform to simplify. + public sealed class VectorToImageTransform : OneToOneTransformBase + { + public class Column : OneToOneColumn + { + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use alpha channel", ShortName = "alpha")] + public bool? ContainsAlpha; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use red channel", ShortName = "red")] + public bool? ContainsRed; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use green channel", ShortName = "green")] + public bool? ContainsGreen; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use blue channel", ShortName = "blue")] + public bool? ContainsBlue; + + // REVIEW: Consider turning this into an enum that allows for pixel, line, or planar interleaving. + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to separate each channel or interleave in ARGB order", ShortName = "interleave")] + public bool? InterleaveArgb; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Width of the image", ShortName = "width")] + public int? ImageWidth; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Height of the image", ShortName = "height")] + public int? ImageHeight; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Offset (pre-scale)")] + public Single? Offset; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Scale factor")] + public Single? Scale; + + public static Column Parse(string str) + { + Contracts.AssertNonEmpty(str); + + var res = new Column(); + if (res.TryParse(str)) + return res; + return null; + } + + public bool TryUnparse(StringBuilder sb) + { + Contracts.AssertValue(sb); + if (ContainsAlpha != null || ContainsRed != null || ContainsGreen != null || ContainsBlue != null || ImageWidth != null || + ImageHeight != null || Offset != null || Scale != null || InterleaveArgb != null) + { + return false; + } + return TryUnparseCore(sb); + } + } + + public class Arguments : TransformInputBase + { + [Argument(ArgumentType.Multiple | ArgumentType.Required, HelpText = "New column definition(s) (optional form: name:src)", ShortName = "col", SortOrder = 1)] + public Column[] Column; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use alpha channel", ShortName = "alpha")] + public bool ContainsAlpha = false; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use red channel", ShortName = "red")] + public bool ContainsRed = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use green channel", ShortName = "green")] + public bool ContainsGreen = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to use blue channel", ShortName = "blue")] + public bool ContainsBlue = true; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Whether to separate each channel or interleave in ARGB order", ShortName = "interleave")] + public bool InterleaveArgb = false; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Width of the image", ShortName = "width")] + public int ImageWidth; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Height of the image", ShortName = "height")] + public int ImageHeight; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Offset (pre-scale)")] + public Single? Offset; + + [Argument(ArgumentType.AtMostOnce, HelpText = "Scale factor")] + public Single? Scale; + } + + /// + /// Which color channels are extracted. Note that these values are serialized so should not be modified. + /// + [Flags] + private enum ColorBits : byte + { + Alpha = 0x01, + Red = 0x02, + Green = 0x04, + Blue = 0x08, + + All = Alpha | Red | Green | Blue + } + + private sealed class ColInfoEx + { + public readonly ColorBits Colors; + public readonly byte Planes; + + public readonly int Width; + public readonly int Height; + public readonly Single Offset; + public readonly Single Scale; + public readonly bool Interleave; + + public bool Alpha { get { return (Colors & ColorBits.Alpha) != 0; } } + public bool Red { get { return (Colors & ColorBits.Red) != 0; } } + public bool Green { get { return (Colors & ColorBits.Green) != 0; } } + public bool Blue { get { return (Colors & ColorBits.Blue) != 0; } } + + public ColInfoEx(Column item, Arguments args) + { + if (item.ContainsAlpha ?? args.ContainsAlpha) { Colors |= ColorBits.Alpha; Planes++; } + if (item.ContainsRed ?? args.ContainsRed) { Colors |= ColorBits.Red; Planes++; } + if (item.ContainsGreen ?? args.ContainsGreen) { Colors |= ColorBits.Green; Planes++; } + if (item.ContainsBlue ?? args.ContainsBlue) { Colors |= ColorBits.Blue; Planes++; } + Contracts.CheckUserArg(Planes > 0, nameof(item.ContainsRed), "Need to use at least one color plane"); + + Interleave = item.InterleaveArgb ?? args.InterleaveArgb; + + Width = item.ImageWidth ?? args.ImageWidth; + Height = item.ImageHeight ?? args.ImageHeight; + Offset = item.Offset ?? args.Offset ?? 0; + Scale = item.Scale ?? args.Scale ?? 1; + Contracts.CheckUserArg(FloatUtils.IsFinite(Offset), nameof(item.Offset)); + Contracts.CheckUserArg(FloatUtils.IsFiniteNonZero(Scale), nameof(item.Scale)); + } + + public ColInfoEx(ModelLoadContext ctx) + { + Contracts.AssertValue(ctx); + + // *** Binary format *** + // byte: colors + // int: widht + // int: height + // Float: offset + // Float: scale + // byte: separateChannels + Colors = (ColorBits)ctx.Reader.ReadByte(); + Contracts.CheckDecode(Colors != 0); + Contracts.CheckDecode((Colors & ColorBits.All) == Colors); + + // Count the planes. + int planes = (int)Colors; + planes = (planes & 0x05) + ((planes >> 1) & 0x05); + planes = (planes & 0x03) + ((planes >> 2) & 0x03); + Planes = (byte)planes; + Contracts.Assert(0 < Planes & Planes <= 4); + + Width = ctx.Reader.ReadInt32(); + Contracts.CheckDecode(Width > 0); + Height = ctx.Reader.ReadInt32(); + Contracts.CheckDecode(Height > 0); + Offset = ctx.Reader.ReadFloat(); + Contracts.CheckDecode(FloatUtils.IsFinite(Offset)); + Scale = ctx.Reader.ReadFloat(); + Contracts.CheckDecode(FloatUtils.IsFiniteNonZero(Scale)); + Interleave = ctx.Reader.ReadBoolByte(); + } + + public void Save(ModelSaveContext ctx) + { + Contracts.AssertValue(ctx); + +#if DEBUG + // This code is used in deserialization - assert that it matches what we computed above. + int planes = (int)Colors; + planes = (planes & 0x05) + ((planes >> 1) & 0x05); + planes = (planes & 0x03) + ((planes >> 2) & 0x03); + Contracts.Assert(planes == Planes); +#endif + + // *** Binary format *** + // byte: colors + // byte: convert + // Float: offset + // Float: scale + // byte: separateChannels + Contracts.Assert(Colors != 0); + Contracts.Assert((Colors & ColorBits.All) == Colors); + ctx.Writer.Write((byte)Colors); + ctx.Writer.Write(Width); + ctx.Writer.Write(Height); + Contracts.Assert(FloatUtils.IsFinite(Offset)); + ctx.Writer.Write(Offset); + Contracts.Assert(FloatUtils.IsFiniteNonZero(Scale)); + ctx.Writer.Write(Scale); + ctx.Writer.WriteBoolByte(Interleave); + } + } + + internal const string Summary = "Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point."; + internal const string UserName = "Image Pixel Extractor Transform"; + public const string LoaderSignature = "ImagePixelExtractor"; + private static VersionInfo GetVersionInfo() + { + return new VersionInfo( + modelSignature: "IMGPXEXT", + verWrittenCur: 0x00010001, // Initial + verReadableCur: 0x00010001, + verWeCanReadBack: 0x00010001, + loaderSignature: LoaderSignature); + } + + private const string RegistrationName = "ImagePixelExtractor"; + + private readonly ColInfoEx[] _exes; + private readonly ImageType[] _types; + + /// + /// Public constructor corresponding to SignatureDataTransform. + /// + public VectorToImageTransform(IHostEnvironment env, Arguments args, IDataView input) + : base(env, RegistrationName, Contracts.CheckRef(args, nameof(args)).Column, input, + t => t is VectorType ? null : "Expected VectorType type") + { + Host.AssertNonEmpty(Infos); + Host.Assert(Infos.Length == Utils.Size(args.Column)); + + _exes = new ColInfoEx[Infos.Length]; + _types = new ImageType[Infos.Length]; + for (int i = 0; i < _exes.Length; i++) + { + var item = args.Column[i]; + _exes[i] = new ColInfoEx(item, args); + _types[i] = new ImageType(_exes[i].Height, _exes[i].Width); + } + Metadata.Seal(); + } + + private VectorToImageTransform(IHost host, ModelLoadContext ctx, IDataView input) + : base(host, ctx, input, t => t is VectorType ? null : "Expected VectorType type") + { + Host.AssertValue(ctx); + + // *** Binary format *** + // + // + // foreach added column + // ColInfoEx + Host.AssertNonEmpty(Infos); + _exes = new ColInfoEx[Infos.Length]; + _types = new ImageType[Infos.Length]; + for (int i = 0; i < _exes.Length; i++) + { + _exes[i] = new ColInfoEx(ctx); + _types[i] = new ImageType(_exes[i].Height, _exes[i].Width); + } + Metadata.Seal(); + } + + public static VectorToImageTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) + { + Contracts.CheckValue(env, nameof(env)); + var h = env.Register(RegistrationName); + h.CheckValue(ctx, nameof(ctx)); + h.CheckValue(input, nameof(input)); + ctx.CheckAtModel(GetVersionInfo()); + + return h.Apply("Loading Model", + ch => + { + // *** Binary format *** + // int: sizeof(Float) + // + int cbFloat = ctx.Reader.ReadInt32(); + ch.CheckDecode(cbFloat == sizeof(Single)); + return new VectorToImageTransform(h, ctx, input); + }); + } + + public override void Save(ModelSaveContext ctx) + { + Host.CheckValue(ctx, nameof(ctx)); + ctx.CheckAtModel(); + ctx.SetVersionInfo(GetVersionInfo()); + + // *** Binary format *** + // int: sizeof(Float) + // + // foreach added column + // ColInfoEx + ctx.Writer.Write(sizeof(Single)); + SaveBase(ctx); + + Host.Assert(_exes.Length == Infos.Length); + for (int i = 0; i < _exes.Length; i++) + _exes[i].Save(ctx); + } + + + protected override ColumnType GetColumnTypeCore(int iinfo) + { + Host.Assert(0 <= iinfo & iinfo < Infos.Length); + return _types[iinfo]; + } + + protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) + { + Host.AssertValueOrNull(ch); + Host.AssertValue(input); + Host.Assert(0 <= iinfo && iinfo < Infos.Length); + + var type = _types[iinfo]; + var ex = _exes[iinfo]; + + disposer = null; + return GetterFromFloatType(input, iinfo, ex); + + } + //REVIEW add byte support! + private ValueGetter GetterFromFloatType(IRow input, int iinfo, ColInfoEx ex) + { + var getSrc = GetSrcGetter>(input, iinfo); + var src = default(VBuffer); + int width = ex.Width; + int height = ex.Height; + float offset = ex.Offset; + float scale = ex.Scale; + + return + (ref Bitmap dst) => + { + getSrc(ref src); + if (src.Count == 0) + { + dst = null; + return; + } + VBuffer dense = default; + src.CopyToDense(ref dense); + dst = new Bitmap(width, height); + + dst.SetResolution(width, height); + int cpix = height * width; + int planes = dense.Count / cpix; + int position = 0; + bool needScale = offset != 0 || scale != 1; + + for (int x = 0; x < width; x++) + for (int y = 0; y < height; ++y) + + { + float R = 0; + float G = 0; + float B = 0; + float A = 0; + if (ex.Interleave) + { + if (ex.Alpha) position++; + if (ex.Red) R = dense.Values[position++]; + if (ex.Green) G = dense.Values[position++]; + if (ex.Blue) B = dense.Values[position++]; + + } + else + { + position = x * width + y; + if (ex.Alpha) { A = dense.Values[position]; position += cpix; } + if (ex.Red) { R = dense.Values[position]; position += cpix; } + if (ex.Green) { G = dense.Values[position]; position += cpix; } + if (ex.Blue) { B = dense.Values[position]; position += cpix; } + + } + Color pixel; + if (!needScale) + pixel = Color.FromArgb((int)A, (int)R, (int)G, (int)B); + else + { + pixel = Color.FromArgb( + (int)((A - offset) * scale), + (int)((R - offset) * scale), + (int)((G - offset) * scale), + (int)((B - offset) * scale)); + } + dst.SetPixel(x, y, pixel); + } + }; + } + } +} + diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs new file mode 100644 index 0000000000..e8dc8f4111 --- /dev/null +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -0,0 +1,77 @@ +using Microsoft.ML.Runtime.Api; +using Microsoft.ML.Runtime.Data; +using Microsoft.ML.TestFramework; +using System.Drawing; +using System.IO; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.ML.Tests +{ + public class ImageTests : BaseTestClass + { + public ImageTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void TestImages() + { + using (var env = new TlcEnvironment()) + { + var dataFile = GetDataPath("images/images.tsv"); + var data = env.CreateLoader("Text{col=ImagePath:TX:0 col=Name:TX:1}", new MultiFileSource(dataFile)); + var images = new ImageLoaderTransform(env, new ImageLoaderTransform.Arguments() + { + Column = new ImageLoaderTransform.Column[1] + { + new ImageLoaderTransform.Column() { Source= "ImagePath", Name="ImageReal" } + }, + ImageFolder = Path.GetDirectoryName(dataFile) + }, data); + var cropped = new ImageResizerTransform(env, new ImageResizerTransform.Arguments() + { + Column = new ImageResizerTransform.Column[1]{ + new ImageResizerTransform.Column() { Name= "ImageCropped", Source = "ImageReal", ImageHeight =100, ImageWidth = 100, Resizing = ImageResizerTransform.ResizingKind.IsoCrop} + } + }, images); + + var pixels = new ImagePixelExtractorTransform(env, new ImagePixelExtractorTransform.Arguments() + { + Column = new ImagePixelExtractorTransform.Column[1]{ + new ImagePixelExtractorTransform.Column() { Source= "ImageCropped", Name = "ImagePixels"} + } + }, cropped); + + + var backToBitmaps = new VectorToImageTransform(env, new VectorToImageTransform.Arguments() + { + Column = new VectorToImageTransform.Column[1]{ + new VectorToImageTransform.Column() { Source= "ImagePixels", Name = "ImageBitmaps" , ImageHeight=100, ImageWidth=100} + } + }, pixels); + + backToBitmaps.Schema.TryGetColumnIndex("ImagePixels", out int cropColumn); + backToBitmaps.Schema.TryGetColumnIndex("ImageBitmaps", out int bitmapColumn); + backToBitmaps.Schema.TryGetColumnIndex("ImageCropped", out int cropBitmapColumn); + using (var cursor = backToBitmaps.GetRowCursor((x) => true)) + { + var pixelsGetter = cursor.GetGetter>(cropColumn); + VBuffer pixelcolumn = new VBuffer(); + var bitmapGetter = cursor.GetGetter(bitmapColumn); + Bitmap bitmapcolumn = default; + var bitmapCropGetter = cursor.GetGetter(cropBitmapColumn); + Bitmap bitmapCropcolumn = default; + while (cursor.MoveNext()) + { + pixelsGetter(ref pixelcolumn); + bitmapGetter(ref bitmapcolumn); + bitmapCropGetter(ref bitmapCropcolumn); + } + } + + } + + } + } +} diff --git a/test/Microsoft.ML.Tests/Microsoft.ML.Tests.csproj b/test/Microsoft.ML.Tests/Microsoft.ML.Tests.csproj index 2a2ea8bca1..b088cfb05e 100644 --- a/test/Microsoft.ML.Tests/Microsoft.ML.Tests.csproj +++ b/test/Microsoft.ML.Tests/Microsoft.ML.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/test/data/images/banana.jpg b/test/data/images/banana.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45cb14b60782eb99235537696ef996a0e0911637 GIT binary patch literal 30719 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

`;(lG7fO{mLAF8k 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

t7Mp>VVsLXBOYL~Qu#Q2Te1TDG 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 literal 0 HcmV?d00001 diff --git a/test/data/images/images.tsv b/test/data/images/images.tsv new file mode 100644 index 0000000000..0315f7865b --- /dev/null +++ b/test/data/images/images.tsv @@ -0,0 +1,3 @@ +banana.jpg banana +hotdog.jpg hotdog +tomato.jpg tomato \ No newline at end of file diff --git a/test/data/images/tomato.jpg b/test/data/images/tomato.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b101856e14d8c6556ab0b3b6d5ba0bf50b24041c GIT binary patch literal 46523 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 x_z Date: Thu, 12 Jul 2018 15:53:50 -0700 Subject: [PATCH 03/16] grayscale transform, more tests. --- .../ImageGrayscaleTransform.cs | 173 ++++++++++++++++++ .../ImagePixelExtractorTransform.cs | 8 +- .../VectorToImageTransform.cs | 60 +++--- test/Microsoft.ML.Tests/ImagesTests.cs | 133 ++++++++++++-- 4 files changed, 325 insertions(+), 49 deletions(-) create mode 100644 src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs diff --git a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs new file mode 100644 index 0000000000..9010dab133 --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Drawing; +using System.Text; +using Microsoft.ML.Runtime; +using Microsoft.ML.Runtime.CommandLine; +using Microsoft.ML.Runtime.Data; +using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.Internal.Internallearn; +using Microsoft.ML.Runtime.Internal.Utilities; +using Microsoft.ML.Runtime.Model; +using Microsoft.ML.Runtime.ImageAnalytics; +using System.Drawing.Imaging; + +[assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), typeof(ImageGreyscaleTransform.Arguments), typeof(SignatureDataTransform), + ImageGreyscaleTransform.UserName, "ImageResizerTransform", "ImageResizer")] + +[assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), null, typeof(SignatureLoadDataTransform), + ImageGreyscaleTransform.UserName, ImageGreyscaleTransform.LoaderSignature)] + +namespace Microsoft.ML.Runtime.Data +{ + // REVIEW: Rewrite as LambdaTransform to simplify. + public sealed class ImageGreyscaleTransform : OneToOneTransformBase + { + + public sealed class Column : OneToOneColumn + { + public static Column Parse(string str) + { + var res = new Column(); + if (res.TryParse(str)) + return res; + return null; + } + + public bool TryUnparse(StringBuilder sb) + { + Contracts.AssertValue(sb); + return TryUnparseCore(sb); + } + + } + + public class Arguments : TransformInputBase + { + [Argument(ArgumentType.Multiple | ArgumentType.Required, HelpText = "New column definition(s) (optional form: name:src)", ShortName = "col", SortOrder = 1)] + public Column[] Column; + } + + internal const string Summary = "Scales an image to specified dimensions using one of the three scale types: isotropic with padding, " + + "isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image."; + + internal const string UserName = "Image Resizer Transform"; + public const string LoaderSignature = "ImageScalerTransform"; + private static VersionInfo GetVersionInfo() + { + return new VersionInfo( + modelSignature: "IMGSCALF", + verWrittenCur: 0x00010001, // Initial + verReadableCur: 0x00010001, + verWeCanReadBack: 0x00010001, + loaderSignature: LoaderSignature); + } + + private const string RegistrationName = "ImageScaler"; + + ///

+ /// Public constructor corresponding to SignatureDataTransform. + /// + public ImageGreyscaleTransform(IHostEnvironment env, Arguments args, IDataView input) + : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") + { + Host.AssertNonEmpty(Infos); + Host.Assert(Infos.Length == Utils.Size(args.Column)); + Metadata.Seal(); + } + + private ImageGreyscaleTransform(IHost host, ModelLoadContext ctx, IDataView input) + : base(host, ctx, input, t => t is ImageType ? null : "Expected Image type") + { + Host.AssertValue(ctx); + // *** Binary format *** + // + Host.AssertNonEmpty(Infos); + Metadata.Seal(); + } + + public static ImageGreyscaleTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) + { + Contracts.CheckValue(env, nameof(env)); + var h = env.Register(RegistrationName); + h.CheckValue(ctx, nameof(ctx)); + h.CheckValue(input, nameof(input)); + ctx.CheckAtModel(GetVersionInfo()); + return h.Apply("Loading Model", ch => new ImageGreyscaleTransform(h, ctx, input)); + } + + public override void Save(ModelSaveContext ctx) + { + Host.CheckValue(ctx, nameof(ctx)); + ctx.CheckAtModel(); + ctx.SetVersionInfo(GetVersionInfo()); + + // *** Binary format *** + // + SaveBase(ctx); + } + + protected override ColumnType GetColumnTypeCore(int iinfo) + { + Host.Assert(0 <= iinfo & iinfo < Infos.Length); + return Infos[iinfo].TypeSrc; + } + + public ColorMatrix GreyscaleColorMatrix = new ColorMatrix( + new float[][] + { + new float[] {.3f, .3f, .3f, 0, 0}, + new float[] {.59f, .59f, .59f, 0, 0}, + new float[] {.11f, .11f, .11f, 0, 0}, + new float[] {0, 0, 0, 1, 0}, + new float[] {0, 0, 0, 0, 1} + }); + + protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) + { + Host.AssertValue(ch, "ch"); + Host.AssertValue(input); + Host.Assert(0 <= iinfo && iinfo < Infos.Length); + + var src = default(Bitmap); + var getSrc = GetSrcGetter(input, iinfo); + + disposer = + () => + { + if (src != null) + { + src.Dispose(); + src = null; + } + }; + + ValueGetter del = + (ref Bitmap dst) => + { + if (dst != null) + dst.Dispose(); + + getSrc(ref src); + if (src == null || src.Height <= 0 || src.Width <= 0) + return; + + dst = new Bitmap(src.Width, src.Height); + dst.SetResolution(src.VerticalResolution, src.VerticalResolution); + ImageAttributes attributes = new ImageAttributes(); + attributes.SetColorMatrix(GreyscaleColorMatrix); + var srcRectangle = new Rectangle(0, 0, src.Width, src.Height); + using (var g = Graphics.FromImage(dst)) + { + g.DrawImage(src, srcRectangle, 0, 0, src.Width, src.Height, GraphicsUnit.Pixel, attributes); + } + Host.Assert(dst.Width == src.Width && dst.Height == src.Height); + }; + + return del; + } + } +} diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs index 2f82350afe..14198f3633 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -406,7 +406,7 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo return; } - Host.Check(src.PixelFormat== System.Drawing.Imaging.PixelFormat.Format32bppArgb); + Host.Check(src.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb); Host.Check(src.Height == height && src.Width == width); var values = dst.Values; @@ -499,7 +499,7 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo { for (int x = 0; x < w; x++, idstBase++) { - var pb = src.GetPixel(y, x); + var pb = src.GetPixel(x, y); int idst = idstBase; if (a) { vb[idst] = pb.A; idst += cpix; } if (r) { vb[idst] = pb.R; idst += cpix; } @@ -511,7 +511,7 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo { for (int x = 0; x < w; x++, idstBase++) { - var pb = src.GetPixel(y, x); + var pb = src.GetPixel(x, y); int idst = idstBase; if (a) { vf[idst] = pb.A; idst += cpix; } if (r) { vf[idst] = pb.R; idst += cpix; } @@ -523,7 +523,7 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo { for (int x = 0; x < w; x++, idstBase++) { - var pb = src.GetPixel(y, x); + var pb = src.GetPixel(x, y); int idst = idstBase; if (a) { vf[idst] = (pb.A - offset) * scale; idst += cpix; } if (r) { vf[idst] = (pb.R - offset) * scale; idst += cpix; } diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index c20f7b3786..577fdc70ef 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Drawing; +using System.Text; using Microsoft.ML.Runtime; using Microsoft.ML.Runtime.CommandLine; using Microsoft.ML.Runtime.Data; @@ -8,12 +11,9 @@ using Microsoft.ML.Runtime.ImageAnalytics; using Microsoft.ML.Runtime.Internal.Utilities; using Microsoft.ML.Runtime.Model; -using System; -using System.Drawing; -using System.Text; [assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), typeof(VectorToImageTransform.Arguments), typeof(SignatureDataTransform), - ImagePixelExtractorTransform.UserName, "ImagePixelExtractorTransform", "ImagePixelExtractor")] + VectorToImageTransform.UserName, "VectorToImageTransform", "VectorToImage")] [assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), null, typeof(SignatureLoadDataTransform), VectorToImageTransform.UserName, VectorToImageTransform.LoaderSignature)] @@ -220,20 +220,20 @@ public void Save(ModelSaveContext ctx) } } - internal const string Summary = "Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point."; - internal const string UserName = "Image Pixel Extractor Transform"; - public const string LoaderSignature = "ImagePixelExtractor"; + internal const string Summary = "Converts vector array into image type."; + internal const string UserName = "Vector To Image Transform"; + public const string LoaderSignature = "VectorToImageConverter"; private static VersionInfo GetVersionInfo() { return new VersionInfo( - modelSignature: "IMGPXEXT", + modelSignature: "VECTOIMG", verWrittenCur: 0x00010001, // Initial verReadableCur: 0x00010001, verWeCanReadBack: 0x00010001, loaderSignature: LoaderSignature); } - private const string RegistrationName = "ImagePixelExtractor"; + private const string RegistrationName = "VectorToImageConverter"; private readonly ColInfoEx[] _exes; private readonly ImageType[] _types; @@ -334,16 +334,22 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou var type = _types[iinfo]; var ex = _exes[iinfo]; - + bool needScale = ex.Offset != 0 || ex.Scale != 1; disposer = null; - return GetterFromFloatType(input, iinfo, ex); + var sourceType = Schema.GetColumnType(Infos[iinfo].Source); + if (sourceType.ItemType == NumberType.R4 || sourceType.ItemType == NumberType.R8) + return GetterFromType(input, iinfo, ex, needScale); + else + if (sourceType.ItemType == NumberType.U1) + return GetterFromType(input, iinfo, ex, false); + else + throw Contracts.Except("We only support float or byte arrays"); } - //REVIEW add byte support! - private ValueGetter GetterFromFloatType(IRow input, int iinfo, ColInfoEx ex) + private ValueGetter GetterFromType(IRow input, int iinfo, ColInfoEx ex, bool needScale) where TValue : IConvertible { - var getSrc = GetSrcGetter>(input, iinfo); - var src = default(VBuffer); + var getSrc = GetSrcGetter>(input, iinfo); + var src = default(VBuffer); int width = ex.Width; int height = ex.Height; float offset = ex.Offset; @@ -358,19 +364,17 @@ private ValueGetter GetterFromFloatType(IRow input, int iinfo, ColInfoEx dst = null; return; } - VBuffer dense = default; + VBuffer dense = default; src.CopyToDense(ref dense); + var values = dense.Values; dst = new Bitmap(width, height); - dst.SetResolution(width, height); int cpix = height * width; int planes = dense.Count / cpix; int position = 0; - bool needScale = offset != 0 || scale != 1; for (int x = 0; x < width; x++) for (int y = 0; y < height; ++y) - { float R = 0; float G = 0; @@ -379,19 +383,17 @@ private ValueGetter GetterFromFloatType(IRow input, int iinfo, ColInfoEx if (ex.Interleave) { if (ex.Alpha) position++; - if (ex.Red) R = dense.Values[position++]; - if (ex.Green) G = dense.Values[position++]; - if (ex.Blue) B = dense.Values[position++]; - + if (ex.Red) R = Convert.ToSingle(values[position++]); + if (ex.Green) G = Convert.ToSingle(values[position++]); + if (ex.Blue) B = Convert.ToSingle(values[position++]); } else { - position = x * width + y; - if (ex.Alpha) { A = dense.Values[position]; position += cpix; } - if (ex.Red) { R = dense.Values[position]; position += cpix; } - if (ex.Green) { G = dense.Values[position]; position += cpix; } - if (ex.Blue) { B = dense.Values[position]; position += cpix; } - + position = y * width + x; + if (ex.Alpha) { A = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Red) { R = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Green) { G = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Blue) { B = Convert.ToSingle(values[position]); position += cpix; } } Color pixel; if (!needScale) diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs index e8dc8f4111..982bbdbe8f 100644 --- a/test/Microsoft.ML.Tests/ImagesTests.cs +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -15,11 +15,12 @@ public ImageTests(ITestOutputHelper output) : base(output) } [Fact] - public void TestImages() + public void TestSaveImages() { using (var env = new TlcEnvironment()) { var dataFile = GetDataPath("images/images.tsv"); + var imageFolder = Path.GetDirectoryName(dataFile); var data = env.CreateLoader("Text{col=ImagePath:TX:0 col=Name:TX:1}", new MultiFileSource(dataFile)); var images = new ImageLoaderTransform(env, new ImageLoaderTransform.Arguments() { @@ -27,51 +28,151 @@ public void TestImages() { new ImageLoaderTransform.Column() { Source= "ImagePath", Name="ImageReal" } }, - ImageFolder = Path.GetDirectoryName(dataFile) + ImageFolder = imageFolder }, data); var cropped = new ImageResizerTransform(env, new ImageResizerTransform.Arguments() { Column = new ImageResizerTransform.Column[1]{ - new ImageResizerTransform.Column() { Name= "ImageCropped", Source = "ImageReal", ImageHeight =100, ImageWidth = 100, Resizing = ImageResizerTransform.ResizingKind.IsoCrop} + new ImageResizerTransform.Column() { Name= "ImageCropped", Source = "ImageReal", ImageHeight =100, ImageWidth = 100, Resizing = ImageResizerTransform.ResizingKind.IsoPad} + } + }, images); + + cropped.Schema.TryGetColumnIndex("ImagePath", out int pathColumn); + cropped.Schema.TryGetColumnIndex("ImageCropped", out int cropBitmapColumn); + using (var cursor = cropped.GetRowCursor((x) => true)) + { + var pathGetter = cursor.GetGetter(pathColumn); + DvText path = default; + var bitmapCropGetter = cursor.GetGetter(cropBitmapColumn); + Bitmap bitmap = default; + while (cursor.MoveNext()) + { + pathGetter(ref path); + bitmapCropGetter(ref bitmap); + var fileToSave = GetOutputPath(Path.GetFileNameWithoutExtension(path.ToString()) + ".cropped.jpg"); + bitmap.Save(fileToSave, System.Drawing.Imaging.ImageFormat.Jpeg); + } + } + + } + } + + [Fact] + public void TestGreyscaleTransformImages() + { + using (var env = new TlcEnvironment()) + { + var imageHeight = 100; + var imageWidth = 100; + var dataFile = GetDataPath("images/images.tsv"); + var imageFolder = Path.GetDirectoryName(dataFile); + var data = env.CreateLoader("Text{col=ImagePath:TX:0 col=Name:TX:1}", new MultiFileSource(dataFile)); + var images = new ImageLoaderTransform(env, new ImageLoaderTransform.Arguments() + { + Column = new ImageLoaderTransform.Column[1] + { + new ImageLoaderTransform.Column() { Source= "ImagePath", Name="ImageReal" } + }, + ImageFolder = imageFolder + }, data); + var cropped = new ImageResizerTransform(env, new ImageResizerTransform.Arguments() + { + Column = new ImageResizerTransform.Column[1]{ + new ImageResizerTransform.Column() { Name= "ImageCropped", Source = "ImageReal", ImageHeight =imageHeight, ImageWidth = imageWidth, Resizing = ImageResizerTransform.ResizingKind.IsoCrop} + } + }, images); + + var grey = new ImageGreyscaleTransform(env, new ImageGreyscaleTransform.Arguments() + { + Column = new ImageGreyscaleTransform.Column[1]{ + new ImageGreyscaleTransform.Column() { Name= "ImageGrey", Source = "ImageCropped"} + } + }, cropped); + + grey.Schema.TryGetColumnIndex("ImageGrey", out int greyColumn); + using (var cursor = grey.GetRowCursor((x) => true)) + { + var bitmapGetter = cursor.GetGetter(greyColumn); + Bitmap bitmap = default; + while (cursor.MoveNext()) + { + bitmapGetter(ref bitmap); + for (int x = 0; x < imageWidth; x++) + for (int y = 0; y < imageHeight; y++) + { + var pixel = bitmap.GetPixel(x, y); + // greyscale image has same values for R,G and B + Assert.True(pixel.R == pixel.G && pixel.G == pixel.B); + } + } + } + + } + + } + + [Fact] + public void TestBackAndForthConversion() + { + using (var env = new TlcEnvironment()) + { + var imageHeight = 100; + var imageWidth = 100; + var dataFile = GetDataPath("images/images.tsv"); + var imageFolder = Path.GetDirectoryName(dataFile); + var data = env.CreateLoader("Text{col=ImagePath:TX:0 col=Name:TX:1}", new MultiFileSource(dataFile)); + var images = new ImageLoaderTransform(env, new ImageLoaderTransform.Arguments() + { + Column = new ImageLoaderTransform.Column[1] + { + new ImageLoaderTransform.Column() { Source= "ImagePath", Name="ImageReal" } + }, + ImageFolder = imageFolder + }, data); + var cropped = new ImageResizerTransform(env, new ImageResizerTransform.Arguments() + { + Column = new ImageResizerTransform.Column[1]{ + new ImageResizerTransform.Column() { Source = "ImageReal", Name= "ImageCropped", ImageHeight =imageHeight, ImageWidth = imageWidth, Resizing = ImageResizerTransform.ResizingKind.IsoCrop} } }, images); var pixels = new ImagePixelExtractorTransform(env, new ImagePixelExtractorTransform.Arguments() { Column = new ImagePixelExtractorTransform.Column[1]{ - new ImagePixelExtractorTransform.Column() { Source= "ImageCropped", Name = "ImagePixels"} + new ImagePixelExtractorTransform.Column() { Source= "ImageCropped", Name = "ImagePixels", UseAlpha=true} } }, cropped); - var backToBitmaps = new VectorToImageTransform(env, new VectorToImageTransform.Arguments() { Column = new VectorToImageTransform.Column[1]{ - new VectorToImageTransform.Column() { Source= "ImagePixels", Name = "ImageBitmaps" , ImageHeight=100, ImageWidth=100} + new VectorToImageTransform.Column() { Source= "ImagePixels", Name = "ImageRestored" , ImageHeight=imageHeight, ImageWidth=imageWidth, ContainsAlpha=true} } }, pixels); - backToBitmaps.Schema.TryGetColumnIndex("ImagePixels", out int cropColumn); - backToBitmaps.Schema.TryGetColumnIndex("ImageBitmaps", out int bitmapColumn); + backToBitmaps.Schema.TryGetColumnIndex("ImageRestored", out int bitmapColumn); backToBitmaps.Schema.TryGetColumnIndex("ImageCropped", out int cropBitmapColumn); using (var cursor = backToBitmaps.GetRowCursor((x) => true)) { - var pixelsGetter = cursor.GetGetter>(cropColumn); - VBuffer pixelcolumn = new VBuffer(); var bitmapGetter = cursor.GetGetter(bitmapColumn); - Bitmap bitmapcolumn = default; + Bitmap restoredBitmap = default; + var bitmapCropGetter = cursor.GetGetter(cropBitmapColumn); - Bitmap bitmapCropcolumn = default; + Bitmap croppedBitmap = default; while (cursor.MoveNext()) { - pixelsGetter(ref pixelcolumn); - bitmapGetter(ref bitmapcolumn); - bitmapCropGetter(ref bitmapCropcolumn); + bitmapGetter(ref restoredBitmap); + bitmapCropGetter(ref croppedBitmap); + for (int x = 0; x < imageWidth; x++) + for (int y = 0; y < imageHeight; y++) + { + Assert.True(croppedBitmap.GetPixel(x, y) == restoredBitmap.GetPixel(x, y)); + } } + } } - } } } From dbf6e2a29d4a13c47f627d04acd3e77b4de18b14 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Fri, 13 Jul 2018 07:27:58 -0700 Subject: [PATCH 04/16] small cleanup --- .../ImageGrayscaleTransform.cs | 16 +++++++--------- .../ImagePixelExtractorTransform.cs | 1 + .../ImageResizerTransform.cs | 1 - .../VectorToImageTransform.cs | 1 + 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs index 9010dab133..f418c22137 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs @@ -4,19 +4,18 @@ using System; using System.Drawing; +using System.Drawing.Imaging; using System.Text; using Microsoft.ML.Runtime; using Microsoft.ML.Runtime.CommandLine; using Microsoft.ML.Runtime.Data; using Microsoft.ML.Runtime.EntryPoints; -using Microsoft.ML.Runtime.Internal.Internallearn; using Microsoft.ML.Runtime.Internal.Utilities; using Microsoft.ML.Runtime.Model; using Microsoft.ML.Runtime.ImageAnalytics; -using System.Drawing.Imaging; [assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), typeof(ImageGreyscaleTransform.Arguments), typeof(SignatureDataTransform), - ImageGreyscaleTransform.UserName, "ImageResizerTransform", "ImageResizer")] + ImageGreyscaleTransform.UserName, "ImageGreyscaleTransform", "ImageGreyscale")] [assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), null, typeof(SignatureLoadDataTransform), ImageGreyscaleTransform.UserName, ImageGreyscaleTransform.LoaderSignature)] @@ -51,22 +50,21 @@ public class Arguments : TransformInputBase public Column[] Column; } - internal const string Summary = "Scales an image to specified dimensions using one of the three scale types: isotropic with padding, " - + "isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image."; + internal const string Summary = "Convert image into grayscale."; - internal const string UserName = "Image Resizer Transform"; - public const string LoaderSignature = "ImageScalerTransform"; + internal const string UserName = "Image Greyscale Transform"; + public const string LoaderSignature = "ImageGreyscaleTransform"; private static VersionInfo GetVersionInfo() { return new VersionInfo( - modelSignature: "IMGSCALF", + modelSignature: "IMGGREY ", verWrittenCur: 0x00010001, // Initial verReadableCur: 0x00010001, verWeCanReadBack: 0x00010001, loaderSignature: LoaderSignature); } - private const string RegistrationName = "ImageScaler"; + private const string RegistrationName = "ImageGreyscale"; /// /// Public constructor corresponding to SignatureDataTransform. diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs index 14198f3633..1124947208 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -365,6 +365,7 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou return GetGetterCore(input, iinfo, out disposer); } + //REVIEW Rewrite it to where TValue : IConvertible private ValueGetter> GetGetterCore(IRow input, int iinfo, out Action disposer) { var type = _types[iinfo]; diff --git a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs index c6e5f9f079..248323e3f4 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs @@ -14,7 +14,6 @@ using Microsoft.ML.Runtime.Model; using Microsoft.ML.Runtime.ImageAnalytics; - [assembly: LoadableClass(ImageResizerTransform.Summary, typeof(ImageResizerTransform), typeof(ImageResizerTransform.Arguments), typeof(SignatureDataTransform), ImageResizerTransform.UserName, "ImageResizerTransform", "ImageResizer")] diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index 577fdc70ef..99e9bbec2c 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -346,6 +346,7 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou throw Contracts.Except("We only support float or byte arrays"); } + private ValueGetter GetterFromType(IRow input, int iinfo, ColInfoEx ex, bool needScale) where TValue : IConvertible { var getSrc = GetSrcGetter>(input, iinfo); From cf0a06c8b0429361339c0cb8a096a7121b15affe Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 16 Jul 2018 13:11:04 -0700 Subject: [PATCH 05/16] add nuget address some comment. remove SetResolution calls. unify namespace. --- Microsoft.ML.sln | 23 ++++++++++++++++++- build/Dependencies.props | 3 ++- .../Microsoft.ML.ImageAnalytics.nupkgproj | 13 +++++++++++ ...rosoft.ML.ImageAnalytics.symbols.nupkgproj | 5 ++++ .../ImageGrayscaleTransform.cs | 12 ++++++---- .../ImageLoaderTransform.cs | 15 +++++++----- .../ImagePixelExtractorTransform.cs | 7 ++++-- .../ImageResizerTransform.cs | 15 ++++++------ src/Microsoft.ML.ImageAnalytics/ImageType.cs | 20 +++++++--------- .../Microsoft.ML.ImageAnalytics.csproj | 4 ++-- .../VectorToImageTransform.cs | 10 +++++--- test/Microsoft.ML.Tests/ImagesTests.cs | 1 + 12 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.nupkgproj create mode 100644 pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.symbols.nupkgproj diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln index 2113696e59..33973296dd 100644 --- a/Microsoft.ML.sln +++ b/Microsoft.ML.sln @@ -82,7 +82,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.LightGBM", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.Ensemble", "src\Microsoft.ML.Ensemble\Microsoft.ML.Ensemble.csproj", "{DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.ML.ImageAnalytics", "src\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.csproj", "{80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.ImageAnalytics", "src\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.csproj", "{80FBE6ED-F29B-4FDC-A2A7-137A5577DD00}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.ML.Onnx", "Microsoft.ML.Onnx", "{2C03D198-5025-444C-9D71-2C884C542E0F}" + ProjectSection(SolutionItems) = preProject + pkg\Microsoft.ML.Onnx\Microsoft.ML.Onnx.nupkgproj = pkg\Microsoft.ML.Onnx\Microsoft.ML.Onnx.nupkgproj + pkg\Microsoft.ML.Onnx\Microsoft.ML.Onnx.symbols.nupkgproj = pkg\Microsoft.ML.Onnx\Microsoft.ML.Onnx.symbols.nupkgproj + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.ML.LightGBM", "Microsoft.ML.LightGBM", "{6E3F0007-592A-434A-B0BB-FA7700E2DC29}" + ProjectSection(SolutionItems) = preProject + pkg\Microsoft.ML.LightGBM\Microsoft.ML.LightGBM.nupkgproj = pkg\Microsoft.ML.LightGBM\Microsoft.ML.LightGBM.nupkgproj + pkg\Microsoft.ML.LightGBM\Microsoft.ML.LightGBM.symbols.nupkgproj = pkg\Microsoft.ML.LightGBM\Microsoft.ML.LightGBM.symbols.nupkgproj + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.ML.ImageAnalytics", "Microsoft.ML.ImageAnalytics", "{002F5886-4F9B-403B-826E-479E055DAE44}" + ProjectSection(SolutionItems) = preProject + pkg\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.nupkgproj = pkg\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.nupkgproj + pkg\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.symbols.nupkgproj = pkg\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.symbols.nupkgproj + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -234,6 +252,9 @@ Global {001F3B4E-FBE4-4001-AFD2-A6A989CD1C25} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {80FBE6ED-F29B-4FDC-A2A7-137A5577DD00} = {09EADF06-BE25-4228-AB53-95AE3E15B530} + {2C03D198-5025-444C-9D71-2C884C542E0F} = {D3D38B03-B557-484D-8348-8BADEE4DF592} + {6E3F0007-592A-434A-B0BB-FA7700E2DC29} = {D3D38B03-B557-484D-8348-8BADEE4DF592} + {002F5886-4F9B-403B-826E-479E055DAE44} = {D3D38B03-B557-484D-8348-8BADEE4DF592} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D} diff --git a/build/Dependencies.props b/build/Dependencies.props index 21e65c9007..b687627500 100644 --- a/build/Dependencies.props +++ b/build/Dependencies.props @@ -7,6 +7,7 @@ 4.4.0 4.3.0 1.0.0-beta-62824-02 - 2.1.2.2 + 2.1.2.2 + 4.5.0 diff --git a/pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.nupkgproj b/pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.nupkgproj new file mode 100644 index 0000000000..8bdef45d07 --- /dev/null +++ b/pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.nupkgproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + ML.NET component for Image support + + + + + + + + diff --git a/pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.symbols.nupkgproj b/pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.symbols.nupkgproj new file mode 100644 index 0000000000..b36800ea0b --- /dev/null +++ b/pkg/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.symbols.nupkgproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs index f418c22137..56c68373fd 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs @@ -20,12 +20,16 @@ [assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), null, typeof(SignatureLoadDataTransform), ImageGreyscaleTransform.UserName, ImageGreyscaleTransform.LoaderSignature)] -namespace Microsoft.ML.Runtime.Data +namespace Microsoft.ML.Runtime.ImageAnalytics { // REVIEW: Rewrite as LambdaTransform to simplify. + // REVIEW: Should it be separate transform or part of ImageResizerTransform? + /// + /// Transform which takes one or many columns of type in IDataView and + /// convert them to greyscale representation of the same image. + /// public sealed class ImageGreyscaleTransform : OneToOneTransformBase { - public sealed class Column : OneToOneColumn { public static Column Parse(string str) @@ -41,7 +45,6 @@ public bool TryUnparse(StringBuilder sb) Contracts.AssertValue(sb); return TryUnparseCore(sb); } - } public class Arguments : TransformInputBase @@ -126,7 +129,7 @@ protected override ColumnType GetColumnTypeCore(int iinfo) protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) { - Host.AssertValue(ch, "ch"); + Host.AssertValueOrNull(ch); Host.AssertValue(input); Host.Assert(0 <= iinfo && iinfo < Infos.Length); @@ -154,7 +157,6 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou return; dst = new Bitmap(src.Width, src.Height); - dst.SetResolution(src.VerticalResolution, src.VerticalResolution); ImageAttributes attributes = new ImageAttributes(); attributes.SetColorMatrix(GreyscaleColorMatrix); var srcRectangle = new Rectangle(0, 0, src.Width, src.Height); diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index 6ba6639d9e..c034f9ac79 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -20,9 +20,12 @@ [assembly: LoadableClass(ImageLoaderTransform.Summary, typeof(ImageLoaderTransform), null, typeof(SignatureLoadDataTransform), ImageLoaderTransform.UserName, ImageLoaderTransform.LoaderSignature)] -namespace Microsoft.ML.Runtime.Data +namespace Microsoft.ML.Runtime.ImageAnalytics { // REVIEW: Rewrite as LambdaTransform to simplify. + /// + /// Transform which takes one or many columns of type and loads them as + /// public sealed class ImageLoaderTransform : OneToOneTransformBase { public sealed class Column : OneToOneColumn @@ -50,7 +53,7 @@ public sealed class Arguments : TransformInputBase ShortName = "col", SortOrder = 1)] public Column[] Column; - [Argument(ArgumentType.AtMostOnce, HelpText = "Image folder", ShortName = "folder")] + [Argument(ArgumentType.AtMostOnce, HelpText = "Folder where to search for images", ShortName = "folder")] public string ImageFolder; } @@ -69,7 +72,7 @@ private static VersionInfo GetVersionInfo() } private readonly ImageType _type; - private string _imageFolder; + private readonly string _imageFolder; private const string RegistrationName = "ImageLoader"; @@ -128,13 +131,13 @@ protected override ColumnType GetColumnTypeCore(int iinfo) protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) { - Host.AssertValueOrNull(ch); + Host.AssertValue(ch, nameof(ch)); Host.AssertValue(input); Host.Assert(0 <= iinfo && iinfo < Infos.Length); disposer = null; var getSrc = GetSrcGetter(input, iinfo); - DvText src = default(DvText); + DvText src = default; ValueGetter del = (ref Bitmap dst) => { @@ -154,7 +157,7 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou string path = src.ToString(); if (!string.IsNullOrWhiteSpace(_imageFolder)) path = Path.Combine(_imageFolder, path); - dst = new Bitmap(filename: path, useIcm: false); + dst = new Bitmap(path); } catch (Exception e) { diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs index 1124947208..01f8c18490 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -9,9 +9,9 @@ using Microsoft.ML.Runtime.CommandLine; using Microsoft.ML.Runtime.Data; using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.ImageAnalytics; using Microsoft.ML.Runtime.Internal.Utilities; using Microsoft.ML.Runtime.Model; -using Microsoft.ML.Runtime.ImageAnalytics; [assembly: LoadableClass(ImagePixelExtractorTransform.Summary, typeof(ImagePixelExtractorTransform), typeof(ImagePixelExtractorTransform.Arguments), typeof(SignatureDataTransform), ImagePixelExtractorTransform.UserName, "ImagePixelExtractorTransform", "ImagePixelExtractor")] @@ -19,9 +19,12 @@ [assembly: LoadableClass(ImagePixelExtractorTransform.Summary, typeof(ImagePixelExtractorTransform), null, typeof(SignatureLoadDataTransform), ImagePixelExtractorTransform.UserName, ImagePixelExtractorTransform.LoaderSignature)] -namespace Microsoft.ML.Runtime.Data +namespace Microsoft.ML.Runtime.ImageAnalytics { // REVIEW: Rewrite as LambdaTransform to simplify. + /// + /// Transform which takes one or many columns of and convert them into vector representation. + /// public sealed class ImagePixelExtractorTransform : OneToOneTransformBase { public class Column : OneToOneColumn diff --git a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs index 248323e3f4..c47061934e 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs @@ -9,20 +9,23 @@ using Microsoft.ML.Runtime.CommandLine; using Microsoft.ML.Runtime.Data; using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.ImageAnalytics; using Microsoft.ML.Runtime.Internal.Internallearn; using Microsoft.ML.Runtime.Internal.Utilities; using Microsoft.ML.Runtime.Model; -using Microsoft.ML.Runtime.ImageAnalytics; -[assembly: LoadableClass(ImageResizerTransform.Summary, typeof(ImageResizerTransform), typeof(ImageResizerTransform.Arguments), typeof(SignatureDataTransform), - ImageResizerTransform.UserName, "ImageResizerTransform", "ImageResizer")] +[assembly: LoadableClass(ImageResizerTransform.Summary, typeof(ImageResizerTransform), typeof(ImageResizerTransform.Arguments), + typeof(SignatureDataTransform), ImageResizerTransform.UserName, "ImageResizerTransform", "ImageResizer")] [assembly: LoadableClass(ImageResizerTransform.Summary, typeof(ImageResizerTransform), null, typeof(SignatureLoadDataTransform), ImageResizerTransform.UserName, ImageResizerTransform.LoaderSignature)] -namespace Microsoft.ML.Runtime.Data +namespace Microsoft.ML.Runtime.ImageAnalytics { // REVIEW: Rewrite as LambdaTransform to simplify. + /// + /// Transform which takes one or many columns of and resize them to provided height and width. + /// public sealed class ImageResizerTransform : OneToOneTransformBase { public enum ResizingKind : byte @@ -248,7 +251,7 @@ protected override ColumnType GetColumnTypeCore(int iinfo) protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, out Action disposer) { - Host.AssertValue(ch, "ch"); + Host.AssertValueOrNull(ch); Host.AssertValue(input); Host.Assert(0 <= iinfo && iinfo < Infos.Length); @@ -353,8 +356,6 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou destHeight = (int)(sourceHeight * aspect); } dst = new Bitmap(ex.Width, ex.Height); - dst.SetResolution(src.VerticalResolution, src.VerticalResolution); - var srcRectangle = new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight); var destRectangle = new Rectangle(destX, destY, destWidth, destHeight); using (var g = Graphics.FromImage(dst)) diff --git a/src/Microsoft.ML.ImageAnalytics/ImageType.cs b/src/Microsoft.ML.ImageAnalytics/ImageType.cs index 75826ca435..da1258b4be 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageType.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageType.cs @@ -7,7 +7,7 @@ namespace Microsoft.ML.Runtime.ImageAnalytics { - public sealed class ImageType: StructuredType + public sealed class ImageType : StructuredType { public readonly int Height; public readonly int Width; @@ -16,13 +16,12 @@ public ImageType(int height, int width) { Contracts.CheckParam(height > 0, nameof(height)); Contracts.CheckParam(width > 0, nameof(width)); - Contracts.CheckParam((long)height * width <= int.MaxValue / 4, nameof(height), "height * width is too large"); + Contracts.CheckParam((long)height * width <= int.MaxValue / 4, nameof(height), $"{nameof(height)} * {nameof(width)} is too large"); Height = height; Width = width; } - public ImageType() - : base(typeof(Image)) + public ImageType() : base(typeof(Image)) { } @@ -30,23 +29,20 @@ public override bool Equals(ColumnType other) { if (other == this) return true; - var tmp = other as ImageType; - if (tmp == null) + if (!(other is ImageType tmp)) return false; if (Height != tmp.Height) return false; - if (Width != tmp.Width) - return false; - return true; + return Width != tmp.Width; } public override string ToString() { if (Height == 0 && Width == 0) - return "Picture"; - return string.Format("Picture<{0}, {1}>", Height, Width); + return "Image"; + return string.Format("Image<{0}, {1}>", Height, Width); } } - + } diff --git a/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj b/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj index 81011c5276..1a4fa6b66d 100644 --- a/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj +++ b/src/Microsoft.ML.ImageAnalytics/Microsoft.ML.ImageAnalytics.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -7,7 +7,7 @@ - + diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index 99e9bbec2c..02f234e8c5 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -12,15 +12,19 @@ using Microsoft.ML.Runtime.Internal.Utilities; using Microsoft.ML.Runtime.Model; -[assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), typeof(VectorToImageTransform.Arguments), typeof(SignatureDataTransform), - VectorToImageTransform.UserName, "VectorToImageTransform", "VectorToImage")] +[assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), typeof(VectorToImageTransform.Arguments), + typeof(SignatureDataTransform), VectorToImageTransform.UserName, "VectorToImageTransform", "VectorToImage")] [assembly: LoadableClass(VectorToImageTransform.Summary, typeof(VectorToImageTransform), null, typeof(SignatureLoadDataTransform), VectorToImageTransform.UserName, VectorToImageTransform.LoaderSignature)] -namespace Microsoft.ML.Runtime.Data +namespace Microsoft.ML.Runtime.ImageAnalytics { // REVIEW: Rewrite as LambdaTransform to simplify. + + /// + /// Transform which takes one or many columns with vectors in them and transform them to representation. + /// public sealed class VectorToImageTransform : OneToOneTransformBase { public class Column : OneToOneColumn diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs index 982bbdbe8f..2c3fd03e26 100644 --- a/test/Microsoft.ML.Tests/ImagesTests.cs +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -1,5 +1,6 @@ using Microsoft.ML.Runtime.Api; using Microsoft.ML.Runtime.Data; +using Microsoft.ML.Runtime.ImageAnalytics; using Microsoft.ML.TestFramework; using System.Drawing; using System.IO; From 46cc8b136dab4d1610c6ef7dffcd3d9903af7e72 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 16 Jul 2018 14:43:58 -0700 Subject: [PATCH 06/16] add entry points for ImageAnalytics --- .../EntryPoints/ImageAnalytics.cs | 75 ++ .../ImageLoaderTransform.cs | 2 +- .../VectorToImageTransform.cs | 4 +- .../Common/EntryPoints/core_ep-list.tsv | 5 + .../Common/EntryPoints/core_manifest.json | 902 ++++++++++++++++++ .../Microsoft.ML.Core.Tests.csproj | 1 + .../UnitTests/TestEntryPoints.cs | 47 + 7 files changed, 1033 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs diff --git a/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs b/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs new file mode 100644 index 0000000000..c9a39fa2e6 --- /dev/null +++ b/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs @@ -0,0 +1,75 @@ +using Microsoft.ML.Runtime; +using Microsoft.ML.Runtime.EntryPoints; +using Microsoft.ML.Runtime.ImageAnalytics.EntryPoints; + +[assembly: LoadableClass(typeof(void), typeof(ImageAnalytics), null, typeof(SignatureEntryPointModule), "ImageAnalytics")] +namespace Microsoft.ML.Runtime.ImageAnalytics.EntryPoints +{ + public static class ImageAnalytics + { + [TlcModule.EntryPoint(Name = "Transforms.ImageLoader", Desc = ImageLoaderTransform.Summary, + UserName = ImageLoaderTransform.UserName, ShortName = ImageLoaderTransform.LoaderSignature)] + public static CommonOutputs.TransformOutput ImageLoader(IHostEnvironment env, ImageLoaderTransform.Arguments input) + { + var h = EntryPointUtils.CheckArgsAndCreateHost(env, "ImageLoaderTransform", input); + var xf = new ImageLoaderTransform(h, input, input.Data); + return new CommonOutputs.TransformOutput() + { + Model = new TransformModel(h, xf, input.Data), + OutputData = xf + }; + } + + [TlcModule.EntryPoint(Name = "Transforms.ImageResizer", Desc = ImageResizerTransform.Summary, + UserName = ImageResizerTransform.UserName, ShortName = ImageResizerTransform.LoaderSignature)] + public static CommonOutputs.TransformOutput ImageResizer(IHostEnvironment env, ImageResizerTransform.Arguments input) + { + var h = EntryPointUtils.CheckArgsAndCreateHost(env, "ImageResizerTransform", input); + var xf = new ImageResizerTransform(h, input, input.Data); + return new CommonOutputs.TransformOutput() + { + Model = new TransformModel(h, xf, input.Data), + OutputData = xf + }; + } + + [TlcModule.EntryPoint(Name = "Transforms.ImagePixelExtractor", Desc = ImagePixelExtractorTransform.Summary, + UserName = ImagePixelExtractorTransform.UserName, ShortName = ImagePixelExtractorTransform.LoaderSignature)] + public static CommonOutputs.TransformOutput ImagePixelExtractor(IHostEnvironment env, ImagePixelExtractorTransform.Arguments input) + { + var h = EntryPointUtils.CheckArgsAndCreateHost(env, "ImagePixelExtractorTransform", input); + var xf = new ImagePixelExtractorTransform(h, input, input.Data); + return new CommonOutputs.TransformOutput() + { + Model = new TransformModel(h, xf, input.Data), + OutputData = xf + }; + } + + [TlcModule.EntryPoint(Name = "Transforms.ImageGreyscale", Desc = ImageGreyscaleTransform.Summary, + UserName = ImageGreyscaleTransform.UserName, ShortName = ImageGreyscaleTransform.LoaderSignature)] + public static CommonOutputs.TransformOutput ImageGreyscale(IHostEnvironment env, ImageGreyscaleTransform.Arguments input) + { + var h = EntryPointUtils.CheckArgsAndCreateHost(env, "ImageGreyscaleTransform", input); + var xf = new ImageGreyscaleTransform(h, input, input.Data); + return new CommonOutputs.TransformOutput() + { + Model = new TransformModel(h, xf, input.Data), + OutputData = xf + }; + } + + [TlcModule.EntryPoint(Name = "Transforms.VectorToImage", Desc = VectorToImageTransform.Summary, + UserName = VectorToImageTransform.UserName, ShortName = VectorToImageTransform.LoaderSignature)] + public static CommonOutputs.TransformOutput VectorToImage(IHostEnvironment env, VectorToImageTransform.Arguments input) + { + var h = EntryPointUtils.CheckArgsAndCreateHost(env, "VectorToImageTransform", input); + var xf = new VectorToImageTransform(h, input, input.Data); + return new CommonOutputs.TransformOutput() + { + Model = new TransformModel(h, xf, input.Data), + OutputData = xf + }; + } + } +} diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index c034f9ac79..b7eaa92739 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -57,7 +57,7 @@ public sealed class Arguments : TransformInputBase public string ImageFolder; } - internal const string Summary = "Loads an image from a file."; + internal const string Summary = "Load images from a file."; internal const string UserName = "Image Loader Transform"; public const string LoaderSignature = "ImageLoaderTransform"; diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index 02f234e8c5..e8a4018959 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -224,8 +224,8 @@ public void Save(ModelSaveContext ctx) } } - internal const string Summary = "Converts vector array into image type."; - internal const string UserName = "Vector To Image Transform"; + public const string Summary = "Converts vector array into image type."; + public const string UserName = "Vector To Image Transform"; public const string LoaderSignature = "VectorToImageConverter"; private static VersionInfo GetVersionInfo() { diff --git a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv index 644318c05a..92c2dcc96b 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv +++ b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv @@ -87,6 +87,10 @@ Transforms.FeatureSelectorByCount Selects the slots for which the count of non-d Transforms.FeatureSelectorByMutualInformation Selects the top k slots across all specified columns ordered by their mutual information with the label column. Microsoft.ML.Runtime.EntryPoints.SelectFeatures MutualInformationSelect Microsoft.ML.Runtime.Data.MutualInformationFeatureSelectionTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.GlobalContrastNormalizer Performs a global contrast normalization on input values: Y = (s * X - M) / D, where s is a scale, M is mean and D is either L2 norm or standard deviation. Microsoft.ML.Runtime.Data.LpNormalization GcNormalize Microsoft.ML.Runtime.Data.LpNormNormalizerTransform+GcnArguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.HashConverter Converts column values into hashes. This transform accepts both numeric and text inputs, both single and vector-valued columns. This is a part of the Dracula transform. Microsoft.ML.Runtime.Data.HashJoin Apply Microsoft.ML.Runtime.Data.HashJoinTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImageGreyscale Convert image into grayscale. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageGreyscale Microsoft.ML.Runtime.ImageAnalytics.ImageGreyscaleTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImageLoader Load images from a file. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageLoader Microsoft.ML.Runtime.ImageAnalytics.ImageLoaderTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImagePixelExtractor Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImagePixelExtractor Microsoft.ML.Runtime.ImageAnalytics.ImagePixelExtractorTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImageResizer Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageResizer Microsoft.ML.Runtime.ImageAnalytics.ImageResizerTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.KeyToTextConverter KeyToValueTransform utilizes KeyValues metadata to map key indices to the corresponding values in the KeyValues metadata. Microsoft.ML.Runtime.Data.Categorical KeyToText Microsoft.ML.Runtime.Data.KeyToValueTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.LabelColumnKeyBooleanConverter Transforms the label to either key or bool (if needed) to make it suitable for classification. Microsoft.ML.Runtime.EntryPoints.FeatureCombiner PrepareClassificationLabel Microsoft.ML.Runtime.EntryPoints.FeatureCombiner+ClassificationLabelInput Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.LabelIndicator Label remapper used by OVA Microsoft.ML.Runtime.Data.LabelIndicatorTransform LabelIndicator Microsoft.ML.Runtime.Data.LabelIndicatorTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput @@ -123,4 +127,5 @@ Transforms.TextToKeyConverter Converts input values (words, numbers, etc.) to in Transforms.TrainTestDatasetSplitter Split the dataset into train and test sets Microsoft.ML.Runtime.EntryPoints.TrainTestSplit Split Microsoft.ML.Runtime.EntryPoints.TrainTestSplit+Input Microsoft.ML.Runtime.EntryPoints.TrainTestSplit+Output Transforms.TreeLeafFeaturizer Trains a tree ensemble, or loads it from a file, then maps a numeric feature vector to three outputs: 1. A vector containing the individual tree outputs of the tree ensemble. 2. A vector indicating the leaves that the feature vector falls on in the tree ensemble. 3. A vector indicating the paths that the feature vector falls on in the tree ensemble. If a both a model file and a trainer are specified - will use the model file. If neither are specified, will train a default FastTree model. This can handle key labels by training a regression model towards their optionally permuted indices. Microsoft.ML.Runtime.Data.TreeFeaturize Featurizer Microsoft.ML.Runtime.Data.TreeEnsembleFeaturizerTransform+ArgumentsForEntryPoint Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.TwoHeterogeneousModelCombiner Combines a TransformModel and a PredictorModel into a single PredictorModel. Microsoft.ML.Runtime.EntryPoints.ModelOperations CombineTwoModels Microsoft.ML.Runtime.EntryPoints.ModelOperations+SimplePredictorModelInput Microsoft.ML.Runtime.EntryPoints.ModelOperations+PredictorModelOutput +Transforms.VectorToImage Converts vector array into image type. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics VectorToImage Microsoft.ML.Runtime.ImageAnalytics.VectorToImageTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.WordTokenizer The input to this transform is text, and the output is a vector of text containing the words (tokens) in the original text. The separator is space, but can be specified as any other character (or multiple characters) if needed. Microsoft.ML.Runtime.Transforms.TextAnalytics DelimitedTokenizeTransform Microsoft.ML.Runtime.Data.DelimitedTokenizeTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput diff --git a/test/BaselineOutput/Common/EntryPoints/core_manifest.json b/test/BaselineOutput/Common/EntryPoints/core_manifest.json index f7d73f54b4..3ff8a1e75a 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_manifest.json +++ b/test/BaselineOutput/Common/EntryPoints/core_manifest.json @@ -17840,6 +17840,628 @@ "ITransformOutput" ] }, + { + "Name": "Transforms.ImageGreyscale", + "Desc": "Convert image into grayscale.", + "FriendlyName": "Image Greyscale Transform", + "ShortName": "ImageGreyscaleTransform", + "Inputs": [ + { + "Name": "Column", + "Type": { + "Kind": "Array", + "ItemType": { + "Kind": "Struct", + "Fields": [ + { + "Name": "Name", + "Type": "String", + "Desc": "Name of the new column", + "Aliases": [ + "name" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "Source", + "Type": "String", + "Desc": "Name of the source column", + "Aliases": [ + "src" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + } + ] + } + }, + "Desc": "New column definition(s) (optional form: name:src)", + "Aliases": [ + "col" + ], + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "Data", + "Type": "DataView", + "Desc": "Input dataset", + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + } + ], + "Outputs": [ + { + "Name": "OutputData", + "Type": "DataView", + "Desc": "Transformed dataset" + }, + { + "Name": "Model", + "Type": "TransformModel", + "Desc": "Transform model" + } + ], + "InputKind": [ + "ITransformInput" + ], + "OutputKind": [ + "ITransformOutput" + ] + }, + { + "Name": "Transforms.ImageLoader", + "Desc": "Load images from a file.", + "FriendlyName": "Image Loader Transform", + "ShortName": "ImageLoaderTransform", + "Inputs": [ + { + "Name": "Column", + "Type": { + "Kind": "Array", + "ItemType": { + "Kind": "Struct", + "Fields": [ + { + "Name": "Name", + "Type": "String", + "Desc": "Name of the new column", + "Aliases": [ + "name" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "Source", + "Type": "String", + "Desc": "Name of the source column", + "Aliases": [ + "src" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + } + ] + } + }, + "Desc": "New column definition(s) (optional form: name:src)", + "Aliases": [ + "col" + ], + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "Data", + "Type": "DataView", + "Desc": "Input dataset", + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "ImageFolder", + "Type": "String", + "Desc": "Folder where to search for images", + "Aliases": [ + "folder" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + } + ], + "Outputs": [ + { + "Name": "OutputData", + "Type": "DataView", + "Desc": "Transformed dataset" + }, + { + "Name": "Model", + "Type": "TransformModel", + "Desc": "Transform model" + } + ], + "InputKind": [ + "ITransformInput" + ], + "OutputKind": [ + "ITransformOutput" + ] + }, + { + "Name": "Transforms.ImagePixelExtractor", + "Desc": "Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point.", + "FriendlyName": "Image Pixel Extractor Transform", + "ShortName": "ImagePixelExtractor", + "Inputs": [ + { + "Name": "Column", + "Type": { + "Kind": "Array", + "ItemType": { + "Kind": "Struct", + "Fields": [ + { + "Name": "UseAlpha", + "Type": "Bool", + "Desc": "Whether to use alpha channel", + "Aliases": [ + "alpha" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "UseRed", + "Type": "Bool", + "Desc": "Whether to use red channel", + "Aliases": [ + "red" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "UseGreen", + "Type": "Bool", + "Desc": "Whether to use green channel", + "Aliases": [ + "green" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "UseBlue", + "Type": "Bool", + "Desc": "Whether to use blue channel", + "Aliases": [ + "blue" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "InterleaveArgb", + "Type": "Bool", + "Desc": "Whether to separate each channel or interleave in ARGB order", + "Aliases": [ + "interleave" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Convert", + "Type": "Bool", + "Desc": "Whether to convert to floating point", + "Aliases": [ + "conv" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Offset", + "Type": "Float", + "Desc": "Offset (pre-scale)", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Scale", + "Type": "Float", + "Desc": "Scale factor", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Name", + "Type": "String", + "Desc": "Name of the new column", + "Aliases": [ + "name" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "Source", + "Type": "String", + "Desc": "Name of the source column", + "Aliases": [ + "src" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + } + ] + } + }, + "Desc": "New column definition(s) (optional form: name:src)", + "Aliases": [ + "col" + ], + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "Data", + "Type": "DataView", + "Desc": "Input dataset", + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "UseAlpha", + "Type": "Bool", + "Desc": "Whether to use alpha channel", + "Aliases": [ + "alpha" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": false + }, + { + "Name": "UseRed", + "Type": "Bool", + "Desc": "Whether to use red channel", + "Aliases": [ + "red" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "UseGreen", + "Type": "Bool", + "Desc": "Whether to use green channel", + "Aliases": [ + "green" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "UseBlue", + "Type": "Bool", + "Desc": "Whether to use blue channel", + "Aliases": [ + "blue" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "InterleaveArgb", + "Type": "Bool", + "Desc": "Whether to separate each channel or interleave in ARGB order", + "Aliases": [ + "interleave" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": false + }, + { + "Name": "Convert", + "Type": "Bool", + "Desc": "Whether to convert to floating point", + "Aliases": [ + "conv" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "Offset", + "Type": "Float", + "Desc": "Offset (pre-scale)", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Scale", + "Type": "Float", + "Desc": "Scale factor", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + } + ], + "Outputs": [ + { + "Name": "OutputData", + "Type": "DataView", + "Desc": "Transformed dataset" + }, + { + "Name": "Model", + "Type": "TransformModel", + "Desc": "Transform model" + } + ], + "InputKind": [ + "ITransformInput" + ], + "OutputKind": [ + "ITransformOutput" + ] + }, + { + "Name": "Transforms.ImageResizer", + "Desc": "Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image.", + "FriendlyName": "Image Resizer Transform", + "ShortName": "ImageScalerTransform", + "Inputs": [ + { + "Name": "Column", + "Type": { + "Kind": "Array", + "ItemType": { + "Kind": "Struct", + "Fields": [ + { + "Name": "ImageWidth", + "Type": "Int", + "Desc": "Width of the resized image", + "Aliases": [ + "width" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "ImageHeight", + "Type": "Int", + "Desc": "Height of the resized image", + "Aliases": [ + "height" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Resizing", + "Type": { + "Kind": "Enum", + "Values": [ + "IsoPad", + "IsoCrop" + ] + }, + "Desc": "Resizing method", + "Aliases": [ + "scale" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "CropAnchor", + "Type": { + "Kind": "Enum", + "Values": [ + "Right", + "Left", + "Top", + "Bottom", + "Center" + ] + }, + "Desc": "Anchor for cropping", + "Aliases": [ + "anchor" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Name", + "Type": "String", + "Desc": "Name of the new column", + "Aliases": [ + "name" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "Source", + "Type": "String", + "Desc": "Name of the source column", + "Aliases": [ + "src" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + } + ] + } + }, + "Desc": "New column definition(s) (optional form: name:src)", + "Aliases": [ + "col" + ], + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "Data", + "Type": "DataView", + "Desc": "Input dataset", + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "ImageWidth", + "Type": "Int", + "Desc": "Resized width of the image", + "Aliases": [ + "width" + ], + "Required": true, + "SortOrder": 150.0, + "IsNullable": false, + "Default": 0 + }, + { + "Name": "ImageHeight", + "Type": "Int", + "Desc": "Resized height of the image", + "Aliases": [ + "height" + ], + "Required": true, + "SortOrder": 150.0, + "IsNullable": false, + "Default": 0 + }, + { + "Name": "Resizing", + "Type": { + "Kind": "Enum", + "Values": [ + "IsoPad", + "IsoCrop" + ] + }, + "Desc": "Resizing method", + "Aliases": [ + "scale" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": "IsoCrop" + }, + { + "Name": "CropAnchor", + "Type": { + "Kind": "Enum", + "Values": [ + "Right", + "Left", + "Top", + "Bottom", + "Center" + ] + }, + "Desc": "Anchor for cropping", + "Aliases": [ + "anchor" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": "Center" + } + ], + "Outputs": [ + { + "Name": "OutputData", + "Type": "DataView", + "Desc": "Transformed dataset" + }, + { + "Name": "Model", + "Type": "TransformModel", + "Desc": "Transform model" + } + ], + "InputKind": [ + "ITransformInput" + ], + "OutputKind": [ + "ITransformOutput" + ] + }, { "Name": "Transforms.KeyToTextConverter", "Desc": "KeyToValueTransform utilizes KeyValues metadata to map key indices to the corresponding values in the KeyValues metadata.", @@ -21450,6 +22072,286 @@ } ] }, + { + "Name": "Transforms.VectorToImage", + "Desc": "Converts vector array into image type.", + "FriendlyName": "Vector To Image Transform", + "ShortName": "VectorToImageConverter", + "Inputs": [ + { + "Name": "Column", + "Type": { + "Kind": "Array", + "ItemType": { + "Kind": "Struct", + "Fields": [ + { + "Name": "ContainsAlpha", + "Type": "Bool", + "Desc": "Whether to use alpha channel", + "Aliases": [ + "alpha" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "ContainsRed", + "Type": "Bool", + "Desc": "Whether to use red channel", + "Aliases": [ + "red" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "ContainsGreen", + "Type": "Bool", + "Desc": "Whether to use green channel", + "Aliases": [ + "green" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "ContainsBlue", + "Type": "Bool", + "Desc": "Whether to use blue channel", + "Aliases": [ + "blue" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "InterleaveArgb", + "Type": "Bool", + "Desc": "Whether to separate each channel or interleave in ARGB order", + "Aliases": [ + "interleave" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "ImageWidth", + "Type": "Int", + "Desc": "Width of the image", + "Aliases": [ + "width" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "ImageHeight", + "Type": "Int", + "Desc": "Height of the image", + "Aliases": [ + "height" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Offset", + "Type": "Float", + "Desc": "Offset (pre-scale)", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Scale", + "Type": "Float", + "Desc": "Scale factor", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Name", + "Type": "String", + "Desc": "Name of the new column", + "Aliases": [ + "name" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "Source", + "Type": "String", + "Desc": "Name of the source column", + "Aliases": [ + "src" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": null + } + ] + } + }, + "Desc": "New column definition(s) (optional form: name:src)", + "Aliases": [ + "col" + ], + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "Data", + "Type": "DataView", + "Desc": "Input dataset", + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "ContainsAlpha", + "Type": "Bool", + "Desc": "Whether to use alpha channel", + "Aliases": [ + "alpha" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": false + }, + { + "Name": "ContainsRed", + "Type": "Bool", + "Desc": "Whether to use red channel", + "Aliases": [ + "red" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "ContainsGreen", + "Type": "Bool", + "Desc": "Whether to use green channel", + "Aliases": [ + "green" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "ContainsBlue", + "Type": "Bool", + "Desc": "Whether to use blue channel", + "Aliases": [ + "blue" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": true + }, + { + "Name": "InterleaveArgb", + "Type": "Bool", + "Desc": "Whether to separate each channel or interleave in ARGB order", + "Aliases": [ + "interleave" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": false + }, + { + "Name": "ImageWidth", + "Type": "Int", + "Desc": "Width of the image", + "Aliases": [ + "width" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": 0 + }, + { + "Name": "ImageHeight", + "Type": "Int", + "Desc": "Height of the image", + "Aliases": [ + "height" + ], + "Required": false, + "SortOrder": 150.0, + "IsNullable": false, + "Default": 0 + }, + { + "Name": "Offset", + "Type": "Float", + "Desc": "Offset (pre-scale)", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "Scale", + "Type": "Float", + "Desc": "Scale factor", + "Required": false, + "SortOrder": 150.0, + "IsNullable": true, + "Default": null + } + ], + "Outputs": [ + { + "Name": "OutputData", + "Type": "DataView", + "Desc": "Transformed dataset" + }, + { + "Name": "Model", + "Type": "TransformModel", + "Desc": "Transform model" + } + ], + "InputKind": [ + "ITransformInput" + ], + "OutputKind": [ + "ITransformOutput" + ] + }, { "Name": "Transforms.WordTokenizer", "Desc": "The input to this transform is text, and the output is a vector of text containing the words (tokens) in the original text. The separator is space, but can be specified as any other character (or multiple characters) if needed.", diff --git a/test/Microsoft.ML.Core.Tests/Microsoft.ML.Core.Tests.csproj b/test/Microsoft.ML.Core.Tests/Microsoft.ML.Core.Tests.csproj index ea92975b35..a9b1b991ae 100644 --- a/test/Microsoft.ML.Core.Tests/Microsoft.ML.Core.Tests.csproj +++ b/test/Microsoft.ML.Core.Tests/Microsoft.ML.Core.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/test/Microsoft.ML.Core.Tests/UnitTests/TestEntryPoints.cs b/test/Microsoft.ML.Core.Tests/UnitTests/TestEntryPoints.cs index afd488915e..5c3056ab58 100644 --- a/test/Microsoft.ML.Core.Tests/UnitTests/TestEntryPoints.cs +++ b/test/Microsoft.ML.Core.Tests/UnitTests/TestEntryPoints.cs @@ -265,6 +265,53 @@ private string GetBuildPrefix() #endif } + [Fact(Skip = "Execute this test if you want to regenerate ep-list and _manifest.json")] + public void RegenerateEntryPointCatalog() + { + var buildPrefix = GetBuildPrefix(); + var epListFile = buildPrefix + "_ep-list.tsv"; + var manifestFile = buildPrefix + "_manifest.json"; + + var entryPointsSubDir = Path.Combine("..", "Common", "EntryPoints"); + var catalog = ModuleCatalog.CreateInstance(Env); + var epListPath = GetBaselinePath(entryPointsSubDir, epListFile); + DeleteOutputPath(epListPath); + + var regex = new Regex(@"\r\n?|\n", RegexOptions.Compiled); + File.WriteAllLines(epListPath, catalog.AllEntryPoints() + .Select(x => string.Join("\t", + x.Name, + regex.Replace(x.Description, ""), + x.Method.DeclaringType, + x.Method.Name, + x.InputType, + x.OutputType) + .Replace(Environment.NewLine, "")) + .OrderBy(x => x)); + + + var jObj = JsonManifestUtils.BuildAllManifests(Env, catalog); + + //clean up the description from the new line characters + if (jObj[FieldNames.TopEntryPoints] != null && jObj[FieldNames.TopEntryPoints] is JArray) + { + foreach (JToken entry in jObj[FieldNames.TopEntryPoints].Children()) + if (entry[FieldNames.Desc] != null) + entry[FieldNames.Desc] = regex.Replace(entry[FieldNames.Desc].ToString(), ""); + } + var manifestPath = GetBaselinePath(entryPointsSubDir, manifestFile); + DeleteOutputPath(manifestPath); + + using (var file = File.OpenWrite(manifestPath)) + using (var writer = new StreamWriter(file)) + using (var jw = new JsonTextWriter(writer)) + { + jw.Formatting = Formatting.Indented; + jObj.WriteTo(jw); + } + } + + [Fact] public void EntryPointCatalog() { From 601bab958da9a83c742db92d6f6a7a0d0aaa158d Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 16 Jul 2018 14:59:47 -0700 Subject: [PATCH 07/16] update csharpApi --- src/Microsoft.ML/CSharpApi.cs | 981 ++++++++++++++++++++++++++++++++-- 1 file changed, 933 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.ML/CSharpApi.cs b/src/Microsoft.ML/CSharpApi.cs index 83723638e1..8d688933f7 100644 --- a/src/Microsoft.ML/CSharpApi.cs +++ b/src/Microsoft.ML/CSharpApi.cs @@ -1090,6 +1090,54 @@ public void Add(Microsoft.ML.Transforms.HashConverter input, Microsoft.ML.Transf _jsonNodes.Add(Serialize("Transforms.HashConverter", input, output)); } + public Microsoft.ML.Transforms.ImageGreyscale.Output Add(Microsoft.ML.Transforms.ImageGreyscale input) + { + var output = new Microsoft.ML.Transforms.ImageGreyscale.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImageGreyscale input, Microsoft.ML.Transforms.ImageGreyscale.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImageGreyscale", input, output)); + } + + public Microsoft.ML.Transforms.ImageLoader.Output Add(Microsoft.ML.Transforms.ImageLoader input) + { + var output = new Microsoft.ML.Transforms.ImageLoader.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImageLoader input, Microsoft.ML.Transforms.ImageLoader.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImageLoader", input, output)); + } + + public Microsoft.ML.Transforms.ImagePixelExtractor.Output Add(Microsoft.ML.Transforms.ImagePixelExtractor input) + { + var output = new Microsoft.ML.Transforms.ImagePixelExtractor.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImagePixelExtractor input, Microsoft.ML.Transforms.ImagePixelExtractor.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImagePixelExtractor", input, output)); + } + + public Microsoft.ML.Transforms.ImageResizer.Output Add(Microsoft.ML.Transforms.ImageResizer input) + { + var output = new Microsoft.ML.Transforms.ImageResizer.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImageResizer input, Microsoft.ML.Transforms.ImageResizer.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImageResizer", input, output)); + } + public Microsoft.ML.Transforms.KeyToTextConverter.Output Add(Microsoft.ML.Transforms.KeyToTextConverter input) { var output = new Microsoft.ML.Transforms.KeyToTextConverter.Output(); @@ -1522,6 +1570,18 @@ public void Add(Microsoft.ML.Transforms.TwoHeterogeneousModelCombiner input, Mic _jsonNodes.Add(Serialize("Transforms.TwoHeterogeneousModelCombiner", input, output)); } + public Microsoft.ML.Transforms.VectorToImage.Output Add(Microsoft.ML.Transforms.VectorToImage input) + { + var output = new Microsoft.ML.Transforms.VectorToImage.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.VectorToImage input, Microsoft.ML.Transforms.VectorToImage.Output output) + { + _jsonNodes.Add(Serialize("Transforms.VectorToImage", input, output)); + } + public Microsoft.ML.Transforms.WordTokenizer.Output Add(Microsoft.ML.Transforms.WordTokenizer input) { var output = new Microsoft.ML.Transforms.WordTokenizer.Output(); @@ -11867,7 +11927,7 @@ public HashConverterPipelineStep(Output output) namespace Transforms { - public sealed partial class KeyToValueTransformColumn : OneToOneColumn, IOneToOneColumn + public sealed partial class ImageGreyscaleTransformColumn : OneToOneColumn, IOneToOneColumn { /// /// Name of the new column @@ -11882,16 +11942,16 @@ public sealed partial class KeyToValueTransformColumn : OneToOneColumn - /// KeyToValueTransform utilizes KeyValues metadata to map key indices to the corresponding values in the KeyValues metadata. + /// Convert image into grayscale. /// - public sealed partial class KeyToTextConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImageGreyscale : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { - public KeyToTextConverter() + public ImageGreyscale() { } - public KeyToTextConverter(params string[] inputColumns) + public ImageGreyscale(params string[] inputColumns) { if (inputColumns != null) { @@ -11902,7 +11962,7 @@ public KeyToTextConverter(params string[] inputColumns) } } - public KeyToTextConverter(params (string inputColumn, string outputColumn)[] inputOutputColumns) + public ImageGreyscale(params (string inputColumn, string outputColumn)[] inputOutputColumns) { if (inputOutputColumns != null) { @@ -11915,15 +11975,15 @@ public KeyToTextConverter(params (string inputColumn, string outputColumn)[] inp public void AddColumn(string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); Column = list.ToArray(); } public void AddColumn(string outputColumn, string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); Column = list.ToArray(); } @@ -11931,7 +11991,7 @@ public void AddColumn(string outputColumn, string inputColumn) /// /// New column definition(s) (optional form: name:src) /// - public KeyToValueTransformColumn[] Column { get; set; } + public ImageGreyscaleTransformColumn[] Column { get; set; } /// /// Input dataset @@ -11960,18 +12020,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(KeyToTextConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImageGreyscale)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new KeyToTextConverterPipelineStep(output); + return new ImageGreyscalePipelineStep(output); } - private class KeyToTextConverterPipelineStep : ILearningPipelineDataStep + private class ImageGreyscalePipelineStep : ILearningPipelineDataStep { - public KeyToTextConverterPipelineStep(Output output) + public ImageGreyscalePipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -11986,22 +12046,76 @@ public KeyToTextConverterPipelineStep(Output output) namespace Transforms { + public sealed partial class ImageLoaderTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + /// - /// Transforms the label to either key or bool (if needed) to make it suitable for classification. + /// Load images from a file. /// - public sealed partial class LabelColumnKeyBooleanConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImageLoader : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { + public ImageLoader() + { + } + + public ImageLoader(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public ImageLoader(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + /// - /// Convert the key values to text + /// New column definition(s) (optional form: name:src) /// - public bool TextKeyValues { get; set; } = true; + public ImageLoaderTransformColumn[] Column { get; set; } /// - /// The label column + /// Folder where to search for images /// - public string LabelColumn { get; set; } + public string ImageFolder { get; set; } /// /// Input dataset @@ -12030,18 +12144,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(LabelColumnKeyBooleanConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImageLoader)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new LabelColumnKeyBooleanConverterPipelineStep(output); + return new ImageLoaderPipelineStep(output); } - private class LabelColumnKeyBooleanConverterPipelineStep : ILearningPipelineDataStep + private class ImageLoaderPipelineStep : ILearningPipelineDataStep { - public LabelColumnKeyBooleanConverterPipelineStep(Output output) + public ImageLoaderPipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -12056,12 +12170,47 @@ public LabelColumnKeyBooleanConverterPipelineStep(Output output) namespace Transforms { - public sealed partial class LabelIndicatorTransformColumn : OneToOneColumn, IOneToOneColumn + public sealed partial class ImagePixelExtractorTransformColumn : OneToOneColumn, IOneToOneColumn { /// - /// The positive example class for binary classification. + /// Whether to use alpha channel /// - public int? ClassIndex { get; set; } + public bool? UseAlpha { get; set; } + + /// + /// Whether to use red channel + /// + public bool? UseRed { get; set; } + + /// + /// Whether to use green channel + /// + public bool? UseGreen { get; set; } + + /// + /// Whether to use blue channel + /// + public bool? UseBlue { get; set; } + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool? InterleaveArgb { get; set; } + + /// + /// Whether to convert to floating point + /// + public bool? Convert { get; set; } + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } /// /// Name of the new column @@ -12076,16 +12225,16 @@ public sealed partial class LabelIndicatorTransformColumn : OneToOneColumn - public sealed partial class LabelIndicator : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImagePixelExtractor : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { - public LabelIndicator() + public ImagePixelExtractor() { } - public LabelIndicator(params string[] inputColumns) + public ImagePixelExtractor(params string[] inputColumns) { if (inputColumns != null) { @@ -12096,7 +12245,7 @@ public LabelIndicator(params string[] inputColumns) } } - public LabelIndicator(params (string inputColumn, string outputColumn)[] inputOutputColumns) + public ImagePixelExtractor(params (string inputColumn, string outputColumn)[] inputOutputColumns) { if (inputOutputColumns != null) { @@ -12109,15 +12258,15 @@ public LabelIndicator(params (string inputColumn, string outputColumn)[] inputOu public void AddColumn(string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); Column = list.ToArray(); } public void AddColumn(string outputColumn, string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); Column = list.ToArray(); } @@ -12125,12 +12274,47 @@ public void AddColumn(string outputColumn, string inputColumn) /// /// New column definition(s) (optional form: name:src) /// - public LabelIndicatorTransformColumn[] Column { get; set; } + public ImagePixelExtractorTransformColumn[] Column { get; set; } /// - /// Label of the positive class. + /// Whether to use alpha channel /// - public int ClassIndex { get; set; } + public bool UseAlpha { get; set; } = false; + + /// + /// Whether to use red channel + /// + public bool UseRed { get; set; } = true; + + /// + /// Whether to use green channel + /// + public bool UseGreen { get; set; } = true; + + /// + /// Whether to use blue channel + /// + public bool UseBlue { get; set; } = true; + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool InterleaveArgb { get; set; } = false; + + /// + /// Whether to convert to floating point + /// + public bool Convert { get; set; } = true; + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } /// /// Input dataset @@ -12159,18 +12343,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(LabelIndicator)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImagePixelExtractor)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new LabelIndicatorPipelineStep(output); + return new ImagePixelExtractorPipelineStep(output); } - private class LabelIndicatorPipelineStep : ILearningPipelineDataStep + private class ImagePixelExtractorPipelineStep : ILearningPipelineDataStep { - public LabelIndicatorPipelineStep(Output output) + public ImagePixelExtractorPipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -12184,18 +12368,510 @@ public LabelIndicatorPipelineStep(Output output) namespace Transforms { + public enum ImageResizerTransformResizingKind : byte + { + IsoPad = 0, + IsoCrop = 1 + } + + public enum ImageResizerTransformAnchor : byte + { + Right = 0, + Left = 1, + Top = 2, + Bottom = 3, + Center = 4 + } + + + public sealed partial class ImageResizerTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Width of the resized image + /// + public int? ImageWidth { get; set; } + + /// + /// Height of the resized image + /// + public int? ImageHeight { get; set; } + + /// + /// Resizing method + /// + public ImageResizerTransformResizingKind? Resizing { get; set; } + + /// + /// Anchor for cropping + /// + public ImageResizerTransformAnchor? CropAnchor { get; set; } + + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } /// - /// Transforms the label to float to make it suitable for regression. + /// Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image. /// - public sealed partial class LabelToFloatConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImageResizer : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { + public ImageResizer() + { + } + + public ImageResizer(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public ImageResizer(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + /// - /// The label column + /// New column definition(s) (optional form: name:src) /// - public string LabelColumn { get; set; } + public ImageResizerTransformColumn[] Column { get; set; } + + /// + /// Resized width of the image + /// + public int ImageWidth { get; set; } + + /// + /// Resized height of the image + /// + public int ImageHeight { get; set; } + + /// + /// Resizing method + /// + public ImageResizerTransformResizingKind Resizing { get; set; } = ImageResizerTransformResizingKind.IsoCrop; + + /// + /// Anchor for cropping + /// + public ImageResizerTransformAnchor CropAnchor { get; set; } = ImageResizerTransformAnchor.Center; + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(ImageResizer)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new ImageResizerPipelineStep(output); + } + + private class ImageResizerPipelineStep : ILearningPipelineDataStep + { + public ImageResizerPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + public sealed partial class KeyToValueTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + + /// + /// KeyToValueTransform utilizes KeyValues metadata to map key indices to the corresponding values in the KeyValues metadata. + /// + public sealed partial class KeyToTextConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public KeyToTextConverter() + { + } + + public KeyToTextConverter(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public KeyToTextConverter(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public KeyToValueTransformColumn[] Column { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(KeyToTextConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new KeyToTextConverterPipelineStep(output); + } + + private class KeyToTextConverterPipelineStep : ILearningPipelineDataStep + { + public KeyToTextConverterPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + /// + /// Transforms the label to either key or bool (if needed) to make it suitable for classification. + /// + public sealed partial class LabelColumnKeyBooleanConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + + /// + /// Convert the key values to text + /// + public bool TextKeyValues { get; set; } = true; + + /// + /// The label column + /// + public string LabelColumn { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(LabelColumnKeyBooleanConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new LabelColumnKeyBooleanConverterPipelineStep(output); + } + + private class LabelColumnKeyBooleanConverterPipelineStep : ILearningPipelineDataStep + { + public LabelColumnKeyBooleanConverterPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + public sealed partial class LabelIndicatorTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// The positive example class for binary classification. + /// + public int? ClassIndex { get; set; } + + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + + /// + /// Label remapper used by OVA + /// + public sealed partial class LabelIndicator : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public LabelIndicator() + { + } + + public LabelIndicator(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public LabelIndicator(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public LabelIndicatorTransformColumn[] Column { get; set; } + + /// + /// Label of the positive class. + /// + public int ClassIndex { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(LabelIndicator)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new LabelIndicatorPipelineStep(output); + } + + private class LabelIndicatorPipelineStep : ILearningPipelineDataStep + { + public LabelIndicatorPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + /// + /// Transforms the label to float to make it suitable for regression. + /// + public sealed partial class LabelToFloatConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + + /// + /// The label column + /// + public string LabelColumn { get; set; } /// /// Input dataset @@ -15441,6 +16117,215 @@ public sealed class Output } } + namespace Transforms + { + + public sealed partial class VectorToImageTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Whether to use alpha channel + /// + public bool? ContainsAlpha { get; set; } + + /// + /// Whether to use red channel + /// + public bool? ContainsRed { get; set; } + + /// + /// Whether to use green channel + /// + public bool? ContainsGreen { get; set; } + + /// + /// Whether to use blue channel + /// + public bool? ContainsBlue { get; set; } + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool? InterleaveArgb { get; set; } + + /// + /// Width of the image + /// + public int? ImageWidth { get; set; } + + /// + /// Height of the image + /// + public int? ImageHeight { get; set; } + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } + + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + + /// + /// Converts vector array into image type. + /// + public sealed partial class VectorToImage : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public VectorToImage() + { + } + + public VectorToImage(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public VectorToImage(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public VectorToImageTransformColumn[] Column { get; set; } + + /// + /// Whether to use alpha channel + /// + public bool ContainsAlpha { get; set; } = false; + + /// + /// Whether to use red channel + /// + public bool ContainsRed { get; set; } = true; + + /// + /// Whether to use green channel + /// + public bool ContainsGreen { get; set; } = true; + + /// + /// Whether to use blue channel + /// + public bool ContainsBlue { get; set; } = true; + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool InterleaveArgb { get; set; } = false; + + /// + /// Width of the image + /// + public int ImageWidth { get; set; } + + /// + /// Height of the image + /// + public int ImageHeight { get; set; } + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(VectorToImage)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new VectorToImagePipelineStep(output); + } + + private class VectorToImagePipelineStep : ILearningPipelineDataStep + { + public VectorToImagePipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + namespace Transforms { From 6c9df1841a9cb3c5ca9fe8b0977e29e097db664c Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Tue, 17 Jul 2018 09:52:32 -0700 Subject: [PATCH 08/16] disable try catch temporary --- .../EntryPoints/ImageAnalytics.cs | 6 +++- .../ImageLoaderTransform.cs | 30 +++++++++---------- src/Microsoft.ML.ImageAnalytics/ImageType.cs | 2 -- .../VectorToImageTransform.cs | 1 + test/Microsoft.ML.Tests/ImagesTests.cs | 6 +++- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs b/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs index c9a39fa2e6..162ae6c9fc 100644 --- a/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs +++ b/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs @@ -1,4 +1,8 @@ -using Microsoft.ML.Runtime; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.ML.Runtime; using Microsoft.ML.Runtime.EntryPoints; using Microsoft.ML.Runtime.ImageAnalytics.EntryPoints; diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index b7eaa92739..3984324ca2 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -152,25 +152,25 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou if (src.Length > 0) { // Catch exceptions and pass null through. Should also log failures... - try - { + //try + //{ string path = src.ToString(); if (!string.IsNullOrWhiteSpace(_imageFolder)) path = Path.Combine(_imageFolder, path); dst = new Bitmap(path); - } - catch (Exception e) - { - // REVIEW: We catch everything since the documentation for new Bitmap(string) - // appears to be incorrect. When the file isn't found, it throws an ArgumentException, - // while the documentation says FileNotFoundException. Not sure what it will throw - // in other cases, like corrupted file, etc. - - // REVIEW : Log failures. - ch.Info(e.Message); - ch.Info(e.StackTrace); - dst = null; - } + //} + //catch (Exception e) + //{ + // // REVIEW: We catch everything since the documentation for new Bitmap(string) + // // appears to be incorrect. When the file isn't found, it throws an ArgumentException, + // // while the documentation says FileNotFoundException. Not sure what it will throw + // // in other cases, like corrupted file, etc. + + // // REVIEW : Log failures. + // ch.Info(e.Message); + // ch.Info(e.StackTrace); + // dst = null; + //} } }; return del; diff --git a/src/Microsoft.ML.ImageAnalytics/ImageType.cs b/src/Microsoft.ML.ImageAnalytics/ImageType.cs index da1258b4be..95c78d4b1e 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageType.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageType.cs @@ -43,6 +43,4 @@ public override string ToString() return string.Format("Image<{0}, {1}>", Height, Width); } } - - } diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index e8a4018959..3fbd6cf039 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. + using System; using System.Drawing; using System.Text; diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs index 2c3fd03e26..51accb9903 100644 --- a/test/Microsoft.ML.Tests/ImagesTests.cs +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -1,4 +1,8 @@ -using Microsoft.ML.Runtime.Api; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.ML.Runtime.Api; using Microsoft.ML.Runtime.Data; using Microsoft.ML.Runtime.ImageAnalytics; using Microsoft.ML.TestFramework; From d97f8ae33017ade3f80914d48b5dda3eff1e15f4 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Tue, 17 Jul 2018 14:56:03 -0700 Subject: [PATCH 09/16] address some comments. Grayscale return exception catching --- .../EntryPoints/ImageAnalytics.cs | 10 ++--- .../ImageGrayscaleTransform.cs | 24 ++++++------ .../ImageLoaderTransform.cs | 32 ++++++++-------- src/Microsoft.ML.ImageAnalytics/ImageType.cs | 2 +- src/Microsoft.ML/CSharpApi.cs | 38 +++++++++---------- .../Common/EntryPoints/core_ep-list.tsv | 2 +- .../Common/EntryPoints/core_manifest.json | 4 +- test/Microsoft.ML.Tests/ImagesTests.cs | 6 +-- 8 files changed, 58 insertions(+), 60 deletions(-) diff --git a/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs b/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs index 162ae6c9fc..97c613485f 100644 --- a/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs +++ b/src/Microsoft.ML.ImageAnalytics/EntryPoints/ImageAnalytics.cs @@ -50,12 +50,12 @@ public static CommonOutputs.TransformOutput ImagePixelExtractor(IHostEnvironment }; } - [TlcModule.EntryPoint(Name = "Transforms.ImageGreyscale", Desc = ImageGreyscaleTransform.Summary, - UserName = ImageGreyscaleTransform.UserName, ShortName = ImageGreyscaleTransform.LoaderSignature)] - public static CommonOutputs.TransformOutput ImageGreyscale(IHostEnvironment env, ImageGreyscaleTransform.Arguments input) + [TlcModule.EntryPoint(Name = "Transforms.ImageGrayscale", Desc = ImageGrayscaleTransform.Summary, + UserName = ImageGrayscaleTransform.UserName, ShortName = ImageGrayscaleTransform.LoaderSignature)] + public static CommonOutputs.TransformOutput ImageGrayscale(IHostEnvironment env, ImageGrayscaleTransform.Arguments input) { - var h = EntryPointUtils.CheckArgsAndCreateHost(env, "ImageGreyscaleTransform", input); - var xf = new ImageGreyscaleTransform(h, input, input.Data); + var h = EntryPointUtils.CheckArgsAndCreateHost(env, "ImageGrayscaleTransform", input); + var xf = new ImageGrayscaleTransform(h, input, input.Data); return new CommonOutputs.TransformOutput() { Model = new TransformModel(h, xf, input.Data), diff --git a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs index 56c68373fd..ca4caea1ec 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs @@ -14,11 +14,11 @@ using Microsoft.ML.Runtime.Model; using Microsoft.ML.Runtime.ImageAnalytics; -[assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), typeof(ImageGreyscaleTransform.Arguments), typeof(SignatureDataTransform), - ImageGreyscaleTransform.UserName, "ImageGreyscaleTransform", "ImageGreyscale")] +[assembly: LoadableClass(ImageGrayscaleTransform.Summary, typeof(ImageGrayscaleTransform), typeof(ImageGrayscaleTransform.Arguments), typeof(SignatureDataTransform), + ImageGrayscaleTransform.UserName, "ImageGrayscaleTransform", "ImageGrayscale")] -[assembly: LoadableClass(ImageGreyscaleTransform.Summary, typeof(ImageGreyscaleTransform), null, typeof(SignatureLoadDataTransform), - ImageGreyscaleTransform.UserName, ImageGreyscaleTransform.LoaderSignature)] +[assembly: LoadableClass(ImageGrayscaleTransform.Summary, typeof(ImageGrayscaleTransform), null, typeof(SignatureLoadDataTransform), + ImageGrayscaleTransform.UserName, ImageGrayscaleTransform.LoaderSignature)] namespace Microsoft.ML.Runtime.ImageAnalytics { @@ -28,7 +28,7 @@ namespace Microsoft.ML.Runtime.ImageAnalytics /// Transform which takes one or many columns of type in IDataView and /// convert them to greyscale representation of the same image. /// - public sealed class ImageGreyscaleTransform : OneToOneTransformBase + public sealed class ImageGrayscaleTransform : OneToOneTransformBase { public sealed class Column : OneToOneColumn { @@ -56,7 +56,7 @@ public class Arguments : TransformInputBase internal const string Summary = "Convert image into grayscale."; internal const string UserName = "Image Greyscale Transform"; - public const string LoaderSignature = "ImageGreyscaleTransform"; + public const string LoaderSignature = "ImageGrayscaleTransform"; private static VersionInfo GetVersionInfo() { return new VersionInfo( @@ -67,12 +67,10 @@ private static VersionInfo GetVersionInfo() loaderSignature: LoaderSignature); } - private const string RegistrationName = "ImageGreyscale"; + private const string RegistrationName = "ImageGrayscale"; - /// /// Public constructor corresponding to SignatureDataTransform. - /// - public ImageGreyscaleTransform(IHostEnvironment env, Arguments args, IDataView input) + public ImageGrayscaleTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") { Host.AssertNonEmpty(Infos); @@ -80,7 +78,7 @@ public ImageGreyscaleTransform(IHostEnvironment env, Arguments args, IDataView i Metadata.Seal(); } - private ImageGreyscaleTransform(IHost host, ModelLoadContext ctx, IDataView input) + private ImageGrayscaleTransform(IHost host, ModelLoadContext ctx, IDataView input) : base(host, ctx, input, t => t is ImageType ? null : "Expected Image type") { Host.AssertValue(ctx); @@ -90,14 +88,14 @@ private ImageGreyscaleTransform(IHost host, ModelLoadContext ctx, IDataView inpu Metadata.Seal(); } - public static ImageGreyscaleTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) + public static ImageGrayscaleTransform Create(IHostEnvironment env, ModelLoadContext ctx, IDataView input) { Contracts.CheckValue(env, nameof(env)); var h = env.Register(RegistrationName); h.CheckValue(ctx, nameof(ctx)); h.CheckValue(input, nameof(input)); ctx.CheckAtModel(GetVersionInfo()); - return h.Apply("Loading Model", ch => new ImageGreyscaleTransform(h, ctx, input)); + return h.Apply("Loading Model", ch => new ImageGrayscaleTransform(h, ctx, input)); } public override void Save(ModelSaveContext ctx) diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index 3984324ca2..53b515f817 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -57,7 +57,7 @@ public sealed class Arguments : TransformInputBase public string ImageFolder; } - internal const string Summary = "Load images from a file."; + internal const string Summary = "Load images from a files."; internal const string UserName = "Image Loader Transform"; public const string LoaderSignature = "ImageLoaderTransform"; @@ -152,25 +152,25 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou if (src.Length > 0) { // Catch exceptions and pass null through. Should also log failures... - //try - //{ + try + { string path = src.ToString(); if (!string.IsNullOrWhiteSpace(_imageFolder)) path = Path.Combine(_imageFolder, path); dst = new Bitmap(path); - //} - //catch (Exception e) - //{ - // // REVIEW: We catch everything since the documentation for new Bitmap(string) - // // appears to be incorrect. When the file isn't found, it throws an ArgumentException, - // // while the documentation says FileNotFoundException. Not sure what it will throw - // // in other cases, like corrupted file, etc. - - // // REVIEW : Log failures. - // ch.Info(e.Message); - // ch.Info(e.StackTrace); - // dst = null; - //} + } + catch (Exception e) + { + // REVIEW: We catch everything since the documentation for new Bitmap(string) + // appears to be incorrect. When the file isn't found, it throws an ArgumentException, + // while the documentation says FileNotFoundException. Not sure what it will throw + // in other cases, like corrupted file, etc. + + // REVIEW : Log failures. + ch.Info(e.Message); + ch.Info(e.StackTrace); + dst = null; + } } }; return del; diff --git a/src/Microsoft.ML.ImageAnalytics/ImageType.cs b/src/Microsoft.ML.ImageAnalytics/ImageType.cs index 95c78d4b1e..bc822f80b0 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageType.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageType.cs @@ -16,7 +16,7 @@ public ImageType(int height, int width) { Contracts.CheckParam(height > 0, nameof(height)); Contracts.CheckParam(width > 0, nameof(width)); - Contracts.CheckParam((long)height * width <= int.MaxValue / 4, nameof(height), $"{nameof(height)} * {nameof(width)} is too large"); + Contracts.CheckParam((long)height * width <= int.MaxValue / 4, nameof(height), nameof(height) + " * " + nameof(width) + " is too large"); Height = height; Width = width; } diff --git a/src/Microsoft.ML/CSharpApi.cs b/src/Microsoft.ML/CSharpApi.cs index 8d688933f7..e78a5a5cd4 100644 --- a/src/Microsoft.ML/CSharpApi.cs +++ b/src/Microsoft.ML/CSharpApi.cs @@ -1090,16 +1090,16 @@ public void Add(Microsoft.ML.Transforms.HashConverter input, Microsoft.ML.Transf _jsonNodes.Add(Serialize("Transforms.HashConverter", input, output)); } - public Microsoft.ML.Transforms.ImageGreyscale.Output Add(Microsoft.ML.Transforms.ImageGreyscale input) + public Microsoft.ML.Transforms.ImageGrayscale.Output Add(Microsoft.ML.Transforms.ImageGrayscale input) { - var output = new Microsoft.ML.Transforms.ImageGreyscale.Output(); + var output = new Microsoft.ML.Transforms.ImageGrayscale.Output(); Add(input, output); return output; } - public void Add(Microsoft.ML.Transforms.ImageGreyscale input, Microsoft.ML.Transforms.ImageGreyscale.Output output) + public void Add(Microsoft.ML.Transforms.ImageGrayscale input, Microsoft.ML.Transforms.ImageGrayscale.Output output) { - _jsonNodes.Add(Serialize("Transforms.ImageGreyscale", input, output)); + _jsonNodes.Add(Serialize("Transforms.ImageGrayscale", input, output)); } public Microsoft.ML.Transforms.ImageLoader.Output Add(Microsoft.ML.Transforms.ImageLoader input) @@ -11927,7 +11927,7 @@ public HashConverterPipelineStep(Output output) namespace Transforms { - public sealed partial class ImageGreyscaleTransformColumn : OneToOneColumn, IOneToOneColumn + public sealed partial class ImageGrayscaleTransformColumn : OneToOneColumn, IOneToOneColumn { /// /// Name of the new column @@ -11944,14 +11944,14 @@ public sealed partial class ImageGreyscaleTransformColumn : OneToOneColumn /// Convert image into grayscale. /// - public sealed partial class ImageGreyscale : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImageGrayscale : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { - public ImageGreyscale() + public ImageGrayscale() { } - public ImageGreyscale(params string[] inputColumns) + public ImageGrayscale(params string[] inputColumns) { if (inputColumns != null) { @@ -11962,7 +11962,7 @@ public ImageGreyscale(params string[] inputColumns) } } - public ImageGreyscale(params (string inputColumn, string outputColumn)[] inputOutputColumns) + public ImageGrayscale(params (string inputColumn, string outputColumn)[] inputOutputColumns) { if (inputOutputColumns != null) { @@ -11975,15 +11975,15 @@ public ImageGreyscale(params (string inputColumn, string outputColumn)[] inputOu public void AddColumn(string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); Column = list.ToArray(); } public void AddColumn(string outputColumn, string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); Column = list.ToArray(); } @@ -11991,7 +11991,7 @@ public void AddColumn(string outputColumn, string inputColumn) /// /// New column definition(s) (optional form: name:src) /// - public ImageGreyscaleTransformColumn[] Column { get; set; } + public ImageGrayscaleTransformColumn[] Column { get; set; } /// /// Input dataset @@ -12020,18 +12020,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(ImageGreyscale)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImageGrayscale)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new ImageGreyscalePipelineStep(output); + return new ImageGrayscalePipelineStep(output); } - private class ImageGreyscalePipelineStep : ILearningPipelineDataStep + private class ImageGrayscalePipelineStep : ILearningPipelineDataStep { - public ImageGreyscalePipelineStep(Output output) + public ImageGrayscalePipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -12061,7 +12061,7 @@ public sealed partial class ImageLoaderTransformColumn : OneToOneColumn - /// Load images from a file. + /// Load images from a files. /// public sealed partial class ImageLoader : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { diff --git a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv index 92c2dcc96b..91090ae3cb 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv +++ b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv @@ -87,7 +87,7 @@ Transforms.FeatureSelectorByCount Selects the slots for which the count of non-d Transforms.FeatureSelectorByMutualInformation Selects the top k slots across all specified columns ordered by their mutual information with the label column. Microsoft.ML.Runtime.EntryPoints.SelectFeatures MutualInformationSelect Microsoft.ML.Runtime.Data.MutualInformationFeatureSelectionTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.GlobalContrastNormalizer Performs a global contrast normalization on input values: Y = (s * X - M) / D, where s is a scale, M is mean and D is either L2 norm or standard deviation. Microsoft.ML.Runtime.Data.LpNormalization GcNormalize Microsoft.ML.Runtime.Data.LpNormNormalizerTransform+GcnArguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.HashConverter Converts column values into hashes. This transform accepts both numeric and text inputs, both single and vector-valued columns. This is a part of the Dracula transform. Microsoft.ML.Runtime.Data.HashJoin Apply Microsoft.ML.Runtime.Data.HashJoinTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput -Transforms.ImageGreyscale Convert image into grayscale. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageGreyscale Microsoft.ML.Runtime.ImageAnalytics.ImageGreyscaleTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImageGrayscale Convert image into grayscale. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageGrayscale Microsoft.ML.Runtime.ImageAnalytics.ImageGrayscaleTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImageLoader Load images from a file. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageLoader Microsoft.ML.Runtime.ImageAnalytics.ImageLoaderTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImagePixelExtractor Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImagePixelExtractor Microsoft.ML.Runtime.ImageAnalytics.ImagePixelExtractorTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImageResizer Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageResizer Microsoft.ML.Runtime.ImageAnalytics.ImageResizerTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput diff --git a/test/BaselineOutput/Common/EntryPoints/core_manifest.json b/test/BaselineOutput/Common/EntryPoints/core_manifest.json index 3ff8a1e75a..d06a4a710f 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_manifest.json +++ b/test/BaselineOutput/Common/EntryPoints/core_manifest.json @@ -17841,10 +17841,10 @@ ] }, { - "Name": "Transforms.ImageGreyscale", + "Name": "Transforms.ImageGrayscale", "Desc": "Convert image into grayscale.", "FriendlyName": "Image Greyscale Transform", - "ShortName": "ImageGreyscaleTransform", + "ShortName": "ImageGrayscaleTransform", "Inputs": [ { "Name": "Column", diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs index 51accb9903..2f3e3567a3 100644 --- a/test/Microsoft.ML.Tests/ImagesTests.cs +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -87,10 +87,10 @@ public void TestGreyscaleTransformImages() } }, images); - var grey = new ImageGreyscaleTransform(env, new ImageGreyscaleTransform.Arguments() + var grey = new ImageGrayscaleTransform(env, new ImageGrayscaleTransform.Arguments() { - Column = new ImageGreyscaleTransform.Column[1]{ - new ImageGreyscaleTransform.Column() { Name= "ImageGrey", Source = "ImageCropped"} + Column = new ImageGrayscaleTransform.Column[1]{ + new ImageGrayscaleTransform.Column() { Name= "ImageGrey", Source = "ImageCropped"} } }, cropped); From dbd2e42327e636a96cd2b4d74872c5531ce77f5b Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Fri, 20 Jul 2018 12:09:12 -0700 Subject: [PATCH 10/16] small cleanup --- .../ImageLoaderTransform.cs | 9 ++++----- .../ImagePixelExtractorTransform.cs | 12 ++++-------- .../ImageResizerTransform.cs | 9 ++++----- .../VectorToImageTransform.cs | 9 ++++----- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index 53b515f817..19d4b37e22 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -65,9 +65,10 @@ private static VersionInfo GetVersionInfo() { return new VersionInfo( modelSignature: "IMGLOADT", - verWrittenCur: 0x00010001, // Initial - verReadableCur: 0x00010001, - verWeCanReadBack: 0x00010001, + //verWrittenCur: 0x00010001, // Initial + verWrittenCur: 0x00010002, // Swith from OpenCV to Bitmap + verReadableCur: 0x00010002, + verWeCanReadBack: 0x00010002, loaderSignature: LoaderSignature); } @@ -76,9 +77,7 @@ private static VersionInfo GetVersionInfo() private const string RegistrationName = "ImageLoader"; - /// /// Public constructor corresponding to SignatureDataTransform. - /// public ImageLoaderTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, TestIsText) { diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs index 01f8c18490..2d93950e0b 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -228,9 +228,10 @@ private static VersionInfo GetVersionInfo() { return new VersionInfo( modelSignature: "IMGPXEXT", - verWrittenCur: 0x00010001, // Initial - verReadableCur: 0x00010001, - verWeCanReadBack: 0x00010001, + //verWrittenCur: 0x00010001, // Initial + verWrittenCur: 0x00010002, // Swith from OpenCV to Bitmap + verReadableCur: 0x00010002, + verWeCanReadBack: 0x00010002, loaderSignature: LoaderSignature); } @@ -239,9 +240,7 @@ private static VersionInfo GetVersionInfo() private readonly ColInfoEx[] _exes; private readonly VectorType[] _types; - /// /// Public constructor corresponding to SignatureDataTransform. - /// public ImagePixelExtractorTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, Contracts.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") @@ -439,7 +438,6 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo { int idst = 0; for (int y = 0; y < h; ++y) - { for (int x = 0; x < w; x++) { var pb = src.GetPixel(y, x); @@ -465,8 +463,6 @@ private ValueGetter> GetGetterCore(IRow input, int iinfo if (b) { vf[idst++] = (pb.G - offset) * scale; } } } - } - Host.Assert(idst == size); } else diff --git a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs index c47061934e..4c15809985 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs @@ -132,9 +132,10 @@ private static VersionInfo GetVersionInfo() { return new VersionInfo( modelSignature: "IMGSCALF", - verWrittenCur: 0x00010001, // Initial - verReadableCur: 0x00010001, - verWeCanReadBack: 0x00010001, + //verWrittenCur: 0x00010001, // Initial + verWrittenCur: 0x00010002, // Swith from OpenCV to Bitmap + verReadableCur: 0x00010002, + verWeCanReadBack: 0x00010002, loaderSignature: LoaderSignature); } @@ -143,9 +144,7 @@ private static VersionInfo GetVersionInfo() // This is parallel to Infos. private readonly ColInfoEx[] _exes; - /// /// Public constructor corresponding to SignatureDataTransform. - /// public ImageResizerTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") { diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index 3fbd6cf039..a8d4cfd2cc 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -232,9 +232,10 @@ private static VersionInfo GetVersionInfo() { return new VersionInfo( modelSignature: "VECTOIMG", - verWrittenCur: 0x00010001, // Initial - verReadableCur: 0x00010001, - verWeCanReadBack: 0x00010001, + //verWrittenCur: 0x00010001, // Initial + verWrittenCur: 0x00010002, // Swith from OpenCV to Bitmap + verReadableCur: 0x00010002, + verWeCanReadBack: 0x00010002, loaderSignature: LoaderSignature); } @@ -243,9 +244,7 @@ private static VersionInfo GetVersionInfo() private readonly ColInfoEx[] _exes; private readonly ImageType[] _types; - /// /// Public constructor corresponding to SignatureDataTransform. - /// public VectorToImageTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, Contracts.CheckRef(args, nameof(args)).Column, input, t => t is VectorType ? null : "Expected VectorType type") From a31a4b1d40ed430d42ffe755b462844edb354c07 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Fri, 20 Jul 2018 14:37:33 -0700 Subject: [PATCH 11/16] more merge with master --- Microsoft.ML.sln | 11 + src/Microsoft.ML/CSharpApi.cs | 1135 +++++++++++++++-- .../Common/EntryPoints/core_ep-list.tsv | 2 +- .../Common/EntryPoints/core_manifest.json | 2 +- 4 files changed, 1023 insertions(+), 127 deletions(-) diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln index 6ad93c3886..e9e4e6c33e 100644 --- a/Microsoft.ML.sln +++ b/Microsoft.ML.sln @@ -88,6 +88,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.ML.CpuMath", "Mic pkg\Microsoft.ML.CpuMath\Microsoft.ML.CpuMath.symbols.nupkgproj = pkg\Microsoft.ML.CpuMath\Microsoft.ML.CpuMath.symbols.nupkgproj EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.ImageAnalytics", "src\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.csproj", "{8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -304,6 +306,14 @@ Global {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}.Release|Any CPU.Build.0 = Release|Any CPU {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}.Release-Intrinsics|Any CPU.ActiveCfg = Release|Any CPU {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27}.Release-Intrinsics|Any CPU.Build.0 = Release|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Debug-Intrinsics|Any CPU.ActiveCfg = Debug|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Debug-Intrinsics|Any CPU.Build.0 = Debug|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Release|Any CPU.Build.0 = Release|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Release-Intrinsics|Any CPU.ActiveCfg = Release|Any CPU + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975}.Release-Intrinsics|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,6 +350,7 @@ Global {001F3B4E-FBE4-4001-AFD2-A6A989CD1C25} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {DCF46B79-1FDB-4DBA-A263-D3D64E3AAA27} = {09EADF06-BE25-4228-AB53-95AE3E15B530} {BF66A305-DF10-47E4-8D81-42049B149D2B} = {D3D38B03-B557-484D-8348-8BADEE4DF592} + {8424DCB2-AA5C-46C7-81D3-1B0DB8C55975} = {09EADF06-BE25-4228-AB53-95AE3E15B530} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D} diff --git a/src/Microsoft.ML/CSharpApi.cs b/src/Microsoft.ML/CSharpApi.cs index feded5243d..f569c3d52d 100644 --- a/src/Microsoft.ML/CSharpApi.cs +++ b/src/Microsoft.ML/CSharpApi.cs @@ -1090,6 +1090,54 @@ public void Add(Microsoft.ML.Transforms.HashConverter input, Microsoft.ML.Transf _jsonNodes.Add(Serialize("Transforms.HashConverter", input, output)); } + public Microsoft.ML.Transforms.ImageGrayscale.Output Add(Microsoft.ML.Transforms.ImageGrayscale input) + { + var output = new Microsoft.ML.Transforms.ImageGrayscale.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImageGrayscale input, Microsoft.ML.Transforms.ImageGrayscale.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImageGrayscale", input, output)); + } + + public Microsoft.ML.Transforms.ImageLoader.Output Add(Microsoft.ML.Transforms.ImageLoader input) + { + var output = new Microsoft.ML.Transforms.ImageLoader.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImageLoader input, Microsoft.ML.Transforms.ImageLoader.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImageLoader", input, output)); + } + + public Microsoft.ML.Transforms.ImagePixelExtractor.Output Add(Microsoft.ML.Transforms.ImagePixelExtractor input) + { + var output = new Microsoft.ML.Transforms.ImagePixelExtractor.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImagePixelExtractor input, Microsoft.ML.Transforms.ImagePixelExtractor.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImagePixelExtractor", input, output)); + } + + public Microsoft.ML.Transforms.ImageResizer.Output Add(Microsoft.ML.Transforms.ImageResizer input) + { + var output = new Microsoft.ML.Transforms.ImageResizer.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.ImageResizer input, Microsoft.ML.Transforms.ImageResizer.Output output) + { + _jsonNodes.Add(Serialize("Transforms.ImageResizer", input, output)); + } + public Microsoft.ML.Transforms.KeyToTextConverter.Output Add(Microsoft.ML.Transforms.KeyToTextConverter input) { var output = new Microsoft.ML.Transforms.KeyToTextConverter.Output(); @@ -1522,6 +1570,18 @@ public void Add(Microsoft.ML.Transforms.TwoHeterogeneousModelCombiner input, Mic _jsonNodes.Add(Serialize("Transforms.TwoHeterogeneousModelCombiner", input, output)); } + public Microsoft.ML.Transforms.VectorToImage.Output Add(Microsoft.ML.Transforms.VectorToImage input) + { + var output = new Microsoft.ML.Transforms.VectorToImage.Output(); + Add(input, output); + return output; + } + + public void Add(Microsoft.ML.Transforms.VectorToImage input, Microsoft.ML.Transforms.VectorToImage.Output output) + { + _jsonNodes.Add(Serialize("Transforms.VectorToImage", input, output)); + } + public Microsoft.ML.Transforms.WordTokenizer.Output Add(Microsoft.ML.Transforms.WordTokenizer input) { var output = new Microsoft.ML.Transforms.WordTokenizer.Output(); @@ -11875,7 +11935,7 @@ public HashConverterPipelineStep(Output output) namespace Transforms { - public sealed partial class KeyToValueTransformColumn : OneToOneColumn, IOneToOneColumn + public sealed partial class ImageGrayscaleTransformColumn : OneToOneColumn, IOneToOneColumn { /// /// Name of the new column @@ -11889,15 +11949,17 @@ public sealed partial class KeyToValueTransformColumn : OneToOneColumn - public sealed partial class KeyToTextConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + /// + /// Convert image into grayscale. + /// + public sealed partial class ImageGrayscale : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { - public KeyToTextConverter() + public ImageGrayscale() { } - public KeyToTextConverter(params string[] inputColumns) + public ImageGrayscale(params string[] inputColumns) { if (inputColumns != null) { @@ -11908,7 +11970,7 @@ public KeyToTextConverter(params string[] inputColumns) } } - public KeyToTextConverter(params (string inputColumn, string outputColumn)[] inputOutputColumns) + public ImageGrayscale(params (string inputColumn, string outputColumn)[] inputOutputColumns) { if (inputOutputColumns != null) { @@ -11921,15 +11983,15 @@ public KeyToTextConverter(params (string inputColumn, string outputColumn)[] inp public void AddColumn(string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); Column = list.ToArray(); } public void AddColumn(string outputColumn, string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); Column = list.ToArray(); } @@ -11937,7 +11999,7 @@ public void AddColumn(string outputColumn, string inputColumn) /// /// New column definition(s) (optional form: name:src) /// - public KeyToValueTransformColumn[] Column { get; set; } + public ImageGrayscaleTransformColumn[] Column { get; set; } /// /// Input dataset @@ -11966,18 +12028,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(KeyToTextConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImageGrayscale)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new KeyToTextConverterPipelineStep(output); + return new ImageGrayscalePipelineStep(output); } - private class KeyToTextConverterPipelineStep : ILearningPipelineDataStep + private class ImageGrayscalePipelineStep : ILearningPipelineDataStep { - public KeyToTextConverterPipelineStep(Output output) + public ImageGrayscalePipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -11992,22 +12054,76 @@ public KeyToTextConverterPipelineStep(Output output) namespace Transforms { + public sealed partial class ImageLoaderTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + /// - /// Transforms the label to either key or bool (if needed) to make it suitable for classification. + /// Load images from a files. /// - public sealed partial class LabelColumnKeyBooleanConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImageLoader : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { + public ImageLoader() + { + } + + public ImageLoader(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public ImageLoader(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + /// - /// Convert the key values to text + /// New column definition(s) (optional form: name:src) /// - public bool TextKeyValues { get; set; } = true; + public ImageLoaderTransformColumn[] Column { get; set; } /// - /// The label column + /// Folder where to search for images /// - public string LabelColumn { get; set; } + public string ImageFolder { get; set; } /// /// Input dataset @@ -12036,18 +12152,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(LabelColumnKeyBooleanConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImageLoader)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new LabelColumnKeyBooleanConverterPipelineStep(output); + return new ImageLoaderPipelineStep(output); } - private class LabelColumnKeyBooleanConverterPipelineStep : ILearningPipelineDataStep + private class ImageLoaderPipelineStep : ILearningPipelineDataStep { - public LabelColumnKeyBooleanConverterPipelineStep(Output output) + public ImageLoaderPipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -12062,12 +12178,47 @@ public LabelColumnKeyBooleanConverterPipelineStep(Output output) namespace Transforms { - public sealed partial class LabelIndicatorTransformColumn : OneToOneColumn, IOneToOneColumn + public sealed partial class ImagePixelExtractorTransformColumn : OneToOneColumn, IOneToOneColumn { /// - /// The positive example class for binary classification. + /// Whether to use alpha channel /// - public int? ClassIndex { get; set; } + public bool? UseAlpha { get; set; } + + /// + /// Whether to use red channel + /// + public bool? UseRed { get; set; } + + /// + /// Whether to use green channel + /// + public bool? UseGreen { get; set; } + + /// + /// Whether to use blue channel + /// + public bool? UseBlue { get; set; } + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool? InterleaveArgb { get; set; } + + /// + /// Whether to convert to floating point + /// + public bool? Convert { get; set; } + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } /// /// Name of the new column @@ -12082,16 +12233,16 @@ public sealed partial class LabelIndicatorTransformColumn : OneToOneColumn - public sealed partial class LabelIndicator : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + public sealed partial class ImagePixelExtractor : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { - public LabelIndicator() + public ImagePixelExtractor() { } - public LabelIndicator(params string[] inputColumns) + public ImagePixelExtractor(params string[] inputColumns) { if (inputColumns != null) { @@ -12102,7 +12253,7 @@ public LabelIndicator(params string[] inputColumns) } } - public LabelIndicator(params (string inputColumn, string outputColumn)[] inputOutputColumns) + public ImagePixelExtractor(params (string inputColumn, string outputColumn)[] inputOutputColumns) { if (inputOutputColumns != null) { @@ -12115,15 +12266,15 @@ public LabelIndicator(params (string inputColumn, string outputColumn)[] inputOu public void AddColumn(string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); Column = list.ToArray(); } public void AddColumn(string outputColumn, string inputColumn) { - var list = Column == null ? new List() : new List(Column); - list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); Column = list.ToArray(); } @@ -12131,77 +12282,47 @@ public void AddColumn(string outputColumn, string inputColumn) /// /// New column definition(s) (optional form: name:src) /// - public LabelIndicatorTransformColumn[] Column { get; set; } + public ImagePixelExtractorTransformColumn[] Column { get; set; } /// - /// Label of the positive class. + /// Whether to use alpha channel /// - public int ClassIndex { get; set; } + public bool UseAlpha { get; set; } = false; /// - /// Input dataset + /// Whether to use red channel /// - public Var Data { get; set; } = new Var(); - - - public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput - { - /// - /// Transformed dataset - /// - public Var OutputData { get; set; } = new Var(); - - /// - /// Transform model - /// - public Var Model { get; set; } = new Var(); - - } - public Var GetInputData() => Data; - - public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) - { - if (previousStep != null) - { - if (!(previousStep is ILearningPipelineDataStep dataStep)) - { - throw new InvalidOperationException($"{ nameof(LabelIndicator)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); - } - - Data = dataStep.Data; - } - Output output = experiment.Add(this); - return new LabelIndicatorPipelineStep(output); - } + public bool UseRed { get; set; } = true; - private class LabelIndicatorPipelineStep : ILearningPipelineDataStep - { - public LabelIndicatorPipelineStep(Output output) - { - Data = output.OutputData; - Model = output.Model; - } + /// + /// Whether to use green channel + /// + public bool UseGreen { get; set; } = true; - public Var Data { get; } - public Var Model { get; } - } - } - } + /// + /// Whether to use blue channel + /// + public bool UseBlue { get; set; } = true; - namespace Transforms - { + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool InterleaveArgb { get; set; } = false; - /// - /// Transforms the label to float to make it suitable for regression. - /// - public sealed partial class LabelToFloatConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem - { + /// + /// Whether to convert to floating point + /// + public bool Convert { get; set; } = true; + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } /// - /// The label column + /// Scale factor /// - public string LabelColumn { get; set; } + public float? Scale { get; set; } /// /// Input dataset @@ -12230,18 +12351,18 @@ public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Exper { if (!(previousStep is ILearningPipelineDataStep dataStep)) { - throw new InvalidOperationException($"{ nameof(LabelToFloatConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + throw new InvalidOperationException($"{ nameof(ImagePixelExtractor)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); } Data = dataStep.Data; } Output output = experiment.Add(this); - return new LabelToFloatConverterPipelineStep(output); + return new ImagePixelExtractorPipelineStep(output); } - private class LabelToFloatConverterPipelineStep : ILearningPipelineDataStep + private class ImagePixelExtractorPipelineStep : ILearningPipelineDataStep { - public LabelToFloatConverterPipelineStep(Output output) + public ImagePixelExtractorPipelineStep(Output output) { Data = output.OutputData; Model = output.Model; @@ -12255,56 +12376,611 @@ public LabelToFloatConverterPipelineStep(Output output) namespace Transforms { + public enum ImageResizerTransformResizingKind : byte + { + IsoPad = 0, + IsoCrop = 1 + } - public sealed partial class LdaTransformColumn : OneToOneColumn, IOneToOneColumn + public enum ImageResizerTransformAnchor : byte { - /// - /// The number of topics in the LDA - /// - public int? NumTopic { get; set; } + Right = 0, + Left = 1, + Top = 2, + Bottom = 3, + Center = 4 + } - /// - /// Dirichlet prior on document-topic vectors - /// - public float? AlphaSum { get; set; } + public sealed partial class ImageResizerTransformColumn : OneToOneColumn, IOneToOneColumn + { /// - /// Dirichlet prior on vocab-topic vectors + /// Width of the resized image /// - public float? Beta { get; set; } + public int? ImageWidth { get; set; } /// - /// Number of Metropolis Hasting step + /// Height of the resized image /// - public int? Mhstep { get; set; } + public int? ImageHeight { get; set; } /// - /// Number of iterations + /// Resizing method /// - public int? NumIterations { get; set; } + public ImageResizerTransformResizingKind? Resizing { get; set; } /// - /// Compute log likelihood over local dataset on this iteration interval + /// Anchor for cropping /// - public int? LikelihoodInterval { get; set; } + public ImageResizerTransformAnchor? CropAnchor { get; set; } /// - /// The number of training threads + /// Name of the new column /// - public int? NumThreads { get; set; } + public string Name { get; set; } /// - /// The threshold of maximum count of tokens per doc + /// Name of the source column /// - public int? NumMaxDocToken { get; set; } + public string Source { get; set; } - /// - /// The number of words to summarize the topic - /// - public int? NumSummaryTermPerTopic { get; set; } + } - /// - /// The number of burn-in iterations + /// + /// Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image. + /// + public sealed partial class ImageResizer : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public ImageResizer() + { + } + + public ImageResizer(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public ImageResizer(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public ImageResizerTransformColumn[] Column { get; set; } + + /// + /// Resized width of the image + /// + public int ImageWidth { get; set; } + + /// + /// Resized height of the image + /// + public int ImageHeight { get; set; } + + /// + /// Resizing method + /// + public ImageResizerTransformResizingKind Resizing { get; set; } = ImageResizerTransformResizingKind.IsoCrop; + + /// + /// Anchor for cropping + /// + public ImageResizerTransformAnchor CropAnchor { get; set; } = ImageResizerTransformAnchor.Center; + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(ImageResizer)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new ImageResizerPipelineStep(output); + } + + private class ImageResizerPipelineStep : ILearningPipelineDataStep + { + public ImageResizerPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + public sealed partial class KeyToValueTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + + /// + public sealed partial class KeyToTextConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public KeyToTextConverter() + { + } + + public KeyToTextConverter(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public KeyToTextConverter(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public KeyToValueTransformColumn[] Column { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(KeyToTextConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new KeyToTextConverterPipelineStep(output); + } + + private class KeyToTextConverterPipelineStep : ILearningPipelineDataStep + { + public KeyToTextConverterPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + /// + /// Transforms the label to either key or bool (if needed) to make it suitable for classification. + /// + public sealed partial class LabelColumnKeyBooleanConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + + /// + /// Convert the key values to text + /// + public bool TextKeyValues { get; set; } = true; + + /// + /// The label column + /// + public string LabelColumn { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(LabelColumnKeyBooleanConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new LabelColumnKeyBooleanConverterPipelineStep(output); + } + + private class LabelColumnKeyBooleanConverterPipelineStep : ILearningPipelineDataStep + { + public LabelColumnKeyBooleanConverterPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + public sealed partial class LabelIndicatorTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// The positive example class for binary classification. + /// + public int? ClassIndex { get; set; } + + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + + /// + /// Label remapper used by OVA + /// + public sealed partial class LabelIndicator : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public LabelIndicator() + { + } + + public LabelIndicator(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public LabelIndicator(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public LabelIndicatorTransformColumn[] Column { get; set; } + + /// + /// Label of the positive class. + /// + public int ClassIndex { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(LabelIndicator)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new LabelIndicatorPipelineStep(output); + } + + private class LabelIndicatorPipelineStep : ILearningPipelineDataStep + { + public LabelIndicatorPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + /// + /// Transforms the label to float to make it suitable for regression. + /// + public sealed partial class LabelToFloatConverter : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + + /// + /// The label column + /// + public string LabelColumn { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(LabelToFloatConverter)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new LabelToFloatConverterPipelineStep(output); + } + + private class LabelToFloatConverterPipelineStep : ILearningPipelineDataStep + { + public LabelToFloatConverterPipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + + namespace Transforms + { + + public sealed partial class LdaTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// The number of topics in the LDA + /// + public int? NumTopic { get; set; } + + /// + /// Dirichlet prior on document-topic vectors + /// + public float? AlphaSum { get; set; } + + /// + /// Dirichlet prior on vocab-topic vectors + /// + public float? Beta { get; set; } + + /// + /// Number of Metropolis Hasting step + /// + public int? Mhstep { get; set; } + + /// + /// Number of iterations + /// + public int? NumIterations { get; set; } + + /// + /// Compute log likelihood over local dataset on this iteration interval + /// + public int? LikelihoodInterval { get; set; } + + /// + /// The number of training threads + /// + public int? NumThreads { get; set; } + + /// + /// The threshold of maximum count of tokens per doc + /// + public int? NumMaxDocToken { get; set; } + + /// + /// The number of words to summarize the topic + /// + public int? NumSummaryTermPerTopic { get; set; } + + /// + /// The number of burn-in iterations /// public int? NumBurninIterations { get; set; } = 10; @@ -15431,6 +16107,215 @@ public sealed class Output } } + namespace Transforms + { + + public sealed partial class VectorToImageTransformColumn : OneToOneColumn, IOneToOneColumn + { + /// + /// Whether to use alpha channel + /// + public bool? ContainsAlpha { get; set; } + + /// + /// Whether to use red channel + /// + public bool? ContainsRed { get; set; } + + /// + /// Whether to use green channel + /// + public bool? ContainsGreen { get; set; } + + /// + /// Whether to use blue channel + /// + public bool? ContainsBlue { get; set; } + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool? InterleaveArgb { get; set; } + + /// + /// Width of the image + /// + public int? ImageWidth { get; set; } + + /// + /// Height of the image + /// + public int? ImageHeight { get; set; } + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } + + /// + /// Name of the new column + /// + public string Name { get; set; } + + /// + /// Name of the source column + /// + public string Source { get; set; } + + } + + /// + /// Converts vector array into image type. + /// + public sealed partial class VectorToImage : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem + { + + public VectorToImage() + { + } + + public VectorToImage(params string[] inputColumns) + { + if (inputColumns != null) + { + foreach (string input in inputColumns) + { + AddColumn(input); + } + } + } + + public VectorToImage(params (string inputColumn, string outputColumn)[] inputOutputColumns) + { + if (inputOutputColumns != null) + { + foreach (var inputOutput in inputOutputColumns) + { + AddColumn(inputOutput.outputColumn, inputOutput.inputColumn); + } + } + } + + public void AddColumn(string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(inputColumn)); + Column = list.ToArray(); + } + + public void AddColumn(string outputColumn, string inputColumn) + { + var list = Column == null ? new List() : new List(Column); + list.Add(OneToOneColumn.Create(outputColumn, inputColumn)); + Column = list.ToArray(); + } + + + /// + /// New column definition(s) (optional form: name:src) + /// + public VectorToImageTransformColumn[] Column { get; set; } + + /// + /// Whether to use alpha channel + /// + public bool ContainsAlpha { get; set; } = false; + + /// + /// Whether to use red channel + /// + public bool ContainsRed { get; set; } = true; + + /// + /// Whether to use green channel + /// + public bool ContainsGreen { get; set; } = true; + + /// + /// Whether to use blue channel + /// + public bool ContainsBlue { get; set; } = true; + + /// + /// Whether to separate each channel or interleave in ARGB order + /// + public bool InterleaveArgb { get; set; } = false; + + /// + /// Width of the image + /// + public int ImageWidth { get; set; } + + /// + /// Height of the image + /// + public int ImageHeight { get; set; } + + /// + /// Offset (pre-scale) + /// + public float? Offset { get; set; } + + /// + /// Scale factor + /// + public float? Scale { get; set; } + + /// + /// Input dataset + /// + public Var Data { get; set; } = new Var(); + + + public sealed class Output : Microsoft.ML.Runtime.EntryPoints.CommonOutputs.ITransformOutput + { + /// + /// Transformed dataset + /// + public Var OutputData { get; set; } = new Var(); + + /// + /// Transform model + /// + public Var Model { get; set; } = new Var(); + + } + public Var GetInputData() => Data; + + public ILearningPipelineStep ApplyStep(ILearningPipelineStep previousStep, Experiment experiment) + { + if (previousStep != null) + { + if (!(previousStep is ILearningPipelineDataStep dataStep)) + { + throw new InvalidOperationException($"{ nameof(VectorToImage)} only supports an { nameof(ILearningPipelineDataStep)} as an input."); + } + + Data = dataStep.Data; + } + Output output = experiment.Add(this); + return new VectorToImagePipelineStep(output); + } + + private class VectorToImagePipelineStep : ILearningPipelineDataStep + { + public VectorToImagePipelineStep(Output output) + { + Data = output.OutputData; + Model = output.Model; + } + + public Var Data { get; } + public Var Model { get; } + } + } + } + namespace Transforms { diff --git a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv index 91090ae3cb..5408f93f2b 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv +++ b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv @@ -88,7 +88,7 @@ Transforms.FeatureSelectorByMutualInformation Selects the top k slots across all Transforms.GlobalContrastNormalizer Performs a global contrast normalization on input values: Y = (s * X - M) / D, where s is a scale, M is mean and D is either L2 norm or standard deviation. Microsoft.ML.Runtime.Data.LpNormalization GcNormalize Microsoft.ML.Runtime.Data.LpNormNormalizerTransform+GcnArguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.HashConverter Converts column values into hashes. This transform accepts both numeric and text inputs, both single and vector-valued columns. This is a part of the Dracula transform. Microsoft.ML.Runtime.Data.HashJoin Apply Microsoft.ML.Runtime.Data.HashJoinTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImageGrayscale Convert image into grayscale. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageGrayscale Microsoft.ML.Runtime.ImageAnalytics.ImageGrayscaleTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput -Transforms.ImageLoader Load images from a file. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageLoader Microsoft.ML.Runtime.ImageAnalytics.ImageLoaderTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImageLoader Load images from a files. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageLoader Microsoft.ML.Runtime.ImageAnalytics.ImageLoaderTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImagePixelExtractor Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImagePixelExtractor Microsoft.ML.Runtime.ImageAnalytics.ImagePixelExtractorTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImageResizer Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageResizer Microsoft.ML.Runtime.ImageAnalytics.ImageResizerTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.KeyToTextConverter KeyToValueTransform utilizes KeyValues metadata to map key indices to the corresponding values in the KeyValues metadata. Microsoft.ML.Runtime.Data.Categorical KeyToText Microsoft.ML.Runtime.Data.KeyToValueTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput diff --git a/test/BaselineOutput/Common/EntryPoints/core_manifest.json b/test/BaselineOutput/Common/EntryPoints/core_manifest.json index 93fc3e76f7..14a7c0fc40 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_manifest.json +++ b/test/BaselineOutput/Common/EntryPoints/core_manifest.json @@ -17918,7 +17918,7 @@ }, { "Name": "Transforms.ImageLoader", - "Desc": "Load images from a file.", + "Desc": "Load images from a files.", "FriendlyName": "Image Loader Transform", "ShortName": "ImageLoaderTransform", "Inputs": [ From 50d4ac0ce4f40929adac462986c427f44d8912e2 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 23 Jul 2018 10:30:46 -0700 Subject: [PATCH 12/16] assert not null, different image height and image width --- test/Microsoft.ML.Tests/ImagesTests.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs index 2f3e3567a3..1ca0d4f07a 100644 --- a/test/Microsoft.ML.Tests/ImagesTests.cs +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -54,11 +54,11 @@ public void TestSaveImages() { pathGetter(ref path); bitmapCropGetter(ref bitmap); + Assert.NotNull(bitmap); var fileToSave = GetOutputPath(Path.GetFileNameWithoutExtension(path.ToString()) + ".cropped.jpg"); bitmap.Save(fileToSave, System.Drawing.Imaging.ImageFormat.Jpeg); } } - } } @@ -67,7 +67,7 @@ public void TestGreyscaleTransformImages() { using (var env = new TlcEnvironment()) { - var imageHeight = 100; + var imageHeight = 150; var imageWidth = 100; var dataFile = GetDataPath("images/images.tsv"); var imageFolder = Path.GetDirectoryName(dataFile); @@ -102,6 +102,7 @@ public void TestGreyscaleTransformImages() while (cursor.MoveNext()) { bitmapGetter(ref bitmap); + Assert.NotNull(bitmap); for (int x = 0; x < imageWidth; x++) for (int y = 0; y < imageHeight; y++) { @@ -111,9 +112,7 @@ public void TestGreyscaleTransformImages() } } } - } - } [Fact] @@ -122,7 +121,7 @@ public void TestBackAndForthConversion() using (var env = new TlcEnvironment()) { var imageHeight = 100; - var imageWidth = 100; + var imageWidth = 130; var dataFile = GetDataPath("images/images.tsv"); var imageFolder = Path.GetDirectoryName(dataFile); var data = env.CreateLoader("Text{col=ImagePath:TX:0 col=Name:TX:1}", new MultiFileSource(dataFile)); @@ -167,16 +166,16 @@ public void TestBackAndForthConversion() while (cursor.MoveNext()) { bitmapGetter(ref restoredBitmap); + Assert.NotNull(restoredBitmap); bitmapCropGetter(ref croppedBitmap); + Assert.NotNull(croppedBitmap); for (int x = 0; x < imageWidth; x++) for (int y = 0; y < imageHeight; y++) { Assert.True(croppedBitmap.GetPixel(x, y) == restoredBitmap.GetPixel(x, y)); } } - } - } } } From 516a1891ad643336e8fb0fdee251a87dc366e2a4 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 23 Jul 2018 13:23:35 -0700 Subject: [PATCH 13/16] bump build --- test/Microsoft.ML.Tests/ImagesTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Microsoft.ML.Tests/ImagesTests.cs b/test/Microsoft.ML.Tests/ImagesTests.cs index 1ca0d4f07a..a12032400a 100644 --- a/test/Microsoft.ML.Tests/ImagesTests.cs +++ b/test/Microsoft.ML.Tests/ImagesTests.cs @@ -35,6 +35,7 @@ public void TestSaveImages() }, ImageFolder = imageFolder }, data); + var cropped = new ImageResizerTransform(env, new ImageResizerTransform.Arguments() { Column = new ImageResizerTransform.Column[1]{ From 41403bca790a264c45523eec01ec73b3123b1997 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Thu, 26 Jul 2018 12:15:16 -0700 Subject: [PATCH 14/16] after merge with master --- Microsoft.ML.sln | 11 ++++++ .../ImageGrayscaleTransform.cs | 8 ++--- .../ImageLoaderTransform.cs | 2 +- .../ImagePixelExtractorTransform.cs | 2 +- .../ImageResizerTransform.cs | 2 +- .../VectorToImageTransform.cs | 35 +++++++++---------- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Microsoft.ML.sln b/Microsoft.ML.sln index 58e24041f1..28d1528d83 100644 --- a/Microsoft.ML.sln +++ b/Microsoft.ML.sln @@ -97,6 +97,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.CodeAnalyzer", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.CodeAnalyzer.Tests", "test\Microsoft.ML.CodeAnalyzer.Tests\Microsoft.ML.CodeAnalyzer.Tests.csproj", "{3E4ABF07-7970-4BE6-B45B-A13D3C397545}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ML.ImageAnalytics", "src\Microsoft.ML.ImageAnalytics\Microsoft.ML.ImageAnalytics.csproj", "{00E38F77-1E61-4CDF-8F97-1417D4E85053}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -329,6 +331,14 @@ Global {3E4ABF07-7970-4BE6-B45B-A13D3C397545}.Release|Any CPU.Build.0 = Release|Any CPU {3E4ABF07-7970-4BE6-B45B-A13D3C397545}.Release-Intrinsics|Any CPU.ActiveCfg = Release|Any CPU {3E4ABF07-7970-4BE6-B45B-A13D3C397545}.Release-Intrinsics|Any CPU.Build.0 = Release|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Debug-Intrinsics|Any CPU.ActiveCfg = Debug|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Debug-Intrinsics|Any CPU.Build.0 = Debug|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Release|Any CPU.Build.0 = Release|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Release-Intrinsics|Any CPU.ActiveCfg = Release|Any CPU + {00E38F77-1E61-4CDF-8F97-1417D4E85053}.Release-Intrinsics|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -367,6 +377,7 @@ Global {BF66A305-DF10-47E4-8D81-42049B149D2B} = {D3D38B03-B557-484D-8348-8BADEE4DF592} {B4E55B2D-2A92-46E7-B72F-E76D6FD83440} = {7F13E156-3EBA-4021-84A5-CD56BA72F99E} {3E4ABF07-7970-4BE6-B45B-A13D3C397545} = {AED9C836-31E3-4F3F-8ABC-929555D3F3C4} + {00E38F77-1E61-4CDF-8F97-1417D4E85053} = {09EADF06-BE25-4228-AB53-95AE3E15B530} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {41165AF1-35BB-4832-A189-73060F82B01D} diff --git a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs index ca4caea1ec..03df690b96 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs @@ -60,7 +60,7 @@ public class Arguments : TransformInputBase private static VersionInfo GetVersionInfo() { return new VersionInfo( - modelSignature: "IMGGREY ", + modelSignature: "IMGGRAYT", verWrittenCur: 0x00010001, // Initial verReadableCur: 0x00010001, verWeCanReadBack: 0x00010001, @@ -69,7 +69,7 @@ private static VersionInfo GetVersionInfo() private const string RegistrationName = "ImageGrayscale"; - /// Public constructor corresponding to SignatureDataTransform. + // Public constructor corresponding to SignatureDataTransform. public ImageGrayscaleTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") { @@ -115,7 +115,7 @@ protected override ColumnType GetColumnTypeCore(int iinfo) return Infos[iinfo].TypeSrc; } - public ColorMatrix GreyscaleColorMatrix = new ColorMatrix( + private static ColorMatrix _grayscaleColorMatrix = new ColorMatrix( new float[][] { new float[] {.3f, .3f, .3f, 0, 0}, @@ -156,7 +156,7 @@ protected override Delegate GetGetterCore(IChannel ch, IRow input, int iinfo, ou dst = new Bitmap(src.Width, src.Height); ImageAttributes attributes = new ImageAttributes(); - attributes.SetColorMatrix(GreyscaleColorMatrix); + attributes.SetColorMatrix(_grayscaleColorMatrix); var srcRectangle = new Rectangle(0, 0, src.Width, src.Height); using (var g = Graphics.FromImage(dst)) { diff --git a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs index 19d4b37e22..2ef993ae99 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageLoaderTransform.cs @@ -77,7 +77,7 @@ private static VersionInfo GetVersionInfo() private const string RegistrationName = "ImageLoader"; - /// Public constructor corresponding to SignatureDataTransform. + // Public constructor corresponding to SignatureDataTransform. public ImageLoaderTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, TestIsText) { diff --git a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs index 2d93950e0b..de0aa98124 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImagePixelExtractorTransform.cs @@ -240,7 +240,7 @@ private static VersionInfo GetVersionInfo() private readonly ColInfoEx[] _exes; private readonly VectorType[] _types; - /// Public constructor corresponding to SignatureDataTransform. + // Public constructor corresponding to SignatureDataTransform. public ImagePixelExtractorTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, Contracts.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") diff --git a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs index 4c15809985..dd1abc9181 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageResizerTransform.cs @@ -144,7 +144,7 @@ private static VersionInfo GetVersionInfo() // This is parallel to Infos. private readonly ColInfoEx[] _exes; - /// Public constructor corresponding to SignatureDataTransform. + // Public constructor corresponding to SignatureDataTransform. public ImageResizerTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, env.CheckRef(args, nameof(args)).Column, input, t => t is ImageType ? null : "Expected Image type") { diff --git a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs index a8d4cfd2cc..b9d35a6cdc 100644 --- a/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/VectorToImageTransform.cs @@ -244,7 +244,7 @@ private static VersionInfo GetVersionInfo() private readonly ColInfoEx[] _exes; private readonly ImageType[] _types; - /// Public constructor corresponding to SignatureDataTransform. + // Public constructor corresponding to SignatureDataTransform. public VectorToImageTransform(IHostEnvironment env, Arguments args, IDataView input) : base(env, RegistrationName, Contracts.CheckRef(args, nameof(args)).Column, input, t => t is VectorType ? null : "Expected VectorType type") @@ -323,7 +323,6 @@ public override void Save(ModelSaveContext ctx) _exes[i].Save(ctx); } - protected override ColumnType GetColumnTypeCore(int iinfo) { Host.Assert(0 <= iinfo & iinfo < Infos.Length); @@ -381,35 +380,35 @@ private ValueGetter GetterFromType(IRow input, int iinfo, ColInf for (int x = 0; x < width; x++) for (int y = 0; y < height; ++y) { - float R = 0; - float G = 0; - float B = 0; - float A = 0; + float red = 0; + float green = 0; + float blue = 0; + float alpha = 0; if (ex.Interleave) { if (ex.Alpha) position++; - if (ex.Red) R = Convert.ToSingle(values[position++]); - if (ex.Green) G = Convert.ToSingle(values[position++]); - if (ex.Blue) B = Convert.ToSingle(values[position++]); + if (ex.Red) red = Convert.ToSingle(values[position++]); + if (ex.Green) green = Convert.ToSingle(values[position++]); + if (ex.Blue) blue = Convert.ToSingle(values[position++]); } else { position = y * width + x; - if (ex.Alpha) { A = Convert.ToSingle(values[position]); position += cpix; } - if (ex.Red) { R = Convert.ToSingle(values[position]); position += cpix; } - if (ex.Green) { G = Convert.ToSingle(values[position]); position += cpix; } - if (ex.Blue) { B = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Alpha) { alpha = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Red) { red = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Green) { green = Convert.ToSingle(values[position]); position += cpix; } + if (ex.Blue) { blue = Convert.ToSingle(values[position]); position += cpix; } } Color pixel; if (!needScale) - pixel = Color.FromArgb((int)A, (int)R, (int)G, (int)B); + pixel = Color.FromArgb((int)alpha, (int)red, (int)green, (int)blue); else { pixel = Color.FromArgb( - (int)((A - offset) * scale), - (int)((R - offset) * scale), - (int)((G - offset) * scale), - (int)((B - offset) * scale)); + (int)((alpha - offset) * scale), + (int)((red - offset) * scale), + (int)((green - offset) * scale), + (int)((blue - offset) * scale)); } dst.SetPixel(x, y, pixel); } From 75c4ba804bc0e9d204f5bb9d40f366975a75c2c9 Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Thu, 26 Jul 2018 13:44:38 -0700 Subject: [PATCH 15/16] readonly field! --- src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs index 03df690b96..7a267cf1b8 100644 --- a/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs +++ b/src/Microsoft.ML.ImageAnalytics/ImageGrayscaleTransform.cs @@ -115,7 +115,7 @@ protected override ColumnType GetColumnTypeCore(int iinfo) return Infos[iinfo].TypeSrc; } - private static ColorMatrix _grayscaleColorMatrix = new ColorMatrix( + private static readonly ColorMatrix _grayscaleColorMatrix = new ColorMatrix( new float[][] { new float[] {.3f, .3f, .3f, 0, 0}, From 476ade55981018cb270bbc4566d3d64eb8daf1bc Mon Sep 17 00:00:00 2001 From: Ivan Matantsev Date: Mon, 30 Jul 2018 13:22:19 -0700 Subject: [PATCH 16/16] update api and ep baselines --- src/Microsoft.ML/CSharpApi.cs | 2 +- test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv | 2 +- test/BaselineOutput/Common/EntryPoints/core_manifest.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.ML/CSharpApi.cs b/src/Microsoft.ML/CSharpApi.cs index fd9d3ffb5a..19713b1fc2 100644 --- a/src/Microsoft.ML/CSharpApi.cs +++ b/src/Microsoft.ML/CSharpApi.cs @@ -12168,7 +12168,7 @@ public sealed partial class ImageLoaderTransformColumn : OneToOneColumn - /// Load images from a files. + /// Load images from files. /// public sealed partial class ImageLoader : Microsoft.ML.Runtime.EntryPoints.CommonInputs.ITransformInput, Microsoft.ML.ILearningPipelineItem { diff --git a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv index 2d4829faff..ae66209596 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv +++ b/test/BaselineOutput/Common/EntryPoints/core_ep-list.tsv @@ -89,7 +89,7 @@ Transforms.FeatureSelectorByMutualInformation Selects the top k slots across all Transforms.GlobalContrastNormalizer Performs a global contrast normalization on input values: Y = (s * X - M) / D, where s is a scale, M is mean and D is either L2 norm or standard deviation. Microsoft.ML.Runtime.Data.LpNormalization GcNormalize Microsoft.ML.Runtime.Data.LpNormNormalizerTransform+GcnArguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.HashConverter Converts column values into hashes. This transform accepts both numeric and text inputs, both single and vector-valued columns. This is a part of the Dracula transform. Microsoft.ML.Runtime.Data.HashJoin Apply Microsoft.ML.Runtime.Data.HashJoinTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImageGrayscale Convert image into grayscale. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageGrayscale Microsoft.ML.Runtime.ImageAnalytics.ImageGrayscaleTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput -Transforms.ImageLoader Load images from a files. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageLoader Microsoft.ML.Runtime.ImageAnalytics.ImageLoaderTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput +Transforms.ImageLoader Load images from files. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageLoader Microsoft.ML.Runtime.ImageAnalytics.ImageLoaderTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImagePixelExtractor Extract color plane(s) from an image. Options include scaling, offset and conversion to floating point. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImagePixelExtractor Microsoft.ML.Runtime.ImageAnalytics.ImagePixelExtractorTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.ImageResizer Scales an image to specified dimensions using one of the three scale types: isotropic with padding, isotropic with cropping or anisotropic. In case of isotropic padding, transparent color is used to pad resulting image. Microsoft.ML.Runtime.ImageAnalytics.EntryPoints.ImageAnalytics ImageResizer Microsoft.ML.Runtime.ImageAnalytics.ImageResizerTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput Transforms.KeyToTextConverter KeyToValueTransform utilizes KeyValues metadata to map key indices to the corresponding values in the KeyValues metadata. Microsoft.ML.Runtime.Data.Categorical KeyToText Microsoft.ML.Runtime.Data.KeyToValueTransform+Arguments Microsoft.ML.Runtime.EntryPoints.CommonOutputs+TransformOutput diff --git a/test/BaselineOutput/Common/EntryPoints/core_manifest.json b/test/BaselineOutput/Common/EntryPoints/core_manifest.json index b7314497da..a8de59de33 100644 --- a/test/BaselineOutput/Common/EntryPoints/core_manifest.json +++ b/test/BaselineOutput/Common/EntryPoints/core_manifest.json @@ -18061,7 +18061,7 @@ }, { "Name": "Transforms.ImageLoader", - "Desc": "Load images from a files.", + "Desc": "Load images from files.", "FriendlyName": "Image Loader Transform", "ShortName": "ImageLoaderTransform", "Inputs": [