Skip to content

Commit 3b89f21

Browse files
author
adescombes
committed
repo initialized + readme updated
1 parent 7c34c08 commit 3b89f21

14 files changed

+1133
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import json\n",
10+
"import numpy as np\n",
11+
"import imageio\n",
12+
"import os\n",
13+
"import tqdm\n",
14+
"from glob import glob\n",
15+
"import shutil\n",
16+
"import matplotlib.pyplot as plt\n",
17+
"plt.rcParams['figure.figsize']=[12, 12]\n",
18+
"from matplotlib.patches import Polygon\n",
19+
"from PIL import Image, ImageDraw, ImageOps\n",
20+
"import cv2 as cv2\n",
21+
"import json\n",
22+
"import requests\n",
23+
"from datetime import datetime"
24+
]
25+
},
26+
{
27+
"cell_type": "code",
28+
"execution_count": 132,
29+
"metadata": {},
30+
"outputs": [],
31+
"source": [
32+
"def find_poly_in_instance(img_filename, label_dict):\n",
33+
" shape_attributes = label_dict.get(img_filename)\n",
34+
" polygons = []\n",
35+
" for sh_attr in shape_attributes:\n",
36+
" xy = list(zip(sh_attr.get('shape_attributes')['all_points_x'], sh_attr.get('shape_attributes')['all_points_y']))\n",
37+
" polygons.append(xy)\n",
38+
"\n",
39+
" return polygons"
40+
]
41+
},
42+
{
43+
"cell_type": "code",
44+
"execution_count": 130,
45+
"metadata": {},
46+
"outputs": [],
47+
"source": [
48+
"url = \" \"\n",
49+
"response = requests.get(url)\n",
50+
"data = response.json()"
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": 4,
56+
"metadata": {},
57+
"outputs": [
58+
{
59+
"data": {
60+
"text/plain": [
61+
"27"
62+
]
63+
},
64+
"execution_count": 4,
65+
"metadata": {},
66+
"output_type": "execute_result"
67+
}
68+
],
69+
"source": [
70+
"len(data)"
71+
]
72+
},
73+
{
74+
"cell_type": "code",
75+
"execution_count": 111,
76+
"metadata": {},
77+
"outputs": [],
78+
"source": [
79+
"datetime.now()\n",
80+
"now = datetime.now()\n",
81+
"dt_string = now.strftime(\"%Y%m%d-%H%M%S\")\n",
82+
"\n",
83+
"folder_name = dt_string\n",
84+
"export_path = os.path.join('/media/scanvan/web/public/reporting', dt_string)\n",
85+
"os.makedirs(export_path, exist_ok=True)"
86+
]
87+
},
88+
{
89+
"cell_type": "code",
90+
"execution_count": 126,
91+
"metadata": {},
92+
"outputs": [
93+
{
94+
"name": "stderr",
95+
"output_type": "stream",
96+
"text": [
97+
"<ipython-input-126-9d5b9b80684a>:4: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0\n",
98+
"Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`\n",
99+
" for i in tqdm.tqdm_notebook(range(len(data))):\n"
100+
]
101+
},
102+
{
103+
"data": {
104+
"application/vnd.jupyter.widget-view+json": {
105+
"model_id": "ae407ffa21ca49b092f834ca4d784ace",
106+
"version_major": 2,
107+
"version_minor": 0
108+
},
109+
"text/plain": [
110+
"HBox(children=(HTML(value=''), FloatProgress(value=0.0, max=27.0), HTML(value='')))"
111+
]
112+
},
113+
"metadata": {},
114+
"output_type": "display_data"
115+
},
116+
{
117+
"name": "stdout",
118+
"output_type": "stream",
119+
"text": [
120+
"\n"
121+
]
122+
}
123+
],
124+
"source": [
125+
"root_path = '/media/scanvan/web/public/dev/'\n",
126+
"SRCS = []\n",
127+
"MASKS = []\n",
128+
"for i in tqdm.tqdm_notebook(range(len(data))):\n",
129+
" \n",
130+
" im_path = root_path + data[i]['filepath']\n",
131+
" SRCS.append(im_path)\n",
132+
" im = Image.open(im_path)\n",
133+
" width, height = im.size\n",
134+
" \n",
135+
" poly = []\n",
136+
" for order in data[i]['pitchYawOrders']:\n",
137+
" y = height * order['pitch'] / np.pi\n",
138+
" x = width * order['yaw'] / ( 2 * np.pi )\n",
139+
" poly.append( ( x , y ) )\n",
140+
" \n",
141+
" mask = Image.new('L', (width, height), 0)\n",
142+
" ImageDraw.Draw(mask).polygon(poly, outline=1, fill=1)\n",
143+
" mask_inv = cv2.bitwise_not(np.array(mask) * 255)\n",
144+
" \n",
145+
" mask_path = os.path.join('/media/scanvan/web/public/reporting', dt_string, data[i]['viewpointId'] + \"-mask.jpg\")\n",
146+
" MASKS.append(mask_path)\n",
147+
" imageio.imwrite(mask_path, mask_inv)\n",
148+
" \n",
149+
" # images are re-written at the same path\n",
150+
" !/home/descombe/Sources/image-suite/bin/image-missing-pixel -s $im_path -m $mask_path -e $im_path -k 16\n",
151+
" "
152+
]
153+
},
154+
{
155+
"cell_type": "code",
156+
"execution_count": null,
157+
"metadata": {},
158+
"outputs": [],
159+
"source": [
160+
"# useful to check annotations\n",
161+
"extent = 0, width, 0, height\n",
162+
"fig = plt.figure(frameon=False, figsize = (23,23))\n",
163+
"\n",
164+
"im1 = plt.imshow(im,\n",
165+
" extent=extent)\n",
166+
"\n",
167+
"im2 = plt.imshow(mask,\n",
168+
" extent=extent, alpha=0.5)\n",
169+
"\n",
170+
"plt.show()"
171+
]
172+
},
173+
{
174+
"cell_type": "code",
175+
"execution_count": null,
176+
"metadata": {},
177+
"outputs": [],
178+
"source": []
179+
}
180+
],
181+
"metadata": {
182+
"kernelspec": {
183+
"display_name": "Python 3",
184+
"language": "python",
185+
"name": "python3"
186+
},
187+
"language_info": {
188+
"codemirror_mode": {
189+
"name": "ipython",
190+
"version": 3
191+
},
192+
"file_extension": ".py",
193+
"mimetype": "text/x-python",
194+
"name": "python",
195+
"nbconvert_exporter": "python",
196+
"pygments_lexer": "ipython3",
197+
"version": "3.8.5"
198+
}
199+
},
200+
"nbformat": 4,
201+
"nbformat_minor": 4
202+
}

README.md

+78-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,79 @@
11
# scanvan-image-processing
2-
Image anonymization tool for the ScanVan project
2+
3+
Image anonymization tool for the ScanVan project. People and cars are blurred on panoramic images of the city of Sion, in order to be published on [website of the project](https://scanvan.dhlab.epfl.ch/) in compliance with the legal requirements for protection of personnal data.
4+
5+
This image processing pipeline is compatible with 3D reconstruction made with [openMVG](https://github.com/openMVG/openMVG).
6+
7+
### Architecture
8+
9+
```
10+
main-folder
11+
12+
└───images (jpg or png format required)
13+
14+
└───anonymization (created at step 1.)
15+
│ │
16+
│ └──segmentation (step 1.)
17+
│ │
18+
│ └──masks (step 2.)
19+
│ │
20+
│ └──blur (step 3.)
21+
│ │
22+
│ └──gaussian_blur (step 4.)
23+
24+
└───omvg outputs
25+
26+
└───sfm_data.bin
27+
28+
└───sfm_data_bin.json (cf. note below)
29+
30+
```
31+
32+
Note : *sfm_data.bin* must be converted to *sfm_data_bin.json* using *openMVG_main_ConvertSfMDataFormat -i sfm_data.bin -o sfm_data_bin.json* from [openMVG](https://github.com/openMVG/openMVG).
33+
34+
### 1. image segmentation
35+
36+
A segmentation model has been trained to detect people and vehicles. It has been trained using [dhSegment](https://dhsegment.readthedocs.io/en/latest/), the weights and training set are available [here](INSERT LINK).
37+
38+
```
39+
python segmentation_2d.py --model_dir <path-to-dhsegment-model-weights>
40+
--export_dir <path-to-main-folder>
41+
```
42+
![2d-segmentation](./img/probmaps.png)
43+
*probability maps of detection of people (left) and vehicles (right)*
44+
45+
### 2. create masks from the segmentation results
46+
47+
Masks are made from the probability maps, for each image.
48+
49+
```
50+
python custom_masks.py --folder <path-to-main-folder>
51+
52+
```
53+
![mask](./img/20200224-134957-886957-mask.png)
54+
55+
### 3. Blur images
56+
57+
Images are blurred with their corresponding mask.
58+
The blurring is done using a [missing pixel extrapolation](https://github.com/nils-hamel/image-suite/tree/master/src/image-missing-pixel).
59+
The *DST* folder has to be created before starting the blurring process.
60+
```
61+
./scanvan_mask $SRC $MASKS $DST
62+
63+
```
64+
![blur](./img/20200224-134957-886957-blur.png)
65+
66+
### 4. Blur camera (optional)
67+
68+
For aesthetical reasons, a slight blur is applied on all the images to cover the camera and car roof.
69+
70+
![gaussian_blur](./img/gaussian_blur_car_mask.png)
71+
72+
*Left : mask covering the motionless car roof and camera, right : image with gaussian blur.*
73+
74+
### 5. Website reporting tool
75+
76+
A reporting tool is available to the public on the website to notify personnal data which must be blurred and were not detected at step 1. The annotations are checked and the images' masks are updated accordingly on a weekly basis. The process is done with [website-reporting.ipynb](https://github.com/adescombes/scanvan-image-processing/website-reporting.ipynb).
77+
78+
79+

custom_masks.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
### les images publiées sur le site scanvan doivent être floutées à certains endroits :
2+
# le masque cachant la caméra et le toit de la voiture
3+
# /media/scanvan/mask/20200219-151940_dhlab-car/mask.png
4+
# les masques de segmentation comprenant des people ou des vehicles
5+
# /media/scanvan/model/camera_40008603-40009302/%date/%folder/point-cloud-segmentation/scanvan-sbvp-single/2d-segmentation
6+
7+
from glob import glob
8+
import imageio
9+
import os
10+
import sys
11+
sys.path.append('/home/descombe/point-cloud-segmentation/spherical-images/src_scanvan/')
12+
import matplotlib.pyplot as plt
13+
import numpy as np
14+
import cv2 as cv
15+
import tqdm
16+
import click
17+
18+
@click.command()
19+
@click.option('--folder', help="Directory of the project, containing 2d segmentation and jpg images")
20+
def make_masks(folder: str):
21+
22+
masks_folder = folder + "/anonymization/segmentation/"
23+
24+
images = glob(folder + "/images/*.jpg")
25+
export_path = folder + "/anonymization/masks/"
26+
27+
if not os.path.exists(export_path):
28+
os.makedirs(export_path, exist_ok=True)
29+
30+
dhlab_mask_path = '/media/scanvan/mask/20200219-151940_dhlab-car/mask.png'
31+
dhlab_mask = imageio.imread(dhlab_mask_path)
32+
33+
for image in tqdm.tqdm(images):
34+
35+
filename = os.path.basename(image)
36+
probmap_people = cv.imread(masks_folder + filename.replace(".jpg","-probmap-people.png"), 0)
37+
ret,mask_people = cv.threshold(probmap_people,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
38+
39+
kernel = np.array([[0, 0, -1, 0, 0],[0, 0, -1, 0, 0],[0, 0, 1, 0, 0],[0, 0, 1, 0, 0],[0, 0, 1, 0, 0]], dtype=np.uint8)
40+
mask_p_filter_it1 = cv.filter2D(mask_people, -1, kernel)
41+
mask_p_filter = cv.filter2D(mask_p_filter_it1, -1, kernel)
42+
43+
probmap_cars = cv.imread(masks_folder + filename.replace(".jpg","-probmap-vehicle.png"), 0)
44+
ret,mask_cars = cv.threshold(probmap_cars,0,255,cv.THRESH_BINARY+cv.THRESH_OTSU)
45+
46+
mask_thresh = cv.bitwise_or(mask_p_filter,mask_cars)
47+
mask_inv = cv.bitwise_not(mask_thresh)
48+
49+
# resize at the same dimensions as the original image (and dhlab-car mask), add dhlab-car mask
50+
mask_resized = np.zeros_like(dhlab_mask)
51+
height_scaled = dhlab_mask.shape[0] / mask_inv.shape[0]
52+
width_scaled = dhlab_mask.shape[1] / mask_inv.shape[1]
53+
54+
for i in range(mask_inv.shape[0]):
55+
for j in range(mask_inv.shape[1]):
56+
mask_resized[ int( np.floor( i * width_scaled ) ) : int( np.floor( ( i + 1 ) * width_scaled ) ) , int( np.floor( j * height_scaled ) ): int( np.floor( ( j + 1 ) * height_scaled ) ) ] += mask_inv[i,j]
57+
58+
#mask_total = cv.bitwise_and(dhlab_mask,mask_resized)
59+
image_mask = export_path + filename.replace(".jpg", "-mask.png")
60+
cv.imwrite(image_mask, mask_resized)
61+
62+
63+
64+
if __name__ == '__main__':
65+
make_masks()

gaussian_blur.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from glob import glob
2+
import os
3+
import numpy as np
4+
import cv2 as cv
5+
import click
6+
import tqdm
7+
8+
@click.command()
9+
@click.option('--folder', help='folder of images to be blurred')
10+
def gaussian_blur(folder: str):
11+
12+
# model_name = max(folder.split('/'), key=len).replace('-normalized','')
13+
# folder_scanvan = [x for x in glob('/media/scanvan/record/camera_40008603-40009302/*%s' % model_name.split('loop')[-1] ) if x.split('/')[-1][:8] == model_name[16:24]][0]
14+
folder_out = os.path.join(folder, "gaussian_blur")
15+
os.mkdir(folder_out)
16+
mask_inv = cv.imread('/media/scanvan/mask/20200219-151940_dhlab-car/mask.png',0)
17+
mask = cv.bitwise_not(mask_inv)
18+
19+
# images = glob(folder + "/images/*.jpg")
20+
images = glob(folder + "/blur/*.png")
21+
for img in tqdm.tqdm(images):
22+
23+
filename = os.path.basename(img).split('.')[0]
24+
# bmp_img = folder_scanvan + "/" + filename + ".png"
25+
bmp_img = img
26+
bmp = cv.imread(bmp_img)
27+
blur = cv.blur(bmp,(20,20),0)
28+
out = bmp.copy()
29+
out[mask!=0] = blur[mask!=0]
30+
31+
cv.imwrite(os.path.join(folder_out, filename + ".png"), out)
32+
33+
if __name__ == '__main__':
34+
gaussian_blur()

img/20200224-134957-886957-blur.png

23.4 MB
Loading

img/20200224-134957-886957-mask.png

68.6 KB
Loading

img/20200224-153525-814458-labels.png

7.01 MB
Loading

img/gaussian_blur_car_mask.png

1.26 MB
Loading

img/loop7-part3-labelled.png

7.42 MB
Loading

img/probmaps.jpg

93.9 KB
Loading

0 commit comments

Comments
 (0)