Skip to content

Commit 5f91d5f

Browse files
Alex-Lewandowskiemlundell
authored andcommitted
First major release of updated labs and non-lab recipes (#22)
* Add refactored lab 3 and lab 4 Not yet completed * update Lab3, 4. Add Lab 4B, 5 * backup lab 3, 4, 4b, 5. add lab 6 * update lab 3 and 5 * update labs 3, 4, 4B started working on 4B, using my newly created Hyp subscription. Refactored strings using Python3's new f-string format. * update Lab 4B refactoring ~75% complete * update Labs 4, 4B, 5, 6 * add Learn_Vertex_API.ipynb * add asf_notebook.py * change data_level to processing_level add processingLevel param to post request instead of using if stmnts * update Lab4B, 5(about done), and asf_notebook.py * update Labs 4, 5. asf_notebook.py * updates Lab_4B_dev and asf_notebook.py * update lab4B.ipynb * update Labs 4,4B,5,6,8 and asf_notebook.py * update asf_notebook.py add Lab8_dev.ipynb * update Lab8_dev * bring lab8 to stage 1 * Update Labs 3,4,4B,5,6,7_cosine,7_Taizhou, 8, 8_own, and asf-notebook.py * add filter by path and flight direction * add Lab9_Hyp3_dev * update lab4B, lab8_own, and asf_notebook.py * update all labs except 3 * update Lab4 add recipe_notebooks * remove ASF/GEOS_657_Labs/Lab4-copy2.ipynb * remove ASF/GEOS_657_Labs/Lab4B-copy2.ipynb * remove ASF/GEOS_657_Labs/Lab4B-dev.ipynb * remove ASF/GEOS_657_Labs/Lab5-Copy2.ipynb * remove ASF/GEOS_657_Labs/Lab6_copy2.ipynb * remove GEOS 657-Lab3-SARProcessingAndGeocoding-Copy1.ipynb * remove GEOS 657-Lab4-SARTimeSeriesAnalysis-Copy1.ipynb * remove GEOS 657-Lab4B-SARTimeSeriesAnalysis-OwnData-Copy1.ipynb * remove GEOS 657-Lab5-InSARwithSNAP-Copy1.ipynb * remove GEOS 657-Lab6-VolcanoSourceModelingfromInSAR-Copy1.ipynb * rename recipe_notebooks to Recipe_Notebooks * delete Recipe_Notebooks/Time_Series_Example.ipynb * add Recipe_Notebooks * add remaining recipe notebooks * update lab4*, 8_own and non-lab versions of same. asf_notebook.py * update frame merge in lab4B, 8, and non-lab versions * update all notebooks with final changes for pull request * removed data_change_detection_amplitude_time_series_hyp3.ipynb * removed dev copies of labs * remove lab9_hyp3.dev * add SAR_Training and recipes to NISAR * Update Readme to Spanish * update all copies asf_notebook.py * update all copies asf_notebook.py -date_range_valid(): return False for None dates -flight_direction_valid(): return False for None direction -refactor download_hyp3_products() to account for refactored date_range_valid and flight_direction_valid() -add type hints to get_vertex_granule_info() * update all notebooks -organize import statements -set GDAL raster variables to None when done with them -Test all notebooks to confirm that they run successfully * update storage paths in NISAR, SAR_Training/English notebooks * remove NISAR/Recipes * remove ASF/Recipe_Notebooks * add supporting docs to SAR_Training/English
1 parent ed55893 commit 5f91d5f

File tree

57 files changed

+718923
-2559
lines changed

Some content is hidden

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

57 files changed

+718923
-2559
lines changed

ASF/GEOS_657_Labs/GEOS 657-Lab3-SARProcessingAndGeocoding.ipynb

+219-197
Large diffs are not rendered by default.

ASF/GEOS_657_Labs/GEOS 657-Lab4-SARTimeSeriesAnalysis.ipynb

+689,898-250
Large diffs are not rendered by default.

ASF/GEOS_657_Labs/GEOS 657-Lab4B-SARTimeSeriesAnalysis-OwnData.ipynb

+765-278
Large diffs are not rendered by default.

ASF/GEOS_657_Labs/GEOS 657-Lab5-InSARwithSNAP.ipynb

+811-285
Large diffs are not rendered by default.

ASF/GEOS_657_Labs/GEOS 657-Lab6-VolcanoSourceModelingfromInSAR.ipynb

+337-211
Large diffs are not rendered by default.

ASF/GEOS_657_Labs/GEOS 657-Lab7-DeepLearning-CosineDemo.ipynb

+285-157
Large diffs are not rendered by default.

ASF/GEOS_657_Labs/GEOS 657-Lab7-DeepLearning-TaizhouChangeDetect.ipynb

+191-62
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,72 @@
2929
"<hr>"
3030
]
3131
},
32+
{
33+
"cell_type": "markdown",
34+
"metadata": {},
35+
"source": [
36+
"<hr>\n",
37+
"<font face=\"Calibri\" size=\"5\" color=\"red\"> <b>Important Note about JupyterHub</b> </font>\n",
38+
"<br><br>\n",
39+
"<font face=\"Calibri\" size=\"3\"> <b>Your JupyterHub server will automatically shutdown when left idle for more than 1 hour. Your notebooks will not be lost but you will have to restart their kernels and re-run them from the beginning. You will not be able to seamlessly continue running a partially run notebook.</b> </font>"
40+
]
41+
},
42+
{
43+
"cell_type": "markdown",
44+
"metadata": {},
45+
"source": [
46+
"<hr>\n",
47+
"<font face=\"Calibri\" size=\"5\"> <b>0. Importing Relevant Python Packages </b> </font>\n",
48+
"\n",
49+
"<font size=\"3\">Our first step is to <b>import the necessary python libraries into your Jupyter Notebook.</b></font>"
50+
]
51+
},
3252
{
3353
"cell_type": "code",
3454
"execution_count": null,
3555
"metadata": {},
3656
"outputs": [],
3757
"source": [
58+
"import os\n",
59+
"\n",
3860
"import scipy.io as sio\n",
3961
"import numpy as np\n",
62+
"import matplotlib.pyplot as plt\n",
4063
"from keras.optimizers import Nadam\n",
41-
"import matplotlib.pyplot as plt"
64+
"from keras.models import Model\n",
65+
"from keras.engine.input_layer import Input\n",
66+
"from keras.layers import Conv2D, Reshape, Activation, Concatenate, GRU, Dense, LSTM, SimpleRNN\n",
67+
"\n",
68+
"from asf_notebook import new_directory"
4269
]
4370
},
4471
{
4572
"cell_type": "markdown",
4673
"metadata": {},
4774
"source": [
48-
"# Step 1: Data Preparation - loading T1 and T2 images, training map, and test map"
75+
"<hr>\n",
76+
"<font face=\"Calibri\" size=\"5\"><b>1. Create a working directory for the analysis and change into it:</b></font>"
77+
]
78+
},
79+
{
80+
"cell_type": "code",
81+
"execution_count": null,
82+
"metadata": {},
83+
"outputs": [],
84+
"source": [
85+
"base_path = \"/home/jovyan/notebooks/ASF/GEOS_657_Labs/lab_7_taizhou_data\"\n",
86+
"new_directory(base_path)\n",
87+
"print(f\"Current working directory: {os.getcwd()}\")"
88+
]
89+
},
90+
{
91+
"cell_type": "markdown",
92+
"metadata": {},
93+
"source": [
94+
"<hr>\n",
95+
"<font face=\"Calibri\" size=\"5\"><b>2. Data Preparation</b></font> \n",
96+
"\n",
97+
"<font face=\"Calibri\" size=\"3\"><b>load T1 and T2 images, training map, and test map. Save the images (T1.png and T2.png):</b></font> "
4998
]
5099
},
51100
{
@@ -71,63 +120,122 @@
71120
"print('the shape of T2 image is: {}'.format(imgT2.shape))\n",
72121
"\n",
73122
"plt.imshow(imgT1[:, :, [3, 2, 1]])\n",
123+
"plt.savefig(f\"{base_path}/T1.png\", dpi=300)\n",
74124
"plt.show()\n",
75125
"\n",
76126
"plt.imshow(imgT2[:, :, [3, 2, 1]])\n",
127+
"plt.savefig(f\"{base_path}/T2.png\", dpi=300)\n",
77128
"plt.show()\n",
78129
"\n",
79130
"[rows, cols] = np.nonzero(tra_map)\n",
80131
"num_samples = len(rows)\n",
81132
"rows = np.reshape(rows, (num_samples, 1))\n",
82133
"cols = np.reshape(cols, (num_samples, 1))\n",
83-
"temp = np.concatenate((rows, cols), axis = 1)\n",
134+
"temp = np.concatenate((rows, cols), axis=1)\n",
84135
"np.random.shuffle(temp)\n",
85136
"rows = temp[:, 0].reshape((num_samples,))\n",
86-
"cols = temp[:, 1].reshape((num_samples,))\n",
137+
"cols = temp[:, 1].reshape((num_samples,))"
138+
]
139+
},
140+
{
141+
"cell_type": "markdown",
142+
"metadata": {},
143+
"source": [
144+
"<font face=\"Calibri\" size=\"3\">Create 3x3 patches as training samples according to the training map</font> \n",
145+
"<br><br>\n",
146+
"<font face=\"Calibri\" size=\"3\"><b>Create numpy arrays temporarily filled with zeros to hold our 3x3 patches:</b></font> "
147+
]
148+
},
149+
{
150+
"cell_type": "code",
151+
"execution_count": null,
152+
"metadata": {},
153+
"outputs": [],
154+
"source": [
155+
"x_tra_t1 = np.float32(\n",
156+
" np.zeros([num_samples, patch_size, patch_size, num_bands]))\n",
157+
"x_tra_t2 = np.float32(\n",
158+
" np.zeros([num_samples, patch_size, patch_size, num_bands]))\n",
87159
"\n",
88-
"# sampling 3x3 patches as training samples according to the training map\n",
89-
"x_tra_t1 = np.float32(np.zeros([num_samples, patch_size, patch_size, num_bands])) # training samples from T1 image\n",
90-
"x_tra_t2 = np.float32(np.zeros([num_samples, patch_size, patch_size, num_bands])) # training samples from T2 image\n",
91-
"y_tra = np.uint8(np.zeros([num_samples,])) # ground truths for training samples\n",
160+
"y_tra = np.uint8(np.zeros([num_samples, ])) # ground truths for training samples"
161+
]
162+
},
163+
{
164+
"cell_type": "markdown",
165+
"metadata": {},
166+
"source": [
167+
"<font face=\"Calibri\" size=\"3\"><b>Populate the zero-filled arrays with appropriate values:</b></font> "
168+
]
169+
},
170+
{
171+
"cell_type": "code",
172+
"execution_count": null,
173+
"metadata": {},
174+
"outputs": [],
175+
"source": [
92176
"for i in range(num_samples):\n",
93-
" patch = imgT1[rows[i]-int((patch_size-1)/2) : rows[i]+int((patch_size-1)/2)+1, cols[i]-int((patch_size-1)/2) : cols[i]+int((patch_size-1)/2)+1, :]\n",
177+
" patch = imgT1[rows[i]-int((patch_size-1)/2): rows[i]+int((patch_size-1)/2)+1,\n",
178+
" cols[i]-int((patch_size-1)/2): cols[i]+int((patch_size-1)/2)+1, :]\n",
94179
" x_tra_t1[i, :, :, :] = patch\n",
95-
" patch = imgT2[rows[i]-int((patch_size-1)/2) : rows[i]+int((patch_size-1)/2)+1, cols[i]-int((patch_size-1)/2) : cols[i]+int((patch_size-1)/2)+1, :]\n",
180+
" patch = imgT2[rows[i]-int((patch_size-1)/2): rows[i]+int((patch_size-1)/2)+1,\n",
181+
" cols[i]-int((patch_size-1)/2): cols[i]+int((patch_size-1)/2)+1, :]\n",
96182
" x_tra_t2[i, :, :, :] = patch\n",
97183
" y_tra[i] = tra_map[rows[i], cols[i]]-1\n",
98184
"\n",
99185
"[rows, cols] = np.nonzero(test_map)\n",
100186
"num_samples = len(rows)\n",
101187
"rows = np.reshape(rows, (num_samples, 1))\n",
102188
"cols = np.reshape(cols, (num_samples, 1))\n",
103-
"temp = np.concatenate((rows, cols), axis = 1)\n",
189+
"temp = np.concatenate((rows, cols), axis=1)\n",
104190
"np.random.shuffle(temp)\n",
105191
"rows = temp[:, 0].reshape((num_samples,))\n",
106-
"cols = temp[:, 1].reshape((num_samples,))\n",
107-
"\n",
108-
"# sampling 3x3 patches as test samples according to the test map\n",
109-
"x_test_t1 = np.float32(np.zeros([num_samples, patch_size, patch_size, num_bands])) # test samples from T1 image\n",
110-
"x_test_t2 = np.float32(np.zeros([num_samples, patch_size, patch_size, num_bands])) # test samples from T2 image\n",
111-
"y_test = np.uint8(np.zeros([num_samples,])) # ground truths for test samples\n",
192+
"cols = temp[:, 1].reshape((num_samples,))"
193+
]
194+
},
195+
{
196+
"cell_type": "markdown",
197+
"metadata": {},
198+
"source": [
199+
"<font face=\"Calibri\" size=\"3\"><b>Sample 3x3 patches as test samples according to the test map:</b></font> "
200+
]
201+
},
202+
{
203+
"cell_type": "code",
204+
"execution_count": null,
205+
"metadata": {},
206+
"outputs": [],
207+
"source": [
208+
"# test samples from T1 image\n",
209+
"x_test_t1 = np.float32(\n",
210+
" np.zeros([num_samples, patch_size, patch_size, num_bands]))\n",
211+
"# test samples from T2 image\n",
212+
"x_test_t2 = np.float32(\n",
213+
" np.zeros([num_samples, patch_size, patch_size, num_bands]))\n",
214+
"# ground truths for test samples\n",
215+
"y_test = np.uint8(np.zeros([num_samples, ])) \n",
112216
"for i in range(num_samples):\n",
113-
" patch = imgT1[rows[i]-int((patch_size-1)/2) : rows[i]+int((patch_size-1)/2)+1, cols[i]-int((patch_size-1)/2) : cols[i]+int((patch_size-1)/2)+1, :]\n",
217+
" patch = imgT1[rows[i]-int((patch_size-1)/2): rows[i]+int((patch_size-1)/2)+1,\n",
218+
" cols[i]-int((patch_size-1)/2): cols[i]+int((patch_size-1)/2)+1, :]\n",
114219
" x_test_t1[i, :, :, :] = patch\n",
115-
" patch = imgT2[rows[i]-int((patch_size-1)/2) : rows[i]+int((patch_size-1)/2)+1, cols[i]-int((patch_size-1)/2) : cols[i]+int((patch_size-1)/2)+1, :]\n",
220+
" patch = imgT2[rows[i]-int((patch_size-1)/2): rows[i]+int((patch_size-1)/2)+1,\n",
221+
" cols[i]-int((patch_size-1)/2): cols[i]+int((patch_size-1)/2)+1, :]\n",
116222
" x_test_t2[i, :, :, :] = patch\n",
117223
" y_test[i] = test_map[rows[i], cols[i]]-1\n",
118224
"\n",
119225
"print('the shape of input tensors on training set is: {}'.format(x_tra_t1.shape))\n",
120226
"print('the shape of target tensor on training set is: {}'.format(y_tra.shape))\n",
121227
"print('the shape of input tensors on training set is: {}'.format(x_test_t1.shape))\n",
122-
"print('the shape of target tensor on training set is: {}'.format(y_test.shape))\n",
123-
"print('##################################')"
228+
"print('the shape of target tensor on training set is: {}'.format(y_test.shape))"
124229
]
125230
},
126231
{
127232
"cell_type": "markdown",
128233
"metadata": {},
129234
"source": [
130-
"# Step 2: Building up the recurrent convolutional network"
235+
"<hr>\n",
236+
"<font face=\"Calibri\" size=\"5\"> <b>3. Building up the recurrent convolutional network </b> </font> \n",
237+
"\n",
238+
"<font face=\"Calibri\" size=\"3\"><b>Write a function to build the network:</b></font> "
131239
]
132240
},
133241
{
@@ -136,43 +244,42 @@
136244
"metadata": {},
137245
"outputs": [],
138246
"source": [
139-
"from keras.models import Model\n",
140-
"from keras.engine.input_layer import Input\n",
141-
"from keras.layers import Conv2D, Reshape, Activation, Concatenate, GRU, Dense, LSTM, SimpleRNN\n",
142-
"\n",
143247
"def build_network():\n",
144248
" # the T1 branch of the convolutional sub-network\n",
145-
" input1 = Input(shape = (3, 3, 6))\n",
146-
" x1 = Conv2D(filters = 32, kernel_size = 3, strides = 1, padding = 'valid')(input1)\n",
249+
" input1 = Input(shape=(3, 3, 6))\n",
250+
" x1 = Conv2D(filters=32, kernel_size=3, strides=1, padding='valid')(input1)\n",
147251
" x1 = Activation('relu')(x1)\n",
148-
" x1 = Reshape(target_shape = (1, 32))(x1)\n",
149-
" \n",
252+
" x1 = Reshape(target_shape=(1, 32))(x1)\n",
253+
"\n",
150254
" # the T2 branch of the convolutional sub-network\n",
151-
" input2 = Input(shape = (3, 3, 6))\n",
152-
" x2 = Conv2D(filters = 32, kernel_size = 3, strides = 1, padding = 'valid')(input2)\n",
255+
" input2 = Input(shape=(3, 3, 6))\n",
256+
" x2 = Conv2D(filters=32, kernel_size=3, strides=1, padding='valid')(input2)\n",
153257
" x2 = Activation('relu')(x2)\n",
154-
" x2 = Reshape(target_shape = (1, 32))(x2)\n",
155-
" \n",
258+
" x2 = Reshape(target_shape=(1, 32))(x2)\n",
259+
"\n",
156260
" # the recurrent sub-network\n",
157-
" x = Concatenate(axis = 1)([x1, x2])\n",
261+
" x = Concatenate(axis=1)([x1, x2])\n",
158262
" #x = SimpleRNN(units = 128)(x)\n",
159-
" x = LSTM(units = 128)(x)\n",
263+
" x = LSTM(units=128)(x)\n",
160264
" #x = GRU(units = 128)(x)\n",
161-
" x = Dense(units = 32, activation = 'relu')(x)\n",
162-
" y = Dense(units = 1, activation = 'sigmoid')(x)\n",
163-
" \n",
164-
" net = Model(inputs = [input1, input2], outputs = y)\n",
265+
" x = Dense(units=32, activation='relu')(x)\n",
266+
" y = Dense(units=1, activation='sigmoid')(x)\n",
267+
"\n",
268+
" net = Model(inputs=[input1, input2], outputs=y)\n",
165269
"\n",
166270
" net.summary()\n",
167-
" \n",
168-
" return net"
271+
"\n",
272+
" return net\n"
169273
]
170274
},
171275
{
172276
"cell_type": "markdown",
173277
"metadata": {},
174278
"source": [
175-
"# Step 3: Network training"
279+
"<hr>\n",
280+
"<font face=\"Calibri\" size=\"5\"> <b>4. Network training </b> </font> \n",
281+
"\n",
282+
"<font face=\"Calibri\" size=\"3\"><b>Build the network:</b></font> "
176283
]
177284
},
178285
{
@@ -187,37 +294,62 @@
187294
"net = build_network()"
188295
]
189296
},
297+
{
298+
"cell_type": "markdown",
299+
"metadata": {},
300+
"source": [
301+
"<font face=\"Calibri\" size=\"3\"><b>Train the network:</b></font> "
302+
]
303+
},
190304
{
191305
"cell_type": "code",
192306
"execution_count": null,
193307
"metadata": {},
194308
"outputs": [],
195309
"source": [
196-
"nadam = Nadam(lr = 0.00002)\n",
197-
"net.compile(optimizer = nadam, loss = 'binary_crossentropy', metrics = ['accuracy'])\n",
198-
"net_info = net.fit([x_tra_t1, x_tra_t2], y_tra, batch_size = batch_size, validation_split = 0.1, epochs = nb_epoch)\n",
310+
"nadam = Nadam(lr=0.00002)\n",
311+
"net.compile(optimizer=nadam, loss='binary_crossentropy', metrics=['accuracy'])\n",
312+
"net_info = net.fit([x_tra_t1, x_tra_t2], y_tra,\n",
313+
" batch_size=batch_size, validation_split=0.1, epochs=nb_epoch)\n",
199314
"\n",
200315
"loss = net_info.history['loss']\n",
201316
"loss_val = net_info.history['val_loss']\n",
202317
"plt.rcParams.update({'font.size': 18})\n",
203-
"fig = plt.figure(figsize=(8,7))\n",
204-
"ax = fig.add_subplot(1,1,1)\n",
318+
"fig = plt.figure(figsize=(8, 7))\n",
319+
"ax = fig.add_subplot(1, 1, 1)"
320+
]
321+
},
322+
{
323+
"cell_type": "markdown",
324+
"metadata": {},
325+
"source": [
326+
"<font face=\"Calibri\" size=\"3\"><b>Plot and save the results (loss.png):</b></font> "
327+
]
328+
},
329+
{
330+
"cell_type": "code",
331+
"execution_count": null,
332+
"metadata": {},
333+
"outputs": [],
334+
"source": [
205335
"plt.plot(loss)\n",
206336
"plt.plot(loss_val)\n",
207337
"plt.ylabel('loss')\n",
208338
"plt.xlabel('epoch')\n",
209339
"plt.legend(['train', 'val'], loc='upper right')\n",
340+
"plt.savefig(f\"{base_path}/loss.png\", bbox_inches='tight', dpi=200)\n",
210341
"plt.show()\n",
211-
"\n",
212-
"#sio.savemat('loss_curves.mat', {'loss': loss, 'loss_val': loss_val})\n",
213-
"print('##########################################')"
342+
"#sio.savemat('loss_curves.mat', {'loss': loss, 'loss_val': loss_val})"
214343
]
215344
},
216345
{
217346
"cell_type": "markdown",
218347
"metadata": {},
219348
"source": [
220-
"# Step 4: Test"
349+
"<hr>\n",
350+
"<font face=\"Calibri\" size=\"5\"><b>5. Test</b></font> \n",
351+
"\n",
352+
"<font face=\"Calibri\" size=\"3\"><b>Run the network on the test dataset. Save the change map probability and the change map binary (change_map_probability.png and change_map_binary.png):</b></font> "
221353
]
222354
},
223355
{
@@ -238,28 +370,25 @@
238370
"print('sampling patches...')\n",
239371
"for i in range(1, imgT1.shape[0]-1, 1):\n",
240372
" for j in range(1, imgT1.shape[1]-1, 1):\n",
241-
" patch = imgT1[i-int((patch_size-1)/2) : i+int((patch_size-1)/2)+1, j-int((patch_size-1)/2) : j+int((patch_size-1)/2)+1, :]\n",
373+
" patch = imgT1[i-int((patch_size-1)/2): i+int((patch_size-1)/2)+1,\n",
374+
" j-int((patch_size-1)/2): j+int((patch_size-1)/2)+1, :]\n",
242375
" x_t1[cnt, :, :, :] = patch\n",
243-
" patch = imgT2[i-int((patch_size-1)/2) : i+int((patch_size-1)/2)+1, j-int((patch_size-1)/2) : j+int((patch_size-1)/2)+1, :]\n",
376+
" patch = imgT2[i-int((patch_size-1)/2): i+int((patch_size-1)/2)+1,\n",
377+
" j-int((patch_size-1)/2): j+int((patch_size-1)/2)+1, :]\n",
244378
" x_t2[cnt, :, :, :] = patch\n",
245379
" cnt = cnt + 1\n",
246380
"print('sampling done.')\n",
247381
"pred = net.predict([x_t1, x_t2])\n",
248382
"change_map_prob = np.reshape(pred, (400, 400))\n",
249383
"plt.imshow(change_map_prob)\n",
384+
"plt.savefig(f\"{base_path}/change_map_probability.png\", dpi=200)\n",
250385
"plt.show()\n",
251386
"\n",
252-
"change_map_binary = np.where(change_map_prob<0.5,0,1)\n",
387+
"change_map_binary = np.where(change_map_prob < 0.5, 0, 1)\n",
253388
"plt.imshow(change_map_binary)\n",
389+
"plt.savefig(f\"{base_path}/change_map_binary.png\", dpi=200)\n",
254390
"plt.show()"
255391
]
256-
},
257-
{
258-
"cell_type": "code",
259-
"execution_count": null,
260-
"metadata": {},
261-
"outputs": [],
262-
"source": []
263392
}
264393
],
265394
"metadata": {

0 commit comments

Comments
 (0)