Skip to content

Commit

Permalink
Added image plugin (#732)
Browse files Browse the repository at this point in the history
# Description
Added image plugin from TEM demo. Improved the test, added cropping and
an option for interpolation order when resizing.

Closes #685 

## Type of change
- [ ] Bug fix & code cleanup
- [x] New feature
- [ ] Documentation update
- [ ] Test update

## Checklist for the reviewer
This checklist should be used as a help for the reviewer.

- [ ] Is the change limited to one issue?
- [ ] Does this PR close the issue?
- [ ] Is the code easy to read and understand?
- [ ] Do all new feature have an accompanying new test?
- [ ] Has the documentation been updated as necessary?
  • Loading branch information
jesper-friis authored Dec 12, 2023
2 parents d4c176c + b86d40d commit 3f260cc
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 0 deletions.
1 change: 1 addition & 0 deletions storages/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ install(
install(
FILES
python-storage-plugins/blob.json
python-storage-plugins/Image.json
DESTINATION share/dlite/storages
)

Expand Down
20 changes: 20 additions & 0 deletions storages/python/python-storage-plugins/Image.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"uri": "http://onto-ns.com/meta/0.1/Image",
"description": "Metadata for an image.",
"dimensions": {
"height": "Image hight",
"width": "Image width.",
"channels": "Number of channels in each pixel (1 for gray-scale, 3 for RGB and 4 for RGBA)."
},
"properties": {
"filename": {
"type": "string",
"description": "File name."
},
"data": {
"type": "float32",
"shape": ["height", "width", "channels"],
"description": "Image data."
}
}
}
117 changes: 117 additions & 0 deletions storages/python/python-storage-plugins/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""DLite storage plugin for images."""
import numpy as np

from skimage.io import imread, imsave
from skimage.exposure import equalize_hist
from skimage.transform import resize

import dlite
from dlite.options import Options


class image(dlite.DLiteStorageBase):
"""DLite storage plugin for images.
Arguments:
location: Path to YAML file.
options: Supported options:
- `plugin`: Name of scikit image io plugin to use for loading
the image. By default, the different plugins are tried
(starting with imageio) until a suitable candidate is found.
If not given and fname is a tiff file, the 'tifffile' plugin
will be used.
- `crop`: Crop out a part of the image. Applied before other
operations. Specified as comma-separated set of ranges
to crop out for each dimension. Example: for a 2D image will
":,50:150" keep the hight, but reduce the width to 100 pixes
starting from x=50.
- `as_gray`: Whether to convert color images to gray-scale
when loading. Default is false.
- `equalize`: Whether to equalize histogram before saving.
Default is false.
- `resize`: Required image size when saving. Should be given
as `HEIGHTxWIDTH` (ex: "256x128").
- `order`: Order of spline interpolation for resize. Default
to zero for binary images and 1 otherwise. Should be in the
range 0-5.
"""
meta = "http://onto-ns.com/meta/0.1/Image"

def open(self, location, options=None):
"""Loads an image from `location`. No options are supported."""
self.location = location
self.options = Options(
options, defaults="as_gray=false;equalize=false"
)

def load(self, id=None):
"""Returns TEMImage instance."""
as_gray = dlite.asbool(self.options.as_gray)
data = imread(
self.location, as_gray=as_gray, plugin=self.options.get("plugin"),
)

crop = self.options.get("crop")
if crop:
# We call __getitem__() explicitly, since the preferred syntax
# `data[*toindex(crop)]` is not supported by Python 3.7
data = data.__getitem__(*toindex(crop))

# Infer dimensions
shape = [1]*3
shape[3 - len(data.shape):] = data.shape
dimnames = "height", "width", "channels"
dimensions = dict(zip(dimnames, shape))

# Create and populate DLite instance
Image = dlite.get_instance(self.meta)
image = Image(dimensions=dimensions)
image.filename = self.location
image.data = np.array(data, ndmin=3)

return image

def save(self, inst):
"""Stores DLite instance `inst` to storage."""
if dlite.asbool(self.options.equalize):
data = equalize_hist(inst.data)
else:
data = inst.data

if data.shape[0] == 1:
data = data[0,:,:]

crop = self.options.get("crop")
if crop:
# We call __getitem__() explicitly, since the preferred syntax
# `data[*toindex(crop)]` is not supported by Python 3.7
data = data.__getitem__(toindex(crop))

if self.options.get("resize"):
size = [int(s) for s in self.options.resize.split("x")]
shape = list(data.shape)
shape[:len(size)] = size
kw = {}
if self.options.get("order"):
kw["order"] = int(self.options.order)
data = resize(data, output_shape=shape, **kw)

hi, lo = data.max(), data.min()
scaled = np.uint8((data - lo)/(hi - lo + 1e-3)*256)
imsave(
self.location, scaled, plugin=self.options.get("plugin")
)


def toindex(crop_string):
"""Convert a crop string to a valid numpy index.
Example usage: `arr[*toindex(":,50:150")]`
"""
ranges = []
for dim in crop_string.split(","):
if dim == ":":
ranges.append(slice(None))
else:
ranges.append(slice(*[int(d) for d in dim.split(":")]))
return tuple(ranges)
10 changes: 10 additions & 0 deletions storages/python/tests-python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ set(python-tests
test_template
test_template-jinja
test_http
test_image
)

# Paths
set(plugindir "${CMAKE_CURRENT_SOURCE_DIR}/../python-storage-plugins")


include(FindPythonModule)

# Disable BSON test if pymongo is not installed
Expand Down Expand Up @@ -43,6 +48,7 @@ if(NOT PY_YAML)
list(REMOVE_ITEM python-tests test_yaml_storage_python)
endif()


# Add tests
foreach(test ${python-tests})
add_test(
Expand All @@ -62,6 +68,10 @@ foreach(test ${python-tests})
endif()
set_property(TEST ${test} APPEND PROPERTY
ENVIRONMENT "PYTHONPATH=${dlite_PYTHONPATH_NATIVE}")
set_property(TEST ${test} APPEND PROPERTY
ENVIRONMENT "DLITE_STORAGES=${plugindir}/*.json")
set_property(TEST ${test} APPEND PROPERTY
ENVIRONMENT "DLITE_PYDEBUG=")
set_property(TEST ${test} APPEND PROPERTY
ENVIRONMENT "DLITE_USE_BUILD_ROOT=YES")
if (NOT WIN32)
Expand Down
Binary file added storages/python/tests-python/input/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions storages/python/tests-python/output/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.json
*.yaml
*.bson
*.csv
*.xlsx
*.rdf
*.ttl
*.txt
*.png
47 changes: 47 additions & 0 deletions storages/python/tests-python/test_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from pathlib import Path

import dlite


thisdir = Path(__file__).absolute().parent
indir = thisdir / 'input'
outdir = thisdir / 'output'
#entitydir = thisdir.parent / "python-storage-plugins"
#dlite.storage_path.append(entitydir)

# Test save
image = dlite.Instance.from_location("image", indir / "image.png")
image.save("image", outdir / "image.png")
image.save("image", outdir / "image-crop.png", "crop=60:120,60:120")
image.save("image", outdir / "image-cropy.png", "crop=60:120")
image.save("image", outdir / "image-eq.png", "equalize=true")
image.save("image", outdir / "image-resize.png", "resize=128x128")
image.save("image", outdir / "image-resize2.png", "resize=512x512")
image.save("image", outdir / "image-resize3.png", "resize=512x512;order=3")

assert image.filename == str(indir / "image.png")
assert image.data.shape == (256, 256, 4)

# Test load
im = dlite.Instance.from_location("image", outdir / "image.png")
assert im.data.shape == image.data.shape
assert np.all(im.data == image.data)
assert Path(im.filename).name == Path(image.filename).name

im = dlite.Instance.from_location("image", outdir / "image-crop.png")
assert im.data.shape == (60, 60, 4)

im = dlite.Instance.from_location("image", outdir / "image-cropy.png")
assert im.data.shape == (60, 256, 4)

im = dlite.Instance.from_location("image", outdir / "image-eq.png")
assert im.data.shape == (256, 256, 4)

im = dlite.Instance.from_location("image", outdir / "image-resize.png")
assert im.data.shape == (128, 128, 4)

im = dlite.Instance.from_location("image", outdir / "image-resize2.png")
assert im.data.shape == (512, 512, 4)

im = dlite.Instance.from_location("image", outdir / "image-resize3.png")
assert im.data.shape == (512, 512, 4)

0 comments on commit 3f260cc

Please sign in to comment.