本文总结了使用Python进行机器视觉(图像处理)编程时常用的数据结构,主要包括以下内容:

  • 数据结构
  • 通用序列操作索引(indexing)、分片(slicing)、加(adding)、乘(multipying)等
  • 列表创建、list函数、基本操作:赋值、删除、分片赋值、插入、排序等
  • 元组创建、tuple函数、基本操作
  • NumPy数组创建数组、创建图像、获取形状、维度、元素个数、元素类型、访问像素、通道分离、使用mask
原文与更新地址:blog.csdn.net/iracer/article/details/52037938

1. 数据结构

数据结构是通过某种方式(例如对元素进行编号)组织在一起的数据元素的集合,这些数据元素可以是数字或者字符,甚至可以是其他数据结构。在Python中最基本的数据结构是序列(sequence)。序列中每个元素被分配一个序号——即元素的位置,也称为索引(index),第一个元素的索引是0,第二个是1,以此类推。

python包含6种內建序列,最常用的两种类型是:列表元组。列表和元组的主要区别在于列表可以修改,元组不可以修改。而用于处理图像的基本数据结构是数组,由于Python标准库中的內建数组只能处理一维数组并且提供的功能较少,因此做编程时常使用NumPy模块的array()数组表示图像,并进行各类处理。


2. 通用序列操作

所有序列都可以进行某些特定操作,包括:索引(indexing)、分片(slicing)、加(adding)、乘(multipying)以及检查某个元素是否属于序列成员(成员资格),除此之外,python还有计算序列长度、找出最大元素和最小元素的內建函数。

1)索引

序列中所有元素的编号都是从0开始递增。

  1. >>>greeting = 'Hello'  
  2. >>>greeting[0]  
  3. 'H'  
  4. >>>greeting[1]  
  5. 'e'  

所有序列都可以通过这种方式获取元素。最后一个元素的编号是-1

  1. >>>greeting[-1]  
  2. 'o'  

如果一个函数调用返回一个序列,则可以直接对返回序列进行索引操作

  1. >>>fourth = raw_input('Year: ')[3]  
  2. Year: 2016  
  3. >>>fourth  
  4. 6  

2)分片

使用分片操作来方位一定范围内的元素。分片通过冒号隔开两个索引来实现。

  1. >>>tag='<a herf="http://www.python.org">Python web site</a>'  
  2. >>>tag[9:30]  
  3. '"http://www.python.org '  
  4. >>>numbers=[1,2,3,4,5,6,7,8,9,10]  
  5. >>>numbers[3:6]  
  6. [4,5,6]  
  7. >>>numbers[0:1]  
  8. [1]  

注意索引边界:第1个索引的元素包含在分片内,2个索引的元素不在分片内,如果要索引最后一个元素

  1. >>>numbers[-3:]  
  2. [8,9,10]  
  3. >>>print numbers[-1:]  
  4. [10]  

这种方法同样适用于序列开始的元素:

  1. >>>numbers[:3]  
  2. [1,2,3]  

如果需要复制整个序列,可以将两个索引都置空:

  1. >>>numbers[:]  
  2. [1,2,3,4,5,6,7,8,9,10]  

我们还可以使用第三个参数设置分片的步长,下面代码为从numbers序列中选出从0到10,步长为2的元素
  1. >>>numbers[0:10:2]  
  2. [1,3,5,7,9]  

如果要将每4个元素中的第1个提取出来可以这样写

  1. >>>numbers[::4]  
  2. [1,5,9]  

步长为负数将向左提取元素,当使用负数作为步长时开始的点的索引必须大于结束点的索引

  1. >>>number[8:3:-1]  
  2. [9,8,7,6,5]   
  3. >>>numbers[10:0:-2]  
  4. [108642]  

3)序列相加

使用+运算符可以进行序列的连接操作:

  1. >>>[1,2,3] + [4,5,6]  
  2. [1,2,3,4,5,6]  
  3. >>>'Hello, ' + 'world!'  
  4. 'Hello, world!'  

注意同种类型的序列才能连接到一起,列表和字符串是无法连接的。

4)乘法

数字x乘以序列会生成新的序列。新序列中,原来的序列将被重复x

  1. >>>'pyhton' * 5  
  2. 'pyhtonpyhtonpyhtonpyhtonpyhton'  
  3. >>>[42]*10  
  4. [42,42,42,42,42,42,42,42,42,42]  

5None空列表和初始化

空列表可以通过两个中括号中间什么都不写表示[]

如果想创建一个占用 10个元素空间,却不包括任何有用内容的列表,可以用:

  1. >>>[0]*10  

None是一个Python的內建值,它的确切含义是这里什么都没有。

  1. >>>[None]*10  

6)成员资格in

为了检查一个值是否在列表中,可以使用in运算符,返回布尔值真或假:

  1. >>>permission = 'rw'  
  2. >>>'w' in permission  
  3. True  
  4. >>>'x' in permission  
  5. False  

下面的例子,检查用户名和PIN码:

  1. database = [  
  2.     ['albert''1234'],  
  3.     ['dilbert','4242'],  
  4.     ['smith',  '7524'],  
  5.     ['jones',  '9843']  
  6. ]  
  7. username = raw_input('User name: ')  
  8. pin = raw_input('PIN code: ')  
  9. if [username,pin] in database:   
  10.     print 'Access granted'  

运行结果:

User name: jones

PIN code: 9843

Access granted

7)长度、最小值和最大值

內建函数len,min,max

  1. >>>numbers[100,34,678]  
  2. >>>len(numbers)  
  3. 3  
  4. >>>max(numbers)  
  5. 678  
  6. >>>min(numbers)  
  7. 34  
  8. >>>max(2,3)  
  9. 3  
  10. >>>min(2,3,4,5)  
  11. 2  

3. 列表


1list函数

因为字符串不能像列表一样修改,所以有时候根据字符串创建列表很有用

  1. >>>list('Hello')  
  2. ['H','e','l','l','o']  

list适用于所有类型的序列,而不只是列表。


2)列表基本操作

  • 元素赋值

  1. >>>x=[1,1,1]  
  2. >>>x[1]=2  
  3. >>>x  
  4. [1,2,1]  
  • 删除元素

  1. >>>x=[1,2,3]  
  2. >>>del x[1]  
  3. >>>x  
  4. [1,3]  

del也可删除其他元素,甚至是变量。

  • 分片赋值

  1. >>>name=list('Perl')  
  2. >>>name  
  3. ['P','e','r','l']  
  4. >>>name[2:]=list('ar')  
  5. >>>name  
  6. ['P','e','a','r']  
  • 通过分片赋值插入元素和删除元素

  1. >>>numbers=[1,5]  
  2. >>>numbers[1:1]=[2,3,4]  
  3. >>>numbers  
  4. [1,2,3,4,5]  
  5. >>>numbers[1:4]=[]  
  6. >>>numbers  
  7. [1,5]  

3)列表方法

列表方法的使用:对象.方法(参数)

  • append 在列表末尾追加

  1. >>>a = [1,2,3]  
  2. >>>a.append['4']  
  3. >>>a  
  4. [1,2,3,4]  
  • count 统计某个元素在列表中出现的次数

  1. >>>['to','go','will','be','to','not'].count('to')  
  2. 2  
  • extend 在列表末尾一次性追加另一个序列中的多个值

  1. >>>a = [1,2,3]  
  2. >>>b = [4,5,6]  
  3. >>>a.extend(b)  
  4. >>>a  
  5. [1,2,3,4,5,6]  
  6. >>>c = [1,2,3]  
  7. >>>d = [4,5,6]  
  8. >>>c+d  
  9. [1,2,3,4,5,6]  
  10. >>>c  
  11. [1,2,3]  
  • index 从列表中找出某个值第一个匹配项的索引位置

  1. >>>slogen= ['we''are''the''champion']  
  2. >>>slogen.index('are')  
  3. 1  
  4. >>> slogen[1]  
  5. 'are'  
  • insert 将对象插入到列表中

  1. >>>numbers=[1,2,3,4,6]  
  2. >>>numbers.insert(4,'five')  
  3. >>>numbers  
  4. [1234'five'6]  
第1个参数为插入的位置,在此索引前插入;

第2个参数为插入的元素内容

insert方法的操作也可用分片的方法实现元素插入

  1. >>>numbers=[1,2,3,4,6]  
  2. >>>numbers[4:4]=['five']  
  3. >>>numbers  
  4. [1234'five'6]  
  • pop 移除列表中的一个元素

pop方法可实现一个常见的数据结构——栈。栈的原理就像堆盘子,只能在顶部放一个盘子,同样也只能从顶部拿走一个盘子。最后被放入堆栈的元素最先被移除。(此原则称为后进先出,LIFO)。

pop()方法默认移除列表中的最后一个元素,并返回该元素的值
  1. >>>numbers=[1,2,3,4,5]  
  2. >>>numbers.pop()   
  3. 5  
  4. >>> numbers  
  5. [1234]  
如果想要移除列表中的第一个元素,可以用pop(0)
  1. >>>numbers=[1,2,3,4,5]  
  2. >>>numbers.pop(0)   
  3. 1  
  4. >>> numbers  
  5. [2345]  

Python没有入栈操作,可以用append方法代替。

如果想要实现一个先进先出(FIFO)队列,可以使用insert(0,...)来替代append方法。或者也可以使用append方法,但必须用pop(0)替代pop()。也可使用collection模块中的deque对象。
  • remove 移除列表中的某个值的第一个匹配项

  1. >>>x=['to','go','will','be','to','not']  
  2. >>>x.remove('to')  
  3. >>>x  
  4. ['go','will','be','to','not']  
  • reverse 将列表中的元素反向存放

  1. >>>x=[1,2,3]  
  2. >>>x.reverse()  
  3. >>>x  
  4. [3,2,1]  
  • sort 在原始位置对列表进行排序

在原始位置排序将改变原来的列表,从而让其中的元素能按一定的顺序重新排列,而不是简单的返回一个排序的列表副本。

  1. >>>x=[3,1,2,6,4,5,7,9,8]  
  2. >>>x.sort()  
  3. >>>x  
  4. [1,2,3,4,5,6,7,8,9]  
注意:sort方法只改变原始列表的排序,并没有返回值。如果需要一个重新排序的列表副本,应该如下操作:
  1. >>>x=[3,1,2,6,4,5,7,9,8]  
  2. >>>y=x[:]  #不能直接y=x  
  3. >>>y.sort()  
  4. >>>y  
  5. [1,2,3,4,5,6,7,8,9]  
  6. >>>x  
  7. [3,1,2,6,4,5,7,9,8]  
注意:y=x只是让y和x指向同一个列表,而y=x[:]是复制整个x列表给y。
  • sorted 获取排序列表的副本

  1. >>>x=[3,1,2,6,4,5,7,9,8]  
  2. >>>y=sorted(x)  
  3. >>>y  
  4. [1,2,3,4,5,6,7,8,9]  
  5. >>>x  
  6. [3,1,2,6,4,5,7,9,8]  
  • sort方法的高级排序

希望列表元素能按照特定的方式排序(而不是sort函数默认的方式,即根据Python默认排序规则按升序排列元素),可以通过compare(x,y)的形式自定义比较函数。compare(x,y)函数会在x<y时返回负数,在x>y时返回正数,如果x=y则返回0(根据自己定义)。定义好该函数后,可以提供给sort方法作为参数。內建函数cmp提供了比较函数的默认实现方式:

  1. >>>cmp(16,12)  
  2. 1  
  3. >>>cmp(10,12)  
  4. -1  
  5. >>>cmp(10,10)  
  6. 0  
  7. >>>numbers=[1,4,2,9]  
  8. >>>numbers.sort(cmp)  
  9. >>>numbers  
  10. [1,2,4,9]  

sort方法还有另外两个参数可选,可以通过某个名字来指定该参数(关键字参数):

参数:key

提供一个在排序中使用的函数,该函数不是直接确定对象的大小,而是为每个元素创建一个键,然后所有元素根据键来排序。

  1. >>>x=['nor','break','if','then','present']  
  2. >>>x.sort(key=len) # 按字符串长度排序  
  3. >>>x  
  4. ['if''nor''then''break''present']  

参数:reverse

另一个关键字参数reverse是简单的布尔值,用于指明是否要进行反向排序
  1. >>> x=[3,1,2,6,4,5,7,9,8]  
  2. >>> x.sort(reverse=True)  
  3. >>>x  
  4. [987654321]  
注意:cmp,key,reverse参数都可用于sorted函数。

4.  元组

元组与列表一样,也是一种序列,但元组是不可变列表,元组不能修改。

元组的作用

  • 体现在映射(和集合的成员)中当做键使用——列表不行
  • 元组在很多內建函数的返回值存在,也就是说我们必须对元组进行处理


(1)创建元组

  • 创建一个元组

  1. >>>1,2,3  
  2. (1,2,3)  

元组大部时候通过圆括号括起来

  1. >>>x=(1,2,3)  
  2. >>>x  
  3. (1,2,3)  
  • 创建一个空元组

空元组可以用没有内容的空括号括起来

  1. >>>()  
  2. ()  
  • 创建一个包含一个元素的元组

  1. >>>(10,)  
  2. (10,)  

是的,一个元素也需要用逗号。逗号很重要,看下面的例子
  1. >>>2*(3+2)  
  2. 10   
  3. >>>2*(3+2,)  
  4. (55)  
不加逗号为数字,加逗号就是元组。


2tuple函数

tuple函数以一个序列作为参数并转换为元组,如果参数本身就是元组,则不发生变化。

  1. >>>tuple([1,2,3])  
  2. (1,2,3)  
  3. >>>tuple(['a','b','c'])  
  4. ('a','b','c')  
  5. >>>tuple((1,2,3))  
  6. (1,2,3)  

3)元组基本操作

元组除了创建和访问其元素外,没有太多其他操作,元组操作与操作其他序列类似。

  1. >>>x=1,2,3  
  2. >>>x[1]  
  3. 2  
  4. >>>x[0:2]  
  5. (1,2)  

5. NumPyarray(数组)对象


NumPy模块用于python计算机视觉编程时的向量、矩阵的表示与操作,是opencv for python的主要数据结构模块。NumPy中的数组对象array是多维的,可以用来表示向量、矩阵和图像。一个数组对象很像一个列表(或者是列表的列表),但数组中的元素必须具有相同的数据类型。除非创建数组对象时指定数据类型,否则数据类型会按照数据的类型自动确定。

本节代码假定已经以如下形式导入OpenCV和NumPy两个库

  1. import cv2  
  2. import numpy as np  

(1)np.array()创建数组

  1. >>> a = np.array([1234])  
  2. >>> b = np.array((5678))  
  3. >>> c = np.array([[1234],[4567], [78910]])  
  4. >>> b  
  5. array([5678])  
  6. >>> c  
  7. array([[1234],  
  8. [4567],  
  9. [78910]])  
  10. >>> d = np.arange(15).reshape(35)  
  11. >>> d  
  12. array([[ 0,  1,  2,  3,  4],  
  13.        [ 5,  6,  7,  8,  9],  
  14.        [1011121314]])  

(2)np.array()创建黑白图像

使用np.zeros()创建一幅图像,dtype为元素数据类型,下文有具体分析,8位灰度图像为uint8型。接着用np.ones()创建一幅图像,通过赋值称为一幅白色图像。

  1. img1 = np.ones((100,200),dtype=np.uint8)  
  2. img2 = np.ones((100,200),dtype=np.uint8)  
  3. img2[:]=255;  
  4. cv2.imshow('img1',img1)  
  5. cv2.imshow('img2',img2)  
  6. cv2.waitKey(0)  

彩色图像的创建需要指定一个3维数组,具体方法请看下文。

(3)ndarray.shape属性获得/修改数组形状

  • 获取数组 shape 属性

数组的形状可以通过其shape 属性获得,它是一个描述数组各个轴长度的元组(tuple),看看上文定义的a,c数组的shape属性:

  1. >>> a.shape  
  2. (4,)  
  3. >>> c.shape  
  4. (34)  
数组a的shape 属性只有一个元素,因此它是一维数组。而数组c的shape属性有两个元素,因此它是二维数组,其中第0轴的长度为3,第1轴的长度为4。
  • 获取图像的宽高

图像本质是矩阵,因此可以使用shape属性获取图像矩阵的行、列和通道数,如果图像是灰度图,则没有第3个参数。我们也可以用ndim方法判断图像通道数:

  1. img = cv2.imread('f:/images/cow.jpg')  
  2. rows,cols,channels = img.shape  
  3. print 'rows,cols,channels = ',rows,cols,channels  
  4. print 'demension = ',img.ndim  
  5. cv2.imshow('test',img)  
  6. cv2.waitKey(0)  

运行结果:

rows,cols,channels =  400 600 3

demension =  3

  • 修改数组 shape 属性

可以通过修改数组的shape 属性,在保持数组元素个数不变的情况下,改变数组每个轴的长度。下面的例子将数组c的shape 属性改为(4,3),注意:从(3,4)改为(4,3)并不是对数组进行转置,而只是改变每个轴的大小,数组元素在内存中的位置并没有改变。

  1. >>> c = np.array([[1234],[4567], [78910]])  
  2. >>> c.shape = 4,3  
  3. >>> c  
  4. array([[ 123],  
  5. 445],  
  6. 677],  
  7. 8910]])  
当设置某个轴的元素个数为-1 时,将自动计算此轴的长度。由于数组c 中有12 个元素,因此下面的程序将数组c 的shape 属性改为了(2,6):
  1. >>> c.shape = 2,-1  
  2. >>> c  
  3. array([[ 123445],  
  4. 6778910]])  

使用数组的reshape()方法,可以创建指定形状的新数组,而原数组的形状保持不变

  1. >>> a = np.array([1234])  
  2. >>> e = a.reshape((2,2)) # 也可以用a.reshape(2,2)  
  3. >>> e  
  4. array([[12],  
  5. [34]])  
  6. >>> a  
  7. array([1234])  

注意:数组a 和e 其实共享数据存储空间,因此修改其中任意一个数组的元素都会同时修改另外一个数组的内容:

  1. >>> a[1] = 100 # 将数组a 的第一个元素改为100  
  2. >>> e # 注意数组d 中的2 也被改为了100  
  3. array([[ 1100],  
  4. [34]])  

(4)ndarray.ndim属性数组维度

返回数组的轴数量,即维度。在Python中维度称为rank

(5)ndarray.dtype属性:数组元素类型

数组的元素类型可以通过dtype 属性获得。前面例子中,创建数组所用序列的元素都是整数,因此所创建的数组的元素类型是整型,并且是32bit 的长整型:

  1. >>> c.dtype  
  2. dtype('int32')  
  3. >>>print img1.dtype  
  4. uint8  

NumPy 中的数据类型都有几种字符串表示方式,字符串和类型之间的对应关系都存储在typeDict 字典中,例如'd'、'double'、'float64'都表示双精度浮点类型:

  1. >>> np.typeDict["d"]  
  2. <type 'numpy.float64'>  
  3. >>> np.typeDict["double"]  
  4. <type 'numpy.float64'>  
  5. >>> np.typeDict["float64"]  
  6. <type 'numpy.float64'>  

完整的类型列表可以通过下面的语句得到,它将typeDict字典中所有的值转换为一个集合,从而去除其中的重复项:

  1. >>> print set(np.typeDict.values())  
  2. set([<type 'numpy.float64'>, <type 'numpy.int32'>,  
  3. <type 'numpy.bool_'>, <type 'numpy.float64'>,   
  4. <type 'numpy.uint64'>, <type 'numpy.int64'>,  
  5. <type 'numpy.datetime64'>, <type 'numpy.uint8'>,   
  6. <type 'numpy.timedelta64'>, <type 'numpy.object_'>,   
  7. <type 'numpy.uint16'>, <type 'numpy.string_'>,   
  8. <type 'numpy.uint32'>, <type 'numpy.unicode_'>,   
  9. <type 'numpy.complex128'>, <type 'numpy.uint32'>,   
  10. <type 'numpy.void'>, <type 'numpy.complex64'>,   
  11. <type 'numpy.complex128'>, <type 'numpy.int8'>,   
  12. <type 'numpy.float16'>, <type 'numpy.int16'>,   
  13. <type 'numpy.float32'>, <type 'numpy.int32'>])  

(6)ndarray.size属性:数组元素个数

数组中所有元素的个数。这个参数等于shape属性返回的参数的乘积。

  1. >>>print img1.shape  
  2. (100,200)  
  3. >>>print img1.size  
  4. 20000  

(7)ndarray.itemsize属性:单个数组元素所占字节数

数组单个元素所占的字节数。例如,数组元素为float64型时,其itemsize=8 (=64/8)。如果是复数complex32类型,则itemsize 4 (=32/8)。
  1. >>>print img1.itemsize  
  2. 1  

(8)ndarray.data属性:实际数组元素的缓存

通常用不到这个属性,因为可以通过下标方位数组元素。


(9)访问像素/访问多维数组元素

NumPy的array数组对象与Python中的序列一样,可以通过下标、切片的方式访问。

多维数组的存取和一维数组类似,因为多维数组有多个轴,因此它的下标需要用多个值表示。NumPy 采用元组(tuple)作为数组的下标,元组中的每个元素和数组的每个轴对应。下图显示了一个形状为(6,6)的数组a,图中用不同颜色和线型标识出各个下标对应的选择区域。

  1. >>> a = np.arange(06010).reshape(-11) + np.arange(06)  
  2. >>> a  
  3. array([[ 012345],  
  4. [101112131415],  
  5. [202122232425],  
  6. [303132333435],  
  7. [404142434445],  
  8. [505152535455]])  


Python 的下标语法(用[]存取序列中的元素)本身并不支持多维,但是由于可以使用任何对象作为下标,因此NumPy 使用元组作为下标存取数组中的元素,使用元组可以很方便地表示多个轴的下标。虽然在Python 程序中,经常用圆括号将元组的元素括起来,但其实元组的语法只需要用逗号隔开元素即可,例如“x,y=y,x”就是用元组交换变量值的一个例子。因此a[1,2]和a[(1,2)]完全相同,都是使用元组(1,2)作为数组a 的下标。

(10)np.array()创建彩色图像

下面的例子通过创建一个3维数组作为3通道彩色图像,并给不同通道赋值,创建两幅不同的色彩图像:
  1. import cv2  
  2. import numpy as np  
  3. import random  
  4.   
  5. # create a blue image  
  6. img3 = np.zeros((100,200,3),dtype=np.uint8)  
  7. img3[:,:,0]=255 # blue channel  
  8.   
  9. # create a random color image  
  10. img4 = np.zeros((100,200,3),dtype=np.uint8)  
  11. seq = xrange(0,255)  
  12. ch0 = random.sample(seq,200)  
  13. ch1 = random.sample(seq,200)  
  14. ch2 = random.sample(seq,200)  
  15. img4[0:200,:,0] = ch0  
  16. img4[0:200,:,1] = ch1  
  17. img4[0:200,:,2] = ch2  
  18.   
  19. # display image      
  20. cv2.imshow('img3',img3)  
  21. cv2.imshow('img4',img4)  
  22. cv2.waitKey(0)  
  23. cv2.destroyAllWindows()  



(11)通道分离

下面的代码实现BGR通道分离:

  1. img = cv2.imread('f:/images/Lena.jpg')  
  2.   
  3. b = np.zeros((img.shape[0],img.shape[1]), dtype=img.dtype)    
  4. g = np.zeros((img.shape[0],img.shape[1]), dtype=img.dtype)    
  5. r = np.zeros((img.shape[0],img.shape[1]), dtype=img.dtype)    
  6.     
  7. b[:,:] = img[:,:,0]    
  8. g[:,:] = img[:,:,1]    
  9. r[:,:] = img[:,:,2]    
  10.     
  11. cv2.imshow("Blue", r)    
  12. cv2.imshow("Red", g)    
  13. cv2.imshow("Green", b)    
  14. cv2.waitKey(0)    
  15. cv2.destroyAllWindows()   

(12)设置mask屏蔽不感兴趣区域

  1. img = cv2.imread('f:/images/Lena.jpg',0)  
  2. mask = np.zeros((img.shape[0],img.shape[1]),dtype=img.dtype)  
  3. mask[img.shape[0]/4:3*img.shape[0]/4,img.shape[1]/4:3*img.shape[1]/4] = 255  
  4. img_mask = img.copy()  
  5. img_mask[mask==0]=0  
  6. cv2.imshow('mask',mask)  
  7. cv2.imshow('Lena_mask',img_mask)  
  8. cv2.imshow('Lena',img)  
  9. cv2.waitKey(0)  
  10. cv2.destroyAllWindows()  
读取彩色或黑白图像都可已使用上述代码。读取彩色图像cv2.imread()的可选标志为置1。

本文部分内容参考:

Pyhton基础教程、Python科学计算、NumPy官方指南

转载请注明出处(本文更新链接):blog.csdn.net/iracer/article/details/52037938
posted on 2017-10-03 10:25  未雨愁眸  阅读(668)  评论(0编辑  收藏  举报