I do use the vizcave module, but even when I make sure no viewpoints are moving around (like by replacing the trackers with something that doesn't move), I get a lot of different disparity values. The disparity only seems to change when certain important code is run, too (it is when I re-apply the orthographic projection, for example).
Here's some code that reproduces the problem. Press 4 or 6 to have it repeatedly display the "stimulus". In case of no stereo equipment, the most obvious symptom is that the white box doesn't always stay in the same centered location from one trial to the next - it appears left or right by some unpredictable amount. (With stereo equipment, it's more apparent that what is happening is that the disparity is changing.) It usually starts happening by the 5th trial.
Here's the file for the stimulus part with as much code as I dared to remove taken out. I don't think most of this is too important, except that I do call
orthoUpdateCaveAndView() in the
restart() helper function, which is run each time you press 4 or 6.
Code:
import viz
import vizact
import vizshape
import sunyhardware
def makeCylinderSpheres(radius, height, vertexCount, vertexRadius, vac=None, dc=None):
# FIXME quite slow - profile for problems
import random
from math import sin, cos, pi
if vac and vertexRadius != 1.0:
print "WARNING: setting vertex radius to 1.0 meters"
print " for VisualAngleConstantizer to scale later"
vertexRadius = 1.0
cylinder = viz.addGroup()
# Distribution of verts in theta, stratifed, parameters by trial and error
# TODO parameterize
skipWidth = 4.0*pi/5.0
sliceWidth = pi/3.0
# in height
base = -height*0.5
climb = height/(vertexCount-1)
for vertNum in xrange(vertexCount):
# HACK Don't render the middle few to leave room for fixation dot
if abs(vertNum - vertexCount/2.0) <= 1.1:
continue;
theta = skipWidth*vertNum + sliceWidth*random.random()
pos = [radius*sin(theta), base + vertNum*climb, radius*cos(theta)]
sphere = vizshape.addSphere(radius=vertexRadius, parent=cylinder)
sphere.setPosition(*pos)
if dc:
dc.add(sphere)
if vac:
vac.add(sphere, vertexRadius*2)
return cylinder
# TODO error message if HiBall not available?
(METERS, DEGREES) = xrange(2) # valid units
(offX, offY, offZ) = sunyhardware.getValue(('xZero', 'yZero', 'zZero'))
DEFAULT_PARAMS = {
'vertexRadius': 0.01, # radius of spheres, in visual angle or length
'vertexUnits': METERS, # unit of the above radius ('degrees' only works if not orthographic)
# Rotation options
'rotateSpeed': 180.0, # Degrees per second, positive = front goes left
'spinAxis': (0,1,0), # Axis of rotation in world space, not local cube space
'fixationSize': 0.02,
'fixationColor': [0.5,0.5,0.5],
'fixationFrameSize': 0.005, # just the frame around fixationSize
'fixationFrameColor': [1.0,1.0,1.0],
# Graphical options
'orthographic': True,
'objectConstructor': makeCylinderSpheres,
# Define either 'dcTable' or a 'dc' DepthColorizer object; 'dc' takes priority over 'dcTable'
'dcTable': ( # (distance from head, [H,S,V])
( 1.7, [240, 0.75, 0.0]),
( 1.7, [240, 0.75, 1.0]),
( 2.3, [360, 0.75, 1.0]),
( 2.3, [360, 0.75, 0.0])
),
#Interface options
'interfaceParams': {
'usePosition': False, #True,
'centerThreshold': 180, #offX, # Dividing line between left and right (degrees or meters)
'centerIgnore': 5, #0.02 # Zone (in each direction) in which answer is ambiguous and ignored (degrees or meters)
'reverse': True, # Move or point rightwards for a 'correct' answer normally, leftwards for not
},
'indicParams': {
# visual indicators for pointing left/center/right
'leftObjPos': [-0.5, -0.25, 0], # Relative to center position
'ambigObjPos': [0.0, -0.5, 0], # Relative to center position
'rightObjPos': [0.5, -0.25, 0], # Relative to center position
}
}
DEFAULT_PARAMS.update({
'objectParams': {
'radius': 0.10, # of the circle around the cylinder
'height': 0.45,
'vertexCount': 24,
},
'startAngle': (0,0,0), # Euler angles of initial rotation
'centerPosition': [offX, offY, offZ]
})
class Stimulus(viz.EventClass):
""" TODO documentation """
TIMER_ID = 0
def __init__(self, stimParams):
viz.EventClass.__init__(self)
self.headTrkr = stimParams['headTrkr']
# Construct stimulus object to display
params = stimParams['objectParams'].copy()
params['dc']=None
params['vertexRadius'] = stimParams['vertexRadius']
self.obj = stimParams['objectConstructor'](**params)
# Apply actual spin to object
self.spinObject = viz.addGroup()
self.obj.parent(self.spinObject)
self.spinObject.setPosition(stimParams['centerPosition'])
spinAxis = stimParams['spinAxis']
spin = vizact.spin(spinAxis[0], spinAxis[1], spinAxis[2], stimParams['rotateSpeed'], viz.FOREVER)
self.spinObject.addAction(spin)
self.obj.visible(viz.OFF)
self.spinObject.disable(viz.ANIMATIONS)
# Create fixation block
self.fixGroup = viz.addGroup()
self.fixation = vizshape.addQuad(
size=(stimParams['fixationSize'], stimParams['fixationSize']),
parent = self.fixGroup)
self.fixation.color(stimParams['fixationColor'])
self.fixation.billboard(viz.BILLBOARD_VIEW)
totalFrameSize = stimParams['fixationSize'] + stimParams['fixationFrameSize']
self.fixationFrame = vizshape.addQuad(size=(totalFrameSize, totalFrameSize),
parent = self.fixGroup)
self.fixationFrame.color(stimParams['fixationFrameColor'])
self.fixationFrame.billboard(viz.BILLBOARD_VIEW)
self.fixGroup.setPosition(stimParams['centerPosition'])
self.fixGroup.visible(viz.OFF)
# Save parameters for later
self.stimParams = stimParams
def startFixation(self, trialParams):
self.obj.setEuler(*self.stimParams['startAngle'])
viz.ipd(trialParams['ipd'])
self.fixGroup.visible(viz.ON)
def startStimulus(self, trialParams):
self.spinObject.enable(viz.ANIMATIONS)
self.obj.visible(viz.ON)
def endStimulus(self, stairCallback):
self.fixGroup.visible(viz.OFF)
self.obj.visible(viz.OFF)
self.spinObject.disable(viz.ANIMATIONS)
#self.callback(viz.SENSOR_UP_EVENT, self.onButtonUp)
self.callback(viz.KEYUP_EVENT, self.onKeyUp)
self.stairCallback = stairCallback
def onKeyUp(self, key):
if key == '4' or key == '6':
self.callback(viz.KEYUP_EVENT, None)
return self.stairCallback(key == '4', False)
else:
pass # TODO 'try again' prompt
if __name__=='__main__':
viz.go()
stimParams = DEFAULT_PARAMS.copy()
trackers = sunyhardware.getAllSensors()
(stimParams['headTrkr'], stimParams['stylusTrkr'], stimParams['stylusBtn']) = trackers
# test multiple simultaneous stimulus objects
stim = Stimulus(stimParams)
#stim2 = Stimulus(stimParams)
RESTART_DELAY = 0.5
import random
def delayedRestart(*args):
viz.callback(viz.TIMER_EVENT, restart)
viz.starttimer(0, RESTART_DELAY)
def restart(*args):
print 'starting 1'
headTrkr = stimParams['headTrkr']
sunyhardware.orthoUpdateCaveAndView(
tracker = headTrkr.head,
cave = headTrkr.cave,
view = headTrkr.view,
ortho = stimParams['orthographic'])
stim.startFixation({'ipd': random.uniform(-0.05, 0.05)})
stim.startStimulus({})
viz.callback(viz.TIMER_EVENT, endStim)
viz.starttimer(0, 1.0)
def endStim(*args):
print 'ended 1'
viz.killtimer(0)
#stim.endStimulus(delayedRestart2)
stim.endStimulus(delayedRestart)
delayedRestart()
...and in the next post(s) are the potentially more-relevant hardware abstraction code. The most relevant functions are
loadCave() (where I define helper functions
makeOrthographic() and
makePerspective()) and
orthoUpdateCaveAndView(), which does the dirty work of turning off caves and such.
I edited the file so you'd get dummy VRPN sensors from localhost, which definitely don't move at all. It throws slightly-annoying "no response from server" warnings, though.