Merge branch 'master' into gpu-cuda-rename

Conflicts:
	modules/core/include/opencv2/core/cuda.hpp
	modules/cudaimgproc/src/gftt.cpp
	modules/objdetect/include/opencv2/objdetect/erfilter.hpp
	modules/objdetect/src/erfilter.cpp
	modules/superres/perf/perf_superres.cpp
	modules/superres/src/btv_l1_cuda.cpp
	modules/superres/src/optical_flow.cpp
	modules/videostab/src/global_motion.cpp
	modules/videostab/src/inpainting.cpp
	samples/cpp/stitching_detailed.cpp
	samples/cpp/videostab.cpp
	samples/gpu/stereo_multi.cpp
pull/1299/head
Vladislav Vinogradov 11 years ago
commit 1036ce807c
  1. 3
      modules/bioinspired/src/retina_ocl.cpp
  2. 5
      modules/bioinspired/src/retina_ocl.hpp
  3. 4
      modules/core/doc/opengl_interop.rst
  4. 24
      modules/core/src/opengl.cpp
  5. 36
      modules/cudacodec/src/video_reader.cpp
  6. 2
      modules/cudacodec/src/video_source.cpp
  7. 4
      modules/cudacodec/src/video_writer.cpp
  8. 6
      modules/objdetect/include/opencv2/objdetect/erfilter.hpp
  9. 39
      modules/objdetect/src/erfilter.cpp
  10. 2038
      samples/cpp/trained_classifierNM1.xml
  11. 2000
      samples/cpp/trained_classifierNM2.xml

@ -1040,9 +1040,6 @@ const oclMat &MagnoRetinaFilter::runFilter(const oclMat &OPL_ON, const oclMat &O
x##_slices[_SLICE_INDEX_] = x(getROI(_SLICE_INDEX_));\ x##_slices[_SLICE_INDEX_] = x(getROI(_SLICE_INDEX_));\
} }
static float _LMStoACr1Cr2[] = {1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -0.5, -0.5, 1.0};
static float _LMStoLab[] = {0.5774f, 0.5774f, 0.5774f, 0.4082f, 0.4082f, -0.8165f, 0.7071f, -0.7071f, 0.f};
RetinaColor::RetinaColor(const unsigned int NBrows, const unsigned int NBcolumns, const int samplingMethod) RetinaColor::RetinaColor(const unsigned int NBrows, const unsigned int NBcolumns, const int samplingMethod)
: BasicRetinaFilter(NBrows, NBcolumns, 3), : BasicRetinaFilter(NBrows, NBcolumns, 3),
_RGBmosaic(NBrows * 3, NBcolumns, CV_32FC1), _RGBmosaic(NBrows * 3, NBcolumns, CV_32FC1),

@ -160,14 +160,15 @@ public:
}; };
protected: protected:
cv::ocl::oclMat _filterOutput;
cv::ocl::oclMat _localBuffer;
int _NBrows; int _NBrows;
int _NBcols; int _NBcols;
unsigned int _halfNBrows; unsigned int _halfNBrows;
unsigned int _halfNBcolumns; unsigned int _halfNBcolumns;
cv::ocl::oclMat _filterOutput;
cv::ocl::oclMat _localBuffer;
std::valarray <float>_filteringCoeficientsTable; std::valarray <float>_filteringCoeficientsTable;
float _v0; float _v0;
float _maxInputValue; float _maxInputValue;

@ -122,6 +122,8 @@ Decrements the reference counter and destroys the buffer object if needed.
.. ocv:function:: void ogl::Buffer::release() .. ocv:function:: void ogl::Buffer::release()
The function will call `setAutoRelease(true)` .
ogl::Buffer::setAutoRelease ogl::Buffer::setAutoRelease
@ -323,6 +325,8 @@ Decrements the reference counter and destroys the texture object if needed.
.. ocv:function:: void ogl::Texture2D::release() .. ocv:function:: void ogl::Texture2D::release()
The function will call `setAutoRelease(true)` .
ogl::Texture2D::setAutoRelease ogl::Texture2D::setAutoRelease

@ -484,7 +484,7 @@ cv::ogl::Buffer::Buffer(int arows, int acols, int atype, unsigned int abufId, bo
(void) autoRelease; (void) autoRelease;
throw_no_ogl(); throw_no_ogl();
#else #else
impl_ = new Impl(abufId, autoRelease); impl_.reset(new Impl(abufId, autoRelease));
rows_ = arows; rows_ = arows;
cols_ = acols; cols_ = acols;
type_ = atype; type_ = atype;
@ -500,7 +500,7 @@ cv::ogl::Buffer::Buffer(Size asize, int atype, unsigned int abufId, bool autoRel
(void) autoRelease; (void) autoRelease;
throw_no_ogl(); throw_no_ogl();
#else #else
impl_ = new Impl(abufId, autoRelease); impl_.reset(new Impl(abufId, autoRelease));
rows_ = asize.height; rows_ = asize.height;
cols_ = asize.width; cols_ = asize.width;
type_ = atype; type_ = atype;
@ -529,7 +529,7 @@ cv::ogl::Buffer::Buffer(InputArray arr, Target target, bool autoRelease) : rows_
Mat mat = arr.getMat(); Mat mat = arr.getMat();
CV_Assert( mat.isContinuous() ); CV_Assert( mat.isContinuous() );
const GLsizeiptr asize = mat.rows * mat.cols * mat.elemSize(); const GLsizeiptr asize = mat.rows * mat.cols * mat.elemSize();
impl_ = new Impl(asize, mat.data, target, autoRelease); impl_.reset(new Impl(asize, mat.data, target, autoRelease));
rows_ = mat.rows; rows_ = mat.rows;
cols_ = mat.cols; cols_ = mat.cols;
type_ = mat.type(); type_ = mat.type();
@ -552,7 +552,7 @@ void cv::ogl::Buffer::create(int arows, int acols, int atype, Target target, boo
if (rows_ != arows || cols_ != acols || type_ != atype) if (rows_ != arows || cols_ != acols || type_ != atype)
{ {
const GLsizeiptr asize = arows * acols * CV_ELEM_SIZE(atype); const GLsizeiptr asize = arows * acols * CV_ELEM_SIZE(atype);
impl_ = new Impl(asize, 0, target, autoRelease); impl_.reset(new Impl(asize, 0, target, autoRelease));
rows_ = arows; rows_ = arows;
cols_ = acols; cols_ = acols;
type_ = atype; type_ = atype;
@ -563,7 +563,7 @@ void cv::ogl::Buffer::create(int arows, int acols, int atype, Target target, boo
void cv::ogl::Buffer::release() void cv::ogl::Buffer::release()
{ {
#ifdef HAVE_OPENGL #ifdef HAVE_OPENGL
if (*impl_.refcount == 1) if (impl_)
impl_->setAutoRelease(true); impl_->setAutoRelease(true);
impl_ = Impl::empty(); impl_ = Impl::empty();
rows_ = 0; rows_ = 0;
@ -968,7 +968,7 @@ cv::ogl::Texture2D::Texture2D(int arows, int acols, Format aformat, unsigned int
(void) autoRelease; (void) autoRelease;
throw_no_ogl(); throw_no_ogl();
#else #else
impl_ = new Impl(atexId, autoRelease); impl_.reset(new Impl(atexId, autoRelease));
rows_ = arows; rows_ = arows;
cols_ = acols; cols_ = acols;
format_ = aformat; format_ = aformat;
@ -984,7 +984,7 @@ cv::ogl::Texture2D::Texture2D(Size asize, Format aformat, unsigned int atexId, b
(void) autoRelease; (void) autoRelease;
throw_no_ogl(); throw_no_ogl();
#else #else
impl_ = new Impl(atexId, autoRelease); impl_.reset(new Impl(atexId, autoRelease));
rows_ = asize.height; rows_ = asize.height;
cols_ = asize.width; cols_ = asize.width;
format_ = aformat; format_ = aformat;
@ -1024,7 +1024,7 @@ cv::ogl::Texture2D::Texture2D(InputArray arr, bool autoRelease) : rows_(0), cols
{ {
ogl::Buffer buf = arr.getOGlBuffer(); ogl::Buffer buf = arr.getOGlBuffer();
buf.bind(ogl::Buffer::PIXEL_UNPACK_BUFFER); buf.bind(ogl::Buffer::PIXEL_UNPACK_BUFFER);
impl_ = new Impl(internalFormats[cn], asize.width, asize.height, srcFormats[cn], gl_types[depth], 0, autoRelease); impl_.reset(new Impl(internalFormats[cn], asize.width, asize.height, srcFormats[cn], gl_types[depth], 0, autoRelease));
ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER); ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER);
break; break;
} }
@ -1037,7 +1037,7 @@ cv::ogl::Texture2D::Texture2D(InputArray arr, bool autoRelease) : rows_(0), cols
GpuMat dmat = arr.getGpuMat(); GpuMat dmat = arr.getGpuMat();
ogl::Buffer buf(dmat, ogl::Buffer::PIXEL_UNPACK_BUFFER); ogl::Buffer buf(dmat, ogl::Buffer::PIXEL_UNPACK_BUFFER);
buf.bind(ogl::Buffer::PIXEL_UNPACK_BUFFER); buf.bind(ogl::Buffer::PIXEL_UNPACK_BUFFER);
impl_ = new Impl(internalFormats[cn], asize.width, asize.height, srcFormats[cn], gl_types[depth], 0, autoRelease); impl_.reset(new Impl(internalFormats[cn], asize.width, asize.height, srcFormats[cn], gl_types[depth], 0, autoRelease));
ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER); ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER);
#endif #endif
@ -1049,7 +1049,7 @@ cv::ogl::Texture2D::Texture2D(InputArray arr, bool autoRelease) : rows_(0), cols
Mat mat = arr.getMat(); Mat mat = arr.getMat();
CV_Assert( mat.isContinuous() ); CV_Assert( mat.isContinuous() );
ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER); ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER);
impl_ = new Impl(internalFormats[cn], asize.width, asize.height, srcFormats[cn], gl_types[depth], mat.data, autoRelease); impl_.reset(new Impl(internalFormats[cn], asize.width, asize.height, srcFormats[cn], gl_types[depth], mat.data, autoRelease));
break; break;
} }
} }
@ -1072,7 +1072,7 @@ void cv::ogl::Texture2D::create(int arows, int acols, Format aformat, bool autoR
if (rows_ != arows || cols_ != acols || format_ != aformat) if (rows_ != arows || cols_ != acols || format_ != aformat)
{ {
ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER); ogl::Buffer::unbind(ogl::Buffer::PIXEL_UNPACK_BUFFER);
impl_ = new Impl(aformat, acols, arows, aformat, gl::FLOAT, 0, autoRelease); impl_.reset(new Impl(aformat, acols, arows, aformat, gl::FLOAT, 0, autoRelease));
rows_ = arows; rows_ = arows;
cols_ = acols; cols_ = acols;
format_ = aformat; format_ = aformat;
@ -1083,7 +1083,7 @@ void cv::ogl::Texture2D::create(int arows, int acols, Format aformat, bool autoR
void cv::ogl::Texture2D::release() void cv::ogl::Texture2D::release()
{ {
#ifdef HAVE_OPENGL #ifdef HAVE_OPENGL
if (*impl_.refcount == 1) if (impl_)
impl_->setAutoRelease(true); impl_->setAutoRelease(true);
impl_ = Impl::empty(); impl_ = Impl::empty();
rows_ = 0; rows_ = 0;

@ -58,12 +58,14 @@ namespace cv { namespace cuda { namespace device
void NV12_to_RGB(const PtrStepb decodedFrame, PtrStepSz<uint> interopFrame, cudaStream_t stream = 0); void NV12_to_RGB(const PtrStepb decodedFrame, PtrStepSz<uint> interopFrame, cudaStream_t stream = 0);
}}} }}}
using namespace cv::cudacodec::detail;
namespace namespace
{ {
class VideoReaderImpl : public VideoReader class VideoReaderImpl : public VideoReader
{ {
public: public:
explicit VideoReaderImpl(const Ptr<detail::VideoSource>& source); explicit VideoReaderImpl(const Ptr<VideoSource>& source);
~VideoReaderImpl(); ~VideoReaderImpl();
bool nextFrame(OutputArray frame); bool nextFrame(OutputArray frame);
@ -71,11 +73,11 @@ namespace
FormatInfo format() const; FormatInfo format() const;
private: private:
Ptr<detail::VideoSource> videoSource_; Ptr<VideoSource> videoSource_;
Ptr<detail::FrameQueue> frameQueue_; Ptr<FrameQueue> frameQueue_;
Ptr<detail::VideoDecoder> videoDecoder_; Ptr<VideoDecoder> videoDecoder_;
Ptr<detail::VideoParser> videoParser_; Ptr<VideoParser> videoParser_;
CUvideoctxlock lock_; CUvideoctxlock lock_;
@ -87,7 +89,7 @@ namespace
return videoSource_->format(); return videoSource_->format();
} }
VideoReaderImpl::VideoReaderImpl(const Ptr<detail::VideoSource>& source) : VideoReaderImpl::VideoReaderImpl(const Ptr<VideoSource>& source) :
videoSource_(source), videoSource_(source),
lock_(0) lock_(0)
{ {
@ -99,9 +101,9 @@ namespace
cuSafeCall( cuCtxGetCurrent(&ctx) ); cuSafeCall( cuCtxGetCurrent(&ctx) );
cuSafeCall( cuvidCtxLockCreate(&lock_, ctx) ); cuSafeCall( cuvidCtxLockCreate(&lock_, ctx) );
frameQueue_ = new detail::FrameQueue; frameQueue_.reset(new FrameQueue);
videoDecoder_ = new detail::VideoDecoder(videoSource_->format(), lock_); videoDecoder_.reset(new VideoDecoder(videoSource_->format(), lock_));
videoParser_ = new detail::VideoParser(videoDecoder_, frameQueue_); videoParser_.reset(new VideoParser(videoDecoder_, frameQueue_));
videoSource_->setVideoParser(videoParser_); videoSource_->setVideoParser(videoParser_);
videoSource_->start(); videoSource_->start();
@ -159,7 +161,7 @@ namespace
return false; return false;
// Wait a bit // Wait a bit
detail::Thread::sleep(1); Thread::sleep(1);
} }
bool isProgressive = displayInfo.progressive_frame != 0; bool isProgressive = displayInfo.progressive_frame != 0;
@ -212,25 +214,25 @@ Ptr<VideoReader> cv::cudacodec::createVideoReader(const String& filename)
{ {
CV_Assert( !filename.empty() ); CV_Assert( !filename.empty() );
Ptr<detail::VideoSource> videoSource; Ptr<VideoSource> videoSource;
try try
{ {
videoSource = new detail::CuvidVideoSource(filename); videoSource.reset(new CuvidVideoSource(filename));
} }
catch (...) catch (...)
{ {
Ptr<RawVideoSource> source(new detail::FFmpegVideoSource(filename)); Ptr<RawVideoSource> source(new FFmpegVideoSource(filename));
videoSource = new detail::RawVideoSourceWrapper(source); videoSource.reset(new RawVideoSourceWrapper(source));
} }
return new VideoReaderImpl(videoSource); return makePtr<VideoReaderImpl>(videoSource);
} }
Ptr<VideoReader> cv::cudacodec::createVideoReader(const Ptr<RawVideoSource>& source) Ptr<VideoReader> cv::cudacodec::createVideoReader(const Ptr<RawVideoSource>& source)
{ {
Ptr<detail::VideoSource> videoSource(new detail::RawVideoSourceWrapper(source)); Ptr<VideoSource> videoSource(new RawVideoSourceWrapper(source));
return new VideoReaderImpl(videoSource); return makePtr<VideoReaderImpl>(videoSource);
} }
#endif // HAVE_NVCUVID #endif // HAVE_NVCUVID

@ -69,7 +69,7 @@ void cv::cudacodec::detail::RawVideoSourceWrapper::start()
{ {
stop_ = false; stop_ = false;
hasError_ = false; hasError_ = false;
thread_ = new Thread(readLoop, this); thread_.reset(new Thread(readLoop, this));
} }
void cv::cudacodec::detail::RawVideoSourceWrapper::stop() void cv::cudacodec::detail::RawVideoSourceWrapper::stop()

@ -908,12 +908,12 @@ Ptr<VideoWriter> cv::cudacodec::createVideoWriter(const String& fileName, Size f
Ptr<VideoWriter> cv::cudacodec::createVideoWriter(const Ptr<EncoderCallBack>& encoderCallback, Size frameSize, double fps, SurfaceFormat format) Ptr<VideoWriter> cv::cudacodec::createVideoWriter(const Ptr<EncoderCallBack>& encoderCallback, Size frameSize, double fps, SurfaceFormat format)
{ {
return new VideoWriterImpl(encoderCallback, frameSize, fps, format); return makePtr<VideoWriterImpl>(encoderCallback, frameSize, fps, format);
} }
Ptr<VideoWriter> cv::cudacodec::createVideoWriter(const Ptr<EncoderCallBack>& encoderCallback, Size frameSize, double fps, const EncoderParams& params, SurfaceFormat format) Ptr<VideoWriter> cv::cudacodec::createVideoWriter(const Ptr<EncoderCallBack>& encoderCallback, Size frameSize, double fps, const EncoderParams& params, SurfaceFormat format)
{ {
return new VideoWriterImpl(encoderCallback, frameSize, fps, params, format); return makePtr<VideoWriterImpl>(encoderCallback, frameSize, fps, params, format);
} }
#endif // !defined HAVE_CUDA || !defined WIN32 #endif // !defined HAVE_CUDA || !defined WIN32

@ -172,8 +172,8 @@ public:
\param minProbability The minimum probability difference between local maxima and local minima ERs \param minProbability The minimum probability difference between local maxima and local minima ERs
*/ */
CV_EXPORTS Ptr<ERFilter> createERFilterNM1(const Ptr<ERFilter::Callback>& cb = Ptr<ERFilter::Callback>(), CV_EXPORTS Ptr<ERFilter> createERFilterNM1(const Ptr<ERFilter::Callback>& cb = Ptr<ERFilter::Callback>(),
int thresholdDelta = 1, float minArea = 0.000025, int thresholdDelta = 1, float minArea = 0.00025,
float maxArea = 0.13, float minProbability = 0.2, float maxArea = 0.13, float minProbability = 0.4,
bool nonMaxSuppression = true, bool nonMaxSuppression = true,
float minProbabilityDiff = 0.1); float minProbabilityDiff = 0.1);
@ -191,7 +191,7 @@ CV_EXPORTS Ptr<ERFilter> createERFilterNM1(const Ptr<ERFilter::Callback>& cb = P
\param minProbability The minimum probability P(er|character) allowed for retreived ER's \param minProbability The minimum probability P(er|character) allowed for retreived ER's
*/ */
CV_EXPORTS Ptr<ERFilter> createERFilterNM2(const Ptr<ERFilter::Callback>& cb = Ptr<ERFilter::Callback>(), CV_EXPORTS Ptr<ERFilter> createERFilterNM2(const Ptr<ERFilter::Callback>& cb = Ptr<ERFilter::Callback>(),
float minProbability = 0.85); float minProbability = 0.3);
} }
#endif // _OPENCV_ERFILTER_HPP_ #endif // _OPENCV_ERFILTER_HPP_

@ -631,9 +631,10 @@ void ERFilterNM::er_merge(ERStat *parent, ERStat *child)
child->probability = classifier->eval(*child); child->probability = classifier->eval(*child);
} }
if ( ((classifier!=NULL)?(child->probability >= minProbability):true) && if ( (((classifier!=NULL)?(child->probability >= minProbability):true)||(nonMaxSuppression)) &&
((child->area >= (minArea*region_mask.rows*region_mask.cols)) && ((child->area >= (minArea*region_mask.rows*region_mask.cols)) &&
(child->area <= (maxArea*region_mask.rows*region_mask.cols))) ) (child->area <= (maxArea*region_mask.rows*region_mask.cols)) &&
(child->rect.width > 2) && (child->rect.height > 2)) )
{ {
num_accepted_regions++; num_accepted_regions++;
@ -699,18 +700,24 @@ ERStat* ERFilterNM::er_save( ERStat *er, ERStat *parent, ERStat *prev )
regions->back().parent = parent; regions->back().parent = parent;
if (prev != NULL) if (prev != NULL)
{
prev->next = &(regions->back()); prev->next = &(regions->back());
}
else if (parent != NULL) else if (parent != NULL)
parent->child = &(regions->back()); parent->child = &(regions->back());
ERStat *old_prev = NULL; ERStat *old_prev = NULL;
ERStat *this_er = &regions->back(); ERStat *this_er = &regions->back();
if (this_er->parent == NULL)
{
this_er->probability = 0;
}
if (nonMaxSuppression) if (nonMaxSuppression)
{ {
if (this_er->parent == NULL) if (this_er->parent == NULL)
{ {
this_er->probability = 0; //TODO this makes sense in order to select at least one region in short tree's but is it really necessary?
this_er->max_probability_ancestor = this_er; this_er->max_probability_ancestor = this_er;
this_er->min_probability_ancestor = this_er; this_er->min_probability_ancestor = this_er;
} }
@ -722,15 +729,22 @@ ERStat* ERFilterNM::er_save( ERStat *er, ERStat *parent, ERStat *prev )
if ( (this_er->max_probability_ancestor->probability > minProbability) && (this_er->max_probability_ancestor->probability - this_er->min_probability_ancestor->probability > minProbabilityDiff)) if ( (this_er->max_probability_ancestor->probability > minProbability) && (this_er->max_probability_ancestor->probability - this_er->min_probability_ancestor->probability > minProbabilityDiff))
{ {
this_er->max_probability_ancestor->local_maxima = true;
if ((this_er->max_probability_ancestor == this_er) && (this_er->parent->local_maxima))
{
this_er->parent->local_maxima = false;
}
}
else if (this_er->probability < this_er->parent->probability)
{
this_er->min_probability_ancestor = this_er;
}
else if (this_er->probability > this_er->parent->probability)
{
this_er->max_probability_ancestor = this_er;
}
this_er->max_probability_ancestor->local_maxima = true;
//TODO check here if the last local_maxima can be also suppressed, is the following correct?
//if (this_er->min_probability_ancestor->local_maxima)
// this_er->min_probability_ancestor->local_maxima = false;
this_er->max_probability_ancestor = this_er;
this_er->min_probability_ancestor = this_er;
}
} }
} }
@ -769,8 +783,7 @@ ERStat* ERFilterNM::er_tree_filter ( InputArray image, ERStat * stat, ERStat *pa
findContours( region, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0) ); findContours( region, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE, Point(0, 0) );
//TODO check epsilon parameter of approxPolyDP (set empirically) : we want more precission //TODO check epsilon parameter of approxPolyDP (set empirically) : we want more precission
// if the region is very small because otherwise we'll loose all the convexities // if the region is very small because otherwise we'll loose all the convexities
approxPolyDP( Mat(contours[0]), contour_poly, max(rect.width,rect.height)/25, true ); approxPolyDP( Mat(contours[0]), contour_poly, (float)min(rect.width,rect.height)/17, true );
bool was_convex = false; bool was_convex = false;
int num_inflexion_points = 0; int num_inflexion_points = 0;
@ -1120,7 +1133,6 @@ Ptr<ERFilter> createERFilterNM2(const Ptr<ERFilter::Callback>& cb, float minProb
Ptr<ERFilterNM> filter = makePtr<ERFilterNM>(); Ptr<ERFilterNM> filter = makePtr<ERFilterNM>();
if (cb == NULL) if (cb == NULL)
filter->setCallback(makePtr<ERClassifierNM2>()); filter->setCallback(makePtr<ERClassifierNM2>());
else else
@ -1129,5 +1141,4 @@ Ptr<ERFilter> createERFilterNM2(const Ptr<ERFilter::Callback>& cb, float minProb
filter->setMinProbability(minProbability); filter->setMinProbability(minProbability);
return (Ptr<ERFilter>)filter; return (Ptr<ERFilter>)filter;
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save