Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions stage6.0_branded_application/pycasa/app/app.py
Original file line number Diff line number Diff line change
@@ -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
88 changes: 88 additions & 0 deletions stage6.0_branded_application/pycasa/ui/tasks/pycasa_task.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions stage6.1_branded_application/README.md
Original file line number Diff line number Diff line change
@@ -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.
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions stage6.1_branded_application/pycasa/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

from pycasa.app.app import PycasaApplication


def main():
app = PycasaApplication()
app.run()


if __name__ == '__main__':
main()
Empty file.
9 changes: 9 additions & 0 deletions stage6.1_branded_application/pycasa/model/file_browser.py
Original file line number Diff line number Diff line change
@@ -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
64 changes: 64 additions & 0 deletions stage6.1_branded_application/pycasa/model/image_file.py
Original file line number Diff line number Diff line change
@@ -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
69 changes: 69 additions & 0 deletions stage6.1_branded_application/pycasa/model/image_folder.py
Original file line number Diff line number Diff line change
@@ -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
Empty file.
52 changes: 52 additions & 0 deletions stage6.1_branded_application/pycasa/model/tests/test_image_file.py
Original file line number Diff line number Diff line change
@@ -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)
Loading