require(rgdal)
require(raster)
require(RStoolbox)
require(ggplot2)
require(reshape2)
require(knitr)
require(kableExtra)
imadir2 <- "./pcatest2"
1. PCA with R
Input image
b <- brick(file.path(imadir2,"mini1.tif"))
ggRGB(b,5,3,1,stretch="lin")

Run PCA with RStoolbox and extract eigenmatrix.
(read again for time benchmarking)
t0 <- Sys.time()
b <- brick(file.path(imadir2,"mini1.tif"))
bpca <- rasterPCA(b,spca=FALSE)
trpca <- Sys.time() -t0
trpca
Time difference of 5.875336 secs
eigenvR <- loadings(bpca$model)[]
PCA image (composite of PC1 to PC3)
ggRGB(bpca$map,1,2,3,stretch="lin")

2. PCA with OTB
For OTB the current (v7.1) implementation of otbcli_DimensionalityReduction implies an awkard sequence to cope with nodata: (otbPCAtest_mini1.scr)
C:\ALOBO\OTB-7.1.0-Win64\otbenv.bat
cd D:\OTBtests\OTBpca\pcatest2
otbcli_ManageNoData.bat -in ..\mini1.tif -out mini1MASK.tif uint8 -mode.buildmask.inv 1 -mode.buildmask.outv 0
otbcli_ManageNoData.bat -in mini1.tif -out mini1Mask.tif uint8 -mode.buildmask.inv 1 -mode.buildmask.outv 0
#without whitening
otbcli_DimensionalityReduction.bat -in mini1.tif -bv 0 -out mini1PCAotb.tif -method pca -method.pca.whiten false -outmatrix mini1PCAotbnowhit.eigmat.csv
otbcli_ManageNoData.bat -in mini1PCAotb.tif -out mini1PCAotbM.tif -mode apply -mode.apply.mask mini1Mask.tif -mode.apply.ndval 0
3. Compare eigenvectors
eigenvOTB <- t(read.csv(file.path(imadir2,"mini1PCAotbnowhit.eigmat.csv"),header=FALSE,sep=""))
eigenvOTB <- data.frame(eigenvOTB)
eigenvR <- read.csv(file.path(imadir2,"Rmini1eigenmat.csv"),stringsAsFactors = FALSE)[,-1]
colnames(eigenvR) <- colnames(eigenvOTB) <- paste0("PC",1:5)
rownames(eigenvR) <- rownames(eigenvOTB) <- paste0("Band_",1:5)
Eigenvectors from R:
kable(eigenvR) %>%
kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
| |
PC1 |
PC2 |
PC3 |
PC4 |
PC5 |
| Band_1 |
0.0217211 |
0.2969163 |
0.2623071 |
0.2694614 |
0.8774705 |
| Band_2 |
0.1326099 |
0.4015052 |
0.0335164 |
-0.8967462 |
0.1262185 |
| Band_3 |
0.0331460 |
0.6279545 |
0.5775238 |
0.2428258 |
-0.4605176 |
| Band_4 |
0.3865421 |
0.5127257 |
-0.7221213 |
0.2534016 |
-0.0450125 |
| Band_5 |
0.9118275 |
-0.3056467 |
0.2740052 |
0.0077485 |
-0.0034370 |
Eigenvectors from OTB:
kable(eigenvOTB) %>%
kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
| |
PC1 |
PC2 |
PC3 |
PC4 |
PC5 |
| Band_1 |
0.0225902 |
-0.2931714 |
-0.2664791 |
0.1835331 |
0.8993579 |
| Band_2 |
0.1346418 |
-0.3977168 |
-0.0786326 |
-0.9037259 |
0.0280966 |
| Band_3 |
0.0361656 |
-0.6199247 |
-0.5704303 |
0.3142812 |
-0.4361444 |
| Band_4 |
0.3874017 |
-0.5230245 |
0.7251642 |
0.2244495 |
-0.0111637 |
| Band_5 |
0.9110287 |
0.3130665 |
-0.2674920 |
0.0210914 |
-0.0043922 |
Difference: most values < 0.01, but note larger difference values in some elements of the eigenvectors
kable(abs(eigenvR) - abs(eigenvOTB)) %>%
kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
| |
PC1 |
PC2 |
PC3 |
PC4 |
PC5 |
| Band_1 |
-0.0008691 |
0.0037449 |
-0.0041720 |
0.0859282 |
-0.0218874 |
| Band_2 |
-0.0020319 |
0.0037884 |
-0.0451162 |
-0.0069797 |
0.0981219 |
| Band_3 |
-0.0030196 |
0.0080297 |
0.0070935 |
-0.0714554 |
0.0243732 |
| Band_4 |
-0.0008596 |
-0.0102988 |
-0.0030430 |
0.0289522 |
0.0338488 |
| Band_5 |
0.0007988 |
-0.0074198 |
0.0065132 |
-0.0133429 |
-0.0009552 |
4. Plot PC values
rversion <- bpca$map
otbversion <- brick(file.path(imadir2,"mini1PCAotbM.tif")) #from otbPCAtest_mini1.scr
names(rversion) <- names(otbversion) <- paste0("PC",1:5)
Extract random values and tidy up for ggplot in long format
set.seed(121)
pdata <- sampleRandom(stack(rversion,otbversion), size=1000, na.rm=TRUE,
rowcol=TRUE, xy=TRUE, sp=FALSE)
pdata1 <- pdata[,1:9]
pdata2 <- pdata[,c(1:4,10:14)]
colnames(pdata1)[5:9] <- colnames(pdata2)[5:9] <- paste0("PC",1:5)
pdata1lf <- melt(data.frame(pdata1),id.vars=1:4)
names(pdata1lf)[6] <- "Rversion"
pdata2lf <- melt(data.frame(pdata2),id.vars=1:4)
names(pdata2lf)[6] <- "OTBversion"
pdatalf <- data.frame(cbind(pdata1lf,OTBversion=pdata2lf$OTBversion))
#kable(head(pdatalf)) %>%
# kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
Note dispersion increases for higher PCs. I’ve found this is consistent across images
ggplot(data=pdatalf) +
geom_point(aes(x=Rversion,y=OTBversion)) +
theme(aspect.ratio = 1) +
facet_wrap(~variable,scale="free",ncol=3)

5. Execution times
I often need a pca output in the form of a linearly stretched PCs image in uint8 and leaving 0 for nodata, thus I include this step in the benchmarking too. The stretching is done from the 2% and 98% quantiles to the 1:255 range. For R, this linear stretching can done by combining raster::clamp() and RStoolbox::RescalImage
writeRaster(rescaleImage(clamp(bpca$map,lower=q[,1], upper=q[,2]),
xmin=q[,1], xmax=q[,2],ymin=1, ymax=255),
file.path(imadir2,"mini1PCARresc"),
datatype="INT1U", NAflag=0,
format="GTiff",overwrite=TRUE)
Stretching with the current OTB v7.1 implies fiddling again to cope with nodata:
otbcli_DynamicConvert -in mini1PCAotbM.tif type linear -mask mini1Mask.tif -out mini1PCAotbMresc.tif uint8 -outmin 1 -outmax 255
otbcli_ManageNoData.bat -in mini1PCAotbMresc.tif -out mini1PCAotbMrescM.tif -mode apply -mode.apply.mask mini1Mask.tif -mode.apply.ndval 0
5.1 Small test image
Test with mini1.tif (small test image 85 rows x 116 cols x 5 bands x 32 bit).
Runing on a very low-profile, consumer grade old laptop:
- Lenovo X220 with Intel Core i5-2520M CPU @2.50 GHz
- SSD hard-drive
- 8 Gb of RAM
Values in the table are in seconds
extimes <- read.csv("TimeBenchmark_PCA.csv",header=FALSE,stringsAsFactors = FALSE)
extimes1 <- extimes[2:4,1:3]
names(extimes1) <- extimes1[1,]
rownames(extimes1) <- extimes1[,1]
kable(extimes1[-1,-1]) %>%
kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
| |
R |
OTB |
| PCA |
2.05 |
1.7 |
| stretching |
0.15 |
1.04 |
5.2 Large-size image
Execution times with a real image (BertMICA20190531v2.tif) (8282 rows x 11329 cols x 5 bands x 32 bit).
For images with a real-life size, an R alternative is using gdalUtilities::gdal_transform(), but the output cannot be clamped to the 1:255 range using gdalUtilities, thus the image has to read back into R for clamp(), which significantly increases the execution time:
gdal_translate(src_dataset=file.path(rasdirpca,"BertMICA20190531v2PCARfloat.tif"),
dst_dataset=file.path(rasdirpca,"BertMICA20190531v2PCARresc.tif"),
scale=mscal,dryrun=FALSE)
BertMICA20190531v2PCARresc <- brick(file.path(rasdirpca,"BertMICA20190531v2PCARresc.tif"))
a1 <- clamp(BertMICA20190531v2PCARresc,lower=1,upper=255, useValues=TRUE,
format="GTiff",datatype="INT1U",NAflag=0,
file=file.path(rasdirpca,"BertMICA20190531v2PCARrescG.tif"),
overwrite=TRUE)
Note the advantage of OTB: despite the fact so much reading and writing in OTB severely increases execution time, the OTB run is performed in a reasonable time while waiting for R is unpractical. Values in the table are in minutes.
extimes2 <- extimes[7:10,1:4]
names(extimes2) <- extimes2[1,]
rownames(extimes2) <- extimes2[,1]
kable(extimes2[-1,-1]) %>%
kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
| |
R |
gdal+R |
OTB |
| PCA |
33.73 |
|
3.36 |
| PCA with resampling |
6.9 |
|
|
| stretching |
6.38 |
4.43 |
1.66 |
LS0tDQp0aXRsZTogIiBDb21wYXJpbmcgUENBIHJlc3VsdHMgZnJvbSBSIGFuZCBmcm9tIE9UQiINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgIGNvZGVfZm9sZGluZzogaGlkZQ0KZWRpdG9yX29wdGlvbnM6IA0KICBjaHVua19vdXRwdXRfdHlwZTogaW5saW5lDQotLS0NCg0KKiBBZ3VzdGluLkxvYm9AaWN0amEuY3NpYy5lcw0KKiAyMDIwMDUwOA0KDQpgYGB7cn0NCnJlcXVpcmUocmdkYWwpDQpyZXF1aXJlKHJhc3RlcikNCnJlcXVpcmUoUlN0b29sYm94KQ0KcmVxdWlyZShnZ3Bsb3QyKQ0KcmVxdWlyZShyZXNoYXBlMikNCnJlcXVpcmUoa25pdHIpDQpyZXF1aXJlKGthYmxlRXh0cmEpDQppbWFkaXIyIDwtICIuL3BjYXRlc3QyIg0KYGBgDQoNCiMjIDEuIFBDQSB3aXRoIFINCklucHV0IGltYWdlDQpgYGB7ciBmaWcuaGVpZ2h0PTQsIGZpZy53aWR0aD02fQ0KYiA8LSBicmljayhmaWxlLnBhdGgoaW1hZGlyMiwibWluaTEudGlmIikpDQpnZ1JHQihiLDUsMywxLHN0cmV0Y2g9ImxpbiIpDQpgYGANClJ1biBQQ0Egd2l0aCBSU3Rvb2xib3ggYW5kIGV4dHJhY3QgZWlnZW5tYXRyaXguICANCihyZWFkIGFnYWluIGZvciB0aW1lIGJlbmNobWFya2luZykNCmBgYHtyfQ0KdDAgPC0gU3lzLnRpbWUoKQ0KYiA8LSBicmljayhmaWxlLnBhdGgoaW1hZGlyMiwibWluaTEudGlmIikpDQpicGNhIDwtIHJhc3RlclBDQShiLHNwY2E9RkFMU0UpDQp0cnBjYSA8LSBTeXMudGltZSgpIC10MA0KdHJwY2ENCmVpZ2VudlIgPC0gbG9hZGluZ3MoYnBjYSRtb2RlbClbXQ0KYGBgDQpQQ0EgaW1hZ2UgKGNvbXBvc2l0ZSBvZiBQQzEgdG8gUEMzKQ0KYGBge3IgZmlnLmhlaWdodD00LCBmaWcud2lkdGg9Nn0NCmdnUkdCKGJwY2EkbWFwLDEsMiwzLHN0cmV0Y2g9ImxpbiIpDQpgYGANCg0KIyMgMi4gUENBIHdpdGggT1RCDQpGb3IgT1RCIHRoZSBjdXJyZW50ICh2Ny4xKSBpbXBsZW1lbnRhdGlvbiBvZiBvdGJjbGlfRGltZW5zaW9uYWxpdHlSZWR1Y3Rpb24gaW1wbGllcyBhbiBhd2thcmQgc2VxdWVuY2UgdG8gY29wZSB3aXRoIG5vZGF0YToNCihvdGJQQ0F0ZXN0X21pbmkxLnNjcikNCg0KYGBgDQpDOlxBTE9CT1xPVEItNy4xLjAtV2luNjRcb3RiZW52LmJhdA0KY2QgRDpcT1RCdGVzdHNcT1RCcGNhXHBjYXRlc3QyDQpvdGJjbGlfTWFuYWdlTm9EYXRhLmJhdCAtaW4gLi5cbWluaTEudGlmIC1vdXQgbWluaTFNQVNLLnRpZiB1aW50OCAtbW9kZS5idWlsZG1hc2suaW52IDEgLW1vZGUuYnVpbGRtYXNrLm91dHYgMCANCm90YmNsaV9NYW5hZ2VOb0RhdGEuYmF0IC1pbiBtaW5pMS50aWYgLW91dCBtaW5pMU1hc2sudGlmIHVpbnQ4IC1tb2RlLmJ1aWxkbWFzay5pbnYgMSAtbW9kZS5idWlsZG1hc2sub3V0diAwIA0KI3dpdGhvdXQgd2hpdGVuaW5nDQpvdGJjbGlfRGltZW5zaW9uYWxpdHlSZWR1Y3Rpb24uYmF0IC1pbiBtaW5pMS50aWYgLWJ2IDAgLW91dCBtaW5pMVBDQW90Yi50aWYgLW1ldGhvZCBwY2EgLW1ldGhvZC5wY2Eud2hpdGVuIGZhbHNlIC1vdXRtYXRyaXggbWluaTFQQ0FvdGJub3doaXQuZWlnbWF0LmNzdg0Kb3RiY2xpX01hbmFnZU5vRGF0YS5iYXQgLWluIG1pbmkxUENBb3RiLnRpZiAtb3V0IG1pbmkxUENBb3RiTS50aWYgLW1vZGUgYXBwbHkgLW1vZGUuYXBwbHkubWFzayBtaW5pMU1hc2sudGlmIC1tb2RlLmFwcGx5Lm5kdmFsIDANCmBgYA0KIyMgMy4gQ29tcGFyZSBlaWdlbnZlY3RvcnMNCmBgYHtyfQ0KZWlnZW52T1RCIDwtIHQocmVhZC5jc3YoZmlsZS5wYXRoKGltYWRpcjIsIm1pbmkxUENBb3Ribm93aGl0LmVpZ21hdC5jc3YiKSxoZWFkZXI9RkFMU0Usc2VwPSIiKSkNCmVpZ2Vudk9UQiA8LSBkYXRhLmZyYW1lKGVpZ2Vudk9UQikNCmVpZ2VudlIgPC0gcmVhZC5jc3YoZmlsZS5wYXRoKGltYWRpcjIsIlJtaW5pMWVpZ2VubWF0LmNzdiIpLHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSlbLC0xXQ0KY29sbmFtZXMoZWlnZW52UikgPC0gY29sbmFtZXMoZWlnZW52T1RCKSA8LSBwYXN0ZTAoIlBDIiwxOjUpDQpyb3duYW1lcyhlaWdlbnZSKSA8LSByb3duYW1lcyhlaWdlbnZPVEIpIDwtIHBhc3RlMCgiQmFuZF8iLDE6NSkNCmBgYA0KRWlnZW52ZWN0b3JzIGZyb20gUjoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICdmb2xkLWhpZGUnfQ0Ka2FibGUoZWlnZW52UikgICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gInN0cmlwZWQiLCBmdWxsX3dpZHRoID0gRiwgcG9zaXRpb24gPSAibGVmdCIpDQpgYGANCkVpZ2VudmVjdG9ycyBmcm9tIE9UQjoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICdmb2xkLWhpZGUnfQ0Ka2FibGUoZWlnZW52T1RCKSAgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IikNCmBgYA0KRGlmZmVyZW5jZTogbW9zdCB2YWx1ZXMgPCAwLjAxLCBidXQgbm90ZSBsYXJnZXIgZGlmZmVyZW5jZSB2YWx1ZXMgaW4gc29tZSBlbGVtZW50cyBvZiB0aGUgZWlnZW52ZWN0b3JzDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAnZm9sZC1oaWRlJ30NCmthYmxlKGFicyhlaWdlbnZSKSAtIGFicyhlaWdlbnZPVEIpKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEYsIHBvc2l0aW9uID0gImxlZnQiKQ0KYGBgDQojIyA0LiBQbG90IFBDIHZhbHVlcw0KYGBge3J9DQpydmVyc2lvbiA8LSBicGNhJG1hcA0Kb3RidmVyc2lvbiA8LSBicmljayhmaWxlLnBhdGgoaW1hZGlyMiwibWluaTFQQ0FvdGJNLnRpZiIpKSAjZnJvbSBvdGJQQ0F0ZXN0X21pbmkxLnNjcg0KbmFtZXMocnZlcnNpb24pIDwtIG5hbWVzKG90YnZlcnNpb24pIDwtIHBhc3RlMCgiUEMiLDE6NSkNCmBgYA0KDQpFeHRyYWN0IHJhbmRvbSB2YWx1ZXMgYW5kIHRpZHkgdXAgZm9yIGdncGxvdCBpbiBsb25nIGZvcm1hdA0KYGBge3J9DQpzZXQuc2VlZCgxMjEpDQpwZGF0YSA8LSBzYW1wbGVSYW5kb20oc3RhY2socnZlcnNpb24sb3RidmVyc2lvbiksIHNpemU9MTAwMCwgbmEucm09VFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICByb3djb2w9VFJVRSwgeHk9VFJVRSwgc3A9RkFMU0UpDQpwZGF0YTEgPC0gcGRhdGFbLDE6OV0NCnBkYXRhMiA8LSBwZGF0YVssYygxOjQsMTA6MTQpXQ0KY29sbmFtZXMocGRhdGExKVs1OjldIDwtIGNvbG5hbWVzKHBkYXRhMilbNTo5XSA8LSBwYXN0ZTAoIlBDIiwxOjUpDQpwZGF0YTFsZiA8LSBtZWx0KGRhdGEuZnJhbWUocGRhdGExKSxpZC52YXJzPTE6NCkNCm5hbWVzKHBkYXRhMWxmKVs2XSA8LSAiUnZlcnNpb24iDQpwZGF0YTJsZiA8LSBtZWx0KGRhdGEuZnJhbWUocGRhdGEyKSxpZC52YXJzPTE6NCkNCm5hbWVzKHBkYXRhMmxmKVs2XSA8LSAiT1RCdmVyc2lvbiINCnBkYXRhbGYgPC0gZGF0YS5mcmFtZShjYmluZChwZGF0YTFsZixPVEJ2ZXJzaW9uPXBkYXRhMmxmJE9UQnZlcnNpb24pKQ0KI2thYmxlKGhlYWQocGRhdGFsZikpICAlPiUNCiMgIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IikNCmBgYA0KDQpOb3RlIGRpc3BlcnNpb24gaW5jcmVhc2VzIGZvciBoaWdoZXIgUENzLiBJJ3ZlIGZvdW5kIHRoaXMgaXMgY29uc2lzdGVudCBhY3Jvc3MgaW1hZ2VzDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdGE9cGRhdGFsZikgKw0KICAgIGdlb21fcG9pbnQoYWVzKHg9UnZlcnNpb24seT1PVEJ2ZXJzaW9uKSkgKw0KICAgIHRoZW1lKGFzcGVjdC5yYXRpbyA9IDEpICsNCiAgICBmYWNldF93cmFwKH52YXJpYWJsZSxzY2FsZT0iZnJlZSIsbmNvbD0zKQ0KYGBgDQojIyA1LiBFeGVjdXRpb24gdGltZXMNCkkgb2Z0ZW4gbmVlZCBhIHBjYSBvdXRwdXQgaW4gdGhlIGZvcm0gb2YgYSBsaW5lYXJseSBzdHJldGNoZWQgUENzIGltYWdlIGluIHVpbnQ4IGFuZCBsZWF2aW5nIDAgZm9yIG5vZGF0YSwgdGh1cyBJIGluY2x1ZGUgdGhpcyBzdGVwIGluIHRoZSBiZW5jaG1hcmtpbmcgdG9vLiBUaGUgc3RyZXRjaGluZyBpcyBkb25lIGZyb20gdGhlIDIlIGFuZCA5OCUgcXVhbnRpbGVzIHRvIHRoZSAxOjI1NSByYW5nZS4NCkZvciBSLCB0aGlzIGxpbmVhciBzdHJldGNoaW5nIGNhbiBkb25lIGJ5IGNvbWJpbmluZyByYXN0ZXI6OmNsYW1wKCkgYW5kIFJTdG9vbGJveDo6UmVzY2FsSW1hZ2UNCmBgYHtyLCBldmFsID0gRkFMU0V9DQp3cml0ZVJhc3RlcihyZXNjYWxlSW1hZ2UoY2xhbXAoYnBjYSRtYXAsbG93ZXI9cVssMV0sIHVwcGVyPXFbLDJdKSwgDQogICAgICAgICAgICB4bWluPXFbLDFdLCB4bWF4PXFbLDJdLHltaW49MSwgeW1heD0yNTUpLA0KICAgICAgICAgICAgZmlsZS5wYXRoKGltYWRpcjIsIm1pbmkxUENBUnJlc2MiKSwNCiAgICAgICAgICAgIGRhdGF0eXBlPSJJTlQxVSIsIE5BZmxhZz0wLA0KICAgICAgICAgICAgZm9ybWF0PSJHVGlmZiIsb3ZlcndyaXRlPVRSVUUpDQpgYGANCg0KU3RyZXRjaGluZyB3aXRoIHRoZSBjdXJyZW50IE9UQiB2Ny4xIGltcGxpZXMgZmlkZGxpbmcgYWdhaW4gdG8gY29wZSB3aXRoIG5vZGF0YToNCmBgYA0Kb3RiY2xpX0R5bmFtaWNDb252ZXJ0IC1pbiBtaW5pMVBDQW90Yk0udGlmIHR5cGUgbGluZWFyIC1tYXNrIG1pbmkxTWFzay50aWYgLW91dCBtaW5pMVBDQW90Yk1yZXNjLnRpZiB1aW50OCAtb3V0bWluIDEgLW91dG1heCAyNTUNCm90YmNsaV9NYW5hZ2VOb0RhdGEuYmF0IC1pbiBtaW5pMVBDQW90Yk1yZXNjLnRpZiAtb3V0IG1pbmkxUENBb3RiTXJlc2NNLnRpZiAtbW9kZSBhcHBseSAtbW9kZS5hcHBseS5tYXNrIG1pbmkxTWFzay50aWYgLW1vZGUuYXBwbHkubmR2YWwgMA0KDQpgYGANCiMjIyA1LjEgU21hbGwgdGVzdCBpbWFnZQ0KVGVzdCB3aXRoIG1pbmkxLnRpZiAoc21hbGwgdGVzdCBpbWFnZSA4NSByb3dzIHggMTE2IGNvbHMgeCA1IGJhbmRzIHggMzIgYml0KS4gIA0KUnVuaW5nIG9uIGEgdmVyeSBsb3ctcHJvZmlsZSwgY29uc3VtZXIgZ3JhZGUgb2xkIGxhcHRvcDoNCg0KKiBMZW5vdm8gWDIyMCB3aXRoIEludGVsIENvcmUgaTUtMjUyME0gQ1BVIEAyLjUwIEdIeg0KKiBTU0QgaGFyZC1kcml2ZQ0KKiA4IEdiIG9mIFJBTQ0KDQpWYWx1ZXMgaW4gdGhlIHRhYmxlIGFyZSBpbiBzZWNvbmRzDQpgYGB7cn0NCmV4dGltZXMgPC0gcmVhZC5jc3YoIlRpbWVCZW5jaG1hcmtfUENBLmNzdiIsaGVhZGVyPUZBTFNFLHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkNCmV4dGltZXMxIDwtIGV4dGltZXNbMjo0LDE6M10NCm5hbWVzKGV4dGltZXMxKSA8LSBleHRpbWVzMVsxLF0NCnJvd25hbWVzKGV4dGltZXMxKSA8LSBleHRpbWVzMVssMV0NCmthYmxlKGV4dGltZXMxWy0xLC0xXSkgICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gInN0cmlwZWQiLCBmdWxsX3dpZHRoID0gRiwgcG9zaXRpb24gPSAibGVmdCIpDQpgYGANCiMjIyA1LjIgTGFyZ2Utc2l6ZSBpbWFnZQ0KRXhlY3V0aW9uIHRpbWVzIHdpdGggYSByZWFsIGltYWdlIChCZXJ0TUlDQTIwMTkwNTMxdjIudGlmKSAoODI4MiByb3dzIHggMTEzMjkgY29scyB4IDUgYmFuZHMgeCAzMiBiaXQpLiAgDQpGb3IgaW1hZ2VzIHdpdGggYSByZWFsLWxpZmUgc2l6ZSwgYW4gUiBhbHRlcm5hdGl2ZSBpcyB1c2luZyBnZGFsVXRpbGl0aWVzOjpnZGFsX3RyYW5zZm9ybSgpLCBidXQgdGhlIG91dHB1dCBjYW5ub3QgYmUgY2xhbXBlZCB0byB0aGUgMToyNTUgcmFuZ2UgdXNpbmcgZ2RhbFV0aWxpdGllcywgdGh1cyB0aGUgaW1hZ2UgaGFzIHRvIHJlYWQgYmFjayBpbnRvIFIgZm9yIGNsYW1wKCksIHdoaWNoIHNpZ25pZmljYW50bHkgaW5jcmVhc2VzIHRoZSBleGVjdXRpb24gdGltZToNCmBgYHtyLCBldmFsID0gRkFMU0V9DQpnZGFsX3RyYW5zbGF0ZShzcmNfZGF0YXNldD1maWxlLnBhdGgocmFzZGlycGNhLCJCZXJ0TUlDQTIwMTkwNTMxdjJQQ0FSZmxvYXQudGlmIiksDQogICAgICAgICAgICAgICBkc3RfZGF0YXNldD1maWxlLnBhdGgocmFzZGlycGNhLCJCZXJ0TUlDQTIwMTkwNTMxdjJQQ0FScmVzYy50aWYiKSwNCiAgICAgICAgICAgICAgIHNjYWxlPW1zY2FsLGRyeXJ1bj1GQUxTRSkNCkJlcnRNSUNBMjAxOTA1MzF2MlBDQVJyZXNjIDwtIGJyaWNrKGZpbGUucGF0aChyYXNkaXJwY2EsIkJlcnRNSUNBMjAxOTA1MzF2MlBDQVJyZXNjLnRpZiIpKQ0KYTEgPC0gY2xhbXAoQmVydE1JQ0EyMDE5MDUzMXYyUENBUnJlc2MsbG93ZXI9MSx1cHBlcj0yNTUsIHVzZVZhbHVlcz1UUlVFLA0KICAgICAgICAgICAgZm9ybWF0PSJHVGlmZiIsZGF0YXR5cGU9IklOVDFVIixOQWZsYWc9MCwNCiAgICAgICAgICAgIGZpbGU9ZmlsZS5wYXRoKHJhc2RpcnBjYSwiQmVydE1JQ0EyMDE5MDUzMXYyUENBUnJlc2NHLnRpZiIpLA0KICAgICAgICAgICAgb3ZlcndyaXRlPVRSVUUpDQpgYGANCg0KTm90ZSB0aGUgYWR2YW50YWdlIG9mIE9UQjogZGVzcGl0ZSB0aGUgZmFjdCBzbyBtdWNoIHJlYWRpbmcgYW5kIHdyaXRpbmcgaW4gT1RCIHNldmVyZWx5IGluY3JlYXNlcyBleGVjdXRpb24gdGltZSwgdGhlIE9UQiBydW4gaXMgcGVyZm9ybWVkIGluIGEgcmVhc29uYWJsZSB0aW1lIHdoaWxlIHdhaXRpbmcgZm9yIFIgaXMgdW5wcmFjdGljYWwuDQpWYWx1ZXMgaW4gdGhlIHRhYmxlIGFyZSBpbiBtaW51dGVzLg0KDQoNCmBgYHtyfQ0KZXh0aW1lczIgPC0gZXh0aW1lc1s3OjEwLDE6NF0NCm5hbWVzKGV4dGltZXMyKSA8LSBleHRpbWVzMlsxLF0NCnJvd25hbWVzKGV4dGltZXMyKSA8LSBleHRpbWVzMlssMV0NCmthYmxlKGV4dGltZXMyWy0xLC0xXSkgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IikNCmBgYA0K