"""Image collection class.
Image collection class is similar to AnalysisGrid but for image-based analysis.
Use ImageCollection to collect the path to different images and generate their
combinations using pcomb.
"""
from ..command.pcomb import Pcomb, PcombImage
from collections import defaultdict, OrderedDict
import types
import os
try:
from itertools import izip as zip
except ImportError:
# python 3
pass
import ladybug.dt as dt
# TODO(mostapha): This class needs to be reviewed and tested.
# It has been put together in the last minute before the release.
[docs]class ImageCollection(object):
"""Image collection.
Image collection class is similar to AnalysisGrid but for image-based analysis.
Use ImageCollection to collect the path to different images and generate their
combinations using pcomb.
"""
def __init__(self, name='untitled', output_folder=None):
"""Initiate an image collection."""
# name of sources and their state. It's only meaningful in multi-phase daylight
# analysis. In analysis for a single time it will be {None: [None]}
# It is set inside _createDataStructure method on setting values.
self._sources = OrderedDict()
# an empty list for values
# for each source there will be a new list
# inside each source list there will be a dictionary for each state
# in each dictionary the key is the hoy and the values are a list which
# is [total, direct]. If the value is not available it will be None
self._values = []
self.name = name or 'untitled'
self.output_folder = output_folder or '.'
@property
def sources(self):
"""Get sorted list of light sources.
In most of the cases light sources are window groups.
"""
srcs = range(len(self._sources))
for name, d in self._sources.items():
srcs[d['id']] = name
return srcs
@property
def details(self):
"""Human readable details."""
header = '#hours: {}\n#window groups: {}\n'.format(
len(self.hoys), len(self._sources)
)
sep = '-' * 15
wg = '\nWindow Group {}: {}\n'
st = '....State {}: {}\n'
# sort sources based on ids
sources = range(len(self._sources))
for s, d in self._sources.items():
sources[d['id']] = (s, d)
# create the string for eacj window groups
notes = [header, sep]
for count, s in enumerate(sources):
name, states = s
notes.append(wg.format(count, name))
for count, name in enumerate(states['state']):
notes.append(st.format(count, name))
return ''.join(notes)
@property
def has_values(self):
"""Check if this point has results values."""
return len(self._values) != 0
@property
def hoys(self):
"""Return hours of the year for results if any."""
if not self.has_values:
return []
else:
return sorted(self._values[0][0].keys())
[docs] def source_id(self, source):
"""Get source id from source name."""
# find the id for source and state
try:
return self._sources[source]['id']
except KeyError:
raise ValueError('Invalid source input: {}'.format(source))
[docs] def blind_state_id(self, source, state):
"""Get state id if available."""
try:
return int(state)
except ValueError:
pass
try:
return self._sources[source]['state'].index(state)
except ValueError:
raise ValueError('Invalid state input: {}'.format(state))
[docs] def source_state_name(self, sid, stateid):
"""Get source and state name based on ids."""
source_name = None
for k, v in self._sources.items():
if v['id'] == sid:
source_name = k
break
assert source_name, \
ValueError('Failed to find source name for sid [{}]'.format(sid))
# find state name
try:
return source_name, self._sources[source_name]['state'][stateid]
except IndexError:
raise IndexError(
'State id [{}] is not valid. {} has {} states.'
.format(stateid, source_name, len(self._sources[source_name]['state'])))
@property
def states(self):
"""Get list of states names for each source."""
return tuple(s[1]['state'] for s in self._sources.items())
def _create_data_structure(self, source, state):
"""Create place holders for sources and states if needed.
Returns:
source id and state id as a tuple.
"""
def triple():
"""Place holder for three files.
Total, direct and sun hdr images file path. The final image is the result
of total - direct + sun.
"""
return [None, None, None]
current_sources = self._sources.keys()
if source not in current_sources:
self._sources[source] = {
'id': len(current_sources),
'state': []
}
# append a new list to values for the new source
self._values.append([])
# find the id for source and state
sid = self._sources[source]['id']
if state not in self._sources[source]['state']:
# add sources
self._sources[source]['state'].append(state)
# append a new dictionary for this state
self._values[sid].append(defaultdict(triple))
# find the state id
stateid = self._sources[source]['state'].index(state)
return sid, stateid
[docs] def add_image_file(self, file_path, hoy, source=None, state=None, index=0):
"""Set value for a specific hour of the year.
Args:
value: Value as a number.
hoy: The hour of the year that corresponds to this value.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
index: 0 > Default scene, 1 > Direct, 2 > Sun.
"""
if hoy is None:
return
sid, stateid = self._create_data_structure(source, state)
self._values[sid][stateid][int(hoy * 60)][index] = file_path
[docs] def add_image_files(self, file_paths, hoys, source=None, state=None, index=0):
"""Set values for several hours of the year.
Args:
file_paths: List of file paths as string.
hoys: List of hours of the year that corresponds to input values.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
index: 0 > Default scene, 1 > Direct, 2 > Sun.
"""
if not (isinstance(file_paths, types.GeneratorType) or
isinstance(hoys, types.GeneratorType)):
assert len(file_paths) == len(hoys), \
ValueError(
'Length of values [%d] is not equal to length of hoys [%d].'
% (len(file_paths), len(hoys)))
sid, stateid = self._create_data_structure(source, state)
ind = index or 0
for hoy, value in zip(hoys, file_paths):
if hoy is None:
continue
try:
self._values[sid][stateid][int(hoy * 60)][ind] = value
except Exception as e:
raise ValueError(
'Failed to load {} results for window_group [{}], state[{}]'
' for hour {}.\n{}'.format('direct' if index else 'total',
sid, stateid, hoy, e)
)
[docs] def add_coupled_image_files(self, file_paths, hoys, source=None, state=None):
"""Set total, direct and sun file paths for several hours of the year.
Args:
file_paths: List of filepaths as tuples (total, direct, sun).
hoys: List of hours of the year that corresponds to input values.
source: Name of the source of light. Only needed in case of multiple
sources / window groups (default: None).
state: State of the source if any (default: None).
"""
if not (isinstance(file_paths, types.GeneratorType) or
isinstance(hoys, types.GeneratorType)):
assert len(file_paths) == len(hoys), \
ValueError(
'Length of values [%d] is not equal to length of hoys [%d].'
% (len(file_paths), len(hoys)))
sid, stateid = self._create_data_structure(source, state)
for hoy, filepathCol in zip(hoys, file_paths):
if hoy is None:
continue
try:
self._values[sid][stateid][int(hoy * 60)] = \
filepathCol[0], filepathCol[1], filepathCol[2]
except TypeError:
raise ValueError(
"Wrong input: {}. Input values must be of length of 3."
.format(filepathCol)
)
except IndexError:
raise ValueError(
"Wrong input: {}. Input values must be of length of 3."
.format(filepathCol)
)
[docs] def get_image(self, hoy, source=None, state=None):
"""Get list of images for an hour in the year."""
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return self._values[sid][stateid][int(hoy * 60)]
[docs] def get_images(self, hoys, source=None, state=None):
"""Get list of images for an hour in the year."""
sid = self.source_id(source)
# find the state id
stateid = self.blind_state_id(source, state)
for hoy in hoys:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return tuple(self._values[sid][stateid][int(hoy * 60)] for hoy in hoys)
[docs] def get_image_by_id(self, hoy, sid=None, stateid=None):
"""Get list of images for an hour in the year."""
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return self._values[sid][stateid][int(hoy * 60)]
[docs] def get_images_by_id(self, hoys, sid=None, stateid=None):
"""Get list of images for an hour in the year."""
for hoy in hoys:
if int(hoy * 60) not in self._values[sid][stateid]:
raise ValueError('Hourly values are not available for {}.'
.format(dt.DateTime.from_hoy(hoy)))
return tuple(self._values[sid][stateid][int(hoy * 60)] for hoy in hoys)
[docs] def generate_image(self, hoy, source=None, state=None, mode=0, output=None):
"""Generate the image for an hour of the year for input blindStates."""
# find the id for source and state
images = self.get_image(hoy, source, state)
if mode > 0:
return images[mode - 1]
else:
# generate the image.
fpt, fpd, fps = images
if not output:
output = os.path.join(
self.output_folder,
'{}_{:04d}..{}..{}.hdr'.format(mode, int(hoy), source, state)
)
ti = PcombImage(input_image_file=fpt)
di = PcombImage(input_image_file=fpd, scaling_factor=-1)
si = PcombImage(input_image_file=fps)
res = Pcomb((ti, di, si), output)
return res.execute()
[docs] def generate_image_by_id(self, hoy, sid=None, stateid=None, mode=0, output=None):
"""Generate the image for an hour of the year for input blindStates."""
# find the id for source and state
images = self.get_image_by_id(hoy, sid, stateid)
if mode > 0:
return images[mode - 1]
else:
# generate the image.
fpt, fpd, fps = images
if not output:
output = os.path.join(
self.output_folder,
'{:04d}..{}..{}.hdr'.format(int(hoy), sid, stateid)
)
ti = PcombImage(input_image_file=fpt)
di = PcombImage(input_image_file=fpd, scaling_factor=-1)
si = PcombImage(input_image_file=fps)
res = Pcomb((ti, di, si), output)
return res.execute()
[docs] def generate_images(self, hoys, source=None, state=None, mode=0, outputs=None):
"""Generate images for several hours of the year for input blindStates."""
# find the id for source and state
image_col = self.get_images(hoys, source, state)
if not outputs:
outputs = tuple(
os.path.join(self.output_folder,
'{}_{:04d}..{}..{}.hdr'.format(mode, int(hoy),
source, state))
for hoy in hoys
)
if mode > 0:
return tuple(images[mode - 1] for images in image_col)
else:
results = []
# generate the image.
for images, hoy, output in zip(image_col, hoys, outputs):
fpt, fpd, fps = images
ti = PcombImage(input_image_file=fpt)
di = PcombImage(input_image_file=fpd, scaling_factor=-1)
si = PcombImage(input_image_file=fps)
res = Pcomb((ti, di, si), output)
results.append(res.execute())
return results
[docs] def generate_images_by_id(self, hoys, sid=None, stateid=None, mode=0, outputs=None):
"""Generate images for several hours of the year for input blindStates."""
# find the id for source and state
image_col = self.get_images_by_id(hoys, sid, stateid)
if not outputs:
outputs = tuple(
os.path.join(self.output_folder,
'{}_{:04d}..{}..{}.hdr'.format(mode, int(hoy),
sid, stateid))
for hoy in hoys
)
if mode > 0:
return tuple(images[mode - 1] for images in image_col)
else:
results = []
# generate the image.
for images, hoy, output in zip(image_col, hoys, outputs):
fpt, fpd, fps = images
ti = PcombImage(input_image_file=fpt)
di = PcombImage(input_image_file=fpd, scaling_factor=-1)
si = PcombImage(input_image_file=fps)
res = Pcomb((ti, di, si), output)
results.append(res.execute())
return results
[docs] def generate_combined_image_by_id(
self, hoy, blinds_state_ids=None, mode=0, output=None):
"""Get combined value from all sources based on state_id.
Args:
hoy: hour of the year.
blinds_state_ids: List of state ids for all the sources for an hour. If you
want a source to be removed set the state to -1.
mode: 0 > combined scene, 1 > Default scene, 2 > Direct, 3 > Sun.
Returns:
combined image from all sources.
"""
if not blinds_state_ids:
blinds_state_ids = [0] * len(self._sources)
assert len(self._sources) == len(blinds_state_ids), \
'There should be a state for each source. #sources[{}] != #states[{}]' \
.format(len(self._sources), len(blinds_state_ids))
image_col = []
for sid, stateid in enumerate(blinds_state_ids):
# collect the images for each source
if stateid == -1:
pass
else:
image_col.append(self.get_image_by_id(hoy, sid, stateid))
if not output:
name = '_'.join('%d_%d' % (sid, stateid)
for sid, stateid in enumerate(blinds_state_ids))
output = os.path.join(
self.output_folder,
'{}_{:04d}..{}.hdr'.format(mode, int(hoy), name)
)
if mode > 0:
pcomb_images = tuple(PcombImage(input_image_file=images[mode - 1])
for images in image_col)
else:
# the output is going to be the combined of all the sources
pcomb_images = []
for images in image_col:
# generate the image.
fpt, fpd, fps = images
ti = PcombImage(input_image_file=fpt)
di = PcombImage(input_image_file=fpd, scaling_factor=-1)
si = PcombImage(input_image_file=fps)
pcomb_images.extend((ti, di, si))
res = Pcomb(pcomb_images, output)
return res.execute()
[docs] def generate_combined_images_by_id(self, hoys=None, blinds_state_ids=None, mode=0,
outputs=None):
"""Get combined value from all sources based on state_id.
Args:
hoys: A collection of hours of the year.
blinds_state_ids: List of state ids for all the sources for input hoys. If
you want a source to be removed set the state to -1.
Returns:
Return a generator for (total, direct) values.
"""
hoys = hoys or self.hoys
if not blinds_state_ids:
try:
hours_count = len(hoys)
except TypeError:
raise TypeError('hoys must be an iterable object: {}'.format(hoys))
blinds_state_ids = [[0] * len(self._sources)] * hours_count
assert len(hoys) == len(blinds_state_ids), \
'There should be a list of states for each hour. #states[{}] != #hours[{}]' \
.format(len(blinds_state_ids), len(hoys))
outputs = outputs or []
results = []
for count, hoy in enumerate(hoys):
image_col = []
for sid, stateid in enumerate(blinds_state_ids[count]):
if stateid == -1:
continue
else:
image_col.append(self.get_image_by_id(hoy, sid, stateid))
assert image_col, \
ValueError('All the state ids cannot be -1.')
# create outputs
try:
output = outputs[count]
except IndexError:
name = '_'.join('%d_%d' % (sid, stateid)
for sid, stateid in enumerate(blinds_state_ids[count]))
output = os.path.join(
self.output_folder,
'{}_{:04d}..{}.hdr'.format(mode, int(hoy), name)
)
if mode > 0:
pcomb_images = tuple(PcombImage(input_image_file=images[mode - 1])
for images in image_col)
else:
# the output is going to be the combined of all the sources
pcomb_images = []
for images in image_col:
# generate the image.
fpt, fpd, fps = images
ti = PcombImage(input_image_file=fpt)
di = PcombImage(input_image_file=fpd, scaling_factor=-1)
si = PcombImage(input_image_file=fps)
pcomb_images.extend((ti, di, si))
res = Pcomb(pcomb_images, output)
results.append(res.execute().to_rad_string())
return results
[docs] @staticmethod
def parse_blind_states(blinds_state_ids):
"""Parse input blind states.
The method tries to convert each state to a tuple of a list. Use this method
to parse the input from plugins.
Args:
blinds_state_ids: List of state ids for all the sources for an hour. If you
want a source to be removed set the state to -1. If not provided
a longest combination of states from sources (window groups) will
be used. Length of each item in states should be equal to number
of sources.
"""
try:
combs = [list(eval(cc)) for cc in blinds_state_ids]
except Exception as e:
ValueError('Failed to convert input blind states:\n{}'.format(e))
return combs
[docs] def ToString(self):
"""Overwrite ToString .NET method."""
return self.__repr__()
def __str__(self):
"""String repr."""
return self.__repr__()
def __repr__(self):
"""Return analysis points and directions."""
return 'ImgCol::{}::#{}'.format(self.name, len(self.hoys))