Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
Greenplum
Opencv
提交
04ebedb6
O
Opencv
项目概览
Greenplum
/
Opencv
12 个月 前同步成功
通知
7
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
O
Opencv
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
未验证
提交
04ebedb6
编写于
9月 21, 2022
作者:
A
Alexander Smorkalov
提交者:
GitHub
9月 21, 2022
浏览文件
操作
浏览文件
下载
差异文件
Merge pull request #22128 from ocpalo:multipage_img_decoder
[GSoC 2022] Multipage Image Decoder API
上级
71ae1cac
062cee29
变更
3
隐藏空白更改
内联
并排
Showing
3 changed file
with
471 addition
and
49 deletion
+471
-49
modules/imgcodecs/include/opencv2/imgcodecs.hpp
modules/imgcodecs/include/opencv2/imgcodecs.hpp
+45
-0
modules/imgcodecs/src/loadsave.cpp
modules/imgcodecs/src/loadsave.cpp
+249
-49
modules/imgcodecs/test/test_read_write.cpp
modules/imgcodecs/test/test_read_write.cpp
+177
-0
未找到文件。
modules/imgcodecs/include/opencv2/imgcodecs.hpp
浏览文件 @
04ebedb6
...
...
@@ -332,6 +332,51 @@ CV_EXPORTS_W bool haveImageReader( const String& filename );
*/
CV_EXPORTS_W
bool
haveImageWriter
(
const
String
&
filename
);
/** @brief To read Multi Page images on demand
The ImageCollection class provides iterator API to read multi page images on demand. Create iterator
to the collection of the images and iterate over the collection. Decode the necessary page with operator*.
The performance of page decoding is O(1) if collection is increment sequentially. If the user wants to access random page,
then the time Complexity is O(n) because the collection has to be reinitialized every time in order to go to the correct page.
However, the intermediate pages are not decoded during the process, so typically it's quite fast.
This is required because multipage codecs does not support going backwards.
After decoding the one page, it is stored inside the collection cache. Hence, trying to get Mat object from already decoded page is O(1).
If you need memory, you can use .releaseCache() method to release cached index.
The space complexity is O(n) if all pages are decoded into memory. The user is able to decode and release images on demand.
*/
class
CV_EXPORTS
ImageCollection
{
public:
struct
CV_EXPORTS
iterator
{
iterator
(
ImageCollection
*
col
);
iterator
(
ImageCollection
*
col
,
int
end
);
Mat
&
operator
*
();
Mat
*
operator
->
();
iterator
&
operator
++
();
iterator
operator
++
(
int
);
friend
bool
operator
==
(
const
iterator
&
a
,
const
iterator
&
b
)
{
return
a
.
m_curr
==
b
.
m_curr
;
};
friend
bool
operator
!=
(
const
iterator
&
a
,
const
iterator
&
b
)
{
return
a
.
m_curr
!=
b
.
m_curr
;
};
private:
ImageCollection
*
m_pCollection
;
int
m_curr
;
};
ImageCollection
();
ImageCollection
(
const
String
&
filename
,
int
flags
);
void
init
(
const
String
&
img
,
int
flags
);
size_t
size
()
const
;
const
Mat
&
at
(
int
index
);
const
Mat
&
operator
[](
int
index
);
void
releaseCache
(
int
index
);
iterator
begin
();
iterator
end
();
class
Impl
;
Ptr
<
Impl
>
getImpl
();
protected:
Ptr
<
Impl
>
pImpl
;
};
//! @} imgcodecs
...
...
modules/imgcodecs/src/loadsave.cpp
浏览文件 @
04ebedb6
...
...
@@ -54,6 +54,8 @@
#include <cerrno>
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/core/utils/configuration.private.hpp>
#include <opencv2/imgcodecs.hpp>
/****************************************************************************************\
...
...
@@ -661,57 +663,14 @@ bool imreadmulti(const String& filename, std::vector<Mat>& mats, int start, int
static
size_t
imcount_
(
const
String
&
filename
,
int
flags
)
{
/// Search for the relevant decoder to handle the imagery
ImageDecoder
decoder
;
#ifdef HAVE_GDAL
if
(
flags
!=
IMREAD_UNCHANGED
&&
(
flags
&
IMREAD_LOAD_GDAL
)
==
IMREAD_LOAD_GDAL
)
{
decoder
=
GdalDecoder
().
newDecoder
();
}
else
{
#else
CV_UNUSED
(
flags
);
#endif
decoder
=
findDecoder
(
filename
);
#ifdef HAVE_GDAL
}
#endif
/// if no decoder was found, return nothing.
if
(
!
decoder
)
{
return
0
;
}
/// set the filename in the driver
decoder
->
setSource
(
filename
);
// read the header to make sure it succeeds
try
{
// read the header to make sure it succeeds
if
(
!
decoder
->
readHeader
())
return
0
;
}
catch
(
const
cv
::
Exception
&
e
)
{
std
::
cerr
<<
"imcount_('"
<<
filename
<<
"'): can't read header: "
<<
e
.
what
()
<<
std
::
endl
<<
std
::
flush
;
return
0
;
}
catch
(...)
{
std
::
cerr
<<
"imcount_('"
<<
filename
<<
"'): can't read header: unknown exception"
<<
std
::
endl
<<
std
::
flush
;
try
{
ImageCollection
collection
(
filename
,
flags
);
return
collection
.
size
();
}
catch
(
cv
::
Exception
const
&
e
)
{
// Reading header or finding decoder for the filename is failed
return
0
;
}
size_t
result
=
1
;
while
(
decoder
->
nextPage
())
{
++
result
;
}
return
result
;
return
0
;
}
size_t
imcount
(
const
String
&
filename
,
int
flags
)
...
...
@@ -1035,6 +994,247 @@ bool haveImageWriter( const String& filename )
return
!
encoder
.
empty
();
}
class
ImageCollection
::
Impl
{
public:
Impl
()
=
default
;
Impl
(
const
std
::
string
&
filename
,
int
flags
);
void
init
(
String
const
&
filename
,
int
flags
);
size_t
size
()
const
;
Mat
&
at
(
int
index
);
Mat
&
operator
[](
int
index
);
void
releaseCache
(
int
index
);
ImageCollection
::
iterator
begin
(
ImageCollection
*
ptr
);
ImageCollection
::
iterator
end
(
ImageCollection
*
ptr
);
Mat
read
();
int
width
()
const
;
int
height
()
const
;
bool
readHeader
();
Mat
readData
();
bool
advance
();
int
currentIndex
()
const
;
void
reset
();
private:
String
m_filename
;
int
m_flags
{};
std
::
size_t
m_size
{};
int
m_width
{};
int
m_height
{};
int
m_current
{};
std
::
vector
<
cv
::
Mat
>
m_pages
;
ImageDecoder
m_decoder
;
};
ImageCollection
::
Impl
::
Impl
(
std
::
string
const
&
filename
,
int
flags
)
{
this
->
init
(
filename
,
flags
);
}
void
ImageCollection
::
Impl
::
init
(
String
const
&
filename
,
int
flags
)
{
m_filename
=
filename
;
m_flags
=
flags
;
#ifdef HAVE_GDAL
if
(
m_flags
!=
IMREAD_UNCHANGED
&&
(
m_flags
&
IMREAD_LOAD_GDAL
)
==
IMREAD_LOAD_GDAL
)
{
m_decoder
=
GdalDecoder
().
newDecoder
();
}
else
{
#endif
m_decoder
=
findDecoder
(
filename
);
#ifdef HAVE_GDAL
}
#endif
CV_Assert
(
m_decoder
);
m_decoder
->
setSource
(
filename
);
CV_Assert
(
m_decoder
->
readHeader
());
// count the pages of the image collection
size_t
count
=
1
;
while
(
m_decoder
->
nextPage
())
count
++
;
m_size
=
count
;
m_pages
.
resize
(
m_size
);
// Reinitialize the decoder because we advanced to the last page while counting the pages of the image
#ifdef HAVE_GDAL
if
(
m_flags
!=
IMREAD_UNCHANGED
&&
(
m_flags
&
IMREAD_LOAD_GDAL
)
==
IMREAD_LOAD_GDAL
)
{
m_decoder
=
GdalDecoder
().
newDecoder
();
}
else
{
#endif
m_decoder
=
findDecoder
(
m_filename
);
#ifdef HAVE_GDAL
}
#endif
m_decoder
->
setSource
(
m_filename
);
m_decoder
->
readHeader
();
}
size_t
ImageCollection
::
Impl
::
size
()
const
{
return
m_size
;
}
Mat
ImageCollection
::
Impl
::
read
()
{
auto
result
=
this
->
readHeader
();
if
(
!
result
)
{
return
{};
}
return
this
->
readData
();
}
int
ImageCollection
::
Impl
::
width
()
const
{
return
m_width
;
}
int
ImageCollection
::
Impl
::
height
()
const
{
return
m_height
;
}
bool
ImageCollection
::
Impl
::
readHeader
()
{
bool
status
=
m_decoder
->
readHeader
();
m_width
=
m_decoder
->
width
();
m_height
=
m_decoder
->
height
();
return
status
;
}
// readHeader must be called before calling this method
Mat
ImageCollection
::
Impl
::
readData
()
{
int
type
=
m_decoder
->
type
();
if
((
m_flags
&
IMREAD_LOAD_GDAL
)
!=
IMREAD_LOAD_GDAL
&&
m_flags
!=
IMREAD_UNCHANGED
)
{
if
((
m_flags
&
IMREAD_ANYDEPTH
)
==
0
)
type
=
CV_MAKETYPE
(
CV_8U
,
CV_MAT_CN
(
type
));
if
((
m_flags
&
IMREAD_COLOR
)
!=
0
||
((
m_flags
&
IMREAD_ANYCOLOR
)
!=
0
&&
CV_MAT_CN
(
type
)
>
1
))
type
=
CV_MAKETYPE
(
CV_MAT_DEPTH
(
type
),
3
);
else
type
=
CV_MAKETYPE
(
CV_MAT_DEPTH
(
type
),
1
);
}
// established the required input image size
Size
size
=
validateInputImageSize
(
Size
(
m_width
,
m_height
));
Mat
mat
(
size
.
height
,
size
.
width
,
type
);
bool
success
=
false
;
try
{
if
(
m_decoder
->
readData
(
mat
))
success
=
true
;
}
catch
(
const
cv
::
Exception
&
e
)
{
std
::
cerr
<<
"ImageCollection class: can't read data: "
<<
e
.
what
()
<<
std
::
endl
<<
std
::
flush
;
}
catch
(...)
{
std
::
cerr
<<
"ImageCollection class:: can't read data: unknown exception"
<<
std
::
endl
<<
std
::
flush
;
}
if
(
!
success
)
return
cv
::
Mat
();
if
((
m_flags
&
IMREAD_IGNORE_ORIENTATION
)
==
0
&&
m_flags
!=
IMREAD_UNCHANGED
)
{
ApplyExifOrientation
(
m_decoder
->
getExifTag
(
ORIENTATION
),
mat
);
}
return
mat
;
}
bool
ImageCollection
::
Impl
::
advance
()
{
++
m_current
;
return
m_decoder
->
nextPage
();
}
int
ImageCollection
::
Impl
::
currentIndex
()
const
{
return
m_current
;
}
ImageCollection
::
iterator
ImageCollection
::
Impl
::
begin
(
ImageCollection
*
ptr
)
{
return
ImageCollection
::
iterator
(
ptr
);
}
ImageCollection
::
iterator
ImageCollection
::
Impl
::
end
(
ImageCollection
*
ptr
)
{
return
ImageCollection
::
iterator
(
ptr
,
this
->
size
());
}
void
ImageCollection
::
Impl
::
reset
()
{
m_current
=
0
;
#ifdef HAVE_GDAL
if
(
m_flags
!=
IMREAD_UNCHANGED
&&
(
m_flags
&
IMREAD_LOAD_GDAL
)
==
IMREAD_LOAD_GDAL
)
{
m_decoder
=
GdalDecoder
().
newDecoder
();
}
else
{
#endif
m_decoder
=
findDecoder
(
m_filename
);
#ifdef HAVE_GDAL
}
#endif
m_decoder
->
setSource
(
m_filename
);
m_decoder
->
readHeader
();
}
Mat
&
ImageCollection
::
Impl
::
at
(
int
index
)
{
CV_Assert
(
index
>=
0
&&
size_t
(
index
)
<
m_size
);
return
operator
[](
index
);
}
Mat
&
ImageCollection
::
Impl
::
operator
[](
int
index
)
{
if
(
m_pages
.
at
(
index
).
empty
())
{
// We can't go backward in multi images. If the page is not in vector yet,
// go back to first page and advance until the desired page and read it into memory
if
(
m_current
!=
index
)
{
reset
();
for
(
int
i
=
0
;
i
!=
index
&&
advance
();
++
i
)
{}
}
m_pages
[
index
]
=
read
();
}
return
m_pages
[
index
];
}
void
ImageCollection
::
Impl
::
releaseCache
(
int
index
)
{
CV_Assert
(
index
>=
0
&&
size_t
(
index
)
<
m_size
);
m_pages
[
index
].
release
();
}
/* ImageCollection API*/
ImageCollection
::
ImageCollection
()
:
pImpl
(
new
Impl
())
{}
ImageCollection
::
ImageCollection
(
const
std
::
string
&
filename
,
int
flags
)
:
pImpl
(
new
Impl
(
filename
,
flags
))
{}
void
ImageCollection
::
init
(
const
String
&
img
,
int
flags
)
{
pImpl
->
init
(
img
,
flags
);
}
size_t
ImageCollection
::
size
()
const
{
return
pImpl
->
size
();
}
const
Mat
&
ImageCollection
::
at
(
int
index
)
{
return
pImpl
->
at
(
index
);
}
const
Mat
&
ImageCollection
::
operator
[](
int
index
)
{
return
pImpl
->
operator
[](
index
);
}
void
ImageCollection
::
releaseCache
(
int
index
)
{
pImpl
->
releaseCache
(
index
);
}
Ptr
<
ImageCollection
::
Impl
>
ImageCollection
::
getImpl
()
{
return
pImpl
;
}
/* Iterator API */
ImageCollection
::
iterator
ImageCollection
::
begin
()
{
return
pImpl
->
begin
(
this
);
}
ImageCollection
::
iterator
ImageCollection
::
end
()
{
return
pImpl
->
end
(
this
);
}
ImageCollection
::
iterator
::
iterator
(
ImageCollection
*
col
)
:
m_pCollection
(
col
),
m_curr
(
0
)
{}
ImageCollection
::
iterator
::
iterator
(
ImageCollection
*
col
,
int
end
)
:
m_pCollection
(
col
),
m_curr
(
end
)
{}
Mat
&
ImageCollection
::
iterator
::
operator
*
()
{
CV_Assert
(
m_pCollection
);
return
m_pCollection
->
getImpl
()
->
operator
[](
m_curr
);
}
Mat
*
ImageCollection
::
iterator
::
operator
->
()
{
CV_Assert
(
m_pCollection
);
return
&
m_pCollection
->
getImpl
()
->
operator
[](
m_curr
);
}
ImageCollection
::
iterator
&
ImageCollection
::
iterator
::
operator
++
()
{
if
(
m_pCollection
->
pImpl
->
currentIndex
()
==
m_curr
)
{
m_pCollection
->
pImpl
->
advance
();
}
m_curr
++
;
return
*
this
;
}
ImageCollection
::
iterator
ImageCollection
::
iterator
::
operator
++
(
int
)
{
iterator
tmp
=
*
this
;
++
(
*
this
);
return
tmp
;
}
}
/* End of file. */
modules/imgcodecs/test/test_read_write.cpp
浏览文件 @
04ebedb6
...
...
@@ -303,4 +303,181 @@ TEST(Imgcodecs_Image, write_umat)
EXPECT_EQ
(
0
,
remove
(
dst_name
.
c_str
()));
}
TEST
(
Imgcodecs_Image
,
multipage_collection_size
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
ImageCollection
collection
(
filename
,
IMREAD_ANYCOLOR
);
EXPECT_EQ
((
std
::
size_t
)
6
,
collection
.
size
());
}
TEST
(
Imgcodecs_Image
,
multipage_collection_read_pages_iterator
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
const
string
page_files
[]
=
{
root
+
"readwrite/multipage_p1.tif"
,
root
+
"readwrite/multipage_p2.tif"
,
root
+
"readwrite/multipage_p3.tif"
,
root
+
"readwrite/multipage_p4.tif"
,
root
+
"readwrite/multipage_p5.tif"
,
root
+
"readwrite/multipage_p6.tif"
};
ImageCollection
collection
(
filename
,
IMREAD_ANYCOLOR
);
auto
collectionBegin
=
collection
.
begin
();
for
(
size_t
i
=
0
;
i
<
collection
.
size
();
++
i
,
++
collectionBegin
)
{
double
diff
=
cv
::
norm
(
collectionBegin
.
operator
*
(),
imread
(
page_files
[
i
]),
NORM_INF
);
EXPECT_EQ
(
0.
,
diff
);
}
}
TEST
(
Imgcodecs_Image
,
multipage_collection_two_iterator
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
const
string
page_files
[]
=
{
root
+
"readwrite/multipage_p1.tif"
,
root
+
"readwrite/multipage_p2.tif"
,
root
+
"readwrite/multipage_p3.tif"
,
root
+
"readwrite/multipage_p4.tif"
,
root
+
"readwrite/multipage_p5.tif"
,
root
+
"readwrite/multipage_p6.tif"
};
ImageCollection
collection
(
filename
,
IMREAD_ANYCOLOR
);
auto
firstIter
=
collection
.
begin
();
auto
secondIter
=
collection
.
begin
();
// Decode all odd pages then decode even pages -> 1, 0, 3, 2 ...
firstIter
++
;
for
(
size_t
i
=
1
;
i
<
collection
.
size
();
i
+=
2
,
++
firstIter
,
++
firstIter
,
++
secondIter
,
++
secondIter
)
{
Mat
mat
=
*
firstIter
;
double
diff
=
cv
::
norm
(
mat
,
imread
(
page_files
[
i
]),
NORM_INF
);
EXPECT_EQ
(
0.
,
diff
);
Mat
evenMat
=
*
secondIter
;
diff
=
cv
::
norm
(
evenMat
,
imread
(
page_files
[
i
-
1
]),
NORM_INF
);
EXPECT_EQ
(
0.
,
diff
);
}
}
TEST
(
Imgcodecs_Image
,
multipage_collection_operator_plusplus
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
// operator++ test
ImageCollection
collection
(
filename
,
IMREAD_ANYCOLOR
);
auto
firstIter
=
collection
.
begin
();
auto
secondIter
=
firstIter
++
;
// firstIter points to second page, secondIter points to first page
double
diff
=
cv
::
norm
(
*
firstIter
,
*
secondIter
,
NORM_INF
);
EXPECT_NE
(
diff
,
0.
);
}
TEST
(
Imgcodecs_Image
,
multipage_collection_backward_decoding
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
const
string
page_files
[]
=
{
root
+
"readwrite/multipage_p1.tif"
,
root
+
"readwrite/multipage_p2.tif"
,
root
+
"readwrite/multipage_p3.tif"
,
root
+
"readwrite/multipage_p4.tif"
,
root
+
"readwrite/multipage_p5.tif"
,
root
+
"readwrite/multipage_p6.tif"
};
ImageCollection
collection
(
filename
,
IMREAD_ANYCOLOR
);
EXPECT_EQ
((
size_t
)
6
,
collection
.
size
());
// backward decoding -> 5,4,3,2,1,0
for
(
int
i
=
(
int
)
collection
.
size
()
-
1
;
i
>=
0
;
--
i
)
{
cv
::
Mat
ithPage
=
imread
(
page_files
[
i
]);
EXPECT_FALSE
(
ithPage
.
empty
());
double
diff
=
cv
::
norm
(
collection
[
i
],
ithPage
,
NORM_INF
);
EXPECT_EQ
(
diff
,
0.
);
}
for
(
int
i
=
0
;
i
<
(
int
)
collection
.
size
();
++
i
)
{
collection
.
releaseCache
(
i
);
}
double
diff
=
cv
::
norm
(
collection
[
2
],
imread
(
page_files
[
2
]),
NORM_INF
);
EXPECT_EQ
(
diff
,
0.
);
}
TEST
(
ImgCodecs
,
multipage_collection_decoding_range_based_for_loop_test
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
const
string
page_files
[]
=
{
root
+
"readwrite/multipage_p1.tif"
,
root
+
"readwrite/multipage_p2.tif"
,
root
+
"readwrite/multipage_p3.tif"
,
root
+
"readwrite/multipage_p4.tif"
,
root
+
"readwrite/multipage_p5.tif"
,
root
+
"readwrite/multipage_p6.tif"
};
ImageCollection
collection
(
filename
,
IMREAD_ANYCOLOR
);
size_t
index
=
0
;
for
(
auto
&
i
:
collection
)
{
cv
::
Mat
ithPage
=
imread
(
page_files
[
index
]);
EXPECT_FALSE
(
ithPage
.
empty
());
double
diff
=
cv
::
norm
(
i
,
ithPage
,
NORM_INF
);
EXPECT_EQ
(
0.
,
diff
);
++
index
;
}
EXPECT_EQ
(
index
,
collection
.
size
());
index
=
0
;
for
(
auto
&&
i
:
collection
)
{
cv
::
Mat
ithPage
=
imread
(
page_files
[
index
]);
EXPECT_FALSE
(
ithPage
.
empty
());
double
diff
=
cv
::
norm
(
i
,
ithPage
,
NORM_INF
);
EXPECT_EQ
(
0.
,
diff
);
++
index
;
}
EXPECT_EQ
(
index
,
collection
.
size
());
}
TEST
(
ImgCodecs
,
multipage_collection_two_iterator_operatorpp
)
{
const
string
root
=
cvtest
::
TS
::
ptr
()
->
get_data_path
();
const
string
filename
=
root
+
"readwrite/multipage.tif"
;
ImageCollection
imcol
(
filename
,
IMREAD_ANYCOLOR
);
auto
it0
=
imcol
.
begin
(),
it1
=
it0
,
it2
=
it0
;
vector
<
Mat
>
img
(
6
);
for
(
int
i
=
0
;
i
<
6
;
i
++
)
{
img
[
i
]
=
*
it0
;
it0
->
release
();
++
it0
;
}
for
(
int
i
=
0
;
i
<
3
;
i
++
)
{
++
it2
;
}
for
(
int
i
=
0
;
i
<
3
;
i
++
)
{
auto
img2
=
*
it2
;
auto
img1
=
*
it1
;
++
it2
;
++
it1
;
EXPECT_TRUE
(
cv
::
norm
(
img2
,
img
[
i
+
3
],
NORM_INF
)
==
0
);
EXPECT_TRUE
(
cv
::
norm
(
img1
,
img
[
i
],
NORM_INF
)
==
0
);
}
}
}}
// namespace
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录