From 4d0d6e5d7d0f75e30184c33e9e3915dc8536515b Mon Sep 17 00:00:00 2001 From: scaramallion Date: Fri, 12 Jun 2020 16:12:28 +1000 Subject: [PATCH 1/3] First pass at changes for openjpeg plugin --- README.md | 29 +++++--- docs/release_notes/v1.1.0.rst | 2 + pylibjpeg/pydicom/pixel_data_handler.py | 10 +-- pylibjpeg/pydicom/utils.py | 2 +- pylibjpeg/tests/test_pydicom.py | 94 ++++++++++++++++++++++++- pylibjpeg/tools/s10918/_parsers.py | 2 +- pylibjpeg/utils.py | 24 +++++-- setup.py | 2 +- 8 files changed, 140 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 7b75bb6..fa5e3e6 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,12 @@ plugins come in. To support a given JPEG format or DICOM Transfer Syntax you first have to install the corresponding package: #### JPEG Format -| Format | Decode? | Encode? | Plugin | Based on | -|---|------|---|---|---| -| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] | +| Format | Decode? | Encode? | Plugin | Based on | Included? | +|---|------|---|---|---|---| +| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] | No | +| JPEG 2000 | Yes | No | [pylibjpeg-openjpeg][3] | [openjpeg][4] | Yes | -#### Transfer Syntax +#### DICOM Transfer Syntax | UID | Description | Plugin | |---|---|----| @@ -44,8 +45,9 @@ you first have to install the corresponding package: | 1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction
(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]| | 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless | [pylibjpeg-libjpeg][1]| | 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | [pylibjpeg-libjpeg][1]| -| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | Not yet supported | -| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | Not yet supported | +| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | [pylibjpeg-openjpeg][4] | +| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | [pylibjpeg-openjpeg][4] | +| 1.2.840.10008.1.2.5 | RLE Lossless | Not yet supported | If you're not sure what the dataset's *Transfer Syntax UID* is, it can be determined with: @@ -57,21 +59,28 @@ determined with: [1]: https://github.com/pydicom/pylibjpeg-libjpeg [2]: https://github.com/thorfdbg/libjpeg +[3]: https://github.com/pydicom/pylibjpeg-openjpeg +[4]: https://github.com/uclouvain/openjpeg ### Usage #### With pydicom -Assuming you already have *pydicom* v1.4+ installed: +Assuming you already have *pydicom* v1.4+ and suitable plugins installed: ```python from pydicom import dcmread from pydicom.data import get_testdata_file -# With the pylibjpeg-libjpeg plugin installed +# Importing the package adds the pixel data handler to pydicom import pylibjpeg +# With the pylibjpeg-libjpeg plugin ds = dcmread(get_testdata_file('JPEG-LL.dcm')) -arr = ds.pixel_array +jpg_arr = ds.pixel_array + +# With the pylibjpeg-openjpeg plugin +ds = dcmread(get_testdata_file('JPEG2000.dcm')) +j2k_arr = ds.pixel_array ``` For datasets with multiple frames you can reduce your memory usage by @@ -93,7 +102,7 @@ You can also just use *pylibjpeg* to decode JPEG images to a [numpy ndarray](htt ```python from pylibjpeg import decode -# Can decode using the path to a JPG file as str or pathlike +# Can decode using the path to a JPG file as str or path-like arr = decode('filename.jpg') # Or a file-like... diff --git a/docs/release_notes/v1.1.0.rst b/docs/release_notes/v1.1.0.rst index 1831c2b..bd3c4b7 100644 --- a/docs/release_notes/v1.1.0.rst +++ b/docs/release_notes/v1.1.0.rst @@ -7,3 +7,5 @@ * Removed the ``plugins`` and ``_config`` modules * Added ``utils.get_decoders()`` function * Added ``pydicom.utils.get_pixel_data_decoders()`` function +* Changed arguments passed to pixel data decoding functions to + ``func(bytes, Dataset)`` diff --git a/pylibjpeg/pydicom/pixel_data_handler.py b/pylibjpeg/pydicom/pixel_data_handler.py index ee6340f..bcdebb5 100644 --- a/pylibjpeg/pydicom/pixel_data_handler.py +++ b/pylibjpeg/pydicom/pixel_data_handler.py @@ -142,6 +142,7 @@ def get_pixeldata(ds): # Note: this does NOT include the trailing null byte for odd length data expected_len = get_expected_length(ds) if ds.PhotometricInterpretation == 'YBR_FULL_422': + # JPEG Transfer Syntaxes # Plugin should have already resampled the pixel data # see PS3.3 C.7.6.3.1.2 expected_len = expected_len // 2 * 3 @@ -158,12 +159,13 @@ def get_pixeldata(ds): decoder = _DECODERS[tsyntax] LOGGER.debug("Decoding {} Pixel Data using {}".format(tsyntax, decoder)) - # Generators for the encoded JPG image frame(s) and insertion offsets + # Generators for the encoded JPEG image frame(s) and insertion offsets generate_frames = generate_pixel_data_frame(ds.PixelData, nr_frames) generate_offsets = range(0, expected_len, frame_len) for frame, offset in zip(generate_frames, generate_offsets): - # Encoded JPG data to be sent to the decoder - frame = np.frombuffer(frame, np.uint8) - arr[offset:offset + frame_len] = decoder(frame, p_interp) + # Encoded JPEG data to be sent to the decoder + arr[offset:offset + frame_len] = decoder( + frame, ds.group_dataset(0x0028) + ) return arr.view(pixel_dtype(ds)) diff --git a/pylibjpeg/pydicom/utils.py b/pylibjpeg/pydicom/utils.py index 357c15a..7ee76f8 100644 --- a/pylibjpeg/pydicom/utils.py +++ b/pylibjpeg/pydicom/utils.py @@ -25,7 +25,7 @@ def generate_frames(ds): p_interp = ds.PhotometricInterpretation nr_frames = getattr(ds, 'NumberOfFrames', 1) for frame in generate_pixel_data_frame(ds.PixelData, nr_frames): - arr = decode(frame, p_interp).view(pixel_dtype(ds)) + arr = decode(frame, ds.group_dataset(0x0028)).view(pixel_dtype(ds)) yield reshape_frame(ds, arr) diff --git a/pylibjpeg/tests/test_pydicom.py b/pylibjpeg/tests/test_pydicom.py index 86f87bd..9899d5e 100644 --- a/pylibjpeg/tests/test_pydicom.py +++ b/pylibjpeg/tests/test_pydicom.py @@ -21,7 +21,12 @@ ) from pylibjpeg.utils import add_handler, remove_handler -HAS_PLUGINS = bool(get_pixel_data_decoders()) + +decoders = get_pixel_data_decoders() +HAS_PLUGINS = bool(decoders) +HAS_JPEG_PLUGIN = '1.2.840.10008.1.2.4.50' in decoders +HAS_JPEG_LS_PLUGIN = '1.2.840.10008.1.2.4.80' in decoders +HAS_JPEG_2K_PLUGIN = '1.2.840.10008.1.2.4.90' in decoders @pytest.mark.skipif(not HAS_PYDICOM or HAS_PLUGINS, reason="Plugins available") @@ -121,6 +126,93 @@ def test_ybr_full_422(self): arr = ds.pixel_array +@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_PLUGIN, reason="No plugin") +class TestJPEGPlugin(object): + """Test interaction with plugins that support JPEG.""" + uid = '1.2.840.10008.1.2.4.50' + + def test_pixel_array(self): + index = get_indexed_datasets(self.uid) + ds = index['JPEGBaseline_1s_1f_u_08_08.dcm']['ds'] + assert self.uid == ds.file_meta.TransferSyntaxUID + + arr = ds.pixel_array + assert arr.flags.writeable + assert 'uint8' == arr.dtype + assert (ds.Rows, ds.Columns) == arr.shape + + # Reference values from GDCM handler + assert 76 == arr[ 5, 50] + assert 167 == arr[15, 50] + assert 149 == arr[25, 50] + assert 203 == arr[35, 50] + assert 29 == arr[45, 50] + assert 142 == arr[55, 50] + assert 1 == arr[65, 50] + assert 64 == arr[75, 50] + assert 192 == arr[85, 50] + assert 255 == arr[95, 50] + + +@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_LS_PLUGIN, reason="No plugin") +class TestJPEGLSPlugin(object): + """Test interaction with plugins that support JPEG-LS.""" + uid = '1.2.840.10008.1.2.4.80' + + def test_pixel_array(self): + index = get_indexed_datasets(self.uid) + ds = index['MR_small_jpeg_ls_lossless.dcm']['ds'] + assert self.uid == ds.file_meta.TransferSyntaxUID + + arr = ds.pixel_array + assert arr.flags.writeable + assert 'int16' == arr.dtype + assert (ds.Rows, ds.Columns) == arr.shape + + # Reference values from GDCM handler + assert [1194, 879, 127, 661, 1943, 1885, 1857, 1746, 1699] == ( + arr[55:65, 38].tolist() + ) + + +@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_2K_PLUGIN, reason="No plugin") +class TestJPEG2KPlugin(object): + """Test interaction with plugins that support JPEG 2000.""" + uid = '1.2.840.10008.1.2.4.90' + + def test_pixel_array(self): + index = get_indexed_datasets(self.uid) + ds = index['US1_J2KR.dcm']['ds'] + + arr = ds.pixel_array + assert arr.flags.writeable + assert 'uint8' == arr.dtype + assert (ds.Rows, ds.Columns, ds.SamplesPerPixel) == arr.shape + + # Values checked against GDCM + assert [ + [180, 26, 0], + [172, 15, 0], + [162, 9, 0], + [152, 4, 0], + [145, 0, 0], + [132, 0, 0], + [119, 0, 0], + [106, 0, 0], + [ 87, 0, 0], + [ 37, 0, 0], + [ 0, 0, 0], + [ 50, 0, 0], + [100, 0, 0], + [109, 0, 0], + [122, 0, 0], + [135, 0, 0], + [145, 0, 0], + [155, 5, 0], + [165, 11, 0], + [175, 17, 0]] == arr[175:195, 28, :].tolist() + + @pytest.mark.skipif(not HAS_PYDICOM or not HAS_PLUGINS, reason="No plugins") class TestUtils(object): """Test the pydicom.utils functions.""" diff --git a/pylibjpeg/tools/s10918/_parsers.py b/pylibjpeg/tools/s10918/_parsers.py index 134bdf6..e39173c 100644 --- a/pylibjpeg/tools/s10918/_parsers.py +++ b/pylibjpeg/tools/s10918/_parsers.py @@ -52,7 +52,7 @@ from struct import unpack -from tools.utils import split_byte +from pylibjpeg.tools.utils import split_byte def APP(fp): diff --git a/pylibjpeg/utils.py b/pylibjpeg/utils.py index f21e0d4..b41e16f 100644 --- a/pylibjpeg/utils.py +++ b/pylibjpeg/utils.py @@ -2,6 +2,7 @@ import logging import os from pkg_resources import iter_entry_points +from struct import unpack import numpy as np @@ -56,12 +57,12 @@ def decode(data, decoder=None, kwargs=None): if isinstance(data, (str, os.PathLike)): with open(str(data), 'rb') as f: - data = np.frombuffer(f.read(), 'uint8') + data = f.read() elif isinstance(data, bytes): - data = np.frombuffer(data, 'uint8') + pass else: # Try file-like - data = np.frombuffer(data.read(), 'uint8') + data = data.read() kwargs = kwargs or {} @@ -86,13 +87,22 @@ def decode(data, decoder=None, kwargs=None): def get_decoders(decoder_type='JPEG'): """Return a :class:`dict` of JPEG decoders as {package: callable}.""" - if decoder_type == 'JPEG': + entry_points = { + "JPEG" : "pylibjpeg.jpeg_decoders", + "JPEG XT" : "pylibjpeg.jpeg_xt_decoders", + "JPEG-LS" : "pylibjpeg.jpeg_ls_decoders", + "JPEG 2000" : "pylibjpeg.jpeg_2000_decoders", + "JPEG XR" : "pylibjpeg.jpeg_xr_decoders", + "JPEG XS" : "pylibjpeg.jpeg_xs_decoders", + "JPEG XL" : "pylibjpeg.jpeg_xl_decoders", + } + try: return { val.name: val.load() - for val in iter_entry_points('pylibjpeg.jpeg_decoders') + for val in iter_entry_points(entry_points[decoder_type]) } - - raise ValueError("Unknown decoder_type '{}'".format(decoder_type)) + except KeyError: + raise ValueError("Unknown decoder_type '{}'".format(decoder_type)) def remove_handler(): diff --git a/setup.py b/setup.py index 39ec0ca..aa13fc4 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ "Topic :: Software Development :: Libraries", ], packages = find_packages(), - install_requires = ['numpy'], + install_requires = ['numpy', 'pylibjpeg-openjpeg'], include_package_data = True, zip_safe = False, python_requires = ">=3.6", From 56d11f40f3d0338b8c1e25f3e053d5a4ffc59d5e Mon Sep 17 00:00:00 2001 From: scaramallion Date: Fri, 12 Jun 2020 16:13:00 +1000 Subject: [PATCH 2/3] Add plugin spec --- docs/plugins.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 docs/plugins.md diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..ab4ee26 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,102 @@ + +## Plugins + +Plugins should register their entry points via the *entry_points* kwarg for [setuptools.setup()](https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins) in their `setup.py` file. + +### DICOM Pixel Data decoders +#### Decoder plugin registration + +Plugins that decode DICOM *Pixel Data* should register their decoding functions using the corresponding *Transfer Syntax UID* as the entry point name. For example, if the `my_plugin` plugin supported *JPEG Baseline* (1.2.840.10008.1.2.4.50) with the decoding function `decode_jpeg_baseline()` and *JPEG-LS Lossless* (1.2.840.10008.1.2.4.80) with the decoding function `decode_jls_lossless()` then it should include the following in its `setup.py`: + +```python +from setuptools import setup + +setup( + ..., + entry_points={ + "pylibjpeg.pixel_data_decoders": [ + "1.2.840.10008.1.2.4.50 = my_plugin:decode_jpeg_baseline", + "1.2.840.10008.1.2.4.80 = my_plugin:decode_jls_lossless", + ], + } +) +``` + +#### Decoder function signature + +The pixel data decoding function will be passed two arguments; a single encoded +image frame as [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) and a *pydicom* [Dataset](https://pydicom.github.io/pydicom/stable/reference/generated/pydicom.dataset.Dataset.html) object containing the (0028,eeee) elements corresponding to the pixel data. The function should return the decoded pixel data as a one-dimensional numpy [ndarray](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html) of `'uint8'`: + +```python +def my_pixel_data_decoder(data, ds): + """Return the encoded `data` as an unshaped numpy ndarray of uint8. + + Parameters + ---------- + data : bytes + A single frame of the encoded *Pixel Data*. + ds : pydicom.dataset.Dataset + A dataset containing the group ``0x0028`` elements corresponding to + the *Pixel Data*. + + Returns + ------- + numpy.ndarray + A 1-dimensional ndarray of 'uint8' containing the decoded pixel data. + """ + # Decoding happens here +``` + +### JPEG decoders +#### Decoder plugin registration + +Plugins that decoder JPEG data should register their decoding functions uding +the name of the plugin as the entry point name. For example, if the `my_plugin` +plugin supports decoding JPEG images via the `decode_jpeg()` function then +it should include the following in its `setup.py`: + +```python +from setuptools import setup + +setup( + ..., + entry_points={ + "pylibjpeg.jpeg_decoders": "my_plugin = my_plugin:decode_jpeg", + } +) +``` + +Possible entry points for JPEG decoding are: + +| JPEG Format | ISO/IEC Standard | Entry Point | +| --- | --- | --- | +| JPEG | [10918](https://www.iso.org/standard/18902.html) | `"pylibjpeg.jpeg_decoders"` | +| JPEG XT | [18477](https://www.iso.org/standard/62552.html) | `"pylibjpeg.jpeg_xt_decoders"` | +| JPEG-LS | [14495](https://www.iso.org/standard/22397.html) | `"pylibjpeg.jpeg_ls_decoders"` | +| JPEG 2000 | [15444](https://www.iso.org/standard/78321.html) | `"pylibjpeg.jpeg_2000_decoders"` | + + +#### Decoder function signature + +The JPEG decoding function will be passed the encoded JPEG *data* as +[bytes](https://docs.python.org/3/library/stdtypes.html#bytes) and a +[dict](https://docs.python.org/3/library/stdtypes.html#dict) containing keyword arguments passed to the function. The function should return the decoded image data as a numpy [ndarray](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html) with a dtype and shape matching the image format and dimensions: + +```python +def my_jpeg_decoder(data, **kwarg): + """Return the encoded JPEG `data` as an numpy ndarray. + + Parameters + ---------- + data : bytes + The encoded JPEG data. + kwarg + Keyword arguments passed to the decoder. + + Returns + ------- + numpy.ndarray + An ndarray containing the decoded pixel data. + """ + # Decoding happens here +``` From ae8d2d53b70486065f3436424cf78c8f595b5bbd Mon Sep 17 00:00:00 2001 From: scaramallion Date: Fri, 12 Jun 2020 18:51:59 +1000 Subject: [PATCH 3/3] Add github actions --- .github/workflows/pytest-builds.yml | 58 ++++++++++++++++++ .travis.yml | 93 ----------------------------- build_tools/travis/install.sh | 28 --------- 3 files changed, 58 insertions(+), 121 deletions(-) create mode 100644 .github/workflows/pytest-builds.yml delete mode 100644 .travis.yml delete mode 100644 build_tools/travis/install.sh diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml new file mode 100644 index 0000000..cddb137 --- /dev/null +++ b/.github/workflows/pytest-builds.yml @@ -0,0 +1,58 @@ +name: build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + ubuntu: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package and dependencies + run: | + python -m pip install -U pip + python -m pip install . + python -m pip install pytest coverage pytest-cov + python -m pip install git+https://github.com/pydicom/pylibjpeg-data + + - name: Run pytest + run: | + pytest --cov openjpeg --ignore=openjpeg/src/openjpeg + + - name: Install pydicom release and rerun pytest + run: | + pip install pydicom + pytest --cov pylibjpeg + + - name: Install -libjpeg plugin rerun pytest + run: | + pip install git+https://github.com/pydicom/pylibjpeg-libjpeg + pytest --cov pylibjpeg + + - name: Install -openjpeg plugin rerun pytest + run: | + pip install git+https://github.com/pydicom/pylibjpeg-openjpeg + pytest --cov pylibjpeg + + - name: Send coverage results + if: ${{ success() }} + run: | + bash <(curl --connect-timeout 10 --retry 10 --retry-max-time \ + 0 https://codecov.io/bash) || (sleep 30 && bash <(curl \ + --connect-timeout 10 --retry 10 --retry-max-time \ + 0 https://codecov.io/bash)) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cd95e74..0000000 --- a/.travis.yml +++ /dev/null @@ -1,93 +0,0 @@ -branches: - only: - - "master" - -language: python - -matrix: - include: - # No plugins + pydicom - - name: "Python 3.6, Ubuntu + pydicom" - os: linux - dist: bionic - python: "3.6" - env: - - INSTALL_PYDICOM=true - - INSTALL_LIBJPEG=false - - name: "Python 3.7, Ubuntu + pydicom" - os: linux - dist: bionic - python: "3.7" - env: - - INSTALL_PYDICOM=true - - INSTALL_LIBJPEG=false - - name: "Python 3.8, Ubuntu + pydicom" - os: linux - dist: bionic - python: "3.8" - env: - - INSTALL_PYDICOM=true - - INSTALL_LIBJPEG=false - # libjpeg + pydicom - - name: "Python 3.6, Ubuntu + libjpeg + pydicom" - os: linux - dist: bionic - python: "3.6" - env: - - INSTALL_PYDICOM=true - - INSTALL_LIBJPEG=true - - name: "Python 3.7, Ubuntu + libjpeg + pydicom" - os: linux - dist: bionic - python: "3.7" - env: - - INSTALL_PYDICOM=true - - INSTALL_LIBJPEG=true - - name: "Python 3.8, Ubuntu + libjpeg + pydicom" - os: linux - dist: bionic - python: "3.8" - env: - - INSTALL_PYDICOM=true - - INSTALL_LIBJPEG=true - # libjpeg standalone - - name: "Python 3.6, Ubuntu + libjpeg" - os: linux - dist: bionic - python: "3.6" - env: - - INSTALL_PYDICOM=false - - INSTALL_LIBJPEG=true - - name: "Python 3.7, Ubuntu + libjpeg" - os: linux - dist: bionic - python: "3.7" - env: - - INSTALL_PYDICOM=false - - INSTALL_LIBJPEG=true - - name: "Python 3.8, Ubuntu + libjpeg" - os: linux - dist: bionic - python: "3.8" - env: - - INSTALL_PYDICOM=false - - INSTALL_LIBJPEG=true - - -# Install dependencies and package -install: - - source build_tools/travis/install.sh - - python -m pip install . - -# Command to run tests -script: - - python -m pytest --cov pylibjpeg - -after_success: - # Upload coverage results to codecov.io - # curl times out sometimes, so drop the connection timeout but retry more often - - bash <(curl --connect-timeout 10 --retry 10 --retry-max-time 0 https://codecov.io/bash) || - (sleep 30 && bash <(curl --connect-timeout 10 --retry 10 --retry-max-time 0 https://codecov.io/bash)) - -notifications: - email: false diff --git a/build_tools/travis/install.sh b/build_tools/travis/install.sh deleted file mode 100644 index 4c21e84..0000000 --- a/build_tools/travis/install.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -set -e - -echo "" -echo "Test suite: " $TEST_SUITE -echo "Working directory: " $PWD -echo "" - -pip install -U pip -pip install pytest pytest-cov - -if [[ "$INSTALL_PYDICOM" == "true" ]]; then - pip install pydicom - python -c "import pydicom; print('pydicom version', pydicom.__version__)" -fi - -# Install the test data -python -m pip install git+git://github.com/pydicom/pylibjpeg-data -python -c "import data; print('data version', data.__version__)" - -# Install plugins -if [[ "$INSTALL_LIBJPEG" == 'true' ]]; then - python -m pip install git+git://github.com/pydicom/pylibjpeg-libjpeg - python -c "import libjpeg; print('libjpeg version', libjpeg.__version__)" -fi - -python --version