tag:blogger.com,1999:blog-200764572024-02-18T23:47:26.201-08:00Rants of a spudBiospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.comBlogger37125tag:blogger.com,1999:blog-20076457.post-71149211985177999492020-09-13T11:05:00.003-07:002020-09-13T11:05:37.091-07:00Infinite plane rendering 3: Image texturing, filtering, and antialiasingThis post is part three on the topic of infinite plane rendering. See <a href="https://biospud.blogspot.com/2020/05/how-to-draw-infinite-planes-in-computer.html" target="_blank">part 1</a> and <a href="https://biospud.blogspot.com/2020/05/infinite-plane-rendering-2-texturing.html" target="_blank">part 2</a>.<br />
<br />
Last time we textured the plane with a simple procedural texture. Now we add a proper image texture. First we use the texture coordinates from the procedural texture in part 2, to place a repeated version of a proper image on our infinite plane.<br />
<br />
<h3>
Filtering</h3>
Without texture filtering, we see many artifacts. Notice the graininess toward the horizon. Also note the Moire patterns in the patterns of parallel lines at near to mid distance:<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKXjjcn3OUSb3uFPgv2ce9pUDGRyWOsVTzzGVORaDFCvbjfG-U4_inCJghv40McUKOsZejrSUuEx6ky8SpFGwUJb9UHv0LlUeu3b-8NpncHIEjCCp5E7wzdoWSLt1-BQjBVQhp/s1600/texture2b.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="529" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKXjjcn3OUSb3uFPgv2ce9pUDGRyWOsVTzzGVORaDFCvbjfG-U4_inCJghv40McUKOsZejrSUuEx6ky8SpFGwUJb9UHv0LlUeu3b-8NpncHIEjCCp5E7wzdoWSLt1-BQjBVQhp/s640/texture2b.png" width="640" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Inifinite plane tiled with an unfiltered image texture. The image looks sorta OK up close, but everything is grainy at larger distances, and patterns of fine lines look bad even at moderate distances.</td></tr>
</tbody></table>
<br />
We can clear up most of those artifacts with trilinear filtering. When creating the texture, call<br />
<div>
<code> glGenerateMipmap(GL_TEXTURE_2D);</code></div>
and
<br />
<div>
<code> glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);</code></div>
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSpUUnhEqzkir6WUyHYgCJiwcIpsVZuGHrfAc9lkTfttGJObhSIcEAHzh16Oo1rtD6hk7t_Xyi9-SGL7ONBMJM33hvOZ5bswLywnysT6y7l2ps9Nd0TVNKynztHY3yz8QQ4A7s/s1600/filter1.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="530" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSpUUnhEqzkir6WUyHYgCJiwcIpsVZuGHrfAc9lkTfttGJObhSIcEAHzh16Oo1rtD6hk7t_Xyi9-SGL7ONBMJM33hvOZ5bswLywnysT6y7l2ps9Nd0TVNKynztHY3yz8QQ4A7s/s640/filter1.png" width="640" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Compared to the earlier unfiltered image, this trilinear filtered view avoids most of the artifacts. But the areas near the horizon get too blurry too fast.</td></tr>
</tbody></table>
<br />
Regular texture filtering does not work perfectly for oblique viewpoints, which is always the case near the horizon for our infinite plane. We need to use a technique called <i>anisotropic filtering</i>, which applies different filtering in different directions. Anisotropic filtering is not supported in pure OpenGL, so you need to load an extension GL_TEXTURE_MAX_ANISOTROPY_EXT and use it like this.<br />
<div>
<code> glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &f_largest);</code><br />
<code> glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, f_largest);</code></div>
<br />
With anisotropic filtering, the texture pattern looks better near the horizon (below):<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiz38Z1bYLzRXFyb-hJcNmeYqjaoSgAHQWmBMw-LaUxXZFJMcwu-m11at6ssgEJE-TT1BUHzL06G89ekFSmOSRMq78b8N1GzWHl7b1V0Ogj1_1Lk_D68PmEd-aIeRJNZeg9W53Y/s1600/filter3.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="530" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiz38Z1bYLzRXFyb-hJcNmeYqjaoSgAHQWmBMw-LaUxXZFJMcwu-m11at6ssgEJE-TT1BUHzL06G89ekFSmOSRMq78b8N1GzWHl7b1V0Ogj1_1Lk_D68PmEd-aIeRJNZeg9W53Y/s640/filter3.png" width="640" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Anisotropic filtering looks better by increasing the sharpness near the horizon. There remain some minor artifacts and shimmering, especially when viewed in VR. But this is the best I know how to do. Besides, we would ordinarily use much less demanding texture images. This texture is specifically designed to reveal display imperfections.</td></tr>
</tbody></table>
<br />
Using a more natural texture image, like the patch of grass below, shows no artifacts when viewed in VR using anisotropic filtering:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0Haa8dlPhS8JgFrcaxN_LoWLt4e6SsxMmNkY4dx-wVK62BjKOhKAf64F-T6Xye8dcTHmofbMD3qABUP2rQMyc7mwBGdkDDQ-wIWRDN_uBLFNRP8kxs5SRLfRuq45gEBTV1jMb/s1600/grass.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="532" data-original-width="642" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0Haa8dlPhS8JgFrcaxN_LoWLt4e6SsxMmNkY4dx-wVK62BjKOhKAf64F-T6Xye8dcTHmofbMD3qABUP2rQMyc7mwBGdkDDQ-wIWRDN_uBLFNRP8kxs5SRLfRuq45gEBTV1jMb/s320/grass.png" width="320" /></a></div>
<br />
After gazing at this infinite vista of grass in VR, the game "Infinimower" practically writes itself.<br />
<h3><br /></h3><h3>
Efficiency and Antialiasing</h3>
Up to this point we have been inefficiently drawing the plane. The vertex shader creates a procedural triangle strip covering the entire screen, and the fragment shader rejects all the pixels not on the infinite plane using the "discard" command. So even if you are looking up, away from the plane, the fragment shader is still executed for every single pixel on the screen. We can be more efficient than this, and also solve the problem of antialiasing at the horizon line at the same time.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXrbObdP7MwZA60j7XipnP1ZW9lBQX2ebWo8gWLb68TczbwZWayJg5ytk5c3WTabvesS0TFrKJ2j5ts6JlYwNcnKFXDFfgJvff4NVEh_fLUVo7xNyuEejKPeB8FPtkENAcEn9B/s1600/boundary1.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="544" data-original-width="657" height="264" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXrbObdP7MwZA60j7XipnP1ZW9lBQX2ebWo8gWLb68TczbwZWayJg5ytk5c3WTabvesS0TFrKJ2j5ts6JlYwNcnKFXDFfgJvff4NVEh_fLUVo7xNyuEejKPeB8FPtkENAcEn9B/s320/boundary1.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">So far we have been using a procedural vertex buffer, defined in the vertex shader, that covers the entire visible screen (pink rectangle). This is inefficient because the fragment shader gets invoked for each sky pixel too, even though these are never displayed.</td></tr>
</tbody></table>
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzHncsZZHw_Z-Ec9TNwPjHFzCR4xysnYJxyEM2AsdPUq_2Dhg00Urg4L3AznxyXl7p99oWZ1mr4tC5XVWCcOH07kghfIVSPqTVHEsuxBPozMgNH1IQ2Wa2zmlr_JYJZwp_DnLv/s1600/boundary2.png" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="544" data-original-width="657" height="264" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhzHncsZZHw_Z-Ec9TNwPjHFzCR4xysnYJxyEM2AsdPUq_2Dhg00Urg4L3AznxyXl7p99oWZ1mr4tC5XVWCcOH07kghfIVSPqTVHEsuxBPozMgNH1IQ2Wa2zmlr_JYJZwp_DnLv/s320/boundary2.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Here we use a more carefully selected vertex set, that matches the exact visible area of the infinite plane. This is a more efficient imposter geometry.</td></tr>
</tbody></table>
<br />
It would be possible to compute the optimal vertices on the host side, but today we'll compute it using a geometry shader program.<br />
<br />
This also solves the horizon aliasing problem. Now that we have a triangle boundary along the horizon, it can be antialiased using multisample antialiasing (MSAA). This is a widely used technique so I won't describe the details here.<br />
<br />Shader source code is at https://gist.github.com/cmbruns/5b1c2b211766cdcb3b29689e0c32a63d<br /><h4 style="text-align: left;"><br /></h4><h3 style="text-align: left;">Next Time:</h3><div><ul style="text-align: left;"><li>Using a 360 photo to cover the entire plane.</li></ul>
<br /></div>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-90686224811053216292020-05-13T20:15:00.000-07:002020-05-13T20:15:51.689-07:00Infinite Plane Rendering 2: Texturing and depth buffer<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/androidstudio.min.css" rel="stylesheet"></link>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
This is the second post about rendering infinite planes in 3D computer graphics. <a href="https://biospud.blogspot.com/2020/05/how-to-draw-infinite-planes-in-computer.html">Last time</a> we got as far as rendering a uniform brown plane. This time we will add texturing and correct use of the Z/depth buffer.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5PTVkH5B9j4ymFPOXXz6qc3kDNCpM5NBjQPgZxGerjnI8drQUXmi1sxwJRznxkGNLJLQKmMuHH5-iBQFhrARmuvyFuMvov8fzjWdMh3h3a-B5vs7SwR5ozrvb2yyVdXmicbr6/s1600/brown_plane.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh5PTVkH5B9j4ymFPOXXz6qc3kDNCpM5NBjQPgZxGerjnI8drQUXmi1sxwJRznxkGNLJLQKmMuHH5-iBQFhrARmuvyFuMvov8fzjWdMh3h3a-B5vs7SwR5ozrvb2yyVdXmicbr6/s320/brown_plane.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">"Brown plane" example from the previous post.</td></tr>
</tbody></table>
<br />
<h3>
Simplifying the math</h3>
Now that I'm looking at all this again, I see that we can simplify the plane math somewhat. Sorry I'm now going to change the variable names to match the plane ray tracing derivation in the OpenGL Super Bible. By the way, we aren't actually doing ray tracing in this infinite plane project, but rather simply "ray casting".<br />
<br />
Remember the implicit plane equation from last time:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace; text-align: center;"> A</span><i style="font-family: "courier new", courier, monospace; text-align: center;">x</i><span style="font-family: "courier new" , "courier" , monospace; text-align: center;"> + B</span><i style="font-family: "courier new", courier, monospace; text-align: center;">y</i><span style="font-family: "courier new" , "courier" , monospace; text-align: center;"> + C</span><i style="font-family: "courier new", courier, monospace; text-align: center;">z</i><span style="font-family: "courier new" , "courier" , monospace; text-align: center;"> + D<i>w</i> = 0 (1b)</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">Where:</span><br />
<br />
<ul>
<li><span style="font-family: inherit; text-align: center;">(A B C) is a vector normal to the plane, </span></li>
<li><span style="font-family: inherit; text-align: center;">(x y z) is any point in the plane, and </span></li>
<li><span style="font-family: inherit; text-align: center;">D is the signed distance from the plane to the origin</span></li>
</ul>
<br />
<span style="font-family: inherit; text-align: center;">We can rewrite that in vector form:</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span><span style="font-family: "courier new" , "courier" , monospace;"><span style="text-align: center;"> P</span><span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N + d = 0 (1c)</span></span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">Where: </span><br />
<br />
<ul>
<li><span style="font-family: inherit; text-align: center;">N is a vector perpendicular to the plane</span></li>
<li><span style="font-family: inherit; text-align: center;">P is any point on the plane, and </span></li>
<li><span style="font-family: inherit; text-align: center;">d is the signed distance of the plane from the origin</span></li>
</ul>
<span style="font-family: inherit; text-align: center;">That dot represents the dot product or inner product of two vectors.</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
We will combine the plane equation above with the ray equation below:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> P = O + tD (3)</span><br />
<br />
Where:<br />
<br />
<ul>
<li>P is any point along the ray, </li>
<li>O is the origin of the ray (i.e. the camera/viewer location), </li>
<li>D is the direction of the view ray, and </li>
<li>t is the scalar ray parameter. </li>
</ul>
Plug (3) into (1c) to get:<br />
<br />
<span style="font-family: "courier new" , "courier" , monospace;"> (O + tD)<span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N + d = 0</span></span><br />
<span style="font-family: inherit; text-align: center;"><br /></span><span style="font-family: inherit; text-align: center;">now solve for t:</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span><span style="font-family: "courier new" , "courier" , monospace;"><span style="text-align: center;"> t = -(O</span><span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N + d)/(D</span><span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N)</span></span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">and plug that back into the ray equation to get I, the intersection between the plane and the view ray:</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span><span style="font-family: "courier new" , "courier" , monospace;"><span style="text-align: center;"> I = O - D(</span><span style="text-align: center;">O</span><span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N + d)/(D</span><span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N)</span></span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">Now we can simplify this further: If we solve for the intersection of the plane and the view ray in view/camera space, then the view ray origin O, the position of the camera/eye, is all zeros (0, 0, 0), and the intersection point equation reduces to:</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span><span style="font-family: "courier new" , "courier" , monospace;"><span style="text-align: center;"> I = -dD</span><span style="text-align: center;">/(D</span><span style="background-color: white; color: #222222; font-size: 14px; text-align: center;">·</span><span style="text-align: center;">N) (4)</span></span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">That's pretty simple.</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">In our vertex shader, we compute the intersection point and pass it into the fragment shader as a homogeneous coordinate, with the denominator in the homogeneous <i>w</i> coordinate, as we discussed last time. A GLSL vertex shader code fragment is shown below:</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;"></span><br />
<div>
<span style="font-family: inherit; text-align: center;"><code class="glsl"></code></span></div>
<span style="font-family: inherit; text-align: center;">
</span>
<br />
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> point = vec4(</span></div>
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> -d * D.xyz, // xyz, numerator</span></div>
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> dot(D.xyz, N.xyz) // w, denominator</span></div>
<span style="font-family: "courier new" , "courier" , monospace;"><span style="text-align: center;"></span><br /></span>
<br />
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> );</span></div>
<span style="font-family: inherit; text-align: center;"><br /></span>
<br />
<h3>
<span style="font-family: inherit; text-align: center;">Texturing the plane</span></h3>
<span style="font-family: inherit; text-align: center;">That solid brown plane from the previous post could be hiding all sorts of errors. By adding texturing to the plane, we can visually distinguish different parts of the plane, so it becomes much easier to carefully verify correct rendering from inside the VR headset.</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<span style="font-family: inherit; text-align: center;">There are 4 different types of texturing we could apply to our plane:</span><br />
<span style="font-family: inherit; text-align: center;"><br /></span>
<br />
<ol>
<li><span style="font-family: inherit; text-align: center;">Solid color rendering, like we did in the previous brown plane example.</span></li>
<li><span style="font-family: inherit; text-align: center;">Ordinary 2D image texture rendering, where an image pattern is repeated over the plane.</span></li>
<li><span style="font-family: inherit; text-align: center;">Procedural texture rendering, where a computed texture is applied in the fragment shader.</span></li>
<li><span style="font-family: inherit; text-align: center;">Spherical image texture rendering, using a 360 image. This way we can paint the whole plane with one single image. That's a great way to combine the special "whole universe" coverage of spherical panoramas, with the "half universe" coverage of infinite planes. We will get to this final case in a future post.</span></li>
</ol>
<div style="text-align: left;">
For now we will start with the simplest of procedural textures: a color version of the texture coordinates themselves. This helps us to debug texturing, and establishes the basis for other more advanced texturing schemes.</div>
<div style="text-align: left;">
<br /></div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP3KpDaSilWfN_PHKsz_GYtA_s2zpkurO4uuG4d3Sf6fyn3CbsY2_0LPM7Ry-ADCj3ksrIJzI3VP1lMJv3GCP_d456YYMtQc1ttaZvZz0XslW5chniYDuqPR_rGIft-HKY8EgG/s1600/texture1b.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjP3KpDaSilWfN_PHKsz_GYtA_s2zpkurO4uuG4d3Sf6fyn3CbsY2_0LPM7Ry-ADCj3ksrIJzI3VP1lMJv3GCP_d456YYMtQc1ttaZvZz0XslW5chniYDuqPR_rGIft-HKY8EgG/s320/texture1b.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">A simple procedural texture tiling this infinite plane.</td></tr>
</tbody></table>
Now with the texturing in place, we can inspect the plane more carefully in VR, to help debug any rendering defects. For now it's (mostly) looking pretty good.<br />
<br />
The grainy junk near the horizon is there because we have no texture filtering. Texture filtering for procedural textures like this is an advanced and difficult task. We won't be solving it here, because this is just a waypoint for us on the way to ordinary image-based texturing. There, the filtering techniques available to us will become much more conventional and straightforward.<br />
<br />
<h3 style="text-align: left;">
Populating the depth buffer</h3>
<div style="text-align: left;">
I mentioned in the previous post that our initial brown plane example does not correctly clip through other objects. Let's correct that now.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
We can compute the correct depth buffer value for the plane intersection point by first converting the plane/ray intersection point into clip space (Normalized Device Coordinates) and then insert the depth value into the Z-buffer in the fragment shader.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
<div>
</div>
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"><code class="C"></code></span></div>
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> vec4 ndc = projection * point;</span></div>
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> ...</span></div>
<div style="text-align: left;">
<span style="font-family: "courier new" , "courier" , monospace;"> gl_FragDepth = (ndc.z / ndc.w + 1.0) / 2.0;</span></div>
<div style="text-align: left;">
</div>
</div>
<div style="text-align: left;">
</div>
<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhn7crclJo3-_vbUYfPwObNoW8ZLdW4Rw-YzYWXDvNbtDX-5YuW5vlYp282OFrIfSa51NgTvq1JMNXi0b4gbTx59BKhMdS0404NKSd9AZqICw2kxTLK65l0ufzJZqvSnKUvVGeR/s1600/clip1.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhn7crclJo3-_vbUYfPwObNoW8ZLdW4Rw-YzYWXDvNbtDX-5YuW5vlYp282OFrIfSa51NgTvq1JMNXi0b4gbTx59BKhMdS0404NKSd9AZqICw2kxTLK65l0ufzJZqvSnKUvVGeR/s320/clip1.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Notice the top section of a cube mesh being correctly clipped by the infinite plane. The rest of the cube is correctly hidden beneath the surface of the plane. Thanks to <span style="font-family: "courier new" , "courier" , monospace;">gl_FragDepth</span>.</td></tr>
</tbody></table>
There is one more nuance to using infinite planes with the depth buffer. We need to use a special sort of projection matrix that goes all the way to infinity, i.e. it has no far clip plane. So, for example, two infinite planes at different angles would clip each other correctly all the way to the horizon. To infinity! (but not beyond...)<br />
<br />
Shader code for this version is at <a href="https://gist.github.com/cmbruns/3c184d303e665ee2e987e4c1c2fe4b56">https://gist.github.com/cmbruns/3c184d303e665ee2e987e4c1c2fe4b56</a><br />
<br />
<br />
<h3 style="background-color: white; color: #222222; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; margin: 0px; position: relative;">
Topics for future posts:</h3>
<br style="background-color: white; color: #222222; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13.2px;" />
<ul style="background-color: white; color: #222222; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13.2px; line-height: 1.4; margin: 0.5em 0px; padding: 0px 2.5em;">
<li style="margin: 0px 0px 0.25em; padding: 0px;">Image-based texturing</li>
<li style="margin: 0px 0px 0.25em; padding: 0px;">Antialiasing</li>
<li style="margin: 0px 0px 0.25em; padding: 0px;">Drawing the horizon line</li>
<li style="margin: 0px 0px 0.25em; padding: 0px;">What happens if I look underneath that plane?</li>
<li style="margin: 0px 0px 0.25em; padding: 0px;">More efficient imposter geometries for rendering</li>
</ul>
<br />Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-3926390501966438052020-05-10T14:37:00.001-07:002020-05-13T20:17:28.594-07:00How to draw infinite planes in computer graphics<br />
Back in 2016 I figured out how to draw an infinite plane, stretching to the horizon, in my VR headset. I did this because I wanted a very simple scene, with the ground at my feet, stretching out to infinity. This blog post describes some of the techniques I use to render infinite planes.<br />
<br />
These techniques are done at the low-level OpenGL (or DirectX) layer. So you won't be doing this in Unity or Unreal or Blender or Maya. Unless you are developing or writing plugins for those systems.<br />
<br />
The good news is that we can use the usual OpenGL things like model, view, and projection matrices to render infinite planes. Also we can use clipping, depth buffering, texturing, and antialiasing. But we first need to understand some maths beyond what is usually required for the conventional "render this pile of triangles" approach to 3D graphics rendering. The infinite plane is just one among several special graphics primitives I am interested in designing custom renderers for, including infinite lines, transparent volumes, and perfect spheres, cylinders, and other quadric surfaces.<br />
<br />
<h3>
Why not just draw a really big rectangle?</h3>
The first thing I tried was to just draw a very very large rectangle, made of two triangles. But the horizon did not look uniform. I suppose one could fake it better with a more complex set of triangles. But I knew it might be possible to do something clever to render a mathematically perfect infinite plane. A plane that really extends endlessly all the way to the horizon.<br />
<br />
<h3>
How do you draw the horizon?</h3>
The horizon of an infinite plane always appears as a mathematically perfect straight line in the perspective projections ordinarily used in 3D graphics and VR. This horizon line separates the universe into two equal halves. Therefore, our first simple rendering of an infinite plane just needs to find out where that line is, and draw it.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Into_the_Horizon.jpg/320px-Into_the_Horizon.jpg" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="213" data-original-width="320" src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Into_the_Horizon.jpg/320px-Into_the_Horizon.jpg" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">The ocean surface is not really an infinite plane, of course. But it is similar enough to help motivate our expectations of what a truly infinite plane should look like. Notice that the horizon is a straight line. Image courtesy of <a href="https://en.wikipedia.org/wiki/File:Into_the_Horizon.jpg">https://en.wikipedia.org/wiki/File:Into_the_Horizon.jpg</a></td></tr>
</tbody></table>
But before we figure out where that line is, we need to set up some mathematical background.<br />
<br />
<h3>
Implicit equation of a plane</h3>
<div>
A particular plane can be defined using four values A, B, C, D in the implicit equation for a plane:</div>
<div>
<br /></div>
<div style="text-align: center;">
<span style="font-family: "courier new" , "courier" , monospace;"><b>A</b><i>x</i> + <b>B</b><i>y</i> + <b>C</b><i>z</i> + <b>D</b> = 0 (1a)</span></div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
All points (x y z) that satisfy this equation are on the plane. The first three plane values, (A B C), form a vector perpendicular to the plane. I usually scale these three (A B C) values to represent a unit vector with length 1.0. Then the final value, D, is the signed closest distance between the plane and the world origin at (0, 0, 0).</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
This implicit equation for the plane will be used in our custom shaders that we are about to write. It's only four numbers, so it's much more compact than even the "giant rectangle" version of the plane, which takes at least 12 numbers to represent.</div>
<div style="text-align: left;">
<br /></div>
<div style="text-align: left;">
When rendering the plane, we will often want to know exactly where the user's view direction intersects the plane. But there is a problem here. If the plane is truly infinite, sometimes that intersection point will consist of obscenely large coordinate values if the viewer is looking near the horizon. How do we avoid numerical problems in that case? Good news! The answer is already built into OpenGL!<br />
<br />
<h3>
Homogeneous coordinates to the rescue</h3>
In OpenGL we actually manipulate four dimensional vectors (<i>x y z w</i>) instead of three dimensional vectors (<i>x y z</i>). The <i>w</i> coordinate value is usually 1, and we usually ignore it if we can. It's there to make multiplication with the 4x4 matrices work correctly during projection and translation. In cases where the <i>w</i> is not 1, you can reconstruct the expected (<i>x y z</i>) values by dividing everything by <i>w</i>, to get the point (<i>x/w, y/w, z/w</i>, 1), which is equivalent to the point (<i>x y z w</i>).<br />
<br />
But that homogeneous coordinate <i>w</i> is actually our savior here. We are going to pay close attention to <i>w</i>, and we are going to avoid the temptation to divide by <i>w</i> until it is absolutely necessary.<br />
<br />
The general principle is this: if we see cases, like points near the plane horizon, where it seems like coordinate values (<i>x y z</i>) might blow up to infinity, let's instead try to keep the (<i>x y z</i>) values smaller and stable, while allowing the <i>w</i> value to approach zero. Allowing <i>w</i> to approach zero is mathematically equivalent to allowing (<i>x y z</i>) to approach infinity, but it does not cause the same numerical stability problems on real world computers - as long as you avoid dividing by <i>w</i>.</div>
<br />
By the way, the homogeneous version of the plane equation is<br />
<br />
<div style="text-align: center;">
<span style="font-family: "courier new" , "courier" , monospace;">A</span><i style="font-family: "Courier New", Courier, monospace;">x</i><span style="font-family: "courier new" , "courier" , monospace;"> + B</span><i style="font-family: "Courier New", Courier, monospace;">y</i><span style="font-family: "courier new" , "courier" , monospace;"> + C</span><i style="font-family: "Courier New", Courier, monospace;">z</i><span style="font-family: "courier new" , "courier" , monospace;"> + D<i><b>w</b></i> = 0 (1b)</span></div>
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<br />
<h3>
<span style="text-align: center;"><span style="font-family: inherit;"><b>Intersection between view ray and plane</b></span></span></h3>
<span style="text-align: center;"><span style="font-family: inherit;">The intersection between a line and a plane (in vector notation, those are cross products and dot products below) is</span></span><br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<br />
<div style="text-align: center;">
<span style="font-family: "courier new" , "courier" , monospace;"><span style="text-align: center;"><span style="font-family: inherit;">I = ((P<sub>n</sub><span style="background-color: white; color: #222222; font-size: 14px; text-align: left;">⨯</span>(V<span style="background-color: white; color: #222222; font-size: 14px; text-align: left;">⨯</span>L)) - P<sub>d </sub>V)/(</span></span><span style="background-color: white; color: #222222; font-size: 14px;"><span style="color: black; font-family: "times new roman"; font-size: small; text-align: center;">P</span><sub style="color: black;">n</sub>·</span><span style="font-family: inherit; text-align: center;">V) (2)</span></span></div>
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<span style="text-align: center;"><span style="font-family: inherit;">where I is the intersection point, P<sub>n</sub> is the plane normal (A B C), V is the view direction, L is the camera/viewer location, and P<sub>d</sub> is the D component of the plane equation.</span></span><br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<span style="text-align: center;"><span style="font-family: inherit;">Problems can occur when the denominator of this expression approaches zero. The intersection point needs to be expressed in the form <i>(xyz)/w</i>. Instead of setting <i>w</i> to 1, as usual, let's set <i>w</i> to the denominator </span></span><span style="background-color: white; color: #222222; font-family: "courier new" , "courier" , monospace; font-size: 14px; text-align: center;"><span style="color: black; font-family: "times new roman"; font-size: small; text-align: center;">P</span><sub style="color: black;">n</sub>·</span><span style="font-family: "courier new" , "courier" , monospace; text-align: center;">V</span><span style="font-family: inherit; text-align: center;">.</span><br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<span style="text-align: center;"><span style="font-family: inherit;">This means that the <i>w</i> (homogeneous) coordinate of the point on the plane intersecting a particular view ray will be the dot product of the view direction with the plane normal. The range of <i>w</i> values will range between -1 (when the view direction is directly opposite the plane normal) and +1 (when the view direction is exactly aligned with the plane normal). And the</span></span><span style="text-align: center;"> </span><span style="font-family: inherit; text-align: center;"><i>w</i> value will be zero when the view is looking exactly at the horizon.</span><br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<span style="text-align: center;"><span style="font-family: inherit;">At this point, we can actually create our first simple rendering of an infinite plane. And we only need to consider the homogeneous <i>w</i> coordinate of the view/plane intersection point at first.</span></span><br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<br />
<span style="text-align: center;"><span style="font-family: inherit;"><br /></span></span>
<br />
<h3>
Version 1: Brown plane</h3>
<div>
Here is the first version of our plane renderer, which only uses the homogeneous <i>w</i> coordinate:</div>
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDveCCbpVnlgfofMj8qmF3cgRLhWdhr1W6im5Cv-RLZfEJMZ9KoXqU3QeiCq_r5N3iTHCnMWfN_LlHYoRWBv8beO1CzjxYBfPVOCGeEeVoT4c9kBuLoEF5cnhUiJvcP_cjzTBK/s1600/brown_plane.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" data-original-height="532" data-original-width="642" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDveCCbpVnlgfofMj8qmF3cgRLhWdhr1W6im5Cv-RLZfEJMZ9KoXqU3QeiCq_r5N3iTHCnMWfN_LlHYoRWBv8beO1CzjxYBfPVOCGeEeVoT4c9kBuLoEF5cnhUiJvcP_cjzTBK/s320/brown_plane.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">Screenshot as I was viewing this infinite plane in VR</td></tr>
</tbody></table>
<br />
The horizon looks a little higher than it does when you look out at the ocean. But that's physically correct. The earth is not flat.<br />
<br />
This plane doesn't intersect other geometry correctly, the horizon shows jaggedy aliasing, there's no texture, and it probably does not scale optimally. We will fix those things later. But this is a good start.<br />
<br />
Source code for the GLSL shaders I used here is at at <a href="https://gist.github.com/cmbruns/815fc875afd8fe2755500907325b15f0">https://gist.github.com/cmbruns/815fc875afd8fe2755500907325b15f0</a><br />
<br />
Continue to the next post in this series at <a href="https://biospud.blogspot.com/2020/05/infinite-plane-rendering-2-texturing.html">https://biospud.blogspot.com/2020/05/infinite-plane-rendering-2-texturing.html</a><br />
<h3>
Topics for future posts:</h3>
<br />
<ul>
<li>Texturing the plane</li>
<li>Using the depth buffer to intersect other geometry correctly</li>
<li>Antialiasing</li>
<li>Drawing the horizon line</li>
<li>What happens if I look underneath that plane?</li>
<li>More efficient imposter geometries for rendering</li>
</ul>
Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-70133200479603333692016-03-03T05:50:00.001-08:002016-03-03T06:02:28.865-08:00What programming language do 20-year-olds use?In the interest of converting vague correlations into divisive stereotypes, here are some programming languages, sorted by the age of the programmer:<br />
<ul>
<li><b>70</b> year-old programmers code in <b>FORTRAN</b></li>
<li><b>60</b> year-old programmers code in <b>COBOL</b></li>
<li><b>50</b> year-old programmers code in <b>C++</b></li>
<li><b>40</b> year-old programmers code in <b>Java</b></li>
<li><b>30</b> year-old programmers code in <b>JavaScript</b></li>
<li>(I don't know what <b>20</b> year-olds do; something on smartphones?)</li>
<li><b>10</b> year-old programmers code in <a href="http://minecraft.gamepedia.com/Command_Block"><b>Minecraft command blocks</b></a></li>
<li><b>newborn</b> programmers (will) code in <a href="http://www.fisher-price.com/en_US/codeapillar/index.html"><b>Code-a-pillar</b></a>*</li>
</ul>
<ul>
<li>Everyone codes in <b>Python</b> for their hobby projects, because life is short. </li>
</ul>
* I am not affiliated with the makers of Code-a-pillar. I just think it's funnier to have the 20-year-olds be the only ones I'm confused about.<br />
<br />
<b>Corollary</b>: Asking programmer job applicants what their strongest programming languages are should be illegal in the USA, where "age" is a protected hiring category.<br />
<br />
<br />Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-31025677078878025072015-10-02T07:37:00.000-07:002015-10-02T07:37:24.844-07:00High-performance Visualization of Neurons, Molecules, and Other Graph-Like Models Using Quadric Imposters<br />
In my day job in scientific visualization, I am sometimes called upon to display models of neurons or molecules. Both types of models are "graph-like", in the computer science data structure sense, in that they consist of a collecion of Nodes, connected by Edges.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsnZzkWfvWMIVBcy4sJP0TeXskp5sglqYDrWtHqB135Gt3IMcjZTlTDG38qbOlvJ-GjGlQ0qSg3aODv772_Sqq0DULbyjjodESimR0_JxtV8EamZkG9ZVA3_fAPSCe3ybXLtTY/s1600/nodes_edges2.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsnZzkWfvWMIVBcy4sJP0TeXskp5sglqYDrWtHqB135Gt3IMcjZTlTDG38qbOlvJ-GjGlQ0qSg3aODv772_Sqq0DULbyjjodESimR0_JxtV8EamZkG9ZVA3_fAPSCe3ybXLtTY/s320/nodes_edges2.png" width="232" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">General Graph data structure. This graph consists of six edges and seven nodes.</td></tr>
</tbody></table>
<br />
<br />
<br />
<br />
<br />
<br />
In the case of molecules, the Nodes are Atoms, and the Edges are Bonds. Molecules and atoms make up all of the air and earth and animals and tacos and all the other stuff of the world.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgx0XVplP1PaPSO32jRMhyphenhyphen64j0QcGeljxFgc__ELxcyQfAJ86xTeVofTikbfbQRiqP6tpsTI0lr8VOHdP5MLCeRBRTv8V0nbXAhEzHKERXOMJhRGpFfdaV4EvHh7DpECdhTznhD/s1600/MoleculeNodes2.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" height="241" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgx0XVplP1PaPSO32jRMhyphenhyphen64j0QcGeljxFgc__ELxcyQfAJ86xTeVofTikbfbQRiqP6tpsTI0lr8VOHdP5MLCeRBRTv8V0nbXAhEzHKERXOMJhRGpFfdaV4EvHh7DpECdhTznhD/s320/MoleculeNodes2.png" width="320" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">This serotonin molecule contains 24 atoms and 26 bonds. This representation uses spheres and cylinders to represent atoms and bonds.</td></tr>
</tbody></table>
In the case of neurons, the Edges are piecewise linear segments of
neurites, and the Nodes are branch points, tips, and bends of those
segments. In both molecules and neurons, the Nodes have an XYZ location
in space, and a size. These Nodes are usually well represented by
spheres of a particular size.<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhU8e5R-sQqtWRpdooxTjGFQSO6l0uHfLI-70pwhnzqQCfpUefKaTDcP68NZr2EdLQ23JUELLiPFOxAzz6DJWf_nXcXFwQghRk1uKBlqtUlLboji2AcIvzqsiJ7nG1VhWt6pkqT/s1600/Neuron1.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhU8e5R-sQqtWRpdooxTjGFQSO6l0uHfLI-70pwhnzqQCfpUefKaTDcP68NZr2EdLQ23JUELLiPFOxAzz6DJWf_nXcXFwQghRk1uKBlqtUlLboji2AcIvzqsiJ7nG1VhWt6pkqT/s1600/Neuron1.png" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">This neuron has many branch points, tips and bends. Neurons are the cells that animals use to think and to control movement.</td></tr>
</tbody></table>
In the past few years, I have been very interested in using Quadric Imposters to represent scientific models. I have been able to achieve very high-performance rendering, while attaining flawless image quality. By high-performance, I mean that I am now able to display models containing hundreds of thousands of nodes, with flawless performance, compared to perhaps only thousands of nodes using traditional mesh-based rendering methods.<br />
<br />
Imposter models look better and run faster. What's not to like? The only downside is that it requires a lot of tricky work on the part of the programmer. Me.<br />
<br />
You probably learned the quadratic formula in high school:<br />
<span class="st">$$x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}$$</span><br />
<br />
<span class="st">In any case, the quadratic formula can be used to solve for \(x\) in polynomial equations of the form:</span><br />
<span class="st">$$ax^2+bx+c=0$$ </span><br />
<br />
<span class="st">So the trick for imposter rendering is to derive a quadratic polynomial for each analytic shape you want to display. I have already done this and have it working for the following shapes:</span><br />
<ul>
<li><span class="st">Sphere (easiest)</span></li>
<li><span class="st">Cylinder</span></li>
<li><span class="st">Cone</span></li>
</ul>
<span class="st"><a href="http://biospud.blogspot.com/2012/07/sphere-imposters-in-opengl-shading.html">I wrote a little about sphere imposters in the past</a>.</span><br />
<br />
<span class="st">I plan to eventually do the same treatment for</span><br />
<ul>
<li><span class="st">Ellipsoid</span></li>
<li><span class="st">Ellipsoid cylinder</span></li>
<li><span class="st">Ellipsoid cone</span></li>
<li><span class="st">Single sheet hyperboloid</span></li>
<li><span class="st">Dual sheet hyperboloid</span><span class="st"></span></li>
</ul>
<span class="st">By the way, I just now started using </span><a href="https://www.mathjax.org/">MathJax</a> for displaying equations. The equations look nice, right?<br />
<br />
<br />Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-13988021782074640632013-03-17T07:46:00.001-07:002013-03-17T07:46:19.985-07:00Chumby and Zeo and Reader; Oh My!My old digital lifestyle is falling apart. Three of my daily standbys are being discontinued.<br />
<br />
It started last month when my <a href="http://www.chumby.com/">Chumby</a>, a sort of internet connected clock radio, stopped displaying my chosen widgets, and started showing only <a href="http://www.theverge.com/2013/1/14/3875198/chumby-platform-could-die-in-february-as-funding-dries-up">one weird clock</a>. It turns out they have been out of business for a year.<br />
<br />
Then I heard that <a href="http://www.myzeo.com/sleep/">Zeo</a>, my headband-mounted sleep-tracker, is <a href="http://www.wired.com/business/2013/03/lights-out-for-zeo/">going out of business</a>. Unlike chumby, I had time to download my historical data in advance. But I might not be able to get my FUTURE sleep data.<br />
<br />
Now I learn that <a href="http://www.google.com/reader/">Reader</a>, Google's RSS aggregation service <a href="http://www.economist.com/blogs/babbage/2013/03/end-google-reader">will be shut down</a> this summer. That's how I read the internet! I'm looking into <a href="http://www.feedly.com/">feedly</a> as a replacement.<br />
<br />
There is a positive way to look at this. Perhaps the sickly vestiges of the previous boom economy are being sloughed off, so the next boom can commence.<br />
<br />Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-77566700919964602112012-07-14T17:47:00.002-07:002012-07-14T19:08:20.239-07:00Seven ways to communicate depth in 3D graphics<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/rEwOueqHLtU?feature=player_embedded' frameborder='0'></iframe></div>
This video from YouTube shows a molecule visualization I created this morning, using a little application I have been working on. I'm using this as an excuse to pontificate about my philosophy of 3D visualization today.<br />
<br />
Much of my work centers around creating computer software for visualizing various three-dimensional objects. These applications run on computers with two-dimensional displays; so there is a problem with conveying information in that third (depth) dimension. The human brain is hard-wired to convert two dimensional
visual information into an internal three-dimensional representation of a
scene. We can leverage this specialized hardware to convey a sense of
depth using only a two dimensional screen.<br />
<br />
You might assume that I believe stereo 3D to be the best way to convey depth information. But you'd be wrong. I am an evangelist for stereoscopic visualization. I love stereo 3D displays. But there are at least four other 3D visualization techniques that are more important than stereo. You must nail those four before you even think about stereo 3D. Below I have summarized my list of seven ways to enhance the perception of depth in 3D computer applications.<br />
<br />
Without further ado; the list. In order of most important to least important:<br />
<ol>
<li><h2>
Occlusion </h2>
Occlusion is the most important cue for communicating depth (Not "ambient occlusion", that's a kind of shading, technique #2). Occlusion means that you cannot see an object when it is behind another object. It's that simple. But displaying occlusion correctly is the most important part of conveying depth in computer graphics. In my caffeine video, atoms in the front occlude the atoms behind them. Fortunately, almost nobody gets this wrong; because everyone recognizes that it looks terrible when it is done wrong. OpenGL has always used a z-buffer to manage occlusion, so most 3D applications get occlusion right. Another approach, used by side-scrolling video games, is the "painters algorithm" (draw the stuff in back first) to give a sense of depth by occlusion.<br />
<br />
Occlusion tells me that the orange thing is in front of the other things.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://jwopitz.files.wordpress.com/2008/09/badiso.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="186" src="http://jwopitz.files.wordpress.com/2008/09/badiso.png" width="320" /></a></div>
<br />
One interesting display technique that does <i>not</i> respect
occlusion, is volume rendering by maximum intensity projection. Although volume
rendering is difficult to do well, the maximum intensity projection
method yields a robust rendering of brightness and detail But the
image looks the same whether viewed front-to-back or back-to-front. I
know from experience that this can be confusing. But the advantages of
the maximum intensity projection can sometimes make this tradeoff
worthwhile.<br />
<br />
</li>
<li><h2>
Shading</h2>
By shading, I mean all of the realistic colors, gradations, shadows and highlights seen on objects in the real world. Shading is so important, that when some folks say "3D graphics", all they mean is fancy shading. This is one area that has been steadily evolving in computer graphics. My caffeine video uses a primitive (by modern standards) Lambertian shading model for the spheres, with a single point light source. The Lambertian model is sufficient to convey depth and roundness, but looks rather fake compared to state-of-the art rendering. Part of my excruciating jealousy of QuteMol comes from the clever shading techniques they have used. For this reason I plan to continue to improve the shading methods in my application.<br />
<br />
Just look at the beautiful shading possible with QuteMol. I'm so jealous: <br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://qutemol.sourceforge.net/splash/splash.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://qutemol.sourceforge.net/splash/splash.jpg" /></a></div>
<br />
</li>
<li><h2>
Perspective </h2>
Perspective, what artists call foreshortening, is the visual effect that objects close to you appear larger than objects far away from you. In my caffeine video, nearby atoms appear larger than distant atoms, especially when the molecule is close to the camera. This is one area where my worship of QuteMol breaks down. QuteMol appears to use orthographic projection, not perspective. Close atoms are rendered the same size as distant ones. But it took me a long time to notice, because QuteMol's beautiful shading is otherwise so effective at communicating depth.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://upload.wikimedia.org/wikipedia/commons/7/77/Salisbury_Cathedral_Cloisters1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="240" src="http://upload.wikimedia.org/wikipedia/commons/7/77/Salisbury_Cathedral_Cloisters1.jpg" width="320" /></a></div>
</li>
<li><h2>
Motion parallax</h2>
There are several ways that motion can reveal depth information by showing parallax, in which closer objects appear more displaced than more distant objects. When an object rotates or moves, parallax effects reveal important depth information. In my caffeine video, the rotations of the molecule help to convey the sense of depth.<br />
<br />
Many 3D visualization applications use mouse dragging to rotate the scene. Users are constantly rotating the scene with the mouse while trying to examine the objects. These users crave motion parallax. In response, I have been experimenting with automated subtle wiggling of the scene so the user might not need to constantly drag the mouse. But I am not sure I have nailed the solution yet.<br />
<br />
Another important source of parallax is when the <i>viewer</i> moves. This is the basis of head tracking in 3D graphics. Every time I give a stereoscopic 3D demo, the first thing the viewer does after putting on the 3D glasses is to move her head from side to side; because that is the natural response to wanting to maximize the perception of depth. But it doesn't work; because my applications do not do head tracking (yet). Motion parallax is more important than stereoscopic 3D.<br />
<br />
The video below from 2007 is a famous example of the effectiveness of head tracking for conveying depth.<br />
<div class="separator" style="clear: both; text-align: center;">
<iframe allowfullscreen='allowfullscreen' webkitallowfullscreen='webkitallowfullscreen' mozallowfullscreen='mozallowfullscreen' width='320' height='266' src='https://www.youtube.com/embed/Jd3-eiid-Uw?feature=player_embedded' frameborder='0'></iframe></div>
</li>
<li><h2>
Stereoscopy </h2>
Your left and right eyes see slightly different views of a scene, and your brain can use these two images to perceive your distance from the objects you see. This is static parallax, as opposed to motion parallax (described in the previous section). Done properly, stereoscopic viewing can complete the sense of depth in a scene. But there are a lot of ways to get it wrong. That is why stereoscopic display must be approached with extreme care. My caffeine video uses YouTube's awesome stereoscopic support to display the molecule in stereo 3D. I like viewing it with my Nvidia 3D vision glasses (requires a special 120Hz monitor); though for some reason the aspect ratio is wrong in this mode. The other 3D modes seem to work fine though. Part of what I enjoy about stereo 3D is that there are so many details that must be done correctly; I like details.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://upload.wikimedia.org/wikipedia/commons/b/b6/Art_Institute_of_Chicago_Lion_Statue_%28parallel_stereo_pair%29.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="161" src="http://upload.wikimedia.org/wikipedia/commons/b/b6/Art_Institute_of_Chicago_Lion_Statue_%28parallel_stereo_pair%29.jpg" width="320" /></a></div>
</li>
<li><h2>
Fog </h2>
When you see a mountain on the horizon far away, it appears much paler and bluer than it does close up. That distant mountain can be almost indistinguishable from the color of sky behind it. The more distant an object is, the closer its color becomes to the color of the sky. Even on a clear day, for extremely distant objects, like far off mountains, what I am calling "fog" has an important effect. On a foggy day, the same thing occurs on a vastly smaller scale. In either case, that change in color is an important cue about the distance of an object. In computer graphics, fog (or depth cueing) is easy to compute and has been used for ages, especially when other 3D effects were too hard to achieve. My molecule viewing application uses fog, but at a low setting, and might not be visible in my caffeine video. Fog is especially important as objects approach the rear clipping plane, to avoid "pop ups", the sudden appearance or disappearance of objects. It is more pleasing if the objects disappear by gradually receding into the fog.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://upload.wikimedia.org/wikipedia/commons/0/05/Zama_distant_mountains.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="240" src="http://upload.wikimedia.org/wikipedia/commons/0/05/Zama_distant_mountains.JPG" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://upload.wikimedia.org/wikipedia/commons/e/e3/Trees_snow_fog_sk.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="212" src="http://upload.wikimedia.org/wikipedia/commons/e/e3/Trees_snow_fog_sk.jpg" width="320" /></a></div>
</li>
<li><h2>
Depth of field</h2>
When you photograph a scene using a wide aperture lens, the focal point of your scene may be in sharp focus, but other objects that are either much closer to the camera or much farther away appear blurry. This blurriness is a cue that those other objects are not at the same distance as the focal point. This depth cue can also convey a false sense of scale in trick photography. An aerial city scene with an extremely narrow depth of field can appear to be just a tiny model of a city. Depth of field is not widely used in interactive computer graphics, because it is expensive to compute, it's a subtle effect, and to really do it properly, the focused part of the image should follow the user's gaze. Not just head tracking; but <i>eye</i> tracking would be required. Even the Hollywood movies make only light use of depth of field; in part because it is not possible to be certain where the audience's gaze is directed.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://upload.wikimedia.org/wikipedia/commons/thumb/3/38/DOF-ShallowDepthofField.jpg/800px-DOF-ShallowDepthofField.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="230" src="http://upload.wikimedia.org/wikipedia/commons/thumb/3/38/DOF-ShallowDepthofField.jpg/800px-DOF-ShallowDepthofField.jpg" width="320" /></a></div>
</li>
</ol>
<br />
Most of the techniques I know of can be assigned to one of those seven categories. Have I missed any other depth conveying techniques? Comments are welcome below.<br />
<br />Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-43679316832251867962012-07-08T17:44:00.000-07:002012-07-08T17:47:27.211-07:00Sphere imposters in OpenGL shading language<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4F_vWU8ykvkRBI36BnHNkLCvLHfwySkqsxgdkBX8wTOJpmHMDjnErZTbixpAjHGdIoND4rM-HGVfKA4eEt5gxAORExgUUxS7MRxAGwAPoL9LnlsFDXXa9Y-Z0STwPfKImtsDP/s1600/caffeine2.jpg" imageanchor="1" style="clear: left; float: left; margin-bottom: 1em; margin-right: 1em;"><img border="0" height="261" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4F_vWU8ykvkRBI36BnHNkLCvLHfwySkqsxgdkBX8wTOJpmHMDjnErZTbixpAjHGdIoND4rM-HGVfKA4eEt5gxAORExgUUxS7MRxAGwAPoL9LnlsFDXXa9Y-Z0STwPfKImtsDP/s320/caffeine2.jpg" width="320" /></a></div>
I recently started a hobby project to create a little OpenGL viewing application. I am experimenting with custom shaders to create sphere imposters. It's working pretty well. Here is a caffeine molecule.<br />
<br />
Each sphere uses only two triangles; compared with the thousands of triangles that would be needed to get similar smoothness with the classic OpenGL pipeline.<br />
<br />
This week I implemented the custom shaders and various stereoscopic viewing options. Next I intend to implement interpolated fly-through movie making, and then upload a 3D video to YouTube.<br />
<br />
I should also work on measuring and improving performance like I did with my <a href="http://biospud.blogspot.com/2012/02/measuring-performance-of-immeidate-mode.html">earlier sphere rendering project</a>. Performance is sluggish when I try to view a protein with thousands of atoms. But there is a lot of room for improvement.<br />
<br />
Four of the five sphere below use the fixed-function OpenGL pipeline, and have hundreds of polygons each. One sphere has two triangles and uses my shaders. Can you tell which one?<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8SxOSBMebgiwO-VB6QTzjQ5LS8x7yhBcekmbVB613mATArBUiD7iRrfCIju2ocig2866O2gcimeIu2mJ5-lzXbN_9R27YsMf-DqegZTNouYDCoqHAOtB1BT5h0nSKUOQma0Vz/s1600/five_spheres.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="141" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8SxOSBMebgiwO-VB6QTzjQ5LS8x7yhBcekmbVB613mATArBUiD7iRrfCIju2ocig2866O2gcimeIu2mJ5-lzXbN_9R27YsMf-DqegZTNouYDCoqHAOtB1BT5h0nSKUOQma0Vz/s320/five_spheres.jpg" width="320" /></a></div>
My inspiration for this sphere imposter project was<a href="http://qutemol.sourceforge.net/"> QuteMol</a>. I am a long way from achieving what those guys did. But I am proud of what I have done so far anyway.<br />
<br />Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com1tag:blogger.com,1999:blog-20076457.post-11851703713328672272012-02-05T12:00:00.001-08:002012-02-05T13:39:10.795-08:00Measuring performance of immediate mode sphere renderingIn my previous post we created a simple hello world OpenGL application. Now we move one step closer to my goal by actually rendering some spheres. And measuring rendering performance.<br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrOwMAyjFnxXujZgwT6v_sFd8cPwJLP_RoW7qTtHgcFscWHXQQomd9IakU_J8hPAg1vXr3wnrDCn3QCXGdVRjxLQAD6pK0_HNTpBJlIWGLPI3Vui2osFmxxY88nI0XoT0u3OSR/s1600/spheres1.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 248px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrOwMAyjFnxXujZgwT6v_sFd8cPwJLP_RoW7qTtHgcFscWHXQQomd9IakU_J8hPAg1vXr3wnrDCn3QCXGdVRjxLQAD6pK0_HNTpBJlIWGLPI3Vui2osFmxxY88nI0XoT0u3OSR/s320/spheres1.jpg" alt="" id="BLOGGER_PHOTO_ID_5705745825561924146" border="0" /></a><br />This program draws a bunch of spheres in immediate rendering mode. This is the slowest possible way of doing it. But it is also the simplest to program and requires the least indirection. If the performance of this approach would meet my needs, I would be happy to use it. But it does not meet my needs. I ideally want the following:<br /><ol><li>Render over 10000 spheres at once</li><li>In under 30 milliseconds</li><li>With the most realistic rendering available</li></ol><p>It is clear that immediate mode rendering, which is horribly old fashioned, will not come close to meeting these criteria. Let's see how far off we are from the goal.<br /></p><p>In this experiment we vary the number of spheres shown, and the number of polygons used to define each sphere. The number of polygons is determined by the second and third arguments to <span style="font-weight: bold;">glutSolidSphere()</span>. I set both arguments to the same resolution value, either 10 (spheres look OK, but there are obvious artifacts when zoomed in) or 50 (almost as good as a tesselated sphere can look). And I also varied the number of spheres shown.</p><p>Here are the results (click to embiggen):<br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuRo3B3P8T80px_cw4XBTN979AFbYH12w4zAAKq6bla5JwFZK8_cfttKJMDkYCWu0niM3hWVVRlh-jv51oWpMI2n94xp3MZFDLu6oGXeM9H372r0HhqABGqdV_DOJBXrtbnO1v/s1600/ImmediateSpheres.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 233px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuRo3B3P8T80px_cw4XBTN979AFbYH12w4zAAKq6bla5JwFZK8_cfttKJMDkYCWu0niM3hWVVRlh-jv51oWpMI2n94xp3MZFDLu6oGXeM9H372r0HhqABGqdV_DOJBXrtbnO1v/s320/ImmediateSpheres.png" alt="" id="BLOGGER_PHOTO_ID_5705762449167024306" border="0" /></a><br />At each resolution the rendering time is proportional to the number of spheres drawn, as you might expect. At the lower resolution of 10 layers-per-dimension, the rendering time is about 70 microseconds per sphere. At the higher resolution of 50, the rendering time is about 315 microseconds per sphere. To meet my desired performance criteria, the performance would need to be about 0.3 microseconds (300 nanoseconds) per sphere. So we need about a 100-fold speed up from the higher quality rendering to satisfy my needs here.</p><p>But there are many approaches ahead. More next time.<br /></p>The data:<br /><br /><table border="1"><tbody><tr> <td>method</td> <td># spheres</td> <td>resolution</td> <td>frame rate</td></tr><tr> <td>immediate</td> <td>1</td> <td>10</td> <td>0.3 ms</td></tr><tr> <td>immediate</td> <td>1</td> <td>50</td> <td>0.4 ms</td></tr><tr> <td>immediate</td> <td>3</td> <td>10</td> <td>0.4 ms</td></tr><tr> <td>immediate</td> <td>3</td> <td>50</td> <td>1.1 ms</td></tr><tr> <td>immediate</td> <td>10</td> <td>10</td> <td>0.9 ms</td></tr><tr> <td>immediate</td> <td>10</td> <td>50</td> <td>3.4 ms</td></tr><tr> <td>immediate</td> <td>30</td> <td>10</td> <td>2.1 ms</td></tr><tr> <td>immediate</td> <td>30</td> <td>50</td> <td>8.9 ms</td></tr><tr> <td>immediate</td> <td>100</td> <td>10</td> <td>6.8 ms</td></tr><tr> <td>immediate</td> <td>100</td> <td>50</td> <td>29.0 ms</td></tr><tr> <td>immediate</td> <td>300</td> <td>10</td> <td>20.1 ms</td></tr><tr> <td>immediate</td> <td>300</td> <td>50</td> <td>92.3 ms</td></tr><tr> <td>immediate</td> <td>1000</td> <td>10</td> <td>69.8 ms</td></tr><tr> <td>immediate</td> <td>1000</td> <td>50</td> <td>315.1 ms</td></tr></tbody></table><br /><p></p>Here is the full source code for the program to run this test:<br /><br /><pre class="brush: python"><br />#!/usr/bin/python<br /><br /># File sphere_test.py<br /># Investigate performance of various OpenGL sphere rendering techniques<br /># Requires python modules PySide and PyOpenGL<br /><br />from PySide.QtGui import QMainWindow, QApplication<br />from PySide.QtOpenGL import QGLWidget<br />from PySide.QtCore import *<br />from PySide import QtCore<br />from OpenGL.GL import *<br />from OpenGL.GLUT import *<br />from OpenGL.GLU import *<br />from random import random<br />from math import sin, cos<br />import sys<br /><br /><br />class SphereTestApp(QApplication):<br /> "Simple application for testing OpenGL rendering"<br /> def __init__(self):<br /> QApplication.__init__(self, sys.argv)<br /> self.setApplicationName("SphereTest")<br /> self.main_window = QMainWindow()<br /> self.gl_widget = SphereTestGLWidget()<br /> self.main_window.setCentralWidget(self.gl_widget)<br /> self.main_window.resize(1024, 768)<br /> self.main_window.show()<br /> sys.exit(self.exec_()) # Start Qt main loop<br /><br /><br /># This "override" technique is a strictly optional code-sanity-checking <br /># mechanism that I like to use.<br />def override(interface_class):<br /> """<br /> Method to implement Java-like derived class method override annotation.<br /> Courtesy of mkorpela's answer at <br /> http://stackoverflow.com/questions/1167617/in-python-how-do-i-indicate-im-overriding-a-method<br /> """<br /> def override(method):<br /> assert(method.__name__ in dir(interface_class))<br /> return method<br /> return override<br /><br /><br />class SphereTestGLWidget(QGLWidget):<br /> "Rectangular canvas for rendering spheres"<br /> def __init__(self, parent = None):<br /> QGLWidget.__init__(self, parent)<br /> self.y_rot = 0.0<br /> # units are nanometers<br /> self.view_distance = 15.0<br /> self.stopwatch = QTime()<br /> self.frame_times = []<br /> self.param_generator = enumerate_sphere_resolution_and_number()<br /> (r, n) = self.param_generator.next()<br /> self.set_number_of_spheres(n)<br /> self.sphere_resolution = r<br /> <br /> def set_number_of_spheres(self, n):<br /> self.number_of_spheres = n<br /> self.sphere_positions = SpherePositions(self.number_of_spheres)<br /><br /> def update_projection_matrix(self):<br /> "update projection matrix, especially when aspect ratio changes"<br /> glPushAttrib(GL_TRANSFORM_BIT) # remember current GL_MATRIX_MODE<br /> glMatrixMode(GL_PROJECTION)<br /> glLoadIdentity()<br /> gluPerspective(40.0, # aperture angle in degrees<br /> self.width()/float(self.height()), # aspect<br /> self.view_distance/5.0, # near<br /> self.view_distance * 3.0) # far<br /> glPopAttrib() # restore GL_MATRIX_MODE<br /> <br /> @override(QGLWidget)<br /> def initializeGL(self):<br /> "runs once, after OpenGL context is created"<br /> glEnable(GL_DEPTH_TEST)<br /> glClearColor(1,1,1,0) # white background<br /> glShadeModel(GL_SMOOTH)<br /> glEnable(GL_COLOR_MATERIAL)<br /> glMaterialfv(GL_FRONT, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])<br /> glMaterialfv(GL_FRONT, GL_SHININESS, [50.0])<br /> glLightfv(GL_LIGHT0, GL_POSITION, [1.0, 1.0, 1.0, 0.0])<br /> glLightfv(GL_LIGHT0, GL_DIFFUSE, [1.0, 1.0, 1.0, 1.0])<br /> glLightfv(GL_LIGHT0, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])<br /> glLightModelfv(GL_LIGHT_MODEL_AMBIENT, [1.0, 1.0, 1.0, 0.0])<br /> glEnable(GL_LIGHTING)<br /> glEnable(GL_LIGHT0)<br /> self.update_projection_matrix()<br /> gluLookAt(0, 0, -self.view_distance, # camera<br /> 0, 0, 0, # focus<br /> 0, 1, 0) # up vector<br /> # Start animation<br /> timer = QTimer(self)<br /> timer.setInterval(10)<br /> timer.setSingleShot(False)<br /> timer.timeout.connect(self.rotate_view_a_bit)<br /> timer.start()<br /> self.stopwatch.restart()<br /> print "RENDER_MODE\tSPHERES\tRES\tFRAME_RATE"<br /> <br /> @override(QGLWidget)<br /> def resizeGL(self, w, h):<br /> "runs every time the window changes size"<br /> glViewport(0, 0, w, h)<br /> self.update_projection_matrix()<br /> <br /> @override(QGLWidget)<br /> def paintGL(self):<br /> "runs every time an image update is needed"<br /> self.stopwatch.restart()<br /> glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)<br /> glColor3f(255.0/300, 160.0/300, 46.0/300) # set object color<br /> self.paint_immediate_spheres(self.sphere_resolution)<br /> self.frame_times.append(self.stopwatch.elapsed())<br /> # Report on frame rate after enough frames have been rendered<br /> if 200 <= len(self.frame_times):<br /> n = len(self.frame_times)<br /> total = 0.0<br /> for t in self.frame_times:<br /> total += t<br /> mean = total / n<br /> print "immediate\t%d\t%d\t%.1f ms" % (self.number_of_spheres, self.sphere_resolution, mean)<br /> # print "mean frame time = %f milliseconds" % (mean)<br /> # Reset state<br /> self.frame_times = [] # Reset list of frame times<br /> try:<br /> (r, n) = self.param_generator.next()<br /> self.set_number_of_spheres(n)<br /> self.sphere_resolution = r<br /> except StopIteration:<br /> exit(0)<br /> # self.set_number_of_spheres(self.number_of_spheres * 2)<br /> self.stopwatch.restart()<br /> <br /> def paint_immediate_spheres(self, resolution):<br /> glMatrixMode(GL_MODELVIEW)<br /> for pos in self.sphere_positions:<br /> glPushMatrix()<br /> glTranslatef(pos.x, pos.y, pos.z)<br /> glColor3f(pos.color[0], pos.color[1], pos.color[2])<br /> glutSolidSphere(pos.radius, resolution, resolution)<br /> glPopMatrix()<br /> <br /> def paint_teapot(self):<br /> glPushAttrib(GL_POLYGON_BIT) # remember current GL_FRONT_FACE indictor<br /> glFrontFace(GL_CW) # teapot polygon vertex order is opposite to modern convention<br /> glutSolidTeapot(2.0) # thank you GLUT tool kit<br /> glPopAttrib() # restore GL_FRONT_FACE<br /> <br /> @QtCore.Slot(float)<br /> def rotate_view_a_bit(self):<br /> self.y_rot += 0.005<br /> x = self.view_distance * sin(self.y_rot)<br /> z = -self.view_distance * cos(self.y_rot)<br /> glMatrixMode(GL_MODELVIEW)<br /> glLoadIdentity()<br /> gluLookAt(x, 0, z, # camera<br /> 0, 0, 0, # focus<br /> 0, 1, 0) # up vector<br /> self.update()<br /><br /><br />class SpherePosition():<br /> "Simple python container for sphere information"<br /> pass<br /><br /><br />class SpherePositions(list):<br /> "Collection of SpherePosition objects"<br /> def __init__(self, sphere_num):<br /> for s in range(sphere_num):<br /> pos = SpherePosition()<br /> # units are nanometers<br /> pos.x = random() * 10.0 - 5.0<br /> pos.y = random() * 10.0 - 5.0<br /> pos.z = random() * 10.0 - 5.0<br /> pos.color = [0.2, 0.3, 1.0]<br /> pos.radius = 0.16<br /> self.append(pos)<br /> assert(len(self) == sphere_num)<br /><br /><br />def enumerate_sphere_resolution_and_number():<br /> for n in [1, 3, 10, 30, 100, 300, 1000]:<br /> for r in [10, 50]:<br /> yield [r, n]<br /><br /><br /># Automatically execute if run as program, but not if loaded as a module<br />if __name__ == "__main__":<br /> SphereTestApp()<br /></pre>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-65927407973848778502012-02-05T06:01:00.000-08:002012-02-05T11:14:39.886-08:00Proof of concept OpenGL program in python and Qt/PySideHere is the output of my initial test program:<br /><br /><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvBwFTrsqs0RJCrcJOS0CwJ_wJqhvuCTIchxme_h_8BER6oRKxFaKd6lVrIxQ1XZ574bzvzA5ZZuLoeG_QkuppRSuY6MM8fkUCk4VZAf0QNGZMQQSt1qk2stRgYilqp94yzb-U/s1600/teapot1.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 320px; height: 248px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvBwFTrsqs0RJCrcJOS0CwJ_wJqhvuCTIchxme_h_8BER6oRKxFaKd6lVrIxQ1XZ574bzvzA5ZZuLoeG_QkuppRSuY6MM8fkUCk4VZAf0QNGZMQQSt1qk2stRgYilqp94yzb-U/s320/teapot1.jpg" alt="" id="BLOGGER_PHOTO_ID_5705652842443997090" border="0" /></a><br /><br /><br />I am planning to test the performance of various ways of rendering lots of spheres using OpenGL. I will use python and Qt to run the tests. As a first step, I have created a very light hello program that renders the classic Utah teapot.<br /><br /><pre class="brush: python"><br />#!/usr/bin/python<br /><br /># File teapot_test.py<br /># "hello world" type rendering of the classic Utah teapot<br /># Requires python modules PySide and PyOpenGL<br /><br />from PySide.QtGui import QMainWindow, QApplication<br />from PySide.QtOpenGL import QGLWidget<br />from OpenGL.GL import *<br />from OpenGL.GLUT import *<br />from OpenGL.GLU import *<br />import sys<br /><br /><br />def override(interface_class):<br /> """<br /> Method to implement Java-like derived class method override annotation.<br /> Courtesy of mkorpela's answer at<br /> http://stackoverflow.com/questions/1167617/in-python-how-do-i-indicate-im-overriding-a-method<br /> """<br /> def override(method):<br /> assert(method.__name__ in dir(interface_class))<br /> return method<br /> return override<br /><br /><br />class SphereTestGLWidget(QGLWidget):<br /> "GUI rectangle that displays a teapot"<br /> @override(QGLWidget)<br /> def initializeGL(self):<br /> "runs once, after OpenGL context is created"<br /> glEnable(GL_DEPTH_TEST)<br /> glClearColor(1,1,1,0) # white background<br /> glShadeModel(GL_SMOOTH)<br /> glEnable(GL_COLOR_MATERIAL)<br /> glMaterialfv(GL_FRONT, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])<br /> glMaterialfv(GL_FRONT, GL_SHININESS, [50.0])<br /> glLightfv(GL_LIGHT0, GL_POSITION, [1.0, 1.0, 1.0, 0.0])<br /> glLightfv(GL_LIGHT0, GL_DIFFUSE, [1.0, 1.0, 1.0, 1.0])<br /> glLightfv(GL_LIGHT0, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])<br /> glLightModelfv(GL_LIGHT_MODEL_AMBIENT, [1.0, 1.0, 1.0, 0.0])<br /> glEnable(GL_LIGHTING)<br /> glEnable(GL_LIGHT0)<br /> self.orientCamera()<br /> gluLookAt(0, 0, -10, # camera<br /> 0, 0, 0, # focus<br /> 0, 1, 0) # up vector<br /> <br /> @override(QGLWidget)<br /> def paintGL(self):<br /> "runs every time an image update is needed"<br /> glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)<br /> self.paintTeapot()<br /> <br /> @override(QGLWidget)<br /> def resizeGL(self, w, h):<br /> "runs every time the window changes size"<br /> glViewport(0, 0, w, h)<br /> self.orientCamera()<br /> <br /> def orientCamera(self):<br /> "update projection matrix, especially when aspect ratio changes"<br /> glPushAttrib(GL_TRANSFORM_BIT) # remember current GL_MATRIX_MODE<br /> glMatrixMode(GL_PROJECTION)<br /> glLoadIdentity()<br /> gluPerspective (60.0, self.width()/float(self.height()), 1.0, 10.0)<br /> glPopAttrib() # restore GL_MATRIX_MODE<br /> <br /> def paintTeapot(self):<br /> glPushAttrib(GL_POLYGON_BIT) # remember current GL_FRONT_FACE indictor<br /> glFrontFace(GL_CW) # teapot polygon vertex order is opposite to modern convention<br /> glColor3f(0.2,0.2,0.5) # paint it blue<br /> glutSolidTeapot(3.0) # thank you GLUT tool kit<br /> glPopAttrib() # restore GL_FRONT_FACE<br /><br /><br />class SphereTestApp(QApplication):<br /> "Simple application for testing OpenGL rendering"<br /> def __init__(self):<br /> QApplication.__init__(self, sys.argv)<br /> self.setApplicationName("SphereTest")<br /> self.mainWindow = QMainWindow()<br /> self.gl_widget = SphereTestGLWidget()<br /> self.mainWindow.setCentralWidget(self.gl_widget)<br /> self.mainWindow.resize(1024, 768)<br /> self.mainWindow.show()<br /> sys.exit(self.exec_()) # Start Qt main loop<br /><br /><br />if __name__ == "__main__":<br /> SphereTestApp()<br /><br /></pre>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com2tag:blogger.com,1999:blog-20076457.post-23261244045548893242011-03-21T12:43:00.001-07:002011-03-21T13:10:13.352-07:00Autumn 2010 - When Google jumped the sharkI cannot express the depths of my dismay at the "Search instead for..." misfeature that Google has added to search. From the dates of the earliest cries of communal pain on the web, this feature appeared around September 2010.<br /><br />I am not the only angry customer. Google forums include multiple vigorous discussions of this topic.<br /><br />TimInBC <a href="http://www.google.com/support/forum/p/Web+Search/thread?tid=71f1c6ef666229d7&hl=en&fid=71f1c6ef666229d7000494b9bd7f7480&hltp=2">writes</a>:<br /><blockquote>I agree, this is annoying, and not because I am promoting a site. I want to FIND stuff, and when I tell it to search for "Orangerie" I don't want it auto-corrected to "Orangutan" or anything else. I am a very good speller and I don't need Google to help me. Please give us a preference to say "Search for exactly what I tell you to search for".<br /></blockquote><br />Alensha <a href="http://www.google.pl/support/forum/p/Web+Search/thread?tid=7f57d27b17e4c1fd&hl=en&fid=7f57d27b17e4c1fd00049ad77f24ed2b&hltp=2">comments</a>:<br /><blockquote>It would be really good if this feature was optional instead of being forced on us. I encounter it every day, and it messes up my search results several times daily. Last time I was looking for the meaning of the name of the Egyptian god Atum and of the first 50 results 46 were about the name Autumn, explaining that it means, well, autumn. The "did you mean" function was mostly harmless, but _changing_ the word I typed to something else or filling the first ten pages of the search results with completely irrelevant results is annoying.</blockquote><br />This problem seriously interrupts my work flow too. During my work day I run numerous web searches for information. Now that Google is broken, I often find myself concluding "Darn, there must not be any relevant information on this topic on the web." Then I notice that accursed, mocking, evil, cruel cancer of the web, "Search instead for <exactly>". The number of near misses convinces me that I must have actually been completely misled many times by Google's cruel practical joke.<br /><br />Bing is the same way. Here's the deal: Bing, if you create a configuration option to never second guess my search terms, I promise to drop Google and use Bing for my searches. I recommend others make the same promise to Bing.Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-82228946158448962192009-11-22T08:40:00.000-08:002009-11-22T20:17:32.493-08:00YouTube supports 3D stereoscopic videoGoogle's video service <a href="http://www.youtube.com/">YouTube</a> now supports stereoscopic video. This is great news. I predict that soon it will be possible to stream stereoscopic YouTube videos to stereoscopic monitors.<br /><br />The only technical information so far is one very long <a href="http://www.google.com/support/forum/p/youtube/thread?tid=56b6f6f15dabf994">help thread</a>. The Google engineer behind 3d YouTube, "YouTube Pete", participates in that thread.<br /><br />I would like to take a moment to thank YouTube Pete for his beautiful work on the 3D YouTube project. Kudos to Pete. It is much appreciated.<br /><br /><h3>My hummingbird video</h3><br /><br />I made a <a href="http://www.youtube.com/watch?v=2z-cg0qR-0A">hummingbird video</a> to test out the 3d features myself. The embedded video below does not show the 3D interface. You must go to the <a href="http://www.youtube.com/watch?v=2z-cg0qR-0A">YouTube page itself</a> to see the full range of possibilities. Grab a pair of red/blue 3D glasses if you have one.<br /><br /><object height="340" width="560"><param name="movie" value="http://www.youtube.com/v/2z-cg0qR-0A&hl=en_US&fs=1&"><param name="allowFullScreen" value="true"><param name="allowscriptaccess" value="always"><embed src="http://www.youtube.com/v/2z-cg0qR-0A&hl=en_US&fs=1&" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" height="340" width="560"></embed></object><br /><br />Remember to check out the <a href="http://www.youtube.com/watch?v=2z-cg0qR-0A">original movie</a> to see all of the 3D viewing options.<br /><br />This hummingbird movie could be improved in several ways<br /><ol><li>The left side is out of focus. I meant to set the focus for both cameras to 15 cm, but it looks like the focal length of the left eye was set too short.</li><li>The sound doesn't seem to work. I plugged in a microphone, and selected the one audio option that was available in AmCap, but I don't hear any sound in the video. This needs to be investigated.</li><li>I should register <a href="http://www.3dtv.at/Products/Multiplexer/">Stereoscopic Multiplexer</a>, to avoid those watermarks on the video. It will cost about $90. Ouch.</li><li>It would be good to get more light on the bird. Unfortunately, the sun won't shine on my patio until summer.</li><li>The format is Left-Right (parallel), but the emerging YouTube standard is Right-Left (cross-eye), so I should use the Right-Left convention in the future. Plus I have an easier time free-viewing cross-eye, so it will be more convenient for me when viewing embedded videos like the one above. I used the YouTube tag "<span><span>yt3d:swap=true</span></span>" to correct for this inversion.<br /></li></ol><h3>Other YouTube 3D videos</h3><br /><br />The following are examples of other stereoscopic videos on YouTube, created by others:<br /><br />This so-called biodiversity documentary contains professional-quality footage of domesticated ducks, geese, and honeybees in India. The narration is done with a top-quality computer generated voice. The voice is only slightly creepy.<br /><br /><object height="340" width="560"><param name="movie" value="http://www.youtube.com/v/xn-0AwdCE98&hl=en_US&fs=1&"><param name="allowFullScreen" value="true"><param name="allowscriptaccess" value="always"><embed src="http://www.youtube.com/v/xn-0AwdCE98&hl=en_US&fs=1&" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" height="340" width="560"></embed></object><br /><br />This next one is taken with a helmet camera. It is interesting and entertaining. It includes some cityscape images. Unfortunately, a cityscape shows little depth when using a normal human interpupillary distance of 60 mm or so. Hyperstereo might have been nice here.<br /><br /><object height="340" width="560"><param name="movie" value="http://www.youtube.com/v/moINIZuG38E&hl=en_US&fs=1&"><param name="allowFullScreen" value="true"><param name="allowscriptaccess" value="always"><embed src="http://www.youtube.com/v/moINIZuG38E&hl=en_US&fs=1&" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" height="340" width="560"></embed></object><br /><br />There are many many other stereoscopic videos on YouTube. <a href="http://www.youtube.com/results?search_query=yt3d&search_type=&aq=f">Search for "yt3d" on YouTube</a>.<br /><br /><h3>How I made the hummingbird video</h3><br /><br />I created my hummingbird video using two USB pen cameras. So I could get the two cameras as close as possible. This setup is suited for small, close subjects, such as hummingbirds. Because the two cameras are only 14 mm apart, as opposed to the 60 mm separation of human eyes, my setup yields a view as seen by another hummingbird, rather than what would be seen by a person. This is called hypostereo.<br /><br />Two USB pen cameras and a portable netbook style computer are the basis of my stereoscopic video system. I created a custom bracket for the cameras so I can mount them on a tripod. The bracket is carefully shaped to compensate for the idiosyncrasies of these particular cameras. These very cheap cameras do not point in exactly the same direction.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNjjAV8MG43qkjXsY6wcBvKtBwKsrDtN248vNBi9WBm7_hV3hauXb7XW9pFufNixHrohjQtfkYChsQ4CceFgsh0dTc_SxbJRHdhdM0cqxp0XmVsgRH6TKJs_CP0B4Huo_MGyc7/s1600/pencapsetup.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 320px; height: 240px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiNjjAV8MG43qkjXsY6wcBvKtBwKsrDtN248vNBi9WBm7_hV3hauXb7XW9pFufNixHrohjQtfkYChsQ4CceFgsh0dTc_SxbJRHdhdM0cqxp0XmVsgRH6TKJs_CP0B4Huo_MGyc7/s320/pencapsetup.jpg" alt="" id="BLOGGER_PHOTO_ID_5407006220020496642" border="0" /></a><br /><br />The narrow 14 mm distance between the camera lenses is crucial to producing a subtle 3D effect with small close subjects such as the hummingbird. I chose these pen cameras because this form factor permits the smallest camera separation I could find.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixqW8-gTbtl1pSKGz9Ka0Hzv_SzI_DDlLMbW5pT5AFiCA73pL4fUYimPNbVdNVkQHa6mqnhWQs3NBI_xOXy97U9U6Vh0FVYFtYhOPHtL7bE9JoVOqbZjw4KLs_YgdS0BKu_v_U/s1600/pencamsep.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 320px; height: 240px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixqW8-gTbtl1pSKGz9Ka0Hzv_SzI_DDlLMbW5pT5AFiCA73pL4fUYimPNbVdNVkQHa6mqnhWQs3NBI_xOXy97U9U6Vh0FVYFtYhOPHtL7bE9JoVOqbZjw4KLs_YgdS0BKu_v_U/s320/pencamsep.jpg" alt="" id="BLOGGER_PHOTO_ID_5407006223151679218" border="0" /></a><br /><br />Here is a shot of the whole setup prepared to take hummingbird videos.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh8eQMgCdr5avipXS8Mjz3dAV4QkspnMhtsTpa1fBSCAHRfbn86eWgzIRREkC8UI9GqAYBZUi55vDQNZpvVSOAvqU62-FNp9nRdyZMwZ8vj6g78QZHsY9WAWUI-ZanWBM6jdgPq/s1600/wholesetup.jpg"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 240px; height: 320px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh8eQMgCdr5avipXS8Mjz3dAV4QkspnMhtsTpa1fBSCAHRfbn86eWgzIRREkC8UI9GqAYBZUi55vDQNZpvVSOAvqU62-FNp9nRdyZMwZ8vj6g78QZHsY9WAWUI-ZanWBM6jdgPq/s320/wholesetup.jpg" alt="" id="BLOGGER_PHOTO_ID_5407147820116330722" border="0" /></a>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-17569384634330822302009-08-22T07:16:00.000-07:002009-09-06T07:05:40.175-07:00Tk 8.5 is better than wxWidgets on WindowsUPDATE: It appears this issue might be <a href="http://trac.wxwidgets.org/ticket/11008">fixed</a> in a future release of wxwidgets.<br /><br />I frequently write computer programs with graphical user interfaces ("GUI"s). I insist that the interfaces look good on Windows, Mac, and Linux computers. By "good", I mean that the widgets (the buttons, sliders, and what-not), look exactly like those found on most other applications developed specifically for that particular platform. For example, buttons and progress bars on Mac must have that clear blue "Aqua" look.<br /><br />There are several programming tool kits which help to create native-looking user interfaces on multiple platforms. The three platforms I pay particular attention to are Windows, Mac, and Linux. Cross-platform GUI tool kits include <a href="http://www.wxwidgets.org/">wxWidgets</a>, <a href="http://www.tcl.tk/man/tcl8.5/TkCmd/contents.htm">Tk</a>, and <a href="http://http//java.sun.com/javase/6/docs/technotes/guides/swing/">Java Swing</a>. This post documents the failure of wxWidgets and Java Swing to respect Windows font sizes.<br /><br />Look at the following picture to see the failure of wx and Java to respect the Windows font sizes. From left to right, the test programs are in Visual Basic, python/Tk, python/wx, and Java Swing.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-ISWhylh8gpjjkQWYXf7Kj_OrB-Jal94-6H87L4LWEk9BTfHrFBXQANUN18tG-2p0LrjpZsY4OT-gQgLS_MqvNrKApBkKA2_8NEeWl358aiC9As8cej9DHBe0KfmgEryrsE5-/s1600-h/gui_font_sizes.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer; width: 400px; height: 93px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-ISWhylh8gpjjkQWYXf7Kj_OrB-Jal94-6H87L4LWEk9BTfHrFBXQANUN18tG-2p0LrjpZsY4OT-gQgLS_MqvNrKApBkKA2_8NEeWl358aiC9As8cej9DHBe0KfmgEryrsE5-/s400/gui_font_sizes.png" alt="" id="BLOGGER_PHOTO_ID_5372824066644727762" border="0" /></a><br /><br />wxWidgets looks nice in some cases, but it has some ways to go to support native look and feel on Windows. I am working on several Windows XP systems, on which I routinely select "Large Fonts" in my desktop preferences. wxWidgets does not respect those preferences.<br /><br />To see the difference, first set extra large fonts on your desktop:<br /><br />Far click desktop -> Properties -> Appearance -> Font Size -> Extra Large Fonts<br /><br />Next, write an application using wxWidgets and test whether it respects your font choice. I didn't think so.<br /><br />If it's any consolation, Java doesn't respect the Windows font size either.<br /><br />If you want to use a cross-platform widget tool kit, and your definition of "cross-platform" includes Windows, my recommendation is to use Tk 8.5.<br /><br />The table below summarizes the results for the four test programs I wrote:<br /><br /><table border="1"><br /><caption>GUI tool kits on Windows</caption><br /><tbody><tr><br /><th>Tool Kit</th><th>Native look-and-feel?</th><th>Respects font size?</th><br /></tr><br /><tr><br /><td>Visual basic</td><td>No(!)</td><td>Yes</td><br /></tr><br /><tr><br /><td>Tk 8.5</td><td>Yes</td><td>Yes</td><br /></tr><br /><tr><br /><td>wx 2.8.10</td><td>Yes</td><td>No</td><br /></tr><br /><tr><br /><td>Java 1.6.0</td><td>Yes</td><td>No</td><br /></tr><br /></tbody></table><br /><br />Below are the test programs I wrote to create the windows shown at the beginning of this post.<br /><br /><ul><br /><li>Visual Basic<br /><pre><br />' "Hello, World!" program in Visual Basic.<br />Module Hello<br />Sub Main()<br />MsgBox("Hello, World! (VB)") ' Display message on computer screen.<br />End Sub<br />End Module<br /></pre><br /><br /></li><li>Tk 8.5 (tkinter in python 3.1)<br /><pre><br /># Note - requires python 3.1 for ttk 8.5 support<br />import tkinter as tk<br />import tkinter.ttk as ttk<br /><br />root = tk.Tk()<br />padding = 10<br />panel = ttk.Frame(root, padding=padding).pack()<br />label = ttk.Label(panel, text="Hello, World! (Tk)")<br />label.pack(padx=padding, pady=padding)<br />button = ttk.Button(panel, text="Hello", default="active")<br />button.pack(padx=padding, pady=padding)<br />root.mainloop()<br /></pre><br /><br /></li><li>wx 2.8.10 (in python 2.6 with wxpython)<br /><pre><br />import wx<br /><br />padding = 10<br />app = wx.App(0)<br />frame = wx.Frame(None, -1, "Hello")<br />panel = wx.Panel(frame)<br />sizer = wx.BoxSizer(wx.VERTICAL)<br />panel.SetSizer(sizer)<br />text = wx.StaticText(panel, -1, "Hello, World! (wx)")<br />sizer.Add(text, 0, wx.ALL, padding)<br />button = wx.Button(panel, -1, "Hello")<br />sizer.Add(button, 0, wx.ALL, padding)<br />frame.Centre()<br />frame.Show(True)<br />app.MainLoop()<br /></pre><br /><br /></li><li>Java swing 1.6.0<br /><pre><br />import javax.swing.*;<br />import java.awt.Dimension;<br /><br />public class HelloWorldFrame extends JFrame<br />{<br />public static void main(String args[])<br />{<br /> new HelloWorldFrame();<br />}<br />HelloWorldFrame()<br />{<br /> try {<br /> UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());<br /> } catch(Exception e) {}<br /> JPanel panel = new JPanel();<br /> add(panel);<br /> panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));<br /> panel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));<br /> JLabel label = new JLabel("Hello, World! (java)");<br /> panel.add(label);<br /> panel.add(Box.createRigidArea(new Dimension(0, 10)));<br /> JButton button = new JButton("Hello");<br /> panel.add(button);<br /> pack();<br /> setVisible(true);<br />}<br />}<br /></pre><br /><br /></li></ul>The wx bug tracker has had a couple of <a href="http://trac.wxwidgets.org/ticket/1981">bug</a> <a href="http://trac.wxwidgets.org/ticket/11008">reports</a> for this problem, one open for five years. Somehow I doubt they are itching to fix this problem.<br /><br />The Tk source code that sets the windows correctly appears to be near line 418 of file win/tkWinFont.c in the Tk source code:<br /><br /><pre><br />if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS,<br /> sizeof(ncMetrics), &ncMetrics, 0)) {<br /> CreateNamedSystemLogFont(interp, tkwin, "TkDefaultFont",<br /> &ncMetrics.lfMessageFont);<br /> CreateNamedSystemLogFont(interp, tkwin, "TkHeadingFont",<br /> &ncMetrics.lfMessageFont);<br /> CreateNamedSystemLogFont(interp, tkwin, "TkTextFont",<br /> &ncMetrics.lfMessageFont);<br /> CreateNamedSystemLogFont(interp, tkwin, "TkMenuFont",<br /> &ncMetrics.lfMenuFont);<br /> CreateNamedSystemLogFont(interp, tkwin, "TkTooltipFont",<br /> &ncMetrics.lfStatusFont);<br /> CreateNamedSystemLogFont(interp, tkwin, "TkCaptionFont",<br /> &ncMetrics.lfCaptionFont);<br /> CreateNamedSystemLogFont(interp, tkwin, "TkSmallCaptionFont",<br /> &ncMetrics.lfSmCaptionFont);<br />}<br /></pre><br /><br />The wx source code has similar code in a few locations. But it appears that this technique may be only used for menu fonts and message dialog fonts.<br /><br />The main problem might be that the method wxGetCCDefaultFont() in the wx source code uses SPI_GETINCONTITLELOGFONT instead of SPI_GETNONCLIENTMETRICS.<br /><br />Microsoft has <a href="http://msdn.microsoft.com/en-us/library/ms724506%28VS.85%29.aspx">documentation</a> for the NONCLIENTMETRICS data structure.<br /><br />Even if the wx authors fix this today, I fear it will be a long time before the change trickles down into a wxPython release.Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com2tag:blogger.com,1999:blog-20076457.post-8388292205632842352009-08-15T14:57:00.000-07:002009-08-15T16:19:59.137-07:00Write your own stereoscopic 3D program using nVidia's "consumer" stereo driver<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTfaW6W9zQVRvBxDR8OyNXC3GVhB9vqmuENHyXrfpjlRxGAAUCWZ-2X3OMAU8w8tBMzT_Us1uGDyEGeM_VzjWv4UTl_6AcNh7bxMC_D-FX0Zh_tDZMTi7C0u8fbRPJ46_-FqJP/s1600-h/cube.png"><img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 208px; height: 234px;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTfaW6W9zQVRvBxDR8OyNXC3GVhB9vqmuENHyXrfpjlRxGAAUCWZ-2X3OMAU8w8tBMzT_Us1uGDyEGeM_VzjWv4UTl_6AcNh7bxMC_D-FX0Zh_tDZMTi7C0u8fbRPJ46_-FqJP/s320/cube.png" alt="" id="BLOGGER_PHOTO_ID_5370326205205714210" border="0" /></a><br />I have always been a fan of <a href="http://www.nvidia.com/">nVidia</a> graphics boards because of their support for 3D stereoscopic games. But the "consumer level" (non-Quadro) stereoscopic drivers only seem to work with games. I have always wondered how to create my own applications that can use the stereoscopic drivers on less-expensive gaming video boards. Now I have found a way.<br /><br />The "consumer" stereoscopic driver from nVidia only works with "full screen" games. When I started experimenting with OpenGL, I assumed that using the call "<a href="http://pyopengl.sourceforge.net/documentation/manual/glutFullScreen.3GLUT.html">glutFullScreen()</a>" might be enough to get the stereoscopic drivers to kick in. But it is not.<br /><br />The trick is to use the <a href="http://pyopengl.sourceforge.net/documentation/manual/glutEnterGameMode.3GLUT.html">glutEnterGameMode()</a> call. I did a lot of searching on the internet, and nowhere is it mentioned that you must call glutEnterGameMode() to get the nVidia "consumer level" stereoscopic drivers to work. That is why I am sharing this blog post.<br /><br />My working system is on Windows XP. I am uncertain if this approach will work with Windows Vista/7. I am a bit concerned because nVidia seems to be selling a <a href="http://www.nvidia.com/object/3D_Vision_Overview.html">hardware stereoscopic product</a> these days. I am worried that my custom stereoscopic theater, which uses a pair of polarized video projectors, won't work if I upgrade my Windows version.<br /><br />Here is how you can do it too, on Windows XP:<br /><ol><li>Ensure you have a <a href="http://www.3d.wep.dk/driverguide.html">supported nVidia graphics</a> board in your computer. See the stereoscopic driver <a href="http://download.nvidia.com/Windows/77.77/77.77_3D_Stereo_User_Guide.pdf">users' guide</a> for more details.<br /></li><li>Get the stereoscopic driver from nVidia. The most recent version (91.31) released for Windows XP is from 2006. That is <a href="http://www.nvidia.com/object/3dstereo_archive.html">the one I am using</a>. Consult <a href="http://www.3d.wep.dk/driverguide.html">this driver guide</a> for more details.<br /></li><li>Install <a href="http://www.python.org/download/">Python 2.6</a> and <a href="http://sourceforge.net/projects/pyopengl/files/">PyOpenGL version 3.0.0</a>, so you can conveniently create OpenGL programs in python.</li><li>Familiarize yourself with OpenGL programming. I got started by following the examples of the "red book", the <a href="http://www.amazon.com/gp/product/0321481003?ie=UTF8&tag=rotatingpcom-20&linkCode=as2&camp=1789&creative=9325&creativeASIN=0321481003">OpenGL Programming Guide</a>.</li><li>Study my example program, below, to learn how to call glutGameModeString() and glutEnterGameMode().<br /></li></ol>Below is the text of a complete working python program that works with the nVidia "consumer level" stereoscopic driver on my Windows XP computer. (The stereoscopic presentation only appears in the full screen gaming mode):<br /><br />Modify the display() method and the animate() method to show whatever you want!<br /><pre><br />#!/cygdrive/c/Python26/python<br /><br />from OpenGL.GL import *<br />from OpenGL.GLU import *<br />from OpenGL.GLUT import *<br />import sys<br /><br /><br />def do_nothing(*args):<br /> """<br /> Empty method for glutDisplayFunc during risky transition to game mode.<br /> """<br /> pass<br /> <br /><br />class HelloOpenGL(object):<br /> """<br /> Creates a rotating wire frame cube using OpenGL.<br /><br /> Pressing the "f" key toggles full screen game mode.<br /> This full screen mode works with nVidia stereoscopic<br /> driver for Windows XP.<br /> """<br /> def __init__(self):<br /> self.animation_interval = 100 # milliseconds<br /> self.rotation_angle = 0.0 # degrees, starting point<br /> glutInit("Cube.py")<br /> glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH) <br /> glEnable(GL_DEPTH_TEST)<br /> glutInitWindowSize(200, 200)<br /> # Remember window id for when we return from game mode.<br /> self.window_id = glutCreateWindow('Wire Cube')<br /> self.initialize_gl_context() <br /> # glutTimerFunc remains when GL context is replaced,<br /> # so it does not go into self.initialize_gl_context()<br /> glutTimerFunc(self.animation_interval, self.animate, 1)<br /> glutMainLoop() # never returns<br /><br /> def clear_gl_callbacks(self):<br /> """<br /> Set inoccuous callbacks during times when no valid context may be available.<br /> """<br /> glutDisplayFunc(do_nothing)<br /> glutMotionFunc(None)<br /> glutKeyboardFunc(None)<br /><br /> def initialize_gl_context(self):<br /> """<br /> When switching between full-screen and windowed modes,<br /> initialize_gl_context() reinitializes state.<br /> """<br /> glClearColor(0.5,0.5,0.5,0.0)<br /> glutDisplayFunc(self.display)<br /> # glutPassiveMotionFunc(self.mouse_motion)<br /> glutMotionFunc(self.mouse_motion)<br /> glutKeyboardFunc(self.keypress)<br /> # establish the projection matrix (perspective)<br /> glMatrixMode(GL_PROJECTION)<br /> glLoadIdentity()<br /> x,y,width,height = glGetDoublev(GL_VIEWPORT)<br /> gluPerspective(<br /> 45, # field of view in degrees<br /> width/float(height or 1), # aspect ratio<br /> .25, # near clipping plane<br /> 200, # far clipping plane<br /> )<br /> <br /> def start_game_mode(self):<br /> if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):<br /> return # already in game mode<br /> glutGameModeString("800x600:16@60")<br /> if glutGameModeGet(GLUT_GAME_MODE_POSSIBLE):<br /> self.clear_gl_callbacks()<br /> glutEnterGameMode()<br /> self.initialize_gl_context()<br /><br /> def start_windowed_mode(self):<br /> if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):<br /> self.clear_gl_callbacks()<br /> glutLeaveGameMode()<br /> # Remember the window we created at start up?<br /> glutSetWindow(self.window_id)<br /> self.initialize_gl_context() <br /> <br /> def display(self):<br /> """<br /> "display()" method is called every time OpenGL updates the display.<br /> """<br /> # Erase the old image<br /> glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)<br /> # Modelview must be set before geometry is sent<br /> # or else crash when entering stereoscopic mode.<br /> glMatrixMode(GL_MODELVIEW)<br /> glLoadIdentity()<br /> gluLookAt(<br /> 0,-0.5,5, # eyepoint<br /> 0,0,0, # center-of-view<br /> 0,1,0, # up-vector<br /> )<br /> # Rotate about the origin as animation progresses<br /> glRotate(self.rotation_angle, 0, 1, 0)<br /> glPushMatrix()<br /> try:<br /> # Draw the cube<br /> glutWireCube(2.0)<br /> finally:<br /> glPopMatrix()<br /> glutSwapBuffers()<br /><br /> def mouse_motion(self, x, y):<br /> pass<br /><br /> def keypress(self, key, x, y):<br /> if key == '\033':<br /> # Escape key leaves full screen mode<br /> if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):<br /> self.start_windowed_mode()<br /> elif key == "f":<br /> # "f" key toggle full screen and windowed mode.<br /> if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):<br /> self.start_windowed_mode()<br /> else:<br /> self.start_game_mode()<br /><br /> def animate(self, value):<br /> """<br /> Periodically change the rotation angle for the cube animation.<br /> <br /> This animate method() is called as a glutTimerFunc().<br /> """<br /> self.rotation_angle += 1.0<br /> while self.rotation_angle > 360.0:<br /> self.rotation_angle -= 360.0<br /> glutPostRedisplay()<br /> # Be sure to come back for more<br /> glutTimerFunc(self.animation_interval, self.animate, value+1)<br /><br /><br /># Run the HelloOpenGL application when this script is run directly.<br />if (__name__ == '__main__'):<br /> HelloOpenGL()<br /></pre>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com2tag:blogger.com,1999:blog-20076457.post-60403099737036339122007-07-13T15:18:00.000-07:002007-07-13T15:24:31.990-07:00Phantom cell phone vibrationsI have had a cell phone (Treo 680) for about 8 months. I keep it in my front right pants pocket. I always have it set to "vibrate". Lately, my leg has begun to vibrate right where the phone is, causing me to think that the phone is ringing.<br /><br />It's really creepy.<br /><br />One time I moved the cell phone away from my leg, but I could still feel the vibration in my leg. I could feel my leg actually vibrating with my hand. I couldn't get it to stop. My leg kept on "ringing" occasionally for quite some time. I have started keeping the phone in a different pocket.<br /><br />Judging by the number of "I thought it was just me..." responses in <a href="http://ehealthforum.com/health/topic14052.html">a forum I found online</a>, this is a surprisingly common phenomenon. According to <a href="http://jscms.jrn.columbia.edu/cns/2005-05-03/orso-phantomvibes/">this article</a>, it happens when you are expecting a call. I don't get very many calls, so it does not take much!<br /><br />Yikes!<br /><br /><a href="http://jscms.jrn.columbia.edu/cns/2005-05-03/orso-phantomvibes/">Who's calling? Is it your leg or your cell phone? — JSCMS</a><br /><a href="http://www.usatoday.com/news/health/2007-06-12-cellphones_N.htm?csp=34">Good vibrations? Bad? None at all? - USATODAY.com</a><br /><a href="http://digg.com/health/Have_You_Noticed_the_Cell_Phone_Phantom_Vibration_Syndrome">Digg - Have You Noticed the Cell Phone "Phantom Vibration Syndrome"?</a>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-30310491773717221462007-07-09T11:28:00.000-07:002007-07-09T12:29:16.104-07:00Using rxvt in cygwinI don't like the default cygwin bash window.<br /><br />To get a nice terminal in cygwin, I have been typing "rxvt" from the cygwin bash shell for years. After several previous abortive attempts, I have finally succeeded in creating a clickable icon that directly launches a nice rxvt (xterm-like) terminal window under Windows.<br /><br />The solution I found is described at <a href="http://freemode.net/archives/000121.html">http://freemode.net/archives/000121.html</a>.<br /><br /><br />I made a few modifications, because I like a larger font, and the batch file did not work for me without modification.<br /><br />My ~/.Xdefaults file looks like this now:<br /><code><br />! ~/.Xdefaults - X default resource settings<br />Rxvt*geometry: 120x40<br />Rxvt*background: #000020<br />Rxvt*foreground: #ffffbf<br />!Rxvt*borderColor: Blue<br />!Rxvt*scrollColor: Blue<br />!Rxvt*troughColor: Gray<br />Rxvt*scrollBar: True<br />Rxvt*scrollBar_right: True<br />! Rxvt*font: Lucida Console-12<br />Rxvt*font: fixedsys<br />Rxvt*SaveLines: 10000<br />Rxvt*loginShell: True<br />! VIM-like colors<br />Rxvt*color0: #000000<br />Rxvt*color1: #FFFFFF<br />Rxvt*color2: #00A800<br />Rxvt*color3: #FFFF00<br />Rxvt*color4: #0000A8<br />Rxvt*color5: #A800A8<br />Rxvt*color6: #00A8A8<br />Rxvt*color7: #D8D8D8<br />Rxvt*color8: #000000<br />Rxvt*color9: #FFFFFF<br />Rxvt*color10: #00A800<br />Rxvt*color11: #FFFF00<br />Rxvt*color12: #0000A8<br />Rxvt*color13: #A800A8<br />Rxvt*color14: #00A8A8<br />Rxvt*color15: #D8D8D8<br />! eof<br /></code><br /><br />My replacement for the default "cygwin.bat", which I call "cygwin-rxvt.bat" is as follows:<br /><code><br />@echo off<br />C:<br />chdir C:\cygwin\bin<br />set EDITOR=vi<br />set VISUAL=vi<br />set CYGWIN=codepage:oem tty binmode title<br />set HOME=\cygwin\home\spud<br />rxvt -e /bin/tcsh -l<br /></code><br /><br />You will notice that I use tcsh rather than bash. Yes, yes, I know that hard-core UNIX geeks disdain tcsh and only use bash. Shut up. I don't care about you.<br /><br />Finally, I use a simple prompt with tcsh, which has the side effect of setting the title bar for xterm-like terminals (including rxvt). I add the following line to my .tcshrc file:<br /><code><br />set prompt="%{\033]0;%~%L\007%}\[%h\]> "<br /></code>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com2tag:blogger.com,1999:blog-20076457.post-4029739037063053872007-04-22T14:27:00.000-07:002007-04-24T07:55:13.966-07:00Extracting the magnitude component of an image Fourier transform<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGbugyZZNf4XcML-Ss6OhsgBHUgUPj_os9_UOVRs2fpyr65lGyxb8xEDDgGmeysmFsCdQZR9_IDBcF_e1TD8ULIBs9gaEam8eKR4qmYOGrUdy5HUxScicLs9eh9u_CIh76P-d9/s1600-h/mag_four_mask_gray_scale1L.png"><img style="margin: 0pt 0pt 10px 10px; float: right; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGbugyZZNf4XcML-Ss6OhsgBHUgUPj_os9_UOVRs2fpyr65lGyxb8xEDDgGmeysmFsCdQZR9_IDBcF_e1TD8ULIBs9gaEam8eKR4qmYOGrUdy5HUxScicLs9eh9u_CIh76P-d9/s320/mag_four_mask_gray_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5056375290006717490" border="0" /></a><span style="font-size:130%;">New result!</span><br /><br />I finally succeeded in extracting the magnitude component of the image Fourier transform (shown at right).<br /><br /><br /><span style="font-size:130%;">Recapping the story so far</span><br /><br />I previously created a picture of a bird, and a slightly translated version of the same image. I intend to use these images to test ideas about using the Fourier transform to automatically align pairs of images to create aligned stereoscopic pairs.<br /><br />The input images, show in the previous post, are summarized below:<br /><br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikTy8_3qAMnYkjDCiIVtzxXhIT92YFzdozud4NzPr5XTNGHInWipzSx6u9pSByDwI-XwZxRtBSJAe8gC8wLjXKjDXpg2K-YwWFAxxIXnHG27wJmUO45xR9djcuFq4GUx8VcIbZ/s1600-h/scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikTy8_3qAMnYkjDCiIVtzxXhIT92YFzdozud4NzPr5XTNGHInWipzSx6u9pSByDwI-XwZxRtBSJAe8gC8wLjXKjDXpg2K-YwWFAxxIXnHG27wJmUO45xR9djcuFq4GUx8VcIbZ/s200/scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346610979586962" border="0" /></a><br />Original image<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMDY2iY47YK7erTmNFYjHM1Zr0vPeJIf7jjVRRzuR2KOy0uBYsxG3Htg0xqU3J5kRkdg2PX4XU3LkbMDJXY7XphGypkyI0RbPS1PHTAUp9OomgLGvCQ1MFAnsg0r30oGmePses/s1600-h/trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMDY2iY47YK7erTmNFYjHM1Zr0vPeJIf7jjVRRzuR2KOy0uBYsxG3Htg0xqU3J5kRkdg2PX4XU3LkbMDJXY7XphGypkyI0RbPS1PHTAUp9OomgLGvCQ1MFAnsg0r30oGmePses/s200/trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346615274554274" border="0" /></a><br />Translated version of the original image, for testing my hypothesis.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGtzLc1gBKvn6lQc96bYoG8S0fTIz1g-O04p8iPvvYpqVdGM0aBrrrboHz98QY0AO9JV-Xx-hxsw9UDY_hN13C16151B39F-IGm6oNnlphupx5r8Uni2peoZImi3eRU9Q9cEV_/s1600-h/four_mask_gray_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGtzLc1gBKvn6lQc96bYoG8S0fTIz1g-O04p8iPvvYpqVdGM0aBrrrboHz98QY0AO9JV-Xx-hxsw9UDY_hN13C16151B39F-IGm6oNnlphupx5r8Uni2peoZImi3eRU9Q9cEV_/s200/four_mask_gray_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346924512199666" border="0" /></a><br />Fourier transform of original, masked image.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGtrKZKga4HRb512cqz5WMn6VdKBHIZ_2xrPo9y3jbYkLxcFI1SbJWSsRbuNxkks8oPrGbWhswDBZl6JGnUai08cD7V2gAYmMRVHAVxEHuxh4-2khAEluEGjIuTnLGDS58MuKO/s1600-h/four_mask_gray_trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGtrKZKga4HRb512cqz5WMn6VdKBHIZ_2xrPo9y3jbYkLxcFI1SbJWSsRbuNxkks8oPrGbWhswDBZl6JGnUai08cD7V2gAYmMRVHAVxEHuxh4-2khAEluEGjIuTnLGDS58MuKO/s200/four_mask_gray_trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346924512199682" border="0" /></a><br />Fourier transform of translated, masked image<br /></div><br /><br />I took the plunge and learned to write a filter using the pbmplus environment (see previous post). Here is the program as I wrote and used it for this post:<br /><br /><br /><span style="font-size:130%;">The new PGM filter I made</span><br /><br />I understand that it is tedious to mix GIMP and PBM tools in an image processing pipeline. Perhaps I will port the FFT image processing to PBM later...<br /><br />What follows next is C language source code I just now wrote for a new image filter in the PBMPlus or NetPBM image processing tool kit:<br /><pre><br /><em>/* pgm_fourier_recast.c - read a portable graymap produced by the<br />** GIMP Fourier plug-in, and extract magnitude and phase components<br />**<br />** Copyright (C) 2007 by biospud@blogger.com<br />**<br />** Permission to use, copy, modify, and distribute this software and its<br />** documentation for any purpose and without fee is hereby granted, provided<br />** that the above copyright notice appear in all copies and that both that<br />** copyright notice and this permission notice appear in supporting<br />** documentation. This software is provided "as is" without express or<br />** implied warranty.<br />*/</em><br /><br /><em>/*<br />** 1) Place source file pgm_fourier_recast.c in directory with working build of netpbm/editor<br />** 2) Add "pgm_fourier_recast" to list of files in Makefile<br />** 3) "make pgm_fourier_recast" from netpbm/editor directory<br />*/</em><br /><br /><tt>#include</tt> <stdio.h><br /><tt>#include</tt> <math.h><br /><tt>#include</tt> <i>"pgm.h"</i><br /><br /><b>typedef</b> <b>struct</b> pgm_image_struct {<br /><b>int</b> height;<br /><b>int</b> width;<br />gray maximumValue;<br />gray** data;<br />} PgmImage;<br /><br />PgmImage getInputImage( <b>int</b> argc, <b>char</b> *argv[] );<br />PgmImage convertFourierToPhaseMagnitude(PgmImage inputImage);<br /><b>void</b> writeImageAndQuit(PgmImage outputImage);<br /><b>double</b> gimpFourierPixelToDouble(PgmImage image, <b>int</b> x, <b>int</b> y);<br /><b>double</b> getNormalizationFactor(PgmImage image, <b>int</b> x, <b>int</b> y);<br />gray doubleToGimpFourierPixel(<b>double</b> value, PgmImage image, <b>int</b> x, <b>int</b> y);<br /><br /><b>int</b> main( <b>int</b> argc, <b>char</b> *argv[] )<br />{<br />PgmImage inputImage;<br />PgmImage outputImage;<br /><br />inputImage = getInputImage(argc, argv);<br />outputImage = convertFourierToPhaseMagnitude(inputImage);<br />writeImageAndQuit(outputImage);<br />}<br /><br />PgmImage getInputImage( <b>int</b> argc, <b>char</b> *argv[] ) {<br /><b>const</b> <b>char</b>* <b>const</b> usage = <i>"[pgmfile]"</i>;<br /><b>int</b> argn;<br />FILE* inputFile;<br /><br />PgmImage answer;<br /><br />pgm_init( &argc, argv );<br /><br />argn = 1;<br /><br /><b>if</b> ( argn < argc ) {<br /> inputFile = pm_openr( argv[argn] );<br /> ++argn;<br />} <b>else</b> {<br /> inputFile = stdin;<br />}<br /><br /><b>if</b> ( argn != argc )<br />pm_usage( usage );<br /><br />answer.data = pgm_readpgm(<br /> inputFile,<br /> &answer.width,<br /> &answer.height,<br /> &answer.maximumValue<br /> );<br /><br />pm_close( inputFile );<br /><br /><b>return</b> answer;<br />}<br /><br /><b>double</b> gimpFourierPixelToDouble(PgmImage image, <b>int</b> x, <b>int</b> y) {<br /><em>/*<br />** based on source code at<br />** http://people.via.ecp.fr/~remi/soft/gimp/gimp_plugin_en.php3<br />*/</em><br /><br />gray pixel = image.data[x][y];<br /><br /><em>/*<br />** renormalize<br />** from (range 0 -> 255)<br />** to range (-128 -> +127),<br />*/</em><br /><b>double</b> d128 = (<b>double</b>)(pixel) - 128.0; <em>/* double128() */</em><br /><br /><b>double</b> bounded = (d128 / 128.0); <em>/* unboost() */</em><br /><b>double</b> unboosted0 = 160 * (bounded * bounded); <em>/* unboost() */</em><br /><b>double</b> unboosted = d128 > 0 ? unboosted0 : -unboosted0; <em>/* unboost() */</em><br /><br /><b>double</b> answer = unboosted / getNormalizationFactor(image, x, y);<br /><br /><b>return</b> answer;<br />}<br /><br /><em>/* Normalization factor that corrects scale of Fourier transform<br />** pixel based upon distance from origin<br />*/</em><br /><b>double</b> getNormalizationFactor(PgmImage image, <b>int</b> x, <b>int</b> y) {<br /><em>/*<br />** based on source code at<br />** http://people.via.ecp.fr/~remi/soft/gimp/gimp_plugin_en.php3<br />*/</em><br /><b>double</b> cx = (<b>double</b>)abs(x - (image.width + 1)/2 + 1);<br /><b>double</b> cy = (<b>double</b>)abs(y - (image.height + 1)/2 + 1);<br /><b>double</b> energy = (sqrt(cx) + sqrt(cy));<br /><b>return</b> energy*energy;<br />}<br /><br />gray doubleToGimpFourierPixel(<b>double</b> value, PgmImage image, <b>int</b> x, <b>int</b> y) {<br /><br /><b>double</b> normalized = value * getNormalizationFactor(image, x, y);<br /><b>double</b> bounded = fabs( normalized / 160.0 );<br /><b>double</b> boosted0 = 128.0 * sqrt (bounded);<br /><b>double</b> boosted = (value > 0) ? boosted0 : -boosted0;<br /><br /><em>/*<br />** renormalize<br />** from range (-128 -> +127),<br />** to (range 0 -> 255)<br />*/</em><br /><b>int</b> answer = (<b>int</b>)boosted + 128;<br /><b>if</b> (answer >= 255) <b>return</b> 255;<br /><b>if</b> (answer <= 0) <b>return</b> 0;<br /><b>return</b> answer;<br />}<br /><br />PgmImage convertFourierToPhaseMagnitude(PgmImage inputImage) {<br />PgmImage answer;<br /><b>int</b> outRows = inputImage.height;<br /><b>int</b> outCols = inputImage.width;<br /><b>int</b> row, col;<br /><br /><b>double</b> realDouble, imaginaryDouble;<br /><b>double</b> magnitudeDouble, phaseDouble;<br />gray realPixel, imaginaryPixel;<br />gray magnitudePixel, phasePixel;<br /><br /><b>int</b> doUsePhase = 0;<br /><br />answer.height = outRows;<br />answer.width = outCols;<br />answer.maximumValue = inputImage.maximumValue;<br />answer.data = pgm_allocarray( outCols, outRows );<br /><br /><b>for</b> ( row = 0; row < outRows; ++row ) {<br /> <b>for</b> ( col = 0; col < outCols; col += 2) {<br /> <em>/* get pixel values from image */</em><br /> realPixel = inputImage.data[row][col];<br /> imaginaryPixel = inputImage.data[row][col + 1];<br /><br /> <em>/* convert to doubles */</em><br /> realDouble = gimpFourierPixelToDouble(inputImage, row, col);<br /> imaginaryDouble = gimpFourierPixelToDouble(inputImage, row, col);<br /><br /> <em>/* convert real/imaginary to magnitude/phase */</em><br /> magnitudeDouble = sqrt(<br /> realDouble * realDouble +<br /> imaginaryDouble * imaginaryDouble<br /> );<br /><br /> <em>/* convert to pixel values */</em><br /> magnitudePixel = doubleToGimpFourierPixel(<br /> magnitudeDouble,<br /> inputImage, row, col<br /> );<br /><br /> <b>if</b> (doUsePhase) {<br /> phaseDouble = atan2(imaginaryDouble, realDouble);<br /><br /> phasePixel = (<b>int</b>)(256.0 * phaseDouble / (2.0 * 3.14159));<br /> <b>while</b> (phasePixel > 255) phasePixel -= 256;<br /> <b>while</b> (phasePixel < 0) phasePixel += 256;<br /> }<br /><br /> <em>/*<br /> i1 = inputImage.data[row][col];<br /> v = gimpFourierPixelToDouble(inputImage, row, col);<br /> i2 = doubleToGimpFourierPixel(v, inputImage, row, col);<br /> printf("%.3g\t%.3g\t%.3g\t%.3g\n",<br /> realDouble, imaginaryDouble, magnitudeDouble, phaseDouble);<br /> */</em><br /><br /> answer.data[row][col] = magnitudePixel;<br /><br /> <b>if</b> (doUsePhase)<br /> answer.data[row][col + 1] = phasePixel;<br /> <b>else</b><br /> answer.data[row][col + 1] = magnitudePixel;<br /><br /> }<br />}<br /><br /><b>return</b> answer;<br />}<br /><br /><b>void</b> writeImageAndQuit(PgmImage outputImage) {<br /><em>/* Write resulting image */</em><br />pgm_writepgm(<br /> stdout,<br /> outputImage.data,<br /> outputImage.width,<br /> outputImage.height,<br /> outputImage.maximumValue,<br /> 0<br /> );<br /><br /><em>/* and clean up */</em><br />pm_close( stdout );<br />pgm_freearray(<br /> outputImage.data,<br /> outputImage.height<br /> );<br /><br />exit( 0 );<br />}<br /><br /><br /></pre><span style="font-size:130%;">Original vs. translated images in Fourier magnitude space:</span><br /><br />Phew! After writing this filter, I created the following "magnitude only" versions of the test images:<br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-RrjdSsfeME8JR56u7dMgh98EDqM_juLdnfd7ZDsgPlH_HzFBnHbfUtgdbigZaNpdAaR9ZV6ocLMHYOpWoYFHsFYoTMTbi9FyNLFFhymRqpFKtHwL1CSaIGAbbUVRLTcCF19P/s1600-h/mag_four_mask_gray_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-RrjdSsfeME8JR56u7dMgh98EDqM_juLdnfd7ZDsgPlH_HzFBnHbfUtgdbigZaNpdAaR9ZV6ocLMHYOpWoYFHsFYoTMTbi9FyNLFFhymRqpFKtHwL1CSaIGAbbUVRLTcCF19P/s200/mag_four_mask_gray_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5056368254850286610" border="0" /></a><br />Original: Magnitude component of Fourier transform of original image<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4tneeoMkggzvC_PZ36Th_eUha_zyc5ELs_M5RCPA7FifEHjt11NzfeJxMUKP8d_-TgTl75FQFBXaR0KlTHalzfu4Q0Vz3EO4MPEQqMjz-Zga_vLCSTX31uBEnC91LWV3ktRwm/s1600-h/mag_four_mask_gray_trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4tneeoMkggzvC_PZ36Th_eUha_zyc5ELs_M5RCPA7FifEHjt11NzfeJxMUKP8d_-TgTl75FQFBXaR0KlTHalzfu4Q0Vz3EO4MPEQqMjz-Zga_vLCSTX31uBEnC91LWV3ktRwm/s200/mag_four_mask_gray_trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5056368254850286626" border="0" /></a><br />Translated: Magnitude component of Fourier transform of translated image<br /></div><br />A superficial look suggests that the magnitude component is in fact very similar between the two images. But for automation, I need a quantitative measure to decide how similar two images are. More next time...Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-50493213630950769212007-04-19T15:58:00.000-07:002007-04-23T10:40:40.201-07:00Testing my Fourier transform hypothesisIn the past few posts I have repeatedly assumed that the magnitude component of the Fourier transform of an image will be relatively unchanged when the original image is translated vertically and/or horizontally. My next task should be either prove or disprove this hypothesis before going much further.<br /><br />Let's start with two gray-scale images that differ only in horizontal alignment for testing. If my intuition is correct, the magnitude portion of the Fourier transform should differ only slightly between the two images.<br /><br />I downloaded and installed <a href="http://netpbm.sourceforge.net/">NetPBM</a>, to facilitate command line processing of images. I suspect that it will be easier for me to write new pbm filters than to write GIMP plug-ins.<br /><br />One infuriating thing about NetPBM is that one of the maintainers has destroyed many of the original man pages in an effort to "simplify" the distribution. I genuinely appreciate this dude taking on the responsibility to maintain the code, but this one horrible documentation decision has caused me to curse out loud many times in the past several years. My feelings are neatly summed up by the <a href="http://mail-index.netbsd.org/tech-pkg/2004/07/24/0029.html">observations of another user</a> on the netbsd packaging discussion list:<br /><br /><blockquote><br />"...I want the manual as released with the code I'm using, no changes after the fact. Release your manuals, don't blog them. it is *IMPOSSIBLE* for me to get that manual, no matter how many hoops I jump through, because you cannot (as they suggest) 'wget' an old version of the manual, one which still has manual pages instead of links to other non-Netpbm projects featured on the top page, one which has actual documentation for pnmscale rather than a three-page rant about why I should switch to Netpam..."<br /></blockquote><br /><br />Hear hear.<br /><br />In any case, here is a visual overview of the experiment set-up:<br /><center><br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikTy8_3qAMnYkjDCiIVtzxXhIT92YFzdozud4NzPr5XTNGHInWipzSx6u9pSByDwI-XwZxRtBSJAe8gC8wLjXKjDXpg2K-YwWFAxxIXnHG27wJmUO45xR9djcuFq4GUx8VcIbZ/s1600-h/scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikTy8_3qAMnYkjDCiIVtzxXhIT92YFzdozud4NzPr5XTNGHInWipzSx6u9pSByDwI-XwZxRtBSJAe8gC8wLjXKjDXpg2K-YwWFAxxIXnHG27wJmUO45xR9djcuFq4GUx8VcIbZ/s200/scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346610979586962" border="0" /></a><br />Original image<br /><br /><div style="text-align: left;">One thing I will need is a method to compare how similar two images are. As a control, I will be comparing the original image to itself.<br /><br /></div><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMDY2iY47YK7erTmNFYjHM1Zr0vPeJIf7jjVRRzuR2KOy0uBYsxG3Htg0xqU3J5kRkdg2PX4XU3LkbMDJXY7XphGypkyI0RbPS1PHTAUp9OomgLGvCQ1MFAnsg0r30oGmePses/s1600-h/trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMDY2iY47YK7erTmNFYjHM1Zr0vPeJIf7jjVRRzuR2KOy0uBYsxG3Htg0xqU3J5kRkdg2PX4XU3LkbMDJXY7XphGypkyI0RbPS1PHTAUp9OomgLGvCQ1MFAnsg0r30oGmePses/s200/trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346615274554274" border="0" /></a><br />Translated version of the original image, for testing my hypothesis.<br /><div style="text-align: left;"><br />If I am right about the Fourier transform, the magnitudes of the Fourier transform will be almost the same between the original image and the translated one. This will simulate the comparison of stereo pairs that do not perfectly line up.<br /><br /><br /></div><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaW7U-InBqRSyiqdToXnBw6_DiLwQrmUhvqnMStUfc6FbWmhw04KHU9pkGVVMz1JCik5AAVAb_WmbgrIgvsdgXaP-_rXM0jKY-raTp8DCM4yTseWg9SoltMMtmKD5PqZviH2E9/s1600-h/gray_trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaW7U-InBqRSyiqdToXnBw6_DiLwQrmUhvqnMStUfc6FbWmhw04KHU9pkGVVMz1JCik5AAVAb_WmbgrIgvsdgXaP-_rXM0jKY-raTp8DCM4yTseWg9SoltMMtmKD5PqZviH2E9/s200/gray_trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346615274554290" border="0" /></a><br />Gray version of the translated image<br /><div style="text-align: left;"><br />To simplify the analysis, I created a gray-scale version of the images, so the issue of the color channels does not complicate the analysis.<br /><br /><br /></div><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCa6qrsHSNEU7Kpvf8yZsCK9gw4wHGCpLjbpVgPSiIHwfcImhSQk6jDnIWwzXRF1Ywuynunf9KaInjxMbJkfh9DXhh516QdRJkeygf40ZXwI5q1Ow4dRz9T8CXda-stS11Pgwf/s1600-h/circle_mask.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCa6qrsHSNEU7Kpvf8yZsCK9gw4wHGCpLjbpVgPSiIHwfcImhSQk6jDnIWwzXRF1Ywuynunf9KaInjxMbJkfh9DXhh516QdRJkeygf40ZXwI5q1Ow4dRz9T8CXda-stS11Pgwf/s200/circle_mask.png" alt="" id="BLOGGER_PHOTO_ID_5055346615274554306" border="0" /></a><br />The mask I used to "remove" the edges of the images<br /><div style="text-align: left;"><br />Recall from my earlier posting that the blurry circle mask is used to reduce edge artifacts in the Fourier transform.<br /><br /><br /></div><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLWFmBiwFltxOSmsa2tDcorKn99O9YZjByWJxkRd8_ioAIJTbXpKfIhRUN_gyomChqL1mKetPMye2LacX-_Zv7co7tM9a6C2TiWHbXq6MnEq7AZU2nKRBBHX1nyoO1lyelLT83/s1600-h/mask_gray_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLWFmBiwFltxOSmsa2tDcorKn99O9YZjByWJxkRd8_ioAIJTbXpKfIhRUN_gyomChqL1mKetPMye2LacX-_Zv7co7tM9a6C2TiWHbXq6MnEq7AZU2nKRBBHX1nyoO1lyelLT83/s200/mask_gray_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346619569521618" border="0" /></a><br />Apply circle mask to untranslated image<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSX8O0Ng_tfo5vEZ5ScVxiE0P5ZOSyuIWJcfw7fvWoId_nzm69o9Q7ScTCcpsMyBx5HmFD7kSC0FZq0RIEwTrA5Fcl-9bDtYUejy7RfgKd-4_1Q-rsz3xQMncbOAc1iDnWHKyt/s1600-h/mask_gray_trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhSX8O0Ng_tfo5vEZ5ScVxiE0P5ZOSyuIWJcfw7fvWoId_nzm69o9Q7ScTCcpsMyBx5HmFD7kSC0FZq0RIEwTrA5Fcl-9bDtYUejy7RfgKd-4_1Q-rsz3xQMncbOAc1iDnWHKyt/s200/mask_gray_trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346920217232354" border="0" /></a><br />Masked version of translated image<br /><br /><div style="text-align: left;">Finally, create the two Fourier transforms, one for the untranslated image and one for the translated image:<br /><br /></div><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGtzLc1gBKvn6lQc96bYoG8S0fTIz1g-O04p8iPvvYpqVdGM0aBrrrboHz98QY0AO9JV-Xx-hxsw9UDY_hN13C16151B39F-IGm6oNnlphupx5r8Uni2peoZImi3eRU9Q9cEV_/s1600-h/four_mask_gray_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGtzLc1gBKvn6lQc96bYoG8S0fTIz1g-O04p8iPvvYpqVdGM0aBrrrboHz98QY0AO9JV-Xx-hxsw9UDY_hN13C16151B39F-IGm6oNnlphupx5r8Uni2peoZImi3eRU9Q9cEV_/s200/four_mask_gray_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346924512199666" border="0" /></a><br />Fourier transform of original, masked image.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGtrKZKga4HRb512cqz5WMn6VdKBHIZ_2xrPo9y3jbYkLxcFI1SbJWSsRbuNxkks8oPrGbWhswDBZl6JGnUai08cD7V2gAYmMRVHAVxEHuxh4-2khAEluEGjIuTnLGDS58MuKO/s1600-h/four_mask_gray_trans_scale1L.png"><img style="cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGtrKZKga4HRb512cqz5WMn6VdKBHIZ_2xrPo9y3jbYkLxcFI1SbJWSsRbuNxkks8oPrGbWhswDBZl6JGnUai08cD7V2gAYmMRVHAVxEHuxh4-2khAEluEGjIuTnLGDS58MuKO/s200/four_mask_gray_trans_scale1L.png" alt="" id="BLOGGER_PHOTO_ID_5055346924512199682" border="0" /></a><br />Fourier transform of translated, masked image<br /><div style="text-align: left;"><br />Next I need to extract the magnitudes of the Fourier transforms and compute the similarities between the images. I have some ideas of how to do this, but it will require more work. I expect that the PBM tools will come in handy here. More next time...<br /><br /></div></center>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-89904149642741131902007-04-15T17:16:00.000-07:002007-04-19T15:57:06.998-07:00Investigating the GIMP Fourier transformIn my <a href="http://biospud.blogspot.com/2007/04/use-of-fourier-transform-in-aligning.html">previous post</a> I began to work up how we might use the Fourier transform to help align two images that form a 3D stereoscopic image pair.<br /><br />A more detailed investigation reveals that we need to ask a few more questions.<br /><br />I sort of understand what the Fourier transform means for scalar data. But in an image, there are three <span class="blsp-spelling-corrected" id="SPELLING_ERROR_0">different</span> channels of color information, usually decomposed in one of two ways.<br /><br />Two different representations of three-dimensional color data in an image pixel:<br /><ol><li>red, green, and blue (<span class="blsp-spelling-error" id="SPELLING_ERROR_1"><span class="blsp-spelling-error" id="SPELLING_ERROR_0">RGB</span></span>), or alternatively as<br /></li><li>hue, saturation, and brightness. (<span class="blsp-spelling-error" id="SPELLING_ERROR_2"><span class="blsp-spelling-error" id="SPELLING_ERROR_1">HSV</span></span>)</li></ol>For any ONE of these channels (<span style="font-style: italic;">e.g.</span> "red"), I can kind of understand what the Fourier transform is. The transform for any single channel should result in a complex number in each pixel of the transform. Complex numbers have two components. These two components of a complex number can be represented in at least two different ways.<br /><br />Two different representations of a two dimensional complex number:<br /><ol><li>Real component and Imaginary component</li><li>Magnitude and phase</li></ol><br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJbjM8XLjyMzbzKAdFqrBtiBigL3Q23crxSpT7wV3UVOU1ghUsgyARiZtFC4QbXskRpD8xvWbyqeK-jRuQESd6WV_A7_pbf-gAE3NRAHBKLrgl5HqTHsa9GLmV1dx6YL0nNQPH/s1600-h/complex.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJbjM8XLjyMzbzKAdFqrBtiBigL3Q23crxSpT7wV3UVOU1ghUsgyARiZtFC4QbXskRpD8xvWbyqeK-jRuQESd6WV_A7_pbf-gAE3NRAHBKLrgl5HqTHsa9GLmV1dx6YL0nNQPH/s320/complex.png" alt="" id="BLOGGER_PHOTO_ID_5053823226344582882" border="0" /></a>Two ways of representing a complex number: magnitude/phase and real/imaginary<br /></div><br />The bottom line here is that is seems to me that the Fourier transform should have twice as much data as the original image, since the Fourier transform takes regular real numbers, and generates complex numbers. So a regular 3-channel image should create a Fourier transform with 6 channels. So what exactly is in the Fourier transform generated by the <a href="http://people.via.ecp.fr/%7Eremi/soft/gimp/gimp_plugin_en.php3">GIMP plug-in</a>?<br /><br />Unfortunately the <a href="http://people.via.ecp.fr/%7Eremi/sitewrapper.php3?src=ecp/tpi/rapport/fourier.html">documentation for the plug-in</a> is in French, and I have not studied French since the mid-1970s.<br /><br />Understanding how the transform data are represented is especially important at this point for two reasons:<br /><ol><li>The whole trick of using the Fourier transform to ignore the horizontal/vertical translation component requires that we use only the magnitude of the complex numbers (which does not depend upon the image translation), and <span class="blsp-spelling-corrected" id="SPELLING_ERROR_3">ignore</span> the phase <span class="blsp-spelling-corrected" id="SPELLING_ERROR_4">component</span> (which depends exquisitely upon the image translation).<br /></li><li>Where are the six channels of data that should be coming from the Fourier transform?<br /></li></ol>So we need to determine whether the complex Fourier transform is stored as real/imaginary components, or if it is stored as magnitude/phase components. More fundamentally, we need to know how six channels of information are being stored in the seemingly 3 or 4 channeled image data (transparency can provide an additional channel).<br /><br />I did some experimentation and determined that the red channel of the Fourier transform corresponds to the red channel of the original image, etc. Excellent.<br /><br />Further, the <a href="http://people.via.ecp.fr/%7Eremi/sitewrapper.php3?src=ecp/tpi/rapport/fourier.html">French documentation</a> is surprisingly intelligible when filtered through <a href="http://babelfish.altavista.com/"><span class="blsp-spelling-error" id="SPELLING_ERROR_2">AltaVista</span> <span class="blsp-spelling-error" id="SPELLING_ERROR_3">babelfish</span></a>. I still don't quite understand all of the details, but it appears that the complex values are stored in pairs of subsequent pixels, representing the logarithm of the real component, followed by the logarithm of the imaginary component. This is bad news. I want the magnitude of the complex number, which is equal to the square root of the sum of the squares of the real and imaginary components (using Pythagoras' theorem). It will be hairy to extract that information. So I need to either a) find another Fourier transform image filter, b) write a GIMP plug-in that further processes these Fourier transform images, c) think of some other trick, or d) abandon this project.<br /><br />By the way, if you read the English translation of the French documentation, there is a good explanation of why, near the end of the article, he compares his simulated image to a "moose". It turns out that the French word for "moose" is "<span class="blsp-spelling-error" id="SPELLING_ERROR_4">orignal</span>", while the French word for "original" is "original". The author made a typo, misspelling "original" to accidentally type another actual French word. Thus his spell-checker did not catch it. I believe he meant to say that the simulated image resembles the original image, not that it resembles a moose. Or not. Who knows?<br /><br />I will cogitate some more on what to do next. More next time...Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com6tag:blogger.com,1999:blog-20076457.post-77715646424665625052007-04-15T15:38:00.000-07:002007-04-17T07:59:56.206-07:00Use of Fourier transform in aligning stereoscopic image pairsIn my <a href="http://biospud.blogspot.com/2007/04/toward-automatic-alignment-of.html">previous post</a>, I wondered how to begin to determine parameters for aligning two images, when no other parameters have yet been determined.<br /><br />One concept that can help is the Fourier transform. The Fourier transform can be used to eliminate the vertical and horizontal alignment components from the analysis. Thus we should be able to determine certain parameters, such as scale and rotation, without having to first solve the vertical and horizontal alignment problem.<br /><br />The <a href="http://www.gimp.org/">GIMP</a> image tool has a <a href="http://people.via.ecp.fr/%7Eremi/soft/gimp/gimp_plugin_en.php3">plug-in</a> that permits computation of the Fourier transform of an image. (Presumably <span class="blsp-spelling-error" id="SPELLING_ERROR_0">Photoshop</span> has a similar tool).<br /><br /><center><br /><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXyR5uT2CPCWL1lgVBUwsY6ipT4JW1dBkDQY5dbMPZNHlnKGYI9LDFje9Qt_-bzeCFQ7x8TAoBEemE9kAD7fYBaUr6DZguLAFHSWme4Mu1KvQOXhTJL1dMKcDmr-SqRiwsPXih/s320/scale1L.png" alt="Left bird image" /><br />The unmodified left-eye view from yesterday<br /></center><br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW382MfycSs_1NJaR-qQjSF5y-UgIQcJtaKdrgpSRWSg0XfxoFEAJRy6Q3HaQo-XlqcmJS8BFq_GNRwUxsMLqPWu-FskpJOHCo3jfu6z-kCPNOf5j_VhKlc7ev5P0G6QXDMe-h/s1600-h/four1scale1L.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhW382MfycSs_1NJaR-qQjSF5y-UgIQcJtaKdrgpSRWSg0XfxoFEAJRy6Q3HaQo-XlqcmJS8BFq_GNRwUxsMLqPWu-FskpJOHCo3jfu6z-kCPNOf5j_VhKlc7ev5P0G6QXDMe-h/s320/four1scale1L.png" alt="Fourier transform of left eye bird image" id="BLOGGER_PHOTO_ID_5053791855903452850" border="0" /></a>Fourier transform of the same bird image, as generated by the GIMP plug in.<br /><div style="text-align: left;"><br />Believe it or not, the Fourier transform contains all of the information necessary to reconstruct the original image.<br /><br />It is difficult for the human eye to make sense of the Fourier transform image. The two largest features are a big vertical stripe down the middle, and a horizontal stripe across the center. Unfortunately, these features are a BAD thing. They show that the Fourier transform is dominated by something I don't care about.<br /><br />What features of the original image have strong horizontal and vertical components, causing the primary features of the Fourier transform? This is perhaps a subtle point: the <span style="font-style: italic;">edges</span> of the image cause these features. This is a problem. If we want to use the Fourier transform to detect the relative rotation between two images, we cannot have the edges of the image dominating the Fourier transform. The vertical and horizontal edges of the images will be used to form the rotational alignment, and no rotation will occur.<br /><br />The solution is to remove the edges of the image before taking the Fourier transform. But how do you remove the edges of an image? Like this:<br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiJMjQGaz-1heaKcgkpuEYt59Uqur6ITFF3E6S21Qi4YWvsvcF_6Rr16lfylftuhO9HLNPdWUl4lLxWJILWJ3UpiMoptWr9uPskXbBorE3XdN6y7zFJkjZ-QPOCWHswCaxNHzD/s1600-h/maskscale1R.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiJMjQGaz-1heaKcgkpuEYt59Uqur6ITFF3E6S21Qi4YWvsvcF_6Rr16lfylftuhO9HLNPdWUl4lLxWJILWJ3UpiMoptWr9uPskXbBorE3XdN6y7zFJkjZ-QPOCWHswCaxNHzD/s320/maskscale1R.png" alt="" id="BLOGGER_PHOTO_ID_5053804036430704322" border="0" /></a>Bird image with "edges removed"<br /></div><br /><br />I created a circular mask for the imtage, so that it would be radially symmetric, thus minimizing image shape artifacts in lining up the relative rotation of two images. Further, I made the mask a blurry circle, figuring that a blurry edge would have more localized effects on only the low-resolution region of the Fourier transform. The new Fourier transform of the "edge-removed" version of the bird is much smoother:<br /><br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzETx2ldhqnPOASV12Y-e4lTjqClZNlGv6KoyYf4xC6QRBrm0D-mMPgN7eSgkdAogBxihP4I4r6Zquf29-2trGXrLXz0IliS8ZhCgvRidne7RMivAGulbwCFVSWaK8H_JdAUTD/s1600-h/fourmaskscale1R.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzETx2ldhqnPOASV12Y-e4lTjqClZNlGv6KoyYf4xC6QRBrm0D-mMPgN7eSgkdAogBxihP4I4r6Zquf29-2trGXrLXz0IliS8ZhCgvRidne7RMivAGulbwCFVSWaK8H_JdAUTD/s320/fourmaskscale1R.png" alt="" id="BLOGGER_PHOTO_ID_5053804036430704338" border="0" /></a>Fourier transform of edge-removed bird image<br /></div><br />It now becomes clear that many of the other primary features of that initial Fourier transform were also "ringing" artifacts related to the edge effect. To sum up the results so far:<br /><ol><li>The Fourier transform looks like it might theoretically be a useful tool for determining the scale and/or rotation relationship between two images, without needing to first determine the <span class="blsp-spelling-corrected" id="SPELLING_ERROR_1">translational</span> components.</li><li>If we end up using the Fourier transform in this way, we should include a <span class="blsp-spelling-error" id="SPELLING_ERROR_2">pre-processing</span> step in which we make a blurry-edged circular version of the two images to be compared.</li></ol>This is a small amount of progress, but I feel it will probably pay off. More next time...<br /><br /><br /><div style="text-align: left;"><br /></div></div></div>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-55109502551643298912007-04-15T14:07:00.000-07:002007-04-15T17:15:43.804-07:00Toward automatic alignment of stereoscopic image pairsWhen aligning the two images of a stereoscopic pair, we wish to determine the following parameters:<br /><ol><li>Scale: The relative scale between the two images. Usually close to 1.0, but might vary if two cameras were used with slightly different zoom or distance.</li><li>Rotation: There may be a small relative rotation between the two images, either clockwise or counterclockwise. This can be tedious to determine manually.<br /></li><li>Eye axis: The direction relating the left eye to the right eye is usually left to right, but might be off by a small angle. For various special <span style="font-style: italic;">ad <span class="blsp-spelling-error" id="SPELLING_ERROR_0">hoc</span></span> stereoscopic techniques, such as 3D photos of the moon, determining this direction is very important. It is tedious and imprecise to determine this axis manually. Most folks just assume that the eye axis is perfectly horizontal and move on.<br /></li><li>Translation: Alignment in the left/right direction and in the up/down direction.<br /><ul><li>Up/down: There is a single clear value for the correct alignment in the up/down direction, perpendicular to the eye axis. This value can be determined manually, but should be amenable to automatic determination as well.<br /></li><li>Left/right: Alignment along the eye-axis varies from pixel to pixel depending upon the depth of the subject. This is how 3D photos work. Determining left/right alignment may be the hardest part to automate.<br /></li></ul></li><li>Brightness and color balance: Especially when the two images are taken with two cameras, as in my set-up, the two images may differ in brightness and color balance. These differences should be corrected before generating a final stereo pair.<br /></li></ol>How can you determine any of these relationships between two images when you don't know the values of the other parameters? This can be a tricky problem. And it probably requires some tricky solutions.<br /><br /><center><br /><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXyR5uT2CPCWL1lgVBUwsY6ipT4JW1dBkDQY5dbMPZNHlnKGYI9LDFje9Qt_-bzeCFQ7x8TAoBEemE9kAD7fYBaUr6DZguLAFHSWme4Mu1KvQOXhTJL1dMKcDmr-SqRiwsPXih/s320/scale1L.png" alt="Left bird image" /><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZytW5XZM3-CIeRb4MIWkFKs_CIUpvav7w3P8kXfFrb6KKm75UJdiSLvDBExg5iajp4wy28_3-O527PS5H4MDV3XPopNPErgopFr32K8s5mZXQHrjyZqRGnNEIr-4wHeXkPGur/s320/scale1R.png" alt="Right bird image" /><br /></center><div style="text-align: center;">The images above are a typical example of a raw stereoscopic pair. The two images obviously differ in color balance, vertical alignment, and horizontal alignment.<br /></div><br />I will attempt to attack the problem of determining each parameter in turn, in subsequent posts.Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-22589223362780469842007-04-07T09:56:00.000-07:002007-04-07T10:08:21.815-07:00Hummingbird with tongue hanging out<div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhATOJOvY-8XGI4OQ842oucla5nvDN4unjPEPKhUtbyW8J6q1Hs7BYlngytAyN_CA8GpQjJfGn0tXmWhe3zGQSCTe0flqH3dgTGNWpN89NPHk9aRjUzQFPTsC12WI6yxbzP9nYW/s1600-h/DSC06787tongue.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhATOJOvY-8XGI4OQ842oucla5nvDN4unjPEPKhUtbyW8J6q1Hs7BYlngytAyN_CA8GpQjJfGn0tXmWhe3zGQSCTe0flqH3dgTGNWpN89NPHk9aRjUzQFPTsC12WI6yxbzP9nYW/s320/DSC06787tongue.png" alt="" id="BLOGGER_PHOTO_ID_5050731474823262754" border="0" /></a><span style="font-weight: bold;">Juvenile male Anna's hummingbird </span><span style="font-weight: bold; color: rgb(0, 0, 0);font-family:Geneva,Arial,sans-serif;font-size:100%;" ><i>(Calypte anna) </i></span><span style="font-weight: bold;">tasting the air</span><br /></div><br />Just now I got a nice photo of a hummingbird sticking his tongue out (click bird for larger image). Notice the fine silvery tongue extending beyond the tip of the beak. This photo represents about the limit of image resolution I will be able to acheive with my current optical set-up. I am pretty happy with this resolution. Unfortunately, in this particular shot the companion camera image was out of focus, so there will be no stereoscopic version of this tongue shot forthcoming. (Not that I have yet created <span style="font-style: italic;">any</span> 3D photos good enough to post!)<br /><br />Today's shoot was my first success at getting decent photos using mirrors. Previously my mirror photos were too blurry and displayed second reflection artifacts. Today I used first surface mirrors mounted more securely. That seems to have done the trick! Now perhaps I will be able to get stereoscopic photos with a smaller interpupilary separation. With today's mirror setup, the separation is about 50 mm, which is still sort of big at this 400 mm distance. I don't know how I can get it smaller though.Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-51819654103722978582007-04-04T18:46:00.000-07:002007-04-04T18:52:56.256-07:00What a coincidence! Here's one with a full purple head now!<div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbXS0LHMr3iRv7yy_4IRxpSZ39zQrVMhP2dq3FNlstIA-2WKAVAvp0BiDW-kKYYaxeKKW1hTSLBOky___XyENaeIri0HfjrS1zPBPn0oVudc_JSnb-fu1j_-hzlHUwO_tCtMyf/s1600-h/DSC06542.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbXS0LHMr3iRv7yy_4IRxpSZ39zQrVMhP2dq3FNlstIA-2WKAVAvp0BiDW-kKYYaxeKKW1hTSLBOky___XyENaeIri0HfjrS1zPBPn0oVudc_JSnb-fu1j_-hzlHUwO_tCtMyf/s320/DSC06542.png" alt="" id="BLOGGER_PHOTO_ID_5049755233051849234" border="0" /></a><span style="font-weight: bold;">Deep magenta throat and crown of male Anna's hummingbird (</span><i style="font-weight: bold;">Calypte anna</i><span style="font-weight: bold;">)</span><br /></div><br />(click bird for larger image)<br /><br />Yesterday I said that the male Anna's hummingbird can have a completely magenta head. On cue, my charming bride captured this image of a male in full display. I guess the male in the earlier pictures is either a hybrid species, a juvenile, or just a mutant. Perhaps it helps that the sky was overcast today.Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0tag:blogger.com,1999:blog-20076457.post-50640171768127251432007-04-03T17:20:00.000-07:002007-04-03T17:46:13.038-07:00Male Anna's hummingbird at even higher resolution.<div style="text-align: left;"><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKqmSFc2c-lFNvEy2cuEDQKmN4AX3zNqqawvt9iczWmp5-uV-VYAWBK8-0JzQ5Pu90_yaZuzh-VVWUugUBQ-iHmO2CwgxtLiKfR1GLs8Q1XIpMK9CHJcZYxAkKX34HM-zFZ38h/s1600-h/DSC06513_enhance.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKqmSFc2c-lFNvEy2cuEDQKmN4AX3zNqqawvt9iczWmp5-uV-VYAWBK8-0JzQ5Pu90_yaZuzh-VVWUugUBQ-iHmO2CwgxtLiKfR1GLs8Q1XIpMK9CHJcZYxAkKX34HM-zFZ38h/s320/DSC06513_enhance.png" alt="" id="BLOGGER_PHOTO_ID_5049361696441119554" border="0" /></a><span style="font-weight: bold;">Male Anna's Hummingbird (</span><span style="font-style: italic; font-weight: bold;">Calypte anna</span><span style="font-weight: bold;">) in repose at feeder</span><br /></div><br /></div> My beautiful wife captured our best hummingbird pictures yet this morning (click bird for higher resolution view). Notice the fine detail in the feathers. This male Anna's hummingbird, like many male hummingbirds, has a bright red neck when viewed from certain angles. I am uncertain whether this depends upon the orientation of the feathers, the orientation of the sun, the orientation of the person viewing, or some combination of those three. In any case, the geometry of this bird was right to show the red throat. From other angles, the throat of the male appears dark or black. (The throat of the female is much paler. See some of our previous photos in older posts).<br /><br />This species, Anna's hummingbird (<span style="font-style: italic;">Calypte anna</span>), is the only hummingbird species in which the crown (top of the head) of the male can also appear crimson (in addition to the throat). If you look carefully at the photo above, you can see a few reddish feathers on the head. Google image search for "Anna's Hummingbird" and you will find many images of male birds in which the entire head glows with a brilliant magenta color. You have to view the bird from just the right angle to get that effect.<br /><br />On the lower left of this bird's throat is a region that is yellow-green, almost the exact complementary (opposite) color to the red-magenta seen on the rest of the throat. I suspect that the complementary color viewed from a different angle is no coincidence. It reminds me of the cytological stain eosin, which is colored red-magenta when you view light through the solution, but is yellow-olive when you view light reflected off of the solution's surface. Eosin is one of the important stains used in Pap smears, and many other important microscopic tissue staining methods.Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com1tag:blogger.com,1999:blog-20076457.post-55622876043755659542007-04-02T19:33:00.000-07:002007-04-02T19:39:06.627-07:00Today's hummingbird pictures<div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6asHkY8bLVmzosEmM-ajRGMvgynxKSk8jaK3mGJAleT2XOzcgRU94fJw5dfjza0pS6tqar9va4C2dD4v1fuXTOVahNXP0OKmmppKfFJeZ-iQrV1QATojWvVXEujmdpnPxShRY/s1600-h/DSC04674crop.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg6asHkY8bLVmzosEmM-ajRGMvgynxKSk8jaK3mGJAleT2XOzcgRU94fJw5dfjza0pS6tqar9va4C2dD4v1fuXTOVahNXP0OKmmppKfFJeZ-iQrV1QATojWvVXEujmdpnPxShRY/s400/DSC04674crop.png" alt="" id="BLOGGER_PHOTO_ID_5049025606660272930" border="0" /></a>What is that yellow material on the male hummingbird's beak? Pollen?<br /></div><br /><br /><div style="text-align: center;"><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJuQhYpWQ3wM-9q-CgTieV3e9T2WVc7Lcp-33XQbgGblqteRLWHi7JzyzgJXyGhgolFvjfaJB5bG722uuRVNhZ2TNAqhWHsrc4QLURs7FFUE0l1dJOAe-QjY3-2AxKMNdcoj08/s1600-h/DSC04680crop.png"><img style="margin: 0px auto 10px; display: block; text-align: center; cursor: pointer;" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJuQhYpWQ3wM-9q-CgTieV3e9T2WVc7Lcp-33XQbgGblqteRLWHi7JzyzgJXyGhgolFvjfaJB5bG722uuRVNhZ2TNAqhWHsrc4QLURs7FFUE0l1dJOAe-QjY3-2AxKMNdcoj08/s400/DSC04680crop.png" alt="" id="BLOGGER_PHOTO_ID_5049024743371846402" border="0" /></a>Notice the fluffy feathers on this male hummingbird's underside<br /></div><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJuQhYpWQ3wM-9q-CgTieV3e9T2WVc7Lcp-33XQbgGblqteRLWHi7JzyzgJXyGhgolFvjfaJB5bG722uuRVNhZ2TNAqhWHsrc4QLURs7FFUE0l1dJOAe-QjY3-2AxKMNdcoj08/s1600-h/DSC04680crop.png"><br /></a>Biospudhttp://www.blogger.com/profile/13304428707742568072noreply@blogger.com0