Skip to content
This repository has been archived by the owner on Mar 28, 2022. It is now read-only.

Commit

Permalink
Enable 32-bit depth image output with EXR + Drop Python 2.7 support (#91
Browse files Browse the repository at this point in the history
)

* Add EXR support with 32-bit depth

* Drop Python 2.7 support

* Fix circleci config

* Remove test

* Bump version

* Test fixes
  • Loading branch information
agermanidis committed Apr 12, 2020
1 parent 19e36b7 commit 239f1ec
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 99 deletions.
34 changes: 0 additions & 34 deletions .circleci/config.yml
Expand Up @@ -7,10 +7,6 @@ workflows:
filters:
tags:
only: /.*/
- build-py27:
filters:
tags:
only: /.*/
jobs:
build-py36:
docker:
Expand Down Expand Up @@ -58,33 +54,3 @@ jobs:
command: |
. venv/bin/activate
make docs
build-py27:
docker:
- image: continuumio/miniconda:latest
steps:
- checkout
- run:
name: install python dependencies
command: |
conda create -y -n dev python=2.7 pip --force
source activate dev
- run:
name: install make
command: |
apt-get update
apt-get install -y build-essential
- restore_cache:
key: v1-dependency-cache-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt" }}
- run:
name: install python dependencies
command: |
make dev
- save_cache:
key: v1-dependency-cache-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements-dev.txt"}}
paths:
- "/opt/conda/envs/dev/"
- run:
name: run tests
command: |
source activate dev
make test
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,12 @@ The Runway Model SDK follows [semantic versioning](https://semver.org/). Be awar
Until version 1.0.0, expect that minor version changes may introduce breaking changes. We will take care not to introduce new behavior, features, or breaking changes in patch releases. If you require stability and reproducible behavior you *may* pin to a version or version range of the model SDK like `runway-python>=0.2.0` or `runway-python>=0.2,<0.3`.

## v.0.6.0

- Drop Python 2 support.
- Add ability to specify output format with the `X-Runway-Output-Format` header.
- Add 32-bit depth support for the `image` data type with the EXR format.

## v.0.5.8

- Fix issue when extracting tar archives that include non-ASCII filenames.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -14,7 +14,7 @@ The Runway Model SDK allows you to port new and existing machine learning models

## Installing

The SDK supports both Python 2.7 and Python 3, but we recommend using Python 3. You can install the module using either pip or pip3 like so:
The SDK supports Python 3.6+. You can install the module using either pip or pip3 like so:

```
pip3 install runway-python
Expand All @@ -32,7 +32,7 @@ See the *Importing Models into Runway* [tutorial](https://docs.runwayapp.ai/#/im

## Developing

If you'd like to contribute to the development of the Runway Python SDK, you can clone and modify this repository by following the instructions below. At the time of this writing the SDK is compatible with Python 2.7 and Python 3, however our developer dependencies `requirements-dev.txt` require Python 3.
If you'd like to contribute to the development of the Runway Python SDK, you can clone and modify this repository by following the instructions below.

```bash
git clone https://github.com/runwayml/model-sdk runway-model-sdk
Expand All @@ -50,7 +50,7 @@ make dev

### Testing

Automated tests for the Runway Model SDK are written using `pytest` and live in the `tests/` directory. We also provide support for code coverage via `pytest-cov`. Tests can be run in both Python 2.7 or Python 3 environments.
Automated tests for the Runway Model SDK are written using `pytest` and live in the `tests/` directory. We also provide support for code coverage via `pytest-cov`.

```bash
## Create and activate a python3 virtual environment if you need to.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.md
Expand Up @@ -6,7 +6,7 @@ These documents serve as a reference for the [Runway](https://runwayml.com) Mode

## Installing

This SDK supports both Python 2.7 and Python 3, but we recommend using Python 3. You can install the module using either `pip` or `pip3` like so:
This SDK supports both Python 3.6+. You can install the module using either `pip` or `pip3` like so:

```bash
pip3 install runway-python
Expand Down
4 changes: 2 additions & 2 deletions docs/source/runway_yaml_file.md
Expand Up @@ -12,7 +12,7 @@ A `runway.yml` file is required to be in the root file directory of each Runway
```yaml
# Specify the version of the runway.yml spec.
version: 0.1
# Supported python versions are 2.7 and 3.6
# Supported python versions are: 3.6
python: 3.6
# The command to run your model. This value is used as the CMD value in
# the generated Docker image.
Expand Down Expand Up @@ -66,7 +66,7 @@ build_steps:
## Schema Reference

- `version` (int, optional, default = `0.1`): This version specifies the schema of the configuration file not the version of the Runway Model SDK itself.
- `python` (float, **required**): The Python version to use when running the model installing python dependencies. Currently supported values are `2.7` and `3.6`.
- `python` (float, **required**): The Python version to use when running the model installing python dependencies. Only `3.6` is supported at this time.
- `entrypoint` (string, **required**): The command to run your model. This value is used as the CMD value in the generated Docker image. A standard value for this field might be `entrypoint: python runway_model.py` where `runway_model.py` implements the `@runway.setup()`, `@runway.command()`, and most importantly the `runway.run()` functions.
- `cuda` (float, **required if building for GPU**): The NVIDIA CUDA version to use in the production GPU runtime environment. The currently supported CUDA versions are `10`, `9.2`, and `9`.
- `framework` (string, optional, default = `None`): The machine learning framework to pre-install during the build. Currently we support `"tensorflow"` and `"pytorch"` which will install the appropriate CPU or GPU packages of Tensorflow v1.12.0 and Pytorch v1.0 respectively depending on the build environment. If you require an ML framework other than Tensorflow or Pytorch, or a version of these libraries that is different than the versions provided by the ``frameworks`` object, you can omit this object and install these dependencies manually in the build steps.
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -10,3 +10,4 @@ scipy>=1.2.1
urllib3[secure]>=1.25.7
Unidecode>=1.1.1
flask-compress>=1.3.1
imageio>=2.5.0
2 changes: 1 addition & 1 deletion runway/__version__.py
@@ -1 +1 @@
__version__ = '0.5.8'
__version__ = '0.6.0'
57 changes: 29 additions & 28 deletions runway/data_types.py
Expand Up @@ -5,14 +5,11 @@
import json
import os
import tarfile
if sys.version_info[0] < 3:
from cStringIO import StringIO as IO
else:
from io import BytesIO as IO
from io import BytesIO as IO
import numpy as np
from scipy.spatial.distance import cdist
from PIL import Image
from .utils import is_url, extract_tarball, try_cast_np_scalar, download_file, get_color_palette
from .utils import is_url, extract_tarball, try_cast_np_scalar, download_file, get_color_palette, encode_image
from .exceptions import MissingArgumentError, InvalidArgumentError

class BaseType(object):
Expand All @@ -35,7 +32,7 @@ def __init__(self, data_type, description=None):
# functions to assign names to runway.data_types based on the dictionary keys.
self.name = self.type

def serialize(self, value):
def serialize(self, value, output_format=None):
raise NotImplementedError()

def deserialize(self, value):
Expand Down Expand Up @@ -74,7 +71,7 @@ def setup(opts):
def __init__(self, description=None):
super(any, self).__init__('any', description=description)

def serialize(self, v):
def serialize(self, v, output_format=None):
return v

def deserialize(self, v):
Expand Down Expand Up @@ -121,7 +118,7 @@ def __init__(self, item_type=None, description=None, min_length=0, max_length=No
def deserialize(self, items):
return [self.item_type.deserialize(item) for item in items]

def serialize(self, items):
def serialize(self, items, output_format=None):
return [self.item_type.serialize(item) for item in items]

def to_dict(self):
Expand Down Expand Up @@ -193,11 +190,11 @@ def __init__(
self.channels = channels
if channels not in [1, 3, 4]:
raise InvalidArgumentError(self.name or self.type, 'channels value needs to be 1, 3, or 4')
if default_output_format and default_output_format not in ['JPEG', 'PNG']:
msg = 'default_output_format needs to be JPEG or PNG'
if default_output_format and default_output_format.upper() not in ['JPEG', 'PNG']:
msg = 'default_output_format needs to be "JPEG" or "PNG'
raise InvalidArgumentError(self.name, msg)
if default_output_format:
self.default_output_format = default_output_format
self.default_output_format = default_output_format.upper()
elif self.channels == 3:
self.default_output_format = 'JPEG'
else:
Expand All @@ -223,19 +220,23 @@ def deserialize(self, value):
deserialized_image = deserialized_image.convert(self.get_pil_mode())
return deserialized_image

def serialize(self, value):
def serialize(self, value, output_format=None):
if output_format is None:
output_format = self.default_output_format
should_output_32bit = output_format.upper() == 'EXR'
if type(value) is np.ndarray:
im_pil = Image.fromarray(value.astype(np.uint8))
if not should_output_32bit:
value = value.astype(np.uint8)
im_pil = Image.fromarray(value)
elif issubclass(type(value), Image.Image):
im_pil = value
else:
raise InvalidArgumentError(self.name, 'value is not a PIL or numpy image')
buffer = IO()
if im_pil.mode != self.get_pil_mode():
if not should_output_32bit and im_pil.mode != self.get_pil_mode():
im_pil = im_pil.convert(self.get_pil_mode())
im_pil.save(buffer, format=self.default_output_format)
body = base64.b64encode(buffer.getvalue()).decode('utf8')
return 'data:image/{format};base64,{body}'.format(format=self.default_output_format.lower(), body=body)
encoded = encode_image(im_pil, output_format)
body = base64.b64encode(encoded).decode('utf8')
return 'data:image/{format};base64,{body}'.format(format=output_format.lower(), body=body)

def to_dict(self):
ret = super(image, self).to_dict()
Expand Down Expand Up @@ -296,7 +297,7 @@ def __init__(self, length=None, description=None, default=None, sampling_mean=0,
def deserialize(self, value):
return np.array(value)

def serialize(self, value):
def serialize(self, value, output_format=None):
return value.tolist()

def to_dict(self):
Expand Down Expand Up @@ -353,7 +354,7 @@ def deserialize(self, value):
raise InvalidArgumentError(self.name, msg)
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
return value

def to_dict(self):
Expand Down Expand Up @@ -400,7 +401,7 @@ def __init__(self, description=None, default=None, step=None, min=None, max=None
def deserialize(self, value):
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
return try_cast_np_scalar(value)

def to_dict(self):
Expand Down Expand Up @@ -448,7 +449,7 @@ def __init__(self, description=None, default='', min_length=0, max_length=None):
def deserialize(self, value):
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
return str(value)

def to_dict(self):
Expand Down Expand Up @@ -505,7 +506,7 @@ def deserialize(self, path_or_url):
raise InvalidArgumentError(self.name, 'file path does not have expected extension')
return path_or_url

def serialize(self, value):
def serialize(self, value, output_format=None):
return value

def to_dict(self):
Expand Down Expand Up @@ -647,7 +648,7 @@ def deserialize(self, value):
msg = 'unable to parse expected base64-encoded image'
raise InvalidArgumentError(self.name, msg)

def serialize(self, value):
def serialize(self, value, output_format=None):
if type(value) is np.ndarray:
im_pil = Image.fromarray(value)
elif issubclass(type(value), Image.Image):
Expand Down Expand Up @@ -710,7 +711,7 @@ def deserialize(self, value):
self.validate(value)
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
self.validate(value)
return value

Expand Down Expand Up @@ -750,7 +751,7 @@ def deserialize(self, value):
self.validate(value)
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
value = [try_cast_np_scalar(item) for item in value]
self.validate(value)
return value
Expand Down Expand Up @@ -794,7 +795,7 @@ def deserialize(self, value):
self.validate(value)
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
value = [try_cast_np_scalar(item) for item in value]
self.validate(value)
return value
Expand Down Expand Up @@ -904,7 +905,7 @@ def deserialize(self, value):
self.validate(value)
return value

def serialize(self, value):
def serialize(self, value, output_format=None):
self.validate(value)
value = [[try_cast_np_scalar(pt[0]), try_cast_np_scalar(pt[1])] for pt in value]
return value
Expand Down
9 changes: 7 additions & 2 deletions runway/model.py
Expand Up @@ -18,7 +18,7 @@
from .exceptions import RunwayError, MissingInputError, MissingOptionError, \
InferenceError, UnknownCommandError, SetupError
from .data_types import *
from .utils import gzipped, serialize_command, cast_to_obj, timestamp_millis, \
from .utils import gzipped, parse_output_formats_from_header, serialize_command, cast_to_obj, timestamp_millis, \
validate_post_request_body_is_json, get_json_or_none_if_invalid, argspec, \
deserialize_data, serialize_data, generate_uuid
from .__version__ import __version__ as model_sdk_version
Expand Down Expand Up @@ -126,6 +126,11 @@ def command_route(command_name):
raise UnknownCommandError(command_name)
inputs = self.commands[command_name]['inputs']
outputs = self.commands[command_name]['outputs']
output_formats_header = request.headers.get('X-Runway-Output-Format')
if output_formats_header:
output_formats = parse_output_formats_from_header(output_formats_header)
else:
output_formats = {}
input_dict = get_json_or_none_if_invalid(request)
deserialized_inputs = deserialize_data(input_dict, inputs)
self.millis_last_command = timestamp_millis()
Expand All @@ -144,7 +149,7 @@ def command_route(command_name):
raise reraise(InferenceError, InferenceError(repr(err)), sys.exc_info()[2])
if type(output_data) == tuple:
output_data, _ = output_data
return jsonify(serialize_data(output_data, outputs))
return jsonify(serialize_data(output_data, outputs, output_formats=output_formats))
except RunwayError as err:
err.print_exception()
return jsonify(err.to_response()), err.code
Expand Down

0 comments on commit 239f1ec

Please sign in to comment.