Skip to content

Commit 390fc07

Browse files
Re-align models and views to the unified ones.
1 parent fca2d06 commit 390fc07

6 files changed

Lines changed: 110 additions & 86 deletions

File tree

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# General imports
2-
import os
2+
from os.path import splitext
33
import PIL.Image
44
from PIL.ExifTags import TAGS
55
from skimage import data
66
from skimage.feature import Cascade
77
import numpy as np
88

99
# ETS imports
10-
from traits.api import Array, cached_property, Dict, File, HasStrictTraits, \
11-
List, Property
10+
from traits.api import (
11+
Array, cached_property, Dict, File, HasStrictTraits, List, Property
12+
)
1213

13-
SUPPORTED_FORMATS = [".png", ".jpg", ".jpeg"]
14+
SUPPORTED_FORMATS = [".png", ".jpg", ".jpeg", ".PNG", ".JPG", ".JPEG"]
1415

1516

1617
class ImageFile(HasStrictTraits):
@@ -24,43 +25,40 @@ class ImageFile(HasStrictTraits):
2425

2526
faces = List
2627

27-
def to_array(self):
28-
file_ext = os.path.splitext(self.filepath)[1].lower()
29-
if not self.filepath or file_ext not in SUPPORTED_FORMATS:
30-
return np.array([])
31-
32-
with PIL.Image.open(self.filepath) as img:
33-
return np.asarray(img)
28+
def _is_valid_file(self):
29+
return (
30+
bool(self.filepath) and
31+
splitext(self.filepath)[1].lower() in SUPPORTED_FORMATS
32+
)
3433

3534
@cached_property
3635
def _get_data(self):
37-
return self.to_array()
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)
3840

3941
@cached_property
4042
def _get_metadata(self):
41-
file_ext = os.path.splitext(self.filepath)[1].lower()
42-
if not self.filepath or file_ext not in SUPPORTED_FORMATS:
43+
if not self._is_valid_file():
4344
return {}
44-
4545
with PIL.Image.open(self.filepath) as img:
4646
exif = img._getexif()
47-
48-
if exif:
49-
return {TAGS[k]: v for k, v in exif.items()
50-
if k in TAGS}
51-
else:
47+
if not exif:
5248
return {}
49+
return {TAGS[k]: v for k, v in exif.items() if k in TAGS}
5350

54-
def detect_faces(self, scale_factor=1.2, step_ratio=1, min_size=60,
55-
max_size=600):
56-
""" Detect faces in the image.
57-
"""
51+
def detect_faces(self):
52+
# Load the trained file from the module root.
5853
trained_file = data.lbp_frontal_face_cascade_filename()
54+
55+
# Initialize the detector cascade.
5956
detector = Cascade(trained_file)
60-
faces = detector.detect_multi_scale(img=self.data,
61-
scale_factor=scale_factor,
62-
step_ratio=step_ratio,
63-
min_size=(min_size, min_size),
64-
max_size=(max_size, max_size))
65-
self.faces = faces
66-
return faces
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: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
# General imports
2-
import os
2+
import glob
3+
from os.path import basename, expanduser, isdir
34

4-
import pandas as pd
55
import numpy as np
6+
import pandas as pd
67

78
# ETS imports
8-
from traits.api import Directory, Event, HasStrictTraits, Instance
9+
from traits.api import (
10+
Directory, Event, HasStrictTraits, Instance, List, observe,
11+
)
912

1013
# Local imports
11-
from .image_file import ImageFile, SUPPORTED_FORMATS
14+
from pycasa.model.image_file import ImageFile, SUPPORTED_FORMATS
1215

1316
FILENAME_COL = "filename"
14-
1517
NUM_FACE_COL = "Num. faces"
1618

1719

1820
class ImageFolder(HasStrictTraits):
19-
""" Model to hold an image folder.
21+
""" Model for a folder of images.
2022
"""
21-
path = Directory
23+
directory = Directory(expanduser("~"))
24+
25+
images = List(Instance(ImageFile))
2226

2327
data = Instance(pd.DataFrame)
2428

@@ -27,39 +31,39 @@ class ImageFolder(HasStrictTraits):
2731
def __init__(self, **traits):
2832
# Don't forget this!
2933
super(ImageFolder, self).__init__(**traits)
30-
if not os.path.isdir(self.path):
31-
msg = f"Unable to create an ImageFolder from {self.path} since" \
32-
f" it is not a valid directory."
34+
if not isdir(self.directory):
35+
msg = f"The provided directory isn't a real directory: " \
36+
f"{self.directory}"
3337
raise ValueError(msg)
38+
self.data = self._create_metadata_df()
3439

35-
self.data = self.to_dataframe()
40+
@observe("directory")
41+
def _update_images(self, event):
42+
self.images = [
43+
ImageFile(filepath=file)
44+
for fmt in SUPPORTED_FORMATS
45+
for file in glob.glob(f"{self.directory}/*{fmt}")
46+
]
3647

37-
def to_dataframe(self):
38-
if not self.path:
39-
return pd.DataFrame({FILENAME_COL: [], NUM_FACE_COL: []})
48+
@observe("images.items")
49+
def _update_metadata(self, event):
50+
self.data = self._create_metadata_df()
4051

41-
data = []
42-
for filename in os.listdir(self.path):
43-
file_ext = os.path.splitext(filename)[1].lower()
44-
if file_ext in SUPPORTED_FORMATS:
45-
filepath = os.path.join(self.path, filename)
46-
img_file = ImageFile(filepath=filepath)
47-
file_data = {FILENAME_COL: filename, NUM_FACE_COL: np.nan}
48-
try:
49-
file_data.update(img_file.metadata)
50-
except Exception:
51-
pass
52-
data.append(file_data)
52+
def _create_metadata_df(self):
53+
if not self.images:
54+
return pd.DataFrame({FILENAME_COL: [], NUM_FACE_COL: []})
55+
return pd.DataFrame([
56+
{
57+
FILENAME_COL: basename(img.filepath),
58+
NUM_FACE_COL: np.nan,
59+
**img.metadata
5360

54-
return pd.DataFrame(data)
61+
}
62+
for img in self.images
63+
])
5564

5665
def compute_num_faces(self, **kwargs):
57-
cols = list(self.data.columns)
58-
for i, filename in enumerate(self.data[FILENAME_COL]):
59-
print(filename)
60-
filepath = os.path.join(self.path, filename)
61-
img_file = ImageFile(filepath=filepath)
62-
faces = img_file.detect_faces(**kwargs)
63-
j = cols.index(NUM_FACE_COL)
64-
self.data.iloc[i, j] = len(faces)
66+
for i, image in enumerate(self.images):
67+
faces = image.detect_faces(**kwargs)
68+
self.data[NUM_FACE_COL].iat[i] = len(faces)
6569
self.data_updated = True

stage8_packaging/pycasa/ui/image_file_view.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,18 @@ class ImageFileView(ModelView):
3434
def build_mpl_figure(self, event):
3535
figure = Figure()
3636
axes = figure.add_subplot(111)
37-
axes.imshow(self.model.to_array())
37+
axes.imshow(self.model.data)
3838
self.figure = figure
3939

40-
def _detect_button_fired(self):
40+
@observe("detect_button")
41+
def _detect_button_fired(self, event):
4142
self.model.detect_faces()
4243

4344
@observe("model.faces")
4445
def update_mpl_figure_with_faces(self, events):
4546
figure = Figure()
4647
axes = figure.add_subplot(111)
47-
axes.imshow(self.model.to_array())
48+
axes.imshow(self.model.data)
4849

4950
for face in self.model.faces:
5051
axes.add_patch(

stage8_packaging/pycasa/ui/image_folder_editor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def create(self, parent):
3030
# -------------------------------------------------------------------------
3131

3232
def _get_name(self):
33-
return self.obj.path[:25]
33+
return self.obj.directory[:25]
3434

3535
def _get_tooltip(self):
36-
return self.obj.path
36+
return self.obj.directory

stage8_packaging/pycasa/ui/image_folder_view.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from traitsui.ui_editors.data_frame_editor import DataFrameEditor
1010

1111
# Local imports
12-
from ..model.image_folder import FILENAME_COL, ImageFolder, NUM_FACE_COL
12+
from pycasa.model.image_folder import FILENAME_COL, ImageFolder, NUM_FACE_COL
1313

1414

1515
DISPLAYED_COLUMNS = [FILENAME_COL, NUM_FACE_COL] + [
@@ -19,6 +19,12 @@
1919
'ISOSpeedRatings', 'SceneType'
2020
]
2121

22+
YEAR_KEY = "__year__"
23+
24+
DATETIME_COL = "DateTime"
25+
26+
MAKE_COL = 'Make'
27+
2228

2329
class ImageFolderView(ModelView):
2430
""" ModelView for an image folder object.
@@ -30,6 +36,10 @@ class ImageFolderView(ModelView):
3036
# Filters widgets
3137
view_filter_controls = Bool
3238

39+
# Copy of the model's data, with filtering columns added if missing
40+
all_data = Instance(pd.DataFrame)
41+
42+
# Filtered dataframe based on filtering widgets
3343
filtered_data = Instance(pd.DataFrame)
3444

3545
year_mask = Instance(pd.Series)
@@ -44,7 +54,7 @@ class ImageFolderView(ModelView):
4454

4555
def traits_view(self):
4656
view = View(
47-
Item("model.path", style="readonly", show_label=False),
57+
Item("model.directory", style="readonly", show_label=False),
4858
HGroup(
4959
Spring(),
5060
Item("view_filter_controls"),
@@ -73,7 +83,6 @@ def traits_view(self):
7383
enabled_when="len(model.data) > 0"),
7484
Spring(),
7585
),
76-
resizable=True, height=800
7786
)
7887
return view
7988

@@ -82,22 +91,22 @@ def traits_view(self):
8291
@observe("scan")
8392
def scan_for_faces(self, event):
8493
self.model.compute_num_faces()
94+
self.all_data.update(self.model.data)
8595

8696
@observe("selected_years")
8797
def update_years(self, event):
88-
self.year_mask = self.model.data['Year'].isin(self.selected_years)
98+
self.year_mask = self.all_data[YEAR_KEY].isin(self.selected_years)
8999

90100
@observe("selected_make")
91101
def update_make(self, event):
92102
if self.selected_make == "All":
93-
self.make_mask = pd.Series(np.ones(len(self.model.data),
94-
dtype=bool))
103+
self.make_mask = pd.Series([True] * len(self.model.data))
95104
else:
96-
self.make_mask = self.model.data['Make'] == self.selected_make
105+
self.make_mask = self.all_data[MAKE_COL] == self.selected_make
97106

98-
@observe("year_mask, make_mask")
107+
@observe("year_mask, make_mask, all_data")
99108
def update_filtered_data(self, event):
100-
self.filtered_data = self.model.data[self.year_mask & self.make_mask]
109+
self.filtered_data = self.all_data[self.year_mask & self.make_mask]
101110

102111
# Initialization methods --------------------------------------------------
103112

@@ -107,15 +116,27 @@ def _make_mask_default(self):
107116
def _year_mask_default(self):
108117
return pd.Series(np.ones(len(self.model.data), dtype=bool))
109118

110-
def _filtered_data_default(self):
111-
return self.model.data
119+
def _all_data_default(self):
120+
# Enrich metadata with missing fields: date time, make
121+
data = self.model.data.copy()
122+
123+
if DATETIME_COL not in data.columns:
124+
data[DATETIME_COL] = np.nan
125+
126+
if MAKE_COL not in data.columns:
127+
data[MAKE_COL] = np.nan
112128

113-
def _all_years_default(self):
114129
def parse_year(x):
115130
return x.split(":")[0] if isinstance(x, str) else "unknown"
131+
data[YEAR_KEY] = data[DATETIME_COL].apply(parse_year)
132+
133+
return data
116134

117-
self.model.data['Year'] = self.model.data['DateTime'].apply(parse_year)
118-
return sorted(self.model.data['Year'].unique().tolist())
135+
def _filtered_data_default(self):
136+
return self.all_data
137+
138+
def _all_years_default(self):
139+
return sorted(self.all_data[YEAR_KEY].unique().tolist())
119140

120141

121142
if __name__ == '__main__':

stage8_packaging/pycasa/ui/tasks/pycasa_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def open_in_central_pane(self, filepath):
5757
obj = ImageFile(filepath=filepath)
5858
self.central_pane.edit(obj, factory=ImageFileEditor)
5959
elif file_ext == "":
60-
obj = ImageFolder(path=filepath)
60+
obj = ImageFolder(directory=filepath)
6161
self.central_pane.edit(obj, factory=ImageFolderEditor)
6262
else:
6363
print("Unsupported file format: {}".format(file_ext))

0 commit comments

Comments
 (0)