Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .coveragerc

This file was deleted.

2 changes: 0 additions & 2 deletions .coveralls.yml

This file was deleted.

40 changes: 40 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Publish to PyPI

on:
push:
tags: ["v*"]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Build sdist and wheel
run: |
pip install build
python -m build

- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write

steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- uses: pypa/gh-action-pypi-publish@release/v1
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Tests

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install package with test dependencies
run: pip install -e .[test]

- name: Run tests
run: pytest -v
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ docs/_build/
target/

.DS_Store
test.html
test.html
.pytest_cache/
47 changes: 0 additions & 47 deletions .travis.yml

This file was deleted.

93 changes: 55 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,80 @@
visual-logging
==============

A simple way to generate beautiful html logs with embedded images for CV purposes.

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.

Now you can add OpenCV images (well technically it's numpy arrays), PIL images and matplotlib graphs to your logs.
[![Tests](https://github.com/dchaplinsky/visual-logging/actions/workflows/tests.yml/badge.svg)](https://github.com/dchaplinsky/visual-logging/actions/workflows/tests.yml)
[![PyPI](https://img.shields.io/pypi/v/visual-logging.svg)](https://pypi.org/project/visual-logging/)

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
A simple way to generate beautiful html logs with embedded images for CV purposes.

visual-logging piggybacks on the standard `logging` module and allows you to use the sick power of logging to debug your computer vision application on a whole new level: OpenCV images (well, technically numpy arrays), PIL images and matplotlib figures get embedded into your html logs as inline images.

[![Build Status](https://travis-ci.org/dchaplinsky/visual-logging.svg?branch=master)](https://travis-ci.org/dchaplinsky/visual-logging)
[![Coverage Status](https://coveralls.io/repos/dchaplinsky/visual-logging/badge.png)](https://coveralls.io/r/dchaplinsky/visual-logging)
You can read about it in detail 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.

## Installation
```pip install visual-logging```

No extra dependencies
```
pip install visual-logging
```

No extra dependencies — whichever of OpenCV, PIL/Pillow and matplotlib you already have installed are picked up automatically (numpy arrays render through PIL when OpenCV isn't around). Requires Python 3.9+.

## Usage example (see demo.py)

```python
from logging import FileHandler
from vlogging import VisualRecord
import logging
from vlogging import HTMLFileHandler, VisualRecord

if __name__ == '__main__':
import cv2
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import cv2 # or PIL.Image, or matplotlib — whatever you use

# evenly sampled time at 200ms intervals
t = np.arange(0., 5., 0.2)
logger = logging.getLogger("demo")
logger.setLevel(logging.DEBUG)
logger.addHandler(HTMLFileHandler("test.html", title="My debug log"))

fig1 = plt.figure()
plt.plot(t, t, 'r--', t, t ** 2, 'bs', t, t ** 3, 'g^')
cv_image = cv2.imread("lenna.jpg")

cv_image = cv2.imread('lenna.jpg')
pil_image = Image.open('lenna.jpg')
logger.debug(VisualRecord(
"Hello from OpenCV", cv_image, "This is an OpenCV image", fmt="png"))

import logging
logger = logging.getLogger("demo")
fh = FileHandler('test.html', mode="w")
# Mix images of different origins in one record, downscale the big ones
# to at most 320x240 to keep the log small:
logger.warning(VisualRecord(
"Hello from all", [cv_image, pil_image, mpl_figure],
fmt="png", max_size=(320, 240)))

logger.setLevel(logging.DEBUG)
logger.addHandler(fh)
# Ordinary log calls work too, and land in the same page:
logger.info("Processed frame %d", 42)

logger.debug(VisualRecord(
"Hello from OpenCV", cv_image, "This is openCV image", fmt="png"))
logging.shutdown() # flushes and closes the html file
```

logger.info(VisualRecord(
"Hello from PIL", pil_image, "This is PIL image", fmt="jpeg"))
Open `test.html` in a browser and enjoy: `HTMLFileHandler` writes a styled, self-contained page — records are color-coded by log level with timestamps and logger names, plain messages are escaped, and exceptions logged with `logger.exception(...)` include their traceback. Records are flushed as they happen, so you can watch the page mid-run.

logger.info(VisualRecord(
"Hello from pylab", fig1, "This is PyLab graph", fmt="png"))
Everything composes the stdlib way: `HTMLFileHandler` is a `logging.FileHandler` that installs a `VisualFormatter` (a `logging.Formatter`) by default — use either piece on its own if you prefer. Passing a `VisualRecord` to a plain `FileHandler` still produces bare html fragments, exactly as in 1.x.

logger.warning(
VisualRecord("Hello from all", [cv_image, pil_image, fig1],
fmt="png"))
In **Jupyter**, a `VisualRecord` displays itself inline — no logging setup needed:

```python
VisualRecord("Detected edges", edges_img, "Canny output")
```

You can check generated html [here](http://dchaplinsky.github.io/visual-logging/)
### `VisualRecord` arguments

| Argument | Meaning |
| --- | --- |
| `title` | Header of the log record |
| `imgs` | A single image or a list of images: OpenCV/numpy arrays, PIL images and matplotlib figures in any combination |
| `footnotes` | Optional text rendered as `<pre>` under the images |
| `fmt` | Image format to embed: `png` (default), `jpeg`, `webp` — anything your imaging library can encode |
| `max_size` | Optional `(width, height)` tuple: images bigger than that are downscaled proportionally before embedding (matplotlib figures by lowering the render dpi), to keep log files readable and small |

## Changelog

**2.0**
- Modern packaging (`pyproject.toml`), Python 3.9+ only, `py.typed` type hints
- New `HTMLFileHandler` + `VisualFormatter`: styled, self-contained html pages with level colors, timestamps, escaped plain-text records and tracebacks
- New `max_size` option to downscale embedded images (all renderers)
- `VisualRecord` displays inline in Jupyter notebooks
- numpy arrays render via PIL when OpenCV is not installed
- Embedded images use `loading="lazy"`, so huge logs open fast
- matplotlib support no longer relies on the deprecated `pylab` module
- Tests run on GitHub Actions against Python 3.9–3.13; releases publish to PyPI from tags via trusted publishing
42 changes: 29 additions & 13 deletions demo.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
from logging import FileHandler
from vlogging import VisualRecord
import logging
from pathlib import Path

if __name__ == '__main__':
from vlogging import HTMLFileHandler, VisualRecord

if __name__ == "__main__":
import cv2
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image

lenna = str(Path(__file__).parent / "vlogging" / "tests" / "lenna.jpg")

# evenly sampled time at 200ms intervals
t = np.arange(0., 5., 0.2)

fig1 = plt.figure()
plt.plot(t, t, 'r--', t, t ** 2, 'bs', t, t ** 3, 'g^')

cv_image = cv2.imread('vlogging/tests/lenna.jpg')
pil_image = Image.open('vlogging/tests/lenna.jpg')
cv_image = cv2.imread(lenna)
pil_image = Image.open(lenna)

import logging
logger = logging.getLogger("demo")
fh = FileHandler('test.html', mode="w")

logger.setLevel(logging.DEBUG)
logger.addHandler(fh)
logger.addHandler(HTMLFileHandler("test.html", title="visual-logging demo"))

logger.debug(VisualRecord(
"Hello from OpenCV", cv_image, "This is openCV image", fmt="png"))
"Hello from OpenCV", cv_image, "This is OpenCV image", fmt="png"))

logger.info(VisualRecord(
"Hello from PIL", pil_image, "This is PIL image", fmt="jpeg"))

logger.info(VisualRecord(
"Hello from pylab", fig1, "This is PyLab graph", fmt="png"))
"Hello from matplotlib", fig1, "This is matplotlib graph", fmt="png"))

logger.warning(
VisualRecord("Hello from all", [cv_image, pil_image, fig1],
fmt="png"))

logger.warning(
VisualRecord("Hello from all (downscaled to fit 200x200)",
[cv_image, pil_image, fig1],
fmt="png", max_size=(200, 200)))

logger.info("Plain text records work too, and are escaped: <html> & so on")

try:
1 / 0
except ZeroDivisionError:
logger.exception(VisualRecord(
"Exceptions come with tracebacks", cv_image))

logging.shutdown()
3 changes: 0 additions & 3 deletions devrequirements.txt

This file was deleted.

49 changes: 49 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "visual-logging"
dynamic = ["version"]
description = "A simple way to generate beautiful html logs with embedded images for CV purposes."
readme = "README.md"
license = "MIT"
authors = [
{name = "Dmitry Chaplinsky", email = "chaplinsky.dmitry@gmail.com"},
]
requires-python = ">=3.9"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Image Processing",
"Topic :: System :: Logging",
]

[project.urls]
Homepage = "https://github.com/dchaplinsky/visual-logging"
Issues = "https://github.com/dchaplinsky/visual-logging/issues"

[project.optional-dependencies]
test = [
"pytest",
"numpy",
"opencv-python-headless",
"Pillow",
"matplotlib",
]

[tool.setuptools]
packages = ["vlogging"]

[tool.setuptools.package-data]
vlogging = ["py.typed"]

[tool.setuptools.dynamic]
version = {attr = "vlogging.__version__"}
Loading
Loading