diff --git a/stage6_branded_application/README.md b/stage6.0_branded_application/README.md similarity index 100% rename from stage6_branded_application/README.md rename to stage6.0_branded_application/README.md diff --git a/stage6_branded_application/pycasa/__init__.py b/stage6.0_branded_application/pycasa/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/__init__.py rename to stage6.0_branded_application/pycasa/__init__.py diff --git a/stage6_branded_application/pycasa/app/__init__.py b/stage6.0_branded_application/pycasa/app/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/app/__init__.py rename to stage6.0_branded_application/pycasa/app/__init__.py diff --git a/stage6.0_branded_application/pycasa/app/app.py b/stage6.0_branded_application/pycasa/app/app.py new file mode 100644 index 0000000..7771607 --- /dev/null +++ b/stage6.0_branded_application/pycasa/app/app.py @@ -0,0 +1,67 @@ +# coding=utf-8 +""" TaskApplication object for the Pycasa app. +""" +import logging + +from pyface.tasks.api import TasksApplication, TaskFactory +from pyface.api import SplashScreen +from pyface.action.api import Action +from pyface.action.schema.api import SchemaAddition, SGroup + +from ..ui.tasks.pycasa_task import PycasaTask +from ..ui.image_resources import app_icon, new_icon + +logger = logging.getLogger(__name__) + + +class PycasaApplication(TasksApplication): + """ An application to explore image files and detect faces. + """ + id = "pycasa_application" + + name = "Pycasa" + + description = "An example Tasks application that explores image files." + + def _task_factories_default(self): + return [ + TaskFactory( + id='pycasa.pycasa_task_factory', + name="Main Pycasa Task Factory", + factory=PycasaTask + ) + ] + + def _icon_default(self): + pass + + def _splash_screen_default(self): + pass + + def create_new_task_window(self): + from pyface.tasks.task_window_layout import TaskWindowLayout + + layout = TaskWindowLayout() + layout.items = [self.task_factories[0].id] + window = self.create_window(layout=layout) + self.add_window(window) + window.title += " {}".format(len(self.windows)) + return window + + def create_new_task_menu(self): + return SGroup( + Action(name="New", + accelerator='Ctrl+N', + on_perform=self.create_new_task_window, + image=new_icon), + id='NewGroup', name='NewGroup', + ) + + def _extra_actions_default(self): + extra_actions = [ + SchemaAddition(id='pycasa.custom_new', + factory=self.create_new_task_menu, + path="MenuBar/File/OpenGroup", + absolute_position="first") + ] + return extra_actions diff --git a/stage6_branded_application/pycasa/app/main.py b/stage6.0_branded_application/pycasa/app/main.py similarity index 100% rename from stage6_branded_application/pycasa/app/main.py rename to stage6.0_branded_application/pycasa/app/main.py diff --git a/stage6_branded_application/pycasa/model/__init__.py b/stage6.0_branded_application/pycasa/model/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/model/__init__.py rename to stage6.0_branded_application/pycasa/model/__init__.py diff --git a/stage6_branded_application/pycasa/model/file_browser.py b/stage6.0_branded_application/pycasa/model/file_browser.py similarity index 100% rename from stage6_branded_application/pycasa/model/file_browser.py rename to stage6.0_branded_application/pycasa/model/file_browser.py diff --git a/stage6_branded_application/pycasa/model/image_file.py b/stage6.0_branded_application/pycasa/model/image_file.py similarity index 100% rename from stage6_branded_application/pycasa/model/image_file.py rename to stage6.0_branded_application/pycasa/model/image_file.py diff --git a/stage6_branded_application/pycasa/model/image_folder.py b/stage6.0_branded_application/pycasa/model/image_folder.py similarity index 100% rename from stage6_branded_application/pycasa/model/image_folder.py rename to stage6.0_branded_application/pycasa/model/image_folder.py diff --git a/stage6_branded_application/pycasa/model/tests/__init__.py b/stage6.0_branded_application/pycasa/model/tests/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/model/tests/__init__.py rename to stage6.0_branded_application/pycasa/model/tests/__init__.py diff --git a/stage6_branded_application/pycasa/model/tests/test_image_file.py b/stage6.0_branded_application/pycasa/model/tests/test_image_file.py similarity index 100% rename from stage6_branded_application/pycasa/model/tests/test_image_file.py rename to stage6.0_branded_application/pycasa/model/tests/test_image_file.py diff --git a/stage6_branded_application/pycasa/model/tests/test_image_folder.py b/stage6.0_branded_application/pycasa/model/tests/test_image_folder.py similarity index 100% rename from stage6_branded_application/pycasa/model/tests/test_image_folder.py rename to stage6.0_branded_application/pycasa/model/tests/test_image_folder.py diff --git a/stage6_branded_application/pycasa/ui/__init__.py b/stage6.0_branded_application/pycasa/ui/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/ui/__init__.py rename to stage6.0_branded_application/pycasa/ui/__init__.py diff --git a/stage6_branded_application/pycasa/ui/file_browser_view.py b/stage6.0_branded_application/pycasa/ui/file_browser_view.py similarity index 100% rename from stage6_branded_application/pycasa/ui/file_browser_view.py rename to stage6.0_branded_application/pycasa/ui/file_browser_view.py diff --git a/stage6_branded_application/pycasa/ui/image_file_editor.py b/stage6.0_branded_application/pycasa/ui/image_file_editor.py similarity index 100% rename from stage6_branded_application/pycasa/ui/image_file_editor.py rename to stage6.0_branded_application/pycasa/ui/image_file_editor.py diff --git a/stage6_branded_application/pycasa/ui/image_file_view.py b/stage6.0_branded_application/pycasa/ui/image_file_view.py similarity index 100% rename from stage6_branded_application/pycasa/ui/image_file_view.py rename to stage6.0_branded_application/pycasa/ui/image_file_view.py diff --git a/stage6_branded_application/pycasa/ui/image_folder_editor.py b/stage6.0_branded_application/pycasa/ui/image_folder_editor.py similarity index 100% rename from stage6_branded_application/pycasa/ui/image_folder_editor.py rename to stage6.0_branded_application/pycasa/ui/image_folder_editor.py diff --git a/stage6_branded_application/pycasa/ui/image_folder_view.py b/stage6.0_branded_application/pycasa/ui/image_folder_view.py similarity index 100% rename from stage6_branded_application/pycasa/ui/image_folder_view.py rename to stage6.0_branded_application/pycasa/ui/image_folder_view.py diff --git a/stage6_branded_application/pycasa/ui/image_resources.py b/stage6.0_branded_application/pycasa/ui/image_resources.py similarity index 100% rename from stage6_branded_application/pycasa/ui/image_resources.py rename to stage6.0_branded_application/pycasa/ui/image_resources.py diff --git a/stage6_branded_application/pycasa/ui/images/document-new.png b/stage6.0_branded_application/pycasa/ui/images/document-new.png similarity index 100% rename from stage6_branded_application/pycasa/ui/images/document-new.png rename to stage6.0_branded_application/pycasa/ui/images/document-new.png diff --git a/stage6_branded_application/pycasa/ui/images/scipy_logo.png b/stage6.0_branded_application/pycasa/ui/images/scipy_logo.png similarity index 100% rename from stage6_branded_application/pycasa/ui/images/scipy_logo.png rename to stage6.0_branded_application/pycasa/ui/images/scipy_logo.png diff --git a/stage6_branded_application/pycasa/ui/path_selector.py b/stage6.0_branded_application/pycasa/ui/path_selector.py similarity index 100% rename from stage6_branded_application/pycasa/ui/path_selector.py rename to stage6.0_branded_application/pycasa/ui/path_selector.py diff --git a/stage6_branded_application/pycasa/ui/tasks/__init__.py b/stage6.0_branded_application/pycasa/ui/tasks/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/__init__.py rename to stage6.0_branded_application/pycasa/ui/tasks/__init__.py diff --git a/stage6_branded_application/pycasa/ui/tasks/images/applications-education-university.png b/stage6.0_branded_application/pycasa/ui/tasks/images/applications-education-university.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/applications-education-university.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/applications-education-university.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png b/stage6.0_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/document-encrypted.png b/stage6.0_branded_application/pycasa/ui/tasks/images/document-encrypted.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/document-encrypted.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/document-encrypted.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/document-open-recent.png b/stage6.0_branded_application/pycasa/ui/tasks/images/document-open-recent.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/document-open-recent.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/document-open-recent.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/document-open.png b/stage6.0_branded_application/pycasa/ui/tasks/images/document-open.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/document-open.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/document-open.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/document-save-as.png b/stage6.0_branded_application/pycasa/ui/tasks/images/document-save-as.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/document-save-as.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/document-save-as.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/document-save.png b/stage6.0_branded_application/pycasa/ui/tasks/images/document-save.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/document-save.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/document-save.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png b/stage6.0_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png b/stage6.0_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/folder-blue.png b/stage6.0_branded_application/pycasa/ui/tasks/images/folder-blue.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/folder-blue.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/folder-blue.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/insert-image.png b/stage6.0_branded_application/pycasa/ui/tasks/images/insert-image.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/insert-image.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/insert-image.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/ipython_icon.png b/stage6.0_branded_application/pycasa/ui/tasks/images/ipython_icon.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/ipython_icon.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/ipython_icon.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/measure.png b/stage6.0_branded_application/pycasa/ui/tasks/images/measure.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/measure.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/measure.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/text-x-python.png b/stage6.0_branded_application/pycasa/ui/tasks/images/text-x-python.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/text-x-python.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/text-x-python.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/tool-animator.png b/stage6.0_branded_application/pycasa/ui/tasks/images/tool-animator.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/tool-animator.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/tool-animator.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/user-properties.png b/stage6.0_branded_application/pycasa/ui/tasks/images/user-properties.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/user-properties.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/user-properties.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png b/stage6.0_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png diff --git a/stage6_branded_application/pycasa/ui/tasks/images/zoom-draw.png b/stage6.0_branded_application/pycasa/ui/tasks/images/zoom-draw.png similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/images/zoom-draw.png rename to stage6.0_branded_application/pycasa/ui/tasks/images/zoom-draw.png diff --git a/stage6_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py b/stage6.0_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py rename to stage6.0_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py diff --git a/stage6.0_branded_application/pycasa/ui/tasks/pycasa_task.py b/stage6.0_branded_application/pycasa/ui/tasks/pycasa_task.py new file mode 100644 index 0000000..24c1351 --- /dev/null +++ b/stage6.0_branded_application/pycasa/ui/tasks/pycasa_task.py @@ -0,0 +1,88 @@ +# General imports +from os.path import splitext + +# ETS imports +from traits.api import Instance +from pyface.api import error, ImageResource +from pyface.action.api import StatusBarManager +from pyface.tasks.api import PaneItem, SplitEditorAreaPane, Task, TaskLayout +from pyface.tasks.action.api import DockPaneToggleGroup, SGroup, SMenu, \ + SMenuBar, SToolBar, TaskAction, TaskWindowAction + +# Local imports +from .pycasa_browser_pane import PycasaBrowserPane +from ...model.image_folder import ImageFolder +from ..image_folder_editor import ImageFolderEditor +from ...model.image_file import ImageFile, SUPPORTED_FORMATS +from ..image_file_editor import ImageFileEditor +from ..path_selector import PathSelector + + +class PycasaTask(Task): + # 'Task' traits ----------------------------------------------------------- + + #: The unique id of the task. + id = "pycasa.pycasa_task" + + #: The human-readable name of the task. + name = "Pycasa" + + central_pane = Instance(SplitEditorAreaPane) + + # Task interface ---------------------------------------------------------- + + def create_central_pane(self): + """ Create the central pane: the script editor. + """ + # Let's keep a handle on it so we can invoke it later to open objects + # in it: + self.central_pane = SplitEditorAreaPane() + return self.central_pane + + def create_dock_panes(self): + return [PycasaBrowserPane()] + + def _default_layout_default(self): + """ Control where to place each (visible) dock panes. + """ + return TaskLayout( + left=PaneItem('pycasa.file_browser_pane', width=300) + ) + + # Task interface ---------------------------------------------------------- + + def open_in_central_pane(self, filepath): + file_ext = splitext(filepath)[1].lower() + if file_ext in SUPPORTED_FORMATS: + obj = ImageFile(filepath=filepath) + self.central_pane.edit(obj, factory=ImageFileEditor) + elif file_ext == "": + obj = ImageFolder(directory=filepath) + self.central_pane.edit(obj, factory=ImageFolderEditor) + else: + print("Unsupported file format: {}".format(file_ext)) + obj = None + + return obj + + # Menu action methods ----------------------------------------------------- + + def create_me(self): + # TODO: Create menu entries here! + pass + + # Initialization methods -------------------------------------------------- + + def _tool_bars_default(self): + # No accelerators here: they are added to menu entries + # Note: Image resources are looked for in an images folder next to the + # module invoking the resource. + tool_bars = [ + # TODO: Create toolbar entries here! + ] + return tool_bars + + def _menu_bar_default(self): + # TODO: Create menu entries here! + menu_bar = SMenuBar() + return menu_bar diff --git a/stage6_branded_application/pycasa/ui/tests/__init__.py b/stage6.0_branded_application/pycasa/ui/tests/__init__.py similarity index 100% rename from stage6_branded_application/pycasa/ui/tests/__init__.py rename to stage6.0_branded_application/pycasa/ui/tests/__init__.py diff --git a/stage6_branded_application/setup.py b/stage6.0_branded_application/setup.py similarity index 100% rename from stage6_branded_application/setup.py rename to stage6.0_branded_application/setup.py diff --git a/stage6.1_branded_application/README.md b/stage6.1_branded_application/README.md new file mode 100644 index 0000000..202c314 --- /dev/null +++ b/stage6.1_branded_application/README.md @@ -0,0 +1,3 @@ +# Second real version of the pycasa ETS pyface application +Building on the application state 5.1, this version adds a button to the folder +and file views so the faces can be detected. diff --git a/stage6.1_branded_application/pycasa/__init__.py b/stage6.1_branded_application/pycasa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6.1_branded_application/pycasa/app/__init__.py b/stage6.1_branded_application/pycasa/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6_branded_application/pycasa/app/app.py b/stage6.1_branded_application/pycasa/app/app.py similarity index 100% rename from stage6_branded_application/pycasa/app/app.py rename to stage6.1_branded_application/pycasa/app/app.py diff --git a/stage6.1_branded_application/pycasa/app/main.py b/stage6.1_branded_application/pycasa/app/main.py new file mode 100644 index 0000000..20e5fe0 --- /dev/null +++ b/stage6.1_branded_application/pycasa/app/main.py @@ -0,0 +1,11 @@ + +from pycasa.app.app import PycasaApplication + + +def main(): + app = PycasaApplication() + app.run() + + +if __name__ == '__main__': + main() diff --git a/stage6.1_branded_application/pycasa/model/__init__.py b/stage6.1_branded_application/pycasa/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6.1_branded_application/pycasa/model/file_browser.py b/stage6.1_branded_application/pycasa/model/file_browser.py new file mode 100644 index 0000000..873a057 --- /dev/null +++ b/stage6.1_branded_application/pycasa/model/file_browser.py @@ -0,0 +1,9 @@ +from os.path import expanduser +from traits.api import Directory, Event, HasStrictTraits + + +class FileBrowser(HasStrictTraits): + root = Directory(expanduser("~")) + + #: Item last double-clicked on in the tree view + requested_item = Event diff --git a/stage6.1_branded_application/pycasa/model/image_file.py b/stage6.1_branded_application/pycasa/model/image_file.py new file mode 100644 index 0000000..e32b718 --- /dev/null +++ b/stage6.1_branded_application/pycasa/model/image_file.py @@ -0,0 +1,64 @@ +# General imports +from os.path import splitext +import PIL.Image +from PIL.ExifTags import TAGS +from skimage import data +from skimage.feature import Cascade +import numpy as np + +# ETS imports +from traits.api import ( + Array, cached_property, Dict, File, HasStrictTraits, List, Property +) + +SUPPORTED_FORMATS = [".png", ".jpg", ".jpeg", ".PNG", ".JPG", ".JPEG"] + + +class ImageFile(HasStrictTraits): + """ Model to hold an image file. + """ + filepath = File + + metadata = Property(Dict, depends_on="filepath") + + data = Property(Array, depends_on="filepath") + + faces = List + + def _is_valid_file(self): + return ( + bool(self.filepath) and + splitext(self.filepath)[1].lower() in SUPPORTED_FORMATS + ) + + @cached_property + def _get_data(self): + if not self._is_valid_file(): + return np.array([]) + with PIL.Image.open(self.filepath) as img: + return np.asarray(img) + + @cached_property + def _get_metadata(self): + if not self._is_valid_file(): + return {} + with PIL.Image.open(self.filepath) as img: + exif = img._getexif() + if not exif: + return {} + return {TAGS[k]: v for k, v in exif.items() if k in TAGS} + + def detect_faces(self): + # Load the trained file from the module root. + trained_file = data.lbp_frontal_face_cascade_filename() + + # Initialize the detector cascade. + detector = Cascade(trained_file) + + detected = detector.detect_multi_scale(img=self.data, + scale_factor=1.2, + step_ratio=1, + min_size=(60, 60), + max_size=(600, 600)) + self.faces = detected + return self.faces diff --git a/stage6.1_branded_application/pycasa/model/image_folder.py b/stage6.1_branded_application/pycasa/model/image_folder.py new file mode 100644 index 0000000..8b1c423 --- /dev/null +++ b/stage6.1_branded_application/pycasa/model/image_folder.py @@ -0,0 +1,69 @@ +# General imports +import glob +from os.path import basename, expanduser, isdir + +import numpy as np +import pandas as pd + +# ETS imports +from traits.api import ( + Directory, Event, HasStrictTraits, Instance, List, observe, +) + +# Local imports +from pycasa.model.image_file import ImageFile, SUPPORTED_FORMATS + +FILENAME_COL = "filename" +NUM_FACE_COL = "Num. faces" + + +class ImageFolder(HasStrictTraits): + """ Model for a folder of images. + """ + directory = Directory(expanduser("~")) + + images = List(Instance(ImageFile)) + + data = Instance(pd.DataFrame) + + data_updated = Event + + def __init__(self, **traits): + # Don't forget this! + super(ImageFolder, self).__init__(**traits) + if not isdir(self.directory): + msg = f"The provided directory isn't a real directory: " \ + f"{self.directory}" + raise ValueError(msg) + self.data = self._create_metadata_df() + + @observe("directory") + def _update_images(self, event): + self.images = [ + ImageFile(filepath=file) + for fmt in SUPPORTED_FORMATS + for file in glob.glob(f"{self.directory}/*{fmt}") + ] + + @observe("images.items") + def _update_metadata(self, event): + self.data = self._create_metadata_df() + + def _create_metadata_df(self): + if not self.images: + return pd.DataFrame({FILENAME_COL: [], NUM_FACE_COL: []}) + return pd.DataFrame([ + { + FILENAME_COL: basename(img.filepath), + NUM_FACE_COL: np.nan, + **img.metadata + + } + for img in self.images + ]) + + def compute_num_faces(self, **kwargs): + for i, image in enumerate(self.images): + faces = image.detect_faces(**kwargs) + self.data[NUM_FACE_COL].iat[i] = len(faces) + self.data_updated = True diff --git a/stage6.1_branded_application/pycasa/model/tests/__init__.py b/stage6.1_branded_application/pycasa/model/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6.1_branded_application/pycasa/model/tests/test_image_file.py b/stage6.1_branded_application/pycasa/model/tests/test_image_file.py new file mode 100644 index 0000000..cc4d56c --- /dev/null +++ b/stage6.1_branded_application/pycasa/model/tests/test_image_file.py @@ -0,0 +1,52 @@ +from os.path import dirname, join +from unittest import TestCase + +import numpy as np + +from pycasa.model.image_file import ImageFile + +import ets_tutorial + +TUTORIAL_DIR = dirname(ets_tutorial.__file__) + +SAMPLE_IMG_DIR = join(TUTORIAL_DIR, "..", "sample_images") + +SAMPLE_IMG1 = join(SAMPLE_IMG_DIR, "IMG-0311_xmas_2020.JPG") + + +class TestImageFile(TestCase): + def test_no_image_file(self): + img = ImageFile() + self.assertEqual(img.metadata, {}) + self.assertIsInstance(img.data, np.ndarray) + self.assertEqual(img.data.shape, (0,)) + + def test_bad_type_image_file(self): + img = ImageFile(filepath=__file__) + self.assertEqual(img.metadata, {}) + self.assertIsInstance(img.data, np.ndarray) + self.assertEqual(img.data.shape, (0,)) + + def test_image_metadata(self): + img = ImageFile(filepath=SAMPLE_IMG1) + self.assertNotEqual(img.metadata, {}) + for key in ['ExifVersion', 'ExifImageWidth', 'ExifImageHeight']: + self.assertIn(key, img.metadata.keys()) + expected_shape = (img.metadata['ExifImageHeight'], + img.metadata['ExifImageWidth'], 3) + self.assertEqual(img.data.shape, expected_shape) + + def test_image_data(self): + img = ImageFile(filepath=SAMPLE_IMG1) + self.assertNotIn(0, img.data.shape) + np.testing.assert_almost_equal(img.data, img.data) + self.assertIsInstance(img.data, np.ndarray) + self.assertNotEqual(img.data.mean(), 0) + + def test_face_detection(self): + img = ImageFile(filepath=SAMPLE_IMG1) + faces = img.detect_faces() + self.assertIsInstance(faces, list) + for face in faces: + self.assertIsInstance(face, dict) + self.assertEqual(len(faces), 5) diff --git a/stage6.1_branded_application/pycasa/model/tests/test_image_folder.py b/stage6.1_branded_application/pycasa/model/tests/test_image_folder.py new file mode 100644 index 0000000..19c62c3 --- /dev/null +++ b/stage6.1_branded_application/pycasa/model/tests/test_image_folder.py @@ -0,0 +1,35 @@ +from os.path import dirname, join +from unittest import TestCase + +import pandas as pd + +from pycasa.model.image_folder import ImageFolder + +import ets_tutorial + +TUTORIAL_DIR = dirname(ets_tutorial.__file__) + +SAMPLE_IMG_DIR = join(TUTORIAL_DIR, "..", "sample_images") + +HERE = dirname(__file__) + + +class TestImageFolder(TestCase): + def test_no_folder(self): + with self.assertRaises(ValueError): + ImageFolder(directory="path/to/nonexistent/dir") + + def test_with_file(self): + with self.assertRaises(ValueError): + ImageFolder(directory=__file__) + + def test_empty_folder(self): + img_folder = ImageFolder(directory=HERE) + self.assertIsInstance(img_folder.data, pd.DataFrame) + self.assertEqual(len(img_folder.data), 0) + + def test_real_folder(self): + img_folder = ImageFolder(directory=SAMPLE_IMG_DIR) + self.assertEqual(len(img_folder.data), 4) + for key in ['ExifVersion', 'ExifImageWidth', 'ExifImageHeight']: + self.assertIn(key, img_folder.data.columns) diff --git a/stage6.1_branded_application/pycasa/ui/__init__.py b/stage6.1_branded_application/pycasa/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6.1_branded_application/pycasa/ui/file_browser_view.py b/stage6.1_branded_application/pycasa/ui/file_browser_view.py new file mode 100644 index 0000000..b904f75 --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/file_browser_view.py @@ -0,0 +1,19 @@ +# General imports + +# ETS imports +from traits.api import Instance +from traitsui.api import Item, ModelView, FileEditor, View + +# Local imports +from ..model.file_browser import FileBrowser + + +class FileBrowserView(ModelView): + model = Instance(FileBrowser, ()) + + def traits_view(self): + editor = FileEditor(dclick_name="model.requested_item") + return View( + Item("model.root", editor=editor, style="custom", + show_label=False), + ) diff --git a/stage6.1_branded_application/pycasa/ui/image_file_editor.py b/stage6.1_branded_application/pycasa/ui/image_file_editor.py new file mode 100644 index 0000000..51ec208 --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/image_file_editor.py @@ -0,0 +1,36 @@ +from traits.api import Property +from pyface.tasks.api import Editor + +from .image_file_view import ImageFileView + + +class ImageFileEditor(Editor): + name = Property + + tooltip = Property + + # ------------------------------------------------------------------------- + # 'Editor' interface methods + # ------------------------------------------------------------------------- + + def create(self, parent): + """ Create and set the toolkit-specific control that represents the + editor. + """ + # Setting the kind and the parent allows for the ui to be embedded + # within the parent UI + view = ImageFileView(model=self.obj) + ui = view.edit_traits(kind="subpanel", parent=parent) + + # Grab the Qt widget to return to the editor area + self.control = ui.control + + # ------------------------------------------------------------------------- + # Traits property methods + # ------------------------------------------------------------------------- + + def _get_name(self): + return self.obj.filepath[:25] + + def _get_tooltip(self): + return self.obj.filepath diff --git a/stage6.1_branded_application/pycasa/ui/image_file_view.py b/stage6.1_branded_application/pycasa/ui/image_file_view.py new file mode 100644 index 0000000..8e868ea --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/image_file_view.py @@ -0,0 +1,61 @@ +# General imports +from matplotlib.figure import Figure +from matplotlib import patches + +# ETS imports +from traits.api import Button, Instance, observe +from traitsui.api import HGroup, Item, ModelView, Spring, View + +# Local imports +from ets_tutorial.util.mpl_figure_editor import MplFigureEditor +from ..model.image_file import ImageFile + + +class ImageFileView(ModelView): + """ ModelView for an image file object. + """ + model = Instance(ImageFile) + + figure = Instance(Figure) + + detect_button = Button("Detect faces") + + view = View( + Item("model.filepath", style="readonly", show_label=False), + Item("figure", editor=MplFigureEditor(), show_label=False), + HGroup( + Spring(), + Item("detect_button", show_label=False), + Spring(), + ), + ) + + @observe("model.filepath") + def build_mpl_figure(self, event): + figure = Figure() + axes = figure.add_subplot(111) + axes.imshow(self.model.data) + self.figure = figure + + @observe("detect_button") + def _detect_button_fired(self, event): + self.model.detect_faces() + + @observe("model.faces") + def update_mpl_figure_with_faces(self, events): + figure = Figure() + axes = figure.add_subplot(111) + axes.imshow(self.model.data) + + for face in self.model.faces: + axes.add_patch( + patches.Rectangle( + (face['c'], face['r']), + face['width'], + face['height'], + fill=False, + color='r', + linewidth=2 + ) + ) + self.figure = figure diff --git a/stage6.1_branded_application/pycasa/ui/image_folder_editor.py b/stage6.1_branded_application/pycasa/ui/image_folder_editor.py new file mode 100644 index 0000000..10a75e5 --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/image_folder_editor.py @@ -0,0 +1,36 @@ +from traits.api import Property +from pyface.tasks.api import Editor + +from .image_folder_view import ImageFolderView + + +class ImageFolderEditor(Editor): + name = Property + + tooltip = Property + + # ------------------------------------------------------------------------- + # 'Editor' interface methods + # ------------------------------------------------------------------------- + + def create(self, parent): + """ Create and set the toolkit-specific control that represents the + editor. + """ + # Setting the kind and the parent allows for the ui to be embedded + # within the parent UI + view = ImageFolderView(model=self.obj) + ui = view.edit_traits(kind="subpanel", parent=parent) + + # Grab the Qt widget to return to the editor area + self.control = ui.control + + # ------------------------------------------------------------------------- + # Traits property methods + # ------------------------------------------------------------------------- + + def _get_name(self): + return self.obj.directory[:25] + + def _get_tooltip(self): + return self.obj.directory diff --git a/stage6.1_branded_application/pycasa/ui/image_folder_view.py b/stage6.1_branded_application/pycasa/ui/image_folder_view.py new file mode 100644 index 0000000..9ccd65a --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/image_folder_view.py @@ -0,0 +1,151 @@ +# General imports + +# ETS imports +import numpy as np +import pandas as pd +from traits.api import Bool, Button, Enum, Instance, List, observe +from traitsui.api import HGroup, Item, Label, ListStrEditor, ModelView, \ + Spring, View +from traitsui.ui_editors.data_frame_editor import DataFrameEditor + +# Local imports +from pycasa.model.image_folder import FILENAME_COL, ImageFolder, NUM_FACE_COL + + +DISPLAYED_COLUMNS = [FILENAME_COL, NUM_FACE_COL] + [ + 'ApertureValue', 'ExifVersion', 'Model', 'Make', 'LensModel', 'DateTime', + 'ShutterSpeedValue', 'ExposureTime', 'XResolution', 'YResolution', + 'Orientation', 'GPSInfo', 'DigitalZoomRatio', 'FocalLengthIn35mmFilm', + 'ISOSpeedRatings', 'SceneType' +] + +YEAR_KEY = "__year__" + +DATETIME_COL = "DateTime" + +MAKE_COL = 'Make' + + +class ImageFolderView(ModelView): + """ ModelView for an image folder object. + """ + model = Instance(ImageFolder) + + scan = Button("Scan for faces...") + + # Filters widgets + view_filter_controls = Bool + + # Copy of the model's data, with filtering columns added if missing + all_data = Instance(pd.DataFrame) + + # Filtered dataframe based on filtering widgets + filtered_data = Instance(pd.DataFrame) + + year_mask = Instance(pd.Series) + + selected_years = List + + all_years = List + + selected_make = Enum(["All", "Canon", "Nikon", "Sony", "Apple", "samsung"]) + + make_mask = Instance(pd.Series) + + def traits_view(self): + view = View( + Item("model.directory", style="readonly", show_label=False), + HGroup( + Spring(), + Item("view_filter_controls"), + ), + HGroup( + Item("all_years", label="Years", + editor=ListStrEditor(selected="selected_years", + multi_select=True) + ), + Item("selected_make", label="Camera make"), + visible_when="view_filter_controls", + ), + Item("filtered_data", + editor=DataFrameEditor(columns=DISPLAYED_COLUMNS, + update="data_updated"), + show_label=False, visible_when="len(model.data) > 0"), + HGroup( + Spring(), + Label("No images found. No data to show"), + Spring(), + visible_when="len(model.data) == 0", + ), + HGroup( + Spring(), + Item("scan", show_label=False, + enabled_when="len(model.data) > 0"), + Spring(), + ), + ) + return view + + # Listener methods -------------------------------------------------------- + + @observe("scan") + def scan_for_faces(self, event): + self.model.compute_num_faces() + self.all_data.update(self.model.data) + + @observe("selected_years") + def update_years(self, event): + self.year_mask = self.all_data[YEAR_KEY].isin(self.selected_years) + + @observe("selected_make") + def update_make(self, event): + if self.selected_make == "All": + self.make_mask = pd.Series([True] * len(self.model.data)) + else: + self.make_mask = self.all_data[MAKE_COL] == self.selected_make + + @observe("year_mask, make_mask, all_data") + def update_filtered_data(self, event): + self.filtered_data = self.all_data[self.year_mask & self.make_mask] + + # Initialization methods -------------------------------------------------- + + def _make_mask_default(self): + return pd.Series(np.ones(len(self.model.data), dtype=bool)) + + def _year_mask_default(self): + return pd.Series(np.ones(len(self.model.data), dtype=bool)) + + def _all_data_default(self): + # Enrich metadata with missing fields: date time, make + data = self.model.data.copy() + + if DATETIME_COL not in data.columns: + data[DATETIME_COL] = np.nan + + if MAKE_COL not in data.columns: + data[MAKE_COL] = np.nan + + def parse_year(x): + return x.split(":")[0] if isinstance(x, str) else "unknown" + data[YEAR_KEY] = data[DATETIME_COL].apply(parse_year) + + return data + + def _filtered_data_default(self): + return self.all_data + + def _all_years_default(self): + return sorted(self.all_data[YEAR_KEY].unique().tolist()) + + +if __name__ == '__main__': + from os.path import dirname, join + import ets_tutorial + + TUTORIAL_DIR = dirname(ets_tutorial.__file__) + SAMPLE_IMG_DIR = join(TUTORIAL_DIR, "..", "sample_images") + + image_file = ImageFolder(directory=SAMPLE_IMG_DIR) + view = ImageFolderView(model=image_file) + view.configure_traits() diff --git a/stage6.1_branded_application/pycasa/ui/image_resources.py b/stage6.1_branded_application/pycasa/ui/image_resources.py new file mode 100644 index 0000000..c6599d1 --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/image_resources.py @@ -0,0 +1,5 @@ +from pyface.api import ImageResource + +app_icon = ImageResource('scipy_logo.png') + +new_icon = ImageResource('document-new.png') diff --git a/stage6.1_branded_application/pycasa/ui/images/document-new.png b/stage6.1_branded_application/pycasa/ui/images/document-new.png new file mode 100644 index 0000000..3d0f5cc Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/images/document-new.png differ diff --git a/stage6.1_branded_application/pycasa/ui/images/scipy_logo.png b/stage6.1_branded_application/pycasa/ui/images/scipy_logo.png new file mode 100644 index 0000000..b716c49 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/images/scipy_logo.png differ diff --git a/stage6.1_branded_application/pycasa/ui/path_selector.py b/stage6.1_branded_application/pycasa/ui/path_selector.py new file mode 100644 index 0000000..7db6367 --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/path_selector.py @@ -0,0 +1,16 @@ +from traits.api import Bool, HasStrictTraits, File +from traitsui.api import Item, OKCancelButtons, View +from pycasa.ui.image_resources import app_icon + + +class PathSelector(HasStrictTraits): + filepath = File + + scan_for_faces = Bool + + view = View(Item("filepath"), + Item("scan_for_faces"), + resizable=True, + icon=app_icon, + width=400, height=200, + buttons=OKCancelButtons) diff --git a/stage6.1_branded_application/pycasa/ui/tasks/__init__.py b/stage6.1_branded_application/pycasa/ui/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/applications-education-university.png b/stage6.1_branded_application/pycasa/ui/tasks/images/applications-education-university.png new file mode 100644 index 0000000..09cc14c Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/applications-education-university.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png b/stage6.1_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png new file mode 100644 index 0000000..01334d0 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/dialog-ok-apply.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/document-encrypted.png b/stage6.1_branded_application/pycasa/ui/tasks/images/document-encrypted.png new file mode 100644 index 0000000..e5a2b36 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/document-encrypted.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/document-open-recent.png b/stage6.1_branded_application/pycasa/ui/tasks/images/document-open-recent.png new file mode 100644 index 0000000..ae0859f Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/document-open-recent.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/document-open.png b/stage6.1_branded_application/pycasa/ui/tasks/images/document-open.png new file mode 100644 index 0000000..3432ed2 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/document-open.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/document-save-as.png b/stage6.1_branded_application/pycasa/ui/tasks/images/document-save-as.png new file mode 100644 index 0000000..ed2453d Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/document-save-as.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/document-save.png b/stage6.1_branded_application/pycasa/ui/tasks/images/document-save.png new file mode 100644 index 0000000..cc380a0 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/document-save.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png b/stage6.1_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png new file mode 100644 index 0000000..195560c Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/edit-table-insert-column-left.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png b/stage6.1_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png new file mode 100644 index 0000000..342e372 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/editing-compare-icon.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/folder-blue.png b/stage6.1_branded_application/pycasa/ui/tasks/images/folder-blue.png new file mode 100644 index 0000000..352c3b0 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/folder-blue.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/insert-image.png b/stage6.1_branded_application/pycasa/ui/tasks/images/insert-image.png new file mode 100644 index 0000000..07fdd6f Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/insert-image.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/ipython_icon.png b/stage6.1_branded_application/pycasa/ui/tasks/images/ipython_icon.png new file mode 100644 index 0000000..6b33eba Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/ipython_icon.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/measure.png b/stage6.1_branded_application/pycasa/ui/tasks/images/measure.png new file mode 100644 index 0000000..8686b3f Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/measure.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/text-x-python.png b/stage6.1_branded_application/pycasa/ui/tasks/images/text-x-python.png new file mode 100644 index 0000000..aac29af Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/text-x-python.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/tool-animator.png b/stage6.1_branded_application/pycasa/ui/tasks/images/tool-animator.png new file mode 100644 index 0000000..09043d2 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/tool-animator.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/user-properties.png b/stage6.1_branded_application/pycasa/ui/tasks/images/user-properties.png new file mode 100644 index 0000000..b267971 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/user-properties.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png b/stage6.1_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png new file mode 100644 index 0000000..f3d601c Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/view-media-equalizer.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/images/zoom-draw.png b/stage6.1_branded_application/pycasa/ui/tasks/images/zoom-draw.png new file mode 100644 index 0000000..0758725 Binary files /dev/null and b/stage6.1_branded_application/pycasa/ui/tasks/images/zoom-draw.png differ diff --git a/stage6.1_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py b/stage6.1_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py new file mode 100644 index 0000000..8663edf --- /dev/null +++ b/stage6.1_branded_application/pycasa/ui/tasks/pycasa_browser_pane.py @@ -0,0 +1,28 @@ +# General imports + +# ETS imports +from traits.api import Instance, observe +from traitsui.api import InstanceEditor, Item, View +from pyface.tasks.api import TraitsDockPane + +# Local imports +from ..file_browser_view import FileBrowserView + + +class PycasaBrowserPane(TraitsDockPane): + + id = 'pycasa.file_browser_pane' + + name = "File browser" + + file_browser_view = Instance(FileBrowserView, ()) + + def traits_view(self): + return View( + Item("file_browser_view", editor=InstanceEditor(), style="custom", + show_label=False) + ) + + @observe("file_browser_view.model.requested_item") + def open_in_central_pane(self, event): + self.task.open_in_central_pane(event.new) diff --git a/stage6_branded_application/pycasa/ui/tasks/pycasa_task.py b/stage6.1_branded_application/pycasa/ui/tasks/pycasa_task.py similarity index 100% rename from stage6_branded_application/pycasa/ui/tasks/pycasa_task.py rename to stage6.1_branded_application/pycasa/ui/tasks/pycasa_task.py diff --git a/stage6.1_branded_application/pycasa/ui/tests/__init__.py b/stage6.1_branded_application/pycasa/ui/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stage6.1_branded_application/setup.py b/stage6.1_branded_application/setup.py new file mode 100644 index 0000000..d84c020 --- /dev/null +++ b/stage6.1_branded_application/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup, find_packages + + +setup( + name="pycasa", + version="0.0.1", + description='ETS based GUI application for image exploration and face ' + 'detection', + ext_modules=[], + packages=find_packages(), + data_files=[ + (".", ["README.md"]), + ], +)