Now is time to create an EOPatch
for each out of 293 tiles of the AOI. The EOPatch
is created by filling it with Sentinel-2 data using Sentinel Hub services. We will add the following data to each EOPatch
:
Using the above information we can then also count how many times in a time series a pixel is valid or not from the cloud mask.
An EOPatch
is created and manipulated using EOTasks
chained in an EOWorkflow
. In this example the final workflow is a sequence of the following tasks:
EOPatch
by filling it with RGB L1C datas2cloudless
)%reload_ext autoreload
%autoreload 2
%matplotlib inline
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import numpy as np
from eolearn.core import EOTask, EOPatch, LinearWorkflow, Dependency, FeatureType, SaveToDisk, LoadFromDisk, RemoveFeature, OverwritePermission
from eolearn.io import ExportToTiff
from eolearn.io import S2L1CWCSInput
from eolearn.mask import AddCloudMaskTask, get_s2_pixel_cloud_detector
from eolearn.mask import AddValidDataMaskTask
from eolearn.features import SimpleFilterTask
from sentinelhub import BBoxSplitter, CRS, MimeType
import logging
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
import pickle
from pathlib import Path
data = Path('./')
import os
if not os.path.exists(data/'data'/'valid_count-L1C'):
os.makedirs(data/'data'/'valid_count-L1C')
with open(data/'tile-def'/'slovenia_buffered_bbox_32633_17x25_293.pickle','rb') as fp:
bbox_splitter = pickle.load(fp)
len(bbox_splitter.bbox_list)
293
bbox_splitter.bbox_list[0]
BBox(((370230.5261411405, 5085303.344972428), (380225.31836121203, 5095400.767924464)), crs=EPSG:32633)
bbox_splitter.info_list[0]
{'parent_bbox': BBox(((370230.5261411405, 5024718.807260214), (620100.3316429297, 5196374.997444821)), crs=EPSG:32633), 'index_x': 0, 'index_y': 6}
Valid pixel is if:
IS_DATA == True
CLOUD_MASK == 0
(1 indicates that pixel was identified to be covered with cloud)class SentinelHubValidData:
"""
Combine Sen2Cor's classification map with `IS_DATA` to define a `VALID_DATA_SH` mask
The SentinelHub's cloud mask is asumed to be found in eopatch.mask['CLM']
"""
def __call__(self, eopatch):
return np.logical_and(eopatch.mask['IS_DATA'].astype(np.bool),
np.logical_not(eopatch.mask['CLM'].astype(np.bool)))
class CountValid(EOTask):
"""
The task counts number of valid observations in time-series and stores the results in the timeless mask.
"""
def __init__(self, count_what, feature_name):
self.what = count_what
self.name = feature_name
def execute(self, eopatch):
eopatch.add_feature(FeatureType.MASK_TIMELESS, self.name, np.count_nonzero(eopatch.mask[self.what],axis=0))
return eopatch
INSTANCE_ID = None
# 1. Create `EOPatch` by filling it with RGB L1C data
# Add TRUE COLOR from L1C
# The `TRUE-COLOR-S2-L1C` needs to be defined in your configurator
input_task = S2L1CWCSInput('TRUE-COLOR-S2-L1C', resx='10m', resy='10m', maxcc=0.8)#, instance_id=INSTANCE_ID)
# 2. Run SentinelHub's cloud detector
# (cloud detection is performed at 160m resolution
# and the resulting cloud probability map and mask
# are upscaled to EOPatch's resolution)
cloud_classifier = get_s2_pixel_cloud_detector(average_over=2, dilation_size=1, all_bands=False)
add_clm = AddCloudMaskTask(cloud_classifier, 'BANDS-S2CLOUDLESS', cm_size_y='160m', cm_size_x='160m',
cmask_feature='CLM', cprobs_feature='CLP', instance_id=INSTANCE_ID)
# 3. Validate pixels using SentinelHub's cloud detection mask
add_sh_valmask = AddValidDataMaskTask(SentinelHubValidData(), 'VALID_DATA_SH')
# 4. Count number of valid observations per pixel using valid data mask
count_val_sh = CountValid('VALID_DATA_SH', 'VALID_COUNT_SH')
# 5. Export valid pixel count to tiff file
export_val_sh = ExportToTiff((FeatureType.MASK_TIMELESS, 'VALID_COUNT_SH'))
# 6. Save EOPatch to disk
save = SaveToDisk(str(data/'data'/'eopatch-L1C'), overwrite_permission=OverwritePermission.OVERWRITE_PATCH)
Finished loading model, total used 170 iterations
In this example the workflow is linear
workflow = LinearWorkflow(input_task,
add_clm,
add_sh_valmask, count_val_sh,
export_val_sh, save)
time_interval = ['2017-01-01','2017-12-31']
idx = 0
bbox = bbox_splitter.bbox_list[idx]
info = bbox_splitter.info_list[idx]
tiff_name = f'val_count_sh_eopatch_{idx}_row-{info["index_x"]}_col-{info["index_y"]}.tiff'
patch_name = f'eopatch_{idx}_row-{info["index_x"]}_col-{info["index_y"]}'
results = workflow.execute({input_task:{'bbox':bbox, 'time_interval':time_interval},
export_val_sh:{'filename':str(data/'data'/'valid_count-L1C'/tiff_name)},
save:{'eopatch_folder':patch_name}
})
patch = list(results.values())[-1]
patch
EOPatch( data: { CLP: <class 'numpy.ndarray'>, shape=(81, 1010, 999, 1), dtype=float32 TRUE-COLOR-S2-L1C: <class 'numpy.ndarray'>, shape=(81, 1010, 999, 3), dtype=float32 } mask: { CLM: <class 'numpy.ndarray'>, shape=(81, 1010, 999, 1), dtype=uint8 IS_DATA: <class 'numpy.ndarray'>, shape=(81, 1010, 999, 1), dtype=uint8 VALID_DATA_SH: <class 'numpy.ndarray'>, shape=(81, 1010, 999, 1), dtype=bool } scalar: {} label: {} vector: {} data_timeless: {} mask_timeless: { VALID_COUNT_SH: <class 'numpy.ndarray'>, shape=(1010, 999, 1), dtype=int64 } scalar_timeless: {} label_timeless: {} vector_timeless: {} meta_info: { maxcc: 0.8 service_type: 'wcs' size_x: '10m' size_y: '10m' time_difference: datetime.timedelta(-1, 86399) time_interval: <class 'list'>, length=2 } bbox: BBox(((370230.5261411405, 5085303.344972428), (380225.31836121203, 5095400.767924464)), crs=EPSG:32633) timestamp: <class 'list'>, length=81 )
patch.timestamp
[datetime.datetime(2017, 1, 1, 10, 4, 7), datetime.datetime(2017, 1, 4, 10, 14, 5), datetime.datetime(2017, 1, 11, 10, 3, 51), datetime.datetime(2017, 1, 14, 10, 13, 46), datetime.datetime(2017, 1, 24, 10, 14, 7), datetime.datetime(2017, 2, 10, 10, 1, 32), datetime.datetime(2017, 2, 20, 10, 6, 35), datetime.datetime(2017, 3, 2, 10, 0, 20), datetime.datetime(2017, 3, 12, 10, 7, 6), datetime.datetime(2017, 3, 15, 10, 12, 14), datetime.datetime(2017, 3, 25, 10, 10, 18), datetime.datetime(2017, 4, 1, 10, 0, 22), datetime.datetime(2017, 4, 4, 10, 11, 19), datetime.datetime(2017, 4, 11, 10, 0, 25), datetime.datetime(2017, 4, 14, 10, 13, 50), datetime.datetime(2017, 4, 21, 10, 5, 41), datetime.datetime(2017, 4, 24, 10, 11, 20), datetime.datetime(2017, 5, 4, 10, 13, 49), datetime.datetime(2017, 5, 11, 10, 5, 39), datetime.datetime(2017, 5, 14, 10, 11, 25), datetime.datetime(2017, 5, 21, 10, 0, 29), datetime.datetime(2017, 5, 24, 10, 13, 53), datetime.datetime(2017, 5, 31, 10, 5, 36), datetime.datetime(2017, 6, 3, 10, 10, 26), datetime.datetime(2017, 6, 10, 10, 0, 27), datetime.datetime(2017, 6, 13, 10, 16, 8), datetime.datetime(2017, 6, 20, 10, 4, 53), datetime.datetime(2017, 6, 23, 10, 11, 21), datetime.datetime(2017, 7, 3, 10, 10, 41), datetime.datetime(2017, 7, 5, 10, 0, 26), datetime.datetime(2017, 7, 8, 10, 11, 25), datetime.datetime(2017, 7, 10, 10, 5, 40), datetime.datetime(2017, 7, 13, 10, 11, 19), datetime.datetime(2017, 7, 15, 10, 0, 26), datetime.datetime(2017, 7, 18, 10, 13, 46), datetime.datetime(2017, 7, 20, 10, 0, 27), datetime.datetime(2017, 7, 23, 10, 10, 29), datetime.datetime(2017, 7, 25, 10, 5, 36), datetime.datetime(2017, 7, 28, 10, 10, 24), datetime.datetime(2017, 8, 2, 10, 10, 51), datetime.datetime(2017, 8, 4, 10, 6, 8), datetime.datetime(2017, 8, 7, 10, 13, 49), datetime.datetime(2017, 8, 9, 10, 0, 28), datetime.datetime(2017, 8, 12, 10, 10, 26), datetime.datetime(2017, 8, 17, 10, 10, 21), datetime.datetime(2017, 8, 22, 10, 10, 57), datetime.datetime(2017, 8, 24, 10, 0, 22), datetime.datetime(2017, 8, 27, 10, 13, 12), datetime.datetime(2017, 8, 29, 10, 0, 26), datetime.datetime(2017, 9, 6, 10, 10, 46), datetime.datetime(2017, 9, 8, 10, 6, 55), datetime.datetime(2017, 9, 18, 10, 0, 23), datetime.datetime(2017, 9, 21, 10, 14, 36), datetime.datetime(2017, 9, 23, 10, 5, 2), datetime.datetime(2017, 9, 26, 10, 10, 10), datetime.datetime(2017, 9, 28, 10, 6, 17), datetime.datetime(2017, 10, 1, 10, 13, 50), datetime.datetime(2017, 10, 6, 10, 17, 15), datetime.datetime(2017, 10, 8, 10, 3, 22), datetime.datetime(2017, 10, 11, 10, 11, 46), datetime.datetime(2017, 10, 13, 10, 0, 12), datetime.datetime(2017, 10, 16, 10, 16, 36), datetime.datetime(2017, 10, 18, 10, 2), datetime.datetime(2017, 10, 23, 10, 4, 46), datetime.datetime(2017, 10, 26, 10, 11, 32), datetime.datetime(2017, 10, 31, 10, 14, 6), datetime.datetime(2017, 11, 5, 10, 13, 28), datetime.datetime(2017, 11, 10, 10, 15, 43), datetime.datetime(2017, 11, 12, 10, 2, 29), datetime.datetime(2017, 11, 15, 10, 12, 48), datetime.datetime(2017, 11, 17, 10, 3, 38), datetime.datetime(2017, 11, 20, 10, 13, 20), datetime.datetime(2017, 11, 22, 10, 6, 14), datetime.datetime(2017, 11, 27, 10, 3, 39), datetime.datetime(2017, 12, 2, 10, 6, 47), datetime.datetime(2017, 12, 5, 10, 13, 56), datetime.datetime(2017, 12, 7, 10, 7, 25), datetime.datetime(2017, 12, 17, 10, 5, 40), datetime.datetime(2017, 12, 20, 10, 14, 26), datetime.datetime(2017, 12, 22, 10, 4, 15), datetime.datetime(2017, 12, 25, 10, 15, 32)]
def plot_frame(patch, idx, save_fig=False):
fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(20,20))
axs[0,0].imshow(patch.data['TRUE-COLOR-S2-L1C'][idx])
axs[0,0].set_title(f'RGB {patch.timestamp[idx]}')
axs[0,1].imshow(patch.mask['VALID_DATA_SH'][idx, ..., 0],vmin=0,vmax=1)
axs[0,1].set_title(f'Valid data {patch.timestamp[idx]}')
axs[1,0].imshow(patch.mask['CLM'][idx,...,0],vmin=0,vmax=1)
axs[1,0].set_title(f'SentinelHub Cloud Mask {patch.timestamp[idx]}')
divider = make_axes_locatable(axs[1,1])
cax = divider.append_axes('right', size='5%', pad=0.05)
im = axs[1,1].imshow(patch.data['CLP'][idx,...,0],cmap=plt.cm.inferno, vmin=0.0, vmax=1.0)
fig.colorbar(im, cax=cax, orientation='vertical')
axs[1,1].imshow(patch.data['CLP'][idx,...,0],cmap=plt.cm.inferno)
axs[1,1].set_title(f'SentinelHub Cloud Probability {patch.timestamp[idx]}')
if save_fig:
fig.savefig(f'figs/patch_{idx}_{patch.timestamp[idx]}.png', bbox_inches='tight')
fig.clf()
plot_frame(patch, 0)
plot_frame(patch, -1)
for idx, _ in enumerate(patch.timestamp):
plot_frame(patch, idx, True)
/home/mlubej/software/anaconda3/lib/python3.6/site-packages/matplotlib/pyplot.py:537: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`). max_open_warning, RuntimeWarning)
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
<Figure size 1440x1440 with 0 Axes>
fig, ax = plt.subplots(figsize=(10,10))
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size='5%', pad=0.05)
im = ax.imshow(patch.mask_timeless['VALID_COUNT_SH'][...,0], cmap=plt.cm.inferno, vmin=0, vmax=np.max(patch.mask_timeless['VALID_COUNT_SH']))
ax.set_title('Number of valid observations 2017');
fig.colorbar(im, cax=cax, orientation='vertical')
fig.savefig(f'figs/number_of_valid_observations_eopatch_0.png', bbox_inches='tight')
avecld = np.sum(patch.data['CLP'][...,0],axis=(0))/patch.data['CLP'].shape[0]
fig, ax = plt.subplots(figsize=(10,10))
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size='5%', pad=0.05)
im = ax.imshow(avecld*100, cmap=plt.cm.inferno, vmin=0, vmax=100.0)
ax.set_title('Average cloud probability');
fig.colorbar(im, cax=cax, orientation='vertical')
<matplotlib.colorbar.Colorbar at 0x7f181adc6978>
plt.hist((avecld*100).flatten(),range=(0,100), bins=100, log=True);
plt.hist((patch.data['CLP']*100).flatten(),range=(0,100), bins=100, log=True);
def valid_fraction(arr):
"""
Calculates fraction of non-zero pixels.
"""
return np.count_nonzero(arr)/np.size(arr)
valid_frac = np.apply_along_axis(valid_fraction,axis=1,arr=np.reshape(patch.mask['VALID_DATA_SH'], (patch.mask['VALID_DATA_SH'].shape[0], -1)))
valid_frac.shape
(81,)
fig, ax = plt.subplots(figsize=(20,5))
ax.plot(patch.timestamp, valid_frac,'o-')
ax.set_title('Fraction of valid pixels per frame');
ax.set_xlabel('Date');
ax.set_ylim(0.0,1.2)
ax.grid()
fig.savefig('figs/fraction_valid_pixels_per_frame_eopatch-0.png', bbox_inches='tight')
After inspection of the individual frames in the time series it gets clear that some frames are useless for further analysis -- covered almost completely with clouds. Let's remove frames with 60% or less valid pixels.
For this purpose we will define a new workflow which will:
In the real world example the last two tasks would be part of the original workflow defined above.
class AddValidDataFraction(EOTask):
def execute(self, eopatch):
vld = eopatch.get_feature(FeatureType.MASK, 'VALID_DATA_SH')
frac = np.apply_along_axis(valid_fraction, 1, np.reshape(vld, (vld.shape[0], -1)))
eopatch.add_feature(FeatureType.SCALAR, 'VALID_FRAC', frac[:,np.newaxis])
return eopatch
class ValidDataFractionPredicate:
def __init__(self, threshold):
self.threshold = threshold
def __call__(self, array):
coverage = array[...,0]
return coverage > self.threshold
load = LoadFromDisk(str(data/'data'/'eopatch-L1C'))
add_coverage = AddValidDataFraction()
remove_cloudy_scenes = SimpleFilterTask((FeatureType.SCALAR, 'VALID_FRAC'), ValidDataFractionPredicate(0.6))
clean_up_work = LinearWorkflow(load, add_coverage, remove_cloudy_scenes)
idx = 0
bbox = bbox_splitter.bbox_list[idx]
info = bbox_splitter.info_list[idx]
patch_name = f'eopatch_{idx}_row-{info["index_x"]}_col-{info["index_y"]}'
cleaned_up_results = clean_up_work.execute({load:{'eopatch_folder':patch_name}
})
cleaned_patch = list(cleaned_up_results.values())[-1]
fig, ax = plt.subplots(figsize=(20,5))
ax.plot(cleaned_patch.timestamp, cleaned_patch.scalar['VALID_FRAC'][...,0],'o-')
ax.set_title('Fraction of valid pixels per frame');
ax.set_xlabel('Date');
ax.set_ylim(0.0,1.2)
ax.grid()
fig.savefig('figs/fraction_valid_pixels_per_frame_cleaned-eopatch-0.png', bbox_inches='tight')
Number of valid frames
print(len(cleaned_patch.timestamp))
48
Create a single workflow with all tasks.
# 1. Create `EOPatch` by filling it with RGB L1C data
# Add NDVI from L1C
input_task = S2L1CWCSInput('NDVI', resx='10m', resy='10m', maxcc=0.8)#, instance_id=INSTANCE_ID)
# 2. Run SentinelHub's cloud detector
# (cloud detection is performed at 160m resolution
# and the resulting cloud probability map and mask
# are upscaled to EOPatch's resolution)
cloud_classifier = get_s2_pixel_cloud_detector(average_over=2, dilation_size=1, all_bands=False)
add_clm = AddCloudMaskTask(cloud_classifier, 'BANDS-S2CLOUDLESS', cm_size_y='160m', cm_size_x='160m',
cmask_feature='CLM', cprobs_feature='CLP', instance_id=INSTANCE_ID)
# 3. Delete the NDVI
rm_NDVI = RemoveFeature((FeatureType.DATA,'NDVI'))
# 4. Validate pixels using SentinelHub's cloud detection mask
add_sh_valmask = AddValidDataMaskTask(SentinelHubValidData(), 'VALID_DATA_SH')
# 5. Calculate fraction of valid pixels
add_coverage = AddValidDataFraction()
# 6. Remove frames with fraction of valid pixels below 60%
remove_cloudy_scenes = SimpleFilterTask((FeatureType.SCALAR, 'VALID_FRAC'), ValidDataFractionPredicate(0.6))
# 7. Count number of valid observations per pixel using valid data mask
count_val_sh = CountValid('VALID_DATA_SH', 'VALID_COUNT_SH')
# 8. Export valid pixel count to tiff file
export_val_sh = ExportToTiff((FeatureType.MASK_TIMELESS, 'VALID_COUNT_SH'))
# 9. Save EOPatch to disk
save = SaveToDisk(str(data/'data'/'eopatch-L1C'), overwrite_permission=OverwritePermission.OVERWRITE_PATCH)
Finished loading model, total used 170 iterations
final_workflow = LinearWorkflow(input_task, add_clm, rm_NDVI, add_sh_valmask,
add_coverage, remove_cloudy_scenes, count_val_sh, export_val_sh, save)
def execute_workflow(tile_idx):
bbox = bbox_splitter.bbox_list[tile_idx]
info = bbox_splitter.info_list[tile_idx]
tiff_name = f'val_count_sh_cleaned_eopatch_{tile_idx}_row-{info["index_x"]}_col-{info["index_y"]}.tiff'
patch_name = f'eopatch_{tile_idx}_row-{info["index_x"]}_col-{info["index_y"]}'
results = final_workflow.execute({input_task:{'bbox':bbox, 'time_interval':time_interval},
export_val_sh:{'filename':str(data/'data'/'valid_count-L1C'/tiff_name)},
save:{'eopatch_folder':patch_name}
})
del results
time_interval = ['2017-01-01','2017-12-31']
execute_workflow(0)
Next step would be to run the workflow over all 293 tiles.