Implement a SYCL-accelerated image denoising algorithm using bilateral filtering. The algorithm takes a grey-scale image as input and applies a bilateral filter to reduce noise while preserving edges. The algorithm is similar to a blur filter (which we did in the labs), but the computation is slightly different. Like in the blur filter, the new value of each pixel is computed from the values of its neighbors defined by a square window of size `2 * radius + 1` centered on the pixel in question.

The filter iterates over the window and computes a weighted average of the neighboring pixels. The new value is then computed simply as

```c++
float newPixelValue = weightedSum / weights;
```

where `weightedSum` is the sum of the neighboring pixel values multiplied by their corresponding weights, and `weights` is the sum of all weights (necessary for normalization). The only difference from simple blur filters is the way the weights are computed:

```c++
// these values can be precomputed as they are fixed for the whole image (gSpatial needs to be computed for all possible dx, dy combinations)
float distance = (float)(dx * dx + dy * dy); // dx, dy is relative position of the neighbor pixel in the window
float gSpatial = std::exp(-distance / (2.0f * spatialSigma * spatialSigma));
float rangeDenominator = 2.0f * rangeSigma * rangeSigma;

// individually for every centralPixel and neighborPixel in the window:
float diff = centralPixel - neighborPixel; // difference in the pixel values (both in [0.0, 1.0] range)
float gRange = std::exp(-(diff * diff) / rangeDenominator);
float weight = gSpatial * gRange;

weightedSum += neighborPixel * weight;
weights += weight;
```

The `spatialSigma`, `rangeSigma`, and `radius` are filtering parameters that are passed to the algorithm. The `dx`, `dy` values are from the range `[-radius, radius]`, and they need to be bounded to the image dimensions (i.e., if the neighbor pixel is outside the image, it should be ignored). These equations are already implemented in the serial version of the algorithm; you may copy-paste the appropriate parts and refactor them for SYCL.

### Technical details

The starter pack with the serial implementation and makefile is available to you on the `hpc.troja.mff.cuni.cz` server in `/home/teaching/NPRG042/labtest`. The performance will be tested on the `volta02` machine, using two V100 GPUs (same as in the SYCL home assignment).

The reference serial implementation can be found in the `serial.hpp` file (the `operator()` of the `SerialImplementation` class). You need to implement the SYCL version in the `sycl.hpp` file (the interface of the `SyclImplementation` class must be preserved), and the `sycl.hpp` file **is the only file you should submit to ReCodEx**.

The images are available in the `/home/teaching/NPRG042/labtest/data` directory. The `*-out.pbm` files are the expected outputs used for verification. All three images are being tested in ReCodEx and will be measured for performance.

### Evaluation

ReCodEx only evaluates the correctness of the implementation. You need to test the performance on the `volta02` worker yourselves. The default values of the `spatialSigma` and `rangeSigma` parameters are used in testing, and the `radius` parameter is derived from `spatialSigma`. The grading table is as follows:

-   50× or less: **0 points**
-   50× to 100×: **1 point**
-   100× to 150×: **2 points**
-   150× to 200×: **3 points**
-   200× or more: **4 points**

You may use the associated `test.sh` script (this is a regular bash script, run it directly on the head server) to test the correctness and performance of your implementation (it expects one argument -- the bare name of the input image, e.g., `./test.sh beaver`). It will execute both the serial and SYCL implementations, compare their outputs, and measure their execution times to determine the speedup.

### Hints

Please note that both the `Image` class and SYCL multi-dimensional buffers use row-major order. **However, they differ in the order in which the dimension coordinates are specified.** In the case of our `Image`, we usually use `x, y` (i.e., column, row) order, while the SYCL buffers have the row first, the column second. I.e., when creating a SYCL 2D buffer, its size must be specified as `sycl::range<2>{height, width}`. Similarly, when accessing the buffer using `sycl::id<2>`, the first dimension corresponds to the row (y), and the second dimension corresponds to the column (x).

If you are not comfortable with SYCL multi-dimensional buffers, you can also use 1D buffers and calculate the corresponding 1D index from the 2D coordinates yourselves (and vice versa).