Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*.tar
*.tgz
*.egg-info
*.DS_Store
102 changes: 80 additions & 22 deletions skdata/pubfig83.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Dan Yamins <yamins@mit.edu>
# James Bergstra <bergstra@rowland.harvard.edu>
# Nicolas Pinto <pinto@rowland.harvard.edu>
# Giovani Chiachia <chiachia@rowland.harvard.edu>

# License: Simplified BSD

Expand All @@ -30,11 +31,15 @@
from glob import glob
import hashlib

import larray
from data_home import get_data_home
from utils import download, extract
from utils import download, extract, int_labels
import utils
import utils.image
from utils.image import ImgLoader

from sklearn import cross_validation
import numpy as np

class PubFig83(object):
"""PubFig83 Face Dataset
Expand Down Expand Up @@ -82,16 +87,8 @@ class PubFig83(object):
def __init__(self, meta=None):
if meta is not None:
self._meta = meta

self.name = self.__class__.__name__

try:
from joblib import Memory
mem = Memory(cachedir=self.home('cache'))
self._get_meta = mem.cache(self._get_meta)
except ImportError:
pass

def home(self, *suffix_paths):
return path.join(get_data_home(), self.name, *suffix_paths)

Expand Down Expand Up @@ -129,29 +126,71 @@ def fetch(self, download_if_missing=True):

@property
def meta(self):
if hasattr(self, '_meta'):
return self._meta
else:
if not hasattr(self, '_meta'):
self.fetch(download_if_missing=True)
self._meta = self._get_meta()
return self._meta
return self._meta

def _get_meta(self):
names = sorted(os.listdir(self.home('pubfig83')))
names2 = sorted(os.listdir(self.home('pubfig83')))
genders = self._GENDERS
assert len(names) == len(genders)
assert len(names2) == len(genders)
meta = []
ind = 0
for gender, name in zip(genders, names):
for gender, name in zip(genders, names2):
img_filenames = sorted(glob(self.home('pubfig83', name, '*.jpg')))
for img_filename in img_filenames:
img_data = open(img_filename, 'rb').read()
sha1 = hashlib.sha1(img_data).hexdigest()
meta.append(dict(gender=gender, name=name, id=ind,
filename=img_filename, sha1=sha1))
ind += 1

return meta

@property
def names(self):
if not hasattr(self, '_names'):
self._names = np.array([self.meta[ind]['name'] for ind in xrange(len(self.meta))])
return self._names

@property
def classification_splits(self):
"""
generates splits and attaches them in the "splits" attribute

"""
if not hasattr(self, '_classification_splits'):
self._classification_splits = self._generate_classification_splits()
return self._classification_splits

def _generate_classification_splits(self):
meta = self.meta
rng = np.random.RandomState(0)
classification_splits = {}

splits = {}
labels = np.unique(self.names)
for label in labels:
samples_to_consider = (self.names == label)
samples_to_consider = np.where(samples_to_consider)[0]
assert len(samples_to_consider) >= 100
p = rng.permutation(len(samples_to_consider))
if 'Test' not in splits:
splits['Test'] = []
splits['Test'].extend(samples_to_consider[p[:10]])
remainder = samples_to_consider[p[10:]]
for _ind in range(5):
p = rng.permutation(len(remainder))
if 'Train%d' % _ind not in splits:
splits['Train%d' % _ind] = []
splits['Train%d' % _ind].extend(remainder[p[:80]].copy())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to have hardcoded values here (eg 90, 10, 80) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally preferred the variable ntest, ntrain approach, like you suggest. But, on the other hand, it might be good to have "standard" splits. I've occasionally heard @zstone and @davidcox talking about standard 90/10 splits. Of course, we COULD make them variables and then just set defaults. I think it depends on how the creators of this dataset intended to be used -- @npinto, you and @zstone worked on this together (?) so if you think giving people guidance that suggests that non-standard-sized splits are "OK", then that seems fine to me.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support the creation of a set of standard splits of any dataset, but I would favor canonizing a minimal, language-agnostic materialization of said standard splits rather than defining them implicitly by code in a specific language that must be run correctly in a specific environment to regenerate the right splits. Concretely, I would advocate looking for the simplest-possible JSON format that would completely specify, say, ten 90/10 splits with reference to relative image paths within a canonical archive of a dataset.

That's just my opinion, though, and it may not fit the way scikit-data already works!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zstone I'm not sure if it's obvious how to use JSON inside scikit-data in a natural way. however, could you comment on the split scheme currently proposed in the code? I just added support for non-standard splits. I put the split related data in the dataset init, because we might want to have the split data travel with the dataset instance. The current code has the idea that to change splits, you have to re-instantiate a new instance (or I guess, monkey around inside the code; probably the various attributes should be private). What do you think?

if 'Validate%d' % _ind not in splits:
splits['Validate%d' % _ind] = []
splits['Validate%d' % _ind].extend(remainder[p[80:90]].copy())

return splits

# ------------------------------------------------------------------------
# -- Dataset Interface: clean_up()
# ------------------------------------------------------------------------
Expand All @@ -165,23 +204,42 @@ def clean_up(self):
# ------------------------------------------------------------------------

def image_path(self, m):
return self.home('pubfig83', m['name'], m['jpgfile'])
return self.home('pubfig83', m['name'], m['filename'])
#return self.home('pubfig83', m['name'], m['jpgfile'])

# ------------------------------------------------------------------------
# -- Standard Tasks
# ------------------------------------------------------------------------

def raw_recognition_task(self):
names = [m['name'] for m in self.meta]
paths = [self.image_path(m) for m in self.meta]
labels = utils.int_labels(names)
return paths, labels
def raw_classification_task(self, split=None):
"""
:param split: an integer from 0 to 9 inclusive.
:param split_role: either 'train' or 'test'

:returns: either all samples (when split_k=None) or the specific
train/test split
"""

if split is not None:
inds = self.classification_splits[split]
else:
inds = range(len(self.meta))
names = self.names[inds]
paths = [self.meta[ind]['filename'] for ind in inds]
labels = int_labels(names)
return paths, labels, inds

def raw_gender_task(self):
genders = [m['gender'] for m in self.meta]
paths = [self.image_path(m) for m in self.meta]
return paths, utils.int_labels(genders)

def img_classification_task(self, dtype='uint8', split=None):
img_paths, labels, inds = self.raw_classification_task(split=split)
imgs = larray.lmap(ImgLoader(shape=(100, 100, 3), dtype=dtype, mode='RGB'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here, why hardcoding 100, 100 ? this could be done upstream

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again ... this is a "standard" issue; This basically checks that the images are in the right format and gives people a standard way to load them. I agree it could be handled downstream ... in fact, we overload the equivalent method in the lfw stuff we're doing now. I think the question is really how hard you want to make it to NOT use the "standard." We could not only make the size variable, but also the mode (e.g. allow people to pass a desired mode and then do the conversion for them, and also various resizing option variables, &c...) If your feeling is to give people maximum flexibility through this particular interface -- and set the "standards" via easily-changeable default values -- that's fine with me.

img_paths)
return imgs, labels


# ------------------------------------------------------------------------
# -- Drivers for skdata/bin executables
Expand Down
Loading