From 5c1542e71b4cad7db51510794bd16c648dae368e Mon Sep 17 00:00:00 2001 From: Kedar Bellare Date: Sun, 11 Nov 2018 05:43:22 -0800 Subject: [PATCH] Port of scala Image API to clojure (#13107) * Port of scala Image API to clojure * Minor style changes * Add specs and other minor fixes * Fix unit tests (:facepalm:) --- .../src/org/apache/clojure_mxnet/image.clj | 139 ++++++++++++++++++ .../org/apache/clojure_mxnet/image_test.clj | 79 ++++++++++ 2 files changed, 218 insertions(+) create mode 100644 contrib/clojure-package/src/org/apache/clojure_mxnet/image.clj create mode 100644 contrib/clojure-package/test/org/apache/clojure_mxnet/image_test.clj diff --git a/contrib/clojure-package/src/org/apache/clojure_mxnet/image.clj b/contrib/clojure-package/src/org/apache/clojure_mxnet/image.clj new file mode 100644 index 000000000000..6e726eba9da6 --- /dev/null +++ b/contrib/clojure-package/src/org/apache/clojure_mxnet/image.clj @@ -0,0 +1,139 @@ +;; +;; Licensed to the Apache Software Foundation (ASF) under one or more +;; contributor license agreements. See the NOTICE file distributed with +;; this work for additional information regarding copyright ownership. +;; The ASF licenses this file to You under the Apache License, Version 2.0 +;; (the "License"); you may not use this file except in compliance with +;; the License. You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. +;; + +(ns org.apache.clojure-mxnet.image + (:require [t6.from-scala.core :refer [$ $$] :as $] + [org.apache.clojure-mxnet.dtype :as dtype] + [org.apache.clojure-mxnet.ndarray :as ndarray] + [org.apache.clojure-mxnet.util :as util] + [clojure.spec.alpha :as s]) + (:import (org.apache.mxnet Image NDArray) + (java.io InputStream))) + +;; Flags for conversion of images +(def GRAYSCALE 0) +(def COLOR 1) + +(s/def ::input-stream #(instance? InputStream %)) +(s/def ::color-flag #{GRAYSCALE COLOR}) +(s/def ::to-rgb boolean?) +(s/def ::ndarray #(instance? NDArray %)) +(s/def ::output (s/or :empty nil? :ndarray ::ndarray)) +(s/def ::decode-image-opts + (s/keys :opt-un [::color-flag ::to-rgb ::output])) + +(defn decode-image + "Decodes an image from an input stream" + ([input-stream {:keys [color-flag to-rgb output] + :or {color-flag COLOR to-rgb true output nil} + :as opts}] + (util/validate! ::input-stream input-stream "Invalid input stream") + (util/validate! ::decode-image-opts opts "Invalid options for decoding") + (Image/imDecode input-stream color-flag to-rgb ($/option output))) + ([input-stream] + (decode-image input-stream {}))) + +(s/def ::filename string?) +(s/def ::optional-color-flag + (s/or :none nil? :some ::color-flag)) +(s/def ::optional-to-rgb + (s/or :none nil? :some ::to-rgb)) + +(defn read-image + "Reads an image file and returns an ndarray" + ([filename {:keys [color-flag to-rgb output] + :or {color-flag nil to-rgb nil output nil} + :as opts}] + (util/validate! ::filename filename "Invalid filename") + (util/validate! ::optional-color-flag color-flag "Invalid color flag") + (util/validate! ::optional-to-rgb to-rgb "Invalid conversion flag") + (util/validate! ::output output "Invalid output") + (Image/imRead + filename + ($/option color-flag) + ($/option to-rgb) + ($/option output))) + ([filename] + (read-image filename {}))) + +(s/def ::int int?) +(s/def ::optional-int (s/or :none nil? :some int?)) + +(defn resize-image + "Resizes the image array to (width, height)" + ([input w h {:keys [interpolation output] + :or {interpolation nil output nil} + :as opts}] + (util/validate! ::ndarray input "Invalid input array") + (util/validate! ::int w "Invalid width") + (util/validate! ::int h "Invalid height") + (util/validate! ::optional-int interpolation "Invalid interpolation") + (util/validate! ::output output "Invalid output") + (Image/imResize input w h ($/option interpolation) ($/option output))) + ([input w h] + (resize-image input w h {}))) + +(defn apply-border + "Pad image border" + ([input top bottom left right + {:keys [fill-type value values output] + :or {fill-type nil value nil values nil output nil} + :as opts}] + (util/validate! ::ndarray input "Invalid input array") + (util/validate! ::int top "Invalid top margin") + (util/validate! ::int bottom "Invalid bottom margin") + (util/validate! ::int left "Invalid left margin") + (util/validate! ::int right "Invalid right margin") + (util/validate! ::optional-int fill-type "Invalid fill type") + (util/validate! ::output output "Invalid output") + (Image/copyMakeBorder input top bottom left right + ($/option fill-type) + ($/option value) + ($/option values) + ($/option output))) + ([input top bottom left right] + (apply-border input top bottom left right {}))) + +(defn fixed-crop + "Return a fixed crop of the image" + [input x0 y0 w h] + (util/validate! ::ndarray input "Invalid input array") + (util/validate! ::int x0 "Invalid starting x coordinate") + (util/validate! ::int y0 "Invalid starting y coordinate") + (util/validate! ::int w "Invalid width") + (util/validate! ::int h "Invalid height") + (Image/fixedCrop input x0 y0 w h)) + +(defn rgb-array? + "Returns whether the ndarray is in the RGB format" + [input] + (util/validate! ::ndarray input "Invalid input array") + (let [shape (ndarray/shape-vec input)] + (and + (= 3 (count shape)) + (= 3 (shape 2))))) + +(s/def ::all-bytes #(= dtype/UINT8 (ndarray/dtype %))) +(s/def ::rgb-array rgb-array?) +(s/def ::to-image-ndarray + (s/and ::ndarray ::all-bytes ::rgb-array)) + +(defn to-image + "Convert a NDArray image in RGB format to a real image" + [input] + (util/validate! ::to-image-ndarray input "Invalid input array") + (Image/toImage input)) diff --git a/contrib/clojure-package/test/org/apache/clojure_mxnet/image_test.clj b/contrib/clojure-package/test/org/apache/clojure_mxnet/image_test.clj new file mode 100644 index 000000000000..38ab11c86012 --- /dev/null +++ b/contrib/clojure-package/test/org/apache/clojure_mxnet/image_test.clj @@ -0,0 +1,79 @@ +;; +;; Licensed to the Apache Software Foundation (ASF) under one or more +;; contributor license agreements. See the NOTICE file distributed with +;; this work for additional information regarding copyright ownership. +;; The ASF licenses this file to You under the Apache License, Version 2.0 +;; (the "License"); you may not use this file except in compliance with +;; the License. You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. +;; + +(ns org.apache.clojure-mxnet.image-test + (:require [org.apache.clojure-mxnet.image :as image] + [org.apache.clojure-mxnet.ndarray :as ndarray] + [clojure.java.io :as io] + [clojure.test :refer :all]) + (:import (javax.imageio ImageIO))) + +(def tmp-dir (System/getProperty "java.io.tmpdir")) +(def image-path (.getAbsolutePath (io/file tmp-dir "Pug-Cookie.jpg"))) + +(defn download-image [] + (with-open [in (io/input-stream "https://s3.amazonaws.com/model-server/inputs/Pug-Cookie.jpg") + out (io/output-stream (io/file image-path))] + (io/copy in out))) + +(defn delete-image [] + (io/delete-file image-path)) + +(defn with-downloaded-image [f] + (download-image) + (f) + (delete-image)) + +(use-fixtures :once with-downloaded-image) + +(deftest test-decode-image + (let [img-arr (image/decode-image + (io/input-stream image-path)) + img-arr-2 (image/decode-image + (io/input-stream image-path) + {:color-flag image/GRAYSCALE})] + (is (= [576 1024 3] (ndarray/shape-vec img-arr))) + (is (= [576 1024 1] (ndarray/shape-vec img-arr-2))))) + +(deftest test-read-image + (let [img-arr (image/read-image image-path) + img-arr-2 (image/read-image + image-path + {:color-flag image/GRAYSCALE})] + (is (= [576 1024 3] (ndarray/shape-vec img-arr))) + (is (= [576 1024 1] (ndarray/shape-vec img-arr-2))))) + +(deftest test-resize-image + (let [img-arr (image/read-image image-path) + resized-arr (image/resize-image img-arr 224 224)] + (is (= [224 224 3] (ndarray/shape-vec resized-arr))))) + +(deftest test-crop-image + (let [img-arr (image/read-image image-path) + cropped-arr (image/fixed-crop img-arr 0 0 224 224)] + (is (= [224 224 3] (ndarray/shape-vec cropped-arr))))) + +(deftest test-apply-border + (let [img-arr (image/read-image image-path) + padded-arr (image/apply-border img-arr 1 1 1 1)] + (is (= [578 1026 3] (ndarray/shape-vec padded-arr))))) + +(deftest test-to-image + (let [img-arr (image/read-image image-path) + resized-arr (image/resize-image img-arr 224 224) + new-img (image/to-image resized-arr)] + (is (= true (ImageIO/write new-img "png" (io/file tmp-dir "out.png"))))))