├── .coveragerc ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo.py ├── devrequirements.txt ├── setup.py └── vlogging ├── __init__.py └── tests ├── basic.py ├── corner_cases.py ├── dummies ├── PIL.py ├── __init__.py ├── cv2.py └── pylab.py └── lenna.jpg /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */python?.?/* 4 | */lib-python/?.?/*.py 5 | */lib_pypy/*.py 6 | */site-packages/nose/* 7 | */unittest2/* 8 | */pyshared/* 9 | */dist-packages/* 10 | */tests/* 11 | */venv/* 12 | demo.py 13 | setup.py 14 | 15 | [report] 16 | exclude_lines = 17 | # Have to re-enable the standard pragma 18 | pragma: no cover -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # .coveralls.yml 2 | repo_token: GzsXpWT6B1llGwECNpJAyzcpXB6KK89Sq -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | .DS_Store 57 | test.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | virtualenv: 3 | system_site_packages: true 4 | 5 | cache: 6 | apt: true 7 | directories: 8 | - /home/travis/virtualenv/python2.7/lib/python2.7/site-packages 9 | 10 | before_install: 11 | - sudo apt-get update 12 | - sudo apt-get install -qq zlib1g-dev build-essential python-numpy 13 | - wget http://prdownloads.sourceforge.net/libpng/libpng-1.5.12.tar.gz 14 | - tar xzf libpng-1.5.12.tar.gz 15 | - rm libpng-1.5.12.tar.gz 16 | - pushd libpng-1.5.12 17 | - ./configure --prefix=/usr/local 18 | - make 19 | - sudo make install 20 | - sudo ldconfig 21 | - popd 22 | - curl -sL https://github.com/Itseez/opencv/archive/2.4.9.zip > opencv.zip 23 | - unzip opencv.zip 24 | - rm opencv.zip 25 | - mkdir opencv-build 26 | - pushd opencv-build/ 27 | - cmake -DCMAKE_BUILD_TYPE=RELEASE -DBUILD_DOCS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_opencv_java=OFF -DBUILD_JASPER=ON -DWITH_JASPER=ON -DBUILD_ZLIB=ON -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DWITH_OPENEXR=OFF -DBUILD_PNG=ON -DWITH_PNG=ON -DWITH_TIFF=ON -DBUILD_TIFF=ON -DWITH_WEBP=OFF -DWITH_JPEG=ON -DBUILD_JPEG=ON ../opencv-2.4.9/ 28 | - sudo make install 29 | - popd 30 | 31 | python: 32 | - "2.7" 33 | # - "3.4" 34 | # - "3.3" 35 | 36 | install: 37 | - pip install -r devrequirements.txt 38 | 39 | # command to run tests 40 | script: nosetests -vs vlogging/tests/corner_cases.py vlogging/tests/basic.py --with-isolation --with-coverage --cover-erase --cover-package=. 41 | 42 | notifications: 43 | slack: unshredit:2nm3MUsOgq9Bz9bqDPAOPn7S 44 | 45 | after_success: 46 | - coveralls 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dmitry Chaplinsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | visual-logging 2 | ============== 3 | 4 | A simple way to generate beautiful html logs with embedded images for CV purposes. 5 | 6 | visual-logging piggy backs on logging module and allows you to use sick power of logging to debug your computer vision application on a whole new level. 7 | 8 | Now you can add OpenCV images (well technically it's numpy arrays), PIL images and matplotlib graphs to your logs. 9 | 10 | You can read about it in details in a great blog post [visual-logging, my new favorite tool for debugging OpenCV and Python apps](http://www.pyimagesearch.com/2014/12/22/visual-logging-new-favorite-tool-debugging-opencv-python-apps/) written by Adrian Rosebrock 11 | 12 | 13 | [![Build Status](https://travis-ci.org/dchaplinsky/visual-logging.svg?branch=master)](https://travis-ci.org/dchaplinsky/visual-logging) 14 | [![Coverage Status](https://coveralls.io/repos/dchaplinsky/visual-logging/badge.png)](https://coveralls.io/r/dchaplinsky/visual-logging) 15 | 16 | ## Installation 17 | ```pip install visual-logging``` 18 | 19 | No extra dependencies 20 | 21 | ## Usage example (see demo.py) 22 | ```python 23 | from logging import FileHandler 24 | from vlogging import VisualRecord 25 | 26 | if __name__ == '__main__': 27 | import cv2 28 | from PIL import Image 29 | import numpy as np 30 | import matplotlib.pyplot as plt 31 | 32 | # evenly sampled time at 200ms intervals 33 | t = np.arange(0., 5., 0.2) 34 | 35 | fig1 = plt.figure() 36 | plt.plot(t, t, 'r--', t, t ** 2, 'bs', t, t ** 3, 'g^') 37 | 38 | cv_image = cv2.imread('lenna.jpg') 39 | pil_image = Image.open('lenna.jpg') 40 | 41 | import logging 42 | logger = logging.getLogger("demo") 43 | fh = FileHandler('test.html', mode="w") 44 | 45 | logger.setLevel(logging.DEBUG) 46 | logger.addHandler(fh) 47 | 48 | logger.debug(VisualRecord( 49 | "Hello from OpenCV", cv_image, "This is openCV image", fmt="png")) 50 | 51 | logger.info(VisualRecord( 52 | "Hello from PIL", pil_image, "This is PIL image", fmt="jpeg")) 53 | 54 | logger.info(VisualRecord( 55 | "Hello from pylab", fig1, "This is PyLab graph", fmt="png")) 56 | 57 | logger.warning( 58 | VisualRecord("Hello from all", [cv_image, pil_image, fig1], 59 | fmt="png")) 60 | 61 | ``` 62 | 63 | You can check generated html [here](http://dchaplinsky.github.io/visual-logging/) 64 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from logging import FileHandler 2 | from vlogging import VisualRecord 3 | 4 | if __name__ == '__main__': 5 | import cv2 6 | from PIL import Image 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | 10 | # evenly sampled time at 200ms intervals 11 | t = np.arange(0., 5., 0.2) 12 | 13 | fig1 = plt.figure() 14 | plt.plot(t, t, 'r--', t, t ** 2, 'bs', t, t ** 3, 'g^') 15 | 16 | cv_image = cv2.imread('vlogging/tests/lenna.jpg') 17 | pil_image = Image.open('vlogging/tests/lenna.jpg') 18 | 19 | import logging 20 | logger = logging.getLogger("demo") 21 | fh = FileHandler('test.html', mode="w") 22 | 23 | logger.setLevel(logging.DEBUG) 24 | logger.addHandler(fh) 25 | 26 | logger.debug(VisualRecord( 27 | "Hello from OpenCV", cv_image, "This is openCV image", fmt="png")) 28 | 29 | logger.info(VisualRecord( 30 | "Hello from PIL", pil_image, "This is PIL image", fmt="jpeg")) 31 | 32 | logger.info(VisualRecord( 33 | "Hello from pylab", fig1, "This is PyLab graph", fmt="png")) 34 | 35 | logger.warning( 36 | VisualRecord("Hello from all", [cv_image, pil_image, fig1], 37 | fmt="png")) 38 | -------------------------------------------------------------------------------- /devrequirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==1.4.1 2 | Pillow==2.5.1 3 | coveralls -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from vlogging import __version__ 3 | 4 | setup( 5 | name='visual-logging', 6 | version=__version__, 7 | url='https://github.com/dchaplinsky/visual-logging', 8 | license='MIT', 9 | author='Dmitry Chaplinsky', 10 | author_email='chaplinsky.dmitry@gmail.com', 11 | packages=["vlogging"], 12 | description="A simple way to generate beautiful html " 13 | "logs with embedded images for CV purposes.", 14 | platforms='any' 15 | ) 16 | -------------------------------------------------------------------------------- /vlogging/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from io import BytesIO as StringIO 4 | from string import Template 5 | import base64 6 | 7 | __version__ = "1.0" 8 | renderers = [] 9 | 10 | try: 11 | import cv2 12 | import numpy 13 | 14 | def render_opencv(img, fmt="png"): 15 | if not isinstance(img, numpy.ndarray): 16 | return None 17 | 18 | retval, buf = cv2.imencode(".%s" % fmt, img) 19 | if not retval: 20 | return None 21 | 22 | return buf, "image/%s" % fmt 23 | 24 | renderers.append(render_opencv) 25 | except ImportError: 26 | pass 27 | 28 | try: 29 | from PIL import Image 30 | 31 | def render_pil(img, fmt="png"): 32 | if not callable(getattr(img, "save", None)): 33 | return None 34 | 35 | output = StringIO() 36 | img.save(output, format=fmt) 37 | contents = output.getvalue() 38 | output.close() 39 | 40 | return contents, "image/%s" % fmt 41 | 42 | renderers.append(render_pil) 43 | except ImportError: 44 | pass 45 | 46 | try: 47 | import pylab 48 | 49 | def render_pylab(img, fmt="png"): 50 | if not callable(getattr(img, "savefig", None)): 51 | return None 52 | 53 | output = StringIO() 54 | img.savefig(output, format=fmt) 55 | contents = output.getvalue() 56 | output.close() 57 | 58 | return contents, "image/%s" % fmt 59 | 60 | renderers.append(render_pylab) 61 | except ImportError: 62 | pass 63 | 64 | 65 | class VisualRecord(object): 66 | def __init__(self, title="", imgs=None, footnotes="", fmt="png"): 67 | self.title = title 68 | self.fmt = fmt 69 | 70 | if imgs is None: 71 | imgs = [] 72 | 73 | self.imgs = imgs 74 | 75 | if not isinstance(imgs, (list, tuple, set, frozenset)): 76 | self.imgs = [self.imgs] 77 | 78 | self.footnotes = footnotes 79 | 80 | def render_images(self): 81 | rendered = [] 82 | 83 | for img in self.imgs: 84 | for renderer in renderers: 85 | # Trying renderers we have one by one 86 | res = renderer(img, self.fmt) 87 | 88 | if res is None: 89 | continue 90 | else: 91 | rendered.append(res) 92 | break 93 | 94 | return "".join( 95 | Template('').substitute({ 96 | "data": base64.b64encode(data).decode(), 97 | "mime": mime 98 | }) for data, mime in rendered) 99 | 100 | def render_footnotes(self): 101 | if not self.footnotes: 102 | return "" 103 | 104 | return Template("
$footnotes
").substitute({ 105 | "footnotes": self.footnotes 106 | }) 107 | 108 | def __str__(self): 109 | t = Template( 110 | """ 111 |

$title

112 | $imgs 113 | $footnotes 114 |
""") 115 | 116 | return t.substitute({ 117 | "title": self.title, 118 | "imgs": self.render_images(), 119 | "footnotes": self.render_footnotes() 120 | }) 121 | -------------------------------------------------------------------------------- /vlogging/tests/basic.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import sys 4 | 5 | if sys.path[0].endswith("dummies"): 6 | sys.path = sys.path[1:] 7 | 8 | import vlogging 9 | 10 | 11 | class BasicTestCase(unittest.TestCase): 12 | def test_nothing(self): 13 | s = str(vlogging.VisualRecord()) 14 | self.assertTrue("
" in s) 15 | 16 | def test_text_only(self): 17 | s = str(vlogging.VisualRecord(title="title", footnotes="footnotes")) 18 | self.assertTrue("title" in s) 19 | self.assertTrue("footnotes" in s) 20 | self.assertTrue("
" in s)
 21 | 
 22 |     def test_all_renderers(self):
 23 |         self.assertEqual(len(vlogging.renderers), 3)
 24 | 
 25 |     def test_invalid_images(self):
 26 |         s = str(vlogging.VisualRecord(
 27 |             title="title",
 28 |             imgs="foobar",
 29 |             footnotes="footnotes"))
 30 | 
 31 |         self.assertTrue("title" in s)
 32 |         self.assertTrue("footnotes" in s)
 33 |         self.assertTrue("
" in s)
 34 |         self.assertEqual(s.count("" in s)
 44 |         self.assertEqual(s.count("" in s)
 58 |         self.assertTrue("image/png" in s)
 59 |         self.assertEqual(s.count("" in s)
 89 |         self.assertEqual(s.count("" in s)
121 |         self.assertEqual(s.count("" in s)
140 |         self.assertEqual(s.count("