Skip to content

Commit 4247066

Browse files
LauraMeneghettifandreuzLaura Meneghetti (lmeneghe)Stefano Zanin
authored
Module Machine Learning: reduction NN (#21)
* new module smithers.plot. function for plotting complex numbers * rename file. remove unused parameter. add working example to docs * Added file for the reduction of a Neural Network * Add tutorial for construction reduced net * Added scripts and tutorial for Object Detection * RandSVD implementation, (a)hosvd implementation, tutorials tweaking, cut-off index estimator --------- Co-authored-by: Francesco Andreuzzi <[email protected]> Co-authored-by: Laura Meneghetti (lmeneghe) <[email protected]> Co-authored-by: Stefano Zanin <[email protected]>
1 parent b545ba5 commit 4247066

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+9004
-1
lines changed

.github/workflows/testing_pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Install Python dependencies
2828
run: |
2929
python3 -m pip install --upgrade pip
30-
python3 -m pip install .[test,vtk]
30+
python3 -m pip install .[test,vtk,ml]
3131
3232
- name: Test with pytest
3333
run: |

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
EXTRAS = {
2424
'docs': ['Sphinx', 'sphinx_rtd_theme'],
2525
'vtk': ['vtk'],
26+
'ml': ['torch', 'torchvision', 'scikit-learn', 'tqdm'],
2627
'test': ['pytest', 'pytest-cov'],
2728
}
2829

smithers/ml/dataset/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'''
2+
Dataset Preparation init
3+
'''
4+
__project__ = 'Object_Detector'
5+
__title__ = 'object_detector'
6+
__author__ = 'Laura Meneghetti, Nicola Demo'
7+
__maintainer__ = __author__
8+
9+
#from smithers.ml.dataset.create_json import *
10+
from smithers.ml.dataset.imagerec_dataset import Imagerec_Dataset
11+
from smithers.ml.dataset.pascalvoc_dataset import PascalVOCDataset

smithers/ml/dataset/change_xml.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'''
2+
Utilities to perform changes inside xml files.
3+
'''
4+
5+
from __future__ import print_function
6+
from os import listdir, path
7+
import re
8+
9+
10+
WIDTH_NEW = 800
11+
HEIGHT_NEW = 600
12+
13+
DIMLINE_MASK = r'<(?P<type1>width|height)>(?P<size>\d+)</(?P<type2>width|height)>'
14+
BBLINE_MASK = r'<(?P<type1>xmin|xmax|ymin|ymax)>(?P<size>\d+)</(?P<type2>xmin|xmax|ymin|ymax)>'
15+
NAMELINE_MASK = r'<(?P<type1>filename)>(?P<size>\S+)</(?P<type2>filename)>'
16+
PATHLINE_MASK = r'<(?P<type1>path)>(?P<size>.+)</(?P<type2>path)>'
17+
#regular expression
18+
19+
def resize_file(file_lines):
20+
'''
21+
Function performing the requested changes on the xml file, like changing
22+
th coordinates x and y of the boxes and the height and width accordingly.
23+
24+
:param list file_lines: list containing the lines of the file under
25+
consideration.
26+
'''
27+
new_lines = []
28+
for line in file_lines:
29+
match = re.search(DIMLINE_MASK, line) or re.search(BBLINE_MASK, line)
30+
print(match)
31+
if match is not None:
32+
size = match.group('size')
33+
type1 = match.group('type1')
34+
type2 = match.group('type2')
35+
print(size)
36+
print(type1)
37+
print(type2)
38+
if type1 != type2:
39+
raise ValueError('Malformed line: {}'.format(line))
40+
41+
if type1.startswith('f'):
42+
print('f')
43+
if type1.startswith('x'):
44+
size = int(size)
45+
new_size = int(round(size * WIDTH_NEW / width_old))
46+
new_line = '\t\t\t<{}>{}</{}>\n'.format(type1, new_size, type1)
47+
elif type1.startswith('y'):
48+
size = int(size)
49+
new_size = int(round(size * HEIGHT_NEW / height_old))
50+
new_line = '\t\t\t<{}>{}</{}>\n'.format(type1, new_size, type1)
51+
elif type1.startswith('w'):
52+
size = int(size)
53+
width_old = size
54+
new_size = int(WIDTH_NEW)
55+
new_line = '\t\t<{}>{}</{}>\n'.format(type1, new_size, type1)
56+
elif type1.startswith('h'):
57+
size = int(size)
58+
height_old = size
59+
new_size = int(HEIGHT_NEW)
60+
new_line = '\t\t<{}>{}</{}>\n'.format(type1, new_size, type1)
61+
else:
62+
raise ValueError('Unknown type: {}'.format(type1))
63+
#new_line = '\t\t\t<{}>{}</{}>\n'.format(type1, new_size, type1)
64+
new_lines.append(new_line)
65+
else:
66+
new_lines.append(line)
67+
68+
return ''.join(new_lines)
69+
70+
71+
def change_xml(nome_file):
72+
'''
73+
Function that chnages an xml file.
74+
75+
:param str nome_file: path where the xml files are
76+
contained (if it is a directory) or path of an xml file,
77+
'''
78+
if len(nome_file) < 1:
79+
raise ValueError('No file submitted')
80+
81+
if path.isdir(nome_file):
82+
# the argument is a directory
83+
files = listdir(nome_file)
84+
for file in files:
85+
file_path = path.join(nome_file, file)
86+
_, file_ext = path.splitext(file)
87+
if file_ext.lower() == '.xml':
88+
with open(file_path, 'r') as f:
89+
rows = f.readlines()
90+
91+
new_file = resize_file(rows)
92+
with open(file_path, 'w') as f:
93+
f.write(new_file)
94+
else:
95+
# otherwise i have a file (hopefully)
96+
with open(nome_file, 'r') as f:
97+
rows = f.readlines()
98+
99+
new_file = resize_file(rows)
100+
with open(nome_file, 'w') as f:
101+
f.write(new_file)
102+
103+
#insert name of the xml file or directory that contains them
104+
xml_file = 'voc_dir/VOC_cow/Annotations'
105+
change_xml(xml_file)

smithers/ml/dataset/create_json.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
'''
2+
Utilities to perform the creation of JSON files starting from the xml files.
3+
'''
4+
import json
5+
import xml.etree.ElementTree as ET
6+
import argparse
7+
import os
8+
9+
parser = argparse.ArgumentParser()
10+
parser.add_argument("voc07_path", help="Path to VOC2007 folder", type=str)
11+
parser.add_argument("voc12_path", help="Path to VOC2012 folder")
12+
parser.add_argument("output_folder", help="Path to JSON output folder",
13+
type=str)
14+
args = parser.parse_args()
15+
16+
voc07_path = args.voc07_path
17+
voc12_path = args.voc12_path
18+
output_folder = args.output_folder
19+
20+
# Label map
21+
# NOTE: The labels have to be written using lower case, since in the function
22+
# parse_annotation the label is transformed in the lower_case mode in order to
23+
# avoid problems if in the labeling phase a label was written in a wrong way.
24+
labels_list = ('aeroplane', 'bicycle', 'bird', 'boat',
25+
'bottle', 'bus', 'car', 'cat', 'chair',
26+
'cow', 'diningtable', 'dog', 'horse',
27+
'motorbike', 'person', 'pottedplant',
28+
'sheep', 'sofa', 'train', 'tvmonitor')
29+
#labels_list = ('cat', 'dog')
30+
label_map = {k: v + 1 for v, k in enumerate(labels_list)}
31+
label_map['background'] = 0
32+
rev_label_map = {v: k for k, v in label_map.items()} # Inverse mapping
33+
34+
35+
def parse_annotation(annotation_path):
36+
'''
37+
:param string annotation_path: string for the path to Annotations
38+
return dict: dictionary containing boxes, labels, difficulties for the
39+
different objects in a picture
40+
'''
41+
tree = ET.parse(annotation_path)
42+
root = tree.getroot()
43+
44+
boxes = list()
45+
labels = list()
46+
difficulties = list()
47+
for obj in root.iter('object'):
48+
49+
difficult = int(obj.find('difficult').text == '1')
50+
label = obj.find('name').text.lower().strip()
51+
if label not in label_map:
52+
continue
53+
54+
bbox = obj.find('bndbox')
55+
xmin = int(bbox.find('xmin').text)# - 1
56+
ymin = int(bbox.find('ymin').text)# - 1
57+
xmax = int(bbox.find('xmax').text)# - 1
58+
ymax = int(bbox.find('ymax').text)# - 1
59+
boxes.append([xmin, ymin, xmax, ymax])
60+
labels.append(label_map[label])
61+
difficulties.append(difficult)
62+
return {'boxes': boxes, 'labels': labels, 'difficulties': difficulties}
63+
64+
65+
def create_data_lists(voc07_path, voc12_path, out_folder):
66+
"""
67+
Create lists of images, the bounding boxes and labels of the objects
68+
in these images, and save these to file.
69+
70+
:param string voc07_path: path to the 'VOC2007' folder
71+
:param string voc12_path: path to the 'VOC2012' folder
72+
:param string out_folder: folder where the JSONs must be saved
73+
:output json files: saved json files obtained from our dataset
74+
(images + xml files) saved in the output folder chosen
75+
"""
76+
voc07_path = os.path.abspath(voc07_path)
77+
voc12_path = os.path.abspath(voc12_path)
78+
print(voc07_path)
79+
80+
train_images = list()
81+
train_objects = list()
82+
n_objects = 0
83+
84+
# Training data
85+
for path in [voc07_path, voc12_path]:
86+
if not path.endswith('/None'):
87+
# Find IDs of images in training data
88+
print(path)
89+
with open(os.path.join(path, 'ImageSets/Main/trainval.txt')) as f:
90+
ids = f.read().splitlines()
91+
for ID in ids:
92+
# Parse annotation's XML file
93+
objects = parse_annotation(
94+
os.path.join(path, 'Annotations', ID + '.xml'))
95+
if len(objects) == 0:
96+
continue
97+
n_objects += len(objects)
98+
train_objects.append(objects)
99+
train_images.append(os.path.join(path, 'JPEGImages', ID +
100+
'.jpg'))
101+
102+
assert len(train_objects) == len(train_images)
103+
104+
# Save to file
105+
with open(os.path.join(out_folder, 'TRAIN_images.json'), 'w') as j:
106+
json.dump(train_images, j)
107+
with open(os.path.join(out_folder, 'TRAIN_objects.json'), 'w') as j:
108+
json.dump(train_objects, j)
109+
with open(os.path.join(out_folder, 'label_map.json'), 'w') as j:
110+
json.dump(label_map, j) # save label map too
111+
112+
print(
113+
'\nThere are %d training images containing a total of %d \
114+
objects. Files have been saved to %s.'
115+
%(len(train_images), n_objects, os.path.abspath(out_folder)))
116+
117+
# Test data
118+
test_images = list()
119+
test_objects = list()
120+
n_objects = 0
121+
122+
# Find IDs of images in the test data
123+
with open(os.path.join(voc07_path, 'ImageSets/Main/test.txt')) as f:
124+
ids = f.read().splitlines()
125+
126+
for ID in ids:
127+
# Parse annotation's XML file
128+
ID = ID[0:6]
129+
objects = parse_annotation(
130+
os.path.join(voc07_path, 'Annotations', ID + '.xml'))
131+
if len(objects) == 0:
132+
continue
133+
test_objects.append(objects)
134+
n_objects += len(objects)
135+
test_images.append(os.path.join(voc07_path, 'JPEGImages', ID + '.jpg'))
136+
137+
assert len(test_objects) == len(test_images)
138+
139+
# Save to file
140+
with open(os.path.join(out_folder, 'TEST_images.json'), 'w') as j:
141+
json.dump(test_images, j)
142+
with open(os.path.join(out_folder, 'TEST_objects.json'), 'w') as j:
143+
json.dump(test_objects, j)
144+
145+
print(
146+
'\nThere are %d test images containing a total of %d \
147+
objects. Files have been saved to %s.'
148+
% (len(test_images), n_objects, os.path.abspath(out_folder)))
149+
150+
151+
create_data_lists(voc07_path, voc12_path, output_folder)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'''
2+
Module focused on the creation of a custom dataset class in order
3+
to use our custom dataset for the problem of image recognition
4+
and thus classification.
5+
'''
6+
import os
7+
import torch
8+
from torch.utils.data import Dataset
9+
from PIL import Image
10+
from torchvision import transforms
11+
12+
13+
# CUSTOM DATASET CLASS
14+
class Imagerec_Dataset(Dataset):
15+
'''
16+
Class that handles the creation of a custom dataset class to
17+
be used by data loader.
18+
:param pandas.DataFrame img_data: tabular containing all the
19+
relations (image, label)
20+
:param str img_path: path to the directiory containing all the
21+
images
22+
:param transform_obj transform: list of transoforms to apply to
23+
images. Defaul value set to None.
24+
:param list resize_dim: list of integers corresponding to the
25+
size to which we want to resize the images
26+
'''
27+
def __init__(self, img_data, img_path, resize_dim, transform=None):
28+
self.img_data = img_data
29+
self.img_path = img_path
30+
self.resize_dim = resize_dim
31+
self.transform = transform
32+
self.targets = self.img_data['encoded_labels']
33+
34+
def __len__(self):
35+
'''
36+
Function that returns the number of images in the dataset
37+
:return int: integer number representing the number of
38+
images in the dataset
39+
'''
40+
return len(self.img_data)
41+
42+
def __getitem__(self, index):
43+
'''
44+
Function that returns the data and labels
45+
:param int index: number representing a specific image in the
46+
dataset
47+
:return tensor image, label: image and label associated
48+
with the index given as input
49+
'''
50+
img_name = os.path.join(self.img_path,
51+
self.img_data.loc[index, 'labels'],
52+
self.img_data.loc[index, 'Images'])
53+
image = Image.open(img_name)
54+
image = image.resize((self.resize_dim[0], self.resize_dim[1]))
55+
label = torch.tensor(self.img_data.loc[index, 'encoded_labels'])
56+
if self.transform is not None:
57+
image = self.transform(image)
58+
else:
59+
image = transforms.ToTensor()(image)
60+
return image, label
61+
62+
def getdata(self, index):
63+
'''
64+
Function that returns a subset of the dataset
65+
:param list index: number representing a specific image in the
66+
dataset
67+
:return: subset of the dataset composed by obs of type (img, label)
68+
:rtype: list
69+
'''
70+
output = []
71+
for idx in index:
72+
image, label = self.__getitem__(idx)
73+
output.append([image, label])
74+
return output

0 commit comments

Comments
 (0)