obfuscator.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import math
  2. import numpy as np
  3. import scipy.stats as st
  4. import tensorflow as tf
  5. from anonymizer.obfuscation.helpers import kernel_initializer, bilinear_filter, get_default_session_config
  6. class Obfuscator:
  7. """ This class is used to blur box regions within an image with gaussian blurring. """
  8. def __init__(self, kernel_size=21, sigma=2, channels=3, box_kernel_size=9, smooth_boxes=True):
  9. """
  10. :param kernel_size: Size of the blurring kernel.
  11. :param sigma: standard deviation of the blurring kernel. Higher values lead to sharper edges, less blurring.
  12. :param channels: Number of image channels this blurrer will be used for. This is fixed as blurring kernels will
  13. be created for each channel only once.
  14. :param box_kernel_size: This parameter is only used when smooth_boxes is True. In this case, a smoothing
  15. operation is applied on the bounding box mask to create smooth transitions from blurred to normal image at
  16. the bounding box borders.
  17. :param smooth_boxes: Flag defining if bounding box masks borders should be smoothed.
  18. """
  19. # Kernel must be uneven because of a simplified padding scheme
  20. assert kernel_size % 2 == 1
  21. self.kernel_size = kernel_size
  22. self.box_kernel_size = box_kernel_size
  23. self.sigma = sigma
  24. self.channels = channels
  25. self.smooth_boxes = smooth_boxes
  26. # create internal kernels (3D kernels with the channels in the last dimension)
  27. kernel = self._gaussian_kernel(kernel_size=self.kernel_size, sigma=self.sigma) # kernel for blurring
  28. self.kernels = np.repeat(kernel, repeats=channels, axis=-1).reshape((kernel_size, kernel_size, channels))
  29. mean_kernel = bilinear_filter(filter_size=(box_kernel_size, box_kernel_size)) # kernel for smoothing
  30. self.mean_kernel = np.expand_dims(mean_kernel/np.sum(mean_kernel), axis=-1)
  31. # visualization
  32. # print(self.kernels.shape)
  33. # self._visualize_kernel(kernel=self.kernels[..., 0])
  34. # self._visualize_kernel(kernel=self.mean_kernel[..., 0])
  35. # wrap everything in a tf session which is always open
  36. sess = tf.Session(config=get_default_session_config(0.9))
  37. self._build_graph()
  38. init_op = tf.global_variables_initializer()
  39. sess.run(init_op)
  40. self.sess = sess
  41. def _gaussian_kernel(self, kernel_size=30, sigma=5):
  42. """ Returns a 2D Gaussian kernel array.
  43. :param kernel_size: Size of the kernel, the resulting array will be kernel_size x kernel_size
  44. :param sigma: Standard deviation of the gaussian kernel.
  45. :return: 2D numpy array containing a gaussian kernel.
  46. """
  47. interval = (2 * sigma + 1.) / kernel_size
  48. x = np.linspace(-sigma - interval / 2., sigma + interval / 2., kernel_size + 1)
  49. kern1d = np.diff(st.norm.cdf(x))
  50. kernel_raw = np.sqrt(np.outer(kern1d, kern1d))
  51. kernel = kernel_raw / kernel_raw.sum()
  52. return kernel
  53. def _build_graph(self):
  54. """ Builds the tensorflow graph containing all necessary operations for the blurring procedure. """
  55. with tf.variable_scope('gaussian_blurring'):
  56. image = tf.placeholder(dtype=tf.float32, shape=[None, None, None, self.channels], name='x_input')
  57. mask = tf.placeholder(dtype=tf.float32, shape=[None, None, None, 1], name='x_input')
  58. # ---- mean smoothing
  59. if self.smooth_boxes:
  60. W_mean = tf.get_variable(name='mean_kernel',
  61. shape=[self.mean_kernel.shape[0], self.mean_kernel.shape[1], 1, 1],
  62. dtype=tf.float32,
  63. initializer=kernel_initializer(kernels=self.mean_kernel),
  64. trainable=False, validate_shape=True)
  65. smoothed_mask = tf.nn.conv2d(input=mask, filter=W_mean, strides=[1, 1, 1, 1], padding='SAME',
  66. use_cudnn_on_gpu=True, data_format='NHWC', name='smooth_mask')
  67. else:
  68. smoothed_mask = mask
  69. # ---- blurring the initial image
  70. W_blur = tf.get_variable(name='gaussian_kernels',
  71. shape=[self.kernels.shape[0], self.kernels.shape[1], self.kernels.shape[2], 1],
  72. dtype=tf.float32,
  73. initializer=kernel_initializer(kernels=self.kernels),
  74. trainable=False, validate_shape=True)
  75. # Use reflection padding in conjunction with convolutions without padding (no border effects)
  76. pad = (self.kernel_size - 1) / 2
  77. paddings = np.array([[0, 0], [pad, pad], [pad, pad], [0, 0]])
  78. img = tf.pad(image, paddings=paddings, mode='REFLECT')
  79. blurred_image = tf.nn.depthwise_conv2d_native(input=img, filter=W_blur, strides=[1, 1, 1, 1],
  80. padding='VALID', data_format='NHWC', name='conv_spatial')
  81. # Combination of the blurred image and the original image with a bounding box mask
  82. anonymized_image = image * (1-smoothed_mask) + blurred_image * smoothed_mask
  83. # store internal variables
  84. self.image = image
  85. self.mask = mask
  86. self.anonymized_image = anonymized_image
  87. def _get_all_masks(self, bboxes, images):
  88. """ For a batch of boxes, returns heatmap encoded box images.
  89. :param bboxes: 3D np array containing a batch of box coordinates (see anonymize for more details).
  90. :param images: 4D np array with NHWC encoding containing a batch of images.
  91. :return: 4D np array in NHWC encoding. For each batch sample, there is a binary mask with one channel which
  92. encodes bounding box locations.
  93. """
  94. masks = np.zeros(shape=(images.shape[0], images.shape[1], images.shape[2], 1))
  95. image_size = (images.shape[1], images.shape[2])
  96. for n, boxes in enumerate(bboxes):
  97. masks[n, ...] = self._get_box_mask(box_array=boxes, image_size=image_size)
  98. return masks
  99. def _get_box_mask(self, box_array, image_size):
  100. """ For an array of boxes for a single image, return a binary mask which encodes box locations as heatmap.
  101. :param box_array: 2D numpy array with dimnesions: numer_bboxes x 4.
  102. Boxes are encoded as [x_min, y_min, x_max, y_max]
  103. :param image_size: tuple containing the image dimensions. This is used to create the binary mask layout.
  104. :return: 3D numpy array containing the binary mask (last dimension is always size 1).
  105. """
  106. # assert isinstance(box_array, np.ndarray) and len(box_array.shape) == 2
  107. mask = np.zeros(shape=(image_size[0], image_size[1], 1))
  108. # insert box masks into array
  109. for box in box_array:
  110. mask[box[1]:box[3], box[0]:box[2], :] = 1
  111. return mask
  112. def _obfuscate_numpy(self, images, bboxes):
  113. """ Anonymizes bounding box regions within a given region by applying gaussian blurring.
  114. :param images: 4D np array with NHWC encoding containing a batch of images.
  115. The number of channels must match self.num_channels.
  116. :param bboxes: 3D np array containing a batch of box coordinates. First dimension is the batch dimension.
  117. Second dimension are boxes within an image and third dimension are the box coordinates.
  118. np.array([[[10, 15, 30, 50], [500, 200, 850, 300]]]) contains one batch sample and two boxes for that
  119. sample. Box coordinates are in [x_min, y_min, x_max, y_max] notation.
  120. :return: 4D np array with NHWC encoding containing an anonymized batch of images.
  121. """
  122. # assert isinstance(images, np.ndarray) and len(images.shape) == 4
  123. # assert isinstance(bboxes, np.ndarray) and len(bboxes.shape) == 3 and bboxes.shape[-1] == 4
  124. bbox_masks = self._get_all_masks(bboxes=bboxes, images=images)
  125. anonymized_image = self.sess.run(fetches=self.anonymized_image,
  126. feed_dict={self.image: images, self.mask: bbox_masks})
  127. return anonymized_image
  128. def obfuscate(self, image, boxes):
  129. """
  130. Anonymize all bounding boxes in a given image.
  131. :param image: The image as np.ndarray with shape==(height, width, channels).
  132. :param boxes: A list of boxes.
  133. :return: The anonymized image.
  134. """
  135. if len(boxes) == 0:
  136. return np.copy(image)
  137. image_array = np.expand_dims(image, axis=0)
  138. box_array = []
  139. for box in boxes:
  140. x_min = int(math.floor(box.x_min))
  141. y_min = int(math.floor(box.y_min))
  142. x_max = int(math.ceil(box.x_max))
  143. y_max = int(math.ceil(box.y_max))
  144. box_array.append(np.array([x_min, y_min, x_max, y_max]))
  145. box_array = np.stack(box_array, axis=0)
  146. box_array = np.expand_dims(box_array, axis=0)
  147. anonymized_images = self._obfuscate_numpy(image_array, box_array)
  148. return anonymized_images[0]