Mercurial > cortex
view org/sense.org @ 334:c264ebf683b4
cleanup.
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Fri, 20 Jul 2012 11:22:21 -0500 |
parents | 702b5c78c2de |
children | d37ccb6c888f |
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 of key-value11 pairs which are called "Custom Properties". These are accessible in12 jMonkeyEngine when blender files are imported with the13 =BlenderLoader=. =meta-data= extracts these properties.15 #+name: blender-116 #+begin_src clojure17 (defn meta-data18 "Get the meta-data for a node created with blender."19 [blender-node key]20 (if-let [data (.getUserData blender-node "properties")]21 (.findValue data key) nil))22 #+end_src24 Blender uses a different coordinate system than jMonkeyEngine so it25 is useful to be able to convert between the two. These only come into26 play when the meta-data of a node refers to a vector in the blender27 coordinate system.29 #+name: blender-230 #+begin_src clojure31 (defn jme-to-blender32 "Convert from JME coordinates to Blender coordinates"33 [#^Vector3f in]34 (Vector3f. (.getX in) (- (.getZ in)) (.getY in)))36 (defn blender-to-jme37 "Convert from Blender coordinates to JME coordinates"38 [#^Vector3f in]39 (Vector3f. (.getX in) (.getZ in) (- (.getY in))))40 #+end_src42 * Sense Topology44 Human beings are three-dimensional objects, and the nerves that45 transmit data from our various sense organs to our brain are46 essentially one-dimensional. This leaves up to two dimensions in which47 our sensory information may flow. For example, imagine your skin: it48 is a two-dimensional surface around a three-dimensional object (your49 body). It has discrete touch sensors embedded at various points, and50 the density of these sensors corresponds to the sensitivity of that51 region of skin. Each touch sensor connects to a nerve, all of which52 eventually are bundled together as they travel up the spinal cord to53 the brain. Intersect the spinal nerves with a guillotining plane and54 you will see all of the sensory data of the skin revealed in a roughly55 circular two-dimensional image which is the cross section of the56 spinal cord. Points on this image that are close together in this57 circle represent touch sensors that are /probably/ close together on58 the skin, although there is of course some cutting and rearrangement59 that has to be done to transfer the complicated surface of the skin60 onto a two dimensional image.62 Most human senses consist of many discrete sensors of various63 properties distributed along a surface at various densities. For64 skin, it is Pacinian corpuscles, Meissner's corpuscles, Merkel's65 disks, and Ruffini's endings, which detect pressure and vibration of66 various intensities. For ears, it is the stereocilia distributed67 along the basilar membrane inside the cochlea; each one is sensitive68 to a slightly different frequency of sound. For eyes, it is rods69 and cones distributed along the surface of the retina. In each case,70 we can describe the sense with a surface and a distribution of sensors71 along that surface.73 ** UV-maps75 Blender and jMonkeyEngine already have support for exactly this sort76 of data structure because it is used to "skin" models for games. It is77 called [[http://wiki.blender.org/index.php/Doc:2.6/Manual/Textures/Mapping/UV][UV-mapping]]. The three-dimensional surface of a model is cut78 and smooshed until it fits on a two-dimensional image. You paint79 whatever you want on that image, and when the three-dimensional shape80 is rendered in a game the smooshing and cutting us reversed and the81 image appears on the three-dimensional object.83 To make a sense, interpret the UV-image as describing the distribution84 of that senses sensors. To get different types of sensors, you can85 either use a different color for each type of sensor, or use multiple86 UV-maps, each labeled with that sensor type. I generally use a white87 pixel to mean the presence of a sensor and a black pixel to mean the88 absence of a sensor, and use one UV-map for each sensor-type within a89 given sense. The paths to the images are not stored as the actual90 UV-map of the blender object but are instead referenced in the91 meta-data of the node.93 #+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.94 #+ATTR_HTML: width="300"95 [[../images/finger-UV.png]]97 #+CAPTION: Ventral side of the UV-mapped finger. Notice the density of touch sensors at the tip.98 #+ATTR_HTML: width="300"99 [[../images/finger-1.png]]101 #+CAPTION: Side view of the UV-mapped finger.102 #+ATTR_HTML: width="300"103 [[../images/finger-2.png]]105 #+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.106 #+ATTR_HTML: width="300"107 [[../images/finger-3.png]]109 The following code loads images and gets the locations of the white110 pixels so that they can be used to create senses. =load-image= finds111 images using jMonkeyEngine's asset-manager, so the image path is112 expected to be relative to the =assets= directory. Thanks to Dylan113 for the beautiful version of =filter-pixels=.115 #+name: topology-1116 #+begin_src clojure117 (defn load-image118 "Load an image as a BufferedImage using the asset-manager system."119 [asset-relative-path]120 (ImageToAwt/convert121 (.getImage (.loadTexture (asset-manager) asset-relative-path))122 false false 0))124 (def white 0xFFFFFF)126 (defn white? [rgb]127 (= (bit-and white rgb) white))129 (defn filter-pixels130 "List the coordinates of all pixels matching pred, within the bounds131 provided. If bounds are not specified then the entire image is132 searched.133 bounds -> [x0 y0 width height]"134 {:author "Dylan Holmes"}135 ([pred #^BufferedImage image]136 (filter-pixels pred image [0 0 (.getWidth image) (.getHeight image)]))137 ([pred #^BufferedImage image [x0 y0 width height]]138 ((fn accumulate [x y matches]139 (cond140 (>= y (+ height y0)) matches141 (>= x (+ width x0)) (recur 0 (inc y) matches)142 (pred (.getRGB image x y))143 (recur (inc x) y (conj matches [x y]))144 :else (recur (inc x) y matches)))145 x0 y0 [])))147 (defn white-coordinates148 "Coordinates of all the white pixels in a subset of the image."149 ([#^BufferedImage image bounds]150 (filter-pixels white? image bounds))151 ([#^BufferedImage image]152 (filter-pixels white? image)))153 #+end_src155 ** Topology157 Information from the senses is transmitted to the brain via bundles of158 axons, whether it be the optic nerve or the spinal cord. While these159 bundles more or less preserve the overall topology of a sense's160 two-dimensional surface, they do not preserve the precise euclidean161 distances between every sensor. =collapse= is here to smoosh the162 sensors described by a UV-map into a contiguous region that still163 preserves the topology of the original sense.165 #+name: topology-2166 #+begin_src clojure167 (in-ns 'cortex.sense)169 (defn average [coll]170 (/ (reduce + coll) (count coll)))172 (defn- collapse-1d173 "One dimensional helper for collapse."174 [center line]175 (let [length (count line)176 num-above (count (filter (partial < center) line))177 num-below (- length num-above)]178 (range (- center num-below)179 (+ center num-above))))181 (defn collapse182 "Take a sequence of pairs of integers and collapse them into a183 contiguous bitmap with no \"holes\" or negative entries, as close to184 the origin [0 0] as the shape permits. The order of the points is185 preserved.187 eg.188 (collapse [[-5 5] [5 5] --> [[0 1] [1 1]189 [-5 -5] [5 -5]]) --> [0 0] [1 0]]191 (collapse [[-5 5] [-5 -5] --> [[0 1] [0 0]192 [ 5 -5] [ 5 5]]) --> [1 0] [1 1]]"193 [points]194 (if (empty? points) []195 (let196 [num-points (count points)197 center (vector198 (int (average (map first points)))199 (int (average (map first points))))200 flattened201 (reduce202 concat203 (map204 (fn [column]205 (map vector206 (map first column)207 (collapse-1d (second center)208 (map second column))))209 (partition-by first (sort-by first points))))210 squeezed211 (reduce212 concat213 (map214 (fn [row]215 (map vector216 (collapse-1d (first center)217 (map first row))218 (map second row)))219 (partition-by second (sort-by second flattened))))220 relocated221 (let [min-x (apply min (map first squeezed))222 min-y (apply min (map second squeezed))]223 (map (fn [[x y]]224 [(- x min-x)225 (- y min-y)])226 squeezed))227 point-correspondence228 (zipmap (sort points) (sort relocated))230 original-order231 (vec (map point-correspondence points))]232 original-order)))233 #+end_src234 * Viewing Sense Data236 It's vital to /see/ the sense data to make sure that everything is237 behaving as it should. =view-sense= and its helper, =view-image=238 are here so that each sense can define its own way of turning239 sense-data into pictures, while the actual rendering of said pictures240 stays in one central place. =points->image= helps senses generate a241 base image onto which they can overlay actual sense data.243 #+name: view-senses244 #+begin_src clojure245 (in-ns 'cortex.sense)247 (defn view-image248 "Initializes a JPanel on which you may draw a BufferedImage.249 Returns a function that accepts a BufferedImage and draws it to the250 JPanel. If given a directory it will save the images as png files251 starting at 0000000.png and incrementing from there."252 ([#^File save]253 (let [idx (atom -1)254 image255 (atom256 (BufferedImage. 1 1 BufferedImage/TYPE_4BYTE_ABGR))257 panel258 (proxy [JPanel] []259 (paint260 [graphics]261 (proxy-super paintComponent graphics)262 (.drawImage graphics @image 0 0 nil)))263 frame (JFrame. "Display Image")]264 (SwingUtilities/invokeLater265 (fn []266 (doto frame267 (-> (.getContentPane) (.add panel))268 (.pack)269 (.setLocationRelativeTo nil)270 (.setResizable true)271 (.setVisible true))))272 (fn [#^BufferedImage i]273 (reset! image i)274 (.setSize frame (+ 8 (.getWidth i)) (+ 28 (.getHeight i)))275 (.repaint panel 0 0 (.getWidth i) (.getHeight i))276 (if save277 (ImageIO/write278 i "png"279 (File. save (format "%07d.png" (swap! idx inc))))))))280 ([] (view-image nil)))282 (defn view-sense283 "Take a kernel that produces a BufferedImage from some sense data284 and return a function which takes a list of sense data, uses the285 kernel to convert to images, and displays those images, each in286 its own JFrame."287 [sense-display-kernel]288 (let [windows (atom [])]289 (fn this290 ([data]291 (this data nil))292 ([data save-to]293 (if (> (count data) (count @windows))294 (reset!295 windows296 (doall297 (map298 (fn [idx]299 (if save-to300 (let [dir (File. save-to (str idx))]301 (.mkdir dir)302 (view-image dir))303 (view-image))) (range (count data))))))304 (dorun305 (map306 (fn [display datum]307 (display (sense-display-kernel datum)))308 @windows data))))))311 (defn points->image312 "Take a collection of points and visualize it as a BufferedImage."313 [points]314 (if (empty? points)315 (BufferedImage. 1 1 BufferedImage/TYPE_BYTE_BINARY)316 (let [xs (vec (map first points))317 ys (vec (map second points))318 x0 (apply min xs)319 y0 (apply min ys)320 width (- (apply max xs) x0)321 height (- (apply max ys) y0)322 image (BufferedImage. (inc width) (inc height)323 BufferedImage/TYPE_INT_RGB)]324 (dorun325 (for [x (range (.getWidth image))326 y (range (.getHeight image))]327 (.setRGB image x y 0xFF0000)))328 (dorun329 (for [index (range (count points))]330 (.setRGB image (- (xs index) x0) (- (ys index) y0) -1)))331 image)))333 (defn gray334 "Create a gray RGB pixel with R, G, and B set to num. num must be335 between 0 and 255."336 [num]337 (+ num338 (bit-shift-left num 8)339 (bit-shift-left num 16)))340 #+end_src342 * Building a Sense from Nodes343 My method for defining senses in blender is the following:345 Senses like vision and hearing are localized to a single point346 and follow a particular object around. For these:348 - Create a single top-level empty node whose name is the name of the sense349 - Add empty nodes which each contain meta-data relevant350 to the sense, including a UV-map describing the number/distribution351 of sensors if applicable.352 - Make each empty-node the child of the top-level353 node. =sense-nodes= below generates functions to find these children.355 For touch, store the path to the UV-map which describes touch-sensors in the356 meta-data of the object to which that map applies.358 Each sense provides code that analyzes the Node structure of the359 creature and creates sense-functions. They also modify the Node360 structure if necessary.362 Empty nodes created in blender have no appearance or physical presence363 in jMonkeyEngine, but do appear in the scene graph. Empty nodes that364 represent a sense which "follows" another geometry (like eyes and365 ears) follow the closest physical object. =closest-node= finds this366 closest object given the Creature and a particular empty node.368 #+name: node-1369 #+begin_src clojure370 (defn sense-nodes371 "For some senses there is a special empty blender node whose372 children are considered markers for an instance of that sense. This373 function generates functions to find those children, given the name374 of the special parent node."375 [parent-name]376 (fn [#^Node creature]377 (if-let [sense-node (.getChild creature parent-name)]378 (seq (.getChildren sense-node))379 (do ;;(println-repl "could not find" parent-name "node")380 []))))382 (defn closest-node383 "Return the physical node in creature which is closest to the given384 node."385 [#^Node creature #^Node empty]386 (loop [radius (float 0.01)]387 (let [results (CollisionResults.)]388 (.collideWith389 creature390 (BoundingBox. (.getWorldTranslation empty)391 radius radius radius)392 results)393 (if-let [target (first results)]394 (.getGeometry target)395 (recur (float (* 2 radius)))))))397 (defn world-to-local398 "Convert the world coordinates into coordinates relative to the399 object (i.e. local coordinates), taking into account the rotation400 of object."401 [#^Spatial object world-coordinate]402 (.worldToLocal object world-coordinate nil))404 (defn local-to-world405 "Convert the local coordinates into world relative coordinates"406 [#^Spatial object local-coordinate]407 (.localToWorld object local-coordinate nil))408 #+end_src410 ** Sense Binding412 =bind-sense= binds either a Camera or a Listener object to any413 object so that they will follow that object no matter how it414 moves. It is used to create both eyes and ears.416 #+name: node-2417 #+begin_src clojure418 (defn bind-sense419 "Bind the sense to the Spatial such that it will maintain its420 current position relative to the Spatial no matter how the spatial421 moves. 'sense can be either a Camera or Listener object."422 [#^Spatial obj sense]423 (let [sense-offset (.subtract (.getLocation sense)424 (.getWorldTranslation obj))425 initial-sense-rotation (Quaternion. (.getRotation sense))426 base-anti-rotation (.inverse (.getWorldRotation obj))]427 (.addControl428 obj429 (proxy [AbstractControl] []430 (controlUpdate [tpf]431 (let [total-rotation432 (.mult base-anti-rotation (.getWorldRotation obj))]433 (.setLocation434 sense435 (.add436 (.mult total-rotation sense-offset)437 (.getWorldTranslation obj)))438 (.setRotation439 sense440 (.mult total-rotation initial-sense-rotation))))441 (controlRender [_ _])))))442 #+end_src444 Here is some example code which shows how a camera bound to a blue box445 with =bind-sense= moves as the box is buffeted by white cannonballs.447 #+name: test448 #+begin_src clojure449 (defn test-bind-sense450 "Show a camera that stays in the same relative position to a blue451 cube."452 ([] (test-bind-sense false))453 ([record?]454 (let [eye-pos (Vector3f. 0 30 0)455 rock (box 1 1 1 :color ColorRGBA/Blue456 :position (Vector3f. 0 10 0)457 :mass 30)458 table (box 3 1 10 :color ColorRGBA/Gray :mass 0459 :position (Vector3f. 0 -3 0))]460 (world461 (nodify [rock table])462 standard-debug-controls463 (fn init [world]464 (let [cam (doto (.clone (.getCamera world))465 (.setLocation eye-pos)466 (.lookAt Vector3f/ZERO467 Vector3f/UNIT_X))]468 (bind-sense rock cam)469 (.setTimer world (RatchetTimer. 60))470 (if record?471 (Capture/captureVideo472 world (File. "/home/r/proj/cortex/render/bind-sense0")))473 (add-camera!474 world cam475 (comp (view-image476 (if record?477 (File. "/home/r/proj/cortex/render/bind-sense1")))478 BufferedImage!))479 (add-camera! world (.getCamera world) no-op)))480 no-op))))481 #+end_src483 #+begin_html484 <video controls="controls" width="755">485 <source src="../video/bind-sense.ogg" type="video/ogg"486 preload="none" poster="../images/aurellem-1280x480.png" />487 </video>488 <br> <a href="http://youtu.be/DvoN2wWQ_6o"> YouTube </a>489 #+end_html491 With this, eyes are easy --- you just bind the camera closer to the492 desired object, and set it to look outward instead of inward as it493 does in the video.495 (nb : the video was created with the following commands)497 *** Combine Frames with ImageMagick498 #+begin_src clojure :results silent499 (ns cortex.video.magick500 (:import java.io.File)501 (:use clojure.java.shell))503 (defn combine-images []504 (let505 [idx (atom -1)506 left (rest507 (sort508 (file-seq (File. "/home/r/proj/cortex/render/bind-sense0/"))))509 right (rest510 (sort511 (file-seq512 (File. "/home/r/proj/cortex/render/bind-sense1/"))))513 sub (rest514 (sort515 (file-seq516 (File. "/home/r/proj/cortex/render/bind-senseB/"))))517 sub* (concat sub (repeat 1000 (last sub)))]518 (dorun519 (map520 (fn [im-1 im-2 sub]521 (sh "convert" (.getCanonicalPath im-1)522 (.getCanonicalPath im-2) "+append"523 (.getCanonicalPath sub) "-append"524 (.getCanonicalPath525 (File. "/home/r/proj/cortex/render/bind-sense/"526 (format "%07d.png" (swap! idx inc))))))527 left right sub*))))528 #+end_src530 *** Encode Frames with ffmpeg532 #+begin_src sh :results silent533 cd /home/r/proj/cortex/render/534 ffmpeg -r 30 -i bind-sense/%07d.png -b:v 9000k -vcodec libtheora bind-sense.ogg535 #+end_src537 * Headers538 #+name: sense-header539 #+begin_src clojure540 (ns cortex.sense541 "Here are functions useful in the construction of two or more542 sensors/effectors."543 {:author "Robert McIntyre"}544 (:use (cortex world util))545 (:import ij.process.ImageProcessor)546 (:import jme3tools.converters.ImageToAwt)547 (:import java.awt.image.BufferedImage)548 (:import com.jme3.collision.CollisionResults)549 (:import com.jme3.bounding.BoundingBox)550 (:import (com.jme3.scene Node Spatial))551 (:import com.jme3.scene.control.AbstractControl)552 (:import (com.jme3.math Quaternion Vector3f))553 (:import javax.imageio.ImageIO)554 (:import java.io.File)555 (:import (javax.swing JPanel JFrame SwingUtilities)))556 #+end_src558 #+name: test-header559 #+begin_src clojure560 (ns cortex.test.sense561 (:use (cortex world util sense vision))562 (:import563 java.io.File564 (com.jme3.math Vector3f ColorRGBA)565 (com.aurellem.capture RatchetTimer Capture)))566 #+end_src568 * Source Listing569 - [[../src/cortex/sense.clj][cortex.sense]]570 - [[../src/cortex/test/sense.clj][cortex.test.sense]]571 - [[../assets/Models/subtitles/subtitles.blend][subtitles.blend]]572 - [[../assets/Models/subtitles/Lake_CraterLake03_sm.hdr][subtitles reflection map]]573 #+html: <ul> <li> <a href="../org/sense.org">This org file</a> </li> </ul>574 - [[http://hg.bortreb.com ][source-repository]]576 * Next577 Now that some of the preliminaries are out of the way, in the [[./body.org][next578 post]] I'll create a simulated body.581 * COMMENT generate source582 #+begin_src clojure :tangle ../src/cortex/sense.clj583 <<sense-header>>584 <<blender-1>>585 <<blender-2>>586 <<topology-1>>587 <<topology-2>>588 <<node-1>>589 <<node-2>>590 <<view-senses>>591 #+end_src593 #+begin_src clojure :tangle ../src/cortex/test/sense.clj594 <<test-header>>595 <<test>>596 #+end_src598 #+begin_src clojure :tangle ../src/cortex/video/magick.clj599 <<magick>>600 #+end_src