Recovering the ink of Herculaneum using neural networks
Published: 2023-08-07 11:00:04

---
title: Recovering the ink of Herculaneum using neural networks
published: true
date: 2023-08-06 23:59:20 UTC
tags: 
canonical_url: https://dataroots.io/research/contributions/vesuvius-challenge-predicting-ink-in-x-ray-data-of-papyrus
---

### Context

In 79 AD, Mount Vesuvius erupted, thousands of scrolls in a library located in a Roman villa in Herculaneum were carbonized by the heat of the volcanic debris. For centuries, as virtually every ancient text exposed to the air decayed and disappeared, these **Herculaneum Papyri** waited underground, intact.

The scrolls were discovered a few hundred years ago and have been waiting to be read using modern techniques. In 2015, Dr. Brent Seales was the first to read some scrolls without opening them using X-ray tomography and computer vision techniques.

Unfortunately, X-ray scans are not sufficient to read the scrolls as there is no clear contrast between the x-ray data of the carbon-based ink versus the carbon-based papyrus. It was however found out that machine learning models, trained on manually annotated fragments could pick up subtle surface patterns in the X-ray scans indicating the presence of ink on the papyrus.

Therefore, Dr. Brent Seales and his team asked for help to the ML community by launching the [Vesuvius Challenge](https://scrollprize.org/?ref=dataroots.ghost.io). This challenge offers a $700k grand prize to the first team able to read the Herculaneum Papyri. A big part of this challenge consist of being able to detect ink on 3D X-ray scans of detached fragments. Because it is possible to extract ground truth data of the ink on the fragments, the [Ink Detection Progress Prize](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/overview?ref=dataroots.ghost.io) has been launched on Kaggle.

### Aim of this post

Artificial neural networks are commonly used for segmentation task using 2D images data as training. The ink detection challenge is particular as it is a semantic segmentation problem using 3D (X-ray scan) data. There is not a lot of scientific literature available to assess the performance of neural networks for this type of problem.

The purpose of this post is to further explore this problem, discuss our approach, elaborate on specific model architectures, possible preprocessing, etc. and see how these perform on this specific use case.

<video src="https://dataroots.ghost.io/content/media/2023/07/ink-detection-anim2-dark--1--copy.webm" poster="https://img.spacergif.org/v1/1920x720/0a/spacer.png" width="1920" height="720" loop autoplay muted playsinline preload="metadata" style="background: transparent url('https://dataroots.ghost.io/content/images/2023/07/media-thumbnail-ember251.jpg') 50% 50% / cover no-repeat;"></video>
<button><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path></svg></button>

<button><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"></path></svg></button><button><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"></rect></svg></button>0:00
/
<input type="range" max="100" value="0"><button>1×</button><button><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"></path></svg></button><button><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"></path></svg></button><input type="range" max="100" value="100">

_Ink detection based on 3D X-ray scans_

## Data

The training data available for the Ink detection challenge consist of three broken-off fragments of scrolls that were opened physically (and destroyed in the process). To generate labels, these fragments where photographed and later ink was annotated manually on the resulting [infrared images](https://www.notion.so/Vesuvius-challenge-Ink-detection-a67e2e8542e5442ca01d80d7fb755d82?pvs=21&ref=dataroots.ghost.io).

![](https://dataroots.ghost.io/content/images/2023/07/ __results___ 3_0.png)
_Images generated in the data exploration notebook_

Since the goal is to be able to read ink from the digitally unrolled scrolls, the data available for inference consists of just the [3D X-ray scans](https://www.notion.so/Vesuvius-challenge-Ink-detection-a67e2e8542e5442ca01d80d7fb755d82?pvs=21&ref=dataroots.ghost.io) of the fragments without any IR images. And unlike on the IR images, the ink is not immediately visible on those. However, experiments have shown that it is possible for a trained ML model to indirectly detect the ink anyway.

![](https://dataroots.ghost.io/content/images/2023/07/x-ray_1--1-.gif)
_Horizontal cross-sections of the X-ray data of fragment 1. The number in the top right is the z coordinate of the shown cross-section._

The actual training objective is to generate a 2D, binary mask for a test fragment where a 1 indicates a pixel with ink. To evaluate this the F0.5, metric is used

![](https://dataroots.ghost.io/content/images/2023/07/image-12.png)

This is a variant of the F1 metric that values precision higher than recall since this results in more readable letters in this specific use case.

![](https://dataroots.ghost.io/content/images/2023/07/Untitled--8-.png)
_Representation of the F0.5 metric_

## Challenges

This section will briefly discuss the main challenges in this competition.

### Data set size

A first big challenge is the limited amount of training data. The train set consists of only three different fragments with sizes ranging from approximately 8000x6000px to 14000x8000px. Or about 3500 patches of 256x256px. However if you increase the patch size to 512x512px or even 1024x1024px, the amount of training samples is reduced to around 900 or 225 respectively. Additionally, these fragments are not perfectly rectangular so this still includes quite some completely empty crops in the border of the images. So, while normally increasing crop sizes would be an easy way to increase performance, this is made much more challenging here as the model will be much more prone to overfitting and would require robust regularization to compensate for this.

### 3D data

A much more interesting challenge is the actual shape of the data. While the goal is to generate a 2D mask, the data used to generate this is very much three dimensional, consisting of an X-ray scan with 65 layers. Additionally, since the ink is not clearly visible in X-ray data and has to be identified indirectly in slight changes in texture, surface height, … resulting in the information about the ink being located in “roughly” the upper half of the papyrus. Since the [height of the papyri](https://www.notion.so/Vesuvius-challenge-Ink-detection-a67e2e8542e5442ca01d80d7fb755d82?pvs=21&ref=dataroots.ghost.io) also varies significantly both within and between fragments, one can’t simply select a few layers to greatly simplify the problem but does actually have to take the 3D structure into account.

![](https://dataroots.ghost.io/content/images/2023/07/fig-1-.png)
_Vertical cross-sections of the X-ray data. The images on the left are slices along the x-axis, images on the right are slices along the y-axis_

Vertical cross-sections of the X-ray data. The images on the left are slices along the x-axis, images on the right are slices along the y-axis

As a result one has to either use 2.5D or 3D segmentation approaches. Additionally, every operation suddenly becomes much more memory-hungry since all images now have up to 65 channels instead of the usual 3 for RGB images or just 1 for grayscale images.

## Approach

Here the approach we took to solve the challenges previously mentioned challenges will be discussed. Starting with the model architecture used, attempts to flatten the data, followed by regularisation, optimizing inference and finally a quick recap of the final solution.

### U-net Architecture

The U-net architecture contains an encoder part to extract the relevant features from the image and a decoder part to build the segmentation mask from these features. This is found to be very effective for semantic segmentation tasks where each pixel of the input image needs to be classified. Moreover skip connections are used to add feature maps from the encoder to the corresponding decoder layers which brings high-resolution information that might be lost in the downsampling layers. You can refer to the research of _[Ronneberger et al.](https://arxiv.org/pdf/1505.04597.pdf?ref=dataroots.ghost.io)_ for more information about UNET’s.

![](https://dataroots.ghost.io/content/images/2023/07/Untitled--9-.png)
_U-net architecture representation from Ronneberger et al. . The grey arrows represent the skip connections._

### 3D vs 2.5D CNN segmentation

To handle the uncertainty and variability of in which slices the ink is located in the 3D X-Ray data of our fragments, 3D and  2.5D CNN (convolutional neural network) segmentation methods are explored.

**3D CNN model**  
In 3D CNN, the input is a full 3D volume. The convolution kernels used have 3 dimensions and thus spatial relationships between neighbouring slices are captured. The exact model implemented here is a 18 layers ResNet model. The structure of the ResNet 18 contains 4 layers of 2 basic blocks, each block consists of two 3D convolutions. A skip connection is added between every block in order to let the gradients flow directly backward to initial filters. More info about ResNets can be in [this blogpost](https://towardsdatascience.com/understanding-and-visualizing-resnets-442284831be8?ref=dataroots.ghost.io).

**2.5D CNN model**  
The input data in 2.5D CNN is a set of 2D slices extracted from the 3D volume, each slice is treated as an individual input by 2 dimensional convolution kernels. The outputs of each slices are then combined to generate the final prediction. The exact model implemented here is a U-net architecture, using the EfficientNet b0 backbone as an encoder, from the [segmentation models library](https://github.com/qubvel/segmentation_models.pytorch?ref=dataroots.ghost.io) for PyTorch.

Baseline scores for these two models can be found in the table below:

<!--kg-card-begin: html-->

| Experiment | Crop Size | Z-channels | validation F0.5 |
| --- | --- | --- | --- |
| Resnet18 - 3D baseline | 256 x 256 | 24 - 40 | 0.56 |
| Efficientnet - 2.5D baseline | 224 x 224 | 29 -34 | 0.51 |

<!--kg-card-end: html-->

Overall the 3D Unet was found to outperform the 2.5D version. This was not a very big surprise with lots of the information about the ink being distributed throughout multiple layers. One idea was to flatten the fragments (as discussed below) to mitigate this problem but without much success.

### Flattening the fragments

One idea to decrease the complexity of the problem (and the network required to solve it) was to flatten the fragment and focus on only a few layers, greatly reducing the complexity of the problem by eliminating variables that don’t contribute in the lower layers of the papyrus and the air above the fragments.

To this end, several classical computer vision segmentation techniques were tried to segment the papyrus from the air (a full comparison can be found in [this notebook](https://www.kaggle.com/code/thenoodleninja/fragment-flattening?ref=dataroots.ghost.io)). A deep learning approach would probably have performed better but there were no labels available to train this.

After some experimenting, using a Sobel filter over the Z-axis, thresholding and taking the argmax resulted in the most robust solution in most cases. This also had the added advantage of being a “memory friendly” solution (unlike KMeans and DBSCAN). All classical CV techniques were found to be very sensitive to noise so denoising the fragment beforehand with the help of a Gaussian filter proved essential.

![](https://dataroots.ghost.io/content/images/2023/07/height_map.png)
_Height map of fragment 1_

Three different approaches were tried for incorporating this height information into the training data of the 2.5D baseline:

1. Add the height map as an additional channel, resulting in a model input consisting of 6 intensity channels (z-layer 29-34) + 1 height map channel.
2. Use the height map to flatten the training data and take the top 6 layers of the flattened papyrus, with one air layer as a buffer as model input.
3. See 2. but also include the height map channel as input, for a final input of 7 layers, to reintroduce any height information that was lost.

Despite the idea sounding nice in theory, either the quality of the height map was not sufficiently accurate or the loss in texture/height information outweighed the advantage of having a better cross-section.

<!--kg-card-begin: html-->

| experiment | Model input | # channels | validation F0.5 |
| --- | --- | --- | --- |
| baseline | cross-section (29-34) | 6 | 0.51 |
| 1. | cross-section (29-34) + height map | 7 | 0.49 |
| 2. | flattened cross-section | 6 | 0.50 |
| 3. | flattened cross-section + height map | 7 | 0.50 |

<!--kg-card-end: html-->

![](https://dataroots.ghost.io/content/images/2023/07/Figure_1.png)
_Crop where the height map does look promising_

### Handling overfitting

When trying to increase the crop size that the model uses, it started overfitting noticeably more since doubling the size of the crop side results in 1/4th the amount of training samples. Due to the already limited amount of training data this makes a very big difference. As such robust regularization is needed to offset this.

\*All experiments below show the results of a UNet with a 3D resnet as backbone, trained on crops of 512x512 px.

**1. Augmentation**  
A first way to increase regularization is to increase the amount of training data. Specifically generating new samples using augmentation.

For augmentation, transforms from the [Albumentations](https://albumentations.ai/?ref=dataroots.ghost.io) package was used and the list of used augmentations can be found below.

<!--kg-card-begin: html-->

| augmentation | p | Description |
| --- | --- | --- |
| HorizontalFlip | 0.5 | Flip the image over the y-axis. |
| VerticalFlip | 0.5 | Flip the image over the x-axis. |
| ShiftScaleRotate | 0.75 | Randomly apply a translation, scaling and rotation of the image. |
| CoarseDropout | 0.2 | Randomly introduce a hole in the image. |
| ChannelDropout | 0.2 | Randomly drop a channel from the image. |

<!--kg-card-end: html-->

When comparing experiments with different amounts of augmentations, it becomes clear how important it is for this use case.

While the model with a channel dropout of 0.2 clearly generalizes the best, the one with a slightly more conservative p=0.1 for channel dropout generally yields slightly higher optimal F0.5 scores but is calibrated worse.

<!--kg-card-begin: html-->

| experiment | epoch | train\_loss | val\_loss | thresh | F0.5 |
| --- | --- | --- | --- | --- | --- |
| no augmentation\* | 8 | 0.50 | 0.50 | 0.75 | 0.50 |
| no channel dropout | 23 | 0.33 | 0.42 | 0.8 | 0.57 |
| channel\_dropout 0.1 | 30 | 0.36 | 0.39 | 0.9 | 0.61 |
| channel\_dropout 0.2 | 32 | 0.36 | 0.42 | 0.55 | 0.58 |

\* Training with no augmentation was stopped early to save on limited GPU time<!--kg-card-end: html-->

![](https://dataroots.ghost.io/content/images/2023/07/image-8.png)

**2. Dice loss**  
A second way to have a more robust model is not to just use binary cross entropy, but [DICE loss](https://arxiv.org/pdf/1707.03237v3.pdf?ref=dataroots.ghost.io) (2(_p_∩_gt)/(p_∪_gt))_, as well. This loss was designed specifically as a loss function for highly unbalanced segmentations. Using this loss does lead to a model that is much less sensitive to the threshold but does have a lower optimal F0.5. Using an average of the two leads to a model that has a slightly lower optimal F0.5 but is much more robust to not having an optimal threshold.

![](https://dataroots.ghost.io/content/images/2023/07/bce_dice.png)
_Comparison of the BCE loss, DICE loss and the average of the two._

![](https://dataroots.ghost.io/content/images/2023/07/image-9.png)

## Optimize Inference

Once inference is made, a quick win could be to handle the predictions our model is making. Several techniques were tried using knowledge  in order to get better and more robust results.

**1. Test Time Augmentation (TTA)**

TTA applies different transformations to the test data, to feed these transformations in the trained model and then aggregate (usually by averaging) the results to get a final prediction. Providing the model multiple perspectives on the same image and averaging the prediction will make the inference more robust and reliable. The TTA performed here rotated the input images 4 times by 90°, making inference time significantly longer, but improved the LB by more than 0.1 in most cases.

**2. L1 /  Hession Denoising of the predictions**

This idea proposed by Brett Olsen during the competition ([Improving performance with L1/Hessian denoising](https://www.kaggle.com/code/brettolsen/improving-performance-with-l1-hessian-denoising?ref=dataroots.ghost.io))

The approach here is to make use of known properties of the ink distribution:

- The **ink is sparse** , most regions will not contain ink. We can add a **L1 regularization term** to the predictions to penalize noisy data.
- The **ink** is continuous, a single pixel is more likely to contain ink when it is next to another pixel containing ink. A **Hessian matrix term** is used to penalize strongly variable values of ink/no-ink.

Adding denoising to the output increased inference time by a bit, while raising the LB score by an mostly insignificant amount

<!--kg-card-begin: html-->

| TTA | Denoising | Inference Time | Public test F0.5 | Private test F0.5 |
| --- | --- | --- | --- | --- |
| yes | no | 1h27 | 0.57 | 0.51 |
| no | no | 19 min | 0.44 | 0.35 |
| yes | yes | 1h32 | TODO | TODO |

<!--kg-card-end: html-->
## Final Solution ([kaggle notebook](https://www.kaggle.com/code/thenoodleninja/resnet18d-training?ref=dataroots.ghost.io))

Our final solution consists of a UNet with a 3D Resnet as backbone, trained for 32 epochs, with a combination of BCE and DICE loss, on fragments 2 and 3 (the final model was not retrained on the complete train set due to running out of GPU time). While this was not the model with the highest F0.5 on the validation set, it is the one we trust to generalize the most, which is especially important with the hidden test set. At inference time, test time augmentation is used (inference is performed on images rotated 0, 90, 180 and 270 degrees and the average of these is used as the final prediction) and the prediction masks are denoised in order to remove any small blobs that are extremely unlikely to be ink. The final validation and test scores can be found below together with the final prediction for the validation set.

<!--kg-card-begin: html-->

| threshold | validation F0.5 | public test F0.5 | private test F0.5 |
| --- | --- | --- | --- |
| 0.55 | 0.58 | 0.57 | 0.50 |

<!--kg-card-end: html-->

![](https://dataroots.ghost.io/content/images/2023/07/output.png)
_Predicted mask, thresholded mask and ground truth of fragment 1 (validation set)._

## Other ideas that could have improved performance

While not currently present in our final solution, there are still some things that could be done to improve the result. In this section you can find a list of things that would most likely improve performance/ be interesting to look at/ …

### Ensemble

A not very interesting but very reliable way to improve performance is to just take a bunch of your best performing models and instead of just using the best, use all of them and aggregate the predictions (usually by taking the mean) to end up with a more robust solution. If you want to squeeze the most out of your existing work this is definitely the way to go.

### Thresholding strategy

In our current solution the threshold used was decided by finding the threshold that yielded the best F0.5 for the validation set. This does requires a validation set to decide this optimal threshold and, depending on the variance of the model, can lead to overfitting to this validation set. While some models with a lower variance were calibrated slightly better and were much more robust to a slight change in threshold, these often had a lower optimal F0.5 score. As such, a reliable thresholding strategy could lead to a decent increase in performance.

### Overlap-tile strategy

Some artefacts are clearly visible at the edges of the crops. To prevent this ideally one would only use the centers of the predicted patches this is also mentioned in the original [Unet paper](https://arxiv.org/pdf/1505.04597v1.pdf?ref=dataroots.ghost.io) where they use it for “seamless” segmentation.

### Transformer based segmentation approach

Recently, transformers have been revolutionizing a variety of ML fields and this includes semantic segmentation. Here, models such as [SegFormer](https://arxiv.org/pdf/2105.15203.pdf?ref=dataroots.ghost.io), [Swin Transformer](https://arxiv.org/pdf/2103.14030.pdf?ref=dataroots.ghost.io) and [Mask2Former](https://github.com/facebookresearch/Mask2Former?ref=dataroots.ghost.io) for 2D images and [UNETR](https://arxiv.org/pdf/2103.10504.pdf?ref=dataroots.ghost.io) in 3D have been dethroning the classical CNN based approaches for quite some time now. It would have been interesting to see how they perform here.

## Conclusion

In conclusion, we highlighted in this blog post our approach for the [Ink Detection Progress Prize](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/overview?ref=dataroots.ghost.io) launched on Kaggle. The aim was to further explore this unusual challenge and show insights on questions regarding the models performances on this type of 3D data, if effective preprocessing can be done …

Our approach was data-centric. We prioritized performing a strong exploratory data analysis (EDA) to understand the challenges before training numerous models. These came mainly from the **3D shape of the data** , X-ray scans of 65 layers made every operation much more memory-hungry, and the **small dataset size** , only 3 fragments. Fragment flattening was tried to simplify the problem and, while looking promising at first, did not do much in the end. Instead working with a 3D model that was able to extract as much information from the different layers proved to be the approach that yielded the best results. However this approach needed much more regularization to work properly. To limit overfitting; data augmentation, especially channel dropout and training using a combination of the BCE and DICE losses increased the F0.5 score considerably and led to a more robust model.

Since we only started 2 weeks before the deadline, had access to limited GPU time (30h/week/person) and infrastructure (15 GB of GPU memory) we were prevented from training and tuning all the models we wanted so we limited ourselves to exploring a limited selection in depth. However, the U-net architecture showed, as expected, to be very performant for pixel-level segmentation. The Resnet18 backbone with 3D convolutions has already shown decent results compared to the top of the leaderboard of the competition, where much of the remaining performance gap lies in training deeper models on larger crops (with proper regularization) and selecting those to generate a final ensemble. Nonetheless we would definitely recommend the discussions on Kaggle of the competition winners as literature.

1. [ryches](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/discussion/417496?ref=dataroots.ghost.io)
2. [RTX23090](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/discussion/417255?ref=dataroots.ghost.io)
3. [wuyu](https://www.kaggle.com/competitions/vesuvius-challenge-ink-detection/discussion/417536?ref=dataroots.ghost.io)

We hope you gained useful insights about this challenge reading our blogpost. You can find all our code explained in a comprehensible way in notebooks on our [Github repo](https://github.com/datarootsio/vesuvius-ink-detection?ref=dataroots.ghost.io). Do not hesitate to take a look and try to improve the results yourself, all is explained there!

[

GitHub - datarootsio/vesuvius-ink-detection

Contribute to datarootsio/vesuvius-ink-detection development by creating an account on GitHub.

 ![](https://github.com/fluidicon.png)GitHubdatarootsio

 ![](https://opengraph.githubassets.com/ce05249248548a2b6157c01d9fb24c4cadfba2c5d209ab1bc45322eaeed33ca9/datarootsio/vesuvius-ink-detection)
](https://github.com/datarootsio/vesuvius-ink-detection?ref=dataroots.ghost.io)

Lastly, we participated in this competition and wrote the blogpost as a team with our Dataroots colleagues. We would like to express our gratitude to _Cynthia Laureys_ and _Vitale Sparacello_ for their guidance, and to _Zoë Van Noppen_ and _Omar Safwat_ for their contribution to the ideation and the launch of this initiative.