Skip to content

Commit

Permalink
feat(functions/v2): add imagemagick sample (#7880)
Browse files Browse the repository at this point in the history
* feat(functions/v2): add imagemagick sample

* Header check

* HC

* HC 2

* Fix lint

* Add requirements-test.txt

* Move to requirements-test.txt

* Delete requirements-dev.txt

* Use pytest for all Python versions

* Update functions/v2/imagemagick/README.md

Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com>

* Add zombie image

* Region tag prefix: functions -> cloudfunctions

* Revert cloudfunctions_ region tag change

* Address comments

Co-authored-by: Jennifer Davis <iennae@gmail.com>
Co-authored-by: Dan Lee <71398022+dandhlee@users.noreply.github.com>
  • Loading branch information
3 people committed May 13, 2022
1 parent ca9db6f commit cc69155
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 0 deletions.
54 changes: 54 additions & 0 deletions functions/v2/imagemagick/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<img src="https://avatars2.githubusercontent.com/u/2810941?v=3&s=96" alt="Google Cloud Platform logo" title="Google Cloud Platform" align="right" height="96" width="96"/>

# Google Cloud Functions ImageMagick sample

This sample shows you how to blur an image using ImageMagick in a
Storage-triggered Cloud Function.

View the [source code][code].

[code]: main.py

## Deploy and Test

1. Follow the [Cloud Functions quickstart guide][quickstart] to setup Cloud
Functions for your project.

1. Clone this repository:

git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
cd python-docs-samples/functions/v2/imagemagick

1. Create a Cloud Storage Bucket:

gsutil mb gs://YOUR_INPUT_BUCKET_NAME

This storage bucket is used to upload images for the function to check.

1. Create a second Cloud Storage Bucket:

gsutil mb gs://YOUR_OUTPUT_BUCKET_NAME

This second storage bucket is used to store blurred images. (Images that are **not** blurred will not be saved to this bucket.)

The second bucket is necessary because saving the blurred image to the first (input) bucket would cause your function to be invoked a second time with the blurred image itself.

1. Deploy the `blur_offensive_images` function with a Storage trigger:

gcloud functions deploy blur_offensive_images --trigger-bucket=YOUR_INPUT_BUCKET_NAME --set-env-vars BLURRED_BUCKET_NAME=YOUR_OUTPUT_BUCKET_NAME --runtime python39 --gen2

* Replace `YOUR_INPUT_BUCKET_NAME` and `YOUR_OUTPUT_BUCKET_NAME` with the names of the respective Cloud Storage Buckets you created earlier.

1. Upload an offensive image to the Storage bucket, such as this image of
a flesh-eating zombie: https://cdn.pixabay.com/photo/2015/09/21/14/24/zombie-949916_1280.jpg

1. Check the logs for the `blur_offensive_images` function in the [Cloud Console][console]

You should see something like this in your console:

D ... User function triggered, starting execution
I ... `The image zombie.jpg has been detected as inappropriate.`
D ... Execution took 1 ms, user function completed successfully

[quickstart]: https://cloud.google.com/functions/docs/2nd-gen/console-quickstart
[console]: https://console.cloud.google.com/logs/query;query=resource.type%3D%22cloud_run_revision
94 changes: 94 additions & 0 deletions functions/v2/imagemagick/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


# [START functions_imagemagick_setup]
import os
import tempfile

import functions_framework
from google.cloud import storage, vision
from wand.image import Image

storage_client = storage.Client()
vision_client = vision.ImageAnnotatorClient()
# [END functions_imagemagick_setup]


# [START functions_imagemagick_analyze]
# Blurs uploaded images that are flagged as Adult or Violent imagery.
@functions_framework.cloud_event
def blur_offensive_images(cloud_event):
file_data = cloud_event.data

file_name = file_data["name"]
bucket_name = file_data["bucket"]

blob = storage_client.bucket(bucket_name).get_blob(file_name)
blob_uri = f"gs://{bucket_name}/{file_name}"
blob_source = vision.Image(source=vision.ImageSource(gcs_image_uri=blob_uri))

# Ignore already-blurred files
if file_name.startswith("blurred-"):
print(f"The image {file_name} is already blurred.")
return

print(f"Analyzing {file_name}.")

result = vision_client.safe_search_detection(image=blob_source)
detected = result.safe_search_annotation

# Process image
# 5 maps to VERY_LIKELY
if detected.adult == 5 or detected.violence == 5:
print(f"The image {file_name} was detected as inappropriate.")
return __blur_image(blob)
else:
print(f"The image {file_name} was detected as OK.")


# [END functions_imagemagick_analyze]


# [START functions_imagemagick_blur]
# Blurs the given file using ImageMagick.
def __blur_image(current_blob):
file_name = current_blob.name
_, temp_local_filename = tempfile.mkstemp()

# Download file from bucket.
current_blob.download_to_filename(temp_local_filename)
print(f"Image {file_name} was downloaded to {temp_local_filename}.")

# Blur the image using ImageMagick.
with Image(filename=temp_local_filename) as image:
image.resize(*image.size, blur=16, filter="hamming")
image.save(filename=temp_local_filename)

print(f"Image {file_name} was blurred.")

# Upload result to a second bucket, to avoid re-triggering the function.
# You could instead re-upload it to the same bucket + tell your function
# to ignore files marked as blurred (e.g. those with a "blurred" prefix)
blur_bucket_name = os.getenv("BLURRED_BUCKET_NAME")
blur_bucket = storage_client.bucket(blur_bucket_name)
new_blob = blur_bucket.blob(file_name)
new_blob.upload_from_filename(temp_local_filename)
print(f"Blurred image uploaded to: gs://{blur_bucket_name}/{file_name}")

# Delete the temporary file.
os.remove(temp_local_filename)


# [END functions_imagemagick_blur]
114 changes: 114 additions & 0 deletions functions/v2/imagemagick/main_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import UserDict
import uuid

from mock import MagicMock, patch

import main


@patch('main.__blur_image')
@patch('main.vision_client')
@patch('main.storage_client')
def test_process_offensive_image(
storage_client,
vision_client,
__blur_image,
capsys):
result = UserDict()
result.safe_search_annotation = UserDict()
result.safe_search_annotation.adult = 5
result.safe_search_annotation.violence = 5
vision_client.safe_search_detection = MagicMock(return_value=result)

filename = str(uuid.uuid4())

event = MagicMock()
event.data = {
'bucket': 'my-bucket',
'name': filename
}

main.blur_offensive_images(event)

out, _ = capsys.readouterr()
assert 'Analyzing %s.' % filename in out
assert 'The image %s was detected as inappropriate.' % filename in out
assert main.__blur_image.called


@patch('main.__blur_image')
@patch('main.vision_client')
@patch('main.storage_client')
def test_process_safe_image(
storage_client,
vision_client,
__blur_image,
capsys):
result = UserDict()
result.safe_search_annotation = UserDict()
result.safe_search_annotation.adult = 1
result.safe_search_annotation.violence = 1
vision_client.safe_search_detection = MagicMock(return_value=result)

filename = str(uuid.uuid4())

event = MagicMock()
event.data = {
'bucket': 'my-bucket',
'name': filename
}

main.blur_offensive_images(event)

out, _ = capsys.readouterr()

assert 'Analyzing %s.' % filename in out
assert 'The image %s was detected as OK.' % filename in out
assert __blur_image.called is False


@patch('main.os')
@patch('main.Image')
@patch('main.storage_client')
def test_blur_image(storage_client, image_mock, os_mock, capsys):
filename = str(uuid.uuid4())
blur_bucket = 'blurred-bucket-' + str(uuid.uuid4())

os_mock.remove = MagicMock()
os_mock.path = MagicMock()
os_mock.path.basename = MagicMock(side_effect=(lambda x: x))

os_mock.getenv = MagicMock(return_value=blur_bucket)

image_mock.return_value = image_mock
image_mock.__enter__.return_value = image_mock

blob = UserDict()
blob.name = filename
blob.bucket = UserDict()
blob.download_to_filename = MagicMock()
blob.upload_from_filename = MagicMock()

main.__blur_image(blob)

out, _ = capsys.readouterr()

assert f'Image {filename} was downloaded to' in out
assert f'Image {filename} was blurred.' in out
assert f'Blurred image uploaded to: gs://{blur_bucket}/{filename}' in out
assert os_mock.remove.called
assert image_mock.resize.called
4 changes: 4 additions & 0 deletions functions/v2/imagemagick/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mock==4.0.3
six==1.12.0
uuid==1.30
pytest==7.0.1
5 changes: 5 additions & 0 deletions functions/v2/imagemagick/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
functions-framework==3.0.0
google-cloud-vision==2.7.2
google-cloud-storage==2.0.0; python_version < '3.7'
google-cloud-storage==2.1.0; python_version > '3.6'
Wand==0.6.7
Binary file added functions/v2/imagemagick/zombie.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit cc69155

Please sign in to comment.