This is a simple CNN (convolutional neural network) approach to identify whether a spider in a top-down cropped photo is male or female. I wrote this with behavioral video tracking in mind, where a fast and reliable way to id the sex of ‘blobs’ within video frames can be helpful (for instance when a tracking program switches the identity of a subject after a close interaction). The approach is mostly based on the excellent “Deep Learning in R” by F Chollet and JJ Allaire. Currently working with my model species Habronattus pyrrithrix at >96% accuracy, can be further pushed by retraining the model with a larger batch of training images, adapted to a different species, or extended to work across species with a sufficiently large training image set.

Setting up a new image classification CNN from scratch

Load required packages.

library(keras)
library(stringr)
library(dplyr)
library(ggplot2)
theme_set(theme_bw())

Split images into training, validation, and test sets

Images from both sexes should be in a single folder, with file names like e.g. “male.27.png”. This chunk will create a folder structure that can be fed to the network later.

original_dataset_dir <- "hapy"
base_dir <- "hapy_small"
dir.create(base_dir)
train_dir <- file.path(base_dir, "train")
dir.create(train_dir)
validation_dir <- file.path(base_dir, "validation")
dir.create(validation_dir)
test_dir <- file.path(base_dir, "test")
dir.create(test_dir)
train_female_dir <- file.path(train_dir, "female")
dir.create(train_female_dir)
train_male_dir <- file.path(train_dir, "male")
dir.create(train_male_dir)
validation_female_dir <- file.path(validation_dir, "female")
dir.create(validation_female_dir)
validation_male_dir <- file.path(validation_dir, "male")
dir.create(validation_male_dir)
test_female_dir <- file.path(test_dir, "female")
dir.create(test_female_dir)
test_male_dir <- file.path(test_dir, "male")
dir.create(test_male_dir)

The original image set is now ready to be split and copied into the folder structure we just set up. We’ll do an 80/20 training/test split, with the training set also split 80/20 into train and validation images (so 64/16/20).

images <- list.files(original_dataset_dir)
n.female <- sum(str_detect(images, "female"))
n.male <- sum(!str_detect(images, "female"))

idx.f.train <- sample(1:n.female, n.female * 0.64)
idx.f.val <- sample((1:n.female)[-idx.f.train], n.female * 0.16)
idx.f.test <- sample((1:n.female)[-c(idx.f.train, idx.f.val)], n.female * 0.2)

idx.m.train <- sample(1:n.male, n.male * 0.64)
idx.m.val <- sample((1:n.male)[-idx.f.train], n.male * 0.16)
idx.m.test <- sample((1:n.male)[-c(idx.f.train, idx.f.val)], n.male * 0.2)

fnames <- paste0("female.", idx.f.train, ".png")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(train_female_dir))

fnames <- paste0("female.", idx.f.val, ".png")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(validation_female_dir))

fnames <- paste0("female.", idx.f.test, ".png")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(test_female_dir))

fnames <- paste0("male.", idx.m.train, ".png")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(train_male_dir))

fnames <- paste0("male.", idx.m.val, ".png")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(validation_male_dir))

fnames <- paste0("male.", idx.m.test, ".png")
file.copy(file.path(original_dataset_dir, fnames),
          file.path(test_male_dir))

Initialize Neural Network

Time to set up the network (or model). This consists of a stack of convolution layers that filter the source images into increasingly abstract visual “modules”, pooling layers that downsample the resulting complexity, and a big dense layer (the actual “neural” part) followed by a classification layer that makes the final decision.

model <- keras_model_sequential() %>%
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu",
                input_shape = c(150, 150, 3)) %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = "relu") %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  layer_flatten() %>%
  layer_dropout(rate = 0.5) %>%
  layer_dense(units = 512, activation = "relu") %>%
  layer_dense(units = 1, activation = "sigmoid")
model %>% compile(
  loss = "binary_crossentropy",
  optimizer = optimizer_rmsprop(lr = 1e-4),
  metrics = c("acc")
)

Data Augmentation

For this application I only used a very small number of training images. That means that characteristics of the photos themselves (white balance, subject orientation, etc.) can have a large undesirable impact on classification. In this step, we carry out random transforms on the training images to augment the dataset.

datagen <- image_data_generator(
  rescale = 1/255, 
  rotation_range = 40,
  width_shift_range = 0.2, 
  height_shift_range = 0.2,
  shear_range = 0.2,
  zoom_range = 0.2,
  channel_shift_range = 25,
  horizontal_flip = TRUE,
  fill_mode = "nearest"
)

Plot some examples of randomly transformed images

fnames <- list.files(train_female_dir, full.names = TRUE)
img_path <- fnames[[3]]
img <- image_load(img_path, target_size = c(150, 150))
img_array <- image_to_array(img)
img_array <- array_reshape(img_array, c(1, 150, 150, 3))
augmentation_generator <- flow_images_from_data(
  img_array,
  generator = datagen,
  batch_size = 1
)
op <- par(mfrow = c(2, 2), pty = "s", mar = c(1, 0, 1, 0))
for (i in 1:4) {
  batch <- generator_next(augmentation_generator)
  plot(as.raster(batch[1,,,]))
}
par(op)

Provide functions to stream training data to the model

When working with large amounts of training images, the computer’s memory can be quickly exceeded. This chunk sets up functions that feed small batches of images to the model instead of all at once.

test_datagen <- image_data_generator(rescale = 1/255)
train_generator <- flow_images_from_directory(
  train_dir,
  datagen,
  target_size = c(150, 150),
  batch_size = 8,
  class_mode = "binary"
)
Found 107 images belonging to 2 classes.
validation_generator <- flow_images_from_directory(
  validation_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 8,
  class_mode = "binary"
)
Found 26 images belonging to 2 classes.

Training the model

Now for the fun part. We feed the model our training images and watch it get better at determining spider sex. The validation accuracy should increase with each epoch until it flattens out.

history <- model %>% fit_generator(
  train_generator,
  steps_per_epoch = 10,
  epochs = 50,
  validation_data = validation_generator,
  validation_steps = 5,
  verbose = 0
)

Plot the training results

plot(history)

Testing model accuracy on novel images

Here we unleash our model on the test images we set aside in the beginning. It should be fairly accurate.

test_generator <- flow_images_from_directory(
  test_dir,
  test_datagen,
  target_size = c(150, 150),
  batch_size = 8,
  class_mode = "binary"
)
Found 33 images belonging to 2 classes.
model %>% evaluate_generator(test_generator, steps = 50)
$loss
[1] 0.05357823

$acc
[1] 0.969697

Saving the trained model

Let’s save the model weights so we don’t have to re-train the model in the future.

if(!file.exists("hapy_sexer.h5")){
model %>% save_model_hdf5("hapy_sexer.h5") 
}

Applying the model to a new image

Now for the payoff, determining the sex of a spider in a new image that wasn’t part of the training set. First we load the image and transform it into a tensor that can be inserted into the model:

img_path <- "hapym.png" 
img <- image_load(img_path, target_size = c(150, 150))
img_tensor <- image_to_array(img)
img_tensor <- array_reshape(img_tensor, c(1, 150, 150, 3))
img_tensor <- img_tensor / 255

Let’s display the transformed image before classifying:

plot(as.raster(img_tensor[1,,,]))

Here we go, 0 means female, 1 means male. A strapping spider lad.

# Classify: 0 is female, 1 is male
model %>% predict(img_tensor)
          [,1]
[1,] 0.9968179
LS0tCnRpdGxlOiAiRGV0ZXJtaW5lIGp1bXBpbmcgc3BpZGVyIHNleCB3aXRoIGEgQ29udk5ldCIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKVGhpcyBpcyBhIHNpbXBsZSBDTk4gKGNvbnZvbHV0aW9uYWwgbmV1cmFsIG5ldHdvcmspIGFwcHJvYWNoIHRvIGlkZW50aWZ5IHdoZXRoZXIgYSBzcGlkZXIgaW4gYSB0b3AtZG93biBjcm9wcGVkIHBob3RvIGlzIG1hbGUgb3IgZmVtYWxlLiBJIHdyb3RlIHRoaXMgd2l0aCBiZWhhdmlvcmFsIHZpZGVvIHRyYWNraW5nIGluIG1pbmQsIHdoZXJlIGEgZmFzdCBhbmQgcmVsaWFibGUgd2F5IHRvIGlkIHRoZSBzZXggb2YgJ2Jsb2JzJyB3aXRoaW4gdmlkZW8gZnJhbWVzIGNhbiBiZSBoZWxwZnVsIChmb3IgaW5zdGFuY2Ugd2hlbiBhIHRyYWNraW5nIHByb2dyYW0gc3dpdGNoZXMgdGhlIGlkZW50aXR5IG9mIGEgc3ViamVjdCBhZnRlciBhIGNsb3NlIGludGVyYWN0aW9uKS4gVGhlIGFwcHJvYWNoIGlzIG1vc3RseSBiYXNlZCBvbiB0aGUgZXhjZWxsZW50ICJEZWVwIExlYXJuaW5nIGluIFIiIGJ5IEYgQ2hvbGxldCBhbmQgSkogQWxsYWlyZS4gQ3VycmVudGx5IHdvcmtpbmcgd2l0aCBteSBtb2RlbCBzcGVjaWVzIF9IYWJyb25hdHR1cyBweXJyaXRocml4XyBhdCA+OTYlIGFjY3VyYWN5LCBjYW4gYmUgZnVydGhlciBwdXNoZWQgYnkgcmV0cmFpbmluZyB0aGUgbW9kZWwgd2l0aCBhIGxhcmdlciBiYXRjaCBvZiB0cmFpbmluZyBpbWFnZXMsIGFkYXB0ZWQgdG8gYSBkaWZmZXJlbnQgc3BlY2llcywgb3IgZXh0ZW5kZWQgdG8gd29yayBhY3Jvc3Mgc3BlY2llcyB3aXRoIGEgc3VmZmljaWVudGx5IGxhcmdlIHRyYWluaW5nIGltYWdlIHNldC4KCiMjIFNldHRpbmcgdXAgYSBuZXcgaW1hZ2UgY2xhc3NpZmljYXRpb24gQ05OIGZyb20gc2NyYXRjaApMb2FkIHJlcXVpcmVkIHBhY2thZ2VzLgpgYGB7ciB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KGtlcmFzKQpsaWJyYXJ5KHN0cmluZ3IpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoZ2dwbG90MikKdGhlbWVfc2V0KHRoZW1lX2J3KCkpCmBgYAoKCiMjIyBTcGxpdCBpbWFnZXMgaW50byB0cmFpbmluZywgdmFsaWRhdGlvbiwgYW5kIHRlc3Qgc2V0cwpJbWFnZXMgZnJvbSBib3RoIHNleGVzIHNob3VsZCBiZSBpbiBhIHNpbmdsZSBmb2xkZXIsIHdpdGggZmlsZSBuYW1lcyBsaWtlIGUuZy4gIm1hbGUuMjcucG5nIi4gVGhpcyBjaHVuayB3aWxsIGNyZWF0ZSBhIGZvbGRlciBzdHJ1Y3R1cmUgdGhhdCBjYW4gYmUgZmVkIHRvIHRoZSBuZXR3b3JrIGxhdGVyLgpgYGB7cn0Kb3JpZ2luYWxfZGF0YXNldF9kaXIgPC0gImhhcHkiCmJhc2VfZGlyIDwtICJoYXB5X3NtYWxsIgpkaXIuY3JlYXRlKGJhc2VfZGlyKQoKdHJhaW5fZGlyIDwtIGZpbGUucGF0aChiYXNlX2RpciwgInRyYWluIikKZGlyLmNyZWF0ZSh0cmFpbl9kaXIpCgp2YWxpZGF0aW9uX2RpciA8LSBmaWxlLnBhdGgoYmFzZV9kaXIsICJ2YWxpZGF0aW9uIikKZGlyLmNyZWF0ZSh2YWxpZGF0aW9uX2RpcikKCnRlc3RfZGlyIDwtIGZpbGUucGF0aChiYXNlX2RpciwgInRlc3QiKQpkaXIuY3JlYXRlKHRlc3RfZGlyKQoKdHJhaW5fZmVtYWxlX2RpciA8LSBmaWxlLnBhdGgodHJhaW5fZGlyLCAiZmVtYWxlIikKZGlyLmNyZWF0ZSh0cmFpbl9mZW1hbGVfZGlyKQoKdHJhaW5fbWFsZV9kaXIgPC0gZmlsZS5wYXRoKHRyYWluX2RpciwgIm1hbGUiKQpkaXIuY3JlYXRlKHRyYWluX21hbGVfZGlyKQoKdmFsaWRhdGlvbl9mZW1hbGVfZGlyIDwtIGZpbGUucGF0aCh2YWxpZGF0aW9uX2RpciwgImZlbWFsZSIpCmRpci5jcmVhdGUodmFsaWRhdGlvbl9mZW1hbGVfZGlyKQoKdmFsaWRhdGlvbl9tYWxlX2RpciA8LSBmaWxlLnBhdGgodmFsaWRhdGlvbl9kaXIsICJtYWxlIikKZGlyLmNyZWF0ZSh2YWxpZGF0aW9uX21hbGVfZGlyKQoKdGVzdF9mZW1hbGVfZGlyIDwtIGZpbGUucGF0aCh0ZXN0X2RpciwgImZlbWFsZSIpCmRpci5jcmVhdGUodGVzdF9mZW1hbGVfZGlyKQoKdGVzdF9tYWxlX2RpciA8LSBmaWxlLnBhdGgodGVzdF9kaXIsICJtYWxlIikKZGlyLmNyZWF0ZSh0ZXN0X21hbGVfZGlyKQoKYGBgCgpUaGUgb3JpZ2luYWwgaW1hZ2Ugc2V0IGlzIG5vdyByZWFkeSB0byBiZSBzcGxpdCBhbmQgY29waWVkIGludG8gdGhlIGZvbGRlciBzdHJ1Y3R1cmUgd2UganVzdCBzZXQgdXAuIFdlJ2xsIGRvIGFuIDgwLzIwIHRyYWluaW5nL3Rlc3Qgc3BsaXQsIHdpdGggdGhlIHRyYWluaW5nIHNldCBhbHNvIHNwbGl0IDgwLzIwIGludG8gdHJhaW4gYW5kIHZhbGlkYXRpb24gaW1hZ2VzIChzbyA2NC8xNi8yMCkuCmBgYHtyfQppbWFnZXMgPC0gbGlzdC5maWxlcyhvcmlnaW5hbF9kYXRhc2V0X2RpcikKbi5mZW1hbGUgPC0gc3VtKHN0cl9kZXRlY3QoaW1hZ2VzLCAiZmVtYWxlIikpCm4ubWFsZSA8LSBzdW0oIXN0cl9kZXRlY3QoaW1hZ2VzLCAiZmVtYWxlIikpCgppZHguZi50cmFpbiA8LSBzYW1wbGUoMTpuLmZlbWFsZSwgbi5mZW1hbGUgKiAwLjY0KQppZHguZi52YWwgPC0gc2FtcGxlKCgxOm4uZmVtYWxlKVstaWR4LmYudHJhaW5dLCBuLmZlbWFsZSAqIDAuMTYpCmlkeC5mLnRlc3QgPC0gc2FtcGxlKCgxOm4uZmVtYWxlKVstYyhpZHguZi50cmFpbiwgaWR4LmYudmFsKV0sIG4uZmVtYWxlICogMC4yKQoKaWR4Lm0udHJhaW4gPC0gc2FtcGxlKDE6bi5tYWxlLCBuLm1hbGUgKiAwLjY0KQppZHgubS52YWwgPC0gc2FtcGxlKCgxOm4ubWFsZSlbLWlkeC5mLnRyYWluXSwgbi5tYWxlICogMC4xNikKaWR4Lm0udGVzdCA8LSBzYW1wbGUoKDE6bi5tYWxlKVstYyhpZHguZi50cmFpbiwgaWR4LmYudmFsKV0sIG4ubWFsZSAqIDAuMikKCmZuYW1lcyA8LSBwYXN0ZTAoImZlbWFsZS4iLCBpZHguZi50cmFpbiwgIi5wbmciKQpmaWxlLmNvcHkoZmlsZS5wYXRoKG9yaWdpbmFsX2RhdGFzZXRfZGlyLCBmbmFtZXMpLAogICAgICAgICAgZmlsZS5wYXRoKHRyYWluX2ZlbWFsZV9kaXIpKQoKZm5hbWVzIDwtIHBhc3RlMCgiZmVtYWxlLiIsIGlkeC5mLnZhbCwgIi5wbmciKQpmaWxlLmNvcHkoZmlsZS5wYXRoKG9yaWdpbmFsX2RhdGFzZXRfZGlyLCBmbmFtZXMpLAogICAgICAgICAgZmlsZS5wYXRoKHZhbGlkYXRpb25fZmVtYWxlX2RpcikpCgpmbmFtZXMgPC0gcGFzdGUwKCJmZW1hbGUuIiwgaWR4LmYudGVzdCwgIi5wbmciKQpmaWxlLmNvcHkoZmlsZS5wYXRoKG9yaWdpbmFsX2RhdGFzZXRfZGlyLCBmbmFtZXMpLAogICAgICAgICAgZmlsZS5wYXRoKHRlc3RfZmVtYWxlX2RpcikpCgpmbmFtZXMgPC0gcGFzdGUwKCJtYWxlLiIsIGlkeC5tLnRyYWluLCAiLnBuZyIpCmZpbGUuY29weShmaWxlLnBhdGgob3JpZ2luYWxfZGF0YXNldF9kaXIsIGZuYW1lcyksCiAgICAgICAgICBmaWxlLnBhdGgodHJhaW5fbWFsZV9kaXIpKQoKZm5hbWVzIDwtIHBhc3RlMCgibWFsZS4iLCBpZHgubS52YWwsICIucG5nIikKZmlsZS5jb3B5KGZpbGUucGF0aChvcmlnaW5hbF9kYXRhc2V0X2RpciwgZm5hbWVzKSwKICAgICAgICAgIGZpbGUucGF0aCh2YWxpZGF0aW9uX21hbGVfZGlyKSkKCmZuYW1lcyA8LSBwYXN0ZTAoIm1hbGUuIiwgaWR4Lm0udGVzdCwgIi5wbmciKQpmaWxlLmNvcHkoZmlsZS5wYXRoKG9yaWdpbmFsX2RhdGFzZXRfZGlyLCBmbmFtZXMpLAogICAgICAgICAgZmlsZS5wYXRoKHRlc3RfbWFsZV9kaXIpKQpgYGAKCiMjIyBJbml0aWFsaXplIE5ldXJhbCBOZXR3b3JrClRpbWUgdG8gc2V0IHVwIHRoZSBuZXR3b3JrIChvciBtb2RlbCkuIFRoaXMgY29uc2lzdHMgb2YgYSBzdGFjayBvZiBjb252b2x1dGlvbiBsYXllcnMgdGhhdCBmaWx0ZXIgdGhlIHNvdXJjZSBpbWFnZXMgaW50byBpbmNyZWFzaW5nbHkgYWJzdHJhY3QgdmlzdWFsICJtb2R1bGVzIiwgcG9vbGluZyBsYXllcnMgdGhhdCBkb3duc2FtcGxlIHRoZSByZXN1bHRpbmcgY29tcGxleGl0eSwgYW5kIGEgYmlnIGRlbnNlIGxheWVyICh0aGUgYWN0dWFsICJuZXVyYWwiIHBhcnQpIGZvbGxvd2VkIGJ5IGEgY2xhc3NpZmljYXRpb24gbGF5ZXIgdGhhdCBtYWtlcyB0aGUgZmluYWwgZGVjaXNpb24uIApgYGB7cn0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IDMyLCBrZXJuZWxfc2l6ZSA9IGMoMywgMyksIGFjdGl2YXRpb24gPSAicmVsdSIsCiAgICAgICAgICAgICAgICBpbnB1dF9zaGFwZSA9IGMoMTUwLCAxNTAsIDMpKSAlPiUKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSA2NCwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSAxMjgsIGtlcm5lbF9zaXplID0gYygzLCAzKSwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lCiAgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQocG9vbF9zaXplID0gYygyLCAyKSkgJT4lCiAgbGF5ZXJfY29udl8yZChmaWx0ZXJzID0gMTI4LCBrZXJuZWxfc2l6ZSA9IGMoMywgMyksIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX21heF9wb29saW5nXzJkKHBvb2xfc2l6ZSA9IGMoMiwgMikpICU+JQogIGxheWVyX2ZsYXR0ZW4oKSAlPiUKICBsYXllcl9kcm9wb3V0KHJhdGUgPSAwLjUpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gNTEyLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEsIGFjdGl2YXRpb24gPSAic2lnbW9pZCIpCgptb2RlbCAlPiUgY29tcGlsZSgKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLAogIG9wdGltaXplciA9IG9wdGltaXplcl9ybXNwcm9wKGxyID0gMWUtNCksCiAgbWV0cmljcyA9IGMoImFjYyIpCikKYGBgCgojIyMgRGF0YSBBdWdtZW50YXRpb24KRm9yIHRoaXMgYXBwbGljYXRpb24gSSBvbmx5IHVzZWQgYSB2ZXJ5IHNtYWxsIG51bWJlciBvZiB0cmFpbmluZyBpbWFnZXMuIFRoYXQgbWVhbnMgdGhhdCBjaGFyYWN0ZXJpc3RpY3Mgb2YgdGhlIHBob3RvcyB0aGVtc2VsdmVzICh3aGl0ZSBiYWxhbmNlLCBzdWJqZWN0IG9yaWVudGF0aW9uLCBldGMuKSBjYW4gaGF2ZSBhIGxhcmdlIHVuZGVzaXJhYmxlIGltcGFjdCBvbiBjbGFzc2lmaWNhdGlvbi4gSW4gdGhpcyBzdGVwLCB3ZSBjYXJyeSBvdXQgcmFuZG9tIHRyYW5zZm9ybXMgb24gdGhlIHRyYWluaW5nIGltYWdlcyB0byBhdWdtZW50IHRoZSBkYXRhc2V0LgoKYGBge3J9CmRhdGFnZW4gPC0gaW1hZ2VfZGF0YV9nZW5lcmF0b3IoCiAgcmVzY2FsZSA9IDEvMjU1LCAKICByb3RhdGlvbl9yYW5nZSA9IDQwLAogIHdpZHRoX3NoaWZ0X3JhbmdlID0gMC4yLCAKICBoZWlnaHRfc2hpZnRfcmFuZ2UgPSAwLjIsCiAgc2hlYXJfcmFuZ2UgPSAwLjIsCiAgem9vbV9yYW5nZSA9IDAuMiwKICBjaGFubmVsX3NoaWZ0X3JhbmdlID0gMjUsCiAgaG9yaXpvbnRhbF9mbGlwID0gVFJVRSwKICBmaWxsX21vZGUgPSAibmVhcmVzdCIKKQpgYGAKCiMjIyBQbG90IHNvbWUgZXhhbXBsZXMgb2YgcmFuZG9tbHkgdHJhbnNmb3JtZWQgaW1hZ2VzCgpgYGB7cn0KZm5hbWVzIDwtIGxpc3QuZmlsZXModHJhaW5fZmVtYWxlX2RpciwgZnVsbC5uYW1lcyA9IFRSVUUpCmltZ19wYXRoIDwtIGZuYW1lc1tbM11dCgppbWcgPC0gaW1hZ2VfbG9hZChpbWdfcGF0aCwgdGFyZ2V0X3NpemUgPSBjKDE1MCwgMTUwKSkKaW1nX2FycmF5IDwtIGltYWdlX3RvX2FycmF5KGltZykKaW1nX2FycmF5IDwtIGFycmF5X3Jlc2hhcGUoaW1nX2FycmF5LCBjKDEsIDE1MCwgMTUwLCAzKSkKCmF1Z21lbnRhdGlvbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kYXRhKAogIGltZ19hcnJheSwKICBnZW5lcmF0b3IgPSBkYXRhZ2VuLAogIGJhdGNoX3NpemUgPSAxCikKCm9wIDwtIHBhcihtZnJvdyA9IGMoMiwgMiksIHB0eSA9ICJzIiwgbWFyID0gYygxLCAwLCAxLCAwKSkKCmZvciAoaSBpbiAxOjQpIHsKICBiYXRjaCA8LSBnZW5lcmF0b3JfbmV4dChhdWdtZW50YXRpb25fZ2VuZXJhdG9yKQogIHBsb3QoYXMucmFzdGVyKGJhdGNoWzEsLCxdKSkKfQpwYXIob3ApCmBgYAoKIyMjIFByb3ZpZGUgZnVuY3Rpb25zIHRvIHN0cmVhbSB0cmFpbmluZyBkYXRhIHRvIHRoZSBtb2RlbApXaGVuIHdvcmtpbmcgd2l0aCBsYXJnZSBhbW91bnRzIG9mIHRyYWluaW5nIGltYWdlcywgdGhlIGNvbXB1dGVyJ3MgbWVtb3J5IGNhbiBiZSBxdWlja2x5IGV4Y2VlZGVkLiBUaGlzIGNodW5rIHNldHMgdXAgZnVuY3Rpb25zIHRoYXQgZmVlZCBzbWFsbCBiYXRjaGVzIG9mIGltYWdlcyB0byB0aGUgbW9kZWwgaW5zdGVhZCBvZiBhbGwgYXQgb25jZS4KYGBge3J9CnRlc3RfZGF0YWdlbiA8LSBpbWFnZV9kYXRhX2dlbmVyYXRvcihyZXNjYWxlID0gMS8yNTUpCgp0cmFpbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdHJhaW5fZGlyLAogIGRhdGFnZW4sCiAgdGFyZ2V0X3NpemUgPSBjKDE1MCwgMTUwKSwKICBiYXRjaF9zaXplID0gOCwKICBjbGFzc19tb2RlID0gImJpbmFyeSIKKQoKdmFsaWRhdGlvbl9nZW5lcmF0b3IgPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdmFsaWRhdGlvbl9kaXIsCiAgdGVzdF9kYXRhZ2VuLAogIHRhcmdldF9zaXplID0gYygxNTAsIDE1MCksCiAgYmF0Y2hfc2l6ZSA9IDgsCiAgY2xhc3NfbW9kZSA9ICJiaW5hcnkiCikKYGBgCgojIyMgVHJhaW5pbmcgdGhlIG1vZGVsCk5vdyBmb3IgdGhlIGZ1biBwYXJ0LiBXZSBmZWVkIHRoZSBtb2RlbCBvdXIgdHJhaW5pbmcgaW1hZ2VzIGFuZCB3YXRjaCBpdCBnZXQgYmV0dGVyIGF0IGRldGVybWluaW5nIHNwaWRlciBzZXguIFRoZSB2YWxpZGF0aW9uIGFjY3VyYWN5IHNob3VsZCBpbmNyZWFzZSB3aXRoIGVhY2ggZXBvY2ggdW50aWwgaXQgZmxhdHRlbnMgb3V0LgpgYGB7cn0KaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0X2dlbmVyYXRvcigKICB0cmFpbl9nZW5lcmF0b3IsCiAgc3RlcHNfcGVyX2Vwb2NoID0gMTAsCiAgZXBvY2hzID0gNTAsCiAgdmFsaWRhdGlvbl9kYXRhID0gdmFsaWRhdGlvbl9nZW5lcmF0b3IsCiAgdmFsaWRhdGlvbl9zdGVwcyA9IDUsCiAgdmVyYm9zZSA9IDAKKQpgYGAKClBsb3QgdGhlIHRyYWluaW5nIHJlc3VsdHMKYGBge3J9CnBsb3QoaGlzdG9yeSkKYGBgCgojIyBUZXN0aW5nIG1vZGVsIGFjY3VyYWN5IG9uIG5vdmVsIGltYWdlcwpIZXJlIHdlIHVubGVhc2ggb3VyIG1vZGVsIG9uIHRoZSB0ZXN0IGltYWdlcyB3ZSBzZXQgYXNpZGUgaW4gdGhlIGJlZ2lubmluZy4gSXQgc2hvdWxkIGJlIGZhaXJseSBhY2N1cmF0ZS4KYGBge3J9CnRlc3RfZ2VuZXJhdG9yIDwtIGZsb3dfaW1hZ2VzX2Zyb21fZGlyZWN0b3J5KAogIHRlc3RfZGlyLAogIHRlc3RfZGF0YWdlbiwKICB0YXJnZXRfc2l6ZSA9IGMoMTUwLCAxNTApLAogIGJhdGNoX3NpemUgPSA4LAogIGNsYXNzX21vZGUgPSAiYmluYXJ5IgopCgptb2RlbCAlPiUgZXZhbHVhdGVfZ2VuZXJhdG9yKHRlc3RfZ2VuZXJhdG9yLCBzdGVwcyA9IDUwKQpgYGAKCiMjIFNhdmluZyB0aGUgdHJhaW5lZCBtb2RlbApMZXQncyBzYXZlIHRoZSBtb2RlbCB3ZWlnaHRzIHNvIHdlIGRvbid0IGhhdmUgdG8gcmUtdHJhaW4gdGhlIG1vZGVsIGluIHRoZSBmdXR1cmUuCmBgYHtyfQppZighZmlsZS5leGlzdHMoImhhcHlfc2V4ZXIuaDUiKSl7Cm1vZGVsICU+JSBzYXZlX21vZGVsX2hkZjUoImhhcHlfc2V4ZXIuaDUiKSAKfQpgYGAKCiMjIEFwcGx5aW5nIHRoZSBtb2RlbCB0byBhIG5ldyBpbWFnZQpOb3cgZm9yIHRoZSBwYXlvZmYsIGRldGVybWluaW5nIHRoZSBzZXggb2YgYSBzcGlkZXIgaW4gYSBuZXcgaW1hZ2UgdGhhdCB3YXNuJ3QgcGFydCBvZiB0aGUgdHJhaW5pbmcgc2V0LiBGaXJzdCB3ZSBsb2FkIHRoZSBpbWFnZSBhbmQgdHJhbnNmb3JtIGl0IGludG8gYSB0ZW5zb3IgdGhhdCBjYW4gYmUgaW5zZXJ0ZWQgaW50byB0aGUgbW9kZWw6CmBgYHtyfQppbWdfcGF0aCA8LSAiZGVtbyBpbWFnZXMvaGFweW0ucG5nIiAKCmltZyA8LSBpbWFnZV9sb2FkKGltZ19wYXRoLCB0YXJnZXRfc2l6ZSA9IGMoMTUwLCAxNTApKQppbWdfdGVuc29yIDwtIGltYWdlX3RvX2FycmF5KGltZykKaW1nX3RlbnNvciA8LSBhcnJheV9yZXNoYXBlKGltZ190ZW5zb3IsIGMoMSwgMTUwLCAxNTAsIDMpKQppbWdfdGVuc29yIDwtIGltZ190ZW5zb3IgLyAyNTUKYGBgCgpMZXQncyBkaXNwbGF5IHRoZSB0cmFuc2Zvcm1lZCBpbWFnZSBiZWZvcmUgY2xhc3NpZnlpbmc6IApgYGB7cn0KcGxvdChhcy5yYXN0ZXIoaW1nX3RlbnNvclsxLCwsXSkpCmBgYAoKSGVyZSB3ZSBnbywgMCBtZWFucyBmZW1hbGUsIDEgbWVhbnMgbWFsZS4gQSBzdHJhcHBpbmcgc3BpZGVyIGxhZC4KYGBge3J9CiMgQ2xhc3NpZnk6IDAgaXMgZmVtYWxlLCAxIGlzIG1hbGUKbW9kZWwgJT4lIHByZWRpY3QoaW1nX3RlbnNvcikKYGBgCgo=