Mylos

Leica Monovid review

Leica recently introduced the Monovid monocular. Monoculars are more compact than binoculars, but you lose stereo vision, which is why birdwatchers tend to shun them. I myself have a very strong director eye and correspondingly poor binocular vision, so this is not such a big deal for me.

Monovid

The Monovid is supplied with an accessory screw-on close-up lens that reduces the minimum focus distance. This is useful for butterfly or hummingbird watchers, but the arrangement is clumsier than the (admittedly much larger) Minox Makroskop.

Monovid

The Monovid is essentially half of a pair of Ultravid 8×20 BL binoculars. The barrel is 3-4mm longer to accommodate the threads for the close-up lens, and it has a goiter-like knurled protrusion towards the end for focusing. The eyecup is the same, and can be either pulled out for normal viewing, or left in for eyeglass wearers. This is a far better arrangement than fold-up rubber eyecups. The leather case for the Monovid is quite bulky, and features a screw thread to hold the close-up lens as well as an ingenious ribbon that pulls the monocular out of the case when you flip the lid open. It also has a magnetic catch unlike the Ultravids’ snap button.

Monovid and Ultravid

Unsurprisingly, the performance is nearly identical, that is to say, stellar. The image is bright (most monoculars are in the 12-15mm aperture range). There is no hint of distortion or chromatic aberration across the field. It is quite sensitive to perfectly centered eye placement, specially when you are wearing eyeglasses, otherwise you will black out.

Sadly, the price is not half that of the binoculars, closer to two thirds. Considering that it is not all that much more compact and you lose stereo vision, if you are considering one, I would recommend the more versatile Ultravid 8×20 BL (or the cheaper BR) instead. Another option to consider is the respected line of Zeiss monoculars (most are more compact than the Monovid, but the 8×20 is nearly the same size and not as well built) or the slower but smaller Nikon “high grade” monocular series (unfortunately the 7×15 has been discontinued, but old new stock is still readily available).

Update (2012-11-26):

I purchased a new-old-stock Nikon High Grade 5×15 monocular. It is much lighter than the Monovid, has much closer focusing at 60cm vs 1.80m, no futzing with accessory lenses required (although to be fair the Leica focuses to 25-30cm with the close-up lens).

The fit and finish on the Japanese-made monoculars (vs. Portugal, not Germany, for the Monovid) is superb, with deeply engraved and paint-filled markings (even superior in execution to AI-s Nikkors). The supplied case is made of genuine leather, unfortunately it does not include a belt clip. The aluminium shell is not monocoque like the Monovid, and seems less robust. There is also no mention of the Nikon being nitrogen-purged and waterproof. The finger indents make it quite comfortable to hold and the hexagonal cross-section means it is unlikely to roll off a table. The neck strap on the Nikon is also lighter and far more practical than the bulky hand strap on the Leica. Surprisingly, it is not easier to hand-hold despite the lower magnification – the heft of the Monovid stabilizes it somewhat against shake.

The optics are excellent. It’s hard to compare to the Leica, due to the lower magnification and aperture, but like the Leica, the image is clear across the frame with no color aberrations across the frame. There seems to be some slight chromatic aberration at the very edges that you don’t get with the Leica.

The Nikon 5×15 HG is discontinued, but it appears in their 2012-2013 sports optics catalog, along with the 7×15 HG, so there is a possibility it will be resurrected. It is readily available from Japan on eBay and occasionally Amazon as well.

Update (2017-02-21):

I seem to be collecting monoculars. Some other models:

  • Steiner 8×22 Miniscope: plastic piece of junk, avoid it.
  • Minox 8×25 Macroscope: very bulky, decent but not astounding optics, the focusing knob is not very ergonomic. It’s sole saving grace is its very close focusing distance. It’s best to think of it as a loupe with very long clearance, and as such I would expect it to be ideal for naturalists, entomologists, field geologists and the like.
  • Nikon HG 7×15: much the same as the 5×15, with narrower field of view, smaller sweet spot for eye placement and shorter eye relief. The 5x is a better monocular in most circumstances, if you really need the higher magnification you are better off getting the Monovid or proper binoculars.

Just enough Weave

Note: I am keeping this code around for historical purposes, but it has not worked since Weave 1.0 RC2. I created this because Mozilla’s public sync servers were initially quite unreliable, but they have remedied the situation and performance problems are a thing of the past. I also learned the inner workings of Weave/Firefox Sync in the process, and am satisfied as to the security of the system. Since I no longer use Firefox myself, I do not expect to ever revive this project. Feel free to take it over, otherwise you are best served by using Mozilla’s cloud.

Like most of my readers, I use multiple computers: my Mac Pro at home, my MacBook Air when on the road, 3 desktop PCs at work, a number of virtual machines, and so on. I have Firefox installed on all of them. The Mozilla Weave extension allows me to sync bookmarks, passwords et al between them. Weave encrypts this data before uploading it to the server, but I do not like to rely on third-party web services for mission-critical functions (my Mozilla server was down last Monday, for instance, due to the surge of traffic from people returning to work and performing a full sync against 0.5). Through Weave 0.5, I ran my own instance of the Mozilla public Weave server version 0.3. Unfortunately, Weave 0.6 requires server version 0.5 and I had to upgrade.

The open-source Weave server is implemented in PHP. It doesn’t require Apache compiled with mod_dav as early versions did (I prefer to run nginx), but it is still a fairly gnarly piece of code that is anything but plug-and-play. Somehow I had managed to get version 0.3 running on my home server, but no amount of blundering around got me to a usable state with 0.5. I ended up deciding to implement a minimalist Weave server in Python, as it seemed less painful than continuing to struggle with the Mozilla spaghetti code, which confusingly features multiple pieces of code that appear to do exactly the same thing in three different places. Famous last words…

Three days of hacking later, I managed to get it working. 200 or so lines of Python code replaced approximately 12,000 lines of PHP. Of course, I am not trying to reproduce an entire public cloud infrastructure like Mozilla’s, just enough for my own needs, using the “simplest thing that works” principle. Interestingly, the Mozilla code includes a vestigial Python reference implementation of a Weave server for testing purposes. It does not seem to have been working for a while, though. I used it as a starting point but ended up rewriting almost everything. Here are the simplifying hypotheses:

  • My weave server is meant for a single user (my wife prefers Safari)
  • It does not implement authentication, logging or SSL encryption — it is meant to be used behind a nginx (or Apache) reverse proxy that will perform these functions.
  • It has no configuration file. There are just three variables to set at the top of the source file.
  • It does not implement the full server protocol, just the parts that are actually used by the extension today.
  • More controversially, it does not even implement persistence, keeping all data in RAM instead. Python running on Solaris is very reliable, and the expected uptime of the server is likely months on end. If the server fails, the Firefoxes will just have to perform a full sync and reconciliation. Fortunately, that has been much improved in Weave 0.6, so the cost is minimal. This could even be construed as a security feature, since there is no data on disk to be misplaced. It would take catastrophically losing all my browsers simultaneously to risk data loss. Short of California falling into the ocean, that’s not going to happen, and if it does, I probably have more pressing concerns…

The code could be extended fairly easily to lift these hypotheses, e.g. adding persistence or multiple user support using SQLite, PostgreSQL or MySQL.

Here is the server itself, weave_server.py:

#!/usr/local/bin/python
"""
  Based on tools/scripts/weave_server.py from
  http://hg.mozilla.org/labs/weave/

  do the Simplest Thing That Can Work: just enough to get by with Weave 0.6
  - SSL, authentication and loggin are done by nginx or other reverse proxy
  - no persistence, in case of process failure do a full resync
  - only one user. If you need more, create multiple instances on different
    ports and use rewrite rules to route traffic to the right one
"""

import sys, time, logging, socket, urlparse, httplib, pprint
try:
  import simplejson as json
except ImportError:
  import json
import wsgiref.simple_server

URL_BASE = 'https://your.server.name/'
#BIND_IP = ''
BIND_IP = '127.0.0.1'
DEFAULT_PORT = 8000

class HttpResponse:
  def __init__(self, code, content='', content_type='text/plain'):
    self.status = '%s %s' % (code, httplib.responses.get(code, ''))
    self.headers = [('Content-type', content_type),
                    ('X-Weave-Timestamp', str(timestamp()))]
    self.content = content or self.status

def JsonResponse(value):
  return HttpResponse(httplib.OK, value, content_type='application/json')

class HttpRequest:
  def __init__(self, environ):
    self.environ = environ
    content_length = environ.get('CONTENT_LENGTH')
    if content_length:
      stream = environ['wsgi.input']
      self.contents = stream.read(int(content_length))
    else:
      self.contents = ''

def timestamp():
  # Weave rounds to 2 digits and so must we, otherwise rounding errors will
  # influence the "newer" and "older" modifiers
  return round(time.time(), 2)

class WeaveApp():
  """WSGI app for the Weave server"""
  def __init__(self):
    self.collections = {}

  def url_base(self):
    """XXX should derive this automagically from self.request.environ"""
    return URL_BASE

  def ts_col(self, col):
    self.collections.setdefault('timestamps', {})[col] = str(timestamp())

  def parse_url(self, path):
    if not path.startswith('/0.5/') and not path.startswith('/1.0/'):
      return
    command, args = path.split('/', 4)[3:]
    return command, args

  def opts_test(self, opts):
    if 'older' in opts:
      return float(opts['older'][0]).__ge__
    elif 'newer' in opts:
      return float(opts['newer'][0]).__le__
    else:
      return lambda x: True

  # HTTP method handlers

  def _handle_PUT(self, path, environ):
    command, args = self.parse_url(path)
    col, key = args.split('/', 1)
    assert command == 'storage'
    val = self.request.contents
    if val[0] == '{':
      val = json.loads(val)
      val['modified'] = timestamp()
      val = json.dumps(val, sort_keys=True)
    self.collections.setdefault(col, {})[key] = val
    self.ts_col(col)
    return HttpResponse(httplib.OK)

  def _handle_POST(self, path, environ):
    try:
      status = httplib.NOT_FOUND
      if path.startswith('/0.5/') or path.startswith('/1.0/'):
        command, args = self.parse_url(path)
        col = args.split('/')[0]
        vals = json.loads(self.request.contents)
        for val in vals:
          val['modified'] = timestamp()
          self.collections.setdefault(col, {})[val['id']] = json.dumps(val)
        self.ts_col(col)
        status = httplib.OK
    finally:
      return HttpResponse(status)

  def _handle_DELETE(self, path, environ):
    assert path.startswith('/0.5/') or path.startswith('/1.0/')
    response = HttpResponse(httplib.OK)
    if path.endswith('/storage/0'):
      self.collections.clear()
    elif path.startswith('/0.5/') or path.startswith('/1.0/'):
      command, args = self.parse_url(path)
      col, key = args.split('/', 1)
      if not key:
        opts = urlparse.parse_qs(environ['QUERY_STRING'])
        test = self.opts_test(opts)
        col = self.collections.setdefault(col, {})
        for key in col.keys():
          if test(json.loads(col[key]).get('modified', 0)):
            logging.info('DELETE %s key %s' % (path, key))
            del col[key]
      else:
        try:
          del self.collections[col][key]
        except KeyError:
          return HttpResponse(httplib.NOT_FOUND)
    return response

  def _handle_GET(self, path, environ):
    if path.startswith('/0.5/') or path.startswith('/1.0/'):
      command, args = self.parse_url(path)
      return self.handle_storage(command, args, path, environ)
    elif path.startswith('/1/'):
      return HttpResponse(httplib.OK, self.url_base())
    elif path.startswith('/state'):
      return HttpResponse(httplib.OK, pprint.pformat(self.collections))
    else:
      return HttpResponse(httplib.NOT_FOUND)

  def handle_storage(self, command, args, path, environ):
    if command == 'info':
      if args == 'collections':
        return JsonResponse(json.dumps(self.collections.get('timestamps', {})))
    if command == 'storage':
      if '/' in args:
        col, key = args.split('/')
      else:
        col, key = args, None
      try:
        if not key: # list output requested
          opts = urlparse.parse_qs(environ['QUERY_STRING'])
          test = self.opts_test(opts)
          result = []
          for val in self.collections.setdefault(col, {}).itervalues():
            val = json.loads(val)
            if test(val.get('modified', 0)):
              result.append(val)
          result = sorted(result,
                          key=lambda val: (val.get('sortindex'),
                                           val.get('modified')),
                          reverse=True)
          if 'limit' in opts:
            result = result[:int(opts['limit'][0])]
          logging.info('result set len = %d' % len(result))
          if 'application/newlines' in environ.get('HTTP_ACCEPT', ''):
            value = '\n'.join(json.dumps(val) for val in result)
            return HttpResponse(httplib.OK, value,
                                content_type='application/text')
          else:
            return JsonResponse(json.dumps(result))
        else:
          return JsonResponse(self.collections.setdefault(col, {})[key])
      except KeyError:
        if not key: raise
        return HttpResponse(httplib.NOT_FOUND, '"record not found"',
                            content_type='application/json')

  def __process_handler(self, handler):
    path = self.request.environ['PATH_INFO']
    response = handler(path, self.request.environ)
    return response

  def __call__(self, environ, start_response):
    """Main WSGI application method"""

    self.request = HttpRequest(environ)
    method = '_handle_%s' % environ['REQUEST_METHOD']

    # See if we have a method called 'handle_METHOD', where
    # METHOD is the name of the HTTP method to call.  If we do,
    # then call it.
    if hasattr(self, method):
      handler = getattr(self, method)
      response = self.__process_handler(handler)
    else:
      response = HttpResponse(httplib.METHOD_NOT_ALLOWED,
                              'Method %s is not yet implemented.' % method)

    start_response(response.status, response.headers)
    return [response.content]

class NoLogging(wsgiref.simple_server.WSGIRequestHandler):
  def log_request(self, *args):
    pass

if __name__ == '__main__':
  socket.setdefaulttimeout(300)
  if '-v' in sys.argv:
    logging.basicConfig(level=logging.DEBUG)
    handler_class = wsgiref.simple_server.WSGIRequestHandler
  else:
    logging.basicConfig(level=logging.ERROR)
    handler_class = NoLogging
  logging.info('Serving on port %d.' % DEFAULT_PORT)
  app = WeaveApp()
  httpd = wsgiref.simple_server.make_server(BIND_IP, DEFAULT_PORT, app,
                                            handler_class=handler_class)
  httpd.serve_forever()

Here is the relevant fragment from my nginx configuration file:

# Mozilla Weave
location /0.5 {
  auth_basic            "Weave";
  auth_basic_user_file  /home/majid/web/conf/htpasswd.weave;
  proxy_pass            http://localhost:8000;
  proxy_set_header      Host $http_host;
}
location /1.0 {
  auth_basic            "Weave";
  auth_basic_user_file  /home/majid/web/conf/htpasswd.weave;
  proxy_pass            http://localhost:8000;
  proxy_set_header      Host $http_host;
}
location /1/ {
  auth_basic            "Weave";
  auth_basic_user_file  /home/majid/web/conf/htpasswd.weave;
  proxy_pass            http://localhost:8000;
  proxy_set_header      Host $http_host;
}

This code is hereby released into the public domain. You are welcome to use it as you wish. Just keep in mind that since it is reverse-engineered, it may well break with future releases of the Weave extension, or if Mozilla changes the server protocol.

Update (2009-10-03):

I implemented some minor changes for compatibility with Weave 0.7. The diff with the previous version is as follows:

--- weave_server.py~	Thu Sep  3 17:46:44 2009
+++ weave_server.py	Sat Oct  3 02:59:19 2009
@@ -65,8 +65,7 @@
     command, args = path.split('/', 4)[3:]
     return command, args

-  def opts_test(self, environ):
-    opts = urlparse.parse_qs(environ['QUERY_STRING'])
+  def opts_test(self, opts):
     if 'older' in opts:
       return float(opts['older'][0]).__ge__
     elif 'newer' in opts:
@@ -92,7 +91,7 @@
   def _handle_POST(self, path, environ):
     try:
       status = httplib.NOT_FOUND
-      if path.startswith('/0.5/') and path.endswith('/'):
+      if path.startswith('/0.5/'):
         command, args = self.parse_url(path)
         col = args.split('/')[0]
         vals = json.loads(self.request.contents)
@@ -113,7 +112,8 @@
       command, args = self.parse_url(path)
       col, key = args.split('/', 1)
       if not key:
-        test = self.opts_test(environ)
+        opts = urlparse.parse_qs(environ['QUERY_STRING'])
+        test = self.opts_test(opts)
         col = self.collections.setdefault(col, {})
         for key in col.keys():
           if test(json.loads(col[key]).get('modified', 0)):
@@ -142,10 +142,14 @@
       if args == 'collections':
         return JsonResponse(json.dumps(self.collections.get('timestamps', {})))
     if command == 'storage':
-      col, key = args.split('/')
+      if '/' in args:
+        col, key = args.split('/')
+      else:
+        col, key = args, None
       try:
         if not key: # list output requested
-          test = self.opts_test(environ)
+          opts = urlparse.parse_qs(environ['QUERY_STRING'])
+          test = self.opts_test(opts)
           result = []
           for val in self.collections.setdefault(col, {}).itervalues():
             val = json.loads(val)
@@ -155,6 +159,8 @@
                           key=lambda val: (val.get('sortindex'),
                                            val.get('modified')),
                           reverse=True)
+          if 'limit' in opts:
+            result = result[:int(opts['limit'][0])]
           logging.info('result set len = %d' % len(result))
           if 'application/newlines' in environ.get('HTTP_ACCEPT', ''):
             value = '\n'.join(json.dumps(val) for val in result)

Update (2009-11-17):

Weave 1.0b1 uses 1.0 as the protocol version string instead of 0.5 but is otherwise unchanged. I updated the script and nginx configuration accordingly.

Olympus E-P1 hands-on impressions

I had the opportunity to handle an Olympus E-P1 camera at Keeble & Shuchat in Palo Alto. There has been quite a bit of excitement on sites like Rangefinder Forum and many were expecting this to be the first pocketable camera that could compete with SLRs in image quality.

The Sigma DP1 and DP2 were actually the first cameras with large sensors and reasonable pixel counts, bucking the marketing-driven trend towards too many pixels squeezed onto too small a sensor chip, with horrible noise as the result. I own both, and their image quality is indeed stunning, but they have one Achilles’ heel — speed, or the lack thereof.

The E-P1 is very compact, almost the same size with the 17mm as the Sigma DP2 (some photos released suggested it was closer to the Leica M8). The build quality is fine, and it is nowhere near as heavy as some early users suggested it was. They probably compared it to a plastic fantastic compact rather than a more substantial camera like a Leica or a DSLR.

I was surprised to find the 17mm AF hunted quite a bit, overshooting and then backtracking. Oddly, it did this even on the next shot when the lens was already in focus. I don’t know if this is specific to the 17mm lens, but it is certainly not encouraging.

From my test shots, I was also distinctly unimpressed by the optical quality of the lens, or the noise performance at ISO 1600. The Four-Thirds and Micro Four Thirds formats are hobbled by sensors one half the size of the APS-C used in most entry-level DSLRs, with predictably higher levels of noise and limited dynamic range. I had to go back to 2003 and my then Canon EOS 10D to find similar levels of noise. The Canon Rebel XT was definitely superior in high-ISO performance, let alone current SLRs. Olympus fanboys seem to be in denial about the limitations of Four-Thirds sensors, but you cannot fight against physics and expect to win.

The other disappointing thing about the 17mm lens is that it is not particularly sharp, specially for a prime lens of relatively modest maximum aperture. The pictures were nowhere near as crisp as the lovely Sigma lenses on the DP1 and DP2. This is all the more a let-down as Olympus was renowned for the quality of its miniaturized prime lenses in the days of the ground-breaking OM system. I wasn’t expecting Pentax SMC Limited pancake lens levels of performance (we are talking of a lens one third the price, after all), but there is no point in having 12 megapixels (at least 6 too far in my book) if the lens can’t actually exploit them.

I had preordered an E-P1 with the 17mm kit lens and viewfinder from Amazon. After handling the E-P1 and taking a few test shots, I canceled my order.

Fuji GF670 first impressions

Fuji GF670I just received my Fuji GF670 from Dirk Rösler at Japan Exposures. This is a folding medium-format rangefinder camera, an anachronism in many respects, but I regret not getting a G690 when they were still made and since this is a limited edition (apparently quite a popular one at that), I went ahead. I have not yet shot a roll, but here are my first impressions:

  • The unfolding mechanism is a bit finnicky. You have to be careful to get the front standard aligned with the film plane. Once deployed it seems fairly stable. Folding it back is also quite tricky.
  • The meter indicator LEDs and controls are very reminiscent of the Epson R-D1, not surprising since both are actually made by Cosina.
  • The leaf shutter is amazingly quiet. It makes a Leica sound like a clunker in comparison.
  • The camera is quite light for MF, it feels lighter than a R-D1 (even though it weighs nearly twice as much) and is not that much larger.
  • It does not exude quality like the Fuji-manufactured TX-2 (Hasselblad XPan II).
  • The rangefinder patch is bright and clear. The RF base length is very short as in a VC Bessa, and will probably not be as precise as a Leica, XPan or Zeiss Ikon.
  • The film loading mechanism is very easy to use, and built as well as other Fuji MF cameras such as the G617.
  • You have to remember to reset the lens to infinity focus in order to fold it.
  • You get a choice of 6×6 and 6×7, 120 and 220.
  • The optional case is a snug fit. I wish it included a belt loop.

In grand old techno-fetishistic tradition, I put up an unboxing gallery.

Update (2009-08-27):

I have finally uploaded a gallery of my first test roll from the camera. The lens’ optical quality is outstanding, unlike most older folders (well, apart from the Plaubel Makina, of course).

Anthony’s Cookies grand opening

Anthony

Another gourmet treats shop joined the burgeoning scene in the Mission. Anthony’s Cookies opened today to a line that stretched around the corner.

Opening

As one of the officials present said, it takes courage to start a business in this economic climate. Specially in as business-hostile a city as San Francisco, if I may add.

Anthony

Inside the store was a buzzing hive of activity, with the eponymous proprietor busy preparing batches of free cookies for the awaiting hordes. At $5 for a half dozen, these cookies are a steal. I tried the double chocolate chip, it came fresh from the oven and had a strong chocolate aroma and the right texture. All in all, a great addition to a neighborhood that already has more than its share of good places to indulge a sweet tooth. I added him to my Google map of recommended bakeries, ice cream parlors and sweet shops in San Francisco.