| .. $URL$ |
| .. $Rev$ |
| |
| PyPNG Code Examples |
| =================== |
| |
| |
| This section discusses some example Python programs that use the png |
| module for reading and writing PNG files. |
| |
| |
| Writing |
| ------- |
| |
| The basic strategy is to create a :class:`~png.Writer` object (instance of |
| :class:`png.Writer`) and then call its :meth:`png.write` method |
| with an open (binary) file, and the pixel data. The :class:`Writer` object |
| encapsulates all the information about the PNG file: image size, colour, |
| bit depth, and so on. |
| |
| A Ramp |
| ^^^^^^ |
| |
| Create a one row image, that has all grey values from 0 to 255. This is |
| a bit like Netpbm's ``pgmramp``. :: |
| |
| import png |
| f = open('ramp.png', 'wb') # binary mode is important |
| w = png.Writer(256, 1, greyscale=True) |
| w.write(f, [range(256)]) |
| f.close() |
| |
| Note that our single row, generated by ``range(256)``, must itself be |
| enclosed in a list. That's because the :meth:`png.write` method expects |
| a list of rows. |
| |
| |
| A Little Message |
| ^^^^^^^^^^^^^^^^ |
| |
| A list of strings holds a graphic in ASCII graphic form. We convert it |
| to a list of integer lists (the required form for the :meth:`write` method), |
| and write it out as a black-and-white PNG (bilevel greyscale). :: |
| |
| import png |
| s = ['110010010011', |
| '101011010100', |
| '110010110101', |
| '100010010011'] |
| s = [[int(c) for c in row] for row in s] |
| |
| w = png.Writer(len(s[0]), len(s), greyscale=True, bitdepth=1) |
| f = open('png.png', 'wb') |
| w.write(f, s) |
| f.close() |
| |
| Note how we use ``len(s[0])`` (the length of the first row) for the *x* |
| argument and ``len(s)`` (the number of rows) for the *y* argument. |
| |
| |
| A Palette |
| ^^^^^^^^^ |
| |
| The previous example, "a little message", can be converted to colour |
| simply by creating a PNG file with a palette. The only difference is |
| that a *palette* argument is passed to the :meth:`write` method instead of |
| ``greyscale=True``:: |
| |
| import png |
| s = ['110010010011', |
| '101011010100', |
| '110010110101', |
| '100010010011'] |
| s = [[int(c) for c in row] for row in s] |
| |
| palette=[(0x55,0x55,0x55), (0xff,0x99,0x99)] |
| w = png.Writer(len(s[0]), len(s), palette=palette, bitdepth=1) |
| f = open('png.png', 'wb') |
| w.write(f, s) |
| |
| Note that the palette consists of two entries (the bit depth is 1 so |
| there are only 2 possible colours). Each entry is an RGB triple. If we |
| wanted transparency then we can use RGBA 4‑tuples for each palette |
| entry. |
| |
| |
| Colour |
| ^^^^^^ |
| |
| For colour images the input rows are generally 3 times as long as |
| for greyscale, because there are 3 channels, RGB, instead of just |
| one, grey. Below, the *p* literal has 2 rows of 9 values (3 RGB |
| pixels per row). The spaces are just for your benefit, to mark out |
| the separate pixels; they have no meaning in the code. :: |
| |
| import png |
| p = [(255,0,0, 0,255,0, 0,0,255), |
| (128,0,0, 0,128,0, 0,0,128)] |
| f = open('swatch.png', 'wb') |
| w = png.Writer(3, 2, greyscale=False) |
| w.write(f, p) |
| f.close() |
| |
| |
| More Colour |
| ^^^^^^^^^^^ |
| |
| A further colour example illustrates some of the manoeuvres you have to |
| perform in Python to get the pixel data in the right format. |
| |
| Say we want to produce a PNG image with 1 row of 8 pixels, with all the |
| colours from a 3‑bit colour system (with 1‑bit for each channel; |
| such systems were common on 8‑bit micros from the 1980s). |
| |
| We produce all possible 3‑bit numbers: |
| |
| >>> list(range(8)) |
| [0, 1, 2, 3, 4, 5, 6, 7] |
| |
| We can convert each number into an RGB triple by assigning bit 0 to |
| blue, bit 1 to red, bit 2 to green (the convention used by a certain |
| 8‑bit micro): |
| |
| >>> [(bool(x&2), bool(x&4), bool(x&1)) for x in _] |
| [(False, False, False), (False, False, True), (True, False, False), |
| (True, False, True), (False, True, False), (False, True, True), (True, |
| True, False), (True, True, True)] |
| |
| (later on we will convert False into 0, and True into 255, so don't |
| worry about that just yet). Here we have each pixel as a tuple. We |
| want to flatten the pixels so that we have just one row. In other words |
| instead of [(R,G,B), (R,G,B), ...] we want [R,G,B,R,G,B,...]. It turns |
| out that ``itertools.chain(*...)`` is just what we need: |
| |
| >>> list(itertools.chain(*_)) |
| [False, False, False, False, False, True, True, False, False, True, |
| False, True, False, True, False, False, True, True, True, True, False, |
| True, True, True] |
| |
| Note that the ``list`` is not necessary, we can usually use the iterator |
| directly instead. I just used ``list`` here so we can see the result. |
| |
| Now to convert False to 0 and True to 255 we can multiply by 255 |
| (Python use's Iverson's convention, so ``False==0``, ``True==1``). |
| We could do that with ``map(lambda x:255*x, _)``. Or, we could use a |
| "magic" bound method: |
| |
| >>> list(map((255).__mul__, _)) |
| [0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, |
| 255, 255, 0, 255, 255, 255] |
| |
| Now we write the PNG file out: |
| |
| >>> p=_ |
| >>> f=open('speccy.png', 'wb') |
| >>> w.write(f, [p]) ; f.close() |
| |
| |
| Random Colour |
| ^^^^^^^^^^^^^ |
| |
| (an example suggested by https://github.com/SuperArcherG) |
| |
| A 64×64 tile with randomly selected RGB colours is written to |
| the file `random.png`. :: |
| |
| # An example program that create a random 64×64 RGB PNG file. |
| # Inspired by https://github.com/drj11/pypng/issues/120 |
| import random |
| |
| import png |
| |
| width = 64 |
| height = 64 |
| |
| # values per row |
| vpr = 3 * width |
| |
| # Create a 2D matrix, a sequence of rows. Each row has vpr values. |
| m = [[0] * vpr for y_ in range(height)] |
| |
| for y in range(len(m)): |
| for x in range(len(m[y])): |
| m[y][x] = random.randint(0, 255) |
| |
| png.from_array(m, "RGB").save("random.png") |
| |
| |
| This example is also available in `exrandom.py` in the `code/` |
| directory. |
| |
| |
| Reading |
| ------- |
| |
| The basic strategy is to create a :class:`~png.Reader` object (a |
| :class:`png.Reader` instance), then call its :meth:`png.read` method |
| to extract the size, and pixel data. |
| |
| |
| Reader |
| ^^^^^^ |
| |
| The :meth:`~png.Reader` constructor can take either a filename, a file-like |
| object, or a sequence of bytes directly. Here we use ``urllib`` to download |
| a PNG file from the internet. |
| |
| >>> r=png.Reader(file=urllib.urlopen('http://www.schaik.com/pngsuite/basn0g02.png')) |
| >>> r.read() |
| (32, 32, <itertools.imap object at 0x10b7eb0>, {'greyscale': True, |
| 'alpha': False, 'interlace': 0, 'bitdepth': 2, 'gamma': 1.0}) |
| |
| The :meth:`png.read` method returns a 4‑tuple consisting of: |
| |
| * `width`: Width of PNG image in pixels; |
| * `height`: Height of PNG image in pixes; |
| * `rows`: A sequence or iterator for the row data; |
| * `info`: An info dictionary containing much of the image |
| metadata. |
| |
| Note that the pixels are returned as an iterator or a sequence. |
| Generally if PyPNG can manage to efficiently return a row |
| iterator then it will, but at other times it will return a |
| sequence. |
| |
| >>> l=list(_[2]) |
| >>> l[0] |
| array('B', [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 0, 0, 0, 0, |
| 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3]) |
| |
| We have extracted the top row of the image. Note that the row itself is |
| an ``array`` (see module ``array``), but in general any suitable sequence |
| type may be returned by :meth:`read`. The values in the row are all |
| integers less than 4, because the image has a bit depth of 2. |
| |
| NumPy |
| ----- |
| |
| `NumPy <http://www.numpy.org/>`_ is a package for scientific computing with Python. |
| It is not part |
| of a standard Python installation, it is |
| `downloaded and installed separately <https://pypi.org/project/numpy/>`_ |
| if needed. |
| Numpy's array manipulation facilities make it good for doing |
| certain type of image processing, and scientific users of NumPy may wish |
| to output PNG files for visualisation. |
| |
| PyPNG does not have any direct integration with NumPy, but the basic |
| data format used by PyPNG, an iterator over rows, is fairly easy to get |
| into two- or three-dimensional NumPy arrays. |
| |
| .. note:: |
| |
| Using a NumPy array in PyPNG mostly just works. |
| Sometimes though you might have a problem. |
| An example is that apparently transposing a NumPy array |
| means that it then |
| `cannot be saved to a PNG using PyPNG <https://github.com/drj11/pypng/issues/91>`_. |
| |
| >>> import numpy |
| >>> a = numpy.array([[1,2,3],[4,5,6]], dtype=numpy.uint8) |
| >>> png.from_array(a, mode="L").save("/tmp/foo.png") # works |
| >>> at = a.transpose() |
| >>> png.from_array(at, mode="L").save("/tmp/foo.png") # does not work |
| |
| When trying to save the transposed array, |
| this currently (2019-03) gives a traceback and the error: |
| ``TypeError: can't set bytearray slice from numpy.ndarray``. |
| |
| That's because in this case the NumPy array cannot be used to |
| extend a Python `bytearray` instance. |
| Unfortunately it seems difficult to tell which sorts of |
| NumPy arrays are going to cause difficulty. |
| |
| A workaround is to use ``.copy()`` to copy the NumPy array. |
| |
| Examples |
| ^^^^^^^^ |
| |
| The code in this section is extracted from ``exnumpy.py``, which is a |
| complete runnable example in the ``code/`` subdirectory of the source |
| distribution. Code was originally written by Mel Raab, but has been |
| hacked around since then. |
| |
| |
| PNG to NumPy array (reading) |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| The best thing to do (I think) is to convert each PyPNG row to a |
| 1‑dimensional numpy array, then stack all of those arrays together to |
| make a 2‑dimensional array. A number of features make this surprising |
| compact. Say `pngdata` is the row iterator returned from |
| :meth:`png.Reader.asDirect`. The following code will slurp it into a |
| 2‑dimensional numpy array: |
| |
| .. literalinclude:: ../code/exnumpy.py |
| :start-after: extract 001 start |
| :end-before: extract 001 end |
| |
| Note that the use of ``numpy.uint16``, above, means that an array with |
| data type ``numpy.uint16`` is created which is suitable for |
| bit depth 16 images. Replace ``numpy.uint16`` with ``numpy.uint8`` to |
| create an array with a byte data type (suitable for bit depths up to 8). |
| |
| Reshaping |
| ^^^^^^^^^ |
| |
| For some operations it's easier to have the image data in a |
| 3‑dimensional array. This plays to NumPy's strengths: |
| |
| .. literalinclude:: ../code/exnumpy.py |
| :start-after: extract 002 start |
| :end-before: extract 002 end |
| |
| NumPy array to PNG (writing) |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| Reshape your NumPy data into a 2‑dimensional array, then use the fact |
| that a NumPy array is an iterator over its rows: |
| |
| .. literalinclude:: ../code/exnumpy.py |
| :start-after: extract 003 start |
| :end-before: extract 003 end |
| |