-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
7 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Specify a base image depending on the project. | ||
FROM bitnami/python:3.8 | ||
# For more complex examples, might need to use a different base image. | ||
# FROM pytorch/pytorch:1.9.1-cuda11.1-cudnn8-runtime | ||
|
||
WORKDIR /app | ||
|
||
ENV HTTP_PORT=4000 | ||
|
||
RUN apt-get update \ | ||
&& apt-get -y install gcc | ||
|
||
COPY ./requirements.txt ./ | ||
RUN python -m pip install -U pip \ | ||
&& python -m pip install -r requirements.txt | ||
|
||
COPY . ./ | ||
|
||
# This is needed for Singularity builds. | ||
EXPOSE $HTTP_PORT | ||
|
||
# The entrypoint for a container, | ||
CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:4000", "--pythonpath", ".", "model_server:app"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Submission | ||
TODO: Add a description of the submission process here. | ||
|
||
|
||
|
||
## Launching the submission container | ||
TODO: Create a docker-compose file | ||
```bash | ||
cd ./http_submission | ||
docker build -t sample_pysaliency . | ||
``` | ||
|
||
```bash | ||
docker run --name sample_pysaliency -dp 4000:4000 sample_pysaliency | ||
``` | ||
The above command will launch a container named `sample_pysaliency` and expose the port `4000` to the host machine. The container will be running in the background. | ||
|
||
To test the model server, run the sample_evaluation script (Make sure to have the `pysaliency` package installed): | ||
```bash | ||
python ./http_evaluation/sample_evaluation.py | ||
``` | ||
|
||
|
||
To delete the container, run the following command: | ||
```bash | ||
docker stop sample_pysaliency && docker rm sample_pysaliency | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from flask import Flask, request, jsonify | ||
import numpy as np | ||
import json | ||
from PIL import Image | ||
from io import BytesIO | ||
# import pickle | ||
|
||
# Import your model here | ||
from sample_submission import MySimpleScanpathModel | ||
|
||
app = Flask("saliency-model-server") | ||
app.logger.setLevel("DEBUG") | ||
|
||
# # TODO - replace this with your model | ||
model = MySimpleScanpathModel() | ||
|
||
|
||
@app.route('/conditional_log_density', methods=['POST']) | ||
def conditional_log_density(): | ||
data = json.loads(request.form['json_data']) | ||
x_hist = np.array(data['x_hist']) | ||
y_hist = np.array(data['y_hist']) | ||
t_hist = np.array(data['t_hist']) | ||
attributes = data.get('attributes', {}) | ||
|
||
image_bytes = request.files['stimulus'].read() | ||
image = Image.open(BytesIO(image_bytes)) | ||
stimulus = np.array(image) | ||
|
||
log_density = model.conditional_log_density(stimulus, x_hist, y_hist, t_hist, attributes) | ||
return jsonify({'log_density': log_density.tolist()}) | ||
|
||
|
||
@app.route('/type', methods=['GET']) | ||
def type(): | ||
type = "ScanpathModel" | ||
version = "v1.0.0" | ||
return jsonify({'type': type, 'version': version}) | ||
|
||
|
||
def main(): | ||
app.run(host="localhost", port="4000", debug="True", threaded=True) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
cython | ||
flask | ||
gunicorn | ||
numpy | ||
|
||
# Add additional dependencies here |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import numpy as np | ||
import sys | ||
from sample_submission import MySimpleScanpathModel | ||
from pysaliency.http_models import HTTPScanpathModel | ||
sys.path.insert(0, '..') | ||
import pysaliency | ||
|
||
|
||
if __name__ == "__main__": | ||
http_model = HTTPScanpathModel("http://localhost:4000") | ||
http_model.check_type() | ||
|
||
# for testing | ||
model = MySimpleScanpathModel() | ||
|
||
# get MIT1003 dataset | ||
stimuli, fixations = pysaliency.get_mit1003(location='pysaliency_datasets') | ||
|
||
eval_fixations = fixations[fixations.scanpath_history_length > 0] | ||
|
||
for fixation_index in range(10): | ||
# get server response for one stimulus | ||
server_density = http_model.conditional_log_density( | ||
stimulus=stimuli.stimuli[eval_fixations.n[fixation_index]], | ||
x_hist=eval_fixations.x_hist[fixation_index], | ||
y_hist=eval_fixations.y_hist[fixation_index], | ||
t_hist=eval_fixations.t_hist[fixation_index] | ||
) | ||
# get model response | ||
model_density = model.conditional_log_density( | ||
stimulus=stimuli.stimuli[eval_fixations.n[fixation_index]], | ||
x_hist=eval_fixations.x_hist[fixation_index], | ||
y_hist=eval_fixations.y_hist[fixation_index], | ||
t_hist=eval_fixations.t_hist[fixation_index] | ||
) | ||
|
||
# Testing | ||
test = np.testing.assert_allclose(server_density, model_density) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import numpy as np | ||
import sys | ||
from typing import Union | ||
from scipy.ndimage import gaussian_filter | ||
sys.path.insert(0, '..') | ||
import pysaliency | ||
|
||
|
||
class LocalContrastModel(pysaliency.Model): | ||
def __init__(self, bandwidth=0.05, **kwargs): | ||
super().__init__(**kwargs) | ||
self.bandwidth = bandwidth | ||
|
||
def _log_density(self, stimulus: Union[pysaliency.datasets.Stimulus, np.ndarray]): | ||
|
||
# _log_density can either take pysaliency Stimulus objects, or, for convenience, simply numpy arrays | ||
# `as_stimulus` ensures that we have a Stimulus object | ||
stimulus_object = pysaliency.datasets.as_stimulus(stimulus) | ||
|
||
# grayscale image | ||
gray_stimulus = np.mean(stimulus_object.stimulus_data, axis=2) | ||
|
||
# size contains the height and width of the image, but not potential color channels | ||
height, width = stimulus_object.size | ||
|
||
# define kernel size based on image size | ||
kernel_size = np.round(self.bandwidth * max(width, height)).astype(int) | ||
sigma = (kernel_size - 1) / 6 | ||
|
||
# apply Gausian blur and calculate squared difference between blurred and original image | ||
blurred_stimulus = gaussian_filter(gray_stimulus, sigma) | ||
|
||
prediction = gaussian_filter((gray_stimulus - blurred_stimulus)**2, sigma) | ||
|
||
# normalize to [1, 255] | ||
prediction = (254 * (prediction / prediction.max())).astype(int) + 1 | ||
|
||
density = prediction / prediction.sum() | ||
|
||
return np.log(density) | ||
|
||
class MySimpleScanpathModel(pysaliency.ScanpathModel): | ||
def __init__(self, spatial_model_bandwidth: float=0.05, saccade_width: float=0.1): | ||
self.spatial_model_bandwidth = spatial_model_bandwidth | ||
self.saccade_width = saccade_width | ||
self.spatial_model = LocalContrastModel(spatial_model_bandwidth) | ||
# self.spatial_model = pysaliency.UniformModel() | ||
|
||
|
||
def conditional_log_density(self, stimulus, x_hist, y_hist, t_hist, attributes=None, out=None,): | ||
stimulus_object = pysaliency.datasets.as_stimulus(stimulus) | ||
|
||
# size contains the height and width of the image, but not potential color channels | ||
height, width = stimulus_object.size | ||
|
||
spatial_prior_log_density = self.spatial_model.log_density(stimulus) | ||
spatial_prior_density = np.exp(spatial_prior_log_density) | ||
|
||
# compute saccade bias | ||
last_x = x_hist[-1] | ||
last_y = y_hist[-1] | ||
|
||
xs = np.arange(width, dtype=float) | ||
ys = np.arange(height, dtype=float) | ||
XS, YS = np.meshgrid(xs, ys) | ||
|
||
XS -= last_x | ||
YS -= last_y | ||
|
||
# compute prior | ||
max_size = max(width, height) | ||
actual_kernel_size = self.saccade_width * max_size | ||
|
||
saccade_bias = np.exp(-0.5 * (XS ** 2 + YS ** 2) / actual_kernel_size ** 2) | ||
|
||
prediction = spatial_prior_density * saccade_bias | ||
|
||
density = prediction / prediction.sum() | ||
return np.log(density) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from .models import ScanpathModel | ||
from PIL import Image | ||
from io import BytesIO | ||
import requests | ||
import json | ||
import numpy as np | ||
|
||
class HTTPScanpathModel(ScanpathModel): | ||
def __init__(self, url): | ||
self.url = url | ||
self.check_type() | ||
|
||
@property | ||
def log_density_url(self): | ||
return self.url + "/conditional_log_density" | ||
|
||
@property | ||
def type_url(self): | ||
return self.url + "/type" | ||
|
||
def conditional_log_density(self, stimulus, x_hist, y_hist, t_hist, attributes=None, out=None): | ||
# build request | ||
pil_image = Image.fromarray(stimulus) | ||
image_bytes = BytesIO() | ||
pil_image.save(image_bytes, format='png') | ||
|
||
def _convert_attribute(attribute): | ||
if isinstance(attribute, np.ndarray): | ||
return attribute.tolist() | ||
return attribute | ||
|
||
json_data = { | ||
"x_hist": list(x_hist), | ||
"y_hist": list(y_hist), | ||
"t_hist": list(t_hist), | ||
"attributes": {key: _convert_attribute(value) for key, value in (attributes or {}).items()} | ||
} | ||
|
||
# send request | ||
response = requests.post(f"{self.log_density_url}", data={'json_data': json.dumps(json_data)}, files={'stimulus': image_bytes.getvalue()}) | ||
|
||
# parse response | ||
if response.status_code != 200: | ||
raise ValueError(f"Server returned status code {response.status_code}") | ||
|
||
return np.array(response.json()['log_density']) | ||
|
||
def check_type(self): | ||
response = requests.get(f"{self.type_url}").json() | ||
if not response['type'] == 'ScanpathModel': | ||
raise ValueError(f"invalid Model type: {response['type']}. Expected 'ScanpathModel'") | ||
if not response['version'] in ['v1.0.0']: | ||
raise ValueError(f"invalid Model type: {response['version']}. Expected 'v1.0.0'") |