From 25aedb681baa0d6b4b9d44e6f830cedf9feaf328 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:23:53 +0900 Subject: [PATCH 01/11] Support using PIL Image objects as inputs --- README.md | 19 ++++++++++--------- opennsfw2/_inference.py | 34 ++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 4e6313f..59cfbf7 100644 --- a/README.md +++ b/README.md @@ -62,20 +62,20 @@ For more details, please refer to the [API](#api) section. import opennsfw2 as n2 # To get the NSFW probability of a single image. -image_path = "path/to/your/image.jpg" +image_handle = "path/to/your/image.jpg" # Alternatively, a `PIL.Image.Image` object. -nsfw_probability = n2.predict_image(image_path) +nsfw_probability = n2.predict_image(image_handle) # To get the NSFW probabilities of a list of images. # This is better than looping with `predict_image` as the model will only be instantiated once # and batching is used during inference. -image_paths = [ - "path/to/your/image1.jpg", +image_handles = [ + "path/to/your/image1.jpg", # Alternatively, a list of `PIL.Image.Image` objects. "path/to/your/image2.jpg", # ... ] -nsfw_probabilities = n2.predict_images(image_paths) +nsfw_probabilities = n2.predict_images(image_handles) ``` ## Video @@ -154,8 +154,8 @@ Create an instance of the NSFW model, optionally with pre-trained weights from Y ### `predict_image` End-to-end pipeline function from the input image to the predicted NSFW probability. - Parameters: - - `image_path` (`str`): Path to the input image file. - The image format must be supported by Pillow. + - `image_handle` (`Union[str, PIL.Image.Image]`): + Path to the input image file with a format supported by Pillow, or a `PIL.Image.Image` object. - `preprocessing`: Same as that in `preprocess_image`. - `weights_path`: Same as that in `make_open_nsfw_model`. - `grad_cam_path` (`Optional[str]`, default `None`): If not `None`, e.g., `cam.jpg`, @@ -171,8 +171,9 @@ End-to-end pipeline function from the input image to the predicted NSFW probabil ### `predict_images` End-to-end pipeline function from the input images to the predicted NSFW probabilities. - Parameters: - - `image_paths` (`Sequence[str]`): List of paths to the input image files. - The image format must be supported by Pillow. + - `image_handles` (`Union[Sequence[str], Sequence[PIL.Image.Image]]`): + List of paths to the input image files with formats supported by Pillow, + or list of `PIL.Image.Image` objects. - `batch_size` (`int`, default `8`): Batch size to be used for model inference. Choose a value that works the best with your device resources. - `preprocessing`: Same as that in `preprocess_image`. diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index 0d3c187..7a4a34f 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -28,18 +28,24 @@ def _update_global_model_if_needed(weights_path: Optional[str]) -> None: global_model_path = weights_path +def _load_pil_image(image_handle: str | Image.Image) -> Image.Image: + if isinstance(image_handle, Image.Image): + return image_handle + return Image.open(image_handle) + + def predict_image( - image_path: str, + image_handle: str, preprocessing: Preprocessing = Preprocessing.YAHOO, weights_path: Optional[str] = get_default_weights_path(), grad_cam_path: Optional[str] = None, alpha: float = 0.8 ) -> float: """ - Pipeline from single image path to predicted NSFW probability. + Pipeline from single image handle to predicted NSFW probability. Optionally generate and save the Grad-CAM plot. """ - pil_image = Image.open(image_path) + pil_image = _load_pil_image(image_handle) image = preprocess_image(pil_image, preprocessing) _update_global_model_if_needed(weights_path) assert global_model is not None @@ -56,9 +62,9 @@ def predict_image( return nsfw_probability -def _predict_from_image_paths_in_batches( +def _predict_from_image_handles_in_batches( model_: Model, - image_paths: Sequence[str], + image_handles: Sequence[str], batch_size: int, preprocessing: Preprocessing ) -> NDFloat32Array: @@ -68,10 +74,10 @@ def _predict_from_image_paths_in_batches( https://keras.io/api/models/model_training_apis/#predict-method """ prediction_batches: List[Any] = [] - for i in range(0, len(image_paths), batch_size): - path_batch = image_paths[i: i + batch_size] + for i in range(0, len(image_handles), batch_size): + path_batch = image_handles[i: i + batch_size] image_batch = [ - preprocess_image(Image.open(path), preprocessing) + preprocess_image(_load_pil_image(path), preprocessing) for path in path_batch ] prediction_batches.append(model_(np.array(image_batch))) @@ -80,7 +86,7 @@ def _predict_from_image_paths_in_batches( def predict_images( - image_paths: Sequence[str], + image_handles: Sequence[str], batch_size: int = 8, preprocessing: Preprocessing = Preprocessing.YAHOO, weights_path: Optional[str] = get_default_weights_path(), @@ -88,12 +94,12 @@ def predict_images( alpha: float = 0.8 ) -> List[float]: """ - Pipeline from image paths to predicted NSFW probabilities. + Pipeline from image handles to predicted NSFW probabilities. Optionally generate and save the Grad-CAM plots. """ _update_global_model_if_needed(weights_path) - predictions = _predict_from_image_paths_in_batches( - global_model, image_paths, batch_size, preprocessing + predictions = _predict_from_image_handles_in_batches( + global_model, image_handles, batch_size, preprocessing ) nsfw_probabilities: List[float] = predictions[:, 1].tolist() @@ -101,9 +107,9 @@ def predict_images( # TensorFlow will only be imported here. from ._inspection import make_and_save_nsfw_grad_cam - for image_path, grad_cam_path in zip(image_paths, grad_cam_paths): + for image_handle, grad_cam_path in zip(image_handles, grad_cam_paths): make_and_save_nsfw_grad_cam( - Image.open(image_path), preprocessing, global_model, + _load_pil_image(image_handle), preprocessing, global_model, grad_cam_path, alpha ) From 6f0d21ac1d412d9d4d63ec9519750a210c2f8194 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:25:47 +0900 Subject: [PATCH 02/11] Fix var names --- opennsfw2/_inference.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index 7a4a34f..646666f 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -75,10 +75,10 @@ def _predict_from_image_handles_in_batches( """ prediction_batches: List[Any] = [] for i in range(0, len(image_handles), batch_size): - path_batch = image_handles[i: i + batch_size] + handle_batch = image_handles[i: i + batch_size] image_batch = [ - preprocess_image(_load_pil_image(path), preprocessing) - for path in path_batch + preprocess_image(_load_pil_image(handle), preprocessing) + for handle in handle_batch ] prediction_batches.append(model_(np.array(image_batch))) predictions: NDFloat32Array = np.concatenate(prediction_batches, axis=0) From 7c7aa1b2cd110f6a58071c95d9688d54e9e6b1a8 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:39:03 +0900 Subject: [PATCH 03/11] Use Union type annotation for py3.9 compatibility --- opennsfw2/_inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index 646666f..241bd64 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -2,7 +2,7 @@ Inference utilities. """ from enum import auto, Enum -from typing import Any, Callable, List, Optional, Sequence, Tuple +from typing import Any, Callable, List, Optional, Sequence, Tuple, Union import cv2 import numpy as np @@ -28,7 +28,7 @@ def _update_global_model_if_needed(weights_path: Optional[str]) -> None: global_model_path = weights_path -def _load_pil_image(image_handle: str | Image.Image) -> Image.Image: +def _load_pil_image(image_handle: Union[str, Image.Image]) -> Image.Image: if isinstance(image_handle, Image.Image): return image_handle return Image.open(image_handle) From 3e08be541e2a1ca72c8f9551077554db52a015bc Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 07:56:32 +0900 Subject: [PATCH 04/11] Fix type annotations --- opennsfw2/_inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index 241bd64..bd69e73 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -35,7 +35,7 @@ def _load_pil_image(image_handle: Union[str, Image.Image]) -> Image.Image: def predict_image( - image_handle: str, + image_handle: Union[str, Image.Image], preprocessing: Preprocessing = Preprocessing.YAHOO, weights_path: Optional[str] = get_default_weights_path(), grad_cam_path: Optional[str] = None, @@ -64,7 +64,7 @@ def predict_image( def _predict_from_image_handles_in_batches( model_: Model, - image_handles: Sequence[str], + image_handles: Union[Sequence[str], Sequence[Image.Image]], batch_size: int, preprocessing: Preprocessing ) -> NDFloat32Array: From d85edb27415875d3dc08a1eb3e02d6787885a209 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 07:58:53 +0900 Subject: [PATCH 05/11] Add test for PIL Image input --- tests/_test.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/_test.py b/tests/_test.py index cc3b7fc..7909634 100644 --- a/tests/_test.py +++ b/tests/_test.py @@ -6,6 +6,7 @@ from typing import Optional, Sequence from keras import backend as keras_backend +from PIL import Image import opennsfw2 as n2 @@ -61,9 +62,14 @@ def test_predict_images_simple_preprocessing(self) -> None: self._assert(expected_probabilities, predicted_probabilities) def test_predict_image(self) -> None: + # From path. self.assertAlmostEqual( 0.983, n2.predict_image(IMAGE_PATHS[1]), places=3 ) + # From PIL Image. + self.assertAlmostEqual( + 0.983, n2.predict_image(Image.open(IMAGE_PATHS[1])), places=3 + ) def test_predict_video_frames(self) -> None: elapsed_seconds, nsfw_probabilities = n2.predict_video_frames( From 3b9f495d147fa0c5225034a039a4b324aa8b8901 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 08:03:24 +0900 Subject: [PATCH 06/11] Fix type annotation --- opennsfw2/_inference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index bd69e73..745e555 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -86,7 +86,7 @@ def _predict_from_image_handles_in_batches( def predict_images( - image_handles: Sequence[str], + image_handles: Union[Sequence[str], Sequence[Image.Image]], batch_size: int = 8, preprocessing: Preprocessing = Preprocessing.YAHOO, weights_path: Optional[str] = get_default_weights_path(), From 6f71a3abb7dc26bd41c9c555eae31048caaec6c3 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 08:20:44 +0900 Subject: [PATCH 07/11] Small rearrangement --- opennsfw2/_inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index 745e555..97cdb9b 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -108,9 +108,9 @@ def predict_images( from ._inspection import make_and_save_nsfw_grad_cam for image_handle, grad_cam_path in zip(image_handles, grad_cam_paths): + pil_image = _load_pil_image(image_handle) make_and_save_nsfw_grad_cam( - _load_pil_image(image_handle), preprocessing, global_model, - grad_cam_path, alpha + pil_image, preprocessing, global_model, grad_cam_path, alpha ) return nsfw_probabilities From 0e7e7698fecefcb2366b7382e4b471606c8c5b26 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 08:26:58 +0900 Subject: [PATCH 08/11] Add extra type assertion for mypy --- opennsfw2/_inference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opennsfw2/_inference.py b/opennsfw2/_inference.py index 97cdb9b..1e1652d 100644 --- a/opennsfw2/_inference.py +++ b/opennsfw2/_inference.py @@ -108,6 +108,7 @@ def predict_images( from ._inspection import make_and_save_nsfw_grad_cam for image_handle, grad_cam_path in zip(image_handles, grad_cam_paths): + assert isinstance(image_handle, (str, Image.Image)) # For mypy. pil_image = _load_pil_image(image_handle) make_and_save_nsfw_grad_cam( pil_image, preprocessing, global_model, grad_cam_path, alpha From e5f7b41ac26c6b4cfc6af8d2eee877cfec1cac2d Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 18:35:45 +0900 Subject: [PATCH 09/11] Improve README description --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 59cfbf7..1d0a76b 100644 --- a/README.md +++ b/README.md @@ -61,16 +61,18 @@ For more details, please refer to the [API](#api) section. ```python import opennsfw2 as n2 -# To get the NSFW probability of a single image. -image_handle = "path/to/your/image.jpg" # Alternatively, a `PIL.Image.Image` object. +# To get the NSFW probability of a single image, provide your image file path, +# or a `PIL.Image.Image` object. +image_handle = "path/to/your/image.jpg" nsfw_probability = n2.predict_image(image_handle) -# To get the NSFW probabilities of a list of images. -# This is better than looping with `predict_image` as the model will only be instantiated once -# and batching is used during inference. +# To get the NSFW probabilities of a list of images, provide a list of file paths, +# or a list of `PIL.Image.Image` objects. +# Using this function is better than looping with `predict_image` as the model +# will only be instantiated once and batching is done during inference. image_handles = [ - "path/to/your/image1.jpg", # Alternatively, a list of `PIL.Image.Image` objects. + "path/to/your/image1.jpg", "path/to/your/image2.jpg", # ... ] From f20fa7c358e27b7974826e4e9d48121979b8ba75 Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 18:38:13 +0900 Subject: [PATCH 10/11] Add support Py3.12 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 586cc83..1981786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: [ "3.9", "3.10", "3.11" ] + python: [ "3.9", "3.10", "3.11", "3.12" ] backend: [ tensorflow, jax ] include: - backend: tensorflow From 4cd597cfdac2fdccc6aec3cf32c269c725e081ac Mon Sep 17 00:00:00 2001 From: Bosco Yung <15840328+bhky@users.noreply.github.com> Date: Sun, 18 Aug 2024 18:53:27 +0900 Subject: [PATCH 11/11] Exclude __init__.py in test --- tests/run_code_checks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run_code_checks.sh b/tests/run_code_checks.sh index 64c59c2..cd5328e 100755 --- a/tests/run_code_checks.sh +++ b/tests/run_code_checks.sh @@ -4,4 +4,4 @@ set -e find opennsfw2 -iname "*.py" | grep -v -e "__init__.py" | xargs -L 1 pylint --errors-only find opennsfw2 -iname "*.py" | grep -v -e "__init__.py" | xargs -L 1 pylint --exit-zero find opennsfw2 -iname "*.py" | grep -v -e "__init__.py" | xargs -L 1 mypy --strict --implicit-reexport --disable-error-code attr-defined --disable-error-code unused-ignore -find tests -iname "*.py" | xargs -L 1 python3 -m unittest \ No newline at end of file +find tests -iname "*.py" | grep -v -e "__init__.py" | xargs -L 1 python3 -m unittest \ No newline at end of file