[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# IPython Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# dotenv\n.env\n\n# virtualenv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n\n.DS_Store\n"
  },
  {
    "path": "README.md",
    "content": "\n**bv** is a small tool to quickly view high-resolution multi-band imagery\ndirectly in your [iTerm 2](https://www.iterm2.com). It was designed for\nvisualising very large images located on a remote machine over a low-bandwidth\nconnection. It subsamples and compresses the image sends it over the wire as a\nbase64-encoded PNG (hence the name \"bv\") that iTerm 2 inlines in your terminal.\n\n<img src=\"https://github.com/daleroberts/bv/raw/master/docs/trump.png\" width=\"800\">\n\nNow, go and compare the above to [old-school rendering](https://camo.githubusercontent.com/a6c791a0b4d97315d00b6592f918fe744abe00e6/687474703a2f2f692e696d6775722e636f6d2f556e666e704d722e706e67)\nor my other tool [tv](https://github.com/daleroberts/tv). Welcome to 2017!\n\n# Some Examples\n\nHere are a number of examples that show how this tool can be used.\n\n## Big image over small connection\n\nDisplay a 3.5 billion pixel single-band image (3.3GB) using only 467KB over a SSH connection.\n\n<img src=\"https://github.com/daleroberts/bv/raw/master/docs/bigimg.png\" width=\"800\">\n\n## Different band combinations\n\nDisplay a six-band image (7.2GB) using only 1.1MB over a SSH connection. Here,\nwe put bands 5-4-3 into the RGB channels using `-b 5 -b 4 -b 3` (ordering\nmatters) and set the width of the output image to be 600 pixels using `-w 600`.\n\n<img src=\"https://github.com/daleroberts/bv/raw/master/docs/bands.png\" width=\"800\">\n\nYou can also specify a single band to display (e.g., `-b 1`).\n\n## Subset images\n\nYou can subset images using `gdal_translate` syntax which is `-srcwin xoff yoff\nxsize ysize`. For example, only displaying a small 1000x1000 area of the same large image above.\n\n<img src=\"https://github.com/daleroberts/bv/raw/master/docs/subset.png\" width=\"800\">\n\nThis allows you to quickly identify regions of your image and then paste the same options \ninto `gdal_translate` to complete your desired workflow. For example:\n```\nremote$ gdal_translate tasmania-2014.tif -b 5 -b 4 -b 3 -srcwin 12000 11000 1000 1000 -of PNG -ot UInt16 -scale 0 4000 ~/out.png\nInput file size is 20000, 16000\n0...10...20...30...40...50...60...70...80...90...100 - done.\nremote$\n```\n\n## Machine learning multi-class outputs with different color maps\n\nSometimes you might have a single-band image that only contains classes\n(integers). Different color maps can be applied to these single-band images\nusing the `-cm` option and any choice from [matplotlib's\ncolormaps](http://matplotlib.org/examples/color/colormaps_reference.html).\n\n<img src=\"https://github.com/daleroberts/bv/raw/master/docs/colors.png\" width=\"800\">\n\n## URLs\n\nThe **bv** tool can read from URLs (see the Trump image above). It can also\nparse URLs on `stdin`, this allows you to [do\nthings](https://github.com/developmentseed/landsat-util) like this to quicky\ndisplay available Landsat images roughly over Dubai.\n\n```\nremote$ landsat search --lat 25 --lon 55 --latest 3 | bv -urls -\n```\n\n## Standard Input\n\nFilenames can be read from `stdin`. For example:\n```\nls -1 *.tif | bv -w 100 -\n```\n\n## Compression\n\nThe level of compression can be changed using the `-zlevel` option (0-9).\n\n## Stacking images\n\nIf your bands are located in seperate images then you can stack them and display them\nin the RGB channels using\n```\nbv -stack RED.tif GREEN.tif BLUE.tif\n```\n\nThere is also the `-revstack` option to do it in reverse order.\n\n## Subsampling algorithm\n\nThe subsampling algorithm can be changed using the `-r` option (same syntax as GDAL). The available subsamplings are:\n- Nearest\n- Average\n- Cubic Spline\n- Cubic\n- Mode\n- Lanczos\n- Bilinear\n\n## Alpha channel\n\nFor single-band images, you can specify the color value to set as the alpha\nchannel. This is sometimes useful for machine learning outputs where you want\nto not display certain classes. You can add multiple of these with different\nvalues.\n\n## PDF, EPS, and PNG\n\nThe **bv** tool will display PDF, EPS, and PNG output inline with out any\nchanges to those files. If you want to disable this behaviour you can pass the\n`-nop` option allow GDAL to subsample, etc.\n\n## TMUX Support\n\n<img src=\"https://github.com/daleroberts/bv/raw/master/docs/tmux.png\" width=\"800\">\n\n# Configuration\n\nYou can save your default configuration by setting an alias in your `~/.profile` file. For example, I do:\n```\nalias bv='bv -w 800'\n```\n\n# Installation\n\nIt is just a single-file script so all you'll need to do it put it in your\n`PATH`. Dependencies are Python 3, GDAL 2, Numpy, Matplotlib, and iTerm 2. I've\nfound that the best way to install these dependencies are: \n```bash\n# Python 3\nbrew install python3\n\n# Numpy and matplotlib\npip3 install numpy matplotlib\n\n# GDAL 2\nbrew install gdal --HEAD --without-python\npip3 install gdal\n```\n"
  },
  {
    "path": "bv",
    "content": "#!/usr/bin/env python3\n\"\"\"\nbv: Quickly view hyperspectral imagery, satellite imagery, and \nmachine learning image outputs directly in your iTerm2 terminal.\n\nDale Roberts <dale.o.roberts@gmail.com>\n\nhttp://www.github.com/daleroberts/bv\n\"\"\"\nimport numpy as np\nimport shutil\nimport gdal\nimport sys\nimport os\nimport re\n\nfrom urllib.request import urlopen, URLError\nfrom os.path import splitext\nfrom base64 import b64encode\nfrom uuid import uuid4\n\ngdal.UseExceptions()\n\nSAMPLING = {'nearest': gdal.GRIORA_NearestNeighbour,\n            'bilinear': gdal.GRIORA_Bilinear,\n            'cubic': gdal.GRIORA_Cubic,\n            'cubicspline': gdal.GRIORA_Cubic,\n            'lanczos': gdal.GRIORA_Lanczos,\n            'average': gdal.GRIORA_Average,\n            'mode': gdal.GRIORA_Mode}\nRE_URL = 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'\nTMUX = os.getenv('TERM','').startswith('screen')\n\ndef sizefmt(num, suffix='B'):\n    for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:\n        if abs(num) < 1024.0:\n            return '%3.1f%s%s' % (num, unit, suffix)\n        num /= 1024.0\n    return '%.1f%s%s' % (num, 'Yi', suffix)\n\n\ndef typescale(data, dtype=np.uint8, scale=None):\n    typeinfo = np.iinfo(dtype)\n    low, high = typeinfo.min, typeinfo.max\n    if scale:\n        cmin, cmax = scale\n    else:\n        cmin, cmax = np.min(data), np.max(data)\n    cscale = cmax - cmin\n    scale = float(high - low) / cscale\n    typedata = (data * 1.0 - cmin) * scale + 0.4999\n    with np.errstate(all='ignore'):\n        typedata[typedata < low] = low\n        typedata[typedata > high] = high\n    return typedata.astype(dtype) + np.cast[dtype](low)\n\n\ndef imgcat(data, lines=-1):\n    if TMUX:\n        if lines == -1:\n            lines = 10\n        osc = b'\\033Ptmux;\\033\\033]'\n        st = b'\\a\\033\\\\'\n    else:\n        osc = b'\\033]'\n        st = b'\\a'\n    csi = b'\\033['\n    buf = bytes()\n    if lines > 0:\n        buf += lines*b'\\n' + csi + b'?25l' + csi + b'%dF' % lines + osc\n        dims = b'width=auto;height=%d;preserveAspectRatio=1' % lines\n    else:\n        buf += osc\n        dims = b'width=auto;height=auto'\n    buf += b'1337;File=;size=%d;inline=1;' % len(data) + dims + b':'\n    buf += b64encode(data) + st\n    if lines > 0:\n        buf += csi + b'%dE' % lines + csi + b'?25h'\n    sys.stdout.buffer.write(buf)\n    sys.stdout.flush()\n    print()\n\n\ndef show(rbs, xoff, yoff, ow, oh, w=500, h=500, r='average', zlevel=1,\n         cm='bone', alpha=None, scale=None, quiet=None, lines=-1):\n    memdriver = gdal.GetDriverByName('MEM')\n    if len(rbs) == 1:\n        if alpha is None:\n            md = memdriver.Create('', w, h, 3, gdal.GDT_UInt16)\n        else:\n            md = memdriver.Create('', w, h, 4, gdal.GDT_UInt16)\n        bnd = rbs[0].ReadAsArray(xoff, yoff, ow, oh, buf_xsize=w,\n                                 buf_ysize=h, resample_alg=SAMPLING[r])\n        try:\n            import matplotlib.cm as cms\n            cm = getattr(cms, cm)\n        except AttributeError:\n            print('incorrect colormap, defaulting to \"bone\"')\n            cm = getattr(cms, 'bone')\n        dmin, dmax = bnd.min(), bnd.max()\n        bnds = cm((bnd - dmin) / (dmax - dmin))\n        for i in range(3):\n            obnd = md.GetRasterBand(i + 1)\n            obnd.WriteArray(typescale(bnds[:, :, i], np.uint16), 0, 0)\n        if alpha is not None:\n            obnd = md.GetRasterBand(4)\n            mask = np.logical_and.reduce([bnd != n for n in alpha])\n            obnd.WriteArray((65535 * mask).astype(np.uint16), 0, 0)\n            obnd.SetColorInterpretation(gdal.GCI_AlphaBand)\n    else:\n        if len(rbs) == 4 or alpha is not None:  # RGBA\n            md = memdriver.Create('', w, h, 4, gdal.GDT_UInt16)\n        else:  # RGB\n            md = memdriver.Create('', w, h, 3, gdal.GDT_UInt16)\n            rbs = rbs[:3]\n        for i, b in enumerate(rbs):\n            bnd = b.ReadAsArray(xoff, yoff, ow, oh, buf_xsize=w,\n                                buf_ysize=h, resample_alg=SAMPLING[r])\n            obnd = md.GetRasterBand(i + 1)\n            obnd.WriteArray(typescale(bnd, np.uint16, scale), 0, 0)\n            if i == 3:  # alpha\n                obnd.SetColorInterpretation(gdal.GCI_AlphaBand)\n        if alpha is not None:\n            obnd = md.GetRasterBand(4)\n            mask = np.logical_and.reduce([bnd != n for n in alpha])\n            obnd.WriteArray((65535 * mask).astype(np.uint16), 0, 0)\n            obnd.SetColorInterpretation(gdal.GCI_AlphaBand)\n\n    if zlevel is None:\n        zlevel = 'ZLEVEL=1'\n    else:\n        zlevel = 'ZLEVEL={}'.format(zlevel)\n\n    mmapfn = \"/vsimem/\" + uuid4().hex\n    driver = gdal.GetDriverByName('PNG')\n    fd = driver.CreateCopy(mmapfn, md, 0, [zlevel])\n\n    size = gdal.VSIStatL(mmapfn, gdal.VSI_STAT_SIZE_FLAG).size\n    fd = gdal.VSIFOpenL(mmapfn, 'rb')\n    data = gdal.VSIFReadL(1, size, fd)\n    gdal.VSIFCloseL(fd)\n\n    imgcat(data, lines)\n\n    gdal.Unlink(mmapfn)\n\n    return size\n\n\ndef show_stacked(imgs, *args, **kwargs):\n    b = kwargs.pop('b')\n\n    fds = [gdal.Open(fd) for fd in imgs[:3]]\n    rbs = [fd.GetRasterBand(1) for fd in fds]\n\n    quiet = kwargs.pop('quiet')\n    srcwin = kwargs.pop('srcwin')\n    if srcwin is not None:\n        xoff, yoff, ow, oh = srcwin\n    else:\n        xoff, yoff, ow, oh = 0, 0, fds[0].RasterXSize, fds[0].RasterYSize\n\n    kwargs['h'] = int(oh / ow * kwargs['w'])\n\n    size = show(rbs, xoff, yoff, ow, oh, **kwargs)\n\n    fd = fds[0]\n    geo = fd.GetGeoTransform()\n    if not quiet:\n        desc = '{}x{} pixels / {} bands.  [tfr: {}]'\n        print(desc.format(fd.RasterYSize, fd.RasterXSize,\n                          fd.RasterCount, sizefmt(size)))\n\n\ndef show_fd(fd, *args, **kwargs):\n    b = kwargs.pop('b')\n    rc = fd.RasterCount\n\n    if rc == 1:\n        rbs = [fd.GetRasterBand(1)]\n    else:\n        if b is None:\n            if rc == 4:\n                b = range(1, 5)\n            else:\n                b = range(1, 4)\n        rbs = [fd.GetRasterBand(i) for i in b]\n\n    srcwin = kwargs.pop('srcwin')\n    if srcwin is not None:\n        xoff, yoff, ow, oh = srcwin\n    else:\n        xoff, yoff, ow, oh = 0, 0, fd.RasterXSize, fd.RasterYSize\n\n    kwargs['h'] = int(oh / ow * kwargs['w'])\n\n    return show(rbs, xoff, yoff, ow, oh, **kwargs)\n\n\ndef show_fn(fn, *args, **kwargs):\n    try:\n        quiet = kwargs.pop('quiet')\n        fd = gdal.Open(fn)\n        size = show_fd(fd, *args, **kwargs)\n        geo = fd.GetGeoTransform()\n        if not quiet:\n            desc = '{}x{} pixels / {} bands.  [tfr: {}]'\n            print(desc.format(fd.RasterYSize, fd.RasterXSize,\n                              fd.RasterCount, sizefmt(size)))\n    except RuntimeError as e:\n        print('Error:', e)\n        sys.exit(1)\n    except TypeError:\n        print('Error: bad data. incorrect srcwin?')\n        sys.exit(1)\n\n\ndef show_url(url, *args, **kwargs):\n    try:\n        urlfd = urlopen(url, timeout=15)\n        mmapfn = \"/vsimem/\" + uuid4().hex\n        gdal.FileFromMemBuffer(mmapfn, urlfd.read())\n        return show_fd(gdal.Open(mmapfn), *args, **kwargs)\n    except URLError as e:\n        print(e)\n    finally:\n        gdal.Unlink(mmapfn)\n\n\nif __name__ == '__main__':\n    import argparse\n    parser = argparse.ArgumentParser()\n    parser.add_argument('-w', type=int, default=800)\n    parser.add_argument('-b', action='append', type=int)\n    parser.add_argument('-r', choices=SAMPLING.keys(), default='nearest')\n    parser.add_argument('-cm', default=\"bone\")\n    parser.add_argument('-zlevel', type=int)\n    parser.add_argument('-scale', nargs=2, type=float, metavar=('minval', 'maxval'))\n    parser.add_argument('-alpha', action='append', type=int)\n    parser.add_argument('-quiet', action='store_true')\n    parser.add_argument('-stack', action='store_true')\n    parser.add_argument('-revstack', action='store_true')\n    parser.add_argument('-urls', action='store_true')\n    parser.add_argument('-nofn', action='store_true')\n    parser.add_argument('-nopassthrough', action='store_true')\n    parser.add_argument('-lines', type=int, default=-1)\n    parser.add_argument('-srcwin', nargs=4,\n                        metavar=('xoff', 'yoff', 'xsize', 'ysize'),\n                        type=int)\n    parser.add_argument('img', nargs='+')\n    kwargs = vars(parser.parse_args())\n\n    imgs = kwargs.pop('img')\n    urls = kwargs.pop('urls')\n    nofn = kwargs.pop('nofn') or (imgs[0] != '-' and len(imgs) == 1)\n    stack = kwargs.pop('stack')\n    revstack = kwargs.pop('revstack')\n    nop = kwargs.pop('nopassthrough')\n\n    if TMUX:\n        # dirty hack to make tmux integration work\n        kwargs['w'] = min(kwargs['w'], 370)\n\n    try:\n        if not sys.stdin.isatty() or imgs[0] == '-':\n            imgs = [line.strip() for line in sys.stdin.readlines()]\n\n        if stack:\n            show_stacked(imgs, **kwargs)\n            sys.exit(0)\n\n        if revstack:\n            show_stacked(list(reversed(imgs)), **kwargs)\n            sys.exit(0)\n\n        for img in imgs:\n            if urls:\n                for url in re.findall(RE_URL, img):\n                    if not nofn:\n                        print(url)\n                    show_url(url, **kwargs)\n            else:\n                if not nofn:\n                    print(img)\n                if not nop and splitext(img)[1][1:].lower() in ['png', 'pdf', 'eps']:\n                    with open(img, 'rb') as fd:\n                        data = fd.read()\n                        imgcat(data, kwargs.pop('lines', -1))\n                else:\n                    show_fn(img, **kwargs)\n\n    except KeyboardInterrupt:\n        pass\n\n    finally:\n        print()\n"
  }
]