By far the most efficient way of doing this is using a splitter on the camera's preview port, then connecting that to a couple of renderers. Sadly, the picamera library doesn't support this "out of the box" (it connects a splitter on the video port to support recording at multiple resolutions, or capturing while recording, but it doesn't support multiple previews because that's a fairly niche requirement; see camera hardware for more info on the camera's ports).
However, it's quite easy to arrange by using the
mmalobj layer which picamera is built upon (that link goes to the latest version of the docs which includes a long introduction to
mmalobj, but also has a few currently unreleased features - luckily we don't need the unreleased bits for this).
Basically we're going to build an MMAL pipeline that looks like this:
-->renderer (left eye)
-->splitter-->renderer (right eye)
The code doesn't look like "normal" picamera code, but read through the introduction in the link above and you should get a reasonable feel for the
mmalobj style of doing things. In the code below I assume you've got a 1080p sized display, and I'm using a 960x720 capture size for a 4:3 ratio display in each eye (centered vertically); you'll need to modify things if your display is a different size:
from picamera import mmalobj as mo, mmal
from signal import pause
camera = mo.MMALCamera()
splitter = mo.MMALSplitter()
render_l = mo.MMALRenderer()
render_r = mo.MMALRenderer()
camera.outputs.framesize = (960, 720)
camera.outputs.framerate = 30
p = render_l.inputs.params[mmal.MMAL_PARAMETER_DISPLAYREGION]
p.set = mmal.MMAL_DISPLAY_SET_FULLSCREEN | mmal.MMAL_DISPLAY_SET_DEST_RECT
p.fullscreen = False
p.dest_rect = mmal.MMAL_RECT_T(0, 180, 960, 720)
render_l.inputs.params[mmal.MMAL_PARAMETER_DISPLAYREGION] = p
p.dest_rect = mmal.MMAL_RECT_T(960, 180, 960, 720)
render_r.inputs.params[mmal.MMAL_PARAMETER_DISPLAYREGION] = p
At this point you should have a nice, smooth preview in both eyes (and all the work will be occurring on the GPU, so your CPU is entirely free for other tasks).
Controlling the camera via mmalobj requires a bit more effort than with picamera (unsurprising given that picamera is built upon mmalobj, so picamera is a simpler abstraction on top of the lower level mmalobj). For example, to find out how to change the brightness, take a look at PiCamera._set_brightness:
def _set_brightness(self, value):
if not (0 <= value <= 100):
"Invalid brightness value: %d (valid range 0..100)" % value)
self._camera.control.params[mmal.MMAL_PARAMETER_BRIGHTNESS] = Fraction(value, 100)
We can guess that
_check_camera_open is some method which'll raise an exception if the camera's been closed. The next bit just checks the specified value is somewhere in the range 0 to 100, and then there's the useful bit at the end: using
mmal.MMAL_PARAMETER_BRIGHTNESS on the camera's control port to set the brightness. Note the value is a
Fraction with a denominator of 100, so brightness values are actually between 0 and 1.
How do we use that in the script above? Recall from the
mmalobj documentation (somewhere near the end of Threads & Synchronization) that
self._camera in the snippet above) is simply an instance of
MMALCamera. In the script above we start with
camera = mo.MMALCamera() so this simply becomes:
camera.control.params[mmal.MMAL_PARAMETER_BRIGHTNESS] = 0.5
Note: parameters which return fractions also accept any other numeric type and will automatically convert the value to a fraction.
It's even possible to mix picamera and mmalobj in a script (if you're careful). For example, knowing that
PiCamera._camera is actually an
MMALCamera object I could've constructed the camera object as
pc = picamera.PiCamera(); camera = pc._camera in the script above, but this would actually wind up more complicated as we'd then have to deal with the null-sink that
PiCamera automatically constructs on the preview port.