├── .gitignore ├── LICENSE ├── README.MD ├── image.py ├── images_example ├── IMG_20181109_165212.jpg ├── IMG_20181109_165212.xml ├── IMG_20181109_165212_new.jpg ├── IMG_20181109_165212_new.xml └── boxed_IMG_20181109_165212.jpg ├── main.py ├── requirements.txt └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | output/* 3 | *.zip 4 | /.vs 5 | /env 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Italo José 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Resize-pascal-voc 2 | It's a package for resizing ALL your dataset images (into local folder) and changing the pascal_VOC coodinates in your `.xml` files. 3 | 4 | # Index 5 | 6 | - [How it works?](#How-it-works?) 7 | - [Warnings](#Warnings) 8 | - [Install requirements](#Install-requirements) 9 | - [Usage](#Usage) 10 | - [Example](#Example) 11 | - [Folder structure example](#Folder-structure-example) 12 | - [Results](#Results) 13 | - [Original Image](#Original-Image) 14 | - [Resized image](#Resized-image) 15 | - [Boundies box image](#Boundies-box-image) 16 | - [The calculus behind the scenes](#The-Calculus-Behind-the-Scenes:) 17 | - [Parameters](#Parameters) 18 | 19 | ### How it works? 20 | - Walk by paths and searching by .jpg and .xml files 21 | - Resize the image and change the XY coordinates of each object in xml file. 22 | - Save the new files into output path. [Folder structure example](#Folder-structure-example) for more details 23 | - If save_box_images = 1, draw the boundies box in the resized image and save it in an other file(output_path/boxes_images/)) 24 | 25 | ## Warnings 26 | 1º: Don't worry if you have a big folder structure with many nested folders, the package will walk recursively in your dataset folder and recreate the same structure into output_path. 27 | 28 | 2º: The .jpg and .xml files must be in the same folder 29 | 30 | ## Install requirements 31 | ``` 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | ## Usage 36 | ``` 37 | python3 main.py -p --output --new_x --new_y --save_box_images " 38 | ``` 39 | 40 | ## Example 41 | ``` 42 | python3 main.py -p /home/italojs/Pictures/dataset --output ./output --new_x 150 --new_y 150 --save_box_images 1 43 | ``` 44 | 45 | ## Folder structure example 46 | 47 | Look this *randon* folder structure with ALL my dataset 48 | ``` 49 | dataset 50 | ├── IMG_20181109_165212.jpg 51 | ├── IMG_20181109_165212.xml 52 | ├── IMG_20181109_165213.jpg 53 | ├── IMG_20181109_165213.xml 54 | ├── test 55 | │   ├── IMG_20181109_163524.jpg 56 | │   ├── IMG_20181109_163524.xml 57 | │   ├── IMG_20181109_163525.jpg 58 | │   └── IMG_20181109_163525.xml 59 | ├── train 60 | │   ├── class1 61 | │   │   ├── IMG_20181109_162519.jpg 62 | │   │   ├── IMG_20181109_162519.xml 63 | │   │   ├── IMG_20181109_162523.jpg 64 | │   │   └── IMG_20181109_162523.xml 65 | │   ├── class2 66 | │   │   ├── IMG_20181109_162814.jpg 67 | │   │   ├── IMG_20181109_162814.xml 68 | │   │   ├── IMG_20181109_162818.jpg 69 | │   │   └── IMG_20181109_162818.xml 70 | │   ├── IMG_20181109_163315.jpg 71 | │   ├── IMG_20181109_163315.xml 72 | │   ├── IMG_20181109_163316.jpg 73 | │   └── IMG_20181109_163316.xml 74 | └── validation 75 | ├── IMG_20181109_164824.jpg 76 | ├── IMG_20181109_164824.xml 77 | ├── IMG_20181109_164825.jpg 78 | └── IMG_20181109_164825.xml 79 | ``` 80 | 81 | After executed the package with: 82 | ``` 83 | python3 main.py -p /home/italojs/Pictures/dataset --output ./output --new_x 150 --new_y 150 --save_box_images 1 84 | ``` 85 | The package will resize the images, rewrite the xml files and create the same folder structure into `output path` with new images and xml files. 86 | ``` 87 | output 88 | ├── boxes_images 89 | │ ├── boxed_IMG_20181109_165212.jpg 90 | │ └── boxed_IMG_20181109_165213.jpg 91 | ├── IMG_20181109_165212_new.jpg 92 | ├── IMG_20181109_165212_new.xml 93 | ├── IMG_20181109_165213_new.jpg 94 | ├── IMG_20181109_165213_new.xml 95 | ├── test 96 | │ ├── IMG_20181109_163524_new.jpg 97 | │ ├── IMG_20181109_163524_new.xml 98 | │ ├── IMG_20181109_163525_new.jpg 99 | │ └── IMG_20181109_163525_new.xml 100 | ├── train 101 | │ ├── class1 102 | │ │ ├── IMG_20181109_162519_new.jpg 103 | │ │ ├── IMG_20181109_162519_new.xml 104 | │ │ ├── IMG_20181109_162523_new.jpg 105 | │ │ └── IMG_20181109_162523_new.xml 106 | │ ├── class2 107 | │ │ ├── IMG_20181109_162814_new.jpg 108 | │ │ ├── IMG_20181109_162814_new.xml 109 | │ │ ├── IMG_20181109_162818_new.jpg 110 | │ │ └── IMG_20181109_162818_new.xml 111 | │ ├── IMG_20181109_163315_new.jpg 112 | │ ├── IMG_20181109_163315_new.xml 113 | │ ├── IMG_20181109_163316_new.jpg 114 | │ └── IMG_20181109_163316_new.xml 115 | └── validation 116 | ├── IMG_20181109_164824_new.jpg 117 | ├── IMG_20181109_164824_new.xml 118 | ├── IMG_20181109_164825_new.jpg 119 | └── IMG_20181109_164825_new.xml 120 | ``` 121 | 122 | ## Results 123 | 124 | ### Original Image 125 | Here wee have the original image (650x590) an your xml: 126 | 127 | ![original_image](images_example/IMG_20181109_165212.jpg) 128 | 129 | ``` xml 130 | 131 | imagens 132 | IMG_20181109_165212 133 | [IMAGE_PATH]\IMG_20181109_165212.jpg 134 | 135 | Unknown 136 | 137 | 138 | 650 139 | 590 140 | 3 141 | 142 | 0 143 | 144 | CNH 145 | Unspecified 146 | 0 147 | 0 148 | 149 | 74 150 | 192 151 | 267 152 | 499 153 | 154 | 155 | 156 | CNH 157 | Unspecified 158 | 0 159 | 0 160 | 161 | 297 162 | 168 163 | 483 164 | 478 165 | 166 | 167 | 168 | CNH 169 | Unspecified 170 | 0 171 | 0 172 | 173 | 310 174 | 1 175 | 499 176 | 103 177 | 178 | 179 | 180 | ``` 181 | 182 | ### Resized image 183 | Here is the resized image (150x150) and new xml: 184 | 185 | ![resized_image](images_example/IMG_20181109_165212_new.jpg) 186 | 187 | ```xml 188 | 189 | imagens 190 | IMG_20181109_165212 191 | C:\Users\pc-casa\Music\imagens\IMG_20181109_165212.jpg 192 | 193 | Unknown 194 | 195 | 196 | 650 197 | 590 198 | 3 199 | 200 | 0 201 | 202 | CNH 203 | Unspecified 204 | 0 205 | 0 206 | 207 | 17 208 | 49 209 | 62 210 | 127 211 | 212 | 213 | 214 | CNH 215 | Unspecified 216 | 0 217 | 0 218 | 219 | 69 220 | 43 221 | 111 222 | 122 223 | 224 | 225 | 226 | CNH 227 | Unspecified 228 | 0 229 | 0 230 | 231 | 72 232 | 0 233 | 115 234 | 26 235 | 236 | 237 | 238 | ``` 239 | 240 | ### Boundies box image 241 | 242 | Here is a resized image (150x150), but with the boundies box drawed: 243 | 244 | ![box_image](images_example/boxed_IMG_20181109_165212.jpg) 245 | 246 | ### The Calculus Behind the Scenes: 247 | 248 | For an original image with 650x590 to be resized by 150x150 249 | 250 | - scaleX = 150/650 = 3/13 251 | - scaleY = 150/590 = 15/59 252 | 253 | - Original: 254 | 255 | $$x_{min} = 74$$ 256 | 257 | $$y_{min} = 192$$ 258 | 259 | $$x_{max} = 267$$ 260 | 261 | $$y_{max} = 499$$ 262 | 263 | - Scalling: 264 | 265 | $$x_{min} = 74\times 3/13 = 17.08 \approx 17$$ 266 | 267 | $$y_{min} = 192\times 15/59 = 48.81\approx 49$$ 268 | 269 | $$x_{max} = 267\times 3/13 = 61.62 \approx 62$$ 270 | 271 | $$y_{max} = 499\times 15/59 = 126.86\approx 127$$ 272 | 273 | The same calculation is used to calculate the coordinates of the bounding boxes. 274 | 275 | ## Parameters 276 | To know more about parameters use `python main.py -h`: 277 | ``` 278 | usage: main.py [-h] -p DATASET_PATH -o OUTPUT_PATH -x X -y Y [-m MODE] [-s SAVE_BOX_IMAGES] 279 | ``` 280 | 281 | Parameter|Description 282 | -|- 283 | **-h**, **--help** | Show this help message and exit 284 | **-p** **--path** *DATASET_PATH* | Path to dataset data ?(image and annotations). 285 | **-o** **--output** *OUTPUT_PATH* | Path that will be saved the resized dataset 286 | **-x** **--new_x** *X* | The new x images size / scale / percentage / target 287 | **-y** **--new_y** *Y* | The new y images size / scale / percentage / target 288 | **-m** **--mode** *MODE* | The resize mode: size, scale, percentage or target 289 | **-s** **--save_box_images** *SAVE_BOX_IMAGES* | If True, it will save the resized image and a drawn image with the boxes in the images 290 | -------------------------------------------------------------------------------- /image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import numpy as np 4 | from utils import get_file_name 5 | import xml.etree.ElementTree as ET 6 | from math import floor 7 | 8 | 9 | def process_image(file_path, output_path, x, y, save_box_images, mode): 10 | (base_dir, file_name, ext) = get_file_name(file_path) 11 | image_path = '{}/{}.{}'.format(base_dir, file_name, ext) 12 | xml = '{}/{}.xml'.format(base_dir, file_name) 13 | try: 14 | resize( 15 | image_path, 16 | xml, 17 | (x, y), 18 | output_path, 19 | mode, 20 | save_box_images=save_box_images 21 | ) 22 | except Exception as e: 23 | print('[ERROR] error with {}\n file: {}'.format(image_path, e)) 24 | print('--------------------------------------------------') 25 | 26 | 27 | def draw_box(boxes, image, path): 28 | for i in range(0, len(boxes)): 29 | cv2.rectangle(image, (boxes[i][2], boxes[i][3]), (boxes[i][4], boxes[i][5]), (255, 0, 0), 1) 30 | cv2.imwrite(path, image) 31 | 32 | 33 | def resize(image_path, 34 | xml_path, 35 | newSize, 36 | output_path, 37 | mode, 38 | save_box_images=False, 39 | verbose=False 40 | ): 41 | 42 | image = cv2.imread(image_path) 43 | 44 | mode = mode and mode.lower() 45 | # Standard resize mode 46 | if mode is None or mode == 'size': 47 | newSize = (int(newSize[0]), int(newSize[1])) 48 | scale_x = float(newSize[0]) / float(image.shape[1]) 49 | scale_y = float(newSize[1]) / float(image.shape[0]) 50 | image = cv2.resize(src=image, dsize=(newSize[0], newSize[1])) 51 | else: 52 | # Scaling by factor or percentage of the original image size 53 | if mode == 'scale' or mode == 'percentage': 54 | mul = 0.01 if mode == 'percentage' else 1.0 55 | newSize = ( 56 | floor(float(image.shape[1]) * float(newSize[0]) * mul), 57 | floor(float(image.shape[0]) * float(newSize[1]) * mul)) 58 | scale_x = newSize[0] / image.shape[1] 59 | scale_y = newSize[1] / image.shape[0] 60 | interp = cv2.INTER_LINEAR if (scale_x > 1.0 or scale_y > 1.0) else cv2.INTER_AREA 61 | image = cv2.resize( 62 | src=image, 63 | dsize=(0, 0), dst=None, 64 | fx=scale_x, fy=scale_y, interpolation=interp) 65 | # Target mode; choose the correct ratio to reach one of the x/y targets without oversize 66 | elif mode == 'target': 67 | ratio = float(int(newSize[0])) / float(image.shape[1]) 68 | targetRatio = float(int(newSize[1])) / float(image.shape[0]) 69 | ratio = targetRatio if targetRatio < ratio else ratio 70 | scale_x = scale_y = ratio 71 | interp = cv2.INTER_LINEAR if (scale_x > 1.0 or scale_y > 1.0) else cv2.INTER_AREA 72 | image = cv2.resize( 73 | src=image, 74 | dsize=(0, 0), dst=None, 75 | fx=scale_x, fy=scale_y, interpolation=interp) 76 | else: 77 | raise Exception(f"Invalid resize mode: {mode}") 78 | 79 | newBoxes = [] 80 | xmlRoot = ET.parse(xml_path).getroot() 81 | xmlRoot.find('filename').text = image_path.split('/')[-1] 82 | size_node = xmlRoot.find('size') 83 | size_node.find('width').text = str(newSize[0]) 84 | size_node.find('height').text = str(newSize[1]) 85 | 86 | for member in xmlRoot.findall('object'): 87 | bndbox = member.find('bndbox') 88 | 89 | xmin = bndbox.find('xmin') 90 | ymin = bndbox.find('ymin') 91 | xmax = bndbox.find('xmax') 92 | ymax = bndbox.find('ymax') 93 | 94 | xmin.text = str(int(np.round(float(xmin.text) * scale_x))) 95 | ymin.text = str(int(np.round(float(ymin.text) * scale_y))) 96 | xmax.text = str(int(np.round(float(xmax.text) * scale_x))) 97 | ymax.text = str(int(np.round(float(ymax.text) * scale_y))) 98 | 99 | newBoxes.append([ 100 | 1, 101 | 0, 102 | int(float(xmin.text)), 103 | int(float(ymin.text)), 104 | int(float(xmax.text)), 105 | int(float(ymax.text)) 106 | ]) 107 | 108 | (_, file_name, ext) = get_file_name(image_path) 109 | cv2.imwrite(os.path.join(output_path, '.'.join([file_name, ext])), image) 110 | 111 | tree = ET.ElementTree(xmlRoot) 112 | tree.write('{}/{}.xml'.format(output_path, file_name, ext)) 113 | if int(save_box_images): 114 | save_path = '{}/boxes_images/boxed_{}'.format(output_path, ''.join([file_name, '.', ext])) 115 | draw_box(newBoxes, image, save_path) 116 | -------------------------------------------------------------------------------- /images_example/IMG_20181109_165212.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italojs/resize_dataset_pascalvoc/e8e806866ebf0e3c28884a1d641453ec78b3524f/images_example/IMG_20181109_165212.jpg -------------------------------------------------------------------------------- /images_example/IMG_20181109_165212.xml: -------------------------------------------------------------------------------- 1 | 2 | imagens 3 | IMG_20181109_165212 4 | C:\Users\pc-casa\Music\imagens\IMG_20181109_165212.jpg 5 | 6 | Unknown 7 | 8 | 9 | 650 10 | 590 11 | 3 12 | 13 | 0 14 | 15 | CNH 16 | Unspecified 17 | 0 18 | 0 19 | 20 | 74 21 | 192 22 | 267 23 | 499 24 | 25 | 26 | 27 | CNH 28 | Unspecified 29 | 0 30 | 0 31 | 32 | 297 33 | 168 34 | 483 35 | 478 36 | 37 | 38 | 39 | CNH 40 | Unspecified 41 | 0 42 | 0 43 | 44 | 310 45 | 1 46 | 499 47 | 103 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /images_example/IMG_20181109_165212_new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italojs/resize_dataset_pascalvoc/e8e806866ebf0e3c28884a1d641453ec78b3524f/images_example/IMG_20181109_165212_new.jpg -------------------------------------------------------------------------------- /images_example/IMG_20181109_165212_new.xml: -------------------------------------------------------------------------------- 1 | 2 | imagens 3 | IMG_20181109_165212 4 | C:\Users\pc-casa\Music\imagens\IMG_20181109_165212.jpg 5 | 6 | Unknown 7 | 8 | 9 | 650 10 | 590 11 | 3 12 | 13 | 0 14 | 15 | CNH 16 | Unspecified 17 | 0 18 | 0 19 | 20 | 17.0 21 | 49.0 22 | 62.0 23 | 127.0 24 | 25 | 26 | 27 | CNH 28 | Unspecified 29 | 0 30 | 0 31 | 32 | 69.0 33 | 43.0 34 | 111.0 35 | 122.0 36 | 37 | 38 | 39 | CNH 40 | Unspecified 41 | 0 42 | 0 43 | 44 | 72.0 45 | 0.0 46 | 115.0 47 | 26.0 48 | 49 | 50 | -------------------------------------------------------------------------------- /images_example/boxed_IMG_20181109_165212.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/italojs/resize_dataset_pascalvoc/e8e806866ebf0e3c28884a1d641453ec78b3524f/images_example/boxed_IMG_20181109_165212.jpg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from image import process_image 4 | from utils import create_path, add_end_slash 5 | from optparse import OptionParser 6 | 7 | parser = argparse.ArgumentParser() 8 | 9 | parser.add_argument( 10 | '-p', 11 | '--path', 12 | dest='dataset_path', 13 | help='Path to dataset data ?(image and annotations).', 14 | required=True 15 | ) 16 | parser.add_argument( 17 | '-o', 18 | '--output', 19 | dest='output_path', 20 | help='Path that will be saved the resized dataset', 21 | default='./', 22 | required=True 23 | ) 24 | parser.add_argument( 25 | '-x', 26 | '--new_x', 27 | dest='x', 28 | help='The new x images size / scale / percentage / target', 29 | required=True 30 | ) 31 | parser.add_argument( 32 | '-y', 33 | '--new_y', 34 | dest='y', 35 | help='The new y images size / scale / percentage / target', 36 | required=True 37 | ) 38 | 39 | parser.add_argument( 40 | '-m', 41 | '--mode', 42 | dest='mode', 43 | help='Resize mode: size, scale, percentage or target', 44 | required=False 45 | ) 46 | 47 | parser.add_argument( 48 | '-s', 49 | '--save_box_images', 50 | dest='save_box_images', 51 | help='If True, it will save the resized image and a drawed image with the boxes in the images', 52 | default=0 53 | ) 54 | 55 | IMAGE_FORMATS = ('.jpeg', '.JPEG', '.png', '.PNG', '.jpg', '.JPG') 56 | 57 | args = parser.parse_args() 58 | 59 | create_path(args.output_path) 60 | if int(args.save_box_images): 61 | create_path(''.join([args.output_path, '/boxes_images'])) 62 | 63 | args.dataset_path = add_end_slash(args.dataset_path) 64 | args.output_path = add_end_slash(args.output_path) 65 | 66 | for root, _, files in os.walk(args.dataset_path): 67 | output_path = os.path.join(args.output_path, root[len(args.dataset_path):]) 68 | create_path(output_path) 69 | 70 | for file in files: 71 | if file.endswith(IMAGE_FORMATS): 72 | file_path = os.path.join(root, file) 73 | process_image(file_path, output_path, args.x, args.y, args.save_box_images, args.mode) 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv_python==3.4.3.18 2 | numpy==1.15.1 3 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | 5 | def add_end_slash(path): 6 | if path[-1] is not '/': 7 | return path + '/' 8 | return path 9 | 10 | 11 | def create_path(path): 12 | path = Path(path) 13 | path.mkdir(parents=True, exist_ok=True) 14 | 15 | 16 | def get_file_name(path): 17 | base_dir = os.path.dirname(path) 18 | file_name, ext = os.path.splitext(os.path.basename(path)) 19 | ext = ext.replace(".", "") 20 | return (base_dir, file_name, ext) 21 | --------------------------------------------------------------------------------