Skip to content

Commit

Permalink
Merge pull request #150 from enthought/feature/image-component
Browse files Browse the repository at this point in the history
Add a very basic Image component
  • Loading branch information
jwiggins committed Jan 6, 2015
2 parents 70c1460 + 582fb46 commit 0b881f4
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 1 deletion.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
include kiva/agg/agg.i
include chaco/tests/data/PngSuite/*.png
include chaco/tests/data/PngSuite/LICENSE.txt
74 changes: 74 additions & 0 deletions enable/primitives/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
""" Defines the Image component class.
"""

from __future__ import absolute_import

# Enthought library imports
from traits.api import Array, Bool, Enum, Instance, Property, cached_property

# Local imports
from enable.component import Component
from kiva.image import GraphicsContext


class Image(Component):
""" Component that displays a static image
This is extremely simple right now. By default it will draw the array into
the entire region occupied by the component, stretching or shrinking as
needed. By default the bounds are set to the width and height of the data
array, and we provide the same information to constraints-based layout
with the layout_size_hint trait.
"""

#: the image data as an array
data = Array(shape=(None, None, (3,4)), dtype='uint8')

#: the format of the image data (eg. RGB vs. RGBA)
format = Property(Enum('rgb24', 'rgba32'), depends_on='data')

#: the size-hint for constraints-based layout
layout_size_hint = Property(data, depends_on='data')

#: the image as an Image GC
_image = Property(Instance(GraphicsContext), depends_on='data')

@classmethod
def from_file(cls, filename, **traits):
from PIL import Image
from numpy import asarray
data = asarray(Image.open(filename))
return cls(data=data, **traits)

def __init__(self, data, **traits):
# the default bounds are the size of the image
traits.setdefault('bounds', data.shape[1::-1])
super(Image, self).__init__(data=data, **traits)

def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"):
""" Draws the image. """
with gc:
gc.draw_image(self._image, (self.x, self.y, self.width, self.height))

@cached_property
def _get_format(self):
if self.data.shape[-1] == 3:
return 'rgb24'
elif self.data.shape[-1] == 4:
return 'rgba32'
else:
raise ValueError('Data array not correct shape')

@cached_property
def _get_layout_size_hint(self):
return self.data.shape[1::-1]

@cached_property
def _get__image(self):
if not self.data.flags['C_CONTIGUOUS']:
data = self.data.copy()
else:
data = self.data
image_gc = GraphicsContext(data, pix_format=self.format)
return image_gc
Empty file.
8 changes: 8 additions & 0 deletions enable/tests/primitives/data/PngSuite/LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PngSuite
--------

Permission to use, copy, modify and distribute these images for any
purpose and without fee is hereby granted.


(c) Willem van Schaik, 1996, 2011
Binary file added enable/tests/primitives/data/PngSuite/basi6a08.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added enable/tests/primitives/data/PngSuite/basn2c08.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions enable/tests/primitives/test_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
""" Tests for the Image component """

import os
import sys
if sys.version_info[:2] == (2, 6):
import unittest2 as unittest
else:
import unittest

import numpy as np
from numpy.testing import assert_array_equal
from pkg_resources import resource_filename

from kiva.image import GraphicsContext
from traits.api import TraitError
from traits.testing.unittest_tools import UnittestTools

from enable.primitives.image import Image


data_dir = resource_filename('enable.tests.primitives', 'data')


class ImageTest(unittest.TestCase, UnittestTools):

def setUp(self):
self.data = np.empty(shape=(128, 256, 4), dtype='uint8')
self.data[:, :, 0] = np.arange(256)
self.data[:, :, 1] = np.arange(128)[:, np.newaxis]
self.data[:, :, 2] = np.arange(256)[::-1]
self.data[:, :, 3] = np.arange(128)[::-1, np.newaxis]

self.image_24 = Image(self.data[..., :3])
self.image_32 = Image(self.data)

def test_fromfile_png_rgb(self):
# basic smoke test - assume that kiva.image does the right thing
path = os.path.join(data_dir, 'PngSuite', 'basn2c08.png')
image = Image.from_file(path)

self.assertEqual(image.data.shape, (32, 32, 3))
self.assertEqual(image.format, 'rgb24')

def test_fromfile_png_rgba(self):
# basic smoke test - assume that kiva.image does the right thing
path = os.path.join(data_dir, 'PngSuite', 'basi6a08.png')
image = Image.from_file(path)

self.assertEqual(image.data.shape, (32, 32, 4))
self.assertEqual(image.format, 'rgba32')

def test_init_bad_shape(self):
data = np.zeros(shape=(256, 256), dtype='uint8')
with self.assertRaises(TraitError):
Image(data=data)

def test_init_bad_dtype(self):
data = np.array(['red']*65536).reshape(128, 128, 4)
with self.assertRaises(TraitError):
Image(data=data)

def test_set_bad_shape(self):
data = np.zeros(shape=(256, 256), dtype='uint8')
with self.assertRaises(TraitError):
self.image_32.data = data

def test_set_bad_dtype(self):
data = np.array(['red']*65536).reshape(128, 128, 4)
with self.assertRaises(TraitError):
self.image_32.data = data

def test_format(self):
self.assertEqual(self.image_24.format, 'rgb24')
self.assertEqual(self.image_32.format, 'rgba32')

def test_format_change(self):
image = self.image_24
with self.assertTraitChanges(image, 'format'):
image.data = self.data

self.assertEqual(self.image_24.format, 'rgba32')

def test_bounds_default(self):
self.assertEqual(self.image_24.bounds, [256, 128])
self.assertEqual(self.image_32.bounds, [256, 128])

def test_bounds_overrride(self):
image = Image(self.data, bounds=[200, 100])
self.assertEqual(image.bounds, [200, 100])

def test_size_hint(self):
self.assertEqual(self.image_24.layout_size_hint, (256, 128))
self.assertEqual(self.image_32.layout_size_hint, (256, 128))

def test_size_hint_change(self):
data = np.zeros(shape=(256, 128, 3), dtype='uint8')
image = self.image_24
with self.assertTraitChanges(image, 'layout_size_hint'):
image.data = data

self.assertEqual(self.image_24.layout_size_hint, (128, 256))

def test_image_gc_24(self):
# this is non-contiguous, because data comes from slice
image_gc = self.image_24._image
assert_array_equal(image_gc.bmp_array, self.data[..., :3])

def test_image_gc_32(self):
# this is contiguous
image_gc = self.image_32._image
assert_array_equal(image_gc.bmp_array, self.data)

def test_draw_24(self):
gc = GraphicsContext((256, 128), pix_format='rgba32')
self.image_24.draw(gc)
# if test is failing, uncomment this line to see what is drawn
#gc.save('test_image_draw_24.png')

# smoke test: image isn't all white
assert_array_equal(gc.bmp_array[..., :3], self.data[..., :3])

def test_draw_32(self):
gc = GraphicsContext((256, 128), pix_format='rgba32')
self.image_32.draw(gc)
# if test is failing, uncommetn this line to see what is drawn
#gc.save('test_image_draw_32.png')

# smoke test: image isn't all white
# XXX actually compute what it should look like with alpha transfer
white_image = np.ones(shape=(256, 128, 4), dtype='uint8')*255
self.assertFalse(np.array_equal(white_image, gc.bmp_array))

def test_draw_stretched(self):
gc = GraphicsContext((256, 256), pix_format='rgba32')
self.image_32.bounds = [128, 258]
self.image_32.position = [128, 0]
self.image_32.draw(gc)
# if test is failing, uncommetn this line to see what is drawn
#gc.save('test_image_draw_stretched.png')

# smoke test: image isn't all white
# XXX actually compute what it should look like with alpha transfer
white_image = np.ones(shape=(256, 256, 4), dtype='uint8')*255
self.assertFalse(np.array_equal(white_image, gc.bmp_array))

# left half of the image *should* be white
assert_array_equal(gc.bmp_array[:, :128, :], white_image[:, :128, :])
36 changes: 36 additions & 0 deletions examples/enable/image_draw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
This demonstrates the use of the simple Image component.
"""
import os

from enable.api import ConstraintsContainer, Window
from enable.example_support import DemoFrame, demo_main
from enable.primitives.image import Image

THIS_DIR = os.path.split(__file__)[0]


class MyFrame(DemoFrame):

def _create_window(self):
path = os.path.join(THIS_DIR, 'deepfield.jpg')
image = Image.from_file(path, resist_width='weak',
resist_height='weak')

container = ConstraintsContainer(bounds=[500, 500])
container.add(image)
ratio = float(image.data.shape[1])/image.data.shape[0]
container.layout_constraints = [
image.left == container.contents_left,
image.right == container.contents_right,
image.top == container.contents_top,
image.bottom == container.contents_bottom,
image.layout_width == ratio*image.layout_height,
]
return Window(self, -1, component=container)


if __name__ == "__main__":
# Save demo so that it doesn't get garbage collected when run within
# existing event loop (i.e. from ipython).
demo = demo_main(MyFrame)
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ def run(self):
info['__version__']),
install_requires = info['__requires__'],
license = 'BSD',
package_data = {'': ['*.zip', '*.svg', 'images/*']},
package_data = {
'': ['*.zip', '*.svg', 'images/*'],
'enable': ['tests/primitives/data/PngSuite/*.png'],
},
platforms = ["Windows", "Linux", "Mac OS-X", "Unix", "Solaris"],
setup_requires = [
'cython',
Expand Down

0 comments on commit 0b881f4

Please sign in to comment.