Motivation and Problem

Cuộc thi VinBigData Chest X-ray Abnormalities Detection của Vingroup Big Data Institute với dữ liệu scan có dung lượng (đã nén) là hơn 200 GB. Khối dữ liệu này nằm ngoài khả năng của con máy Dell Precision T5610 chỉ với 64 GB RAM. Do vậy trong post này sử dụng bộ dữ liệu tương tự nhưng nhỏ hơn nhiều từ cuộc thi SARS-COV-2 Ct-Scan Dataset. Thách thức đầu tiên là xử lí bộ dữ liệu này về dạng phù hợp cho các mô hình Machine Learning/Deep Learning. Hàm process_image_data dưới đây được viết dựa trên tham khảo từ Spencer Palladino để xử lí sơ bộ dữ liệu ảnh:

# Clear R environmet: 
rm(list = ls())

library(magrittr)
library(keras)

# Function for pre-processing image data: 

process_image_data <- function(image_path, n_dim) {
  
  # Load image data: 
  img_data <- lapply(image_path, image_load, grayscale = TRUE) 
  
  # Convert to N-dimensional array:
  array_data <- lapply(img_data, image_to_array) 
  
  # Resize and normalize array: 
  
  array_resized <- lapply(array_data, image_array_resize, height = n_dim, width = n_dim) 
  array_normalized <- normalize(array_resized, axis = 1) 
  
  # Return final outputs: 
  return(array_normalized)
  
}

Sử dụng hàm này để load và xử lí 4 cases (2 cases là CT image của 2 bệnh nhân nhiễm Covid 19 và 2 cases là CT scan của 2 người không bị nhiễm Covid 19):

# Paths to image files:

covid_path <- dir("F:/archive/COVID/", full.names = TRUE) 

non_covid_path <- dir("F:/archive/non-COVID/", full.names = TRUE) 

# Select some cases: 

n_dim <- 500

data_4 <- process_image_data(image_path = c(covid_path[1:2], non_covid_path[1:2]), n_dim = n_dim)

data_4 <- data_4[, , , 1] # Extract the first channel. 

Xem qua ảnh CT scan của 4 cases này:

# CT scan image for 4 patients: 

library(reshape2)
library(dplyr)

lapply(1:4, function(x) {melt(data_4[x, , ])}) -> list_covid

do.call("bind_rows", list_covid) -> df_sample

library(ggplot2)

df_sample %>% 
  mutate(type = rep(c("Covid_Case1", "Covid_Case2", "NonCovid_Case1", "NonCovid_Case2"), each = n_dim*n_dim, time = 1)) %>% 
  ggplot(aes(Var1, Var2, fill = value)) + 
  geom_raster() +
  facet_wrap(~ type) + 
  theme_minimal() + 
  theme(legend.position = "none") + 
  theme(panel.grid = element_blank()) +
  theme(axis.title = element_blank()) + 
  theme(axis.text = element_blank()) + 
  labs(x = NULL, y = NULL, title = "Figure 1: Covid and Non-Covid CT scan image") +
  scale_fill_viridis_c()

Prepare Data for Training CNN

Load và xử lí sơ bộ toàn bộ ảnh CT scan, lấy ngẫu nhiên 70% cho training và 30% còn lại cho testing:

# Reset n_dim: 

n_dim <- 100

# Load and process data: 

covid_path %>% 
  process_image_data(n_dim = n_dim) %>% 
  .[, , , 1] -> covid_data

covidData_reshaped <- array_reshape(covid_data, c(nrow(covid_data), n_dim*n_dim))

non_covid_path %>% 
  process_image_data(n_dim = n_dim) %>% 
  .[, , , 1] -> noncovid_data
  
noncovidData_reshaped <- array_reshape(noncovid_data, c(nrow(noncovid_data), n_dim*n_dim))

image_data <- rbind(cbind(covidData_reshaped, 1), cbind(noncovidData_reshaped, 0)) 

# Shuffle our data: 

set.seed(12012020)
image_data <- image_data[sample(nrow(image_data), replace = FALSE), ]

# Set 70% data for training, 30% for testing CNN: 

set.seed(12012020)

split <- sample(x = 1:2, size = nrow(image_data), replace = TRUE, prob = c(0.7, 0.3))

train <- image_data[split == 1, ]

test <- image_data[split == 2, ]

train_target <- image_data[split == 1, n_dim*n_dim + 1] # Label in training dataset. 

test_target <- image_data[split == 2, n_dim*n_dim + 1] # Label in testing dataset. 

# Convert to categorical for binary classification: 
train_label <- to_categorical(train_target)

test_label <- to_categorical(test_target)

Convolutional Neural Network

Convolutional Neural Network - CNN được lựa chọn làm mô hình phân loại. Trong post này, keras - một platform thường được sử dụng cho Deep Learning được sử dụng để huấn luyện CNN. Quá trình huấn luyện CNN dưới đây có thể mất vài ba phút:

# Set drop rate: 

drop_rate <- 0.2

my_dim <- 2**10

# Setup CNN: 

model <- keras_model_sequential() %>%
  layer_dense(units = my_dim, activation = "relu") %>% 
  layer_dropout(drop_rate) %>%
  layer_dense(units = my_dim / 2, activation = "relu") %>%
  layer_dropout(drop_rate) %>%
  layer_dense(units = my_dim / 4, activation = "relu") %>%
  layer_dropout(drop_rate) %>%
  layer_dense(units = 2, activation = "sigmoid") %>% # For binary classification. 
  compile(optimizer = "adam", 
          loss = "binary_crossentropy", 
          metrics = "accuracy")

# Train CNN: 

fit_covid <- model %>%
  fit(x = train,
      y = train_label, 
      epochs = 10*5,
      batch_size = 2**5, 
      verbose = 2,
      validation_split = 0.2)

Chúng ta có thể khảo sát quá trình huấn luyện CNN:

# Process of training CNN: 

plot(fit_covid) + 
  labs(title = "Figure 2: Process of training Convolutional Neural Network ") + 
  theme(legend.position = "top")

Model Performance

Với CNN đã được huấn luyện ở trên, chúng ta có thể sử dụng để đánh giá khả năng phân loại khi áp dụng cho bộ dữ liệu test:

# Use CNN for predicting: 

prob_predicted <- model %>%
  predict(test) %>% 
  .[, 2]

# Model performance by confusion matrix: 

class_predicted <- case_when(prob_predicted > 0.5 ~ "Covid", TRUE ~ "NonCovid") %>% factor()

class_actuals <- case_when(test_target == 1 ~ "Covid", TRUE ~ "NonCovid") %>% factor()

library(caret)

confusionMatrix(data = class_predicted, reference = class_actuals, positive = "Covid")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Covid NonCovid
##   Covid      375        0
##   NonCovid     0      372
##                                      
##                Accuracy : 1          
##                  95% CI : (0.9951, 1)
##     No Information Rate : 0.502      
##     P-Value [Acc > NIR] : < 2.2e-16  
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.000      
##             Specificity : 1.000      
##          Pos Pred Value : 1.000      
##          Neg Pred Value : 1.000      
##              Prevalence : 0.502      
##          Detection Rate : 0.502      
##    Detection Prevalence : 0.502      
##       Balanced Accuracy : 1.000      
##                                      
##        'Positive' Class : Covid      
## 

Từ Confusion Matrix ta có thể thấy CNN đạt mức chính xác 100% trên bộ dữ liệu test.

R Environment and OS

sessionInfo()
## R version 4.1.2 (2021-11-01)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19043)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] caret_6.0-90    lattice_0.20-45 ggplot2_3.3.5   dplyr_1.0.7    
## [5] reshape2_1.4.4  keras_2.7.0     magrittr_2.0.1 
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_1.0.7           lubridate_1.8.0      listenv_0.8.0       
##  [4] png_0.1-7            class_7.3-19         assertthat_0.2.1    
##  [7] zeallot_0.1.0        digest_0.6.28        ipred_0.9-12        
## [10] foreach_1.5.1        utf8_1.2.2           parallelly_1.30.0   
## [13] R6_2.5.1             plyr_1.8.6           stats4_4.1.2        
## [16] evaluate_0.14        highr_0.9            pillar_1.6.4        
## [19] tfruns_1.5.0         rlang_0.4.12         data.table_1.14.2   
## [22] whisker_0.4          jquerylib_0.1.4      rpart_4.1-15        
## [25] Matrix_1.3-4         reticulate_1.22      rmarkdown_2.11      
## [28] labeling_0.4.2       splines_4.1.2        gower_0.2.2         
## [31] stringr_1.4.0        munsell_0.5.0        compiler_4.1.2      
## [34] xfun_0.28            pkgconfig_2.0.3      base64enc_0.1-3     
## [37] globals_0.14.0       tensorflow_2.7.0     htmltools_0.5.2     
## [40] nnet_7.3-16          tidyselect_1.1.1     tibble_3.1.6        
## [43] prodlim_2019.11.13   codetools_0.2-18     viridisLite_0.4.0   
## [46] future_1.23.0        fansi_0.5.0          crayon_1.4.2        
## [49] withr_2.4.2          ModelMetrics_1.2.2.2 MASS_7.3-54         
## [52] recipes_0.1.17       grid_4.1.2           nlme_3.1-153        
## [55] jsonlite_1.7.2.9000  gtable_0.3.0         lifecycle_1.0.1     
## [58] DBI_1.1.1            pROC_1.18.0          scales_1.1.1        
## [61] future.apply_1.8.1   stringi_1.7.6        farver_2.1.0        
## [64] timeDate_3043.102    bslib_0.3.1          ellipsis_0.3.2      
## [67] generics_0.1.1       vctrs_0.3.8          lava_1.6.10         
## [70] iterators_1.0.13     tools_4.1.2          glue_1.5.0          
## [73] purrr_0.3.4          parallel_4.1.2       fastmap_1.1.0       
## [76] survival_3.2-13      yaml_2.2.1           colorspace_2.0-2    
## [79] knitr_1.36           sass_0.4.0

```

LS0tDQp0aXRsZTogIkNvbnZvbHV0aW9uYWwgTmV1cmFsIE5ldHdvcmsgKENOTikgZm9yIFByZWRpY3RpbmcgUGF0aWVudHMgSW5mZWN0ZWQgQ292aWQgMTkgZnJvbSBDVCBTY2FuIEltYWdlIg0KYXV0aG9yOiAnQXV0aG9yOiBOZ3V5ZW4gQ2hpIER1bmcnDQpzdWJ0aXRsZTogIlIgTWFjaGluZSBMZWFybmluZyBTZXJpZXMiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICAjIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogemVuYnVybg0KICAgICMgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImZsYXRseSINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSwgY2FjaGUgPSBUUlVFKQ0KDQpgYGANCg0KIVtdKEM6XFVzZXJzXFxBZG1pblxcRG9jdW1lbnRzXFxjdHNjYW4uanBnKQ0KDQojIE1vdGl2YXRpb24gYW5kIFByb2JsZW0NCg0KDQpDdeG7mWMgdGhpIFtWaW5CaWdEYXRhIENoZXN0IFgtcmF5IEFibm9ybWFsaXRpZXMgRGV0ZWN0aW9uXShodHRwczovL3d3dy5rYWdnbGUuY29tL2MvdmluYmlnZGF0YS1jaGVzdC14cmF5LWFibm9ybWFsaXRpZXMtZGV0ZWN0aW9uKSBj4bunYSBWaW5ncm91cCBCaWcgRGF0YSBJbnN0aXR1dGUgduG7m2kgZOG7ryBsaeG7h3Ugc2NhbiBjw7MgZHVuZyBsxrDhu6NuZyAoxJHDoyBuw6luKSBsw6AgaMahbiAyMDAgR0IuIEto4buRaSBk4buvIGxp4buHdSBuw6B5IG7hurFtIG5nb8OgaSBraOG6oyBuxINuZyBj4bunYSBjb24gbcOheSBEZWxsIFByZWNpc2lvbiBUNTYxMCBjaOG7iSB24bubaSA2NCBHQiBSQU0uIERvIHbhuq15IHRyb25nIHBvc3QgbsOgeSBz4butIGThu6VuZyBi4buZIGThu68gbGnhu4d1IHTGsMahbmcgdOG7sSBuaMawbmcgbmjhu48gaMahbiBuaGnhu4F1IHThu6sgY3Xhu5ljIHRoaSBbU0FSUy1DT1YtMiBDdC1TY2FuIERhdGFzZXRdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vcGxhbWVuZWR1YXJkby9zYXJzY292Mi1jdHNjYW4tZGF0YXNldCkuIFRow6FjaCB0aOG7qWMgxJHhuqd1IHRpw6puIGzDoCB44butIGzDrSBi4buZIGThu68gbGnhu4d1IG7DoHkgduG7gSBk4bqhbmcgcGjDuSBo4bujcCBjaG8gY8OhYyBtw7QgaMOsbmggTWFjaGluZSBMZWFybmluZy9EZWVwIExlYXJuaW5nLiBIw6BtICoqcHJvY2Vzc19pbWFnZV9kYXRhKiogZMaw4bubaSDEkcOieSDEkcaw4bujYyB2aeG6v3QgZOG7sWEgdHLDqm4gdGhhbSBraOG6o28gdOG7qyBbU3BlbmNlciBQYWxsYWRpbm9dKGh0dHBzOi8vd3d3LmxpbmtlZGluLmNvbS9pbi9zcGVuY2VyLXBhbGxhZGluby8pIMSR4buDIHjhu60gbMOtIHPGoSBi4buZIGThu68gbGnhu4d1IOG6o25oOiANCg0KYGBge3J9DQojIENsZWFyIFIgZW52aXJvbm1ldDogDQpybShsaXN0ID0gbHMoKSkNCg0KbGlicmFyeShtYWdyaXR0cikNCmxpYnJhcnkoa2VyYXMpDQoNCiMgRnVuY3Rpb24gZm9yIHByZS1wcm9jZXNzaW5nIGltYWdlIGRhdGE6IA0KDQpwcm9jZXNzX2ltYWdlX2RhdGEgPC0gZnVuY3Rpb24oaW1hZ2VfcGF0aCwgbl9kaW0pIHsNCiAgDQogICMgTG9hZCBpbWFnZSBkYXRhOiANCiAgaW1nX2RhdGEgPC0gbGFwcGx5KGltYWdlX3BhdGgsIGltYWdlX2xvYWQsIGdyYXlzY2FsZSA9IFRSVUUpIA0KICANCiAgIyBDb252ZXJ0IHRvIE4tZGltZW5zaW9uYWwgYXJyYXk6DQogIGFycmF5X2RhdGEgPC0gbGFwcGx5KGltZ19kYXRhLCBpbWFnZV90b19hcnJheSkgDQogIA0KICAjIFJlc2l6ZSBhbmQgbm9ybWFsaXplIGFycmF5OiANCiAgDQogIGFycmF5X3Jlc2l6ZWQgPC0gbGFwcGx5KGFycmF5X2RhdGEsIGltYWdlX2FycmF5X3Jlc2l6ZSwgaGVpZ2h0ID0gbl9kaW0sIHdpZHRoID0gbl9kaW0pIA0KICBhcnJheV9ub3JtYWxpemVkIDwtIG5vcm1hbGl6ZShhcnJheV9yZXNpemVkLCBheGlzID0gMSkgDQogIA0KICAjIFJldHVybiBmaW5hbCBvdXRwdXRzOiANCiAgcmV0dXJuKGFycmF5X25vcm1hbGl6ZWQpDQogIA0KfQ0KYGBgDQoNClPhu60gZOG7pW5nIGjDoG0gbsOgeSDEkeG7gyBsb2FkIHbDoCB44butIGzDrSA0IGNhc2VzICgyIGNhc2VzIGzDoCBDVCBpbWFnZSBj4bunYSAyIGLhu4duaCBuaMOibiBuaGnhu4VtIENvdmlkIDE5IHbDoCAyIGNhc2VzIGzDoCBDVCBzY2FuIGPhu6dhIDIgbmfGsOG7nWkga2jDtG5nIGLhu4sgbmhp4buFbSBDb3ZpZCAxOSk6IA0KDQoNCmBgYHtyfQ0KIyBQYXRocyB0byBpbWFnZSBmaWxlczoNCg0KY292aWRfcGF0aCA8LSBkaXIoIkY6L2FyY2hpdmUvQ09WSUQvIiwgZnVsbC5uYW1lcyA9IFRSVUUpIA0KDQpub25fY292aWRfcGF0aCA8LSBkaXIoIkY6L2FyY2hpdmUvbm9uLUNPVklELyIsIGZ1bGwubmFtZXMgPSBUUlVFKSANCg0KIyBTZWxlY3Qgc29tZSBjYXNlczogDQoNCm5fZGltIDwtIDUwMA0KDQpkYXRhXzQgPC0gcHJvY2Vzc19pbWFnZV9kYXRhKGltYWdlX3BhdGggPSBjKGNvdmlkX3BhdGhbMToyXSwgbm9uX2NvdmlkX3BhdGhbMToyXSksIG5fZGltID0gbl9kaW0pDQoNCmRhdGFfNCA8LSBkYXRhXzRbLCAsICwgMV0gIyBFeHRyYWN0IHRoZSBmaXJzdCBjaGFubmVsLiANCmBgYA0KDQpYZW0gcXVhIOG6o25oIENUIHNjYW4gY+G7p2EgNCBjYXNlcyBuw6B5OiANCg0KYGBge3IsIGZpZy53aWR0aD04LCBmaWcuaGVpZ2h0PTh9DQojIENUIHNjYW4gaW1hZ2UgZm9yIDQgcGF0aWVudHM6IA0KDQpsaWJyYXJ5KHJlc2hhcGUyKQ0KbGlicmFyeShkcGx5cikNCg0KbGFwcGx5KDE6NCwgZnVuY3Rpb24oeCkge21lbHQoZGF0YV80W3gsICwgXSl9KSAtPiBsaXN0X2NvdmlkDQoNCmRvLmNhbGwoImJpbmRfcm93cyIsIGxpc3RfY292aWQpIC0+IGRmX3NhbXBsZQ0KDQpsaWJyYXJ5KGdncGxvdDIpDQoNCmRmX3NhbXBsZSAlPiUgDQogIG11dGF0ZSh0eXBlID0gcmVwKGMoIkNvdmlkX0Nhc2UxIiwgIkNvdmlkX0Nhc2UyIiwgIk5vbkNvdmlkX0Nhc2UxIiwgIk5vbkNvdmlkX0Nhc2UyIiksIGVhY2ggPSBuX2RpbSpuX2RpbSwgdGltZSA9IDEpKSAlPiUgDQogIGdncGxvdChhZXMoVmFyMSwgVmFyMiwgZmlsbCA9IHZhbHVlKSkgKyANCiAgZ2VvbV9yYXN0ZXIoKSArDQogIGZhY2V0X3dyYXAofiB0eXBlKSArIA0KICB0aGVtZV9taW5pbWFsKCkgKyANCiAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKSArIA0KICB0aGVtZShwYW5lbC5ncmlkID0gZWxlbWVudF9ibGFuaygpKSArDQogIHRoZW1lKGF4aXMudGl0bGUgPSBlbGVtZW50X2JsYW5rKCkpICsgDQogIHRoZW1lKGF4aXMudGV4dCA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgbGFicyh4ID0gTlVMTCwgeSA9IE5VTEwsIHRpdGxlID0gIkZpZ3VyZSAxOiBDb3ZpZCBhbmQgTm9uLUNvdmlkIENUIHNjYW4gaW1hZ2UiKSArDQogIHNjYWxlX2ZpbGxfdmlyaWRpc19jKCkNCmBgYA0KDQojIFByZXBhcmUgRGF0YSBmb3IgVHJhaW5pbmcgQ05ODQoNCkxvYWQgdsOgIHjhu60gbMOtIHPGoSBi4buZIHRvw6BuIGLhu5kg4bqjbmggQ1Qgc2NhbiwgbOG6pXkgbmfhuqt1IG5oacOqbiA3MCUgY2hvIHRyYWluaW5nIHbDoCAzMCUgY8OybiBs4bqhaSBjaG8gdGVzdGluZzogDQoNCg0KYGBge3J9DQojIFJlc2V0IG5fZGltOiANCg0Kbl9kaW0gPC0gMTAwDQoNCiMgTG9hZCBhbmQgcHJvY2VzcyBkYXRhOiANCg0KY292aWRfcGF0aCAlPiUgDQogIHByb2Nlc3NfaW1hZ2VfZGF0YShuX2RpbSA9IG5fZGltKSAlPiUgDQogIC5bLCAsICwgMV0gLT4gY292aWRfZGF0YQ0KDQpjb3ZpZERhdGFfcmVzaGFwZWQgPC0gYXJyYXlfcmVzaGFwZShjb3ZpZF9kYXRhLCBjKG5yb3coY292aWRfZGF0YSksIG5fZGltKm5fZGltKSkNCg0Kbm9uX2NvdmlkX3BhdGggJT4lIA0KICBwcm9jZXNzX2ltYWdlX2RhdGEobl9kaW0gPSBuX2RpbSkgJT4lIA0KICAuWywgLCAsIDFdIC0+IG5vbmNvdmlkX2RhdGENCiAgDQpub25jb3ZpZERhdGFfcmVzaGFwZWQgPC0gYXJyYXlfcmVzaGFwZShub25jb3ZpZF9kYXRhLCBjKG5yb3cobm9uY292aWRfZGF0YSksIG5fZGltKm5fZGltKSkNCg0KaW1hZ2VfZGF0YSA8LSByYmluZChjYmluZChjb3ZpZERhdGFfcmVzaGFwZWQsIDEpLCBjYmluZChub25jb3ZpZERhdGFfcmVzaGFwZWQsIDApKSANCg0KIyBTaHVmZmxlIG91ciBkYXRhOiANCg0Kc2V0LnNlZWQoMTIwMTIwMjApDQppbWFnZV9kYXRhIDwtIGltYWdlX2RhdGFbc2FtcGxlKG5yb3coaW1hZ2VfZGF0YSksIHJlcGxhY2UgPSBGQUxTRSksIF0NCg0KIyBTZXQgNzAlIGRhdGEgZm9yIHRyYWluaW5nLCAzMCUgZm9yIHRlc3RpbmcgQ05OOiANCg0Kc2V0LnNlZWQoMTIwMTIwMjApDQoNCnNwbGl0IDwtIHNhbXBsZSh4ID0gMToyLCBzaXplID0gbnJvdyhpbWFnZV9kYXRhKSwgcmVwbGFjZSA9IFRSVUUsIHByb2IgPSBjKDAuNywgMC4zKSkNCg0KdHJhaW4gPC0gaW1hZ2VfZGF0YVtzcGxpdCA9PSAxLCBdDQoNCnRlc3QgPC0gaW1hZ2VfZGF0YVtzcGxpdCA9PSAyLCBdDQoNCnRyYWluX3RhcmdldCA8LSBpbWFnZV9kYXRhW3NwbGl0ID09IDEsIG5fZGltKm5fZGltICsgMV0gIyBMYWJlbCBpbiB0cmFpbmluZyBkYXRhc2V0LiANCg0KdGVzdF90YXJnZXQgPC0gaW1hZ2VfZGF0YVtzcGxpdCA9PSAyLCBuX2RpbSpuX2RpbSArIDFdICMgTGFiZWwgaW4gdGVzdGluZyBkYXRhc2V0LiANCg0KIyBDb252ZXJ0IHRvIGNhdGVnb3JpY2FsIGZvciBiaW5hcnkgY2xhc3NpZmljYXRpb246IA0KdHJhaW5fbGFiZWwgPC0gdG9fY2F0ZWdvcmljYWwodHJhaW5fdGFyZ2V0KQ0KDQp0ZXN0X2xhYmVsIDwtIHRvX2NhdGVnb3JpY2FsKHRlc3RfdGFyZ2V0KQ0KYGBgDQoNCiMgQ29udm9sdXRpb25hbCBOZXVyYWwgTmV0d29yaw0KDQpbQ29udm9sdXRpb25hbCBOZXVyYWwgTmV0d29yayAtIENOTl0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvQ29udm9sdXRpb25hbF9uZXVyYWxfbmV0d29yaykgxJHGsOG7o2MgbOG7sWEgY2jhu41uIGzDoG0gbcO0IGjDrG5oIHBow6JuIGxv4bqhaS4gVHJvbmcgcG9zdCBuw6B5LCBba2VyYXNdKGh0dHBzOi8va2VyYXMuaW8vYWJvdXQvKSAtIG3hu5l0IHBsYXRmb3JtIHRoxrDhu51uZyDEkcaw4bujYyBz4butIGThu6VuZyBjaG8gRGVlcCBMZWFybmluZyDEkcaw4bujYyBz4butIGThu6VuZyDEkeG7gyBodeG6pW4gbHV54buHbiBDTk4uIFF1w6EgdHLDrG5oIGh14bqlbiBsdXnhu4duIENOTiBkxrDhu5tpIMSRw6J5IGPDsyB0aOG7gyBt4bqldCB2w6BpIGJhIHBow7p0OiANCg0KDQpgYGB7cn0NCiMgU2V0IGRyb3AgcmF0ZTogDQoNCmRyb3BfcmF0ZSA8LSAwLjINCg0KbXlfZGltIDwtIDIqKjEwDQoNCiMgU2V0dXAgQ05OOiANCg0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQ0KICBsYXllcl9kZW5zZSh1bml0cyA9IG15X2RpbSwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIA0KICBsYXllcl9kcm9wb3V0KGRyb3BfcmF0ZSkgJT4lDQogIGxheWVyX2RlbnNlKHVuaXRzID0gbXlfZGltIC8gMiwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lDQogIGxheWVyX2Ryb3BvdXQoZHJvcF9yYXRlKSAlPiUNCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSBteV9kaW0gLyA0LCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUNCiAgbGF5ZXJfZHJvcG91dChkcm9wX3JhdGUpICU+JQ0KICBsYXllcl9kZW5zZSh1bml0cyA9IDIsIGFjdGl2YXRpb24gPSAic2lnbW9pZCIpICU+JSAjIEZvciBiaW5hcnkgY2xhc3NpZmljYXRpb24uIA0KICBjb21waWxlKG9wdGltaXplciA9ICJhZGFtIiwgDQogICAgICAgICAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwgDQogICAgICAgICAgbWV0cmljcyA9ICJhY2N1cmFjeSIpDQoNCiMgVHJhaW4gQ05OOiANCg0KZml0X2NvdmlkIDwtIG1vZGVsICU+JQ0KICBmaXQoeCA9IHRyYWluLA0KICAgICAgeSA9IHRyYWluX2xhYmVsLCANCiAgICAgIGVwb2NocyA9IDEwKjUsDQogICAgICBiYXRjaF9zaXplID0gMioqNSwgDQogICAgICB2ZXJib3NlID0gMiwNCiAgICAgIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIpDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyBraOG6o28gc8OhdCBxdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiBDTk46IA0KDQpgYGB7cn0NCiMgUHJvY2VzcyBvZiB0cmFpbmluZyBDTk46IA0KDQpwbG90KGZpdF9jb3ZpZCkgKyANCiAgbGFicyh0aXRsZSA9ICJGaWd1cmUgMjogUHJvY2VzcyBvZiB0cmFpbmluZyBDb252b2x1dGlvbmFsIE5ldXJhbCBOZXR3b3JrICIpICsgDQogIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJ0b3AiKQ0KYGBgDQoNCg0KIyBNb2RlbCBQZXJmb3JtYW5jZQ0KDQpW4bubaSBDTk4gxJHDoyDEkcaw4bujYyBodeG6pW4gbHV54buHbiDhu58gdHLDqm4sIGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgxJHhu4MgxJHDoW5oIGdpw6Ega2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGtoaSDDoXAgZOG7pW5nIGNobyBi4buZIGThu68gbGnhu4d1IHRlc3Q6IA0KDQpgYGB7cn0NCiMgVXNlIENOTiBmb3IgcHJlZGljdGluZzogDQoNCnByb2JfcHJlZGljdGVkIDwtIG1vZGVsICU+JQ0KICBwcmVkaWN0KHRlc3QpICU+JSANCiAgLlssIDJdDQoNCiMgTW9kZWwgcGVyZm9ybWFuY2UgYnkgY29uZnVzaW9uIG1hdHJpeDogDQoNCmNsYXNzX3ByZWRpY3RlZCA8LSBjYXNlX3doZW4ocHJvYl9wcmVkaWN0ZWQgPiAwLjUgfiAiQ292aWQiLCBUUlVFIH4gIk5vbkNvdmlkIikgJT4lIGZhY3RvcigpDQoNCmNsYXNzX2FjdHVhbHMgPC0gY2FzZV93aGVuKHRlc3RfdGFyZ2V0ID09IDEgfiAiQ292aWQiLCBUUlVFIH4gIk5vbkNvdmlkIikgJT4lIGZhY3RvcigpDQoNCmxpYnJhcnkoY2FyZXQpDQoNCmNvbmZ1c2lvbk1hdHJpeChkYXRhID0gY2xhc3NfcHJlZGljdGVkLCByZWZlcmVuY2UgPSBjbGFzc19hY3R1YWxzLCBwb3NpdGl2ZSA9ICJDb3ZpZCIpDQoNCg0KYGBgDQoNClThu6sgQ29uZnVzaW9uIE1hdHJpeCB0YSBjw7MgdGjhu4MgdGjhuqV5IENOTiDEkeG6oXQgbeG7qWMgY2jDrW5oIHjDoWMgMTAwJSB0csOqbiBi4buZIGThu68gbGnhu4d1IHRlc3QuIA0KDQojIFJlZmVyZW5jZXMNCg0KMS4gW0RlZXAgTGVhcm5pbmcgd2l0aCBSXShodHRwczovL3d3dy5tYW5uaW5nLmNvbS9ib29rcy9kZWVwLWxlYXJuaW5nLXdpdGgtcikuIA0KMi4gW1Byb2dyYW1taW5nIENvbXB1dGVyIFZpc2lvbiB3aXRoIFB5dGhvbjogVG9vbHMgYW5kIGFsZ29yaXRobXMgZm9yIGFuYWx5emluZyBpbWFnZXNdKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vUHJvZ3JhbW1pbmctQ29tcHV0ZXItVmlzaW9uLVB5dGhvbi1hbGdvcml0aG1zL2RwLzE0NDkzMTY1NDkpLiANCjMuIFtTaW1wbGUgSW1hZ2UgQ2xhc3NpZmljYXRpb24gd2l0aCBLZXJhc10oaHR0cHM6Ly9ycHVicy5jb20vc3BhbGxhZGlubzE0LzY1MzIzOSkuDQo0LiBbUGl4ZWxzLCBBcnJheXMsIGFuZCBJbWFnZXNdKGh0dHBzOi8vbGV2ZWx1cC5naXRjb25uZWN0ZWQuY29tL3BpeGVscy1hcnJheXMtYW5kLWltYWdlcy1lZjNmMDM2MzhmZTcpDQoNCiMgUiBFbnZpcm9ubWVudCBhbmQgT1MNCg0KYGBge3J9DQpzZXNzaW9uSW5mbygpDQpgYGANCg0KDQoNCg0KYGBgDQoNCg==