From cae6e6ef799bf250d3f24dda80b6e1dbef1a7ebe Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Mon, 30 Oct 2023 19:20:29 +0100 Subject: [PATCH 01/25] adding `self,` to `def compute_pdq(self, iobytes: io.BytesIO) -> str:` --- lib/model/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model/image.py b/lib/model/image.py index 41cdf4c..250f51f 100644 --- a/lib/model/image.py +++ b/lib/model/image.py @@ -8,7 +8,7 @@ from lib import schemas class Model(Model): - def compute_pdq(iobytes: io.BytesIO) -> str: + def compute_pdq(self, iobytes: io.BytesIO) -> str: """Compute perceptual hash using ImageHash library :param im: Numpy.ndarray :returns: Imagehash.ImageHash From 2dad0b6fde03f114d58c2407f0886ed84243bda9 Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Mon, 30 Oct 2023 21:49:31 +0100 Subject: [PATCH 02/25] create `image_sscd.py` and corresponding changes in Dockerfile --- Dockerfile | 5 +++ lib/model/image_sscd.py | 70 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 lib/model/image_sscd.py diff --git a/Dockerfile b/Dockerfile index 9d27bf9..d3247a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,11 @@ EXPOSE ${PRESTO_PORT} WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive +RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git +RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt --extra-index-url https://download.pytorch.org/whl/cu113 +RUN mkdir models_files +RUN cd sscd-copy-detection && wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt + RUN apt-get update && apt-get install -y ffmpeg cmake swig libavcodec-dev libavformat-dev git RUN ln -s /usr/bin/ffmpeg /usr/local/bin/ffmpeg diff --git a/lib/model/image_sscd.py b/lib/model/image_sscd.py new file mode 100644 index 0000000..5f2fa6c --- /dev/null +++ b/lib/model/image_sscd.py @@ -0,0 +1,70 @@ +from typing import Dict +import io +import urllib.request + +from lib.model.model import Model + +from pdqhashing.hasher.pdq_hasher import PDQHasher +from lib import schemas +from torchvision import transforms +from PIL import Image +import torch +from lib.logger import logger +import requests +import numpy as np + +class Model(Model): + def compute_sscd(self, image_url: str) -> str: + """Compute perceptual hash using ImageHash library + :param im: Numpy.ndarray + :returns: Imagehash.ImageHash + """ + # pdq_hasher = PDQHasher() + # hash_and_qual = pdq_hasher.fromBufferedImage(iobytes) + # return hash_and_qual.getHash().dumpBitsFlat() + normalize = transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], + ) + small_288 = transforms.Compose([ + transforms.Resize(288), + transforms.ToTensor(), + normalize, + ]) + skew_320 = transforms.Compose([ + transforms.Resize([320, 320]), + transforms.ToTensor(), + normalize, + ]) + + model = torch.jit.load("sscd_disc_mixup.torchscript.pt") + # img = Image.open(image_file_path).convert('RGB') + + response = requests.get(image_url) + img = Image.open(io.BytesIO(response.content)) + # img = Image.open(image.body.url).convert('RGB') + + batch = small_288(img).unsqueeze(0) + embedding = model(batch)[0, :] + return np.asarray(embedding.detach().numpy()).tolist() + + def get_iobytes_for_image(self, image: schemas.Message) -> io.BytesIO: + """ + Read file as bytes after requesting based on URL. + """ + return io.BytesIO( + urllib.request.urlopen( + urllib.request.Request( + image.body.url, + headers={'User-Agent': 'Mozilla/5.0'} + ) + ).read() + ) + + def process(self, image: schemas.Message) -> schemas.ImageOutput: + """ + Generic function for returning the actual response. + """ + + # get_image_embeddings("example-image-airplane1.png", + # "/content/sscd-copy-detection/models_files/sscd_disc_mixup.torchscript.pt") + return {"embeddings": self.compute_sscd(image.body.url)} From 3c3fc9f5aef3c9299de5b3b54ce97b3f638072a7 Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Wed, 8 Nov 2023 21:20:16 +0100 Subject: [PATCH 03/25] removing `--extra-index-url https://download.pytorch.org/whl/cu113` from sscd-copy-detection installation --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d3247a3..8931d1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git -RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt --extra-index-url https://download.pytorch.org/whl/cu113 +RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt RUN mkdir models_files RUN cd sscd-copy-detection && wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt From 8224cfba766ec48299fc6300e6d0547711a9c6bc Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Wed, 8 Nov 2023 23:03:01 +0100 Subject: [PATCH 04/25] installing sscd requirements directly in Dockerfile --- Dockerfile | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8931d1a..dcde342 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,9 +9,20 @@ WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git -RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt -RUN mkdir models_files -RUN cd sscd-copy-detection && wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt +#RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt +RUN pip install pytorch-lightning==1.6 +RUN pip install lightning-bolts==0.4.0 +RUN pip install classy_vision +RUN pip install torch +RUN pip install torchvision +RUN pip install torchmetrics +RUN pip install faiss-gpu +RUN pip install augly +RUN pip install pandas +RUN pip install numpy +RUN pip install tensorboard +#RUN mkdir models_files +#RUN cd sscd-copy-detection && wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt RUN apt-get update && apt-get install -y ffmpeg cmake swig libavcodec-dev libavformat-dev git RUN ln -s /usr/bin/ffmpeg /usr/local/bin/ffmpeg From b22b803c2932a06363abf8f4be3f39cf3193a86c Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Thu, 9 Nov 2023 00:25:33 +0100 Subject: [PATCH 05/25] changing `pytorch-lightning` version to `1.5.10` --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index dcde342..411718c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git #RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt -RUN pip install pytorch-lightning==1.6 +RUN pip install pytorch-lightning==1.5.10 RUN pip install lightning-bolts==0.4.0 RUN pip install classy_vision RUN pip install torch From 179763702d2fbeb16865d4979f90ae68e61f34ae Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Thu, 9 Nov 2023 00:48:16 +0100 Subject: [PATCH 06/25] trying to build with sscd installation commented in dockerfile --- Dockerfile | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 411718c..42f412a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,19 +8,19 @@ EXPOSE ${PRESTO_PORT} WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive -RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git -#RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt -RUN pip install pytorch-lightning==1.5.10 -RUN pip install lightning-bolts==0.4.0 -RUN pip install classy_vision -RUN pip install torch -RUN pip install torchvision -RUN pip install torchmetrics -RUN pip install faiss-gpu -RUN pip install augly -RUN pip install pandas -RUN pip install numpy -RUN pip install tensorboard +#RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git +##RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt +#RUN pip install pytorch-lightning==1.5.10 +#RUN pip install lightning-bolts==0.4.0 +#RUN pip install classy_vision +#RUN pip install torch +#RUN pip install torchvision +#RUN pip install torchmetrics +#RUN pip install faiss-gpu +#RUN pip install augly +#RUN pip install pandas +#RUN pip install numpy +#RUN pip install tensorboard #RUN mkdir models_files #RUN cd sscd-copy-detection && wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt From 295e0b8c6e7106ac29298a3a85cb58180d18ea1e Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Thu, 9 Nov 2023 20:06:14 +0100 Subject: [PATCH 07/25] Changing `Model.compute_pdq(io.BytesIO(image_content))` to `result = Model().compute_pdq(io.BytesIO(image_content))` in test_image.py --- test/lib/model/test_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/model/test_image.py b/test/lib/model/test_image.py index 54ddb7e..0cbd2b9 100644 --- a/test/lib/model/test_image.py +++ b/test/lib/model/test_image.py @@ -15,7 +15,7 @@ def test_compute_pdq(self, mock_pdq_hasher): image_content = file.read() mock_hasher_instance = mock_pdq_hasher.return_value mock_hasher_instance.fromBufferedImage.return_value.getHash.return_value.dumpBitsFlat.return_value = '1001' - result = Model.compute_pdq(io.BytesIO(image_content)) + result = Model().compute_pdq(io.BytesIO(image_content)) self.assertEqual(result, '0011100000111011010110100001001110001011110100100010101011010111010110101010000111001010111000001010111111110000000101110010000011111110111110100100011111010010110110101111101100111001000000010010100101010111110001001101101011000110001000001110010000111100') @patch("urllib.request.urlopen") From 40e5bcf05ecd662c707d793d0aa43583cc0dec5f Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Thu, 9 Nov 2023 20:19:51 +0100 Subject: [PATCH 08/25] uncommenting lines to install sscd-copy-detection in Dockerfile --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 42f412a..7781603 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,8 @@ EXPOSE ${PRESTO_PORT} WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive -#RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git -##RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt +RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git +RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt #RUN pip install pytorch-lightning==1.5.10 #RUN pip install lightning-bolts==0.4.0 #RUN pip install classy_vision @@ -22,7 +22,7 @@ ENV DEBIAN_FRONTEND=noninteractive #RUN pip install numpy #RUN pip install tensorboard #RUN mkdir models_files -#RUN cd sscd-copy-detection && wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt +RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt RUN apt-get update && apt-get install -y ffmpeg cmake swig libavcodec-dev libavformat-dev git RUN ln -s /usr/bin/ffmpeg /usr/local/bin/ffmpeg From bcc77e3bd09ae947ea7711976710de49d37f25c4 Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Fri, 10 Nov 2023 02:04:43 +0100 Subject: [PATCH 09/25] installing sscd requirements directly in dockerfile instead from requirment.txt --- Dockerfile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7781603..ed76c0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,20 +9,20 @@ WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git -RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt -#RUN pip install pytorch-lightning==1.5.10 -#RUN pip install lightning-bolts==0.4.0 -#RUN pip install classy_vision -#RUN pip install torch -#RUN pip install torchvision -#RUN pip install torchmetrics -#RUN pip install faiss-gpu -#RUN pip install augly -#RUN pip install pandas -#RUN pip install numpy -#RUN pip install tensorboard -#RUN mkdir models_files -RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt +#RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt +RUN pip install pytorch-lightning==1.5.10 +RUN pip install lightning-bolts==0.4.0 +RUN pip install classy_vision +RUN pip install torch +RUN pip install torchvision +RUN pip install torchmetrics +RUN pip install faiss-gpu +RUN pip install augly +RUN pip install pandas +RUN pip install numpy +RUN pip install tensorboard +RUN mkdir models_files +#RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt RUN apt-get update && apt-get install -y ffmpeg cmake swig libavcodec-dev libavformat-dev git RUN ln -s /usr/bin/ffmpeg /usr/local/bin/ffmpeg From c4bb74648e57aff2648fbbc307c9c3e6bfef8b61 Mon Sep 17 00:00:00 2001 From: Scott Hale Date: Fri, 10 Nov 2023 17:21:47 +0000 Subject: [PATCH 10/25] Comment git clone line - Update Dockerfile --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ed76c0d..52b6f14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ EXPOSE ${PRESTO_PORT} WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive -RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git +#RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git #RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt RUN pip install pytorch-lightning==1.5.10 RUN pip install lightning-bolts==0.4.0 @@ -47,4 +47,4 @@ RUN pip install pact-python RUN pip install --no-cache-dir -r requirements.txt RUN cd threatexchange/pdq/python && pip install . COPY . . -CMD ["make", "run"] \ No newline at end of file +CMD ["make", "run"] From 1350858c02e28df92ed85a36da50a5758a41fa99 Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Fri, 10 Nov 2023 20:00:22 +0000 Subject: [PATCH 11/25] Move packages to requirements.txt, remove possibly unneeded ones --- Dockerfile | 20 ++++---------------- requirements.txt | 10 +++++++++- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 52b6f14..63b3e21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,22 +8,6 @@ EXPOSE ${PRESTO_PORT} WORKDIR /app ENV DEBIAN_FRONTEND=noninteractive -#RUN git clone https://github.com/facebookresearch/sscd-copy-detection.git -#RUN cd sscd-copy-detection && python -m pip install -r ./requirements.txt -RUN pip install pytorch-lightning==1.5.10 -RUN pip install lightning-bolts==0.4.0 -RUN pip install classy_vision -RUN pip install torch -RUN pip install torchvision -RUN pip install torchmetrics -RUN pip install faiss-gpu -RUN pip install augly -RUN pip install pandas -RUN pip install numpy -RUN pip install tensorboard -RUN mkdir models_files -#RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt - RUN apt-get update && apt-get install -y ffmpeg cmake swig libavcodec-dev libavformat-dev git RUN ln -s /usr/bin/ffmpeg /usr/local/bin/ffmpeg @@ -46,5 +30,9 @@ RUN pip install transformers RUN pip install pact-python RUN pip install --no-cache-dir -r requirements.txt RUN cd threatexchange/pdq/python && pip install . + +# RUN mkdir models_files +# RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt + COPY . . CMD ["make", "run"] diff --git a/requirements.txt b/requirements.txt index a95670b..7055f27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,12 @@ fasttext==0.9.2 langcodes==3.3.0 requests==2.31.0 pytest==7.4.0 -sentry-sdk==1.30.0 \ No newline at end of file +sentry-sdk==1.30.0 +pytorch-lightning==1.5.10 +lightning-bolts==0.4.0 +classy_vision +torch +torchvision +torchmetrics +augly +tensorboard From 40498282350e1ad58496edfe6ba56e72f3300a8b Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Fri, 10 Nov 2023 20:29:34 +0000 Subject: [PATCH 12/25] add back model download --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 63b3e21..ac66fed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,8 +31,8 @@ RUN pip install pact-python RUN pip install --no-cache-dir -r requirements.txt RUN cd threatexchange/pdq/python && pip install . -# RUN mkdir models_files -# RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt +RUN mkdir models_files +RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt COPY . . CMD ["make", "run"] From fef66c44b686949702b3bbcec4b5368343fb99f6 Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 15:16:45 +0000 Subject: [PATCH 13/25] Remove possibly unused requirements --- requirements.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7055f27..7afdb62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,9 +15,5 @@ pytest==7.4.0 sentry-sdk==1.30.0 pytorch-lightning==1.5.10 lightning-bolts==0.4.0 -classy_vision torch torchvision -torchmetrics -augly -tensorboard From 5372d570a96d47a4400f32264cadbb9082861e7c Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 16:21:37 +0000 Subject: [PATCH 14/25] Large refactor * Create GenericImageModel and have pdq and sscd inherit form it * Move model loading for sscd to __init__ * Update requirements.txt for sscd --- .env_file | 2 + Dockerfile | 3 +- lib/model/image.py | 37 ------------ lib/model/image_sscd.py | 58 +++++-------------- lib/queue/worker.py | 10 ++-- requirements.txt | 4 +- .../{test_image.py => test_image_pdq.py} | 0 7 files changed, 26 insertions(+), 88 deletions(-) delete mode 100644 lib/model/image.py rename test/lib/model/{test_image.py => test_image_pdq.py} (100%) diff --git a/.env_file b/.env_file index 1b880ab..9976955 100644 --- a/.env_file +++ b/.env_file @@ -3,5 +3,7 @@ PRESTO_PORT=8000 DEPLOY_ENV=local # MODEL_NAME=mean_tokens.Model MODEL_NAME=audio.Model +# MODEL_NAME=image_sscd.Model +# MODEL_NAME=image_pdq.Model AWS_ACCESS_KEY_ID=SOMETHING AWS_SECRET_ACCESS_KEY=OTHERTHING diff --git a/Dockerfile b/Dockerfile index ac66fed..70607fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,8 +31,7 @@ RUN pip install pact-python RUN pip install --no-cache-dir -r requirements.txt RUN cd threatexchange/pdq/python && pip install . -RUN mkdir models_files -RUN wget https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt +RUN wget -O "sscd_disc_mixup.torchscript.pt" "https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt" COPY . . CMD ["make", "run"] diff --git a/lib/model/image.py b/lib/model/image.py deleted file mode 100644 index 346e320..0000000 --- a/lib/model/image.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Dict -import io -import urllib.request - -from lib.model.model import Model - -from pdqhashing.hasher.pdq_hasher import PDQHasher -from lib import schemas - -class Model(Model): - def compute_pdq(self, iobytes: io.BytesIO) -> str: - """Compute perceptual hash using ImageHash library - :param im: Numpy.ndarray - :returns: Imagehash.ImageHash - """ - pdq_hasher = PDQHasher() - hash_and_qual = pdq_hasher.fromBufferedImage(iobytes) - return hash_and_qual.getHash().dumpBitsFlat() - - def get_iobytes_for_image(self, image: schemas.Message) -> io.BytesIO: - """ - Read file as bytes after requesting based on URL. - """ - return io.BytesIO( - urllib.request.urlopen( - urllib.request.Request( - image.body.url, - headers={'User-Agent': 'Mozilla/5.0'} - ) - ).read() - ) - - def process(self, image: schemas.Message) -> schemas.GenericItem: - """ - Generic function for returning the actual response. - """ - return self.compute_pdq(self.get_iobytes_for_image(image)) diff --git a/lib/model/image_sscd.py b/lib/model/image_sscd.py index 5f2fa6c..d462c18 100644 --- a/lib/model/image_sscd.py +++ b/lib/model/image_sscd.py @@ -1,27 +1,24 @@ from typing import Dict import io -import urllib.request -from lib.model.model import Model - -from pdqhashing.hasher.pdq_hasher import PDQHasher +from lib.model.generic_image import GenericImageModel from lib import schemas from torchvision import transforms -from PIL import Image import torch from lib.logger import logger -import requests import numpy as np +from PIL import Image + +class Model(GenericImageModel): + def __init__(self): + super().__init__() + self.model = torch.jit.load("sscd_disc_mixup.torchscript.pt") -class Model(Model): - def compute_sscd(self, image_url: str) -> str: + def compute_sscd(self, iobytes: io.BytesIO) -> str: """Compute perceptual hash using ImageHash library - :param im: Numpy.ndarray - :returns: Imagehash.ImageHash + :param im: Numpy.ndarray #FIXME + :returns: Imagehash.ImageHash #FIXME """ - # pdq_hasher = PDQHasher() - # hash_and_qual = pdq_hasher.fromBufferedImage(iobytes) - # return hash_and_qual.getHash().dumpBitsFlat() normalize = transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], ) @@ -36,35 +33,10 @@ def compute_sscd(self, image_url: str) -> str: normalize, ]) - model = torch.jit.load("sscd_disc_mixup.torchscript.pt") - # img = Image.open(image_file_path).convert('RGB') - - response = requests.get(image_url) - img = Image.open(io.BytesIO(response.content)) - # img = Image.open(image.body.url).convert('RGB') - - batch = small_288(img).unsqueeze(0) - embedding = model(batch)[0, :] + image = Image.open(iobytes) + batch = small_288(image).unsqueeze(0) + embedding = self.model(batch)[0, :] return np.asarray(embedding.detach().numpy()).tolist() - def get_iobytes_for_image(self, image: schemas.Message) -> io.BytesIO: - """ - Read file as bytes after requesting based on URL. - """ - return io.BytesIO( - urllib.request.urlopen( - urllib.request.Request( - image.body.url, - headers={'User-Agent': 'Mozilla/5.0'} - ) - ).read() - ) - - def process(self, image: schemas.Message) -> schemas.ImageOutput: - """ - Generic function for returning the actual response. - """ - - # get_image_embeddings("example-image-airplane1.png", - # "/content/sscd-copy-detection/models_files/sscd_disc_mixup.torchscript.pt") - return {"embeddings": self.compute_sscd(image.body.url)} + def compute_imagehash(self, iobytes: io.BytesIO) -> str: + return self.compute_sscd(iobytes) diff --git a/lib/queue/worker.py b/lib/queue/worker.py index b45bbcb..4f6aacf 100644 --- a/lib/queue/worker.py +++ b/lib/queue/worker.py @@ -51,10 +51,12 @@ def safely_respond(self, model: Model) -> List[schemas.Message]: responses = [] if messages_with_queues: logger.debug(f"About to respond to: ({messages_with_queues})") - try: - responses = model.respond([schemas.Message(**{**json.loads(message.body), **{"model_name": model.model_name}}) for message, queue in messages_with_queues]) - except Exception as e: - logger.error(e) + #try: + responses = model.respond([schemas.Message(**{**json.loads(message.body), **{"model_name": model.model_name}}) for message, queue in messages_with_queues]) + logger.info("!!!!") + logger.info(responses) + #except Exception as e: + # logger.error(e) self.delete_messages(messages_with_queues) return responses diff --git a/requirements.txt b/requirements.txt index 7afdb62..730e91b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,5 @@ pytest==7.4.0 sentry-sdk==1.30.0 pytorch-lightning==1.5.10 lightning-bolts==0.4.0 -torch -torchvision +torch==1.9.0 +torchvision==0.10.0 diff --git a/test/lib/model/test_image.py b/test/lib/model/test_image_pdq.py similarity index 100% rename from test/lib/model/test_image.py rename to test/lib/model/test_image_pdq.py From 4713a35050dbd80e7cb26232d3d4abbff637991b Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 16:33:44 +0000 Subject: [PATCH 15/25] Adding missing files --- .gitignore | 1 + lib/model/generic_image.py | 27 +++++++++++++++++++++++++++ lib/model/image_pdq.py | 20 ++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 lib/model/generic_image.py create mode 100644 lib/model/image_pdq.py diff --git a/.gitignore b/.gitignore index d9deb28..edb5959 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.cpython-39.pyc *.pyc +sscd_disc_mixup.torchscript.pt diff --git a/lib/model/generic_image.py b/lib/model/generic_image.py new file mode 100644 index 0000000..9620aa9 --- /dev/null +++ b/lib/model/generic_image.py @@ -0,0 +1,27 @@ +from lib.model.model import Model + +from lib import schemas +import urllib.request +import io + +class GenericImageModel(Model): + + def get_iobytes_for_image(self, image: schemas.Message) -> io.BytesIO: + """ + Read file as bytes after requesting based on URL. + """ + return io.BytesIO( + urllib.request.urlopen( + urllib.request.Request( + image.body.url, + headers={'User-Agent': 'Mozilla/5.0'} + ) + ).read() + ) + + def process(self, image: schemas.Message) -> schemas.GenericItem: + """ + Generic function for returning the actual response. + """ + + return self.compute_imagehash(self.get_iobytes_for_image(image)) \ No newline at end of file diff --git a/lib/model/image_pdq.py b/lib/model/image_pdq.py new file mode 100644 index 0000000..dc5417f --- /dev/null +++ b/lib/model/image_pdq.py @@ -0,0 +1,20 @@ +from typing import Dict +import io + +from lib.model.generic_image import GenericImageModel + +from pdqhashing.hasher.pdq_hasher import PDQHasher +from lib import schemas + +class Model(GenericImageModel): + def compute_pdq(self, iobytes: io.BytesIO) -> str: + """Compute perceptual hash using ImageHash library + :param im: Numpy.ndarray + :returns: Imagehash.ImageHash + """ + pdq_hasher = PDQHasher() + hash_and_qual = pdq_hasher.fromBufferedImage(iobytes) + return hash_and_qual.getHash().dumpBitsFlat() + + def compute_imagehash(self, iobytes: io.BytesIO) -> str: + return self.compute_pdq(iobytes) \ No newline at end of file From 1e9227b738a563c07f639428abba404d4677020e Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 16:40:59 +0000 Subject: [PATCH 16/25] Move SSCD model download to __init__ --- Dockerfile | 2 -- lib/model/image_sscd.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 70607fc..2bc4b1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,5 @@ RUN pip install pact-python RUN pip install --no-cache-dir -r requirements.txt RUN cd threatexchange/pdq/python && pip install . -RUN wget -O "sscd_disc_mixup.torchscript.pt" "https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt" - COPY . . CMD ["make", "run"] diff --git a/lib/model/image_sscd.py b/lib/model/image_sscd.py index d462c18..ba84c46 100644 --- a/lib/model/image_sscd.py +++ b/lib/model/image_sscd.py @@ -8,11 +8,21 @@ from lib.logger import logger import numpy as np from PIL import Image +import urllib.request class Model(GenericImageModel): def __init__(self): super().__init__() - self.model = torch.jit.load("sscd_disc_mixup.torchscript.pt") + #FIXME: Load from a Meedan S3 bucket + try: + self.model = torch.jit.load("sscd_disc_mixup.torchscript.pt") + except: + logging.info("Downloading SSCD model...") + m=urllib.request.urlopen("https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt").read() + with open("sscd_disc_mixup.torchscript.pt","wb") as fh: + fh.write(m) + self.model = torch.jit.load("sscd_disc_mixup.torchscript.pt") + logging.info("SSCD model loaded") def compute_sscd(self, iobytes: io.BytesIO) -> str: """Compute perceptual hash using ImageHash library From d702e40b9a4b675c0309f6e6b8c7fc43cbede5e8 Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 16:41:40 +0000 Subject: [PATCH 17/25] fix typo --- lib/model/image_sscd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model/image_sscd.py b/lib/model/image_sscd.py index ba84c46..47b3966 100644 --- a/lib/model/image_sscd.py +++ b/lib/model/image_sscd.py @@ -17,12 +17,12 @@ def __init__(self): try: self.model = torch.jit.load("sscd_disc_mixup.torchscript.pt") except: - logging.info("Downloading SSCD model...") + logger.info("Downloading SSCD model...") m=urllib.request.urlopen("https://dl.fbaipublicfiles.com/sscd-copy-detection/sscd_disc_mixup.torchscript.pt").read() with open("sscd_disc_mixup.torchscript.pt","wb") as fh: fh.write(m) self.model = torch.jit.load("sscd_disc_mixup.torchscript.pt") - logging.info("SSCD model loaded") + logger.info("SSCD model loaded") def compute_sscd(self, iobytes: io.BytesIO) -> str: """Compute perceptual hash using ImageHash library From 34492864aad692443d75ce5516ceaf7eb08bc65c Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 16:46:01 +0000 Subject: [PATCH 18/25] Revert comments to lib/queue/worker.py --- lib/queue/worker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/queue/worker.py b/lib/queue/worker.py index 4f6aacf..0400f89 100644 --- a/lib/queue/worker.py +++ b/lib/queue/worker.py @@ -51,12 +51,12 @@ def safely_respond(self, model: Model) -> List[schemas.Message]: responses = [] if messages_with_queues: logger.debug(f"About to respond to: ({messages_with_queues})") - #try: - responses = model.respond([schemas.Message(**{**json.loads(message.body), **{"model_name": model.model_name}}) for message, queue in messages_with_queues]) - logger.info("!!!!") - logger.info(responses) - #except Exception as e: - # logger.error(e) + try: + responses = model.respond([schemas.Message(**{**json.loads(message.body), **{"model_name": model.model_name}}) for message, queue in messages_with_queues]) + logger.info("!!!!") + logger.info(responses) + except Exception as e: + logger.error(e) self.delete_messages(messages_with_queues) return responses From 690d606e9fdbffe7e56abc6165ce2c5f9d571fa9 Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Sat, 11 Nov 2023 16:47:03 +0000 Subject: [PATCH 19/25] Revert comments to lib/queue/worker.py --- lib/queue/worker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/queue/worker.py b/lib/queue/worker.py index 0400f89..b45bbcb 100644 --- a/lib/queue/worker.py +++ b/lib/queue/worker.py @@ -53,8 +53,6 @@ def safely_respond(self, model: Model) -> List[schemas.Message]: logger.debug(f"About to respond to: ({messages_with_queues})") try: responses = model.respond([schemas.Message(**{**json.loads(message.body), **{"model_name": model.model_name}}) for message, queue in messages_with_queues]) - logger.info("!!!!") - logger.info(responses) except Exception as e: logger.error(e) self.delete_messages(messages_with_queues) From 2a4e158c0fe5f54812a0af3d3ffe90722cdbe17e Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Tue, 14 Nov 2023 15:31:23 +0000 Subject: [PATCH 20/25] update import in test --- test/lib/model/test_image_pdq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/model/test_image_pdq.py b/test/lib/model/test_image_pdq.py index acd9201..ed51823 100644 --- a/test/lib/model/test_image_pdq.py +++ b/test/lib/model/test_image_pdq.py @@ -4,7 +4,7 @@ from urllib.error import URLError from typing import Dict -from lib.model.image import Model +from lib.model.image_pdq import Model from lib import schemas class TestModel(unittest.TestCase): From a799f219fe6f231949677d702a73e2c491b875cd Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Mon, 20 Nov 2023 12:52:35 +0100 Subject: [PATCH 21/25] changing `from lib.model.image import Model` to `from lib.model.image_pdq import Model` in `test_image_pdq.py` --- test/lib/model/test_image_pdq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lib/model/test_image_pdq.py b/test/lib/model/test_image_pdq.py index acd9201..ed51823 100644 --- a/test/lib/model/test_image_pdq.py +++ b/test/lib/model/test_image_pdq.py @@ -4,7 +4,7 @@ from urllib.error import URLError from typing import Dict -from lib.model.image import Model +from lib.model.image_pdq import Model from lib import schemas class TestModel(unittest.TestCase): From 998d05f577794ecd5ef232b1294239ea265ff1b3 Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Mon, 20 Nov 2023 14:02:34 +0100 Subject: [PATCH 22/25] adding `test_image_sscd.py` and `img/presto_flowchart.jpg` --- img/presto_flowchart.jpg | Bin 0 -> 88249 bytes test/lib/model/test_image_sscd.py | 181 ++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 img/presto_flowchart.jpg create mode 100644 test/lib/model/test_image_sscd.py diff --git a/img/presto_flowchart.jpg b/img/presto_flowchart.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2bf047f84686ca852402896fa32fa168a74f7047 GIT binary patch literal 88249 zcmeFYcUTiqw>}yLL_tB2ju52?iilE`78MZz5fSMnA|OqSh)9Hx2uK$Z5KurOU8F=h z(jpy2q=VEHL}`IUAP%J6@%x?cob#OT-20sSd+z<~-W_%_4>Pl8uRVL#e%HI+;e6*1 zAqUMan_hF4klnk4Aza|rF7N}1@quvvV;cfF z2L3`ITyJ;%$KJQQ|GAf|_AS>xw|9O1dmx7bIeXn3@emQ{jkteAB+uVtH4Dx=MzL|@2<~WwOqU8AiITjaS83>bV8tDk9c7(zF5je^ImfheV*Y4fmt>OX;_a5%Q zWe@M3e-z$-D|`P@_WdpV|6Vv?BfI{d$;HJ3{_*eKv-jWk{f{>|v*5JU=S)HNbL|2r z6PFMK24S<6^Wq`@kJbu#vRdar3~aAH^f8T}+>I|rx}P7(2pcafbusqx*}d~w^Xt3( zWn+|LFbBe;O4Uh7N@J)q^=bI7Ta)|33Vr+vmQ>lpzEU%dLCrJ{q~3c%wk=6y)M;{G zO+Cz18Q1rjt&eFlW!}*s;bYLds9H^7pM!7*=T?GmVd{N++9nlp3Qgp%ENNohNO! z1U=$MP`L&Y3j}vo%}|8b(>*ja-x?5skQ8!;Ja{Q$}8eS!1wufB9U>> zx3%;-+#!4iZ;ZD3Otqtbs!RScjQ7Dwn5L#Vu>?B~DRaFwyq0v&5-&e3s^BWYfkZrC zgz3bf&jN^>J;pS_2*Y6kgKOCLOS7TL+m8yUwOlKwVYZ;XvFKBnwrk9z8_TXzfYkieDLhj;3Y-+u_z*nA&PC4>`d23A=NbMFbM^-l;1AN29)u1>q; z@6IYS4qq?}Z@zNJ`Ty8@?qMREwq}$3*2-7}^8bEC1=CXLbXL0IUSy+XbdEet-p}w% zoKbx=O2;b@x8g0mR+Fy#F!)e=;*@F1SwP_f!&C8X%ConVjK@&sa!JN% zwK7Ld|D2v@<&0J3T5YNay=}yn)YvUZGj&)kL4i%qtlZ4bocv4FoUFU~OY{*mMjgpq zh62~&wNX9&j~UiXqqrD93;DJooM9eZ;3_&h2{p#D&4qyzq{|q6L+}Z{R$r`01u>OG zKADm}@H(Inc@Eg@L`x@a6mTF>aK^)>GLIpcchhwBWKa_Sk~lxWN>P#DKD`b=ZQen_ zWz@5vtYc*!A0EC&4YrY@(ip!}w#kKY-0v zQuoM&%o;M;5SnpC;c@63!?0Q)F6p6zhe1ZbilCs?!{fG*%joPOI3H8@?*wIkX+2A` z$>_j}_)g#jnGWw7;wOw~pp7vVYo4&*-^OslXVsk8%|Em6KPJwvz5iTPuXT;VBVu)B zcf9i3zg3Vk_}@91{7i3ul3zmfKB?Vbzut!&PBC5NftVVjA0Y{BzEFn7(oV}nWb2+c zmh(!nl*1jTutcYzQDU?wu_v%ZucL6PF-P*0n*58vsIyumsb8IVKsmU_xhJ4x5yPb^ ziCK6aUqZGNR+s(x`?tb~KOFj4?8_$CAJ6$WOK$IP0=M1mgsx#b2S%}R9Eez}7)qEL zHqfc|tW|NCsq}V|1Hp|3B{Y=zyAN0lf9-ve^C`z~W5o#^j|@!v<1EEN&hcPZ9a>@KF_k{W>hIa=^1K{au=j_~Gwpmk&s$D7IL_rWk_;=nhBF!8)GmS-%Vp|N!(H^~pQ!0g zR9~VS+SH}lU77<;k*fqOFd?eG0$brPVO4ZP?PlEgm0!>Ecg{KHU z8-@Kfw4B7lmFXt%AO*V)k3Bs09#Y#X)~p1yfITv0r3ain9;0j#WZrK}-7u#%ag!^4 zuQB*w-|#ZEtYRj5>f71FH%imCiq)Er-w8XR*1&4b!$WIe&!N=ogm8u%O|&SIHgYi` zQb8J}M)th0T+mlI_a*H4D{KbrB1-%3fT6$4Y0?f9Khej$n?s$Z`LD#j9c&J~`_TOL z0yurfADdRSB6fa%Y&a~7)cQHrF_0DKUM(&p3W5>RT4+N|BO(^qaeZBI08Cf5h!BSbQB(S$lD; zcVtTpzY9jp!B(I8n2T=5a_>&tBHMIN4POwSVkLg;rNkYx#`;!VOHPo8cnfOP%U00xlEE>pxxM(s=$yV$U-FIuo9y90(4y4BNIc$i{i{c-*g2!$_ zrI&5dvXzExmnL7`*dAwtmyefn8vI;p4~+!Q8ET{Zp!*Efm#+hK_Hop?d6!e_jqqoh zraaF-7SCg6XO;UpUhq#}Q>c4+psI7FD+I?pltF(v2J=IF=Ri8((m{O;)!V;A`~6I? zH%II|a|(}zzjs-(2;058O|DrR>wRhe#gpL|j@)V8J;s4};S<5$iC7>Q?nttMNyiAL zoQjB2{V`nQFbDz$y_GnH^c|L-KH9&f=Dk_py^i>u4)#8kVxJw8)u6zFrPC&UettA* zGU{{(-_z%3N4i6%-oJmwV*|q1Pokfn7m#0O>fR5!nHBG~t%)B#uUEBanUHF2Q2JM&%)2_N0cH+oW&133WTovE|$U6wFSr`kp`gF5|=WMOl3fn}+IY>9~eoC;( znyM%e1Uf5AY9BjBEgla%WO{+7zK4vKzx}%x6nrNf|5wxG09Y1PpK>88`p!OhJmS3zo$8 z`oD2e6S|A*3JCm;{QnpI|J?B6z?dN7)`c=8nggLf0enE9x5G{&sl{FQ*||WIt;OJG zr=~1P2tl~MN^ps@^UN#uNRWQ6)Q$JU+Jn5J(ek{yXMU!_Cwix%)kQt~LVOhWwYx^?QU)2TI=i5}<}c=uWXYl?JLyQkRpZuy143+M5S=x6B#v0|_!ao^RTd z6`M0)hz-w7;bZoy3-*(ePvcghe1`j(%77?InYoV|*ik1dHSB&Tb#)5G&({pbC`YYc zD+c!UF1(pmj8oF05tt%I#6KMa&*AkkZHV0{^AC)mI3S27{8Rk7gX)_9y<20bBSX$k z&KEyxHPdwZ4-e^HiAi&_&kW0;kf;X6#o_u!`6&3{QL>(zxQ2J*4{uluz=Cig$7m5MQ?PYYOBU6ZW%_btWpUQ0Hj+gk`ms4LR>G3+o~5OtoKphRe>8lLE+ zCoT`Y;d5dnVzQ`{a@aZog-MmfhI<)Z9&~P>F*VO zcPJm@C*0Q>RXR~;7+2Z-Z0*E5RIg6x0pCRqB&e*`&*@1h)jhe%4(GE|msWb&bGYL6 zN2_PAvd>B`4d&FW=}C#$IQ6Zl>Z;YizrHZZzLy;FEN4<{0WHsZ+qxTRsGZR@r!%No zXxWU=Qb9s?)?b8J1=w#btB_``*?e#ggquB5L9nO-I!G>e=!Q}wmqu@ot%Yy?K6&-` zujzY!yD@;WP7KP3fs4TGN6R69UM5Mw1V6uZN@cCXIu8_fe`XJh_cC%DZ(L&>D7_a} zI-j%)DNPQJLg@3;nU;z8;2KsR)U2+-A6%DqTke?s{6-%^ z2-9|+t#Q2O4t2fFMan8VWoRIddGR_25>OaVxK{Hrq$VUHVDQ&-clsTGp9l>H@5KPb zgk%*I_TCS)6+5F<4z~b9pTgHx`F=OU!Kr0 z<~n3-FfiAsoYx#V%BP(7hW---$Eg5#S|71zl|@pb{px{5=Ul=dSg698`X3+jPeNK8f zFT-}CLS+7cSe8fXobHeCX5$YYo=h-Cad3;PHwZB)cnzpOJ&FfU6ZgYP6e*k=shvzAbo2(kD9xKJ z6B9ncf$+N+2cDMkP)I?4%4<1cf8z8~@XyC%0kvM^eZ74{ng)kDbdB*bOdYR1SU2wo zW!3V0``kG3ygX&Tw~rvZ;C)^oAdk&41QbDBJ~58*Ia{01MGbyB5U#I5*5lIPSKb_u z>l>n^1s?$9LA2UE}Fr7iK8Hyy#5AbwIqM6*{C14> zyQP3R?AdNZ5ABUU?%2);cF)rak=prvgiVj@Dc785@i$Q*Uf@XrlIPcBp>;9sP;TTQ zyQUH5Ich@J+`h(%sO)lK7vH&)WAo9&Q+I_Lj68h;RMZOFy&~gT4nRM{mObfc*9nsV z0_mZEyni_o^Qp4^UM)e)ub)&JX6tkGkfx)(w$4S5XQxdX(#WyqY23t2fY$=-HyS-D zdxn+5)G0xE){m^CHGqtH-ssjN$ibc<$!KX+;h%fupC6CwjF9HP5}(I`NS}3W{>8k; zifT0kO6l)_l6k>r1g;v(q#FR%UL4IsQ;XMH#D8cuDAH;U=Nb=gHMan*&o4iQ>1xCU_6@;Q*g3cd%2Nu1Fbmr zBojtuv~R$>8@rp1yGI2-$}gQAiT#&c zY=Zqc5Iwb(QK^C{yXoIe>=wbnfe38fdRHiJO2?Fy4)cTV_G#TqQ%cY*?T2^hGzv6o zQ8cV9^!jwt%jF#>`womXnmxbSIn&RPQk0sS@RN;K>;3gUFLQy7hkf1Vy~Ik>vW!%4X7{)n zPPB>^po$U@%45x%=NGY)I_$f)LpzBEtKa9>g9bhHk5Z(9I)O=ZHn>;%z!pb(o}d(z)NsByy}l?bDhGET9F8=tWc z14i@wdj05r{l1pnG>?(mKI5&15jA1y?C8e&D+ZAdf)A_LS=qEPx(T zm|mxf2irfp{=SN{qnWNL;_PmF1(|}lp&obF&|IbgtCY<{!AzEt``2;{JO1dOqwQ0m zDW(rG&-m`S66F2rIah!1qqrxROkSXtKYCtS;SC%teAVt~Vb6M>hgaRS1eE{8w#xcf zvOciI{m}a_k+#5?VVwq#8Sw9-9JMgxSsMr_HWudkTCm$>j zh-Jd2gT&jGp>?^Ir)QU+4QcQME%tu*)IWIO@kX(-o~ix%(|JW?Q%<3=$W4)diC+Fw zM1$TBMlkAN=pl`-{>4g#@8dw0DyCZJaIqNTYs~LN!sG_~Gwvw|vN_=oN7#*z0OGm7 z#|(9rMSGFOr1dAL0Ew9HMpRsezyV zlwDC6Ag$~ZINy!-7W7>JJ!XVIrcL)}6MHa=2<^zoJ~-!&bh@gRwirl9(VnYNbWa#$ zhl*1aXSQM2SRl>o_C^*Cy8@^Y&Fy;ia;a-MF)D)#w`BC}>XGIoXgd~?X2?VBPR&b$ z2!3!d@_%BWU*ouW;`qD5sza%vAM)Z)#PNaNI^qTg^109#O25&19JB0GKHlXH4#08D zkKtND#&Ng^eoJ$F!b*}gHSyNg08U#$Vq zBVm}($0yqSef;(w`K-U8+avX3e|9AwzhM5Snh!L@z=qerymiozjEoMjW8!&faQZ{2oHNG`r#h;OWcHGW5w(fVIbpDGy7eBbS ziCSL)aMb!voyNMXx5x~$)`r1gj87Nh@qV0VyK{xNwHqd-(?wFY;nEh38GEizN*vGrneevSU{5@$*@gxq6^94u9XQlI zCpV%QI%~yjQVCsYC|~%93a?X4e);h;7oVV(V0`OI)Jf{ea>P7Fc*#*C|KhXyi5Jr^ zCiIvuEG9hY%qVTW zZ1cH?>zpRVu7}M%SktRjj&S-=T@A)|M{`17&BtGHw>2)uWw|C$px4>Qjw}D} z>TPJ<&no2k`oQsVz^}mVFJ0Od0@qge2O1ALya^IrsE13p9-Kz0j-j=?$QHWm?T)H% z`dMq`KKC=UE1f3uO*LN2oh-Qg-W*~okx$I%Jb)nM+u?#t3)-F-wde{h#@Y4m*8xY; zL%r|q0c>xJUhH2GRe#%?*muN>zahhk=S@XO+I<4`n%eepe62Vu%}I@Aq_5`E|F8dZ zedNPbT-nhW?8C&fU^mfkhm1vuIp6=AY|gH2J%%F(^5se^=))(qfMgD&*mAF3xm^l- z9RE8-EdoQgKr3LDM%qPJY&Op!sOY=YK*4!bL|)F`Q@MVjFCF9qqHQI)WjkeSRYm9S z=ZKQwlfm>mwA7lM(?Ad@(IKm(d016R39m=?{mobhl!BRu7}h2rj1#BQNuK8d)S@*f z_6KHXPiJrRhb0)BKXqL!Jg54a=7MD&@?!6sDE{I#!rsL^7v9^w$W$ph-cw}U5)u=G z_rOe=x1Xv)T9T5UZq~U*j~G`!O)Bcm7_=LFc|BLOiuatsB)Z)%szG!Yqb5127GKlWu#jJq5}aT?lJDN$<%LSGe0KYL6u+}=6D^DW6cBuc@w1F_%*iV5 z>HxCZ?pTT}dI-*^oIRR!vrb|=yU*>%W!-P*RpE7;b{6|x%p8rEzW*wZDzpA)R!;v@NdvSvUQ82MqNz_9CQ^d3Z&f)BVtd5Y*J5B~B*Is+ozrLB~Us3R^ zbaz>C+O3;UE{HO5;IeMOfq==xsdc&`xbRyn=IB!gR+j?iaS-I~mCkx~WIUlWeSEu4 zca|}rpMD%+FlBsib>*o25C2$??r$c;(0v%v19T^}651gsi_)W^;}i)!m?T}X+IJjC z`R==|bYhaQAZ=FscQU1{B2 z_Y(gf7xc{IdEbQ0U&G7bAvX}-7~IMnAK6`n351 zbMmKCw#&w^t_}Cd$!S~3+mw5{92=`+sj}}_;JUr!>DSy7D>u zZhRKV;5yD2Y9VBw4v-d#j~jg^`eW7k_?LS_c80Au4WN{kPlA3T9011*PD|~a@`~DQ zCu=s0f=Izs!9YIgG0zY8_PmI%*B}ZLuv~T@rrxW2#u;^smYzWky_8XrMjUnVbM`PO z7z^4fHY{u+@M38=P3cDi@yt#SF?jV5Lxl;U9RVUqg8M5<=7pmaXqK@`8IBX#I&)T- zkPXYjUhK!0mJTkygFqf{1)fzsQV~4dcV{cBlKLoy7D|5(P-5|(&?u?2IjkB#8F5%r zUp3z!)+2Jr=vZk$&)2Av!jgN=oaGIAgxQBDzSI^%4@v1zq8P@=u3C7kqkgIW89@6p zN~K7qSVd;!)9+=+XSb+Bu5g2z?4L7xRNx;p?J}IiZ&aj&$fg}u)phLh1@U;G%r0Y* z>;Gq!mpH&Yq{g;tVi;m3wWwHjCZ&|J48*YafkCA5EDH-;%>+PzY4K=`z7^&V3lD*+ zTF;|%jYR=>5`LQBnT5sss!jKiGrBB{9B6`(Y~dav%dE%PJpHxmkuaW9r{t@meYj7* zF6e7;2`Bu#o8z55>W^6gLE-tQhjECNn|SL#y7A0PDlb!nJ>7{FV{#F(v6y|$a3X@A z>F}C@5+^6RuU?+G(O@sNAfGwK|N2zfSDL)$Zn8q~TIsj5rC31a{^(C2wG5>JD9vNU z059TttX3FTw^V-;9TnjCIsYo{+xUxt;KLJT@|FhqQTz&bKWMYfmUMOLFWwo z`UI<6Z&!X#Mj;d~DYOp#JBrp9<+p6zfTtJQB;#O4$L_xWkqm#Z1LU8$T$O=<1a>vnX``^C;0 zaQf$Qh1wKv$&B1Hz00A8T!Ossh$ewgW+4ZXYBma6EkuEl#^{JsdHjD9bxfpXGKIDb zu2z$icP%CV}ltnn(pOcQ>il^*MG|~ zKY8S+w546?Dpc}Y4dXei7&4UoZsdWLU0UiRm)Sa3gwfc$y!h(ue=chO{AHqONz6hW zGKQ6ci^1#!)51_$pa;1^6-;6pgYg*$B8ryvu16osBAFyH4d$~b;+9F@TJR&AC9hme z{GrU6oU?^K^Uoeo4%+k`6dZT`*C+=Uipy&=$vPmqvR(bD+dShSq4)>p42)_G15O}T zUo-4cyJ$+Q7pNxIskWnr@=i-q57F|eU+RWxeAKig++QuHSjbrxfA)Q1CiYW;*>fT} zWt%G>*kW8jodoug1*faQoO$Q;_DPgAP3$v*saUuz49bWu1d`01L_qT%o}!dSnne04nV}SkP(iaX?<)Ae%h( zq-nnn*zMTID)G8KZ}lOXzwULu53w_K=JJrgv^DPx2Xco4kpf`!lQeJ=cF;8FDKx|U zVn7=am7_|XO^Rh&6@Je1&-`UOg40|%P7F;L@g=w=c-90uYxE`yB)x==yb(#4{kFt` z*lkI7x;WsL=VEC6B?CBQ4;XkGp7s;3la&B&pwBU_#)C=;lE9;R`=QlSi1kzO%MD`> z2{qrFy$cs?(jSYe{z}olFK}ij2ArP@KsYslX!(?Vh8Zx*K4R}n5U3@HF9kRd77azQ z9dly3g)`S3zU*9*W88R5{@8%DeLSde?8lWa_F`^~a^r)!U)IAgM5l&A_-jYP*!p*N z(AN6oH@FPyGOHD>f;vtet6lHr_etq#n`&~BU$zalB=KuK^V#^IhB%q1aSV8EE}R?3 zcV+{=YDa=SmDL6|s9YT!2w!tB>45bO>R95cF37lwQ`h{JuGbDPj1)xw?&JLt+ayEenxiK(&xV^H1=?529(Tx6BWHF@M#yk zP;^7QbNvZM6ct8ZYBnXo6I(T@7B5SrD$??%kdK_semTXj;;XAl`R2^*Z+s*PQ?}}I zEdH|A%z+5YT=Vo=0HC5^=Pnu^Lh{eUDTxeafR~&T=SPMKT7n9kfyc#!_Ij~YX@9~}2GQz(OAb_=-^nx*_8_h4(KG}_wb*&CwP{{e{>l*IR`TGK2_mm$=7px2ynsyNj~q82 ziG$+a&W8uarh+YT+eyg+rJH^D3TF|N) z(?7DAXy)(g;9l3LvunPt)n8=N2>Pb>OV>%W;Ct5MpVlLCgn%@f5gkpfk7u5^hZb7K zinA4#*?dD~V?7kQ^4Ig_WGA`+jWBIVp(*#{$ z<(RK*Tf-ig2&Wn8M;_{8w^F$v z*0a>YiD;B=<7N&wH46xB*T~`@QQfTDkg>k*RN7Xw{(SROF89TQtIqzomErMFr&9a1 zvNs;l#s{Qxc^t0v9(lf3zDPx{(^Ynm=|W5W*i$AOy^GiEnL_)64^$dTp8chGb1$2j zEn!pfwK!%$0uc*RxN*>(ht#N$vBzo@F!yy_4p0K);t8?|`;Gol*IKwqItRl0bAbOe zTc=kU7X>PAp|xrtiRqs7eWVL7&4x~PyaV!}l}jN9YI}Av#C;!IhU7=(ovsW=90h&lzRkto+VkRCk0A+(F!d+(_B zZ623P&IN%=Q-zl>m8>^w$4sEQ$G(r79Oa`AE&OEHHOxu|KHxx{zO#(;@WQQ9n6~T9 zZbZQ+*y^Mh!xNz14H4U_$n>VJCKmY6Z}?Qt_}b~Z6lPbsn4g$UK*}{br3SRx8HxD* zw7I-XuzUqhwONnJ;R3{Ip=7VFbuJ*Xz4bT%mC2q{rX~*6W^Ugay~0qSS9j=5&0IJ; zTV?$6R(>hZ5f7VO!cHwHn0e?mz0-o%*z%3v#?B)s1|&rIUmzyIlpo)m?qeB|b4o)C ze1@GKqn!^qIGBfY-(D+FQBbt=hMkf8^cfe2A*MlV;S?E$1-{#zw$Z**k&+_=B;b}p zg8G>|cax{ueAL@BubWH$a92%vC)`)lrK0^K-Kb$qHz6ydl{Yh-@R(PiFX;@Fvz;CZr84)YEcG%v3 zBcgV3M*f35`6uRVr@^9{&&(qPX93IZke~`&A-vnIvAQMdlJL34G#ksus2yr`Zi@u$&cJsOPS{7phU`KC=j&RZA1 zb2YD^TrrS_aT&$j6MPaH(+!H7gisfTZZqbN0uTB%+j#972G?d>dMGk!-Fd&{pEoRn zx{+Zx7h2Esu{%fur#C*&FM6SM;FQI9V`0K!RQ)53vS8c(-t9Ya(m826H267G2rC(X zXJG1)iY+itWE^<#=Nj*-RL*$a*vWn09EaJn`AxO=Ec?0N&1~T=rhDDcwQwetW>}|x z7-W70lad}-L3B5~YA%MMy3|;ZqcBqA6XbAUY(_=;T4VWE*GlPS{!=0|5u>HrxwY27vl8>>k=p!Do0tv*~zLhf<13p0yq54igZ>_XHEz&v1KF*ohUXXbwP-SqqzL-+0}qCHei*Tu`ofIASsI*~cl<+f zgSDL>2W9hi5w*CdrDDM=I=zh<(e?rrS_v@qIc``7o#u74ZCj-=T}jYNvEJg^iB}<1 z|Mdx@U)X3E)lx9F5kXP$VmP8OKnsS()h#wBI@+vQ<6}Ez`$i^>`Ql3vzqjPGdr60? zt3S(|c`T$^iFo@$sTOK{1n@psE zF+Yjgm#=h%?aE~jVnxwsDUoyoz^x4UHsyM#({T+{vLU^_^2^6&EP{h1DT;}Sqh8o- zyQ)RZ2k{=k_=Xl%Ia_5eLGm1CKNIGlujf_+;;O0iZ7rYPrJqhS4k})+8r^Pol1LA0 zF~5H5B7HZiCowJp8svJ46%|VS)`TG9CH?wz;&9es*qDd!Jja6i3b!J!Rhw4)aJ}qs z`-Hl>@r}XF_32;pb1;$vAa4nn=UR+7(pC~E*K-%Ciq(Kd@tr56cmSZ zVeVii8FcT#J5(^F<@IqDscXV$FV!$Sp8I{o{3`tVedC^@!H=}rfd_swe$f@4Hyllj z>e5}V>GoM~n0LUb<^vIp3!RSZ6sK}OsC1Bmo6;oH`ow`fI)}*y!ed8AUL9v71|I0_ z<3J2d6|h4s(96U628B+GuF#t3?(Si&c>d6b_IdG|5;m8vgD4VwzsT{X9e2Txb)`qa zmrtA+vIRva1#bTrgv39IivL|=0{(5CBFIoc54EZ`fK*&eeT@F`;75k4i*wTT)adRc z_OYtTMHN5wt_+FeXO)8~zluz>7{xTNd9OD7lN>R_v%oyLAKEr`IzW87wsi5X>em>B zBPoxJPX@F{rcRYGWdH_MgC0Yj2Revq&mO|1e5_0tOoQ{_9hm(Og1%Gu8*Dic&kw%7 zmo@%4vC9#o?;pH0_<=7aY#Q$Vo5)4k(LS?Mdm=Z_;Y^;ek9hA?G%dV@x>eN=BW|VG z_3eM(CwFu@HU02y{8_wb(v%)OiW&^2Bz-L-Vf^fUKuk~Y4xwv3HoaAP>|t@Wl=aKf9or>^mDZ+=Pm+30!sy;Zdd=~o2IOE3w1k_aFKhqNABYTYwo zcoKh)wDQ8)ql*K`RuRoqS@Ezj#e8g0INWQuhuiXQI@P| zjNhv*d~`;LNdj9Qb#+AFJr@~NQC|45VWg~JsN&JS8%F!GjeQjE=6>1!^5t2FpUx)B zSSiM~c9R1McPle$q2QR(*&r(_J;HKfr}iAQIUp`GLGVH3aE+d77) z$FDY2-ibpy1fO_(`&S60^XTma2GpJdp_;+BHOr0IU+lWnSQc81+iUXeeL^ue=WVYC z&uQbyujx!)tkvfY(L`K$VSGG8^kueeOTO~T)F0QYY$?1CVcMF$4b)Z;kS6rkG;EyQ z8;SDqga`1e;rmwURwvIyvNs>&K$S(rCFBHsl&VCLFJI>CKke7jugCHw8~;pV_vwVy&ZmSlMVU^iYU6QyzPmDPM>z8oODBnG*Ya zGTEyn{-SExYjRae|r z%(ATVdkdqpModF>%zO;TZP|e&4B$-w&Q7pn#h-v&9=MZ{`K( z!VAprouq$h-fmOF82a4?wy0gt<`HC{hmlXZ!4_Zj;i53=KzQ(!i~Vxw;+%pVYzcmeVr*9%Ypn%-TnzejYJM4G7+;FjuZra z_7PAMu?78?)^Uq>;$a`&V+Q>ZOg<>{38vG&9W!($14Aq`;XpL8-vn2S=b_W$J@9&H zS9u_Pky?~S95}2Lj^gKO#pYkBn$~`q855VeFF|pPxFP%K<7HWIJuCEuH~l;QuKVVp zV9E*IY?Zre1r?vX)6D|MaA);K)esxA@oP-KcV ztEhNQJKi?_WwFAWigl$@bWA6l3f|--s1W{+<@c-}f=sJWf;-t#VCsU_lV(fxJt^j| z@;Y$2cJkBZC_5;q^(gp_{%(h@OdIGT2;?ox@+EY4bXp;8&_iw~<_un@i_yy;-A2lM zxuW1dL<12;op+*=2)6oo9MPM;xxmWuLrJkngVE!24kT zf~5$i{V5g3)bwI%2{3SgDTV}2zNb!zSB8CbXl)oyPkU`ce}Fuj*zDf&=$r6^md%l^ zkL30MtJ>O$%}T&1AJb-y@lHyZlK_qrozAVJj+X<~ZQ`z{XxC~<7+zn)GYuQ6h-+Dg zwbQi&X2NpL6+UFpjnN=oy9jP@(2)a@lRjwP6)8OIa-^A5Vv2p>Z@>u`<-K;GAx66x@vo-&@M}g)pDzrs@Ijv(sWzkTg6*D znT;PeR1I(TXAi$g1weQgdL3!`Q;mZ%EcF4IpCKPu7_f49W6cOW=uqstygocZeNhN`d}LrXj<;D8xwM{ zn|RzxF4e080VoeptjTz8Unq~i8VPgQ7b<=iE$uz0G|{YQn4l>Y#!k^nZ)qMXLeAdH z3qST;`Bi(>nYh=G8G&EjxalT(Co6s{>3OR#fGu5u3wcp>qM4GxB)wS8IMt$@jjqPR z@l0HpzxQwbB1s$5`0vNl&wes?P{7>>K=;f2i~6S>bUdxcpga%Ngi&VWpzuOQzH6r* zOBSi7aXUsm^Wk51wAh@x8UTNLDfxD~J>?Z54%Gna%rFe_jZl3pXdGJwSf3Z1!gb6Y ztECdU%2mz}=2g+6s}b*zDFJ!4U%Pi6Kb$PAt@cbu8W%jOVy~={FN#jT@+}_?Vas5M z;vpDuOW-z`jH`oE1`y-{VI)`k4lK4+<#PZr(aT||Ruw~{mlM5JQ5MB=SuTclE%6eH zb@)@LvMY*-`JTbImODTuFapK?jul~_MBPgzsdYo436$G#fx7%K66_H2+Df~u!pPR% z!Z_!tZsPQuz5k>ApA{;e9bETxPby^o)jtveC&h0q?@)fUm<09-7r$1SM(V4qF2|?W z?$#u%hUH0nR@PTvN^vP^T}WHI*2P+Pp8T7yB+@_#_ z3le*y-fO5lXqQB`lR7EYk2P{ZEl#;#TLE(rauvNMwdw58b0&Ta`jbX~qJ(%qeM5(S8rmR@Mrj$z# zPs%6S-r;&RR?XVQgi!4gTcuG4sF)58B+l_KDJYhZ@8IB@SvIUW>^f&8TsETloU8Q@ zPyOlrfhoIiZ=~7#mZ*YkNxgPV<)-j5eMdZ8_S>(HsRHqwYn4TFOKTqWO%V%17Y8Ko zNPf3@ndW(-Q@TXb?3=C+8&17atlYh{g4^amKEVtg!l!lU;M|V})yXIg>U=v}9tbZ* zB20&a;QYuWd+M=5QrsGPBA6a#HoEku&r4=uHnBnH(F#}I%pUusulw|`5~x;)=b0v} zcv@B$To5g{l%r0=M2dF|``F4VP{$^sm=}r$N(hDJosLCsbT22~3*N=k$ra(OZB_!y z8^W|{wF)iI3Da_tXt1tUHChpVK58UoqjRUO@Nh`W`^Am0yiNzLmtuAFh$+<9Jn0~T zAFokuU8p988wdf;1X+rJsalaTeaSK)gWS_ekXdRx7R4$gyIlOHGO+qa&pLi}>eD#Q zhyLMmc+txj3G#Tw$+~g%#GAMupxuB>|IHE|gw_tmg%4UINXcbbfNH>6itPkFW$tLoz zHb3YTtXo&@`6m2X$jcmAe9g#ac#&aTX-iahF4yFhYO|3kOui|11nTpbE713LtpJR@ zZ=M>`ag!$SkrwGp6Yp{&2q8n}+tqZQsmV2JPaPobA%Eeii@_b4wIHfIrZg-0kP|OTm--(#CCAXK*)5HfbwEY99dc>u z?2|q%pzb@Wgi&+Bf6YNU4UFtmZZ|RX*xzCGbA@2^a*u^$s{$S8$7&GI4CRKHI`5H= zBbUTV5qogIj74YcT0^hg6T32OVk8k8qb%aTe-<7I`@0;Nuu4H#9)qnx#2*C$gG+#^ zl}mNh;@D=DApbny5RDmg(X1syx!Aq~UCg1q9yV$7c4<>bgI`VxMlN zx@M6tkf&i>&b|lnF_bOOMx7ElwaKWZ#8XK*>6Ay~BoIS^X~WfJ1(5!RuIwzrd@SmV ziRfENzzHQv@Vtw(D=Xhn#3x5_e6@xkOtAMcgbcSz`(>sT73VKyMt=X|)Oqkcm4E5R z@ng1${mrMuB!ym`lbv8(K+j-mdE4+)UY(7wE|`cBHNPX|5x#gD>AW&0+So`<|5#vF z$G)54dbCbG>^WufmyO-Qg8j)c0uq@FwZG`mg()^3RPOj?x+~}O2x=d0C6TOZ{VCn? znHBQU`t_86AQg4#z{iKJodva>koV@pXR>=Dj2(eu(2Ml~#x$=^*OUJ6ZrL*+oj=dm z8f~^BPRm>r8Qk)FQ&|>mlQL!AkRBksFJ)?3B$fBU?*|`GOow4`E1PTEOK`*rn3ax! znJgCNo%9ug*jPQ`h3vXkQT;-NhzXZfXH}t&)8^?>HLREVN`=TQ(+1ksv#ElemkEbo z_`<32K-m5(Qft%4>7MndD7TN_@6G$NXwEv!LkVm%1O=3cc-n$Q!1V$Ci8!p=>|Nc6 z66Mf7`zeOu(i?lTQw1|OXCBTXfEf^FM%9DR?H-82;iM$yUJ$ILzr?gpgM(9#8B(Ky zB0Lb$_L;GqLCp_C4znq0w+-ug5DpjOKzn<{;sKRl zmM}>5V2GD6%o^O-7iD%Ymc?Vk0N3H(82Ur}FD847ZL$FUmt*tUVg#mW6FUwAl2vjr zwxiwhf(XXLk{$7$MdLtz7%_C=8*EGnsDAmK^k0t!F*84c%{L3i82An-AOz(=R8_Vf z#KWqTz^KRu4S@C?WxY`YeK@QIlgzYZI**~i5tL+Fl_Q5mM)WW5^si1(r|Lf=Lc9V` zT>i4&Zu+7?@4g{z1#Zj!hOZT*{0OpV8c-vzk2T+{Bx)6YRl1RA_rdFh#-&#BC9ZM& z-5O9>2V0#3vvpbn@o4QTY-eVAeTj>&h3M+5+Nd>8AG0zm%Kkm8XliSi3I!twAUfs_ z4d)FutGXeICNG|mX(=<`f5jjf!bKJR4G0K#U#xjQaFCdV;UC*+5&wxqxlXm;f3r=UoN?c zx>}I36ty9s9B$&jQx)#4Sz3&ATo$-yyG+5f$=6Hr@?}?8EH!~Ni#j;1BG_N*;dM!? zNo2w##@pzCzUGq?_)(vJJ#RVtgeb}Qq;a!!m=HFip??aF2pvD-|@Ds(9F~9X2q*|qX(}aB0!WM0sDMas zLP!vmo`8fvif8h!?_J~U?|t{!-&uRDvClf^hl4RP%*^x5XFhY@_jO;_b?>#H>2dZ} zV#HBW6B;X=4)#Nx4rp~)?y+SQRu7YvV_5G)FI{hQW(^*BkX_Q@M;n}@i!*3UA=G~M z6T>jF6YmP;a~;sqWRha)0}#)HPjTqJq7LK4&^xYsmv}qXzrW*6o|c)iJ_b1=WA>B^ z@K<&|TAMQf6IP4u|Ql)&V z^@uC{r&b)cs5A-25K|p%EP^xipy{ef4>8hcQPxFvxUVl~H{&@Yx@lrBEh$@zdmt0ak(?r;YzRz{|l2x_*tkdsN(>}TQr@@2xi)#yrY|Jb9nlV zm5~N!)QoVHL1~BYj=~_ZNVV!DBZ|3NvZGGta_|NDjx_6Wr7xyvI+8F6ZC;XL!KuDP z%wErhJCr0Y)7LF8#$)qO-pXvf(-z3@)qLErHI_?D)w|u6JCn|v7TwUFgQconZoNeT zHH`>As(YKUCi^wv7It)w?#@Vtyk8PU^0h#AqfWk_U}P?}7JQbJtF7ijkL{Z$+ z#}y150(VO0CEZA_FYLhpe2bUm$S|NOPpvfz#u@J%)bUoD{aNv(C(rJ0RGWHA*`S7oINKji+svhpVw!6Hwc17$lp(y1_88m7 zLb85yDWRJH$#%v4%b&G5$nXBY7B~JZa;Q!UGE~J`FFWYzn~eV{O}4g^Eol`nr$pm9 z7hRc_m=P6P1}9VF)wZRLf9U)E)cO4@f1x}6R*2CGrJf_Kn=Q?Qt?1@ZGiC!(g{v-_ z^JMXTS@*(9uR;YHccToc<_iGN+J1-q9A(cuvW=c)2zB}Gy?$}JJahky#yG>kE{)5!A5tkkZ%ccD@tDEfS{hDj&%}h+W8Qc^ z^5Dq#1qUT7iP;h)Fr45kGHssCEL%WV_?u}(!yNp)ZRJ)WZvGwM19 z*TA%-W2w7iJ{hd|;~U{WfakzCN`{GK`3zwup8!!B0Z}*X-$WrqS0L#|>S8=%50GAz zSyh-W^l*jib=c9}b%}T4*6Zk?G8$esC#Ba0=8^LmL#?crq@K0JZkcgDOyh~Ghus`a z$_jcT_;uKm?)H6LYinhNg+d8lQq*ida}fpJMQ#GST(B7e5Zkh~W>Oqly4QFQ>q5Wb z$s%+0&+&$w18;qW47zpVK53+nlsx=wa<)gZzTh_3jbg$nWHeor{emOLxm}fA;F-^D6)Tp6VqnGPsva{ak33&zdKv4*MqoyxE z6a*WN5SdN6U^oG_X)>XAdH{9g71E{E`9Lp6mw_NBR+M=3bSNbi&FFWvo`8>-^=5=y zT+7%lWx;(}K5Y6u?h7snT4f&FxRWy$BG|;f?j3zUi(DOBk0?6!0RJ{?)&p>D9#7Eya>wiMbxqpEU(n(=;5{V6-hytneD-bMMG!B2y& zpRU@O$sdY(ns;O2fCB3=i3i<8s4Q$v8ZGUk=d&wVuH-I%y>J~Y_f>Gxg?P86XUpUi zDeQJQ?xUfcJ?-f@knUjI<2@z(e`!#v^50yyMm@!ksyvRrtatk3>yv!DZkprm?9;w%f3LjJUtJTvIAKuaCb;j7 zjD){R%YKeEqiIrb7|!99p{|h4qoFQ10hD*IHYw6rJf*j{4_?;$Bch!!oUWy6QW(qm zax3}4*{bg|H@@DGTwXqp@igCOec`h04^G|c&7s?Y-#7&B{|8J#F$C4Tf4;-`5F`)q zMv>8P|4z+?HbxRb36};1CEOMQDB(7i{pLb+??u^AdFJORm{`*GJR2I%gN5^TqK{(| zGapNNii?XWl$V(V@ERVHBj*O$@^D3z{S|)v#D3eTJOlP1S4<3CL?`m7QrdHp;e~@= z*Yq?+%(?fx`sk_rG)3WJhr#`c#`x`=`9@5ZA-_AbWgUG69E5NomL@F-xPAfHL(awT zB?D_!IZ&)9zr)gTINc*lraVy~aCK#~4iN1GIIAw*k(h0U7n<;}ok&Zwo~1Je(WAE( ze}&DA-ozt|TyB24$%hKK%LNsQ;)(%;c4oGt%eeDP(P(6nCB)Co;kh8`qL z;-SKlm>N!p*Ih`F4o+xX@quaMAnRbXBR@Dqk8Lg9PhDrxI63uj5|CE zgD-sVE5iAVl~Dioj=C0C+?D?&vdT(ihp?&kxs!ZqT5l|+g-!%;habaz5R8Mr6=#VK zQEW2E-BB0NeNr~@`3!5H{dgKWJD)VhNLe1F9M}_o0iVeBA1fFwV*Yq|Ceg5U_}h<= z*w@GJl1!w$kL7oH!&Je07s&YvsT8el>K-{d3G`lg3DimPc+}-7*mkdtwq@;V+bUVn zDAovUUsk(<+^Aym@bR!>n6l35(t@&1wx-&>H zngUkF5K@S|!5M0y3l@&e2oyFnJser@{XAc`UiP8D&CN4y%%7kuhgvEFjomG(6^oOZGw(TQ-)e0_o0=<*02zDqX5AN=p(e8h!-(0ToX$C zrTx=~Kl7z5+9y4^^6L5*-V)%U=_;qj|%rE2^4u z-z{G*`eJaYTe!r{W1)}l!!pgboIoxEY`)0=Y=;7Zml*?~Dyi4nz56GY-&o&kqoCnU zwuyPp9K5%5wa7nw!{AWLwE9z}k;f?>Pg-io`yQsKyj)^y+8omNt^UQ{vjPR4UaCv+ z-t{_6BbL@6XFtPWl0%>wkkM&0@m7u|O)+=eezb&jQ7y1GAOj!}Fr%g?NxI%;CX&Yj zS=`ldwE5>*Iai)fEFohs<0_kFdPfGmu?Vg4?0x1Q50qHmf`&-;chb`L8MjRD)08A za)qPjMioTAJ(&PYNNBUuN`>D-e}eDCcwnP#a+GL63n!bB^@ETE^HWmyMVi>)aUpD|2R36T15k zP3Ymx2$2S^?>tB7HAsXv)kuQ>)1Y(Uzg^G?;@RT=K`L8}VJK>{GHqS}R6>=K!70cb zzhwhu31@JuR4;(ZNTXT)=DKX)@nD-@i6Prm@MN^8CEcguqpX{`|Yn6@L9j-?|sewHU6Ko&%Y`8dj9K_d~pX$ zSN>hjwwa*(r%B@m{|~N3Zs=r|ZThJDJd8Ru$$3tJ2(yk-=^2yV!k*pBUp+V4qY{}O zlAYg*T^zH2F|)ji^Amj1H+?bZA+2b7<@?>J!s;Hlx-$yFe#w&HW!bWGSV4sk)-rxI zDP$p0DRuM=C0**xgc?&gj`4gm8T-w3Z5Y|lQAfI49pC2j0wa@p^OwZ4c0ZErv;GwD zA1pAF*|!0H<=#bGA%8l52CiOzxqzp`UxH~C+$~yI8SQUgfprTjG(}k}v-V=>Ji^+# za0!$mdNwevY+!%=E&6+ab<&B>19!)Ondh?rA47<8Eb2ULTvnEvQ&+tBi#S}Y#AB3n z_B>vvK4U`Xtgi>5hN%>{e!h<$!+1v_rIO9xLt~BitFPUx?<+Ah5L&}5bad=Gu{tKA z_cAAKc!tCeLYjP%2ycF4>?Ax=ObgTTxhO1vQewJ=vzm|>5>TEzCiO7F$c*P$x3rnT zhd~Mb-cjXJWKo;5Bq-P|)w@nvof@;~)vf_xoRAvr}imdz#&y%qEOH7?$BRgdBd zR{``ChSE|?9!)eFK%JgAVZi^8z_WF4)>b6Jm!J2j`N$tgvGkT3kgFs~bM;u=t=8L9 zt$G;O^Je)-4c_6C>gkYSL)H8sr!rr^y>o8dkA%AiJ_RmmWDOsSn~c!9MjzY zw9CdQ5~#OH^X|;Up!D=sDSHYtOr%>=jq|4xEQx5bKJTm(R3&VHq2rQ`NAH#D3bij2 zS4{*W%+QWewT_r4pZ0P-vXsDG-vTd+IN^$WjXIZ1I%y1Y z+Dc#ATQziQ*%uMDO{>m4++xGM$N4_kOw@q9D}efgod$pKn`UJvlEx)L-(wLT0JK=fC%sA*oSW4ptKY9(H6L|qpGnFzf<`8 z;CFH6M}?*Xxp>a4lAnWKg5&9`Z}^{|qkxcCCFQlrqs+B;(uI7p4I9iFY7z=q}@-iYr&p855I zu$ROL2E2^%11X_|MCYxQc4q|XIH6(31%)YY3#~_f?FO={t~P?6n)M%hvC9<;`l^%z zUU9ep z-h0S`b+kVu&ZrprG>kl7ID+-+fyr;~h?7~(}hKf=uunLoktZujV(v9-(NbO&vV~xp!J+20j*~& z(0Wo`!KM%ALYe)Mgh>C9glKLiA^gt;2|xcg?Bp5SIK1`%FrOoLGgSzHnYXI2&25O-FYX? zEU~t34sy-X1Mg&FS=a9)rCDxO$J8A=ZL!rC;(^FN(X&_f8qcpknb-f3znh2grS5e- z_-@RI^`>6@bz>wPGxWEge)*65!2jwyJb(HaHql^ls3ViU{9eYow5nW0?$1%9;-R6n zwLZm$l9G~INySXhn<~jfsYC982dqyj+*2hlF+v451B^sR5MYQofB-`gRA;x}0BhJD z-AkgMtp~wQLHwH%rzhu>tG}<6xTK24(RF**z`NZtjMa~ z!Bl2~2#F<<%Dl|PCb715vy6bo9*k?Qz8OUEse>RY zefmBty5~r_;|kC> zvSnbcIF=|w^*7hk5GeT)ur>M>02L-sZ6|RY*SVQb2RE`I-%hU9k~&euzq>^9zy5UC zaZ||V?-*A*t|x)A;6IQ06R>3dK=yn3S!cl}01qduXW$AC=Fa+Kl6eN_Fh~C78`>Lo zK52RX#<=fyV}N4)b=1;Rz(~%G{agpOZw4!(0~xiY@N@R}6fbErWlsLWaA(DbI-Q&h zeUgh|m@XHoI-aO((7%momhtS9~5phSKhj0?@z zg~EKQS0G0~Zu(E7_Wa$mg`dA@-VdIZoZx@vih+Zdt4Y^b0d;{<$W$@Kpc{9CYdzCH4onFcQr zy|wbkA-_88W??T_`PHALE0ih7dl-i@SRxqmTHLDUR^%EzoPl^Tm#N-As2`R63W>9< zs;jK6zLCT^i7B$GCdji9a_`6=P&-|Qs`(0 zMKJUspd@iGu^qzC_H`V8p5I-&0KT!J;&!$C&bP5{&KCE-9peh-FQ;nJMcaRz6TAMq z?%$BT-~pcmjS&b`t8OXOjxX969`qNCKmei!t%%Yhr0_w-JpDeTSIHjfDJ?Bf$xw)S zUL9vqa95Woe6)-=F9}fS{&HklzsmhbWFf@q=lqxgVy%kbT!PoZRtfNy4%8;z(y@uR z00;knKJNa%{av~yaI78rH|*<26SM-`M*0W#g_w&M6quv`0e#I#pQAZ+Z9-qB8YeDv z<{p=;zx3C#N}{|b{mR#MoUgo>1i&SLYs` zpdV(mfW=A!-Gt{}8!xdhD$1WgS~mJDBD=rTs;*Rm1$7DIYW95$ z#z?wLAXNk7p+kMfS-CxS?qkP`%sISaU?<_X6|W))sSDpYm4%DqXjK2a?f3oX+&!c7 zJC4u5(w!nZ>##5O+WsiUP_1FZTF`X?#&qS8aeM>dOC2s9d4DQj6Ff9hJtOhBjBxdI2 zY>{o*V>TgUHu!kq@H!`&lJUNq3%SJMJz0eO%{4XDN{Y?VrdCI3cZ(yO#i=>>Cl|ah zf-ToX+1bxb}(wFIcu_x0OeAMYh6$`f|?oTxef;ZJ0 zJxHLwGIY956KSp8QAqQG$2w@Xc0eU9zT*?$h2|M5@j2G*q{Pm9lf$>;uS9t=jJ>KX z&Mp9cL-xixXsNQ>vT1RkHW+e;VqWfaGw*i?H?AT$ij}E`#LjcZa)M|3TlSA&XQSUp@IB((*^X zc{dr<-hm(5Tyz};$adIH)R_U)cHLBJA!P?1@#3ekQkZkE<6!E=ta-Ygo`VC2J~}w- z6u&RZJo$7?ui3qn`n4ALFq}>ttPi8gtkI`TfVFxlr)2)0fYj0{RcDJ$0EQSw?AVk?h1rn1dt=uW1p9{r` zc9dQ7Z;-M?eov6yn=nd7Ejt_C(-;`-<>3xCXYJiax4_pzq%ocZX+(y8U{VnS(Ud&1 z;<16s%z2UTdGqWL;G@G%J*Xt-M zgnB74m1(G!s=rGpdR+{UaV0U7^#GO}i4im8qhO>Ne&pTv3mLqUr$@07ELGp#c#3q= z{wDqkOQS0PNiDZW4juCKSj{J8>6a;Rz|I_}Mbp6ygomKJAQkWjt0l&8N#tb;ETY(Z z^{B7H24)mJBCq;U%q987`n^Xx%*LJx-S+rigs3J}XRw5(^FuX9LGE!2r4!XC3YRm5p{1Zn+SDDgH{&YCNIL)X&vlK0ef3|Kq+g3t_F=dwe_s zb`I4G6;s{ZD4QCZIg!9M*T_Fq360Frq$N}PL)dsuwoYQS-!+r7kIXJFln38;d30O| z_oNVcd!UO22iG3N>48^r?$b<&7?}W2(6U3Wv~MUylEak7%p9L;XKPyY;58SNN!p(( zU`Cyt(^;QPPrQ1*?44bDH;*Q_I8f5*qtm#>Wo~Ctwwc@Tzc=B z(%oOo(X#RMgr}_>Nu+zx z4i0!562isS9?e?d*POGm#jFlX9DCoIxNnL0$q2DB4Rs|wB)nZ~UJ54(ag^Ym`k&*e zB9tA4w}(a@Iv*Mypu{ZBFZ$r*AEQ!6EJ9K{UEC6`MPikAJZ>noEoAAj_7GEF4sjry zE$l7#@5ecuI4QX=!qUn8;HKe=HI|{Uv`KmP+{BUr&aCY)-~~cl?(9F%xh^O`=cG5$ zIS1N`Z`lf4&{36%(d^#@A#nRv!~bkF*o0*$!CARMbQBl{L58w*#%w!A8?iJV`V4sb zDlGAV`d|v2n`OYzsMUC)4a%CcS{_5Sjb%mlr7zUh^aiJW()p0CQ3qedo#vk&`MAY2 z%miEoh*JJX6k?h5kW`+rpiG90@_=`yk~D8gQ5S>_7$5Mg3JHq)r7282R$iBNKH)`W zzq(J!gOo9+7QSFLeyuhY6<3q}E~~medlrTkjeTa$J8gvh_yiydJQNfslnPt14>3D9 z2j?<}+I)8+4alP?7`1=2?Ndj|NrY&Bx^3pIwc@8WTQBZQUpZTFqV&T&^CTn>3*yyV zfcLYT(MEKSJ& z)v&limu43cJP7e2f=SR@Y%m6r7nQSFLB1Q+>)G&T(Dv>Z=Qz#0WdyB&GR$h?>|s>h#198+APkNq7h`N_8@k4i3HfOzlac(?dj$KWO-v5fuO%(QAg_y^wA*YAi**_3_4hx;`tTMd9ec%T6W0KBB#*b}2Nrq#(sM`H1xC;(@1NX>ya<Ck6(j2`ARIcWdKv zId}knhAOw>7%BsW;j0h`qotX{l?7*bQ*l>ndr>fo=)i(H&L-^e{Z|c%PnDN^UX~j? zD83m}XGT#>^@ZL+_rWU(Q5ZL1%I(sI!Cy@;!Oh5u_uL%f7AxmQyxOxZA@St4yU)T; z;ywvVW119DzV1`77|<)HL6B_rA`fLslUa+IqM65|j_fX>IBj)uhU+gMl+WDdjZ0S4 zGQy3@>cpdo_M&F!Id%!u4VnnAgwHS0V062l0H~lRT7+i)K<-QZapjSM#O2Y|wBDe) z_YvPsi*GA;cT8`eb<*STCV&G#n6d3_L{knA zp`UZmYvM>9RSJ1T)BAqBecwXDLZ1;soo6_=eCyo8L5WJAd*=0zr63?F6bxpbU^xKB zzrf@Iq^a@HP`yk6>1U9>V3Pl4PvOzXsOMigY7Qs5SG^0j6MdO>9hLjx;o%WkOEQ6> z^qZ?4wiT(`gcagIykvKNF+PZtZ+A$Fzon)fw!=uixp~aGB1}I_E&th_tNQUa(!Tlj z8vJi|Io-c#Hd`I$y;htYOJ}HCXJJY_saMF0~1}l;c-gZ;3|w+!};l%Q_eE)VSx+6A^!(aX!TlmmWo4mC6JP zz@rrFA1w*Cu>J#}zfy^%9j!Z}{#>Sc_^)IB;~GR5wS=YTw#`qbUN5MkyUJ!ogX_!) zy*;KbUTL-tx4gi?P+QJsT%M7!1VvzS!_i4xobmoHa%e6ED}Y?%h!R=yTgk>f0VS|7 z(UO-$jjzE$LRU?Y#h0h=$L?Ry`9|17m@FDJ-rYzeC9n)8CGmtPIKPf#1Ee)a-pRRl zV%4p0pVABU^e5|?vwTWl75x;>eb?Kg_C)d*L>MeNV;nfc83zlnOlOTcVWieEZ+zC0 zK&5NuR9kIWNqd&(t3_tkW!O894^X|Q{O9!VEc3(@CX*X=`dE%rY;kg6EH#H|N&`x6 z&3aSuIFvOxCcszZr+QJ>kMS0Mzb1A3a$bVGLss;|)B9A5<8l?Uox|hu``I&ncxf3p zEW)OD+J2!l*ZxJ?ZtJPrl6JC_VB^KYWeFe+B?uBc5rh#Ks1+9xVw{yKsFBqVzhy9O zNR}0rhe}JmrIgZd2Ch_(B^|Psu&Dp`GFK<48dKdpFF>Wt!lE;W+95kUGcx(AfM8~{ zA+0`rc@%r-B{Jf9!lT!h?q4}1@xEPk8yR+glPyoL>?in;9&prq4Xq|gLX6$SCVEoA zd5wsxGqq`}t>wF1)J2pxP9$xq<}u^8DNmb!**PWc^w9$edk^*~;}^7CTZSnGu@qTb z7{!rA`2?%6j#Bs*PK_*Du>$>^*MoGA_;|cbRxh-36MlE&Ub);`omjFjLp3m#Vh|=s z`Nr9v(l_}d7C#ooV@10@s4h!Om>wGT#<}%IFTE_w`SvC+>heKzdnv_BXMQ${VVaB# z114ejpyl073$|DL7JZDjdOd;z*U*pCaH3cmrZDZE=m*8cfku?Tn@~z4P`H7O8AHP~ zwSflC3)@T>R^V92MC!cqm7_i6h*sCrvIkRrZ~F*;y6ESv_^L)VK9H~?$twGZRKZe19uUbN0P1a6qSR8$QWR9cZKz(4j1lji9Cx`;Ip__s zFRQ7lVX9SEcnq(kG%5b*(pry#OvJ?XB6&ExrWDOMlJsgf7wQPZ)3||*<3~1Qc6e<> z=2UoWbR_^^juF>}EHmP>^j(_9o{!I5Xa9^TK|O_C}X}t zp4dI!{zHANpz8&WPWhia-wYz{fcKs?L=;oqvvSas9JKH`b2iYxva&7 zv}f}eeciMx<%cAc-!K}$#PQH@4_I)T9C5N5bi$$tMx%E>hl|e+j;1xFwgu2l8&gJ` zCoZ%vd#L9nTAW0l(AdLA=z!;t_5l)K0%jjt9qBv86D|yk*2~Oj^>(>D;B9|ws4!a=aITA49yO&G7yo0;VX3O$e^tU^ND#3PX!bm&?x&Wc(l5Ja^1eF|QwH>JUnijJpRBB*vjkOBt%>w80|8D@1y&PC=?mshM-{c&VH1N@@Ox z#&GZ9y32mGJFb!^!1m*4C{G!OlEVt$V|Xp!YCIAsNx8XJ_dXz*?Y7G-P-M2J3 z#L}c-_(nY)_%|X!EaKtFKJGtf9(qW9%~0jOgrzi3;L80+coo9XD(41Xb5l|-KV*nu zngAQkk5i<3kZr}44Zh^hxwPHhCaoOBx9?KtH}VrR$k#^TU zyCF&aDlB;_e7=wZXDD;Zm`9L9Z4TXZde-Djz3C)Y$hlQfY*1`MrzVlobVI~2MLjM5 zm)PJQNd>jCPmwj;nv2uk7S(8PFcFySPOrR@j%t?BzfApRZBoBU>8MN4GoU<46TJ3k zk&>kkEEK;zMRssL;o6|=bE|Jy0BsMC;q*de&|APGvqXy{am^D431y%?36!tk-IRdv z@h5iUGee_YRS7zsW&Umzj;pl-1E-bo=sa{AWcl1CX~5N%g5?wde^eY1HXJwx;Q0mB zBGx$?e!4zq4^6PS!JXX2!QvA-WSo~JJ3rm2@FGfY`8@e;58P7XY@ka14Hb?2fzl zb>D-R-!rf2EXIO&?_9rh(+tx>ubF)UwIkI*>KLt>o#VkwLg962aMfCFIQqodesuSqeW+IWAQv90f^J(usA5 z8xsVot5CgULZ;>DEE8y30NwO3x@#nmlLeO8CTH|M81EEXx0|Vf{xXW2Crmmyc=M3E za>~eKSm_b3M(w43?frTc5tw;6%G+%CY74@v`atI5!enbcCO|k=Nc%5!T{;I7SS@R5X|qev~?`o+MPs)=)nMjTt>e$gt@5FFIZ3SR`q4=-i_Y{yUQD z@J~C=tnD(RZy_K8e{+@Ap}88;KzLkI&4M|yfeOLvC&3dsR@cH&Vq7nE{>sv)xdRsx zQ9IA&vNMrggKuc-xZP(7QnX4juDx_&zr->RWsYHjYiT;lG34}M#Fhe*gugO`Px}gwA@ucH75Zd}LPdP5JZ#rZU7{Gy(wM!~D3<{sy7+zfx1*4r~0mm4cOIxE9L+=g3cn3qV z#Mu*lXfSs`os#|XZJa^;(_(c=Jtv9%k1SQRAAtstd8jvQ991>}g`mJ<&`MO>`DAMI z90gx^zxbAB0Q;28B>|278`*Cr#zu#WKYU-DDRDl27NVphEcaATm*+jOG8RA&171zi z0-UNxE+CK_MBY0e0~R|1B9|KaOG@Eyb{l%JO4i%`?LaX|I-fyd%XCr)V& z2a?K&3zke>lsT=SjLDC17OSEzwMttZ@=;@1%k*{RdP!M5aJU&{Q!p%AYhD#DOkZAy z*C)wcJ-+qv8>4{gj&9%g=e36rELJtkLLEHD!VHWk%Y_Wt#`yphSTdG(LiW8+pMaL- zOkIjF!$-h!$P1aOuNCbbj;EMj8IC=mafW>HnLTxgwRf7Mh7>11#B!l!;EZ^%64dco zt<>oSmegI(3&^`a<{chqR6luYEmxf+O-2;)7N2jw0e; z1OIOV-#m-^Wq@j^U+{KTM)`P+q^bS62MhdXQxhX}C?o-(c_39P&oAVmjxpk?&{kR5 zI>!A;H(Gjjuy0YxCr|y-c*^+oRbPb;;YupZTkicQin7gt{!&T1T?2D$aBgwKYiHQL zpiaBK0@BxoAm&NN&?GDgrC9uR&RCpQ*y@{<0Ts~BYpAiu8R~i75vZ@eF>z*Osw@AQ zUW!A#_uG$UL>^H#|FFGD!Ej5VV`aj=J;h$G$@z~WCAMdO4D>nfrWpGz1xR@*g7X

9k27dWV-O32(_c9n|jZDb+xyFAFKUT$T?-nh!X; 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 literal 0 HcmV?d00001 diff --git a/test/lib/model/test_image_sscd.py b/test/lib/model/test_image_sscd.py new file mode 100644 index 0000000..1041aa7 --- /dev/null +++ b/test/lib/model/test_image_sscd.py @@ -0,0 +1,181 @@ +import io +import logging +import unittest +from unittest.mock import patch, Mock +from urllib.error import URLError +from typing import Dict + +from lib.model.image_sscd import Model +from lib import schemas + +result_should_be = [-0.07144027203321457, 0.0528595857322216, -0.11396506428718567, 0.0005233244737610221, + -0.04154925048351288, -0.028515873476862907, -0.058826882392168045, 0.011261329986155033, + 0.07468974590301514, 0.04327654838562012, 0.004409992601722479, 0.022582897916436195, + -0.06967438012361526, 0.06872913241386414, 0.0067594232968986034, -0.0035584503784775734, + -0.05984392389655113, 0.06857076287269592, -0.08085829019546509, -0.07211419194936752, + 0.037366725504398346, -0.024030650034546852, 0.10351148992776871, -0.06264083832502365, + 0.040350571274757385, 0.006679327692836523, -0.009951995685696602, 0.0161967221647501, + -0.008031048811972141, 0.009600456804037094, -0.024177879095077515, -0.0065284613519907, + 0.009804324246942997, -0.02729668840765953, 0.0068421075120568275, 0.07183070480823517, + 0.03873312473297119, -0.011135808192193508, 0.024297993630170822, 0.08706283569335938, + -0.011404428631067276, -0.022803163155913353, 0.014162120409309864, -0.043364185839891434, + -0.01327490247786045, -0.010093556717038155, -0.01574164815247059, -0.006964595057070255, + -0.0013869364047423005, -0.07584678381681442, 0.05398833006620407, -0.05736219510436058, + 0.04589315876364708, -0.021578991785645485, 0.019843051210045815, -0.0006519201560877264, + -0.03096906468272209, 0.04863806068897247, 0.008100304752588272, 0.008826173841953278, + 0.07164538651704788, -0.07601186633110046, -0.05091376230120659, -0.028265533968806267, + 0.0003991558332927525, 0.042388275265693665, 0.05119031295180321, -0.01984211802482605, + 0.029248200356960297, 0.033196162432432175, -0.030597658827900887, -0.03332706168293953, + 0.08688798546791077, -0.030620956793427467, -0.005795127712190151, 0.026939084753394127, + 0.04161391407251358, 0.02266402170062065, -0.04052147641777992, 0.012570186518132687, + 0.0005767169059254229, 0.07786484807729721, 0.0015619974583387375, 0.013637062162160873, + 0.05117057263851166, -0.02597726508975029, -0.033111896365880966, 0.0701746866106987, + -0.015584368258714676, 0.02364824339747429, 0.0027465904131531715, -0.04525766521692276, + -0.03272904083132744, 0.03058704361319542, 0.048695776611566544, -0.0582093670964241, + -0.0644807368516922, 0.02251230739057064, -0.0020564638543874025, -0.06945344060659409, + -0.01608332432806492, 0.012174352072179317, -0.0475899763405323, 0.028787409886717796, + 0.040730882436037064, 0.025461556389927864, 0.06789694726467133, 0.062188878655433655, + -0.08665324747562408, 0.030804479494690895, 0.0298762246966362, 0.06593651324510574, + 0.024233700707554817, -0.005684416741132736, -0.05876791477203369, 0.014895725063979626, + 0.012331650592386723, -0.08530636876821518, -0.021535653620958328, -0.005839891731739044, + 0.034899476915597916, 0.03595463186502457, 0.038640473037958145, -0.08569937944412231, + 0.01480958517640829, 0.016735870391130447, 0.025372425094246864, 0.03038204275071621, + 0.00031823653262108564, -0.0313514769077301, -0.12057473510503769, 0.05031529441475868, + 0.03725805878639221, 0.014069506898522377, 0.041856229305267334, 0.007315563969314098, + 0.038740646094083786, -0.0074793435633182526, -0.020819932222366333, 0.02455977164208889, + -0.08965035527944565, -0.081678107380867, 0.057668622583150864, 0.05913791060447693, + -0.006911333184689283, -0.048169031739234924, -0.0727081224322319, -0.10594800114631653, + 0.05663284659385681, -0.019163984805345535, -0.04517679288983345, 0.02395869605243206, + 0.041778430342674255, -0.09522789716720581, 0.009570611640810966, 0.030647773295640945, + 0.02276058867573738, -0.0376124233007431, -0.06889466941356659, -0.029664795845746994, + 0.06314549595117569, 0.030073754489421844, 0.0359380878508091, -0.021464575082063675, + 0.02590654045343399, 0.09144120663404465, -0.026760267093777657, 0.004901545587927103, + 0.03806091472506523, -0.02291925437748432, 0.011413888074457645, -0.01821034960448742, + 0.070054791867733, -0.01789053902029991, 0.02784998156130314, 0.038003288209438324, + -0.015555726364254951, -0.06261636316776276, 0.10744502395391464, 0.028204796835780144, + 0.039750680327415466, -0.006700362078845501, 0.0014031894970685244, -0.006510741543024778, + 0.010954192839562893, -0.0265719722956419, 0.04018286615610123, 0.03822746500372887, + 0.06522148102521896, 0.026165448129177094, -0.010680162347853184, 0.02104169689118862, + 0.039393555372953415, -0.054629307240247726, 0.052427515387535095, -0.05568528175354004, + -0.05142313614487648, 0.02597949653863907, 0.03633121773600578, -0.005130075383931398, + -0.019111020490527153, 0.014608109369874, 0.010372712276875973, -0.004220862407237291, + 0.00493678729981184, 0.062161609530448914, 0.019215654581785202, -0.03241828829050064, + -0.03072202019393444, 0.023111265152692795, -0.007216483820229769, 0.036560285836458206, + 0.01290745846927166, -0.07817694544792175, -0.013376968912780285, -0.05606372654438019, + 0.051513757556676865, -0.012899071909487247, -0.06157049909234047, -0.08024762570858002, + 0.04888973385095596, 0.01188365463167429, 0.009882175363600254, -0.019134674221277237, + 0.024625474587082863, -0.006905965507030487, -0.030667953193187714, 0.01298027578741312, + 0.018715037032961845, -0.026403281837701797, -0.00783504731953144, 0.004286203999072313, + 0.0010807081125676632, -0.00660742586478591, -0.023812497034668922, 0.06302206218242645, + -0.057830214500427246, -0.03886802867054939, 0.024021243676543236, -0.055729299783706665, + -0.10672368109226227, 0.030587900429964066, 0.028457045555114746, 0.04784972965717316, + 0.03260587900876999, 0.06271659582853317, -0.10934319347143173, 0.035900115966796875, + -0.04468424990773201, 0.07048524171113968, 0.05516570061445236, 0.03436368331313133, + 0.00997652392834425, 0.08526262640953064, -0.03130404278635979, 0.005957255605608225, + 0.06824888288974762, -0.036280177533626556, 0.06230608746409416, -0.021922865882515907, + -0.010930586606264114, 0.01367107778787613, -0.018288403749465942, 0.0489020049571991, + -0.04091630131006241, -0.030513420701026917, -0.020151210948824883, -0.023531029000878334, + -0.09813980013132095, -0.02391956001520157, -0.007604392245411873, 0.04859378933906555, + -0.00860443152487278, -0.05193145573139191, 0.015980644151568413, -0.0075520663522183895, + -0.008378726430237293, 9.938422590494156e-05, 0.03541550040245056, -0.04212876781821251, + 0.058003149926662445, -0.022017791867256165, 0.052691467106342316, 0.09577743709087372, + 0.02856585755944252, -4.2199459130642936e-05, 0.005996208172291517, -0.10965994000434875, + 0.02336238883435726, 0.02216327004134655, 0.04850497841835022, -0.005761938635259867, + 0.044221676886081696, -0.0089781004935503, -0.03918464109301567, 0.011156989261507988, + -0.030580462887883186, 0.09308770298957825, 0.025906618684530258, -0.029887555167078972, + 0.04069322720170021, -0.016283219680190086, -0.025910494849085808, 0.0012494147522374988, + 0.0031355945393443108, 0.015254073776304722, 0.060300517827272415, 0.0007453417056240141, + -0.021660279482603073, -0.06608810275793076, 0.008861289359629154, -0.00020786059030797333, + 0.017373064532876015, -0.002692391164600849, 0.026701727882027626, 0.04024803638458252, + -0.016312239691615105, 0.031316451728343964, 0.008049928583204746, 0.04411536455154419, + 0.07697759568691254, -0.043828073889017105, 0.016851941123604774, 0.06488212943077087, + -0.06394548714160919, -0.004850515630096197, -0.06142564117908478, -0.0007274787058122456, + -0.000708505162037909, -0.010618982836604118, 0.07141107320785522, -0.06414534151554108, + 0.09092477709054947, -0.015697801485657692, -0.12162954360246658, 0.01604246161878109, + -0.0338786318898201, 0.043666157871484756, 0.015039476566016674, 0.04161584749817848, + 0.024161431938409805, 0.0301087386906147, 0.003925960510969162, -0.06638433784246445, + -0.014072329737246037, 0.044508274644613266, 0.000865419686306268, 0.028618697077035904, + 0.007238695397973061, -0.025417087599635124, 0.030697954818606377, -0.026795875281095505, + 0.09330523759126663, -0.02380654215812683, 0.0354798324406147, 0.03782563656568527, + 0.015322495251893997, 0.017190296202898026, 0.01645561493933201, 0.07070869207382202, + 0.044643379747867584, -0.09219500422477722, -0.027271369472146034, 0.021323859691619873, + 0.019855061545968056, -0.08344972878694534, 0.02964773029088974, 0.022371770814061165, + 0.010329704731702805, 0.0204318817704916, 0.013014006428420544, -0.04265967756509781, + 0.0034814556129276752, 0.005503968335688114, -0.011838463135063648, -0.09120689332485199, + 0.00863907765597105, -0.016315268352627754, -0.053035762161016464, -0.018841488286852837, + 0.029966143891215324, -0.007713795639574528, 0.015808969736099243, -0.0423244871199131, + 0.006674240343272686, 0.05231969803571701, -0.02070920541882515, 0.06744643300771713, + 0.11502596735954285, -0.03222789987921715, 0.025131283327937126, 0.12610262632369995, + -0.008005252107977867, 0.08683951944112778, -0.016409732401371002, -0.05496629700064659, + -0.018000662326812744, 0.059494391083717346, -0.033545736223459244, 0.009159487672150135, + -0.014886717312037945, -0.03672671318054199, -0.0382157601416111, -0.07774834334850311, + 0.04587544873356819, 0.013530625030398369, 0.03489874303340912, -0.01647481694817543, + -0.018000798299908638, 0.03171844035387039, -0.040251001715660095, -0.04073946923017502, + 0.06595395505428314, -0.03739870339632034, 0.014373987913131714, -0.035246916115283966, + -0.0062555354088544846, 0.03349122777581215, -0.02458479069173336, -0.01668730564415455, + 0.11991753429174423, -0.12086635082960129, -0.005912070628255606, 0.01163905207067728, + 0.036334726959466934, -0.010278688743710518, -0.04652582108974457, -0.0005802358500659466, + -0.08215046674013138, 0.07692704349756241, -0.005483880639076233, 0.019933341071009636, + 0.0025862606707960367, 0.020065804943442345, -0.00863591581583023, 0.07357700914144516, + 0.004476140718907118, -0.012785458005964756, -0.03159927949309349, -0.046191293746232986, + -0.002030089497566223, 0.03555852919816971, -0.024284178391098976, 0.04600340500473976, + 0.03603901341557503, 0.0018025911413133144, -0.13817547261714935, 0.05801040306687355, + 0.03214746713638306, -0.09996572881937027, -0.018432531505823135, -0.05093832314014435, + -0.03224610909819603, -0.01639450527727604, -0.010061380453407764, 0.055063556879758835, + -0.08065234124660492, -0.007862106896936893, -0.021783823147416115, -0.052751604467630386, + -0.0691704973578453, 0.04986613243818283, -0.015607778914272785, 0.03232172876596451, + 0.02679631859064102, 0.0036959717981517315, -0.030126234516501427, -0.02326115220785141, + -0.006363472435623407, 0.015963030979037285, 0.00330563192255795, -0.024462511762976646, + 0.01190184149891138, 0.003605007426813245, -0.050289880484342575, 0.0266175027936697, + 0.05349498614668846, -0.008247151039540768, -0.06670569628477097, 0.06351006031036377, + 0.08114060759544373, -0.006813893094658852, 0.024257315322756767, -0.03206257149577141, + 0.010035752318799496, -0.06654676049947739, 0.012111213989555836, -0.056731320917606354, + 0.024231307208538055, -0.024802042171359062, 0.02894349955022335, 0.08170276880264282, + 0.011898127384483814, 0.006088235415518284, -0.03856944665312767, 0.09566926956176758, + 0.005668847355991602, -0.010908310301601887, -0.044969141483306885, -0.0032908024732023478, + 0.016451604664325714, 0.030626192688941956, 0.027100708335638046, -0.004937123507261276, + -0.01188697014003992, 0.0018644103547558188, 0.028558410704135895, -0.05996338650584221, + -0.033904775977134705, 0.00781658198684454, 0.005846137646585703, 0.022124793380498886] + +class TestModel(unittest.TestCase): + + @patch("torchvision.transforms") + def test_compute_sscd(self, mock_pdq_hasher): + with open("img/presto_flowchart.jpg", "rb") as file: + image_content = file.read() + # mock_hasher_instance = mock_pdq_hasher.return_value + # mock_hasher_instance.fromBufferedImage.return_value.getHash.return_value.dumpBitsFlat.return_value = '1001' + result = Model().compute_sscd(io.BytesIO(image_content)) + self.assertEqual(result, result_should_be) + + @patch("urllib.request.urlopen") + def test_get_iobytes_for_image(self, mock_urlopen): + with open("img/presto_flowchart.jpg", "rb") as file: + image_content = file.read() + mock_response = Mock() + mock_response.read.return_value = image_content + mock_urlopen.return_value = mock_response + image = schemas.Message(body={"id": "123", "callback_url": "http://example.com?callback", "url": "http://example.com/image.jpg"}, model_name="audio__Model") + result = Model().get_iobytes_for_image(image) + self.assertIsInstance(result, io.BytesIO) + self.assertEqual(result.read(), image_content) + + @patch("urllib.request.urlopen") + def test_get_iobytes_for_image_raises_error(self, mock_urlopen): + mock_urlopen.side_effect = URLError('test error') + image = schemas.Message(body={"id": "123", "callback_url": "http://example.com?callback", "url": "http://example.com/image.jpg"}, model_name="audio__Model") + with self.assertRaises(URLError): + Model().get_iobytes_for_image(image) + + @patch.object(Model, "get_iobytes_for_image") + @patch.object(Model, "compute_sscd") + def test_process(self, mock_compute_pdq, mock_get_iobytes_for_image): + mock_compute_pdq.return_value = result_should_be + mock_get_iobytes_for_image.return_value = io.BytesIO(b"image_bytes") + image = schemas.Message(body={"id": "123", "callback_url": "http://example.com?callback", "url": "http://example.com/image.jpg"}, model_name="audio__Model") + result = Model().process(image) + self.assertEqual(result, result_should_be) + + +if __name__ == "__main__": + unittest.main() From d3dc05fa239b99bacb975f6c5f34db873ce6f980 Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Mon, 20 Nov 2023 15:13:03 +0000 Subject: [PATCH 23/25] Use numpy.allclose to accomodate OS/chipset differences --- test/lib/model/test_image_sscd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lib/model/test_image_sscd.py b/test/lib/model/test_image_sscd.py index 1041aa7..8a39f38 100644 --- a/test/lib/model/test_image_sscd.py +++ b/test/lib/model/test_image_sscd.py @@ -7,6 +7,7 @@ from lib.model.image_sscd import Model from lib import schemas +import numpy as np result_should_be = [-0.07144027203321457, 0.0528595857322216, -0.11396506428718567, 0.0005233244737610221, -0.04154925048351288, -0.028515873476862907, -0.058826882392168045, 0.011261329986155033, @@ -146,7 +147,7 @@ def test_compute_sscd(self, mock_pdq_hasher): # mock_hasher_instance = mock_pdq_hasher.return_value # mock_hasher_instance.fromBufferedImage.return_value.getHash.return_value.dumpBitsFlat.return_value = '1001' result = Model().compute_sscd(io.BytesIO(image_content)) - self.assertEqual(result, result_should_be) + self.assertTrue(np.allclose(result, result_should_be)) @patch("urllib.request.urlopen") def test_get_iobytes_for_image(self, mock_urlopen): From cd191d8fdc9c811fdd93058c7d649a41ebe052f6 Mon Sep 17 00:00:00 2001 From: computermacgyver Date: Wed, 29 Nov 2023 22:23:38 +0000 Subject: [PATCH 24/25] fix test --- test/lib/model/test_image_sscd.py | 40 ++++++------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/test/lib/model/test_image_sscd.py b/test/lib/model/test_image_sscd.py index 8a39f38..c98aa79 100644 --- a/test/lib/model/test_image_sscd.py +++ b/test/lib/model/test_image_sscd.py @@ -140,42 +140,16 @@ class TestModel(unittest.TestCase): - @patch("torchvision.transforms") - def test_compute_sscd(self, mock_pdq_hasher): + def test_compute_sscd(self): with open("img/presto_flowchart.jpg", "rb") as file: image_content = file.read() - # mock_hasher_instance = mock_pdq_hasher.return_value - # mock_hasher_instance.fromBufferedImage.return_value.getHash.return_value.dumpBitsFlat.return_value = '1001' result = Model().compute_sscd(io.BytesIO(image_content)) - self.assertTrue(np.allclose(result, result_should_be)) - - @patch("urllib.request.urlopen") - def test_get_iobytes_for_image(self, mock_urlopen): - with open("img/presto_flowchart.jpg", "rb") as file: - image_content = file.read() - mock_response = Mock() - mock_response.read.return_value = image_content - mock_urlopen.return_value = mock_response - image = schemas.Message(body={"id": "123", "callback_url": "http://example.com?callback", "url": "http://example.com/image.jpg"}, model_name="audio__Model") - result = Model().get_iobytes_for_image(image) - self.assertIsInstance(result, io.BytesIO) - self.assertEqual(result.read(), image_content) - - @patch("urllib.request.urlopen") - def test_get_iobytes_for_image_raises_error(self, mock_urlopen): - mock_urlopen.side_effect = URLError('test error') - image = schemas.Message(body={"id": "123", "callback_url": "http://example.com?callback", "url": "http://example.com/image.jpg"}, model_name="audio__Model") - with self.assertRaises(URLError): - Model().get_iobytes_for_image(image) - - @patch.object(Model, "get_iobytes_for_image") - @patch.object(Model, "compute_sscd") - def test_process(self, mock_compute_pdq, mock_get_iobytes_for_image): - mock_compute_pdq.return_value = result_should_be - mock_get_iobytes_for_image.return_value = io.BytesIO(b"image_bytes") - image = schemas.Message(body={"id": "123", "callback_url": "http://example.com?callback", "url": "http://example.com/image.jpg"}, model_name="audio__Model") - result = Model().process(image) - self.assertEqual(result, result_should_be) + # The least significant digits differ between chipsets (arm64 and amd64) + # There fore we do not use self.assertEqual + # self.assertEqual(result, result_should_be) + # Instead, we assert that all the values are with an absolute tolerance + # given by atol in the following assertion. + self.assertTrue(np.allclose(result, result_should_be, rtol=0, atol=0.00001)) if __name__ == "__main__": From 5697bee9547f6ddd05617172b2399fec4e8aee8e Mon Sep 17 00:00:00 2001 From: ahmednasserswe Date: Thu, 30 Nov 2023 00:44:05 +0100 Subject: [PATCH 25/25] Update image_sscd.py with comments describing of normalization and resizing of the image --- lib/model/image_sscd.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/model/image_sscd.py b/lib/model/image_sscd.py index 47b3966..20a1959 100644 --- a/lib/model/image_sscd.py +++ b/lib/model/image_sscd.py @@ -29,19 +29,24 @@ def compute_sscd(self, iobytes: io.BytesIO) -> str: :param im: Numpy.ndarray #FIXME :returns: Imagehash.ImageHash #FIXME """ + # from SSCD-copy-detection readme https://github.com/facebookresearch/sscd-copy-detection/tree/main#preprocessing + # Normalization using the mean and std of Imagenet normalize = transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], ) + # It is recommended by publishers of SSCD-copy-detection to preprocess images for inference either resizing the small edge to 288 or resizing the image to a square tensor. + # resizing the image to a square tensor is more effecient on gpus but can lead to skewed images and so loss of information. So, we are resizing the small edge to 288 small_288 = transforms.Compose([ transforms.Resize(288), transforms.ToTensor(), normalize, ]) - skew_320 = transforms.Compose([ - transforms.Resize([320, 320]), - transforms.ToTensor(), - normalize, - ]) + # Keeping the code example of resizing the image to a square tensor + # skew_320 = transforms.Compose([ + # transforms.Resize([320, 320]), + # transforms.ToTensor(), + # normalize, + # ]) image = Image.open(iobytes) batch = small_288(image).unsqueeze(0)