(ns cortex.sense "Here are functions useful in the construction of two or more sensors/effectors." {:author "Robert McIntyre"} (:use (cortex world util)) (:import ij.process.ImageProcessor) (:import jme3tools.converters.ImageToAwt) (:import java.awt.image.BufferedImage) (:import com.jme3.collision.CollisionResults) (:import com.jme3.bounding.BoundingBox) (:import (com.jme3.scene Node Spatial)) (:import com.jme3.scene.control.AbstractControl) (:import (com.jme3.math Quaternion Vector3f)) (:import javax.imageio.ImageIO) (:import java.io.File) (:import (javax.swing JPanel JFrame SwingUtilities))) (in-ns 'cortex.sense) (defn meta-data "Get the meta-data for a node created with blender." [blender-node key] (if-let [data (.getUserData blender-node "properties")] ;; this part is to accomodate weird blender properties ;; as well as sensible clojure maps. (.findValue data key) (.getUserData blender-node key))) (defn jme-to-blender "Convert from JME coordinates to Blender coordinates" [#^Vector3f in] (Vector3f. (.getX in) (- (.getZ in)) (.getY in))) (defn blender-to-jme "Convert from Blender coordinates to JME coordinates" [#^Vector3f in] (Vector3f. (.getX in) (.getZ in) (- (.getY in)))) (defn load-image "Load an image as a BufferedImage using the asset-manager system." [asset-relative-path] (ImageToAwt/convert (.getImage (.loadTexture (asset-manager) asset-relative-path)) false false 0)) (def white 0xFFFFFF) (defn white? [rgb] (= (bit-and white rgb) white)) (defn filter-pixels "List the coordinates of all pixels matching pred, within the bounds provided. If bounds are not specified then the entire image is searched. bounds -> [x0 y0 width height]" {:author "Dylan Holmes"} ([pred #^BufferedImage image] (filter-pixels pred image [0 0 (.getWidth image) (.getHeight image)])) ([pred #^BufferedImage image [x0 y0 width height]] ((fn accumulate [x y matches] (cond (>= y (+ height y0)) matches (>= x (+ width x0)) (recur 0 (inc y) matches) (pred (.getRGB image x y)) (recur (inc x) y (conj matches [x y])) :else (recur (inc x) y matches))) x0 y0 []))) (defn white-coordinates "Coordinates of all the white pixels in a subset of the image." ([#^BufferedImage image bounds] (filter-pixels white? image bounds)) ([#^BufferedImage image] (filter-pixels white? image))) (in-ns 'cortex.sense) (defn average [coll] (/ (reduce + coll) (count coll))) (defn- collapse-1d "One dimensional helper for collapse." [center line] (let [length (count line) num-above (count (filter (partial < center) line)) num-below (- length num-above)] (range (- center num-below) (+ center num-above)))) (defn collapse "Take a sequence of pairs of integers and collapse them into a contiguous bitmap with no \"holes\" or negative entries, as close to the origin [0 0] as the shape permits. The order of the points is preserved. eg. (collapse [[-5 5] [5 5] --> [[0 1] [1 1] [-5 -5] [5 -5]]) --> [0 0] [1 0]] (collapse [[-5 5] [-5 -5] --> [[0 1] [0 0] [ 5 -5] [ 5 5]]) --> [1 0] [1 1]]" [points] (if (empty? points) [] (let [num-points (count points) center (vector (int (average (map first points))) (int (average (map first points)))) flattened (reduce concat (map (fn [column] (map vector (map first column) (collapse-1d (second center) (map second column)))) (partition-by first (sort-by first points)))) squeezed (reduce concat (map (fn [row] (map vector (collapse-1d (first center) (map first row)) (map second row))) (partition-by second (sort-by second flattened)))) relocated (let [min-x (apply min (map first squeezed)) min-y (apply min (map second squeezed))] (map (fn [[x y]] [(- x min-x) (- y min-y)]) squeezed)) point-correspondence (zipmap (sort points) (sort relocated)) original-order (vec (map point-correspondence points))] original-order))) (defn sense-nodes "For some senses there is a special empty blender node whose children are considered markers for an instance of that sense. This function generates functions to find those children, given the name of the special parent node." [parent-name] (fn [#^Node creature] (if-let [sense-node (.getChild creature parent-name)] (seq (.getChildren sense-node)) (do ;;(println-repl "could not find" parent-name "node") [])))) (defn closest-node "Return the physical node in creature which is closest to the given node." [#^Node creature #^Node empty] (loop [radius (float 0.01)] (let [results (CollisionResults.)] (.collideWith creature (BoundingBox. (.getWorldTranslation empty) radius radius radius) results) (if-let [target (first results)] (.getGeometry target) (recur (float (* 2 radius))))))) (defn world-to-local "Convert the world coordinates into coordinates relative to the object (i.e. local coordinates), taking into account the rotation of object." [#^Spatial object world-coordinate] (.worldToLocal object world-coordinate nil)) (defn local-to-world "Convert the local coordinates into world relative coordinates" [#^Spatial object local-coordinate] (.localToWorld object local-coordinate nil)) (defn bind-sense "Bind the sense to the Spatial such that it will maintain its current position relative to the Spatial no matter how the spatial moves. 'sense can be either a Camera or Listener object." [#^Spatial obj sense] (let [sense-offset (.subtract (.getLocation sense) (.getWorldTranslation obj)) initial-sense-rotation (Quaternion. (.getRotation sense)) base-anti-rotation (.inverse (.getWorldRotation obj))] (.addControl obj (proxy [AbstractControl] [] (controlUpdate [tpf] (let [total-rotation (.mult base-anti-rotation (.getWorldRotation obj))] (.setLocation sense (.add (.mult total-rotation sense-offset) (.getWorldTranslation obj))) (.setRotation sense (.mult total-rotation initial-sense-rotation)))) (controlRender [_ _]))))) (in-ns 'cortex.sense) (defn view-image "Initializes a JPanel on which you may draw a BufferedImage. Returns a function that accepts a BufferedImage and draws it to the JPanel. If given a directory it will save the images as png files starting at 0000000.png and incrementing from there." ([#^File save title] (let [idx (atom -1) image (atom (BufferedImage. 1 1 BufferedImage/TYPE_4BYTE_ABGR)) panel (proxy [JPanel] [] (paint [graphics] (proxy-super paintComponent graphics) (.drawImage graphics @image 0 0 nil))) frame (JFrame. title)] (SwingUtilities/invokeLater (fn [] (doto frame (-> (.getContentPane) (.add panel)) (.pack) (.setLocationRelativeTo nil) (.setResizable true) (.setVisible true)))) (fn [#^BufferedImage i] (reset! image i) (.setSize frame (+ 8 (.getWidth i)) (+ 28 (.getHeight i))) (.repaint panel 0 0 (.getWidth i) (.getHeight i)) (if save (ImageIO/write i "png" (File. save (format "%07d.png" (swap! idx inc)))))))) ([#^File save] (view-image save "Display Image")) ([] (view-image nil))) (defn view-sense "Take a kernel that produces a BufferedImage from some sense data and return a function which takes a list of sense data, uses the kernel to convert to images, and displays those images, each in its own JFrame." [sense-display-kernel] (let [windows (atom [])] (fn this ([data] (this data nil)) ([data save-to] (if (> (count data) (count @windows)) (reset! windows (doall (map (fn [idx] (if save-to (let [dir (File. save-to (str idx))] (.mkdir dir) (view-image dir)) (view-image))) (range (count data)))))) (dorun (map (fn [display datum] (display (sense-display-kernel datum))) @windows data)))))) (defn points->image "Take a collection of points and visualize it as a BufferedImage." [points] (if (empty? points) (BufferedImage. 1 1 BufferedImage/TYPE_BYTE_BINARY) (let [xs (vec (map first points)) ys (vec (map second points)) x0 (apply min xs) y0 (apply min ys) width (- (apply max xs) x0) height (- (apply max ys) y0) image (BufferedImage. (inc width) (inc height) BufferedImage/TYPE_INT_RGB)] (dorun (for [x (range (.getWidth image)) y (range (.getHeight image))] (.setRGB image x y 0xFF0000))) (dorun (for [index (range (count points))] (.setRGB image (- (xs index) x0) (- (ys index) y0) -1))) image))) (defn gray "Create a gray RGB pixel with R, G, and B set to num. num must be between 0 and 255." [num] (+ num (bit-shift-left num 8) (bit-shift-left num 16)))