blob: ced294ae7dd360ef9b75d5831fe9ab2a6b60e17c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*!
* \file image_aug_default.cc
* \brief Default augmenter.
*/
#include <mxnet/base.h>
#include <dmlc/optional.h>
#include <utility>
#include <string>
#include <algorithm>
#include <vector>
#include "./image_augmenter.h"
#include "../common/utils.h"
#if MXNET_USE_OPENCV
#include "./opencv_compatibility.h"
// Registers
namespace dmlc {
DMLC_REGISTRY_ENABLE(::mxnet::io::ImageAugmenterReg);
} // namespace dmlc
#endif
namespace mxnet {
namespace io {
/*! \brief image augmentation parameters*/
struct DefaultImageAugmentParam : public dmlc::Parameter<DefaultImageAugmentParam> {
/*! \brief resize shorter edge to size before applying other augmentations */
int resize;
/*! \brief whether we do random cropping */
bool rand_crop;
/*! \brief whether we do random resized cropping */
bool random_resized_crop;
/*! \brief [-max_rotate_angle, max_rotate_angle] */
int max_rotate_angle;
/*! \brief max aspect ratio */
float max_aspect_ratio;
/*! \brief min aspect ratio */
dmlc::optional<float> min_aspect_ratio;
/*! \brief random shear the image [-max_shear_ratio, max_shear_ratio] */
float max_shear_ratio;
/*! \brief max crop size */
int max_crop_size;
/*! \brief min crop size */
int min_crop_size;
/*! \brief max scale ratio */
float max_random_scale;
/*! \brief min scale ratio */
float min_random_scale;
/*! \brief max area */
float max_random_area;
/*! \brief min area */
float min_random_area;
/*! \brief min image size */
float min_img_size;
/*! \brief max image size */
float max_img_size;
/*! \brief max random brightness */
float brightness;
/*! \brief max random contrast */
float contrast;
/*! \brief max random saturation */
float saturation;
/*! \brief pca noise level */
float pca_noise;
/*! \brief max random in H channel */
int random_h;
/*! \brief max random in S channel */
int random_s;
/*! \brief max random in L channel */
int random_l;
/*! \brief rotate angle */
int rotate;
/*! \brief filled color while padding */
int fill_value;
/*! \brief interpolation method 0-NN 1-bilinear 2-cubic 3-area 4-lanczos4 9-auto 10-rand */
int inter_method;
/*! \brief padding size */
int pad;
/*! \brief shape of the image data*/
mxnet::TShape data_shape;
// declare parameters
DMLC_DECLARE_PARAMETER(DefaultImageAugmentParam) {
DMLC_DECLARE_FIELD(resize).set_default(-1).describe(
"Down scale the shorter edge to a new size "
"before applying other augmentations.");
DMLC_DECLARE_FIELD(rand_crop).set_default(false).describe("If or not randomly crop the image");
DMLC_DECLARE_FIELD(random_resized_crop)
.set_default(false)
.describe(
"If or not perform random resized cropping "
"on the image, as a standard preprocessing "
"for resnet training on ImageNet data.");
DMLC_DECLARE_FIELD(max_rotate_angle)
.set_default(0.0f)
.describe("Rotate by a random degree in ``[-v, v]``");
DMLC_DECLARE_FIELD(max_aspect_ratio)
.set_default(0.0f)
.describe(
"Change the aspect (namely width/height) to a random value. "
"If min_aspect_ratio is None then the aspect ratio ins sampled from "
"[1 - max_aspect_ratio, 1 + max_aspect_ratio], "
"else it is in ``[min_aspect_ratio, max_aspect_ratio]``");
DMLC_DECLARE_FIELD(min_aspect_ratio)
.set_default(dmlc::optional<float>())
.describe(
"Change the aspect (namely width/height) to a random value "
"in ``[min_aspect_ratio, max_aspect_ratio]``");
DMLC_DECLARE_FIELD(max_shear_ratio)
.set_default(0.0f)
.describe(
"Apply a shear transformation (namely ``(x,y)->(x+my,y)``) "
"with ``m`` randomly chose from "
"``[-max_shear_ratio, max_shear_ratio]``");
DMLC_DECLARE_FIELD(max_crop_size)
.set_default(-1)
.describe(
"Crop both width and height into a random size in "
"``[min_crop_size, max_crop_size].``"
"Ignored if ``random_resized_crop`` is True.");
DMLC_DECLARE_FIELD(min_crop_size)
.set_default(-1)
.describe(
"Crop both width and height into a random size in "
"``[min_crop_size, max_crop_size].``"
"Ignored if ``random_resized_crop`` is True.");
DMLC_DECLARE_FIELD(max_random_scale)
.set_default(1.0f)
.describe(
"Resize into ``[width*s, height*s]`` with ``s`` randomly"
" chosen from ``[min_random_scale, max_random_scale]``. "
"Ignored if ``random_resized_crop`` is True.");
DMLC_DECLARE_FIELD(min_random_scale)
.set_default(1.0f)
.describe(
"Resize into ``[width*s, height*s]`` with ``s`` randomly"
" chosen from ``[min_random_scale, max_random_scale]``"
"Ignored if ``random_resized_crop`` is True.");
DMLC_DECLARE_FIELD(max_random_area)
.set_default(1.0f)
.describe(
"Change the area (namely width * height) to a random value "
"in ``[min_random_area, max_random_area]``. "
"Ignored if ``random_resized_crop`` is False.");
DMLC_DECLARE_FIELD(min_random_area)
.set_default(1.0f)
.describe(
"Change the area (namely width * height) to a random value "
"in ``[min_random_area, max_random_area]``. "
"Ignored if ``random_resized_crop`` is False.");
DMLC_DECLARE_FIELD(max_img_size)
.set_default(1e10f)
.describe(
"Set the maximal width and height after all resize and"
" rotate argumentation are applied");
DMLC_DECLARE_FIELD(min_img_size)
.set_default(0.0f)
.describe(
"Set the minimal width and height after all resize and"
" rotate argumentation are applied");
DMLC_DECLARE_FIELD(brightness)
.set_default(0.0f)
.describe(
"Add a random value in ``[-brightness, brightness]`` to "
"the brightness of image.");
DMLC_DECLARE_FIELD(contrast).set_default(0.0f).describe(
"Add a random value in ``[-contrast, contrast]`` to "
"the contrast of image.");
DMLC_DECLARE_FIELD(saturation)
.set_default(0.0f)
.describe(
"Add a random value in ``[-saturation, saturation]`` to "
"the saturation of image.");
DMLC_DECLARE_FIELD(pca_noise).set_default(0.0f).describe("Add PCA based noise to the image.");
DMLC_DECLARE_FIELD(random_h).set_default(0).describe(
"Add a random value in ``[-random_h, random_h]`` to "
"the H channel in HSL color space.");
DMLC_DECLARE_FIELD(random_s).set_default(0).describe(
"Add a random value in ``[-random_s, random_s]`` to "
"the S channel in HSL color space.");
DMLC_DECLARE_FIELD(random_l).set_default(0).describe(
"Add a random value in ``[-random_l, random_l]`` to "
"the L channel in HSL color space.");
DMLC_DECLARE_FIELD(rotate).set_default(-1.0f).describe(
"Rotate by an angle. If set, it overwrites the ``max_rotate_angle`` option.");
DMLC_DECLARE_FIELD(fill_value)
.set_default(255)
.describe("Set the padding pixels value to ``fill_value``.");
DMLC_DECLARE_FIELD(data_shape)
.set_expect_ndim(3)
.enforce_nonzero()
.describe("The shape of a output image.");
DMLC_DECLARE_FIELD(inter_method)
.set_default(1)
.describe(
"The interpolation method: 0-NN 1-bilinear 2-cubic 3-area "
"4-lanczos4 9-auto 10-rand.");
DMLC_DECLARE_FIELD(pad).set_default(0).describe(
"Change size from ``[width, height]`` into "
"``[pad + width + pad, pad + height + pad]`` by padding pixes");
}
};
DMLC_REGISTER_PARAMETER(DefaultImageAugmentParam);
std::vector<dmlc::ParamFieldInfo> ListDefaultAugParams() {
return DefaultImageAugmentParam::__FIELDS__();
}
#if MXNET_USE_OPENCV
#ifdef _MSC_VER
#define M_PI CV_PI
#endif
/*! \brief helper class to do image augmentation */
class DefaultImageAugmenter : public ImageAugmenter {
public:
// contructor
DefaultImageAugmenter() = default;
void Init(const std::vector<std::pair<std::string, std::string> >& kwargs) override {
std::vector<std::pair<std::string, std::string> > kwargs_left;
kwargs_left = param_.InitAllowUnknown(kwargs);
for (auto& kwarg : kwargs_left) {
if (!strcmp(kwarg.first.c_str(), "rotate_list")) {
const char* val = kwarg.second.c_str();
const char* end = val + strlen(val);
char buf[128];
while (val < end) {
sscanf(val, "%[^,]", buf);
val += strlen(buf) + 1;
rotate_list_.push_back(atoi(buf));
}
}
}
}
/*!
* \brief get interpolation method with given inter_method, 0-CV_INTER_NN 1-CV_INTER_LINEAR
* 2-CV_INTER_CUBIC \ 3-CV_INTER_AREA 4-CV_INTER_LANCZOS4 9-AUTO(cubic for enlarge, area for
* shrink, bilinear for others) 10-RAND
*/
int GetInterMethod(int inter_method,
int old_width,
int old_height,
int new_width,
int new_height,
common::RANDOM_ENGINE* prnd) {
if (inter_method == 9) {
if (new_width > old_width && new_height > old_height) {
return 2; // CV_INTER_CUBIC for enlarge
} else if (new_width < old_width && new_height < old_height) {
return 3; // CV_INTER_AREA for shrink
} else {
return 1; // CV_INTER_LINEAR for others
}
} else if (inter_method == 10) {
std::uniform_int_distribution<size_t> rand_uniform_int(0, 4);
return rand_uniform_int(*prnd);
} else {
return inter_method;
}
}
cv::Mat Process(const cv::Mat& src,
std::vector<float>* label,
common::RANDOM_ENGINE* prnd) override {
using mshadow::index_t;
bool is_cropped = false;
float max_aspect_ratio = 1.0f;
float min_aspect_ratio = 1.0f;
if (param_.min_aspect_ratio.has_value()) {
max_aspect_ratio = param_.max_aspect_ratio;
min_aspect_ratio = param_.min_aspect_ratio.value();
} else {
max_aspect_ratio = 1 + param_.max_aspect_ratio;
min_aspect_ratio = 1 - param_.max_aspect_ratio;
}
cv::Mat res;
if (param_.resize != -1) {
int new_height, new_width;
if (src.rows > src.cols) {
new_height = param_.resize * src.rows / src.cols;
new_width = param_.resize;
} else {
new_height = param_.resize;
new_width = param_.resize * src.cols / src.rows;
}
CHECK((param_.inter_method >= 0 && param_.inter_method <= 4) ||
(param_.inter_method >= 9 && param_.inter_method <= 10))
<< "invalid inter_method: valid value 0,1,2,3,4,9,10";
int interpolation_method =
GetInterMethod(param_.inter_method, src.cols, src.rows, new_width, new_height, prnd);
cv::resize(src, res, cv::Size(new_width, new_height), 0, 0, interpolation_method);
} else {
res = src;
}
// normal augmentation by affine transformation.
if (param_.max_rotate_angle > 0 || param_.max_shear_ratio > 0.0f || param_.rotate > 0 ||
rotate_list_.size() > 0 || param_.max_random_scale != 1.0f ||
param_.min_random_scale != 1.0 ||
(!param_.random_resized_crop && (min_aspect_ratio != 1.0f || max_aspect_ratio != 1.0f)) ||
param_.max_img_size != 1e10f || param_.min_img_size != 0.0f) {
std::uniform_real_distribution<float> rand_uniform(0, 1);
// shear
float s = rand_uniform(*prnd) * param_.max_shear_ratio * 2 - param_.max_shear_ratio;
// rotate
int angle = std::uniform_int_distribution<int>(-param_.max_rotate_angle,
param_.max_rotate_angle)(*prnd);
if (param_.rotate > 0)
angle = param_.rotate;
if (rotate_list_.size() > 0) {
angle = rotate_list_[std::uniform_int_distribution<int>(0, rotate_list_.size() - 1)(*prnd)];
}
float a = cos(angle / 180.0 * M_PI);
float b = sin(angle / 180.0 * M_PI);
// scale
float scale = 1.0f;
if (!param_.random_resized_crop) {
scale = rand_uniform(*prnd) * (param_.max_random_scale - param_.min_random_scale) +
param_.min_random_scale;
}
// aspect ratio
float ratio = 1.0f;
if (!param_.random_resized_crop) {
ratio = rand_uniform(*prnd) * (max_aspect_ratio - min_aspect_ratio) + min_aspect_ratio;
}
float hs = 2 * scale / (1 + ratio);
float ws = ratio * hs;
// new width and height
float new_width =
std::max(param_.min_img_size, std::min(param_.max_img_size, scale * res.cols));
float new_height =
std::max(param_.min_img_size, std::min(param_.max_img_size, scale * res.rows));
cv::Mat M(2, 3, CV_32F);
M.at<float>(0, 0) = hs * a - s * b * ws;
M.at<float>(1, 0) = -b * ws;
M.at<float>(0, 1) = hs * b + s * a * ws;
M.at<float>(1, 1) = a * ws;
float ori_center_width = M.at<float>(0, 0) * res.cols + M.at<float>(0, 1) * res.rows;
float ori_center_height = M.at<float>(1, 0) * res.cols + M.at<float>(1, 1) * res.rows;
M.at<float>(0, 2) = (new_width - ori_center_width) / 2;
M.at<float>(1, 2) = (new_height - ori_center_height) / 2;
CHECK((param_.inter_method >= 0 && param_.inter_method <= 4) ||
(param_.inter_method >= 9 && param_.inter_method <= 10))
<< "invalid inter_method: valid value 0,1,2,3,4,9,10";
int interpolation_method =
GetInterMethod(param_.inter_method, res.cols, res.rows, new_width, new_height, prnd);
cv::warpAffine(res,
temp_,
M,
cv::Size(new_width, new_height),
interpolation_method,
cv::BORDER_CONSTANT,
cv::Scalar(param_.fill_value, param_.fill_value, param_.fill_value));
res = temp_;
}
// pad logic
if (param_.pad > 0) {
cv::copyMakeBorder(res,
res,
param_.pad,
param_.pad,
param_.pad,
param_.pad,
cv::BORDER_CONSTANT,
cv::Scalar(param_.fill_value, param_.fill_value, param_.fill_value));
}
if (param_.random_resized_crop) {
// random resize crop
CHECK(param_.min_random_scale == 1.0f && param_.max_random_scale == 1.0f &&
param_.min_crop_size == -1 && param_.max_crop_size == -1 && !param_.rand_crop)
<< "\nSetting random_resized_crop to true conflicts with "
"min_random_scale, max_random_scale, "
"min_crop_size, max_crop_size, "
"and rand_crop.";
if (param_.max_random_area != 1.0f || param_.min_random_area != 1.0f ||
max_aspect_ratio != 1.0f || min_aspect_ratio != 1.0f) {
CHECK(min_aspect_ratio > 0.0f);
CHECK(param_.min_random_area <= param_.max_random_area);
CHECK(min_aspect_ratio <= max_aspect_ratio);
std::uniform_real_distribution<float> rand_uniform_area(param_.min_random_area,
param_.max_random_area);
std::uniform_real_distribution<float> rand_uniform_ratio(min_aspect_ratio,
max_aspect_ratio);
std::uniform_real_distribution<float> rand_uniform(0, 1);
float area = res.rows * res.cols;
for (int i = 0; i < 10; ++i) {
float rand_area = rand_uniform_area(*prnd);
float ratio = rand_uniform_ratio(*prnd);
float target_area = area * rand_area;
int y_area = std::round(std::sqrt(target_area / ratio));
int x_area = std::round(std::sqrt(target_area * ratio));
if (rand_uniform(*prnd) > 0.5) {
float temp_y_area = y_area;
y_area = x_area;
x_area = temp_y_area;
}
if (y_area <= res.rows && x_area <= res.cols) {
index_t rand_y_area =
std::uniform_int_distribution<index_t>(0, res.rows - y_area)(*prnd);
index_t rand_x_area =
std::uniform_int_distribution<index_t>(0, res.cols - x_area)(*prnd);
cv::Rect roi(rand_x_area, rand_y_area, x_area, y_area);
int interpolation_method = GetInterMethod(param_.inter_method,
x_area,
y_area,
param_.data_shape[2],
param_.data_shape[1],
prnd);
cv::resize(res(roi),
res,
cv::Size(param_.data_shape[2], param_.data_shape[1]),
0,
0,
interpolation_method);
is_cropped = true;
break;
}
}
}
} else if (!param_.random_resized_crop &&
(param_.max_crop_size != -1 || param_.min_crop_size != -1)) {
// random_crop
CHECK(res.cols >= param_.max_crop_size && res.rows >= param_.max_crop_size &&
param_.max_crop_size >= param_.min_crop_size)
<< "input image size smaller than max_crop_size";
index_t rand_crop_size =
std::uniform_int_distribution<index_t>(param_.min_crop_size, param_.max_crop_size)(*prnd);
index_t y = res.rows - rand_crop_size;
index_t x = res.cols - rand_crop_size;
if (param_.rand_crop != 0) {
y = std::uniform_int_distribution<index_t>(0, y)(*prnd);
x = std::uniform_int_distribution<index_t>(0, x)(*prnd);
} else {
y /= 2;
x /= 2;
}
cv::Rect roi(x, y, rand_crop_size, rand_crop_size);
int interpolation_method = GetInterMethod(param_.inter_method,
rand_crop_size,
rand_crop_size,
param_.data_shape[2],
param_.data_shape[1],
prnd);
cv::resize(res(roi),
res,
cv::Size(param_.data_shape[2], param_.data_shape[1]),
0,
0,
interpolation_method);
is_cropped = true;
}
if (!is_cropped) {
// center crop
int interpolation_method = GetInterMethod(param_.inter_method,
res.cols,
res.rows,
param_.data_shape[2],
param_.data_shape[1],
prnd);
if (res.rows < param_.data_shape[1]) {
index_t new_cols =
static_cast<index_t>(static_cast<float>(param_.data_shape[1]) /
static_cast<float>(res.rows) * static_cast<float>(res.cols));
cv::resize(res, res, cv::Size(new_cols, param_.data_shape[1]), 0, 0, interpolation_method);
}
if (res.cols < param_.data_shape[2]) {
index_t new_rows =
static_cast<index_t>(static_cast<float>(param_.data_shape[2]) /
static_cast<float>(res.cols) * static_cast<float>(res.rows));
cv::resize(res, res, cv::Size(param_.data_shape[2], new_rows), 0, 0, interpolation_method);
}
CHECK(static_cast<index_t>(res.rows) >= param_.data_shape[1] &&
static_cast<index_t>(res.cols) >= param_.data_shape[2])
<< "input image size smaller than input shape";
index_t y = res.rows - param_.data_shape[1];
index_t x = res.cols - param_.data_shape[2];
if (param_.rand_crop != 0) {
y = std::uniform_int_distribution<index_t>(0, y)(*prnd);
x = std::uniform_int_distribution<index_t>(0, x)(*prnd);
} else {
y /= 2;
x /= 2;
}
cv::Rect roi(x, y, param_.data_shape[2], param_.data_shape[1]);
res = res(roi);
}
// color jitter
if (param_.brightness > 0.0f || param_.contrast > 0.0f || param_.saturation > 0.0f) {
std::uniform_real_distribution<float> rand_uniform(0, 1);
float alpha_b =
1.0 + std::uniform_real_distribution<float>(-param_.brightness, param_.brightness)(*prnd);
float alpha_c =
1.0 + std::uniform_real_distribution<float>(-param_.contrast, param_.contrast)(*prnd);
float alpha_s =
1.0 + std::uniform_real_distribution<float>(-param_.saturation, param_.saturation)(*prnd);
int rand_order[3] = {0, 1, 2};
std::shuffle(std::begin(rand_order), std::end(rand_order), *prnd);
for (int i : rand_order) {
if (i == 0) {
// brightness
res.convertTo(res, -1, alpha_b, 0);
}
if (i == 1) {
// contrast
cvtColor(res, temp_, CV_RGB2GRAY);
float gray_mean = cv::mean(temp_)[0];
res.convertTo(res, -1, alpha_c, (1 - alpha_c) * gray_mean);
}
if (i == 2) {
// saturation
cvtColor(res, temp_, CV_RGB2GRAY);
cvtColor(temp_, temp_, CV_GRAY2BGR);
cv::addWeighted(res, alpha_s, temp_, 1 - alpha_s, 0.0, res);
}
}
}
// color space augmentation
if (param_.random_h != 0 || param_.random_s != 0 || param_.random_l != 0) {
std::uniform_real_distribution<float> rand_uniform(0, 1);
cvtColor(res, res, CV_BGR2HLS);
// use an approximation of gaussian distribution to reduce extreme value
float rh = rand_uniform(*prnd);
rh += 4 * rand_uniform(*prnd);
rh = rh / 5;
float rs = rand_uniform(*prnd);
rs += 4 * rand_uniform(*prnd);
rs = rs / 5;
float rl = rand_uniform(*prnd);
rl += 4 * rand_uniform(*prnd);
rl = rl / 5;
int h = rh * param_.random_h * 2 - param_.random_h;
int s = rs * param_.random_s * 2 - param_.random_s;
int l = rl * param_.random_l * 2 - param_.random_l;
int temp[3] = {h, l, s};
int limit[3] = {180, 255, 255};
for (int i = 0; i < res.rows; ++i) {
for (int j = 0; j < res.cols; ++j) {
for (int k = 0; k < 3; ++k) {
int v = res.at<cv::Vec3b>(i, j)[k];
v += temp[k];
v = std::max(0, std::min(limit[k], v));
res.at<cv::Vec3b>(i, j)[k] = v;
}
}
}
cvtColor(res, res, CV_HLS2BGR);
}
// pca noise
if (param_.pca_noise > 0.0f) {
std::normal_distribution<float> rand_normal(0, param_.pca_noise);
float pca_alpha_r = rand_normal(*prnd);
float pca_alpha_g = rand_normal(*prnd);
float pca_alpha_b = rand_normal(*prnd);
float pca_r =
eigvec[0][0] * pca_alpha_r + eigvec[0][1] * pca_alpha_g + eigvec[0][2] * pca_alpha_b;
float pca_g =
eigvec[1][0] * pca_alpha_r + eigvec[1][1] * pca_alpha_g + eigvec[1][2] * pca_alpha_b;
float pca_b =
eigvec[2][0] * pca_alpha_r + eigvec[2][1] * pca_alpha_g + eigvec[2][2] * pca_alpha_b;
float pca[3] = {pca_b, pca_g, pca_r};
for (int i = 0; i < res.rows; ++i) {
for (int j = 0; j < res.cols; ++j) {
for (int k = 0; k < 3; ++k) {
int vp = res.at<cv::Vec3b>(i, j)[k];
vp += pca[k];
vp = std::max(0, std::min(255, vp));
res.at<cv::Vec3b>(i, j)[k] = vp;
}
}
}
}
return res;
}
private:
// temporal space
cv::Mat temp_;
// eigval and eigvec for adding pca noise
// store eigval * eigvec as eigvec
float eigvec[3][3] = {{55.46f * -0.5675f, 4.794f * 0.7192f, 1.148f * 0.4009f},
{55.46f * -0.5808f, 4.794f * -0.0045f, 1.148f * -0.8140f},
{55.46f * -0.5836f, 4.794f * -0.6948f, 1.148f * 0.4203f}};
// parameters
DefaultImageAugmentParam param_;
/*! \brief list of possible rotate angle */
std::vector<int> rotate_list_;
};
ImageAugmenter* ImageAugmenter::Create(const std::string& name) {
return dmlc::Registry<ImageAugmenterReg>::Find(name)->body();
}
MXNET_REGISTER_IMAGE_AUGMENTER(aug_default).describe("default augmenter").set_body([]() {
return new DefaultImageAugmenter();
});
#endif // MXNET_USE_OPENCV
} // namespace io
} // namespace mxnet