Introduction to Android image processing (on)-image color transformation

I will divide this series into two parts to explain the introductory knowledge of image color transformation and image shape transformation in Android.

RGBA model

RGBA is an abbreviation, and its meanings are Red, Green, Blue, Aplha. That is, the three primary colors of red, green, and blue and the four options of transparency. The various colors we usually see are just the three primary colors mixed in different proportions, and alpha can control the transparency of the picture.

Here are some three important concepts in image processing:

  • Hue/hue: the color transmitted by the object
  • Saturation: the purity of the color, from 0% (gray) to 100% (saturated) to describe
  • Brightness/Lightness: The relative lightness and darkness of the color

In Android, the system provides the class ColorMatrix to help adjust the three important attributes of the image

tone

Set the hue of the entire image through the setRotate method. The first is the color that needs to be set, where 0 corresponds to R, 1 corresponds to G, and 2 corresponds to B. Here for simplicity, set the same value hue to it.

ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0,hue);     //R
hueMatrix.setRotate(1,hue);     //G
hueMatrix.setRotate(2,hue);     //B

saturation

Set the saturation of the image through the setSaturation method. By setting the value, you can control the saturation of the image.

ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);

brightness

Set the brightness of the image through the setScale method. The four parameters are R brightness, G brightness, B brightness and transparency brightness.

ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum,lum,lum,1);

Adjust the image effect through the ColorMatrix method

Next, create an Android project to start learning to use image processing.

Main interface

First, create a main menu to select the modification to the picture.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android = "http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="40dp"
        android:textSize="40sp"
        android:text="美图秀秀"/>

    <Button
        android:id="@+id/btn_primary_color"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="PRIMARY COLOR"/>

    <Button
        android:id="@+id/btn_color_matrix"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="COLOR MATRIX"/>

    <Button
        android:id="@+id/btn_3"
           android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="等待实现"/>
</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private Button primaryButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        primaryButton = (Button)findViewById(R.id.btn_primary_color);
        primaryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                primaryColorEvent();
            }
        });
    }

    public void primaryColorEvent(){
        Intent intent = new Intent(MainActivity.this,PrimaryColorActivity.class);
        startActivity(intent);
    }
}

Three primary color adjustment interface

You can roughly analyze it. If you want to adjust a picture, you have to go through these three steps:

  1. Get a picture
  2. Make a series of modifications to the picture
  3. Return a picture

Therefore, you might as well design a tool class to perform image-related processing, which can improve the reusability of the code.

public class ImageUtils {
    public static Bitmap handleImageEffect(Bitmap bitmap,float hue,float saturation,float lum){
        //由于不能直接在原图上修改,所以创建一个图片,设定宽度高度与原图相同。为32位ARGB图片
        Bitmap currentBitmap = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        //创建一个和原图相同大小的画布
        Canvas canvas = new Canvas(currentBitmap);
        //创建笔刷并设置抗锯齿
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //色相ColorMatrix
        ColorMatrix hueMatrix = new ColorMatrix();
        hueMatrix.setRotate(0,hue);
        hueMatrix.setRotate(1,hue);
        hueMatrix.setRotate(2,hue);
        //饱和度ColorMatrix
        ColorMatrix saturationMatrix = new ColorMatrix();
        saturationMatrix.setSaturation(saturation);
        //亮度ColorMatrix
        ColorMatrix lumMatrix = new ColorMatrix();
        lumMatrix.setScale(lum,lum,lum,1);
        //将三种效果融合起来
        ColorMatrix imageMatrix = new ColorMatrix();
        imageMatrix.postConcat(hueMatrix);
        imageMatrix.postConcat(saturationMatrix);
        imageMatrix.postConcat(lumMatrix);

        paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
        canvas.drawBitmap(bitmap,0,0,paint);

        return currentBitmap;
    }
}

Here, the Canvas class is used. The canvas class is a canvas class, and subsequent operations will be performed on the canvas instead of the original image. Established three ColorMatrix, carried out different aspects of operation, and used the postConcat method to merge these Matrix. Modify the relevant properties of paint through the setColorFilter method, and then draw the picture on the canvas. Finally, return the modified picture.

Back to our interface. The design uses three Seekbars to control the three attributes of hue, saturation and brightness respectively.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/image_view"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:scaleType="centerCrop"
        android:layout_marginTop="24dp"
        android:layout_marginBottom="24dp"
        android:layout_centerHorizontal="true"/>

    <SeekBar
        android:id="@+id/seekbar_hue"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:layout_below="@id/image_view"/>

    <SeekBar
        android:id="@+id/seekbar_saturation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
          android:layout_margin="10dp"
        android:layout_below="@id/seekbar_hue"/>

    <SeekBar
        android:id="@+id/seekbar_lum"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
           android:layout_below="@id/seekbar_saturation"/>
</RelativeLayout>

Then edit PrimaryColorActivity. Here we define a maximum value and an intermediate value, which can be changed from the intermediate value, and then obtain the hue, saturation, and brightness values ​​through a series of methods.

public class PrimaryColorActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener{
    private ImageView mImageView;
    private SeekBar mSeekbarHue,mSeekbarSaturation,mSeekbarLum;
    private final static int MAX_VALUE = 255;   //最大值
    private final static int MID_VALUE = 127;   //中间值

    private float mHue,mSaturation,mLum;

    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_primary_color);

        //获取图片
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);

        mImageView = (ImageView)findViewById(R.id.image_view);

        mImageView.setImageBitmap(bitmap);

        mSeekbarHue = (SeekBar)findViewById(R.id.seekbar_hue);
        mSeekbarSaturation = (SeekBar)findViewById(R.id.seekbar_saturation);
        mSeekbarLum = (SeekBar)findViewById(R.id.seekbar_lum);

        mSeekbarHue.setOnSeekBarChangeListener(this);
        mSeekbarSaturation.setOnSeekBarChangeListener(this);
        mSeekbarLum.setOnSeekBarChangeListener(this);

        mSeekbarHue.setMax(MAX_VALUE);
        mSeekbarSaturation.setMax(MAX_VALUE);
        mSeekbarLum.setMax(MAX_VALUE);

        mSeekbarHue.setProgress(MID_VALUE);
        mSeekbarSaturation.setProgress(MID_VALUE);
        mSeekbarLum.setProgress(MID_VALUE);

    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        switch (seekBar.getId()){
            case R.id.seekbar_hue:
                //将0-255的值转换为色调值
                mHue = (progress - MID_VALUE)*1.0F/MID_VALUE*180;
                break;
            case R.id.seekbar_saturation:
                //将0-255值转换为饱和度值
                mSaturation = progress*1.0F/MID_VALUE;
                break;
            case R.id.seekbar_lum:
                //将0-255的值转换为亮度值
                mLum = progress*1.0F/MID_VALUE;
                break;
        }
        mImageView.setImageBitmap(ImageUtils.handleImageEffect(bitmap,
                mHue,mSaturation,mLum));
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
    }
}

After running, drag the Seekbar, you can find that we have successfully changed the attributes of the picture


Principle analysis-matrix transformation

We used the ColorMatrix class before. As we all know, Matrix means a matrix, so here we are actually processing images by manipulating the matrix. Android provides a color matrix as shown in the figure to help us process image effects. Each point in the image is a matrix component, which is composed of R, G, B, A, and 1:

We multiply this color matrix by the color matrix component corresponding to the pixel to get a new matrix R1G1B1A1. As shown in the figure:

In this way, we transform a pixel into a new pixel through the color matrix (the effect of color adjustment).

We call the matrix shown in the figure the initialization matrix, because it remains unchanged after multiplying the original pixels.

Let’s take a look at this matrix. Based on the original matrix, the 0 in the two places is changed to 100. The result is that the original RG value has changed to their plus 100 value, which is the RG of each pixel. The values ​​are all increased by 100:

Similarly, we can take a look at such a matrix. Based on the original initialization matrix, a 1 in the matrix G is changed to 2. After bringing it in, we can find that G has doubled the original value. The effect is The Green of the whole image is doubled

We can find that the four rows of the color matrix respectively control the four attributes of RGBA of the pixel, and the fifth column of the color matrix, we call it the color offset, it does not directly change the coefficient of a certain color, but Adjust the entire color based on the original.

We need to change a color, not only can change the offset, but also change the color coefficient.

Use matrix transformation to adjust image effects

Based on the original project, we add an Activity and let the second button of MainActivity jump to this Activity.

The first is the layout, we make the following layout, we are going to add 20 EditText to represent the matrix in the GridLayout, and use the button to apply the matrix.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"/>

    <GridLayout
        android:id="@+id/group"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3"
           android:rowCount="4"
        android:columnCount="5"
    </GridLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_change"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="改变"/>

        <Button
            android:id="@+id/btn_reset"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="重置"/>

    </LinearLayout>
</LinearLayout>

<

Then, we modify the code of Activity to dynamically add EditText to GridText to map our ColorMatrix. Change our ColorMatrix by changing EditText to change the picture effect.

public class ColorMatrixActivity extends AppCompatActivity {
    private ImageView mImageView;
    private GridLayout mGroup;
    private Bitmap bitmap;
    private int mEtWidth,mEtHeight;
    private EditText[] mEditTexts = new EditText[20];
    private float[] mColorMatrix = new float[20];   //对应矩阵
    private Button changeButton;
    private Button resetButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_color_matrix);

        bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test1);
        mImageView = (ImageView) findViewById(R.id.image_view);
        mImageView.setImageBitmap(bitmap);

        mGroup = (GridLayout) findViewById(R.id.group);
        //动态创建EditText,填充GridLayout

        //由于在onCreate中,mGroup还没有创建完成,无法获取宽高
        //所以通过post方法,在控件绘制完毕后,执行Runnable的具体方法
        mGroup.post(new Runnable() {
            @Override
            public void run() {
                mEtWidth = mGroup.getWidth()/5;
                mEtHeight = mGroup.getHeight()/4;
                addEditText();
                initMatrix();
            }
        });

        changeButton = (Button)findViewById(R.id.btn_change);
        changeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                changeButtonEvent();
            }
        });

        resetButton = (Button)findViewById(R.id.btn_reset);
        resetButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                resetButtonEvent();
            }
        });
    }

    private void addEditText(){
        for (int i=0;i&lt;20;i++){
            EditText editText = new EditText(ColorMatrixActivity.this);
            mEditTexts[i] = editText;
            mGroup.addView(editText,mEtWidth,mEtHeight);
        }
    }

    private void initMatrix(){
        for (int i=0;i&lt;20;i++){
            if (i%6 == 0){
                //i为第0、6、12、18位时
                mColorMatrix[i]=1;
                mEditTexts[i].setText(String.valueOf(1));
            }else{
                mColorMatrix[i]=0;
                mEditTexts[i].setText(String.valueOf(0));
            }
        }
    }

    private void getMatrix(){
        for (int i=0;i&lt;20;i++){
            mColorMatrix[i] = Float.valueOf(mEditTexts[i].getText().toString());
        }
    }

    private void setImageMatrix(){
        Bitmap currentBitmap = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.set(mColorMatrix);
        Canvas canvas = new Canvas(currentBitmap);
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
        canvas.drawBitmap(bitmap,0,0,paint);
        mImageView.setImageBitmap(currentBitmap);
    }

    public void changeButtonEvent(){
        getMatrix();
        setImageMatrix();
    }

    public void resetButtonEvent(){
        initMatrix();
        setImageMatrix();
    }
}

As you can see, the picture effect has been successfully changed, as shown in the figure

After learning this, we can answer why we used such parameters to change the hue, saturation and brightness. Taking brightness as an example, let's take a look at the source code of the setScale method. It can be found that ColorMatrix also uses such a color array internally. At the same time, the multiple of 6 is set to the corresponding value to change the brightness. It shows that if we want to change the brightness in the color matrix, we only need to change the brightness of each color. The value can be increased at the same time.

  /**
    * Set this colormatrix to scale by the specified values.
    */
    public void setScale(float rScale, float gScale, float bScale,float aScale) {
        final float[] a = mArray;
           for (int i = 19; i &gt; 0; --i) {
            a[i] = 0;
        }
        a[0] = rScale;
        a[6] = gScale;
        a[12] = bScale;
        a[18] = aScale;
    }

Through these studies, we can conclude as follows:

Image processing, in fact, is to study the processing effects of different color matrices on images

For example, we can get the nostalgic effect common in some image processing apps by the setting method in the figure below


Image processing through pixels

After the image is enlarged, it will present a dot matrix, and each dot is actually a pixel. Through the color ratio of RGB, different colors can be displayed.

The following are some examples of pixel processing to form image special effects:

Negative effect

For the three pixels of ABC, the algorithm for finding the film effect of point B is as follows. In fact, the inverse color is calculated for each coordinate point, and you can get

B.r = 255 - B.r;
B.g = 255 - B.g;
B.b = 255 - B.b;

Old photo effect

The algorithm for finding the effect of the old photo on the pixel is as follows, where pixR is the R value of the current pixel, and so on.

newR = (int)(0.393 * pixR + 0.769 * pixG + 0.189 * pixB);
newG = (int)(0.349 * pixR + 0.686 * pixG + 0.168 * pixB);
newB = (int)(0.272 * pixR + 0.534 * pixG + 0.131 * pixB);

Relief effect

For the three points ABC, the algorithm for finding the relief effect of point B is as follows.

B.r = C.r - B.r + 127;
B.g = C.g - B.g + 127;
B.b = C.b - B.b + 127;

Next, we will change the display effect of the image by modifying the pixels.

Create a new Activity first, and add a method to jump to it in the third button of MainActivity.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">        
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <ImageView
            android:id="@+id/image_view1"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

        <ImageView
            android:id="@+id/image_view2"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">
        <ImageView
            android:id="@+id/image_view3"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

        <ImageView
            android:id="@+id/image_view4"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>
    </LinearLayout>
</LinearLayout>

Add the corresponding special effects method

We have added several new methods to ImageUtils to do different processing

Inverted color effect

The code is as follows, we create a new array of pixels corresponding to the picture, and then get all the pixels through the getPixels method.
The second parameter of getPixels is the offset that represents the starting point, and the third parameter is to control the line spacing when reading the array. Generally, width is used. The latter two parameters represent the coordinates of the pixel point read for the first time, and the second to last The parameter represents the width we read from the bitmap, and the last one is the height read.
Then we obtain the four values ​​of rgba through the red green blue alpha method of the Color class for each pixel, and change its rgb value through the algorithm, and convert it into a new pixel array through the argb method of Color. It should be noted that when changing its rgb value, you need to judge whether it exceeds the limit of 0-255, and if some, assign it to 255 or 0 .

    Bitmap currentBitmap = Bitmap.createBitmap(width,height,
            Bitmap.Config.ARGB_8888);
        int[] oldPx = new int[width*height];    //存储像素点数组
        int[] newPx = new int[width*height];
        bitmap.getPixels(oldPx,0,width,0,0,width,height);
        for (int i=0;i&lt;width*height;i++){
            color = oldPx[i];
            r = Color.red(color);
            g = Color.green(color);
            b = Color.blue(color);
            a = Color.alpha(color);
            //通过算法计算新的rgb值
            r = 255 - r;
            g = 255 - g;
            b = 255 - b;

            if (r &gt; 255) r=255;
            else if (r &lt; 0) r=0;

            if (g &gt; 255) g=255;
            else if (g &lt; 0) g=0;

            if (b &gt; 255) b=255;
            else if (b &lt; 0) b=0;

            newPx[i] = Color.argb(a,r,g,b);
        }
        currentBitmap.setPixels(newPx,0,width,0,0,width,height);
        return currentBitmap;
    }

Old photo effect

Other codes are basically the same as before, except that the algorithm is slightly changed and cannot be changed on the basis of the original rgb:

    public static Bitmap handleImageOldpicture(Bitmap bitmap){
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        int color;
        int r,g,b,a;

        Bitmap currentBitmap = Bitmap.createBitmap(width,height,
                Bitmap.Config.ARGB_8888);
        int[] oldPx = new int[width*height];    //存储像素点数组
        int[] newPx = new int[width*height];
        bitmap.getPixels(oldPx,0,width,0,0,width,height);
        for (int i=0;i&lt;width*height;i++){
            color = oldPx[i];
            r = Color.red(color);
            g = Color.green(color);
            b = Color.blue(color);
            a = Color.alpha(color);

            int r1,g1,b1;
            r1 = (int)(0.393 * r + 0.769 * g + 0.189 * b);
            g1 = (int)(0.349 * r + 0.686 * g + 0.168 * b);
            b1 = (int)(0.272 * r + 0.534 * g + 0.131 * b);

            if (r1 &gt; 255) r1=255;
            else if (r1 &lt; 0) r1=0;

            if (g1 &gt; 255) g1=255;
            else if (g1 &lt; 0) g1=0;

            if (b1 &gt; 255) b1=255;
            else if (b1 &lt; 0) b1=0;

            newPx[i] = Color.argb(a,r1,g1,b1);
        }
        currentBitmap.setPixels(newPx,0,width,0,0,width,height);

        return currentBitmap;
    }

Relief effect

Similar to the previous one, the only thing to note is that we need to use the color of the previous pixel, so we need to cycle from 1, and then use the corresponding algorithm to get the picture

    public static Bitmap handleImagePixelsRelief(Bitmap bitmap){
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        int color,colorBefore;
        int r,g,b,a;
        int r1,g1,b1;

        Bitmap currentBitmap = Bitmap.createBitmap(width,height,
                Bitmap.Config.ARGB_8888);

        int[] oldPx = new int[width*height];    //存储像素点数组
        int[] newPx = new int[width*height];
        bitmap.getPixels(oldPx,0,width,0,0,width,height);
        for (int i=1;i&lt;width*height;i++){
            //取出前一个点的颜色
            colorBefore = oldPx[i-1];
            r = Color.red(colorBefore);
            g = Color.green(colorBefore);
            b = Color.blue(colorBefore);
            a = Color.alpha(colorBefore);

            color = oldPx[i];
            r1 = Color.red(color);
            g1 = Color.green(color);
            b1 = Color.blue(color);

            r = (r - r1 + 127);
            g = (g - g1 + 127);
            b = (b - b1 + 127);

            if (r1 &gt; 255) r1=255;

            if (g1 &gt; 255) g1=255;

            if (b1 &gt; 255) b1=255;

            newPx[i] = Color.argb(a,r,g,b);
        }
        currentBitmap.setPixels(newPx,0,width,0,0,width,height);

        return currentBitmap;
    }

View effect

We call the corresponding method in Activity to see the effect.

public class PixelEffectActivity extends AppCompatActivity {
    private ImageView imageView1,imageView2,imageView3,imageView4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pixel_effect);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
        imageView1 = (ImageView) findViewById(R.id.image_view1);
        imageView2 = (ImageView) findViewById(R.id.image_view2);
        imageView3 = (ImageView) findViewById(R.id.image_view3);
        imageView4 = (ImageView) findViewById(R.id.image_view4);

        imageView1.setImageBitmap(bitmap);
        imageView2.setImageBitmap(ImageUtils.handleImageNegative(bitmap));
        imageView3.setImageBitmap(ImageUtils.handleImageOldpicture(bitmap));
        imageView4.setImageBitmap(ImageUtils.handleImagePixelsRelief(bitmap));
    }
}

The effect is shown in the figure


Reference material
Android image processing-create Meitu Xiuxiu from it

This article  has been included in the open source project: https://github.com/Android-Alvin/Android-LearningNotes , which contains self-learning programming routes in different directions, interview question collection/face sutras, and a series of technical articles, etc. The resources are continuously being updated …

Guess you like

Origin blog.csdn.net/weixin_43901866/article/details/114119816