Skip to content

Commit 2149a5f

Browse files
Merge branch 'master' of github.com:jonathanrocher/ets_tutorial
2 parents f17bab6 + 447b980 commit 2149a5f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+936
-0
lines changed

slides/07_background_processing.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
jupyter:
3+
jupytext:
4+
text_representation:
5+
extension: .md
6+
format_name: markdown
7+
format_version: '1.3'
8+
jupytext_version: 1.14.0
9+
kernelspec:
10+
display_name: Python 3 (ipykernel)
11+
language: python
12+
name: python3
13+
---
14+
15+
<!-- #region slideshow={"slide_type": "slide"} -->
16+
## Sharing scientific tools: script to desktop application
17+
18+
### Background processing with Traits Futures
19+
20+
**Jonathan Rocher, Siddhant Wahal, Jason Chambless, Corran Webster, Prabhu Ramachandran**
21+
22+
**SciPy 2022**
23+
24+
<!-- #endregion -->
25+
26+
<!-- #region slideshow={"slide_type": "slide"} -->
27+
## Traits Futures:
28+
29+
- Common problems with GUI frameworks:
30+
- GUIs are unresponsive during heavy computation
31+
- GUI toolkits generally require that widgets are only updated from the
32+
thread from which they were created
33+
- Traits Futures solves both these problems:
34+
- keeping the UI responsive
35+
- safely update the UI in response to calculation results
36+
- Dispatching a background task returns a `future` object which provides:
37+
- information about job status (e.g., job partially finished, completed,
38+
failed)
39+
- access to the job result
40+
- Incoming results arrive as trait changes on the main thread, ensuring thread
41+
safety
42+
- Supports simple callbacks, iterations, and progress-reporting functions
43+
- Supports thread pools (default) and process pools
44+
45+
<!-- #endregion -->
46+
47+
<!-- #region slideshow={"slide_type": "slide"} -->
48+
## Traits Futures
49+
- Let's dive in with an example
50+
- Installation:
51+
- `edm install -e ets_tutorial traits_futures`
52+
- Conda/pip: Activate virtual environment and `pip install traits_futures`
53+
54+
<!-- #endregion -->

stage7.1_background_proc/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Building on the application state in stage 6, this version adds background processing
2+
of image folders using traits futures.

stage7.1_background_proc/pycasa/__init__.py

Whitespace-only changes.

stage7.1_background_proc/pycasa/app/__init__.py

Whitespace-only changes.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# coding=utf-8
2+
""" TaskApplication object for the Pycasa app.
3+
"""
4+
import logging
5+
6+
from pyface.tasks.api import TasksApplication, TaskFactory
7+
from pyface.api import SplashScreen
8+
from pyface.action.api import Action
9+
from pyface.action.schema.api import SchemaAddition, SGroup
10+
11+
from ..ui.tasks.pycasa_task import PycasaTask
12+
from ..ui.image_resources import app_icon, new_icon
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class PycasaApplication(TasksApplication):
18+
""" An application to explore image files and detect faces.
19+
"""
20+
id = "pycasa_application"
21+
22+
name = "Pycasa"
23+
24+
description = "An example Tasks application that explores image files."
25+
26+
def _task_factories_default(self):
27+
return [
28+
TaskFactory(
29+
id='pycasa.pycasa_task_factory',
30+
name="Main Pycasa Task Factory",
31+
factory=PycasaTask
32+
)
33+
]
34+
35+
def _icon_default(self):
36+
return app_icon
37+
38+
def _splash_screen_default(self):
39+
splash_screen = SplashScreen(image=app_icon)
40+
return splash_screen
41+
42+
def create_new_task_window(self):
43+
from pyface.tasks.task_window_layout import TaskWindowLayout
44+
45+
layout = TaskWindowLayout()
46+
layout.items = [self.task_factories[0].id]
47+
window = self.create_window(layout=layout)
48+
self.add_window(window)
49+
window.title += " {}".format(len(self.windows))
50+
return window
51+
52+
def create_new_task_menu(self):
53+
return SGroup(
54+
Action(name="New",
55+
accelerator='Ctrl+N',
56+
on_perform=self.create_new_task_window,
57+
image=new_icon),
58+
id='NewGroup', name='NewGroup',
59+
)
60+
61+
def _extra_actions_default(self):
62+
extra_actions = [
63+
SchemaAddition(id='pycasa.custom_new',
64+
factory=self.create_new_task_menu,
65+
path="MenuBar/File/OpenGroup",
66+
absolute_position="first")
67+
]
68+
return extra_actions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
from pycasa.app.app import PycasaApplication
3+
4+
5+
def main():
6+
app = PycasaApplication()
7+
app.run()
8+
9+
10+
if __name__ == '__main__':
11+
main()

stage7.1_background_proc/pycasa/model/__init__.py

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from os.path import expanduser
2+
from traits.api import Directory, Event, HasStrictTraits
3+
4+
5+
class FileBrowser(HasStrictTraits):
6+
root = Directory(expanduser("~"))
7+
8+
#: Item last double-clicked on in the tree view
9+
requested_item = Event
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# General imports
2+
from os.path import splitext
3+
import PIL.Image
4+
from PIL.ExifTags import TAGS
5+
from skimage import data
6+
from skimage.feature import Cascade
7+
import numpy as np
8+
9+
# ETS imports
10+
from traits.api import (
11+
Array, cached_property, Dict, File, HasStrictTraits, List, Property
12+
)
13+
14+
SUPPORTED_FORMATS = [".png", ".jpg", ".jpeg", ".PNG", ".JPG", ".JPEG"]
15+
16+
17+
class ImageFile(HasStrictTraits):
18+
""" Model to hold an image file.
19+
"""
20+
filepath = File
21+
22+
metadata = Property(Dict, depends_on="filepath")
23+
24+
data = Property(Array, depends_on="filepath")
25+
26+
faces = List
27+
28+
def _is_valid_file(self):
29+
return (
30+
bool(self.filepath) and
31+
splitext(self.filepath)[1].lower() in SUPPORTED_FORMATS
32+
)
33+
34+
@cached_property
35+
def _get_data(self):
36+
if not self._is_valid_file():
37+
return np.array([])
38+
with PIL.Image.open(self.filepath) as img:
39+
return np.asarray(img)
40+
41+
@cached_property
42+
def _get_metadata(self):
43+
if not self._is_valid_file():
44+
return {}
45+
with PIL.Image.open(self.filepath) as img:
46+
exif = img._getexif()
47+
if not exif:
48+
return {}
49+
return {TAGS[k]: v for k, v in exif.items() if k in TAGS}
50+
51+
def detect_faces(self):
52+
# Load the trained file from the module root.
53+
trained_file = data.lbp_frontal_face_cascade_filename()
54+
55+
# Initialize the detector cascade.
56+
detector = Cascade(trained_file)
57+
58+
detected = detector.detect_multi_scale(img=self.data,
59+
scale_factor=1.2,
60+
step_ratio=1,
61+
min_size=(60, 60),
62+
max_size=(600, 600))
63+
self.faces = detected
64+
return self.faces
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# General imports
2+
import glob
3+
from os.path import basename, expanduser, isdir
4+
5+
import numpy as np
6+
import pandas as pd
7+
8+
# ETS imports
9+
from traits.api import (
10+
Bool, Directory, Event, HasStrictTraits, Instance, List, observe, Property
11+
)
12+
from traits_futures.api import (
13+
CallFuture, submit_call, TraitsExecutor
14+
)
15+
16+
# Local imports
17+
from pycasa.model.image_file import ImageFile, SUPPORTED_FORMATS
18+
19+
FILENAME_COL = "filename"
20+
NUM_FACE_COL = "Num. faces"
21+
22+
23+
class ImageFolder(HasStrictTraits):
24+
""" Model for a folder of images.
25+
"""
26+
directory = Directory(expanduser("~"))
27+
28+
images = List(Instance(ImageFile))
29+
30+
data = Instance(pd.DataFrame)
31+
32+
data_updated = Event
33+
34+
traits_executor = Instance(TraitsExecutor)
35+
36+
future = Instance(CallFuture)
37+
38+
executor_idle = Property(Bool, depends_on="future.done")
39+
40+
def __init__(self, **traits):
41+
# Don't forget this!
42+
super(ImageFolder, self).__init__(**traits)
43+
if not isdir(self.directory):
44+
msg = f"The provided directory isn't a real directory: " \
45+
f"{self.directory}"
46+
raise ValueError(msg)
47+
self.data = self._create_metadata_df()
48+
49+
@observe("directory")
50+
def _update_images(self, event):
51+
self.images = [
52+
ImageFile(filepath=file)
53+
for fmt in SUPPORTED_FORMATS
54+
for file in glob.glob(f"{self.directory}/*{fmt}")
55+
]
56+
57+
@observe("images.items")
58+
def _update_metadata(self, event):
59+
self.data = self._create_metadata_df()
60+
61+
def _create_metadata_df(self):
62+
if not self.images:
63+
return pd.DataFrame({FILENAME_COL: [], NUM_FACE_COL: []})
64+
return pd.DataFrame([
65+
{
66+
FILENAME_COL: basename(img.filepath),
67+
NUM_FACE_COL: np.nan,
68+
**img.metadata
69+
70+
}
71+
for img in self.images
72+
])
73+
74+
def compute_num_faces_background(self, **kwargs):
75+
self.future = submit_call(
76+
self.traits_executor,
77+
self._compute_num_faces,
78+
**kwargs
79+
)
80+
81+
def _compute_num_faces(self, **kwargs):
82+
return [
83+
len(img_file.detect_faces(**kwargs))
84+
for img_file in self.images
85+
]
86+
87+
@observe("future:done")
88+
def _update_data(self, event):
89+
num_faces_all_images = self.future.result
90+
for idx, num_faces in enumerate(num_faces_all_images):
91+
self.data[NUM_FACE_COL].iat[idx] = num_faces
92+
self.data_updated = True
93+
94+
def _get_executor_idle(self):
95+
return self.future is None or self.future.done

0 commit comments

Comments
 (0)