Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new augmentation methods and the option of augmentation by addition instead of replacement #653

Merged
merged 89 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
17065c5
NOTEBOOK FOR DATA AUGMENTATION
xaviersantos Mar 31, 2021
0b243c8
SMALL SAMPLE DATASET
xaviersantos Apr 1, 2021
852f049
TRANSLACTIONS GLOBAL AND LOCAL
xaviersantos Apr 2, 2021
39e957f
LOCAL AUGMENTATIONS FINISHED
xaviersantos Apr 2, 2021
c85634b
ADJUSTED DATA FORMATS TO PCDET YAML COMPATIBILITY
xaviersantos Apr 5, 2021
5b57485
BUGFIX
xaviersantos Apr 5, 2021
c95313e
ADDED TITLES
xaviersantos Apr 5, 2021
fcc16c5
ADAPTED OPENPCDET TO FIT NEW AUGMENTATIONS
xaviersantos Apr 5, 2021
bf49660
SAVE IMAGES OF THE DATA AUGMENTATION EXAMPLES
xaviersantos Apr 5, 2021
58692d7
LOCAL TRANSFORMATION RANDOM VALUES VARY WITH OBJECT AND NOT SCENE
xaviersantos Apr 7, 2021
4cdfd92
FIXED YAML
xaviersantos Apr 8, 2021
005b48e
FIXED BUGS IN PARAMETERS
xaviersantos Apr 12, 2021
c153789
FIX IN LOCAL BOXES
xaviersantos Apr 12, 2021
d61277d
NEW BOX FILTER
xaviersantos Apr 12, 2021
f244993
FRUSTUM DROPOUT
xaviersantos Apr 13, 2021
2f5270f
FRUSTUM DROPOUT IMPLEMENTED ON OPENPCDET
xaviersantos Apr 13, 2021
c20c448
REMOVE FIGURES FROM VERSION CONTROL
xaviersantos Apr 13, 2021
c1e9e5d
MORE EFFICIENT LOCAL METHOD
xaviersantos Apr 13, 2021
7f1f088
LOCAL TRANSFORMATION WITH NUMPY SPED UP
xaviersantos Apr 13, 2021
2736850
BUGFIX ON AUGMENTATION FUNCTION
xaviersantos Apr 13, 2021
f55055d
GET PREDICTED LABELS
xaviersantos Apr 29, 2021
a44e6b6
AUGMENTATION DONE BY ADDITION
xaviersantos May 1, 2021
57d5b37
RANDOM SELECTION OF REMAINIG FRAMES
xaviersantos May 3, 2021
d07eb76
LOGGER WITH TRANSFORMATION DETAILS
xaviersantos May 8, 2021
78cdb44
CHANGED COUNTER TO SAMPLE IDX SELECTION METHOD
xaviersantos May 10, 2021
be1cfde
BUGFIX ON KITTY_DATABASE
xaviersantos May 10, 2021
def3c5c
BUGFIX
xaviersantos May 10, 2021
7c71e94
Merge branch 'dev' of https://github.com/xaviersantos/OpenPCDet into dev
xaviersantos May 10, 2021
a906ece
BUGFIX
xaviersantos May 10, 2021
51e90a6
FIX ON GENERATOR
xaviersantos May 10, 2021
fe0180c
DEBUG LOGS TO FILE
xaviersantos May 11, 2021
193010e
FIXED LOGGER
xaviersantos May 11, 2021
8017f64
SAVE POINT CLOUDS TO OBJ FILE
xaviersantos May 11, 2021
ae7b228
UPDATED NOTEBOOK
xaviersantos May 13, 2021
e827761
CLEANUP
xaviersantos May 20, 2021
1610864
.gitignore is now working
xaviersantos May 20, 2021
90b2811
fixed .gitignore
xaviersantos May 20, 2021
a20816f
REMOVED YAML FILES FROM .gitignore
xaviersantos May 20, 2021
575ea35
parse results to a csv
xaviersantos May 21, 2021
615d43d
yaml changed
xaviersantos May 21, 2021
c6e6bba
fixed gitignore
xaviersantos May 21, 2021
c3cc6fc
Merge branch 'dev' of https://github.com/xaviersantos/OpenPCDet into dev
xaviersantos May 21, 2021
8d99396
FIXED BUG THAT DUPLICATED THE TEST SET
xaviersantos Jun 1, 2021
765eb93
small fixes on yaml
xaviersantos Jun 30, 2021
ad8f7ca
fix gitignore
xaviersantos Jun 30, 2021
7139a59
updated generate_sets.py
xaviersantos Jul 3, 2021
a5f2cc9
fixed csv_parser.py
xaviersantos Jul 4, 2021
68b2e53
Delete output directory
xaviersantos Jul 5, 2021
97e7d5e
Delete output directory
xaviersantos Jul 5, 2021
dbe3ab3
saving figures with better resolution
xaviersantos Jul 28, 2021
3e70520
implemented a way to chose to augment the raw pcs
xaviersantos Jul 29, 2021
3679d3f
updated csv_parser.py
xaviersantos Jul 29, 2021
0945547
Merge branch 'dev' of https://github.com/xaviersantos/OpenPCDet into dev
xaviersantos Jul 29, 2021
27326dc
fixed yamls to include the new selection system
xaviersantos Jul 29, 2021
1ca2d19
notebook updated
xaviersantos Sep 6, 2021
fabd838
Merge branch 'dev' of https://github.com/xaviersantos/OpenPCDet into dev
xaviersantos Sep 6, 2021
75c2014
mosaic added
xaviersantos Sep 8, 2021
c74f6c6
new models
xaviersantos Sep 12, 2021
3eb969b
fixed gitignore
xaviersantos Oct 16, 2021
c176d46
fixed gitignore
xaviersantos Oct 16, 2021
ec8a112
applied changes
xaviersantos Oct 16, 2021
82be976
Merge branch 'master' of https://github.com/xaviersantos/OpenPCDet
xaviersantos Oct 16, 2021
c60b53d
Delete data_aumentation.ipynb
xaviersantos Oct 16, 2021
0859079
Delete pointpillar_fullaug.yaml
xaviersantos Oct 16, 2021
e4727de
updated gitignore
xaviersantos Oct 16, 2021
94bd8f6
Merge branch 'master' into dev
xaviersantos Oct 16, 2021
9dae743
bugfix
xaviersantos Oct 16, 2021
6623a16
Merge branch 'dev' of https://github.com/xaviersantos/OpenPCDet into dev
xaviersantos Oct 16, 2021
0c4cd7e
updated training and notebook
xaviersantos Oct 18, 2021
585072c
Merge branch 'dev' of https://github.com/xaviersantos/OpenPCDet into dev
xaviersantos Oct 18, 2021
5fa4acc
updated gitignore
xaviersantos Oct 18, 2021
b98711f
cleanup code
xaviersantos Oct 18, 2021
4bdf3d6
updated yamls
xaviersantos Oct 18, 2021
6c85734
Merge branch 'dev'
xaviersantos Oct 18, 2021
0794d5c
reset
xaviersantos Nov 20, 2021
39e9c52
Merge branch 'master' of https://github.com/xaviersantos/OpenPCDet
xaviersantos Nov 20, 2021
eeb9ffa
Merge branch 'master' of https://github.com/xaviersantos/OpenPCDet
xaviersantos Nov 20, 2021
976d5bb
new augmentation methods
xaviersantos Nov 20, 2021
c5eedbd
example conf for pointpillars
xaviersantos Nov 20, 2021
f0b2e06
updated example config
xaviersantos Nov 20, 2021
099dcc1
bugfix on the config file
xaviersantos Nov 20, 2021
887f8a2
fixed get_points_in_box function
xaviersantos Dec 3, 2021
3c8c5d5
fixed local frustum dropout
xaviersantos Dec 3, 2021
bdbe9b6
fixed pointpillar_newaugs.yaml
xaviersantos Dec 7, 2021
5a7a8df
bugfixed: import math for some augmentations
sshaoshuai Dec 7, 2021
1a0f213
Merge branch 'master' into xaviersantos-master
sshaoshuai Dec 7, 2021
3db5c1b
increased margin in point selection for local augmentation
xaviersantos Dec 9, 2021
6896834
bugfixed: velocity should not be translated for translation augs. Add…
sshaoshuai Dec 9, 2021
46bc7f7
bugfixed: add back the aug of random_image_flip
sshaoshuai Dec 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 357 additions & 5 deletions pcdet/datasets/augmentor/augmentor_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import copy
import numpy as np
import math

from ...utils import common_utils

Expand All @@ -22,7 +22,6 @@ def random_flip_along_x(gt_boxes, points):

return gt_boxes, points


def random_flip_along_y(gt_boxes, points):
"""
Args:
Expand All @@ -41,7 +40,6 @@ def random_flip_along_y(gt_boxes, points):

return gt_boxes, points


def global_rotation(gt_boxes, points, rot_range):
"""
Args:
Expand All @@ -62,7 +60,6 @@ def global_rotation(gt_boxes, points, rot_range):

return gt_boxes, points


def global_scaling(gt_boxes, points, scale_range):
"""
Args:
Expand All @@ -76,6 +73,7 @@ def global_scaling(gt_boxes, points, scale_range):
noise_scale = np.random.uniform(scale_range[0], scale_range[1])
points[:, :3] *= noise_scale
gt_boxes[:, :6] *= noise_scale

return gt_boxes, points

def random_image_flip_horizontal(image, depth_map, gt_boxes, calib):
Expand Down Expand Up @@ -115,4 +113,358 @@ def random_image_flip_horizontal(image, depth_map, gt_boxes, calib):
aug_depth_map = depth_map
aug_gt_boxes = gt_boxes

return aug_image, aug_depth_map, aug_gt_boxes
return aug_image, aug_depth_map, aug_gt_boxes

def random_translation_along_x(gt_boxes, points, offset_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
offset_range: [min max]]
Returns:
"""
offset = np.random.uniform(offset_range[0], offset_range[1])

points[:, 0] += offset
gt_boxes[:, 0] += offset

# if gt_boxes.shape[1] > 7:
# gt_boxes[:, 7] += offset

return gt_boxes, points

def random_translation_along_y(gt_boxes, points, offset_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
offset_range: [min max]]
Returns:
"""
offset = np.random.uniform(offset_range[0], offset_range[1])

points[:, 1] += offset
gt_boxes[:, 1] += offset

# if gt_boxes.shape[1] > 8:
# gt_boxes[:, 8] += offset

return gt_boxes, points

def random_translation_along_z(gt_boxes, points, offset_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
offset_range: [min max]]
Returns:
"""
offset = np.random.uniform(offset_range[0], offset_range[1])

points[:, 2] += offset
gt_boxes[:, 2] += offset

return gt_boxes, points

def random_local_translation_along_x(gt_boxes, points, offset_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
offset_range: [min max]]
Returns:
"""
# augs = {}
for idx, box in enumerate(gt_boxes):
offset = np.random.uniform(offset_range[0], offset_range[1])
# augs[f'object_{idx}'] = offset
points_in_box, mask = get_points_in_box(points, box)
points[mask, 0] += offset

gt_boxes[idx, 0] += offset

# if gt_boxes.shape[1] > 7:
# gt_boxes[idx, 7] += offset

return gt_boxes, points

def random_local_translation_along_y(gt_boxes, points, offset_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
offset_range: [min max]]
Returns:
"""
# augs = {}
for idx, box in enumerate(gt_boxes):
offset = np.random.uniform(offset_range[0], offset_range[1])
# augs[f'object_{idx}'] = offset
points_in_box, mask = get_points_in_box(points, box)
points[mask, 1] += offset

gt_boxes[idx, 1] += offset

# if gt_boxes.shape[1] > 8:
# gt_boxes[idx, 8] += offset

return gt_boxes, points

def random_local_translation_along_z(gt_boxes, points, offset_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
offset_range: [min max]]
Returns:
"""
# augs = {}
for idx, box in enumerate(gt_boxes):
offset = np.random.uniform(offset_range[0], offset_range[1])
# augs[f'object_{idx}'] = offset
points_in_box, mask = get_points_in_box(points, box)
points[mask, 2] += offset

gt_boxes[idx, 2] += offset

return gt_boxes, points

def global_frustum_dropout_top(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
intensity = np.random.uniform(intensity_range[0], intensity_range[1])

threshold = np.max(points[:, 2]) - intensity * (np.max(points[:, 2]) - np.min(points[:, 2]))
points = points[points[:,2] < threshold]
gt_boxes = gt_boxes[gt_boxes[:,2] < threshold]

return gt_boxes, points

def global_frustum_dropout_bottom(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
intensity = np.random.uniform(intensity_range[0], intensity_range[1])

threshold = np.min(points[:, 2]) + intensity * (np.max(points[:, 2]) - np.min(points[:, 2]))
points = points[points[:,2] > threshold]
gt_boxes = gt_boxes[gt_boxes[:,2] > threshold]

return gt_boxes, points

def global_frustum_dropout_left(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
intensity = np.random.uniform(intensity_range[0], intensity_range[1])

threshold = np.max(points[:, 1]) - intensity * (np.max(points[:, 1]) - np.min(points[:, 1]))
points = points[points[:,1] < threshold]
gt_boxes = gt_boxes[gt_boxes[:,1] < threshold]

return gt_boxes, points

def global_frustum_dropout_right(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
intensity = np.random.uniform(intensity_range[0], intensity_range[1])

threshold = np.min(points[:, 1]) + intensity * (np.max(points[:, 1]) - np.min(points[:, 1]))
points = points[points[:,1] > threshold]
gt_boxes = gt_boxes[gt_boxes[:,1] > threshold]

return gt_boxes, points

def local_scaling(gt_boxes, points, scale_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading]
points: (M, 3 + C),
scale_range: [min, max]
Returns:
"""
if scale_range[1] - scale_range[0] < 1e-3:
return gt_boxes, points

# augs = {}
for idx, box in enumerate(gt_boxes):
noise_scale = np.random.uniform(scale_range[0], scale_range[1])
# augs[f'object_{idx}'] = noise_scale
points_in_box, mask = get_points_in_box(points, box)

# tranlation to axis center
points[mask, 0] -= box[0]
points[mask, 1] -= box[1]
points[mask, 2] -= box[2]

# apply scaling
points[mask, :3] *= noise_scale

# tranlation back to original position
points[mask, 0] += box[0]
points[mask, 1] += box[1]
points[mask, 2] += box[2]

gt_boxes[idx, 3:6] *= noise_scale
return gt_boxes, points


def local_rotation(gt_boxes, points, rot_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]]
points: (M, 3 + C),
rot_range: [min, max]
Returns:
"""
# augs = {}
for idx, box in enumerate(gt_boxes):
noise_rotation = np.random.uniform(rot_range[0], rot_range[1])
# augs[f'object_{idx}'] = noise_rotation
points_in_box, mask = get_points_in_box(points, box)

centroid_x = box[0]
centroid_y = box[1]
centroid_z = box[2]

# tranlation to axis center
points[mask, 0] -= centroid_x
points[mask, 1] -= centroid_y
points[mask, 2] -= centroid_z
box[0] -= centroid_x
box[1] -= centroid_y
box[2] -= centroid_z

# apply rotation
points[mask, :] = common_utils.rotate_points_along_z(points[np.newaxis, mask, :], np.array([noise_rotation]))[0]
box[0:3] = common_utils.rotate_points_along_z(box[np.newaxis, np.newaxis, 0:3], np.array([noise_rotation]))[0][0]

# tranlation back to original position
points[mask, 0] += centroid_x
points[mask, 1] += centroid_y
points[mask, 2] += centroid_z
box[0] += centroid_x
box[1] += centroid_y
box[2] += centroid_z

gt_boxes[idx, 6] += noise_rotation
if gt_boxes.shape[1] > 8:
gt_boxes[idx, 7:9] = common_utils.rotate_points_along_z(
np.hstack((gt_boxes[idx, 7:9], np.zeros((gt_boxes.shape[0], 1))))[np.newaxis, :, :],
np.array([noise_rotation])
)[0][:, 0:2]

return gt_boxes, points

def local_frustum_dropout_top(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
for idx, box in enumerate(gt_boxes):
x, y, z, dx, dy, dz = box[0], box[1], box[2], box[3], box[4], box[5]

intensity = np.random.uniform(intensity_range[0], intensity_range[1])
points_in_box, mask = get_points_in_box(points, box)
threshold = (z + dz/2) - intensity * dz

points = points[np.logical_not(np.logical_and(mask, points[:,2] >= threshold))]

return gt_boxes, points

def local_frustum_dropout_bottom(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
for idx, box in enumerate(gt_boxes):
x, y, z, dx, dy, dz = box[0], box[1], box[2], box[3], box[4], box[5]

intensity = np.random.uniform(intensity_range[0], intensity_range[1])
points_in_box, mask = get_points_in_box(points, box)
threshold = (z - dz/2) + intensity * dz

points = points[np.logical_not(np.logical_and(mask, points[:,2] <= threshold))]

return gt_boxes, points

def local_frustum_dropout_left(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
for idx, box in enumerate(gt_boxes):
x, y, z, dx, dy, dz = box[0], box[1], box[2], box[3], box[4], box[5]

intensity = np.random.uniform(intensity_range[0], intensity_range[1])
points_in_box, mask = get_points_in_box(points, box)
threshold = (y + dy/2) - intensity * dy

points = points[np.logical_not(np.logical_and(mask, points[:,1] >= threshold))]

return gt_boxes, points

def local_frustum_dropout_right(gt_boxes, points, intensity_range):
"""
Args:
gt_boxes: (N, 7), [x, y, z, dx, dy, dz, heading, [vx], [vy]],
points: (M, 3 + C),
intensity: [min, max]
Returns:
"""
for idx, box in enumerate(gt_boxes):
x, y, z, dx, dy, dz = box[0], box[1], box[2], box[3], box[4], box[5]

intensity = np.random.uniform(intensity_range[0], intensity_range[1])
points_in_box, mask = get_points_in_box(points, box)
threshold = (y - dy/2) + intensity * dy

points = points[np.logical_not(np.logical_and(mask, points[:,1] <= threshold))]

return gt_boxes, points

def get_points_in_box(points, gt_box):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right?
Since you do not consider the orientation of gt_box, so simply checking the points with (xyz, dxdydz) may return wrong points.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The results were visually validated using this tool: https://github.com/xaviersantos/OpenPCDet/blob/dev/data_augmentation.ipynb

Here are some images taken from these augmentations (the magnitude of the transformations was exaggerated for better visualization):

Original image:
original

Local Translation along Y:
translation_local_y

Local Rotation:
rotated_local

Local Scaling:
scaled_local

Comparing the results of the new augmentation methods with the ones already implemented, these were the improvements:

table

These models models were trained for 80 epochs keeping all the same default hyperparameters.

As for the orientation of the gt_boxes, it should have no influence on the selection of the points inside its bounds (function get_points_in_box), as the center of the box and its dimensions are enough to the describe all the points within the box.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The performance looks good!

But I still do not understand why you do not need to consider the orientation.
Simply checking the center+dimensions of boxes for selecting inside points should only work for boxes with heading_angle=0.
What would happen when you conduct this function for a gt_box with heading_angle=pi/4? I think the inside points could not be well selected by this function since the gt_box is oblique.

Actually I have implemented a function to select inside points from a rotated box (https://github.com/open-mmlab/OpenPCDet/blob/master/pcdet/ops/roiaware_pool3d/src/roiaware_pool3d.cpp#L121-L140), could you comment on the difference between your function and my function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean and you're right.

I've fixed the get_points_in_box function using a numpy logical mask based on your code, as well as the local frustum dropout functions which had the same mistake.

The results were validated visually in scenes with objects in oblique positions and everything seems to be working as intended. I couldn't, however, train a model with this new fix to see if there are further improvements in performance yet, because the workstation I've been using for training is currently unavailable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thank you!

I will try to train with your config and get back to here soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xaviersantos I find a function points_in_boxes_cpu implemented in ./pcdet/ops/roiaware_pool3d/roiaware_pool3d_utils.py similar with your vesion get_points_in_box. I would like to know why you don't use it.

x, y, z = points[:,0], points[:,1], points[:,2]
cx, cy, cz = gt_box[0], gt_box[1], gt_box[2]
dx, dy, dz, rz = gt_box[3], gt_box[4], gt_box[5], gt_box[6]
shift_x, shift_y, shift_z = x - cx, y - cy, z - cz

MARGIN = 1e-1
cosa, sina = math.cos(-rz), math.sin(-rz)
local_x = shift_x * cosa + shift_y * (-sina)
local_y = shift_x * sina + shift_y * cosa

mask = np.logical_and(abs(shift_z) <= dz / 2.0, \
np.logical_and(abs(local_x) <= dx / 2.0 + MARGIN, \
abs(local_y) <= dy / 2.0 + MARGIN ))

points = points[mask]

return points, mask
Loading