{"id":10200,"date":"2019-12-10T01:00:00","date_gmt":"2019-12-09T22:00:00","guid":{"rendered":"https:\/\/handoli.com\/index.php\/2019\/12\/10\/how-to-convert-images-to-mlmultiarray\/"},"modified":"2019-12-10T01:00:00","modified_gmt":"2019-12-09T22:00:00","slug":"how-to-convert-images-to-mlmultiarray","status":"publish","type":"post","link":"https:\/\/handoli.com\/index.php\/2019\/12\/10\/how-to-convert-images-to-mlmultiarray\/","title":{"rendered":"How to convert images to MLMultiArray"},"content":{"rendered":"<p>I see the following questions come up a lot on Stack Overflow, the Apple developer forums, and various Slack groups:<\/p>\n<blockquote>\n<p>My neural network works on images but the Core ML model expects an MLMultiArray object. How do I convert my UIImage to an MLMultiArray?<\/p>\n<p>My neural network outputs an MLMultiArray but how do I convert this back into a UIImage?<\/p>\n<p>My model outputs an image but the UIImage is all black.<\/p>\n<\/blockquote>\n<p>People run into these issues because most training tools, such as Keras or PyTorch, treat images just like any other data \u2014 as an n-dimensional array.<\/p>\n<p><strong>But in Core ML images are special!<\/strong><\/p>\n<p>A common issue is that, after converting a model to Core ML, the data type of the input is \u201cmulti-array\u201d and not \u201cimage\u201d. This is often easily fixed, but sometimes it requires a bit of model surgery.<\/p>\n<p>How to properly handle images with your Core ML models is described in detail in my e-book <a href=\"https:\/\/leanpub.com\/coreml-survival-guide\">Core ML Survival Guide<\/a> \u2014 along with many other handy tips &amp; tricks! Because the topic comes up so often, I decided to write about it on my blog as well.<\/p>\n<p>If you\u2019re having problems using images in Core ML, read on!<\/p>\n<h2>Image as input<\/h2>\n<p>The situation: You have a Core ML model with an <strong>input<\/strong> that is a multi-array of shape <em>(3, height, width)<\/em>.<\/p>\n<p>But in your app you have an image object, such as a <code>UIImage<\/code>.<\/p>\n<p>How do you convert the <code>UIImage<\/code> to an <code>MLMultiArray<\/code> object that you can pass to the model?<\/p>\n<p>The answer is: You probably shouldn\u2019t do this!<\/p>\n<p>A better solution is to <strong>change the model to expect an image<\/strong> instead of a multi-array. Now Core ML will directly accept your <code>UIImage<\/code> as the input \u2014 no need to convert it to an <code>MLMultiArray<\/code> first.<\/p>\n<p>If a model works on images, it should accept images as input! Multi-arrays are useful for other kinds of data, but are not intended for images.<\/p>\n<h3>You can probably do this during conversion<\/h3>\n<p>This problem is easy to fix if you have access to the original model.<\/p>\n<p>Most Core ML converters have an <code>image_input_names<\/code> option that tells the model which inputs should be treated as an image.<\/p>\n<p>In your conversion script, simply provide the additional argument <code>image_input_names=\"your_input\"<\/code> to the <code>convert<\/code> function, where <code>\"your_input\"<\/code> is the name of the input that should take an image.<\/p>\n<p>For a model with multiple image inputs, you can supply a list of names: <code>image_input_names=[\"first_input\", \"second_input\"]<\/code>.<\/p>\n<p>Note that this only works if the shape of the input is <em>(3, height, width)<\/em> for color images, or just <em>(height, width)<\/em> for grayscale images.<\/p>\n<div class=\"note\">\n<p><strong>Note:<\/strong> Core ML doesn\u2019t currently work with 4 channel images. The alpha-channel in such images is simply ignored.<\/p>\n<\/div>\n<h3>Changing the mlmodel file afterwards<\/h3>\n<p>If you only have the <strong>.mlmodel<\/strong> file and not the original model or the conversion script \u2014 or if the converter does not have the <code>image_input_names<\/code> option \u2014 you can still fix this using a bit of Python.<\/p>\n<pre><code class=\"language-python\">import coremltools\nimport coremltools.proto.FeatureTypes_pb2 as ft \n\nspec = coremltools.utils.load_spec(\"YourModel.mlmodel\")\n\ninput = spec.description.input[0]\ninput.type.imageType.colorSpace = ft.ImageFeatureType.RGB\ninput.type.imageType.height = 224 \ninput.type.imageType.width = 224\n\ncoremltools.utils.save_spec(spec, \"YourNewModel.mlmodel\")\n<\/code><\/pre>\n<p>This script changes the data type of the first input, <code>input[0]<\/code>, to expect an image.<\/p>\n<p>This assumes that the image is RGB. If necessary, change <code>colorSpace<\/code> to <code>GRAYSCALE<\/code> or <code>BGR<\/code>. You should use BGR if the model was trained with Caffe or if OpenCV was used to load the training images. Core ML will automatically convert the pixel order of your input images to whatever format the model requires, but it can only do this if you set <code>colorSpace<\/code> correctly.<\/p>\n<p>Also make sure that the width and height are correct for your model!<\/p>\n<div class=\"note\">\n<p><strong>Tip:<\/strong> To support flexible image sizes, see the chapter <em>Size Flexibility<\/em> in the <a href=\"https:\/\/leanpub.com\/coreml-survival-guide\">Core ML Survival Guide<\/a>.<\/p>\n<\/div>\n<h3>Don\u2019t forget preprocessing<\/h3>\n<p>A Core ML model includes a special <strong>preprocessing stage<\/strong> for image inputs.<\/p>\n<p>Usually the original model normalizes the image tensor. For example, by converting the pixels from the range [0, 255] to [-1, +1].<\/p>\n<p>You need to set this up in your Core ML model as well. Getting the preprocessing wrong is the number one reason why people get incorrect predictions out of their Core ML models!<\/p>\n<p>To learn more about this, see my blog post <a href=\"https:\/\/machinethink.net\/blog\/help-core-ml-gives-wrong-output\/\">Help!? The output of my Core ML model is wrong\u2026<\/a> And of course you can read all about this in the <a href=\"https:\/\/leanpub.com\/coreml-survival-guide\">Core ML Survival Guide<\/a>.<\/p>\n<h3>You need a CVPixelBuffer<\/h3>\n<p>OK, I lied a little when I said that Core ML could directly use <code>UIImage<\/code> objects. \ud83d\ude30<\/p>\n<p>The Core ML API requires images to be <code>CVPixelBuffer<\/code> objects. But in the app you probably have the image as a <code>UIImage<\/code>, a <code>CGImage<\/code>, a <code>CIImage<\/code>, or an <code>MTLTexture<\/code>.<\/p>\n<p>In that case, you will still need to convert your image to a <code>CVPixelBuffer<\/code> object. Fortunately, this is easy:<\/p>\n<ul>\n<li>\n<p>On iOS 13 and macOS 10.15 you can use the new <code>MLFeatureValue(cgImage:)<\/code> and <code>MLFeatureValue(imageAt:)<\/code> APIs.<\/p>\n<\/li>\n<li>\n<p>You can use <code>VNCoreMLRequest<\/code> from the Vision framework, which lets you pass in a <code>CGImage<\/code> object that you can easily obtain from your <code>UIImage<\/code>.<\/p>\n<\/li>\n<li>\n<p>Or you can use the helper code from <a href=\"https:\/\/github.com\/hollance\/CoreMLHelpers\">CoreMLHelpers<\/a>.<\/p>\n<\/li>\n<\/ul>\n<h3>Converting UIImage \u2192 MLMultiArray<\/h3>\n<p>If \u2014 for some strange reason \u2014 you really <em>must<\/em> convert the <code>UIImage<\/code> into an <code>MLMultiArray<\/code> anyway, here\u2019s how to do it:<\/p>\n<ul>\n<li>\n<p>Create an <code>MLMultiArray<\/code> of type <code>.double<\/code> with shape <em>(3, height, width)<\/em>.<\/p>\n<\/li>\n<li>\n<p>Loop through the pixels in the image and copy the color values into the rows and columns of this new <code>MLMultiArray<\/code> object.<\/p>\n<\/li>\n<li>\n<p>Don\u2019t forget the preprocessing! You can do this inside the loop. For example, to convert a pixel from [0, 255] to [-1, 1], first divide the pixel value by 127.5, then subtract 1, and put the resulting value into the <code>MLMultiArray<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>I don\u2019t have any code for this (because it\u2019s a bad idea) but it\u2019s basically the opposite of what happens in <a href=\"https:\/\/github.com\/hollance\/CoreMLHelpers\/blob\/master\/CoreMLHelpers\/MLMultiArray%2BImage.swift\">MLMultiArray+Image.swift in CoreMLHelpers<\/a>.<\/p>\n<p>More trouble than it\u2019s worth, really \u2014 it\u2019s much smarter to let Core ML handle this for you. \ud83d\ude01<\/p>\n<h2>Images as output<\/h2>\n<p>What if your model <strong>outputs<\/strong> an image?<\/p>\n<p>As described above, the converters provided with coremltools have an option <code>image_input_names<\/code> that tells the converter which of the <em>inputs<\/em> should be treated as images, so that Core ML lets you pass in a <code>CVPixelBuffer<\/code> object.<\/p>\n<p>However, there is no <code>image_output_names<\/code>. So if you have an image-to-image model, any image outputs will become <strong>multi-array outputs<\/strong> in the mlmodel. That\u2019s not handy.<\/p>\n<p>If a model predicts an image, it should output some kind of image object too! In Core ML, that would be a <code>CVPixelBuffer<\/code> object.<\/p>\n<p>There are two things you can do to get a <code>CVPixelBuffer<\/code> as output from Core ML:<\/p>\n<ol>\n<li>convert the <code>MLMultiArray<\/code> to an image yourself, or<\/li>\n<li>change the mlmodel so that it knows the output should be an image.<\/li>\n<\/ol>\n<p>I recommend against using option 1. It is slow and unnecessary because you can let Core ML do it for you (option 2). But in case you want to do the conversion yourself, check out the <a href=\"https:\/\/github.com\/hollance\/CoreMLHelpers\/blob\/master\/CoreMLHelpers\/MLMultiArray%2BImage.swift\">MLMultiArray+Image extension in CoreMLHelpers<\/a>.<\/p>\n<h3>Changing the output type to image<\/h3>\n<p>Even though the converter doesn\u2019t let you specify that an output should be an image, you can always change this in the mlmodel file afterwards.<\/p>\n<p>Load the mlmodel into a spec object:<\/p>\n<pre><code class=\"language-python\">import coremltools\nimport coremltools.proto.FeatureTypes_pb2 as ft \n\nspec = coremltools.utils.load_spec(\"YourModel.mlmodel\")\n<\/code><\/pre>\n<p>Let\u2019s say that <code>print(spec.description)<\/code> gives the following:<\/p>\n<pre><code class=\"language-nohighlight\">output {\n  name: \"generatedImage\"\n  type {\n    multiArrayType {\n      shape: 3\n      shape: 300\n      shape: 150\n      dataType: DOUBLE\n    }\n  }\n}\n<\/code><\/pre>\n<p>Then you can turn this output description into an image by writing:<\/p>\n<pre><code class=\"language-python\">output = spec.description.output[0]\n\nimport coremltools.proto.FeatureTypes_pb2 as ft\noutput.type.imageType.colorSpace = ft.ImageFeatureType.RGB\noutput.type.imageType.height = 300\noutput.type.imageType.width = 150\n\ncoremltools.utils.save_spec(spec, \"YourNewModel.mlmodel\")\n<\/code><\/pre>\n<p>And now <code>print(spec.description)<\/code> shows the output is an RGB image:<\/p>\n<pre><code class=\"language-nohighlight\">output {\n  name: \"generatedImage\"\n  type {\n    imageType {\n      width: 150\n      height: 300\n      colorSpace: RGB\n    }\n  }\n}\n<\/code><\/pre>\n<p>If you want the image to have BGR pixel order, write <code>colorSpace = ft.ImageFeatureType.BGR<\/code> instead of <code>RGB<\/code>.<\/p>\n<p><strong>Important:<\/strong> You can\u2019t just turn <em>any<\/em> multi-array into an image. Pay attention to the <code>shape<\/code> properties in the <code>multiArrayType<\/code> structure:<\/p>\n<ul>\n<li>the first <code>shape<\/code> is the number of channels<\/li>\n<li>the second is the image\u2019s height<\/li>\n<li>the third is the image\u2019s width<\/li>\n<\/ul>\n<p>You can only turn a multi-array into an RGB or BGR image if the number of channels is 3.<\/p>\n<p>If the number of channels is 1, or if there are only two <code>shape<\/code> values \u2014 height and width \u2014 then you must use <code>colorSpace = ft.ImageFeatureType.GRAYSCALE<\/code>.<\/p>\n<p>If there are more than three <code>shape<\/code> values listed, and the first or last of these are 1, then you can delete these unused dimensions. For example, if you have:<\/p>\n<pre><code class=\"language-nohighlight\">multiArrayType {\n  shape: 1\n  shape: 3\n  shape: 300\n  shape: 150\n  dataType: DOUBLE\n}\n<\/code><\/pre>\n<p>Then do the following to get rid of that first dimension with size 1:<\/p>\n<pre><code class=\"language-python\">del output.type.multiArrayType.shape[0]\n<\/code><\/pre>\n<p>Removing a dimension of size 1 doesn\u2019t change the actual data, and now it lets Core ML interpret the data as being an image.<\/p>\n<h3>Don\u2019t forget the postprocessing<\/h3>\n<p>If your Core ML model includes preprocessing, for example to convert the pixels from the range [0, 255] to [-1, +1], it\u2019s likely that the output of your model is also in that [-1, +1] range.<\/p>\n<p>For Core ML to turn the output data into a <code>CVPixelBuffer<\/code>, it must be a tensor with values in the range 0 \u2013 255. Larger values or negative values will be clipped to this range.<\/p>\n<p>This means that, if your model doesn\u2019t already output pixels from 0 \u2013 255, you\u2019ll need to add some <strong>postprocessing layers<\/strong>.<\/p>\n<p>For outputs in the range [-1, +1], you\u2019ll need to add two operations:<\/p>\n<ol>\n<li>add 1 to the tensor to put it in the range [0, 2]<\/li>\n<li>multiply the tensor by 127.5<\/li>\n<\/ol>\n<p>This is easiest if you add these operations in the original model and then convert it to Core ML.<\/p>\n<p>In case you don\u2019t have access to the original model, you can also change this directly in the mlmodel file by <strong>adding new layers at the end<\/strong>. For more info on how to insert new layers into an existing mlmodel file, see the <a href=\"https:\/\/leanpub.com\/coreml-survival-guide\">Core ML Survival Guide<\/a>.<\/p>\n<p>If you forget to do this postprocessing, your image will be black or the colors will be wrong in some other weird way. Usually the postprocessing needs to be the inverse of the preprocessing.<\/p>\n<h3>You get a CVPixelBuffer<\/h3>\n<p>For image outputs, Core ML gives you a <code>CVPixelBuffer<\/code> object.<\/p>\n<p>If you used Vision, you\u2019ll get a <code>VNPixelBufferObservation<\/code> object that contains a <code>CVPixelBuffer<\/code>.<\/p>\n<p>Fortunately, a <code>CVPixelBuffer<\/code> is easy enough to convert into a <code>UIImage<\/code>. Here is one way, using Core Image:<\/p>\n<pre><code class=\"language-swift\">let ciImage = CIImage(cvPixelBuffer: pixelBuffer)\nlet resultImage = UIImage(ciImage: ciImage)\n<\/code><\/pre>\n<h2>Conclusion<\/h2>\n<p>Core ML can work with image inputs and outputs just fine, but remember:<\/p>\n<ul>\n<li>you need to set the type of the inputs and\/or outputs to accept images instead of multi-arrays<\/li>\n<li>you need to add preprocessing to normalize the pixel values<\/li>\n<li>for models with image outputs, you probably need to add postprocessing too<\/li>\n<\/ul>\n<p>Letting Core ML handle the conversion from <code>CVPixelBuffers<\/code> to tensors and back is a lot easier \u2014 and more efficient \u2014 than doing converting your images to\/from <code>MLMultiArray<\/code> objects by hand!<\/p>\n<p>For more tricks like this, check out my book <a href=\"https:\/\/leanpub.com\/coreml-survival-guide\">Core ML Survival Guide<\/a>. It has a ton of info on how to get the most out of Core ML.<\/p>","protected":false},"excerpt":{"rendered":"<p>I see the following questions come up a lot on Stack Overflow, the Apple developer forums, and various Slack groups: My neural network works on images but the Core ML model expects an MLMultiArray object. How do I convert my UIImage to an MLMultiArray? My neural network outputs an MLMultiArray but how do I convert [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rop_custom_images_group":[],"rop_custom_messages_group":[],"rop_publish_now":"initial","rop_publish_now_accounts":[],"rop_publish_now_history":[],"rop_publish_now_status":"pending","footnotes":""},"categories":[1],"tags":[],"class_list":["post-10200","post","type-post","status-publish","format-standard","hentry","category-explore"],"_links":{"self":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/10200","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/comments?post=10200"}],"version-history":[{"count":0,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/10200\/revisions"}],"wp:attachment":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/media?parent=10200"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/categories?post=10200"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/tags?post=10200"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}