[
  {
    "path": ".flake8",
    "content": "[flake8]\nmax-line-length = 119\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__\n/weights"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "README.md",
    "content": "___\n\n⚠️  **ARCHIVED REPOSITORY** ⚠️   \n\n**We decided to archive this repository to make it read-only and indicate that it's no longer actively maintained.**\n___\n\n# understand.ai Anonymizer [ARCHIVED]\n\nTo improve privacy and make it easier for companies to comply with GDPR, we at [understand.ai](https://understand.ai/) decided to open-source our anonymization software and weights for a model trained on our in-house datasets for faces and license plates.\nThe model is trained with the [Tensorflow Object Detection API](https://github.com/tensorflow/models/tree/master/research/object_detection) to make it easy for everyone to use these weights in their projects.\n\nOur anonymizer is used for projects with some of Germany's largest car manufacturers and suppliers,\nbut we are sure there are many more applications.  \n\n## Disclaimer\n\nPlease note that the version here is not identical to the anonymizer we use in customer projects. This model is an early version in terms of quality and speed. The code is written for easy-of-use instead of speed.  \nFor this reason, no multiprocessing code or batched detection and blurring are used in this repository.\n\nThis version of our anonymizer is trained to detect faces and license plates in images recorded with sensors \ntypically used in autonomous vehicles. It will not work on low-quality or grayscale images and will also not work on \nfish-eye or other extreme camera configurations.\n\n\n## Examples\n\n![License Plate Example Raw](images/coco02.jpg?raw=true \"Title\")\n![License Plate Anonymized](images/coco02_anonymized.jpg?raw=true \"Title\")\n\n![Face Example Raw](images/coco01.jpg?raw=true \"Title\")\n![Face Example Anonymized](images/coco01_anonymized.jpg?raw=true \"Title\")\n\n\n## Installation\n\nTo install the anonymizer just clone this repository, create a new python3.6 environment and install the dependencies.  \nThe sequence of commands to do all this is\n\n```bash\npython -m venv ~/.virtualenvs/anonymizer\nsource ~/.virtualenvs/anonymizer/bin/activate\n\ngit clone https://github.com/understand-ai/anonymizer\ncd anonymizer\n\npip install --upgrade pip\npip install -r requirements.txt\n```\n\nTo make sure everything is working as intended run the test suite with the following command\n\n```bash\npytest\n```\n\nRunning the test cases can take several minutes and is dependent on your GPU (or CPU) and internet speed.  \nSome test cases download model weights and some perform inference to make sure everything works as intended.\n\n## Weights\n[weights_face_v1.0.0.pb](https://drive.google.com/file/d/1CwChAYxJo3mON6rcvXsl82FMSKj82vxF)\n\n[weights_plate_v1.0.0.pb](https://drive.google.com/file/d/1Fls9FYlQdRlLAtw-GVS_ie1oQUYmci9g)\n\n\n## Usage\n\nIn case you want to run the model on CPU, make sure that you install `tensorflow` instead of `tensorflow-gpu` listed\nin the `requirements.txt`.\n\nSince the weights will be downloaded automatically all that is needed to anonymize images is to run\n\n```bash\nPYTHONPATH=$PYTHONPATH:. python anonymizer/bin/anonymize.py --input /path/to/input_folder --image-output /path/to/output_folder --weights /path/to/store/weights\n```\n\nfrom the top folder of this repository. This will save both anonymized images and detection results as json-files to\nthe output folder.\n\n### Advanced Usage\n\nIn case you do not want to save the detections to json, add the parameter `no-write-detections`.\nExample:\n\n```bash\nPYTHONPATH=$PYTHONPATH:. python anonymizer/bin/anonymize.py --input /path/to/input_folder --image-output /path/to/output_folder --weights /path/to/store/weights --no-write-detections\n```\n\nDetection threshold for faces and license plates can be passed as additional parameters.\nBoth are floats in [0.001, 1.0]. Example:\n\n```bash\nPYTHONPATH=$PYTHONPATH:. python anonymizer/bin/anonymize.py --input /path/to/input_folder --image-output /path/to/output_folder --weights /path/to/store/weights --face-threshold=0.1 --plate-threshold=0.9\n```\n\nBy default only `*.jpg` and `*.png` files are anonymized. To for instance only anonymize jpgs and tiffs, \nthe parameter `image-extensions` can be used. Example:\n\n```bash\nPYTHONPATH=$PYTHONPATH:. python anonymizer/bin/anonymize.py --input /path/to/input_folder --image-output /path/to/output_folder --weights /path/to/store/weights --image-extensions=jpg,tiff\n```\n\nThe parameters for the blurring can be changed as well. For this the parameter `obfuscation-kernel` is used.\nIt consists of three values: The size of the gaussian kernel used for blurring, it's standard deviation and the size\nof another kernel that is used to make the transition between blurred and non-blurred regions smoother.\nExample usage:\n\n```bash\nPYTHONPATH=$PYTHONPATH:. python anonymizer/bin/anonymize.py --input /path/to/input_folder --image-output /path/to/output_folder --weights /path/to/store/weights --obfuscation-kernel=\"65,3,19\"\n```\n\n## Attributions\n\nAn image for one of the test cases was taken from the COCO dataset.  \nThe pictures in this README are under an [Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode) license.\nYou can find the pictures [here](http://farm4.staticflickr.com/3081/2289618559_2daf30a365_z.jpg) and [here](http://farm8.staticflickr.com/7062/6802736606_ed325d0452_z.jpg).\n"
  },
  {
    "path": "anonymizer/__init__.py",
    "content": ""
  },
  {
    "path": "anonymizer/anonymization/__init__.py",
    "content": "from anonymizer.anonymization.anonymizer import Anonymizer\n\n__all__ = ['Anonymizer']\n"
  },
  {
    "path": "anonymizer/anonymization/anonymizer.py",
    "content": "import json\nfrom pathlib import Path\n\nimport numpy as np\nfrom PIL import Image\nfrom tqdm import tqdm\n\n\ndef load_np_image(image_path):\n    image = Image.open(image_path).convert('RGB')\n    np_image = np.array(image)\n    return np_image\n\n\ndef save_np_image(image, image_path):\n    pil_image = Image.fromarray((image).astype(np.uint8), mode='RGB')\n    pil_image.save(image_path)\n\n\ndef save_detections(detections, detections_path):\n    json_output = []\n    for box in detections:\n        json_output.append({\n            'y_min': box.y_min,\n            'x_min': box.x_min,\n            'y_max': box.y_max,\n            'x_max': box.x_max,\n            'score': box.score,\n            'kind': box.kind\n        })\n    with open(detections_path, 'w') as output_file:\n        json.dump(json_output, output_file, indent=2)\n\n\nclass Anonymizer:\n    def __init__(self, detectors, obfuscator):\n        self.detectors = detectors\n        self.obfuscator = obfuscator\n\n    def anonymize_image(self, image, detection_thresholds):\n        assert set(self.detectors.keys()) == set(detection_thresholds.keys()),\\\n            'Detector names must match detection threshold names'\n        detected_boxes = []\n        for kind, detector in self.detectors.items():\n            new_boxes = detector.detect(image, detection_threshold=detection_thresholds[kind])\n            detected_boxes.extend(new_boxes)\n        return self.obfuscator.obfuscate(image, detected_boxes), detected_boxes\n\n    def anonymize_images(self, input_path, output_path, detection_thresholds, file_types, write_json):\n        print(f'Anonymizing images in {input_path} and saving the anonymized images to {output_path}...')\n\n        Path(output_path).mkdir(exist_ok=True)\n        assert Path(output_path).is_dir(), 'Output path must be a directory'\n\n        files = []\n        for file_type in file_types:\n            files.extend(list(Path(input_path).glob(f'**/*.{file_type}')))\n\n        for input_image_path in tqdm(files):\n            # Create output directory\n            relative_path = input_image_path.relative_to(input_path)\n            (Path(output_path) / relative_path.parent).mkdir(exist_ok=True, parents=True)\n            output_image_path = Path(output_path) / relative_path\n            output_detections_path = (Path(output_path) / relative_path).with_suffix('.json')\n\n            # Anonymize image\n            image = load_np_image(str(input_image_path))\n            anonymized_image, detections = self.anonymize_image(image=image, detection_thresholds=detection_thresholds)\n            save_np_image(image=anonymized_image, image_path=str(output_image_path))\n            if write_json:\n                save_detections(detections=detections, detections_path=str(output_detections_path))\n"
  },
  {
    "path": "anonymizer/bin/__init__.py",
    "content": ""
  },
  {
    "path": "anonymizer/bin/anonymize.py",
    "content": "\"\"\"\nCopyright 2018 understand.ai\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\"\"\"\n\n\nimport argparse\n\nfrom anonymizer.anonymization import Anonymizer\nfrom anonymizer.detection import Detector, download_weights, get_weights_path\nfrom anonymizer.obfuscation import Obfuscator\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(\n        description='Anonymize faces and license plates in a series of images.')\n    parser.add_argument('--input', required=True,\n                        metavar='/path/to/input_folder',\n                        help='Path to a folder that contains the images that should be anonymized. '\n                             'Images can be arbitrarily nested in subfolders and will still be found.')\n    parser.add_argument('--image-output', required=True,\n                        metavar='/path/to/output_foler',\n                        help='Path to the folder the anonymized images should be written to. '\n                             'Will mirror the folder structure of the input folder.')\n    parser.add_argument('--weights', required=True,\n                        metavar='/path/to/weights_foler',\n                        help='Path to the folder where the weights are stored. If no weights with the '\n                             'appropriate names are found they will be downloaded automatically.')\n    parser.add_argument('--image-extensions', required=False, default='jpg,png',\n                        metavar='\"jpg,png\"',\n                        help='Comma-separated list of file types that will be anonymized')\n    parser.add_argument('--face-threshold', type=float, required=False, default=0.3,\n                        metavar='0.3',\n                        help='Detection confidence needed to anonymize a detected face. '\n                             'Must be in [0.001, 1.0]')\n    parser.add_argument('--plate-threshold', type=float, required=False, default=0.3,\n                        metavar='0.3',\n                        help='Detection confidence needed to anonymize a detected license plate. '\n                             'Must be in [0.001, 1.0]')\n    parser.add_argument('--write-detections', dest='write_detections', action='store_true')\n    parser.add_argument('--no-write-detections', dest='write_detections', action='store_false')\n    parser.set_defaults(write_detections=True)\n    parser.add_argument('--obfuscation-kernel', required=False, default='21,2,9',\n                        metavar='kernel_size,sigma,box_kernel_size',\n                        help='This parameter is used to change the way the blurring is done. '\n                             'For blurring a gaussian kernel is used. The default size of the kernel is 21 pixels '\n                             'and the default value for the standard deviation of the distribution is 2. '\n                             'Higher values of the first parameter lead to slower transitions while blurring and '\n                             'larger values of the second parameter lead to sharper edges and less blurring. '\n                             'To make the transition from blurred areas to the non-blurred image smoother another '\n                             'kernel is used which has a default size of 9. Larger values lead to a smoother '\n                             'transition. Both kernel sizes must be odd numbers.')\n    args = parser.parse_args()\n\n    print(f'input: {args.input}')\n    print(f'image-output: {args.image_output}')\n    print(f'weights: {args.weights}')\n    print(f'image-extensions: {args.image_extensions}')\n    print(f'face-threshold: {args.face_threshold}')\n    print(f'plate-threshold: {args.plate_threshold}')\n    print(f'write-detections: {args.write_detections}')\n    print(f'obfuscation-kernel: {args.obfuscation_kernel}')\n    print()\n\n    return args\n\n\ndef main(input_path, image_output_path, weights_path, image_extensions, face_threshold, plate_threshold,\n         write_json, obfuscation_parameters):\n    download_weights(download_directory=weights_path)\n\n    kernel_size, sigma, box_kernel_size = obfuscation_parameters.split(',')\n    obfuscator = Obfuscator(kernel_size=int(kernel_size), sigma=float(sigma), box_kernel_size=int(box_kernel_size))\n    detectors = {\n        'face': Detector(kind='face', weights_path=get_weights_path(weights_path, kind='face')),\n        'plate': Detector(kind='plate', weights_path=get_weights_path(weights_path, kind='plate'))\n    }\n    detection_thresholds = {\n        'face': face_threshold,\n        'plate': plate_threshold\n    }\n    anonymizer = Anonymizer(obfuscator=obfuscator, detectors=detectors)\n    anonymizer.anonymize_images(input_path=input_path, output_path=image_output_path,\n                                detection_thresholds=detection_thresholds, file_types=image_extensions.split(','),\n                                write_json=write_json)\n\n\nif __name__ == '__main__':\n    args = parse_args()\n    main(input_path=args.input, image_output_path=args.image_output, weights_path=args.weights,\n         image_extensions=args.image_extensions,\n         face_threshold=args.face_threshold, plate_threshold=args.plate_threshold,\n         write_json=args.write_detections, obfuscation_parameters=args.obfuscation_kernel)\n"
  },
  {
    "path": "anonymizer/detection/__init__.py",
    "content": "from anonymizer.detection.detector import Detector\nfrom anonymizer.detection.weights import download_weights, get_weights_path\n\n__all__ = ['Detector', 'download_weights', 'get_weights_path']\n"
  },
  {
    "path": "anonymizer/detection/detector.py",
    "content": "import numpy as np\nimport tensorflow as tf\n\nfrom anonymizer.utils import Box\n\n\nclass Detector:\n    def __init__(self, kind, weights_path):\n        self.kind = kind\n\n        self.detection_graph = tf.Graph()\n        with self.detection_graph.as_default():\n            od_graph_def = tf.GraphDef()\n            with tf.gfile.GFile(weights_path, 'rb') as fid:\n                serialized_graph = fid.read()\n                od_graph_def.ParseFromString(serialized_graph)\n                tf.import_graph_def(od_graph_def, name='')\n\n        conf = tf.ConfigProto()\n        self.session = tf.Session(graph=self.detection_graph, config=conf)\n\n    def _convert_boxes(self, num_boxes, scores, boxes, image_height, image_width, detection_threshold):\n        assert detection_threshold >= 0.001, 'Threshold can not be too close to \"0\".'\n\n        result_boxes = []\n        for i in range(int(num_boxes)):\n            score = float(scores[i])\n            if score < detection_threshold:\n                continue\n\n            y_min, x_min, y_max, x_max = map(float, boxes[i].tolist())\n            box = Box(y_min=y_min * image_height, x_min=x_min * image_width,\n                      y_max=y_max * image_height, x_max=x_max * image_width,\n                      score=score, kind=self.kind)\n            result_boxes.append(box)\n        return result_boxes\n\n    def detect(self, image, detection_threshold):\n        image_tensor = self.detection_graph.get_tensor_by_name('image_tensor:0')\n        num_detections = self.detection_graph.get_tensor_by_name('num_detections:0')\n        detection_scores = self.detection_graph.get_tensor_by_name('detection_scores:0')\n        detection_boxes = self.detection_graph.get_tensor_by_name('detection_boxes:0')\n\n        image_height, image_width, channels = image.shape\n        assert channels == 3, f'Invalid number of channels: {channels}. ' \\\n                              f'Only images with three color channels are supported.'\n\n        np_images = np.array([image])\n        num_boxes, scores, boxes = self.session.run(\n            [num_detections, detection_scores, detection_boxes],\n            feed_dict={image_tensor: np_images})\n\n        converted_boxes = self._convert_boxes(num_boxes=num_boxes[0], scores=scores[0], boxes=boxes[0],\n                                              image_height=image_height, image_width=image_width,\n                                              detection_threshold=detection_threshold)\n        return converted_boxes\n"
  },
  {
    "path": "anonymizer/detection/weights.py",
    "content": "from pathlib import Path\n\nfrom google_drive_downloader import GoogleDriveDownloader as gdd\n\n\nWEIGHTS_GDRIVE_IDS = {\n    '1.0.0': {\n        'face': '1CwChAYxJo3mON6rcvXsl82FMSKj82vxF',\n        'plate': '1Fls9FYlQdRlLAtw-GVS_ie1oQUYmci9g'\n    }\n}\n\n\ndef get_weights_path(base_path, kind, version='1.0.0'):\n    assert version in WEIGHTS_GDRIVE_IDS.keys(), f'Invalid weights version \"{version}\"'\n    assert kind in WEIGHTS_GDRIVE_IDS[version].keys(), f'Invalid weights kind \"{kind}\"'\n\n    return str(Path(base_path) / f'weights_{kind}_v{version}.pb')\n\n\ndef _download_single_model_weights(download_directory, kind, version):\n    file_id = WEIGHTS_GDRIVE_IDS[version][kind]\n    weights_path = get_weights_path(base_path=download_directory, kind=kind, version=version)\n    if Path(weights_path).exists():\n        return\n\n    print(f'Downloading {kind} weights to {weights_path}')\n    gdd.download_file_from_google_drive(file_id=file_id, dest_path=weights_path, unzip=False)\n\n\ndef download_weights(download_directory, version='1.0.0'):\n    for kind in ['face', 'plate']:\n        _download_single_model_weights(download_directory=download_directory, kind=kind, version=version)\n"
  },
  {
    "path": "anonymizer/obfuscation/__init__.py",
    "content": "from anonymizer.obfuscation.obfuscator import Obfuscator\n\n__all__ = ['Obfuscator']\n"
  },
  {
    "path": "anonymizer/obfuscation/helpers.py",
    "content": "import numpy as np\nimport tensorflow as tf\n\n\ndef kernel_initializer(kernels):\n    \"\"\" Wrapper for an initializer of convolution weights.\n\n    :return: Callable initializer object.\n    \"\"\"\n    assert len(kernels.shape) == 3\n    kernels = kernels.astype(np.float32)\n\n    def _initializer(shape, dtype=tf.float32, partition_info=None):\n        \"\"\"Initializer function which is called from tensorflow internally.\n\n        :param shape: Runtime / Construction time shape of the tensor.\n        :param dtype: Data type of the resulting tensor.\n        :param partition_info: Placeholder for internal tf call.\n        :return: 4D numpy array with weights [filter_height, filter_width, in_channels, out_channels].\n        \"\"\"\n        if shape:\n            # second last dimension is input, last dimension is output\n            fan_in = float(shape[-2]) if len(shape) > 1 else float(shape[-1])\n            fan_out = float(shape[-1])\n        else:\n            fan_in = 1.0\n            fan_out = 1.0\n\n        assert fan_out == 1 and fan_in == kernels.shape[-1]\n\n        # define weight matrix (set dtype always to float32)\n        # weights = np.expand_dims(kernels, axis=2)\n        weights = np.expand_dims(kernels, axis=-1)\n\n        return weights\n\n    return _initializer\n\n\ndef bilinear_filter(filter_size=(4, 4)):\n    \"\"\"\n    Make a 2D bilinear kernel suitable for upsampling of the given (h, w) size.\n    Also allows asymmetric kernels.\n\n    :param filter_size: Tuple defining the filter size in width and height.\n\n    :return: 2D numpy array containing bilinear weights.\n    \"\"\"\n    assert isinstance(filter_size, (list, tuple)) and len(filter_size) == 2\n\n    factor = [(size + 1) // 2 for size in filter_size]\n    # define first center dimension\n    if filter_size[0] % 2 == 1:\n        center_x = factor[0] - 1\n    else:\n        center_x = factor[0] - 0.5\n    # define second center dimension\n    if filter_size[1] % 2 == 1:\n        center_y = factor[1] - 1\n    else:\n        center_y = factor[1] - 0.5\n\n    og = np.ogrid[:filter_size[0], :filter_size[1]]\n    kernel = (1 - abs(og[0] - center_x) / float(factor[0])) * (1 - abs(og[1] - center_y) / float(factor[1]))\n\n    return kernel\n\n\ndef get_default_session_config(memory_fraction=0.9):\n    \"\"\" Returns default session configuration\n\n    :param memory_fraction: percentage of the memory which should be kept free (growing is allowed).\n    :return: tensorflow session configuration object\n    \"\"\"\n    conf = tf.ConfigProto()\n    conf.gpu_options.per_process_gpu_memory_fraction = memory_fraction\n    conf.gpu_options.allocator_type = 'BFC'\n    conf.gpu_options.allow_growth = True\n    conf.allow_soft_placement = True\n"
  },
  {
    "path": "anonymizer/obfuscation/obfuscator.py",
    "content": "import math\n\nimport numpy as np\nimport scipy.stats as st\nimport tensorflow as tf\n\nfrom anonymizer.obfuscation.helpers import kernel_initializer, bilinear_filter, get_default_session_config\n\n\nclass Obfuscator:\n    \"\"\" This class is used to blur box regions within an image with gaussian blurring. \"\"\"\n    def __init__(self, kernel_size=21, sigma=2, channels=3, box_kernel_size=9, smooth_boxes=True):\n        \"\"\"\n        :param kernel_size: Size of the blurring kernel.\n        :param sigma: standard deviation of the blurring kernel. Higher values lead to sharper edges, less blurring.\n        :param channels: Number of image channels this blurrer will be used for. This is fixed as blurring kernels will\n            be created for each channel only once.\n        :param box_kernel_size: This parameter is only used when smooth_boxes is True. In this case, a smoothing\n            operation is applied on the bounding box mask to create smooth transitions from blurred to normal image at\n            the bounding box borders.\n        :param smooth_boxes: Flag defining if bounding box masks borders should be smoothed.\n        \"\"\"\n        # Kernel must be uneven because of a simplified padding scheme\n        assert kernel_size % 2 == 1\n\n        self.kernel_size = kernel_size\n        self.box_kernel_size = box_kernel_size\n        self.sigma = sigma\n        self.channels = channels\n        self.smooth_boxes = smooth_boxes\n\n        # create internal kernels (3D kernels with the channels in the last dimension)\n        kernel = self._gaussian_kernel(kernel_size=self.kernel_size, sigma=self.sigma)  # kernel for blurring\n        self.kernels = np.repeat(kernel, repeats=channels, axis=-1).reshape((kernel_size, kernel_size, channels))\n        mean_kernel = bilinear_filter(filter_size=(box_kernel_size, box_kernel_size))  # kernel for smoothing\n        self.mean_kernel = np.expand_dims(mean_kernel/np.sum(mean_kernel), axis=-1)\n\n        # visualization\n        # print(self.kernels.shape)\n        # self._visualize_kernel(kernel=self.kernels[..., 0])\n        # self._visualize_kernel(kernel=self.mean_kernel[..., 0])\n\n        # wrap everything in a tf session which is always open\n        sess = tf.Session(config=get_default_session_config(0.9))\n        self._build_graph()\n        init_op = tf.global_variables_initializer()\n        sess.run(init_op)\n\n        self.sess = sess\n\n    def _gaussian_kernel(self, kernel_size=30, sigma=5):\n        \"\"\" Returns a 2D Gaussian kernel array.\n\n        :param kernel_size: Size of the kernel, the resulting array will be kernel_size x kernel_size\n        :param sigma: Standard deviation of the gaussian kernel.\n        :return: 2D numpy array containing a gaussian kernel.\n        \"\"\"\n\n        interval = (2 * sigma + 1.) / kernel_size\n        x = np.linspace(-sigma - interval / 2., sigma + interval / 2., kernel_size + 1)\n        kern1d = np.diff(st.norm.cdf(x))\n        kernel_raw = np.sqrt(np.outer(kern1d, kern1d))\n        kernel = kernel_raw / kernel_raw.sum()\n\n        return kernel\n\n    def _build_graph(self):\n        \"\"\" Builds the tensorflow graph containing all necessary operations for the blurring procedure. \"\"\"\n        with tf.variable_scope('gaussian_blurring'):\n            image = tf.placeholder(dtype=tf.float32, shape=[None, None, None, self.channels], name='x_input')\n            mask = tf.placeholder(dtype=tf.float32, shape=[None, None, None, 1], name='x_input')\n\n            # ---- mean smoothing\n            if self.smooth_boxes:\n                W_mean = tf.get_variable(name='mean_kernel',\n                                         shape=[self.mean_kernel.shape[0], self.mean_kernel.shape[1], 1, 1],\n                                         dtype=tf.float32,\n                                         initializer=kernel_initializer(kernels=self.mean_kernel),\n                                         trainable=False, validate_shape=True)\n\n                smoothed_mask = tf.nn.conv2d(input=mask, filter=W_mean, strides=[1, 1, 1, 1], padding='SAME',\n                                             use_cudnn_on_gpu=True, data_format='NHWC', name='smooth_mask')\n            else:\n                smoothed_mask = mask\n\n            # ---- blurring the initial image\n            W_blur = tf.get_variable(name='gaussian_kernels',\n                                     shape=[self.kernels.shape[0], self.kernels.shape[1], self.kernels.shape[2], 1],\n                                     dtype=tf.float32,\n                                     initializer=kernel_initializer(kernels=self.kernels),\n                                     trainable=False, validate_shape=True)\n\n            # Use reflection padding in conjunction with convolutions without padding (no border effects)\n            pad = (self.kernel_size - 1) / 2\n            paddings = np.array([[0, 0], [pad, pad], [pad, pad], [0, 0]])\n            img = tf.pad(image, paddings=paddings, mode='REFLECT')\n            blurred_image = tf.nn.depthwise_conv2d_native(input=img, filter=W_blur, strides=[1, 1, 1, 1],\n                                                          padding='VALID', data_format='NHWC', name='conv_spatial')\n\n            # Combination of the blurred image and the original image with a bounding box mask\n            anonymized_image = image * (1-smoothed_mask) + blurred_image * smoothed_mask\n\n            # store internal variables\n            self.image = image\n            self.mask = mask\n            self.anonymized_image = anonymized_image\n\n    def _get_all_masks(self, bboxes, images):\n        \"\"\" For a batch of boxes, returns heatmap encoded box images.\n\n        :param bboxes: 3D np array containing a batch of box coordinates (see anonymize for more details).\n        :param images: 4D np array with NHWC encoding containing a batch of images.\n        :return: 4D np array in NHWC encoding. For each batch sample, there is a binary mask with one channel which\n            encodes bounding box locations.\n        \"\"\"\n        masks = np.zeros(shape=(images.shape[0], images.shape[1], images.shape[2], 1))\n        image_size = (images.shape[1], images.shape[2])\n\n        for n, boxes in enumerate(bboxes):\n            masks[n, ...] = self._get_box_mask(box_array=boxes, image_size=image_size)\n\n        return masks\n\n    def _get_box_mask(self, box_array, image_size):\n        \"\"\" For an array of boxes for a single image, return a binary mask which encodes box locations as heatmap.\n\n        :param box_array: 2D numpy array with dimnesions: numer_bboxes x 4.\n            Boxes are encoded as [x_min, y_min, x_max, y_max]\n        :param image_size: tuple containing the image dimensions. This is used to create the binary mask layout.\n        :return: 3D numpy array containing the binary mask (last dimension is always size 1).\n        \"\"\"\n        # assert isinstance(box_array, np.ndarray) and len(box_array.shape) == 2\n        mask = np.zeros(shape=(image_size[0], image_size[1], 1))\n\n        # insert box masks into array\n        for box in box_array:\n            mask[box[1]:box[3], box[0]:box[2], :] = 1\n\n        return mask\n\n    def _obfuscate_numpy(self, images, bboxes):\n        \"\"\" Anonymizes bounding box regions within a given region by applying gaussian blurring.\n\n        :param images: 4D np array with NHWC encoding containing a batch of images.\n            The number of channels must match self.num_channels.\n        :param bboxes: 3D np array containing a batch of box coordinates. First dimension is the batch dimension.\n            Second dimension are boxes within an image and third dimension are the box coordinates.\n            np.array([[[10, 15, 30, 50], [500, 200, 850, 300]]]) contains one batch sample and two boxes for that\n            sample. Box coordinates are in [x_min, y_min, x_max, y_max] notation.\n        :return: 4D np array with NHWC encoding containing an anonymized batch of images.\n        \"\"\"\n        # assert isinstance(images, np.ndarray) and len(images.shape) == 4\n        # assert isinstance(bboxes, np.ndarray) and len(bboxes.shape) == 3 and bboxes.shape[-1] == 4\n        bbox_masks = self._get_all_masks(bboxes=bboxes, images=images)\n\n        anonymized_image = self.sess.run(fetches=self.anonymized_image,\n                                         feed_dict={self.image: images, self.mask: bbox_masks})\n        return anonymized_image\n\n    def obfuscate(self, image, boxes):\n        \"\"\"\n        Anonymize all bounding boxes in a given image.\n        :param image: The image as np.ndarray with shape==(height, width, channels).\n        :param boxes: A list of boxes.\n        :return: The anonymized image.\n        \"\"\"\n        if len(boxes) == 0:\n            return np.copy(image)\n\n        image_array = np.expand_dims(image, axis=0)\n        box_array = []\n        for box in boxes:\n            x_min = int(math.floor(box.x_min))\n            y_min = int(math.floor(box.y_min))\n            x_max = int(math.ceil(box.x_max))\n            y_max = int(math.ceil(box.y_max))\n            box_array.append(np.array([x_min, y_min, x_max, y_max]))\n        box_array = np.stack(box_array, axis=0)\n        box_array = np.expand_dims(box_array, axis=0)\n\n        anonymized_images = self._obfuscate_numpy(image_array, box_array)\n        return anonymized_images[0]\n"
  },
  {
    "path": "anonymizer/utils/__init__.py",
    "content": "from anonymizer.utils.box import Box\n\n__all__ = ['Box']\n"
  },
  {
    "path": "anonymizer/utils/box.py",
    "content": "class Box:\n    def __init__(self, x_min, y_min, x_max, y_max, score, kind):\n        self.x_min = float(x_min)\n        self.y_min = float(y_min)\n        self.x_max = float(x_max)\n        self.y_max = float(y_max)\n        self.score = float(score)\n        self.kind = str(kind)\n\n    def __repr__(self):\n        return f'Box({self.x_min}, {self.y_min}, {self.x_max}, {self.y_max}, {self.score}, {self.kind})'\n\n    def __eq__(self, other):\n        if isinstance(other, Box):\n            return (self.x_min == other.x_min and self.y_min == other.y_min and\n                    self.x_max == other.x_max and self.y_max == other.y_max and\n                    self.score == other.score and self.kind == other.kind)\n        return False\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\nfilterwarnings =\n    ignore:.*inspect\\.getargspec\\(\\) is deprecated:DeprecationWarning\n"
  },
  {
    "path": "requirements.txt",
    "content": "pytest==3.9.1\nflake8==3.5.0\nnumpy==1.15.2\ntensorflow-gpu==1.11.0\nscipy==1.1.0\nPillow==5.3.0\nrequests==2.20.0\ngoogledrivedownloader==0.3\ntqdm==4.28.0\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n\nfrom distutils.core import setup\nfrom setuptools import find_packages\n\nsetup(name='uai-anonymizer',\n      version='latest',\n      packages=find_packages(exclude=['test', 'test.*']),\n\n      install_requires=[\n          'pytest>=3.9.1',\n          'flake8>=3.5.0',\n          'numpy>=1.15.2',\n          'tensorflow-gpu>=1.11.0',\n          'scipy>=1.1.0',\n          'Pillow>=5.3.0',\n          'requests>=2.20.0',\n          'googledrivedownloader>=0.3',\n          'tqdm>=4.28.0',\n      ],\n\n      dependency_links=[\n      ],\n      )\n"
  },
  {
    "path": "test/__init__.py",
    "content": ""
  },
  {
    "path": "test/anonymization/__init__.py",
    "content": ""
  },
  {
    "path": "test/anonymization/anonymizer_test.py",
    "content": "import numpy as np\nfrom PIL import Image\n\nfrom anonymizer.utils import Box\nfrom anonymizer.anonymization import Anonymizer\n\n\ndef load_np_image(image_path):\n    image = Image.open(image_path).convert('RGB')\n    np_image = np.array(image)\n    return np_image\n\n\nclass MockObfuscator:\n    def obfuscate(self, image, boxes):\n        obfuscated_image = np.copy(image)\n        for box in boxes:\n            obfuscated_image[int(box.y_min):int(box.y_max), int(box.x_min):int(box.x_max), :] = 0.0\n        return obfuscated_image\n\n\nclass MockDetector:\n    def __init__(self, detected_boxes):\n        self.detected_boxes = detected_boxes\n\n    def detect(self, image, detection_threshold):\n        return self.detected_boxes\n\n\nclass TestAnonymizer:\n    @staticmethod\n    def test_it_anonymizes_a_single_image():\n        np.random.seed(42)  # to avoid flaky tests\n        input_image = np.random.rand(128, 64, 3)  # height, width, channels\n        obfuscator = MockObfuscator()\n        mock_detector = MockDetector([Box(y_min=0, x_min=10, y_max=20, x_max=30, score=0.5, kind=''),\n                                      Box(y_min=100, x_min=10, y_max=120, x_max=30, score=0.9, kind='')])\n        expected_anonymized_image = np.copy(input_image)\n        expected_anonymized_image[0:20, 10:30] = 0.0\n        expected_anonymized_image[100:120, 10:30] = 0.0\n\n        anonymizer = Anonymizer(detectors={'face': mock_detector}, obfuscator=obfuscator)\n        anonymized_image, detected_boxes = anonymizer.anonymize_image(input_image, detection_thresholds={'face': 0.1})\n\n        assert np.all(np.isclose(expected_anonymized_image, anonymized_image))\n        assert detected_boxes == [Box(y_min=0, x_min=10, y_max=20, x_max=30, score=0.5, kind=''),\n                                  Box(y_min=100, x_min=10, y_max=120, x_max=30, score=0.9, kind='')]\n\n    @staticmethod\n    def test_it_anonymizes_multiple_images(tmp_path):\n        np.random.seed(42)  # to avoid flaky tests\n        input_images = [np.random.rand(128, 64, 3), np.random.rand(128, 64, 3), np.random.rand(128, 64, 3)]\n        obfuscator = MockObfuscator()\n        mock_detector = MockDetector([Box(y_min=0, x_min=10, y_max=20, x_max=30, score=0.5, kind=''),\n                                      Box(y_min=100, x_min=10, y_max=120, x_max=30, score=0.9, kind='')])\n        expected_anonymized_images = list(map(np.copy, input_images))\n        for i, _ in enumerate(expected_anonymized_images):\n            expected_anonymized_images[i] = (expected_anonymized_images[i] * 255).astype(np.uint8)\n            expected_anonymized_images[i][0:20, 10:30] = 0\n            expected_anonymized_images[i][100:120, 10:30] = 0\n        # write input images to disk\n        input_path = tmp_path / 'input'\n        input_path.mkdir()\n        output_path = tmp_path / 'output'\n        for i, input_image in enumerate(input_images):\n            image_path = input_path / f'{i}.png'\n            pil_image = Image.fromarray((input_image * 255).astype(np.uint8), mode='RGB')\n            pil_image.save(image_path)\n\n        anonymizer = Anonymizer(detectors={'face': mock_detector}, obfuscator=obfuscator)\n        anonymizer.anonymize_images(str(input_path), output_path=str(output_path), detection_thresholds={'face': 0.1},\n                                    file_types=['jpg', 'png'], write_json=False)\n\n        anonymized_images = []\n        for image_path in sorted(output_path.glob('**/*.png')):\n            anonymized_images.append(load_np_image(image_path))\n\n        for i, expected_anonymized_image in enumerate(expected_anonymized_images):\n            assert np.all(np.isclose(expected_anonymized_image, anonymized_images[i]))\n"
  },
  {
    "path": "test/detection/__init__.py",
    "content": ""
  },
  {
    "path": "test/detection/detector_test.py",
    "content": "import numpy as np\nfrom PIL import Image\n\nfrom anonymizer.utils import Box\nfrom anonymizer.detection import Detector\nfrom anonymizer.detection import download_weights, get_weights_path\n\n\ndef box_covers_box(covering_box: Box, covered_box: Box):\n    return (covered_box.x_min > covering_box.x_min and covered_box.y_min > covering_box.y_min and\n            covered_box.x_max < covering_box.x_max and covered_box.y_max < covering_box.y_max)\n\n\ndef load_np_image(image_path):\n    image = Image.open(image_path).convert('RGB')\n    np_image = np.array(image)\n    return np_image\n\n\nclass TestDetector:\n    @staticmethod\n    def test_it_detects_obvious_faces(tmp_path):\n        weights_directory = tmp_path / 'weights'\n        face_weights_path = get_weights_path(weights_directory, kind='face')\n        download_weights(weights_directory)\n\n        detector = Detector(kind='face', weights_path=face_weights_path)\n        np_image = load_np_image('./test/detection/face_test_image.jpg')\n\n        left_face = Box(x_min=267, y_min=64, x_max=311, y_max=184, score=0.0, kind='face')\n        right_face = Box(x_min=369, y_min=68, x_max=420, y_max=152, score=0.0, kind='face')\n\n        boxes = detector.detect(np_image, detection_threshold=0.2)\n\n        assert len(boxes) >= 2\n        for box in boxes:\n            assert box.score >= 0.2\n        assert boxes[0].score >= 0.5 and boxes[1].score >= 0.5\n        assert ((box_covers_box(boxes[0], left_face) and box_covers_box(boxes[1], right_face)) or\n                (box_covers_box(boxes[1], left_face) and box_covers_box(boxes[0], right_face)))\n"
  },
  {
    "path": "test/detection/weights_test.py",
    "content": "from anonymizer.detection import download_weights\n\n\nclass TestDownloadWeights:\n    @staticmethod\n    def test_it_downloads_weights(tmp_path):\n        weights_directory = tmp_path / 'weights'\n        assert len(list(weights_directory.glob('**/*.pb'))) == 0\n\n        download_weights(download_directory=weights_directory, version='1.0.0')\n\n        assert len(list(weights_directory.glob('**/*.pb'))) == 2\n        assert (weights_directory / 'weights_face_v1.0.0.pb').is_file()\n        assert (weights_directory / 'weights_plate_v1.0.0.pb').is_file()\n        assert not (weights_directory / 'nonexistent_path.pb').is_file()\n"
  },
  {
    "path": "test/obfuscation/__init__.py",
    "content": ""
  },
  {
    "path": "test/obfuscation/obfuscator_test.py",
    "content": "import numpy as np\n\nfrom anonymizer.obfuscation import Obfuscator\nfrom anonymizer.utils import Box\n\n\nclass TestObfuscator:\n    @staticmethod\n    def test_it_obfuscates_regions():\n        obfuscator = Obfuscator()\n        np.random.seed(42)  # to avoid flaky tests\n        image = np.random.rand(128, 64, 3)  # height, width, channels\n        boxes = [Box(y_min=0, x_min=10, y_max=20, x_max=30, score=0, kind=''),\n                 Box(y_min=100, x_min=10, y_max=120, x_max=30, score=0, kind='')]\n\n        # copy to make sure the input image does not change\n        obfuscated_image = obfuscator.obfuscate(np.copy(image), boxes)\n\n        assert obfuscated_image.shape == (128, 64, 3)\n        assert not np.any(np.isclose(obfuscated_image[0:20, 10:30, :], image[0:20, 10:30, :]))\n        assert not np.any(np.isclose(obfuscated_image[100:120, 10:30, :], image[100:120, 10:30, :]))\n        assert np.all(np.isclose(obfuscated_image[30:90, :, :], image[30:90, :, :]))\n"
  },
  {
    "path": "test/utils/__init__.py",
    "content": ""
  },
  {
    "path": "test/utils/box_test.py",
    "content": "from anonymizer.utils import Box\n\n\nclass TestBox:\n    @staticmethod\n    def test_it_has_coordinates_a_score_and_a_kind():\n        box = Box(x_min=1.0, y_min=2.0, x_max=3.0, y_max=4.0, score=0.9, kind='face')\n\n        assert box.x_min == 1.0\n        assert box.y_min == 2.0\n        assert box.x_max == 3.0\n        assert box.y_max == 4.0\n        assert box.score == 0.9\n        assert box.kind == 'face'\n"
  }
]