提交 a2975f33 编写于 作者: B Bernat Gabor

1) Converted all images to JPG to reduce size.

2) Added a raw Latex page break directive after each TOC tree. (For the PDF tutorial look).
3) Two finished tutorials: 
   a) one describing how the Mat data structure works and its output capabilities (format function) (demonstration YouTube video included).
   b) one describing image scanning operations plus the LUT function (demonstration YouTube video included). 
   c) a basic filtering approach (plus multi row image scanning demonstration) in the work.
上级 f503b029
......@@ -168,9 +168,9 @@ html_use_index = True
# Output file base name for HTML help builder.
htmlhelp_basename = 'opencv'
# OpenCV docs use some custom LaTeX macros in the formulae. Make sure we include the definitions
# OpenCV docs use some custom LaTeX macros in the formula. Make sure we include the definitions
pngmath_latex_preamble = r"""
\usepackage{euler}\usepackage[usenames,dvipsnames]{color}\usepackage{amssymb}\usepackage{amsmath}\usepackage{bbm}
\usepackage{euler}\usepackage[usenames,dvipsnames]{color}\usepackage{amssymb}\usepackage{amsmath}\usepackage{bbm}\usepackage[table]{xcolor}
\newcommand{\matTT}[9]{
\[
\left|\begin{array}{ccc}
......@@ -235,6 +235,7 @@ preamble ="""
\usepackage{euler}
\usepackage[scaled=0.85]{beramono}
\usepackage{mymath}\usepackage{amssymb}\usepackage{amsmath}\usepackage{bbm}\setcounter{secnumdepth}{1}
\usepackage[table]{xcolor}
"""
latex_elements = {'preamble': preamble}
......@@ -308,44 +309,48 @@ extlinks = {'cvt_color': ('http://opencv.willowgarage.com/documentation/cpp/imgp
'threshold': ('http://opencv.willowgarage.com/documentation/cpp/miscellaneous_image_transformations.html#cv-threshold%s', None),
'filter2d': ('http://opencv.willowgarage.com/documentation/cpp/image_filtering.html#cv-filter2d%s', None),
'copy_make_border': ('http://opencv.willowgarage.com/documentation/cpp/imgproc_image_filtering.html?#copyMakeBorder%s', None),
'sobel': ('http://opencv.willowgarage.com/documentation/cpp/image_filtering.html#cv-sobel%s', None),
'scharr': ('http://opencv.willowgarage.com/documentation/cpp/image_filtering.html#cv-scharr%s', None),
'sobel': ('http://opencv.willowgarage.com/documentation/cpp/image_filtering.html#cv-sobel%s', None),
'scharr': ('http://opencv.willowgarage.com/documentation/cpp/image_filtering.html#cv-scharr%s', None),
'laplacian': ('http://opencv.willowgarage.com/documentation/cpp/image_filtering.html#cv-laplacian%s', None),
'canny': ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#Canny%s', None),
'copy_to': ('http://opencv.willowgarage.com/documentation/cpp/core_basic_structures.html?#Mat::copyTo%s', None),
'canny': ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#Canny%s', None),
'copy_to': ('http://opencv.willowgarage.com/documentation/cpp/core_basic_structures.html?#Mat::copyTo%s', None),
'opencv_group' : ('http://tech.groups.yahoo.com/group/OpenCV/%s', None),
'hough_lines' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-houghlines%s', None),
'hough_lines_p' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-houghlinesp%s', None),
'hough_circles' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-houghcircles%s', None),
'remap' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#remap%s', None),
'warp_affine' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#cv-warpaffine%s' , None),
'get_rotation_matrix_2d' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#cv-getrotationmatrix2d%s', None),
'hough_lines' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-houghlines%s', None),
'hough_lines_p' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-houghlinesp%s', None),
'hough_circles' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-houghcircles%s', None),
'remap' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#remap%s', None),
'warp_affine' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#cv-warpaffine%s' , None),
'get_rotation_matrix_2d' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#cv-getrotationmatrix2d%s', None),
'get_affine_transform' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_geometric_image_transformations.html?#cv-getaffinetransform%s', None),
'equalize_hist' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_histograms.html?#equalizeHist%s', None),
'split' : ('http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#split%s', None),
'calc_hist' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_histograms.html?#calcHist%s', None),
'split' : ('http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#split%s', None),
'calc_hist' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_histograms.html?#calcHist%s', None),
'normalize' : ('http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#normalize%s', None),
'match_template' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_object_detection.html?#matchTemplate%s', None),
'min_max_loc' : ('http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#minMaxLoc%s', None),
'mix_channels' : ( 'http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#mixChannels%s', None),
'calc_back_project' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_histograms.html?#calcBackProject%s', None),
'match_template' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_object_detection.html?#matchTemplate%s', None),
'min_max_loc' : ('http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#minMaxLoc%s', None),
'mix_channels' : ( 'http://opencv.willowgarage.com/documentation/cpp/core_operations_on_arrays.html?#mixChannels%s', None),
'calc_back_project' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_histograms.html?#calcBackProject%s', None),
'compare_hist' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_histograms.html?#compareHist%s', None),
'corner_harris' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cornerHarris%s', None),
'good_features_to_track' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-goodfeaturestotrack%s', None),
'corner_min_eigenval' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-cornermineigenval%s', None),
'corner_eigenvals_and_vecs' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-cornereigenvalsandvecs%s', None),
'corner_sub_pix' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-cornersubpix%s', None),
'corner_harris' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cornerHarris%s', None),
'good_features_to_track' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-goodfeaturestotrack%s', None),
'corner_min_eigenval' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-cornermineigenval%s', None),
'corner_eigenvals_and_vecs' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-cornereigenvalsandvecs%s', None),
'corner_sub_pix' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_feature_detection.html?#cv-cornersubpix%s', None),
'find_contours' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-findcontours%s', None),
'convex_hull' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-convexhull%s', None),
'convex_hull' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-convexhull%s', None),
'draw_contours' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-drawcontours%s', None),
'bounding_rect' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-boundingrect%s', None),
'min_enclosing_circle' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-minenclosingcircle%s', None),
'min_area_rect' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-minarearect%s', None),
'min_area_rect' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-minarearect%s', None),
'fit_ellipse' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-fitellipse%s', None),
'moments' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-moments%s', None),
'contour_area' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-contourarea%s', None),
'arc_length' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-arclength%s', None),
'point_polygon_test' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-pointpolygontest%s', None)
'arc_length' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-arclength%s', None),
'basicstructures' : ('http://opencv.itseez.com/modules/core/doc/basic_structures.html#%s', None),
'readwriteimagevideo' : ('http://opencv.itseez.com/modules/highgui/doc/reading_and_writing_images_and_video.html#%s', None),
'operationsonarrays' : ('http://opencv.itseez.com/modules/core/doc/operations_on_arrays.html#%s', None),
'utilitysystemfunctions':('http://opencv.itseez.com/modules/core/doc/utility_and_system_functions_and_macros.html%s', None),
'point_polygon_test' : ('http://opencv.willowgarage.com/documentation/cpp/imgproc_structural_analysis_and_shape_descriptors.html#cv-pointpolygontest%s', None)
}
......@@ -6,3 +6,7 @@
Although we got most of our images in a 2D format they do come from a 3D world. Here you will learn how to find out from the 2D images information about the 3D world.
.. include:: ../../definitions/noContent.rst
.. raw:: latex
\pagebreak
......@@ -114,6 +114,6 @@ Explanation
Result
=======
.. image:: images/Adding_Images_Tutorial_Result_0.png
.. image:: images/Adding_Images_Tutorial_Result_0.jpg
:alt: Blending Images Tutorial - Final Result
:align: center
......@@ -260,6 +260,6 @@ Result
Compiling and running your program should give you a result like this:
.. image:: images/Drawing_1_Tutorial_Result_0.png
.. image:: images/Drawing_1_Tutorial_Result_0.jpg
:alt: Drawing Tutorial 1 - Final Result
:align: center
......@@ -185,7 +185,7 @@ Result
.. code-block:: bash
$ ./BasicLinearTransforms lena.png
$ ./BasicLinearTransforms lena.jpg
Basic Linear Transforms
-------------------------
* Enter the alpha value [1.0-3.0]: 2.2
......@@ -193,7 +193,7 @@ Result
* We get this:
.. image:: images/Basic_Linear_Transform_Tutorial_Result_0.png
.. image:: images/Basic_Linear_Transform_Tutorial_Result_0.jpg
:height: 400px
:alt: Basic Linear Transform - Final Result
:align: center
.. _howToScanImagesOpenCV:
How to scan images, lookup tables and time measurement with OpenCV
*******************************************************************
Goal
====
We'll seek answers for the following questions:
.. container:: enumeratevisibleitemswithsquare
+ How to go through each and every pixel of an image?
+ How is OpenCV matrix values stored?
+ How to measure the performance of our algorithm?
+ What are lookup tables and why use them?
Our test case
=============
Let us consider a simple color reduction method. Using the unsigned char C and C++ type for matrix item storing a channel of pixel may have up to 256 different values. For a three channel image this can allow the formation of way too many colors (16 million to be exact). Working with so many color shades may give a heavy blow to our algorithm performance. However, sometimes it is enough to work with a lot less of them to get the same final result.
In this cases it's common that we make a *color space reduction*. This means that we divide the color space current value with a new input value to end up with fewer colors. For instance every value between zero and nine takes the new value zero, every value between ten and nineteen the value ten and so on.
When you divide an *uchar* (unsigned char - aka values between zero and 255) value with an *int* value the result will be also *char*. These values may only be char values. Therefore, any fraction will be rounded down. Taking advantage of this fact the upper operation in the *uchar* domain may be expressed as:
.. math::
I_{new} = (\frac{I_{old}}{10}) * 10
A simple color space reduction algorithm would consist of just passing through every pixel of an image matrix and applying this formula. It's worth noting that we do a divide and a multiplication operation. These operations are bloody expensive for a system. If possible it's worth avoiding them by using cheaper operations such as a few subtractions, addition or in best case a simple assignment. Furthermore, note that we only have a limited number of input values for the upper operation. In case of the *uchar* system this is 256 to be exact.
Therefore, for larger images it would be wise to calculate all possible values beforehand and during the assignment just make the assignment, by using a lookup table. Lookup tables are simple arrays (having one or more dimensions) that for a given input value variation holds the final output value. Its strength lies that we do not need to make the calculation, we just need to read the result.
Our test case program (and the sample presented here) will do the following: read in a console line argument image (that may be either color or gray scale - console line argument too) and apply the reduction with the given console line argument integer value. In OpenCV, at the moment they are three major ways of going through an image pixel by pixel. To make things a little more interesting will make the scanning for each image using all of these methods, and print out how long it took.
You can download the full source code :download:`here <../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp>` or look it up in the samples directory of OpenCV at the cpp tutorial code for the core section. Its basic usage is:
.. code-block:: bash
how_to_scan_images imageName.jpg intValueToReduce [G]
The final argument is optional. If given the image will be loaded in gray scale format, otherwise the RGB color way is used. The first thing is to calculate the lookup table.
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp
:language: cpp
:tab-width: 4
:lines: 48-60
Here we first use the C++ *stringstream* class to convert the third command line argument from text to an integer format. Then we use a simple look and the upper formula to calculate the lookup table. No OpenCV specific stuff here.
Another issue is how do we measure time? Well OpenCV offers two simple functions to achieve this :UtilitySystemFunctions:`getTickCount() <gettickcount>` and :UtilitySystemFunctions:`getTickFrequency() <gettickfrequency>`. The first returns the number of ticks of your systems CPU from a certain event (like since you booted your system). The second returns how many times your CPU emits a tick during a second. So to measure in seconds the number of time elapsed between two operations is easy as:
.. code-block:: cpp
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
How the image matrix is stored in the memory?
=============================================
As you could already read in my :ref:`matTheBasicImageContainer` tutorial the size of the matrix depends of the color system used. More accurately, it depends from the number of channels used. In case of a gray scale image we have something like:
.. math::
\newcommand{\tabItG}[1] { \textcolor{black}{#1} \cellcolor{gray}}
\begin{tabular} {ccccc}
~ & \multicolumn{1}{c}{Column 0} & \multicolumn{1}{c}{Column 1} & \multicolumn{1}{c}{Column ...} & \multicolumn{1}{c}{Column m}\\
Row 0 & \tabItG{0,0} & \tabItG{0,1} & \tabItG{...} & \tabItG{0, m} \\
Row 1 & \tabItG{1,0} & \tabItG{1,1} & \tabItG{...} & \tabItG{1, m} \\
Row ... & \tabItG{...,0} & \tabItG{...,1} & \tabItG{...} & \tabItG{..., m} \\
Row n & \tabItG{n,0} & \tabItG{n,1} & \tabItG{n,...} & \tabItG{n, m} \\
\end{tabular}
For multichannel images the columns contain as many sub columns as the number of channels. For example in case of an RGB color system:
.. math::
\newcommand{\tabIt}[1] { \textcolor{yellow}{#1} \cellcolor{blue} & \textcolor{black}{#1} \cellcolor{green} & \textcolor{black}{#1} \cellcolor{red}}
\begin{tabular} {ccccccccccccc}
~ & \multicolumn{3}{c}{Column 0} & \multicolumn{3}{c}{Column 1} & \multicolumn{3}{c}{Column ...} & \multicolumn{3}{c}{Column m}\\
Row 0 & \tabIt{0,0} & \tabIt{0,1} & \tabIt{...} & \tabIt{0, m} \\
Row 1 & \tabIt{1,0} & \tabIt{1,1} & \tabIt{...} & \tabIt{1, m} \\
Row ... & \tabIt{...,0} & \tabIt{...,1} & \tabIt{...} & \tabIt{..., m} \\
Row n & \tabIt{n,0} & \tabIt{n,1} & \tabIt{n,...} & \tabIt{n, m} \\
\end{tabular}
Note that the order of the channels is inverse: BGR instead of RGB. Because in many cases the memory is large enough to store the rows in a successive fashion the rows may follow one after another, creating a single long row. Because everything is in a single place following one after another this may help to speed up the scanning process. We can use the :basicstructures:`isContinuous() <mat-iscontinuous>` function to *ask* the matrix if this is the case. Continue on to the next section to find an example.
The efficient way
=================
When it comes to performance you cannot beat the classic C style operator[] (pointer) access. Therefore, the most efficient method we can recommend for making the assignment is:
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp
:language: cpp
:tab-width: 4
:lines: 116-143
Here we basically just acquire a pointer to the start of each row and go through it until it ends. In the special case that the matrix is stored in a continues manner we only need to request the pointer a single time and go all the way to the end. We need to look out for color images: we have three channels so we need to pass through three times more items in each row.
There's another way of this. The *data* data member of a *Mat* object returns the pointer to the first row, first column. If this pointer is null you have no valid input in that object. Checking this is the simplest method to check if your image loading was a success. In case the storage is continues we can use this to go through the whole data pointer. In case of a gray scale image this would look like:
.. code-block:: cpp
uchar* p = I.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
*p++ = table[*p];
You would get the same result. However, this code is a lot harder to read later on. It gets even harder if you have some more advanced technique there. Moreover, in practice I've observed you'll get the same performance result (as most of the modern compilers will probably make this small optimization trick automatically for you).
The iterator (safe) method
==========================
In case of the efficient way making sure that you pass through the right amount of *uchar* fields and to skip the gaps that may occur between the rows was your responsibility. The iterator method is considered a safer way as it takes over these tasks from the user. All you need to do is ask the begin and the end of the image matrix and then just increase the begin iterator until you reach the end. To acquire the value *pointed* by the iterator use the * operator (add it before it).
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp
:language: cpp
:tab-width: 4
:lines: 145-173
In case of color images we have three uchar items per column. This may be considered a short vector of uchar items, that has been baptized in OpenCV with the *Vec3b* name. To access the n-th sub column we use simple operator[] access. It's important to remember that OpenCV iterators go through the columns and automatically skip to the next row. Therefore in case of color images if you use a simple *uchar* iterator you'll be able to access only the blue channel values.
Random Pixel Access
===================
The final method isn't recommended for scanning. It was made to acquire or modify random elements in the image. Its basic usage is to specify the row and column number of the item you want to access. During our earlier scanning methods you could already observe that is important through what type we are looking at the image. It's no different here as you need manually to specify what type to use at the automatic lookup. You can observe this in case of the gray scale images for the following source code (the usage of the + :basicstructures:`at() <mat-at>` function):
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp
:language: cpp
:tab-width: 4
:lines: 175-207
If you need to multiple lookups using this method for an image it may be troublesome and time consuming to enter the type and the at keyword for each of the accesses. To solve this problem OpenCV has a :basicstructures:`Mat_ <id3>` data type. It's the same as Mat with the extra need that at definition you need to specify the data type through what to look at the data matrix, however in return you can use the operator() for fast access of items. To make things even better this is easily convertible from and to the usual :basicstructures:`Mat <id3>` data type. A sample usage of this you can see in case of the color images of the upper function. Nevertheless, it's important to note that the same operation (with the same runtime speed) could have been done with the :basicstructures:`at() <mat-at>` function. It's just a less to write for the lazy programmer trick.
The Core Function
=================
This is a bonus method of achieving lookup table modification in an image. Because in image processing it's quite common that you want to replace all of a given image value to some other value OpenCV has a function that makes the modification without the need from you to write the scanning of the image. We use the :operationsOnArrays:`LUT() <lut>` function of the core module. First we build a Mat type of the lookup table:
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp
:language: cpp
:tab-width: 4
:lines: 98-101
Finally call the function (I is our input image and J the output one):
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/how_to_scan_images/how_to_scan_images.cpp
:language: cpp
:tab-width: 4
:lines: 106
Performance Difference
======================
For the best result compile the program and run it on your own speed. For showing off better the differences I've used a quite large (2560 X 1600) image. The performance presented here are for color images. For a more accurate value I've averaged the value I got from the call of the function for hundred times.
============= ====================
Efficient Way 79.4717 milliseconds
Iterator 83.7201 milliseconds
Random Access 93.7878 milliseconds
LUT function 32.5759 milliseconds
============= ====================
We can conclude a couple of things. If possible, use the already made functions of OpenCV (instead reinventing these). The fastest method turns out to be the LUT function. This is because the OpenCV library is multi-thread enabled via Intel Threaded Building Blocks. However, if you need to write a simple image scan prefer the pointer method. The iterator is a safer bet, however quite slower. Using the random access method for full image scan is the most costly. Your compiler might observe what you want to do and optimize it out somewhat, however in general case this approach is with a little slower than the iterator method.
Finally, you may watch a sample run of the program on the `video posted <https://www.youtube.com/watch?v=fB3AN5fjgwc>`_ on our YouTube channel.
.. raw:: html
<div align="center">
<iframe title="Install OpenCV by using its source files - Part 1" width="560" height="349" src="http://www.youtube.com/embed/fB3AN5fjgwc?rel=0&loop=1" frameborder="0" allowfullscreen align="middle"></iframe>
</div>
.. _matTheBasicImageContainer:
Mat - The Basic Image Container
*******************************
Goal
====
We have multiple ways to acquire digital images from the real world: digital cameras, scanners, computed tomography or magnetic resonance imaging to just name a few. In every case what we (humans) see are images. However, when transforming this to our digital devices what we record are numerical values for each of the points of the image.
.. image:: images/MatBasicImageForComputer.jpg
:alt: A matrix of the mirror of a car
:align: center
For example in the above image you can see that the mirror of the care is nothing more than a matrix containing all the intensity values of the pixel points. Now, how we get and store the pixels values may vary according to what fits best our need, in the end all images inside a computer world may be reduced to numerical matrices and some other information's describing the matric itself. *OpenCV* is a computer vision library whose main focus is to process and manipulate these information to find out further ones. Therefore, the first thing you need to learn and get accommodated with is how OpenCV stores and handles images.
*Mat*
=====
OpenCV has been around ever since 2001. In those days the library was built around a *C* interface. In those days to store the image in the memory they used a C structure entitled *IplImage*. This is the one you'll see in most of the older tutorials and educational materials. The problem with this is that it brings to the table all the minuses of the C language. The biggest issue is the manual management. It builds on the assumption that the user is responsible for taking care of memory allocation and deallocation. While this is no issue in case of smaller programs once your code base start to grove larger and larger it will be more and more a struggle to handle all this rather than focusing on actually solving your development goal.
Luckily C++ came around and introduced the concept of classes making possible to build another road for the user: automatic memory management (more or less). The good news is that C++ if fully compatible with C so no compatibility issues can arise from making the change. Therefore, OpenCV with its 2.0 version introduced a new C++ interface that by taking advantage of these offers a new way of doing things. A way, in which you do not need to fiddle with memory management; making your code concise (less to write, to achieve more). The only main downside of the C++ interface is that many embedded development systems at the moment support only C. Therefore, unless you are targeting this platform, there's no point on using the *old* methods (unless you're a masochist programmer and you're asking for trouble).
The first thing you need to know about *Mat* is that you no longer need to manually allocate its size and release it as soon as you do not need it. While doing this is still a possibility, most of the OpenCV functions will allocate its output data manually. As a nice bonus if you pass on an already existing *Mat* object, what already has allocated the required space for the matrix, this will be reused. In other words we use at all times only as much memory as much we must to perform the task.
*Mat* is basically a class having two data parts: the matrix header (containing information such as the size of the matrix, the method used for storing, at which address is the matrix stored and so on) and a pointer to the matrix containing the pixel values (may take any dimensionality depending on the method chosen for storing) . The matrix header size is constant. However, the size of the matrix itself may vary from image to image and usually is larger by order of magnitudes. Therefore, when you're passing on images in your program and at some point you need to create a copy of the image the big price you will need to build is for the matrix itself rather than its header. OpenCV is an image processing library. It contains a large collection of image processing functions. To solve a computational challenge most of the time you will end up using multiple functions of the library. Due to this passing on images to functions is a common practice. We should not forget that we are talking about image processing algorithms, which tend to be quite computational heavy. The last thing we want to do is to further decrease the speed of your program by making unnecessary copies of potentially *large* images.
To tackle this issue OpenCV uses a reference counting system. The idea is that each *Mat* object has its own header, however the matrix may be shared between two instance of them by having their matrix pointer point to the same address. Moreover, the copy operators **will only copy the headers**, and as also copy the pointer to the large matrix too, however not the matrix itself.
.. code-block:: cpp
:linenos:
Mat A, C; // creates just the header parts
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A); // Use the copy constructor
C = A; // Assignment operator
All the above objects, in the end point to the same single data matrix. Their headers are different, however making any modification using either one of them will affect all the other ones too. In practice the different objects just provide different access method to the same underlying data. Nevertheless, their header parts are different. The real interesting part comes that you can create headers that refer only to a subsection of the full data. For example, to create a region of interest (*ROI*) in an image you just create a new header with the new boundaries:
.. code-block:: cpp
:linenos:
Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range:all(), Range(1,3)); // using row and column boundaries
Now you may ask if the matrix itself may belong to multiple *Mat* objects who will take responsibility for its cleaning when it's no longer needed. The short answer is: the last object that used it. For this a reference counting mechanism is used. Whenever somebody copies a header of a *Mat* object a counter is increased for the matrix. Whenever a header is cleaned this counter is decreased. When the counter reaches zero the matrix too is freed. Because, sometimes you will still want to copy the matrix itself too, there exists the :basicstructures:`clone() <mat-clone>` or the :basicstructures:`copyTo() <mat-copyto>` function.
.. code-block:: cpp
:linenos:
Mat F = A.clone();
Mat G;
A.copyTo(G);
Now modifying *F* or *G* will not affect the matrix pointed by the *Mat* header. What you need to remember from all this is that:
.. container:: enumeratevisibleitemswithsquare
* Output image allocation for OpenCV functions is automatic (unless specified otherwise).
* No need to think about memory freeing with OpenCVs C++ interface.
* The assignment operator and the copy constructor (*ctor*)copies only the header.
* Use the :basicstructures:`clone()<mat-clone>` or the :basicstructures:`copyTo() <mat-copyto>` function to copy the underlying matrix of an image.
*Storing* methods
=================
This is about how you store the pixel values. You can select the color space and the data type used. The color space refers to how we combine color components in order to code a given color. The simplest one is the gray scale. Here the colors at our disposal are black and white. The combination of these allows us to create many shades of gray.
For *colorful* ways we have a lot more of methods to choose from. However, every one of them breaks it down to three or four basic components and the combination of this will give all others. The most popular one of this is RGB, mainly because this is also how our eye builds up colors in our eyes. Its base colors are red, green and blue. To code the transparency of a color sometimes a fourth element: alpha (A) is added.
However, they are many color systems each with their own advantages:
.. container:: enumeratevisibleitemswithsquare
* RGB is the most common as our eyes use something similar, our display systems also compose colors using these.
* The HSV and HLS decompose colors into their hue, saturation and value/luminance components, which is a more natural way for us to describe colors. Using you may for example dismiss the last component, making your algorithm less sensible to light conditions of the input image.
* YCrCb is used by the popular JPEG image format.
* CIE L*a*b* is a perceptually uniform color space, which comes handy if you need to measure the *distance* of a given color to another color.
Now each of the building components has their own valid domains. This leads to the data type used. How we store a component defines just how fine control we have over its domain. The smallest data type possible is *char*, which means one byte or 8 bits. This may be signed (so can store values from 0 to 255) or unsigned (values from -127 to +127). Although in case of three components this already gives 16 million possible colors to represent (like in case of RGB) we may acquire an even finer control by using the float (4 byte = 32 bit) or double (8 byte = 64 bit) data types for each component. Nevertheless, remember that increasing the size of a component also increases the size of the whole picture in the memory.
Creating explicitly a *Mat* object
==================================
In the :ref:`Load_Save_Image` tutorial you could already see how to write a matrix to an image file by using the :readWriteImageVideo:` imwrite() <imwrite>` function. However, for debugging purposes it's much more convenient to see the actual values. You can achieve this via the << operator of *Mat*. However, be aware that this only works for two dimensional matrices.
Although *Mat* is a great class as image container it is also a general matrix class. Therefore, it is possible to create and manipulate multidimensional matrices. You can create a Mat object in multiple ways:
.. container:: enumeratevisibleitemswithsquare
+ :basicstructures:`Mat() <mat-mat>` Constructor
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 27-28
.. image:: images/MatBasicContainerOut1.jpg
:alt: Demo image of the matrix output
:align: center
For two dimensional and multichannel images we first define their size: row and column count wise.
Then we need to specify the data type to use for storing the elements and the number of channels per matrix point. To do this we have multiple definitions made according to the following convention:
.. code-block:: cpp
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
For instance, *CV_8UC3* means we use unsigned char types that are 8 bit long and each pixel has three items of this to form the three channels. This are predefined for up to four channel numbers. The :basicstructures:`Scalar <scalar>` is four element short vector. Specify this and you can initialize all matrix points with a custom value. However if you need more you can create the type with the upper macro and putting the channel number in parenthesis as you can see below.
+ Use C\\C++ arrays and initialize via constructor
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 35-36
The upper example shows how to create a matrix with more than two dimensions. Specify its dimension, then pass a pointer containing the size for each dimension and the rest remains the same.
+ Create a header for an already existing IplImage pointer:
.. code-block:: cpp
IplImage* img = cvLoadImage("greatwave.jpg", 1);
Mat mtx(img); // convert IplImage* -> Mat
+ :basicstructures:`Create() <mat-create>` function:
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 31-32
.. image:: images/MatBasicContainerOut2.jpg
:alt: Demo image of the matrix output
:align: center
You cannot initialize the matrix values with this construction. It will only reallocate its matrix data memory if the new size will not fit into the old one.
+ MATLAB style initializer: :basicstructures:`zeros() <mat-zeros>`, :basicstructures:`ones() <mat-ones>`, ::basicstructures:`eyes() <mat-eye>`. Specify size and data type to use:
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 40-47
.. image:: images/MatBasicContainerOut3.jpg
:alt: Demo image of the matrix output
:align: center
+ For small matrices you may use comma separated initializers:
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 50-51
.. image:: images/MatBasicContainerOut6.jpg
:alt: Demo image of the matrix output
:align: center
+ Create a new header for an existing *Mat* object and :basicstructures:`clone() <mat-clone>` or :basicstructures:`copyTo() <mat-copyto>` it.
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 53-54
.. image:: images/MatBasicContainerOut7.jpg
:alt: Demo image of the matrix output
:align: center
Print out formatting
====================
.. note::
You can fill out a matrix with random values using the :operationsOnArrays:`randu() <randu>` function. You need to give the lover and upper value between what you want the random values:
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 57-58
In the above examples you could see the default formatting option. Nevertheless, OpenCV allows you to format your matrix output format to fit the rules of:
.. container:: enumeratevisibleitemswithsquare
+ Default
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 61
.. image:: images/MatBasicContainerOut8.jpg
:alt: Default Output
:align: center
+ Python
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 62
.. image:: images/MatBasicContainerOut16.jpg
:alt: Default Output
:align: center
+ Comma separated values (CSV)
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 64
.. image:: images/MatBasicContainerOut10.jpg
:alt: Default Output
:align: center
+ Numpy
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 63
.. image:: images/MatBasicContainerOut9.jpg
:alt: Default Output
:align: center
+ C
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 65
.. image:: images/MatBasicContainerOut11.jpg
:alt: Default Output
:align: center
Print for other common items
============================
OpenCV offers support for print of other common OpenCV data structures too via the << operator like:
.. container:: enumeratevisibleitemswithsquare
+ 2D Point
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 67-68
.. image:: images/MatBasicContainerOut12.jpg
:alt: Default Output
:align: center
+ 3D Point
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 70-71
.. image:: images/MatBasicContainerOut13.jpg
:alt: Default Output
:align: center
+ std::vector via cv::Mat
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 74-77
.. image:: images/MatBasicContainerOut14.jpg
:alt: Default Output
:align: center
+ std::vector of points
.. literalinclude:: ../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp
:language: cpp
:tab-width: 4
:lines: 79-83
.. image:: images/MatBasicContainerOut15.jpg
:alt: Default Output
:align: center
Most of the samples here have been included into a small console application. You can download it from :download:`here <../../../../samples/cpp/tutorial_code/core/mat_the_basic_image_container/mat_the_basic_image_container.cpp>` or in the core section of the cpp samples.
A quick video demonstration of this you can find on `YouTube <https://www.youtube.com/watch?v=1tibU7vGWpk>`_.
.. raw:: html
<div align="center">
<iframe title="Install OpenCV by using its source files - Part 1" width="560" height="349" src="http://www.youtube.com/embed/1tibU7vGWpk?rel=0&loop=1" frameborder="0" allowfullscreen align="middle"></iframe>
</div>
.. _maskOperations:
Mask operations on matrixes
***************************
Mask operations on matrixes are quite simple. The idea is that we recalculate each pixels value in an image according to a matrix mask. This mask holds values that will just how much influence have neighbor pixel values (and the pixel value itself) to the new pixel value. From a mathematical point of view we make a weighted average, with our specified values.
Our test case
=============
Let us consider the issue of an image contrast enchancement method. Basically we want to apply for every pixel of the image the following formula:
.. math::
I(i,j) = 5*I(i,j) - [ I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]
\iff I(i,j)*M, \text{where }
M = \bordermatrix{ _i\backslash ^j & -1 & 0 & -1 \cr
-1 & 0 & -1 & 0 \cr
0 & -1 & 5 & -1 \cr
+1 & 0 & -1 & 0 \cr
}
The first notation is by using a formula, while the second is a compacted version of the first by using a mask. You use the mask by puting the center of the mask matrix (in the upper case noted by the zero-zero index) on the pixel you want to calculate and sum up the pixel values multiplicated with the overlapped matrix values. It's the same thing, however in case of large matrices the later notation is a lot easier to look over.
......@@ -229,56 +229,56 @@ As you just saw in the Code section, the program will sequentially execute diver
#. First a random set of *NUMBER* lines will appear on screen such as it can be seen in this screenshot:
.. image:: images/Drawing_2_Tutorial_Result_0.png
.. image:: images/Drawing_2_Tutorial_Result_0.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 0
:align: center
#. Then, a new set of figures, these time *rectangles* will follow:
.. image:: images/Drawing_2_Tutorial_Result_1.png
.. image:: images/Drawing_2_Tutorial_Result_1.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 1
:align: center
#. Now some ellipses will appear, each of them with random position, size, thickness and arc length:
.. image:: images/Drawing_2_Tutorial_Result_2.png
.. image:: images/Drawing_2_Tutorial_Result_2.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 2
:align: center
#. Now, *polylines* with 03 segments will appear on screen, again in random configurations.
.. image:: images/Drawing_2_Tutorial_Result_3.png
.. image:: images/Drawing_2_Tutorial_Result_3.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 3
:align: center
#. Filled polygons (in this example triangles) will follow:
.. image:: images/Drawing_2_Tutorial_Result_4.png
.. image:: images/Drawing_2_Tutorial_Result_4.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 4
:align: center
#. The last geometric figure to appear: circles!
.. image:: images/Drawing_2_Tutorial_Result_5.png
.. image:: images/Drawing_2_Tutorial_Result_5.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 5
:align: center
#. Near the end, the text *"Testing Text Rendering"* will appear in a variety of fonts, sizes, colors and positions.
.. image:: images/Drawing_2_Tutorial_Result_6.png
.. image:: images/Drawing_2_Tutorial_Result_6.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 6
:align: center
#. And the big end (which by the way expresses a big truth too):
.. image:: images/Drawing_2_Tutorial_Result_7.png
.. image:: images/Drawing_2_Tutorial_Result_7.jpg
:height: 300px
:alt: Drawing Tutorial 2 - Final Result 7
:align: center
......
......@@ -7,6 +7,45 @@ Here you will learn the about the basic building blocks of the library. A must r
.. include:: ../../definitions/tocDefinitions.rst
+
.. tabularcolumns:: m{100pt} m{300pt}
.. cssclass:: toctableopencv
=============== ======================================================
|MatBasicIma| **Title:** :ref:`matTheBasicImageContainer`
*Compatibility:* > OpenCV 2.0
*Author:* |Author_BernatG|
You will learn how to store images in the memory and how to print out their content to the console.
=============== ======================================================
.. |MatBasicIma| image:: images/matTheBasicImageStructure.jpg
:height: 90pt
:width: 90pt
+
.. tabularcolumns:: m{100pt} m{300pt}
.. cssclass:: toctableopencv
=============== ======================================================
|HowScanImag| **Title:** :ref:`howToScanImagesOpenCV`
*Compatibility:* > OpenCV 2.0
*Author:* |Author_BernatG|
You'll find out how to scan images (go through each of the image pixels) with OpenCV. Bonus: time measurement with OpenCV.
=============== ======================================================
.. |HowScanImag| image:: images/howToScanImages.jpg
:height: 90pt
:width: 90pt
+
.. tabularcolumns:: m{100pt} m{300pt}
.. cssclass:: toctableopencv
......@@ -22,7 +61,7 @@ Here you will learn the about the basic building blocks of the library. A must r
=============== ======================================================
.. |Beginners_4| image:: images/Adding_Images_Tutorial_Result_0.png
.. |Beginners_4| image:: images/Adding_Images_Tutorial_Result_0.jpg
:height: 90pt
:width: 90pt
......@@ -41,7 +80,7 @@ Here you will learn the about the basic building blocks of the library. A must r
=============== ====================================================
.. |Bas_Lin_Tran| image:: images/Basic_Linear_Transform_Tutorial_Result_0.png
.. |Bas_Lin_Tran| image:: images/Basic_Linear_Transform_Tutorial_Result_0.jpg
:height: 90pt
:width: 90pt
......@@ -62,7 +101,7 @@ Here you will learn the about the basic building blocks of the library. A must r
=============== ======================================================
.. |Beginners_6| image:: images/Drawing_1_Tutorial_Result_0.png
.. |Beginners_6| image:: images/Drawing_1_Tutorial_Result_0.jpg
:height: 90pt
:width: 90pt
......@@ -81,14 +120,21 @@ Here you will learn the about the basic building blocks of the library. A must r
=============== ======================================================
.. |Beginners_7| image:: images/Drawing_2_Tutorial_Result_7.png
.. |Beginners_7| image:: images/Drawing_2_Tutorial_Result_7.jpg
:height: 90pt
:width: 90pt
.. raw:: latex
\pagebreak
.. toctree::
:hidden:
../mat - the basic image container/mat - the basic image container
../how_to_scan_images/how_to_scan_images
../adding_images/adding_images
../basic_linear_transform/basic_linear_transform
../basic_geometric_drawing/basic_geometric_drawing
../random_generator_and_text/random_generator_and_text
\ No newline at end of file
../random_generator_and_text/random_generator_and_text
../mat-mask-operations/mat-mask-operations.rst
\ No newline at end of file
......@@ -86,6 +86,10 @@ Learn about how to use the feature points detectors, descriptors and matching f
:height: 90pt
:width: 90pt
.. raw:: latex
\pagebreak
.. toctree::
:hidden:
......
......@@ -6,3 +6,8 @@ General tutorials
These tutorials are the bottom of the iceberg as they link together multiple of the modules presented above in order to solve complex problems.
.. include:: ../../definitions/noContent.rst
.. raw:: latex
\pagebreak
......@@ -6,3 +6,7 @@
Squeeze out every little computation power from your system by using the power of your video card to run the OpenCV algorithms.
.. include:: ../../definitions/noContent.rst
.. raw:: latex
\pagebreak
......@@ -5,11 +5,6 @@
This section contains valuable tutorials about how to read/save your image/video files and how to use the built-in graphical user interface of the library.
.. toctree::
:hidden:
../trackbar/trackbar
* :ref:`Adding_Trackbars`
=============== ======================================================
......@@ -21,6 +16,15 @@ This section contains valuable tutorials about how to read/save your image/video
=============== ======================================================
.. |Beginners_5| image:: images/Adding_Trackbars_Tutorial_Cover.png
.. |Beginners_5| image:: images/Adding_Trackbars_Tutorial_Cover.jpg
:height: 100pt
:width: 100pt
.. raw:: latex
\pagebreak
.. toctree::
:hidden:
../trackbar/trackbar
......@@ -7,7 +7,7 @@ Adding a Trackbar to our applications!
* Well, it is time to use some fancy GUI tools. OpenCV provides some GUI utilities (*highgui.h*) for you. An example of this is a **Trackbar**
.. image:: images/Adding_Trackbars_Tutorial_Trackbar.png
.. image:: images/Adding_Trackbars_Tutorial_Trackbar.jpg
:alt: Trackbar example
:align: center
......@@ -145,13 +145,13 @@ Result
* Our program produces the following output:
.. image:: images/Adding_Trackbars_Tutorial_Result_0.png
.. image:: images/Adding_Trackbars_Tutorial_Result_0.jpg
:alt: Adding Trackbars - Windows Linux
:align: center
* As a manner of practice, you can also add 02 trackbars for the program made in :ref:`Basic_Linear_Transform`. One trackbar to set :math:`\alpha` and another for :math:`\beta`. The output might look like:
.. image:: images/Adding_Trackbars_Tutorial_Result_1.png
.. image:: images/Adding_Trackbars_Tutorial_Result_1.jpg
:alt: Adding Trackbars - Lena
:height: 500px
:align: center
......
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异由.gitattributes 抑制。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册