Mercurial > cortex
view org/sense.org @ 485:ac953b562eab
completed first draft.
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Sat, 29 Mar 2014 16:22:49 -0400 |
parents | 258078f78b33 |
children |
line wrap: on
line source
1 #+title: Helper Functions / Motivations2 #+author: Robert McIntyre3 #+email: rlm@mit.edu4 #+description: sensory utilities5 #+keywords: simulation, jMonkeyEngine3, clojure, simulated senses6 #+SETUPFILE: ../../aurellem/org/setup.org7 #+INCLUDE: ../../aurellem/org/level-0.org9 * Blender Utilities10 In blender, any object can be assigned an arbitrary number of11 key-value pairs which are called "Custom Properties". These are12 accessible in jMonkeyEngine when blender files are imported with the13 =BlenderLoader=. =meta-data= extracts these properties.15 #+name: blender-116 #+begin_src clojure17 (in-ns 'cortex.sense)18 (defn meta-data19 "Get the meta-data for a node created with blender."20 [blender-node key]21 (if-let [data (.getUserData blender-node "properties")]22 ;; this part is to accomodate weird blender properties23 ;; as well as sensible clojure maps.24 (.findValue data key)25 (.getUserData blender-node key)))27 #+end_src29 #+results: blender-130 : #'cortex.sense/meta-data32 Blender uses a different coordinate system than jMonkeyEngine so it33 is useful to be able to convert between the two. These only come into34 play when the meta-data of a node refers to a vector in the blender35 coordinate system.37 #+name: blender-238 #+begin_src clojure39 (defn jme-to-blender40 "Convert from JME coordinates to Blender coordinates"41 [#^Vector3f in]42 (Vector3f. (.getX in) (- (.getZ in)) (.getY in)))44 (defn blender-to-jme45 "Convert from Blender coordinates to JME coordinates"46 [#^Vector3f in]47 (Vector3f. (.getX in) (.getZ in) (- (.getY in))))48 #+end_src50 * Sense Topology52 Human beings are three-dimensional objects, and the nerves that53 transmit data from our various sense organs to our brain are54 essentially one-dimensional. This leaves up to two dimensions in which55 our sensory information may flow. For example, imagine your skin: it56 is a two-dimensional surface around a three-dimensional object (your57 body). It has discrete touch sensors embedded at various points, and58 the density of these sensors corresponds to the sensitivity of that59 region of skin. Each touch sensor connects to a nerve, all of which60 eventually are bundled together as they travel up the spinal cord to61 the brain. Intersect the spinal nerves with a guillotining plane and62 you will see all of the sensory data of the skin revealed in a roughly63 circular two-dimensional image which is the cross section of the64 spinal cord. Points on this image that are close together in this65 circle represent touch sensors that are /probably/ close together on66 the skin, although there is of course some cutting and rearrangement67 that has to be done to transfer the complicated surface of the skin68 onto a two dimensional image.70 Most human senses consist of many discrete sensors of various71 properties distributed along a surface at various densities. For72 skin, it is Pacinian corpuscles, Meissner's corpuscles, Merkel's73 disks, and Ruffini's endings, which detect pressure and vibration of74 various intensities. For ears, it is the stereocilia distributed75 along the basilar membrane inside the cochlea; each one is sensitive76 to a slightly different frequency of sound. For eyes, it is rods77 and cones distributed along the surface of the retina. In each case,78 we can describe the sense with a surface and a distribution of sensors79 along that surface.81 ** UV-maps83 Blender and jMonkeyEngine already have support for exactly this sort84 of data structure because it is used to "skin" models for games. It is85 called [[http://wiki.blender.org/index.php/Doc:2.6/Manual/Textures/Mapping/UV][UV-mapping]]. The three-dimensional surface of a model is cut86 and smooshed until it fits on a two-dimensional image. You paint87 whatever you want on that image, and when the three-dimensional shape88 is rendered in a game the smooshing and cutting is reversed and the89 image appears on the three-dimensional object.91 To make a sense, interpret the UV-image as describing the distribution92 of that senses sensors. To get different types of sensors, you can93 either use a different color for each type of sensor, or use multiple94 UV-maps, each labeled with that sensor type. I generally use a white95 pixel to mean the presence of a sensor and a black pixel to mean the96 absence of a sensor, and use one UV-map for each sensor-type within a97 given sense. The paths to the images are not stored as the actual98 UV-map of the blender object but are instead referenced in the99 meta-data of the node.101 #+CAPTION: The UV-map for an elongated icososphere. The white dots each represent a touch sensor. They are dense in the regions that describe the tip of the finger, and less dense along the dorsal side of the finger opposite the tip.102 #+ATTR_HTML: width="300"103 [[../images/finger-UV.png]]105 #+CAPTION: Ventral side of the UV-mapped finger. Notice the density of touch sensors at the tip.106 #+ATTR_HTML: width="300"107 [[../images/finger-1.png]]109 #+CAPTION: Side view of the UV-mapped finger.110 #+ATTR_HTML: width="300"111 [[../images/finger-2.png]]113 #+CAPTION: Head on view of the finger. In both the head and side views you can see the divide where the touch-sensors transition from high density to low density.114 #+ATTR_HTML: width="300"115 [[../images/finger-3.png]]117 The following code loads images and gets the locations of the white118 pixels so that they can be used to create senses. =load-image= finds119 images using jMonkeyEngine's asset-manager, so the image path is120 expected to be relative to the =assets= directory. Thanks to Dylan121 for the beautiful version of =filter-pixels=.123 #+name: topology-1124 #+begin_src clojure125 (defn load-image126 "Load an image as a BufferedImage using the asset-manager system."127 [asset-relative-path]128 (ImageToAwt/convert129 (.getImage (.loadTexture (asset-manager) asset-relative-path))130 false false 0))132 (def white 0xFFFFFF)134 (defn white? [rgb]135 (= (bit-and white rgb) white))137 (defn filter-pixels138 "List the coordinates of all pixels matching pred, within the bounds139 provided. If bounds are not specified then the entire image is140 searched.141 bounds -> [x0 y0 width height]"142 {:author "Dylan Holmes"}143 ([pred #^BufferedImage image]144 (filter-pixels pred image [0 0 (.getWidth image) (.getHeight image)]))145 ([pred #^BufferedImage image [x0 y0 width height]]146 ((fn accumulate [x y matches]147 (cond148 (>= y (+ height y0)) matches149 (>= x (+ width x0)) (recur 0 (inc y) matches)150 (pred (.getRGB image x y))151 (recur (inc x) y (conj matches [x y]))152 :else (recur (inc x) y matches)))153 x0 y0 [])))155 (defn white-coordinates156 "Coordinates of all the white pixels in a subset of the image."157 ([#^BufferedImage image bounds]158 (filter-pixels white? image bounds))159 ([#^BufferedImage image]160 (filter-pixels white? image)))161 #+end_src163 ** Topology165 Information from the senses is transmitted to the brain via bundles of166 axons, whether it be the optic nerve or the spinal cord. While these167 bundles more or less preserve the overall topology of a sense's168 two-dimensional surface, they do not preserve the precise euclidean169 distances between every sensor. =collapse= is here to smoosh the170 sensors described by a UV-map into a contiguous region that still171 preserves the topology of the original sense.173 #+name: topology-2174 #+begin_src clojure175 (in-ns 'cortex.sense)177 (defn average [coll]178 (/ (reduce + coll) (count coll)))180 (defn- collapse-1d181 "One dimensional helper for collapse."182 [center line]183 (let [length (count line)184 num-above (count (filter (partial < center) line))185 num-below (- length num-above)]186 (range (- center num-below)187 (+ center num-above))))189 (defn collapse190 "Take a sequence of pairs of integers and collapse them into a191 contiguous bitmap with no \"holes\" or negative entries, as close to192 the origin [0 0] as the shape permits. The order of the points is193 preserved.195 eg.196 (collapse [[-5 5] [5 5] --> [[0 1] [1 1]197 [-5 -5] [5 -5]]) --> [0 0] [1 0]]199 (collapse [[-5 5] [-5 -5] --> [[0 1] [0 0]200 [ 5 -5] [ 5 5]]) --> [1 0] [1 1]]"201 [points]202 (if (empty? points) []203 (let204 [num-points (count points)205 center (vector206 (int (average (map first points)))207 (int (average (map first points))))208 flattened209 (reduce210 concat211 (map212 (fn [column]213 (map vector214 (map first column)215 (collapse-1d (second center)216 (map second column))))217 (partition-by first (sort-by first points))))218 squeezed219 (reduce220 concat221 (map222 (fn [row]223 (map vector224 (collapse-1d (first center)225 (map first row))226 (map second row)))227 (partition-by second (sort-by second flattened))))228 relocated229 (let [min-x (apply min (map first squeezed))230 min-y (apply min (map second squeezed))]231 (map (fn [[x y]]232 [(- x min-x)233 (- y min-y)])234 squeezed))235 point-correspondence236 (zipmap (sort points) (sort relocated))238 original-order239 (vec (map point-correspondence points))]240 original-order)))241 #+end_src242 * Viewing Sense Data244 It's vital to /see/ the sense data to make sure that everything is245 behaving as it should. =view-sense= and its helper, =view-image=246 are here so that each sense can define its own way of turning247 sense-data into pictures, while the actual rendering of said pictures248 stays in one central place. =points->image= helps senses generate a249 base image onto which they can overlay actual sense data.251 #+name: view-senses252 #+begin_src clojure253 (in-ns 'cortex.sense)255 (defn view-image256 "Initializes a JPanel on which you may draw a BufferedImage.257 Returns a function that accepts a BufferedImage and draws it to the258 JPanel. If given a directory it will save the images as png files259 starting at 0000000.png and incrementing from there."260 ([#^File save title]261 (let [idx (atom -1)262 image263 (atom264 (BufferedImage. 1 1 BufferedImage/TYPE_4BYTE_ABGR))265 panel266 (proxy [JPanel] []267 (paint268 [graphics]269 (proxy-super paintComponent graphics)270 (.drawImage graphics @image 0 0 nil)))271 frame (JFrame. title)]272 (SwingUtilities/invokeLater273 (fn []274 (doto frame275 (-> (.getContentPane) (.add panel))276 (.pack)277 (.setLocationRelativeTo nil)278 (.setResizable true)279 (.setVisible true))))280 (fn [#^BufferedImage i]281 (reset! image i)282 (.setSize frame (+ 8 (.getWidth i)) (+ 28 (.getHeight i)))283 (.repaint panel 0 0 (.getWidth i) (.getHeight i))284 (if save285 (ImageIO/write286 i "png"287 (File. save (format "%07d.png" (swap! idx inc))))))))288 ([#^File save]289 (view-image save "Display Image"))290 ([] (view-image nil)))292 (defn view-sense293 "Take a kernel that produces a BufferedImage from some sense data294 and return a function which takes a list of sense data, uses the295 kernel to convert to images, and displays those images, each in296 its own JFrame."297 [sense-display-kernel]298 (let [windows (atom [])]299 (fn this300 ([data]301 (this data nil))302 ([data save-to]303 (if (> (count data) (count @windows))304 (reset!305 windows306 (doall307 (map308 (fn [idx]309 (if save-to310 (let [dir (File. save-to (str idx))]311 (.mkdir dir)312 (view-image dir))313 (view-image))) (range (count data))))))314 (dorun315 (map316 (fn [display datum]317 (display (sense-display-kernel datum)))318 @windows data))))))321 (defn points->image322 "Take a collection of points and visualize it as a BufferedImage."323 [points]324 (if (empty? points)325 (BufferedImage. 1 1 BufferedImage/TYPE_BYTE_BINARY)326 (let [xs (vec (map first points))327 ys (vec (map second points))328 x0 (apply min xs)329 y0 (apply min ys)330 width (- (apply max xs) x0)331 height (- (apply max ys) y0)332 image (BufferedImage. (inc width) (inc height)333 BufferedImage/TYPE_INT_RGB)]334 (dorun335 (for [x (range (.getWidth image))336 y (range (.getHeight image))]337 (.setRGB image x y 0xFF0000)))338 (dorun339 (for [index (range (count points))]340 (.setRGB image (- (xs index) x0) (- (ys index) y0) -1)))341 image)))343 (defn gray344 "Create a gray RGB pixel with R, G, and B set to num. num must be345 between 0 and 255."346 [num]347 (+ num348 (bit-shift-left num 8)349 (bit-shift-left num 16)))350 #+end_src352 * Building a Sense from Nodes353 My method for defining senses in blender is the following:355 Senses like vision and hearing are localized to a single point356 and follow a particular object around. For these:358 - Create a single top-level empty node whose name is the name of the sense359 - Add empty nodes which each contain meta-data relevant360 to the sense, including a UV-map describing the number/distribution361 of sensors if applicable.362 - Make each empty-node the child of the top-level363 node. =sense-nodes= below generates functions to find these children.365 For touch, store the path to the UV-map which describes touch-sensors in the366 meta-data of the object to which that map applies.368 Each sense provides code that analyzes the Node structure of the369 creature and creates sense-functions. They also modify the Node370 structure if necessary.372 Empty nodes created in blender have no appearance or physical presence373 in jMonkeyEngine, but do appear in the scene graph. Empty nodes that374 represent a sense which "follows" another geometry (like eyes and375 ears) follow the closest physical object. =closest-node= finds this376 closest object given the Creature and a particular empty node.378 #+name: node-1379 #+begin_src clojure380 (defn sense-nodes381 "For some senses there is a special empty blender node whose382 children are considered markers for an instance of that sense. This383 function generates functions to find those children, given the name384 of the special parent node."385 [parent-name]386 (fn [#^Node creature]387 (if-let [sense-node (.getChild creature parent-name)]388 (seq (.getChildren sense-node))389 (do ;;(println-repl "could not find" parent-name "node")390 []))))392 (defn closest-node393 "Return the physical node in creature which is closest to the given394 node."395 [#^Node creature #^Node empty]396 (loop [radius (float 0.01)]397 (let [results (CollisionResults.)]398 (.collideWith399 creature400 (BoundingBox. (.getWorldTranslation empty)401 radius radius radius)402 results)403 (if-let [target (first results)]404 (.getGeometry target)405 (recur (float (* 2 radius)))))))407 (defn world-to-local408 "Convert the world coordinates into coordinates relative to the409 object (i.e. local coordinates), taking into account the rotation410 of object."411 [#^Spatial object world-coordinate]412 (.worldToLocal object world-coordinate nil))414 (defn local-to-world415 "Convert the local coordinates into world relative coordinates"416 [#^Spatial object local-coordinate]417 (.localToWorld object local-coordinate nil))418 #+end_src420 ** Sense Binding422 =bind-sense= binds either a Camera or a Listener object to any423 object so that they will follow that object no matter how it424 moves. It is used to create both eyes and ears.426 #+name: node-2427 #+begin_src clojure428 (defn bind-sense429 "Bind the sense to the Spatial such that it will maintain its430 current position relative to the Spatial no matter how the spatial431 moves. 'sense can be either a Camera or Listener object."432 [#^Spatial obj sense]433 (let [sense-offset (.subtract (.getLocation sense)434 (.getWorldTranslation obj))435 initial-sense-rotation (Quaternion. (.getRotation sense))436 base-anti-rotation (.inverse (.getWorldRotation obj))]437 (.addControl438 obj439 (proxy [AbstractControl] []440 (controlUpdate [tpf]441 (let [total-rotation442 (.mult base-anti-rotation (.getWorldRotation obj))]443 (.setLocation444 sense445 (.add446 (.mult total-rotation sense-offset)447 (.getWorldTranslation obj)))448 (.setRotation449 sense450 (.mult total-rotation initial-sense-rotation))))451 (controlRender [_ _])))))452 #+end_src454 Here is some example code which shows how a camera bound to a blue box455 with =bind-sense= moves as the box is buffeted by white cannonballs.457 #+name: test458 #+begin_src clojure459 (in-ns 'cortex.test.sense)461 (defn test-bind-sense462 "Show a camera that stays in the same relative position to a blue463 cube."464 ([] (test-bind-sense false))465 ([record?]466 (let [eye-pos (Vector3f. 0 30 0)467 rock (box 1 1 1 :color ColorRGBA/Blue468 :position (Vector3f. 0 10 0)469 :mass 30)470 table (box 3 1 10 :color ColorRGBA/Gray :mass 0471 :position (Vector3f. 0 -3 0))]472 (world473 (nodify [rock table])474 standard-debug-controls475 (fn init [world]476 (let [cam (doto (.clone (.getCamera world))477 (.setLocation eye-pos)478 (.lookAt Vector3f/ZERO479 Vector3f/UNIT_X))]480 (bind-sense rock cam)481 (.setTimer world (RatchetTimer. 60))482 (if record?483 (Capture/captureVideo484 world485 (File. "/home/r/proj/cortex/render/bind-sense0")))486 (add-camera!487 world cam488 (comp489 (view-image490 (if record?491 (File. "/home/r/proj/cortex/render/bind-sense1")))492 BufferedImage!))493 (add-camera! world (.getCamera world) no-op)))494 no-op))))495 #+end_src497 #+begin_html498 <video controls="controls" width="755">499 <source src="../video/bind-sense.ogg" type="video/ogg"500 preload="none" poster="../images/aurellem-1280x480.png" />501 </video>502 <br> <a href="http://youtu.be/DvoN2wWQ_6o"> YouTube </a>503 #+end_html505 With this, eyes are easy --- you just bind the camera closer to the506 desired object, and set it to look outward instead of inward as it507 does in the video.509 (nb : the video was created with the following commands)511 *** Combine Frames with ImageMagick512 #+begin_src clojure :results silent513 (ns cortex.video.magick514 (:import java.io.File)515 (:use clojure.java.shell))517 (defn combine-images []518 (let519 [idx (atom -1)520 left (rest521 (sort522 (file-seq (File. "/home/r/proj/cortex/render/bind-sense0/"))))523 right (rest524 (sort525 (file-seq526 (File. "/home/r/proj/cortex/render/bind-sense1/"))))527 sub (rest528 (sort529 (file-seq530 (File. "/home/r/proj/cortex/render/bind-senseB/"))))531 sub* (concat sub (repeat 1000 (last sub)))]532 (dorun533 (map534 (fn [im-1 im-2 sub]535 (sh "convert" (.getCanonicalPath im-1)536 (.getCanonicalPath im-2) "+append"537 (.getCanonicalPath sub) "-append"538 (.getCanonicalPath539 (File. "/home/r/proj/cortex/render/bind-sense/"540 (format "%07d.png" (swap! idx inc))))))541 left right sub*))))542 #+end_src544 *** Encode Frames with ffmpeg546 #+begin_src sh :results silent547 cd /home/r/proj/cortex/render/548 ffmpeg -r 30 -i bind-sense/%07d.png -b:v 9000k -vcodec libtheora bind-sense.ogg549 #+end_src551 * Headers552 #+name: sense-header553 #+begin_src clojure554 (ns cortex.sense555 "Here are functions useful in the construction of two or more556 sensors/effectors."557 {:author "Robert McIntyre"}558 (:use (cortex world util))559 (:import ij.process.ImageProcessor)560 (:import jme3tools.converters.ImageToAwt)561 (:import java.awt.image.BufferedImage)562 (:import com.jme3.collision.CollisionResults)563 (:import com.jme3.bounding.BoundingBox)564 (:import (com.jme3.scene Node Spatial))565 (:import com.jme3.scene.control.AbstractControl)566 (:import (com.jme3.math Quaternion Vector3f))567 (:import javax.imageio.ImageIO)568 (:import java.io.File)569 (:import (javax.swing JPanel JFrame SwingUtilities)))570 #+end_src572 #+name: test-header573 #+begin_src clojure574 (ns cortex.test.sense575 (:use (cortex world util sense vision))576 (:import577 java.io.File578 (com.jme3.math Vector3f ColorRGBA)579 (com.aurellem.capture RatchetTimer Capture)))580 #+end_src582 * Source Listing583 - [[../src/cortex/sense.clj][cortex.sense]]584 - [[../src/cortex/test/sense.clj][cortex.test.sense]]585 - [[../assets/Models/subtitles/subtitles.blend][subtitles.blend]]586 - [[../assets/Models/subtitles/Lake_CraterLake03_sm.hdr][subtitles reflection map]]587 #+html: <ul> <li> <a href="../org/sense.org">This org file</a> </li> </ul>588 - [[http://hg.bortreb.com ][source-repository]]590 * Next591 Now that some of the preliminaries are out of the way, in the [[./body.org][next592 post]] I'll create a simulated body.595 * COMMENT generate source596 #+begin_src clojure :tangle ../src/cortex/sense.clj597 <<sense-header>>598 <<blender-1>>599 <<blender-2>>600 <<topology-1>>601 <<topology-2>>602 <<node-1>>603 <<node-2>>604 <<view-senses>>605 #+end_src607 #+begin_src clojure :tangle ../src/cortex/test/sense.clj608 <<test-header>>609 <<test>>610 #+end_src612 #+begin_src clojure :tangle ../src/cortex/video/magick.clj613 <<magick>>614 #+end_src