JAVA 后端生成热力图图片返回

基于定位的热力图展示,后端实现

  • 首先将实际坐标转换成像素坐标
  • 操作图片像素 不断的增加像素RGB值
  • 扩大像素点范围,根据聚集程度设置色阶,最后高斯模糊形成热力图
  • 这里是根据实际场景自动生成对应等比例大小的透明贴图,当然也可以直接在指定的图片上画热力图

1.坐标转换以前有发过一个工具类任意坐标系转换可以去看看

这里取的4个点分别为图片左下角,图片右上角 对应的实际位置坐标左下角和右上角

使用工具类获取到偏移量,旋转值等基本参数

  double rotation = Math.toRadians(Math.abs(coordinateUtil.getAngle(imgPoint1, imgPoint2) - coordinateUtil.getAngle(mapPoint1, mapPoint2)));
        double scale = coordinateUtil.getScale(mapPoint1, imgPoint1, mapPoint2, imgPoint2);
        double tx = coordinateUtil.getXTranslation(mapPoint1, imgPoint1, rotation, scale);
        double ty = coordinateUtil.getYTranslation(mapPoint1, imgPoint1, rotation, scale);

将实际点转换成像素点

Point boePoint = coordinateUtil.transformBoePoint(new Point(o.getX(), o.getZ(), 0.0), rotation, scale, tx, ty);
//去吹掉超出图片范围的点
 //若转换的坐标超出了图片范围舍弃
            Double x = boePoint.getX();
            Double y = boePoint.getY();
            if (!(x < 0 || y < 0 || x > imageWidth || y > imageHeight)){
                heatMapData.setX(x)
                        .setY(y);
                heatMapDataList.add(heatMapData);
            }

渲染图片 这里说明下,因为像素点的坐标系原点是左上角的点,与我们最开始设置的对应坐标系不同,所有转换出来的像素点的y值需要用图片高度减去他剩下的就是实际的像素点,因为像素点很小,在设置时count我是默认将该点周围8个点作为一个大的像素点,这样在设置颜色时就会很明显,这个参数根据需求设置

 /**
     * 渲染图片
     * @param image 图片流
     * @param x 操作的像素点x
     * @param y 操作的像素点y
     * @param count 渲染的像素点
     */
    public static void renderPictures(BufferedImage image, Double x, Double y, int count){
        //获取画笔对象
        int xValue = x.intValue();
        int yValue = y.intValue();
        int pixel = image.getRGB(xValue, yValue);
        //从pixel中获取rgba的值
        int b = (0xff & pixel);
        int g = (0xff & (pixel >> 8));
        int r = (0xff & (pixel >> 16));
        //α通道值
        int alpha = (0xff & (pixel >> 24));
        //颜色解析
        if (r == 0){
            if (g < 255){
                g = 255;
            }
        }
        int rgb = b + (g << 8) + (r << 16) + (255 << 24);
        int vy = image.getHeight() - yValue;
//        image.setRGB(xValue, vy, rgb);
//        image.setRGB(xValue, vy, rgb);
//        image.setRGB(xValue, vy, rgb);
//        image.setRGB(xValue, vy, rgb);
        for (int i = xValue - count; i< xValue + count; i++) {
            for (int j = vy - count; j< vy + count; j++) {
                if (i >= 0 && i < image.getWidth()) {
                    if (j >=0 && j < image.getHeight() ) {
                        image.setRGB(i, j, rgb);
                    }
                }
            }
        }
    }

最后高斯模糊刚才处理后的图片,根据α通道设置色阶,实现多个目标聚集时颜色加深(这里色阶可根据实际情况调整)

/**
     * 高斯模糊 设置色阶
     * @param blur
     * @param outPat
     */
    public static void gaussFuzzy(BufferedImage blur, String outPat) {
        try {
            blur = HeatMapUtil.blur(blur, 25);
            int width = blur.getWidth();
            int height = blur.getHeight();
            //获取像素点
            for (int i = 0; i< width; i++) {
                for (int j = 0; j < height; j++) {
                    int pixel = blur.getRGB(i, j);
                    //从pixel中获取rgba的值
                    int a = (pixel >> 24) & 0xff;
                    int g = (pixel >> 8) & 0xff;
                    if (g == 255) {
                        //颜色分级
                        if (a > 0 && a <= 25){
                            blur.setRGB(i, j, new Color(0, 0, 255, 15).getRGB());
                        }
                        if (a > 25 && a <= 50){
                            blur.setRGB(i, j, new Color(0, 255, 0, 160).getRGB());
                        }
                        if (a > 50 && a <= 100){
                            blur.setRGB(i, j, new Color(255, 255, 0, 185).getRGB());
                        }
                        if (a > 100 && a <= 125){
                            blur.setRGB(i, j, new Color(255, 213, 0, 200).getRGB());
                        }
                        if (a > 125 && a <= 150){
                            blur.setRGB(i, j, new Color(255, 171, 0, 215).getRGB());
                        }
                        if (a > 125 && a <= 150){
                            blur.setRGB(i, j, new Color(255, 129, 0, 225).getRGB());
                        }
                        if (a > 150 && a <= 175){
                            blur.setRGB(i, j, new Color(255, 87, 0, 235).getRGB());
                        }
                        if (a > 175 && a <= 200){
                            blur.setRGB(i, j, new Color(255, 42, 0, 245).getRGB());
                        }
                        if (a > 200){
                            blur.setRGB(i, j, new Color(255, 0, 0, 255).getRGB());
                        }
                    }
                }
            }
            blur = HeatMapUtil.blur(blur, 10);
            //输出
            ImageIO.write(blur,"png",new File(outPat));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
   /**
     * 高斯模糊
     * @param source 数据源
     * @param radius 模糊半径
     * @return
     */
    public static BufferedImage blur(BufferedImage source, int radius) {
        BufferedImage img = new BufferedImage(source.getWidth() + radius
                * 2, source.getHeight() + radius * 2,
                BufferedImage.TRANSLUCENT);
        Graphics2D g2 = (Graphics2D) img.getGraphics();
        g2.setColor(new Color(0,0,0,0));
        g2.fillRect(0, 0, source.getWidth() + radius * 2,
                source.getHeight() + radius * 2);
        g2.drawImage(source, radius, radius, null);
        g2.dispose();
        int square = radius * radius;
        float sum = 0;
        float[] matrix = new float[square];
        for (int i = 0; i < square; i++) {
            int dx = i % radius - radius / 2;
            int dy = i / radius - radius / 2;
            matrix[i] = (float) (radius - Math.sqrt(dx * dx + dy * dy));
            sum += matrix[i];
        }
        for (int i = 0; i < square; i++) {
            matrix[i] /= sum;
        }
        BufferedImageOp op = new ConvolveOp(new Kernel(radius, radius,
                matrix), ConvolveOp.EDGE_ZERO_FILL, null);
        BufferedImage res = op.filter(img, null);
        BufferedImage out = new BufferedImage(source.getWidth(),
                source.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
        Graphics2D g3 = (Graphics2D) out.getGraphics();
        g3.drawImage(res, -radius, -radius, null);
        g3.dispose();
        return out;
    }

使用方法:

获取到你要渲染的点坐标集合,通过上述坐标转换获取到的实际图片点

遍历调用renderPictures将图片流渲染

将渲染后的图片流

gaussFuzzy(BufferedImage, 输出地址)

获取到二次处理后的图片,此时获取到的就是热力图红点图

注:这里使用我的源坐标点z值永远为0,所以坐标转换写死了,若有实际xyz则正常传入转换封装数据

/**
     * 热力图
     * @param imageWidth 图片宽度
     * @param imageHeight 图片高度
     * @param max 源坐标系对应右上角坐标x,y,z
     * @param min 源坐标系对应左下角坐标x,y,z
     * @param locationList 源数据坐标点集合
     * @param outPath 输出成品图片路径
     * @return
     */
    public String getHeatMapRealTime(Double imageWidth, Double imageHeight, String max, String min, List<PositionTagLocation> locationList, String outPath) {
        //渲染图片
        CoordinateSystemVo point = getPoint(min, max, imageWidth, imageHeight);
        //生成透明图片
        BufferedImage img = HeatMapUtil.createTransparentImg(imageWidth.intValue(), imageHeight.intValue());
        if (locationList.size() > 0){
            List<HeatMapData> heatMapData = getHeatMapData(locationList, imageWidth, imageHeight, point);
            //渲染图片
            heatMapData.forEach(x-> HeatMapUtil.renderPictures(img, x.getX(), x.getY(), 8));
            //上色,高斯模糊
            //二次处理图片导出
            HeatMapUtil.gaussFuzzy(img, outPath);
        }else {
            try {
                ImageIO.write(img,"png", new File(outPath));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return  outPath;
    }



    /**
     * 将数据点迹封装成热力图数据
     * @param tagLocationList 源数据点几个
     * @return
     */
    private List<HeatMapData> getHeatMapData(List<PositionTagLocation> tagLocationList, Double imageWidth,
                                             Double imageHeight, CoordinateSystemVo point) {
        List<HeatMapData> heatMapDataList = Lists.newArrayList();
        double rotation = point.getRotation();
        double scale = point.getScale();
        double tx = point.getTx();
        double ty = point.getTy();
        //封装集合
        tagLocationList.forEach(o->{
            Point boePoint = coordinateUtil.transformBoePoint(new Point(o.getX(), o.getZ(), 0.0), rotation, scale, tx, ty);
            HeatMapData heatMapData = new HeatMapData();
            //若转换的坐标超出了图片范围舍弃
            Double x = boePoint.getX();
            Double y = boePoint.getY();
            if (!(x < 0 || y < 0 || x > imageWidth || y > imageHeight)){
                heatMapData.setX(x)
                        .setY(y);
                heatMapDataList.add(heatMapData);
            }
        });
        return heatMapDataList;
    }

    /**
     * 初始化坐标转换
     * @return
     */
    private CoordinateSystemVo getPoint(String min, String max, Double imageWidth, Double imageHeight) {
        //获取基础转换坐标的4个点
        String[] strings1 = StringUtil.stringSplit(min);
        String[] strings2 = StringUtil.stringSplit(max);
        Point imgPoint1 = new Point(0.0, 0.0, 0.0);
        Point imgPoint2 = new Point(imageWidth, imageHeight, 0.0);
        Point mapPoint1 = new Point(Double.valueOf(strings1[0]), Double.valueOf(strings1[2]), 0.0);
        Point mapPoint2 = new Point(Double.valueOf(strings2[0]), Double.valueOf(strings2[2]), 0.0);
        //初始化4点
        double rotation = Math.toRadians(Math.abs(coordinateUtil.getAngle(imgPoint1, imgPoint2) - coordinateUtil.getAngle(mapPoint1, mapPoint2)));
        double scale = coordinateUtil.getScale(mapPoint1, imgPoint1, mapPoint2, imgPoint2);
        double tx = coordinateUtil.getXTranslation(mapPoint1, imgPoint1, rotation, scale);
        double ty = coordinateUtil.getYTranslation(mapPoint1, imgPoint1, rotation, scale);
        CoordinateSystemVo coordinateSystem = new CoordinateSystemVo();
        coordinateSystem.setRotation(rotation)
                .setScale(scale)
                .setTx(tx)
                .setTy(ty);
        return coordinateSystem;
    }
import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @author zf
 */
@Data
@Accessors(chain = true)
public class CoordinateSystemVo {
    private double rotation = 0.6386242345924739;
    private double scale = 1.0553303144165487;
    private double tx = -68.66969875118602;
    private double ty = -91.43752692911154;
    private double tz = 0.0;
}



import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;

import java.time.LocalDateTime;

/**
 * 位置数据
 * @author zf
 * @date 2018/12/19
 */
@Data
@ToString
@Accessors(chain = true)
public class PositionTagLocation {

    @ApiParam(hidden = true)
    private String uniqueKey;

    @ApiParam(hidden = true)
    private LocalDateTime gmtCreate;

    @ApiParam(hidden = true)
    private LocalDateTime gmtModified;

    @ApiParam(value = "X坐标")
    private Double x;

    @ApiParam(value = "Y坐标")
    private Double y;

    @ApiParam(value = "Z坐标")
    private Double z;

}
import lombok.Data;
import lombok.experimental.Accessors;

/**
 * @author zf
 */
@Data
@Accessors(chain = true)
public class HeatMapData {
    private Double x;
    private Double y;
    private Double z;
}

2022-05-10更新 缺少的代码:

import java.awt.image.BufferedImage;

    /**
     * 生成透明图片
     * @param width 高度
     * @param height 宽度
     */
    public static BufferedImage createTransparentImg(int width, int height) {
        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = buffImg.createGraphics();
        // ----------  增加下面的代码使得背景透明  -----------------
        buffImg = g2d.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
        //释放对象
        g2d.dispose();
        return buffImg;
    }

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
要使用Java实现克里金插值并生成热力图,你可以按照以下步骤进行: 1. 收集数据:首先需要收集一些有关要绘制热力图的数据。这些数据可以是从传感器或其他设备中收集的实时数据,也可以是从历史记录中收集的数据。 2. 进行克里金插值:使用Java中的插值库,例如Apache Commons Math,对数据进行克里金插值。这将生成一个平滑的表面,该表面表示原始数据点之间的连续函数。你可以控制插值的精度和平滑度,以使其最适合你的应用程序。 3. 生成热力图:将插值生成的表面转换为热力图。你可以使用Java中的图形库,例如JavaFX或Java 2D API,来绘制热力图。在绘制热力图时,你可以根据表面的高度值来选择颜色和透明度,以便更好地显示数据的分布。 下面是一个简单的Java代码示例,演示了如何使用Apache Commons Math和JavaFX库来实现克里金插值和热力图绘制: ```java import org.apache.commons.math3.analysis.interpolation.KrigingInterpolator; import org.apache.commons.math3.analysis.interpolation.LinearInterpolator; import org.apache.commons.math3.analysis.interpolation.NevilleInterpolator; import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; import org.apache.commons.math3.analysis.interpolation.TricubicInterpolator; import org.apache.commons.math3.analysis.interpolation.TricubicSplineInterpolator; import org.apache.commons.math3.analysis.interpolation.TriangularInterpolation; import org.apache.commons.math3.analysis.interpolation.WeightedEvaluation; import org.apache.commons.math3.analysis.interpolation.polynomials.PolynomialSplineFunction; import org.apache.commons.math3.analysis.polynomials.PolynomialFunction; import org.apache.commons.math3.analysis.polynomials.PolynomialFunctionLagrangeForm; import org.apache.commons.math3.analysis.polynomials.PolynomialFunctionNewtonForm; import org.apache.commons.math3.analysis.polynomials.PolynomialFunction.Parametric; import org.apache.commons.math3.exception.ZeroException; import org.apache.commons.math3.geometry.euclidean.threed.Line; import org.apache.commons.math3.geometry.euclidean.threed.LineSubSpace; import org.apache.commons.math3.geometry.euclidean.threed.Plane; import org.apache.commons.math3.geometry.euclidean.twod.Line; import org.apache.commons.math3.linear.Array2DRowRealMatrix; import org.apache.commons.math3.linear.BlockRealMatrix; import org.apache.commons.math3.linear.DefaultRealMatrixPreservingVisitor; import org.apache.commons.math3.linear.MatrixUtils; import org.apache.commons.math3.linear.NonSquareMatrixException; import org.apache.commons.math3.linear.QRDecomposition; import org.apache.commons.math3.linear.RealMatrix; import org.apache.commons.math3.linear.RealMatrixChangingVisitor; import org.apache.commons.math3.linear.RealMatrixPreservingVisitor; import org.apache.commons.math3.linear.RealMatrixVisitor; import org.apache.commons.math3.optim.ConvergenceChecker; import org.apache.commons.math3.optim.PointValuePair; import org.apache.commons.math3.optim.SimpleValueChecker; import org.apache.commons.math3.optim.nonlinear.scalar.GoalType; import org.apache.commons.math3.optim.nonlinear.scalar.MultivariateOptimizer; import org.apache.commons.math3.optim.nonlinear.scalar.ObjectiveFunction; import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.AbstractSimplex; import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.BOBYQAOptimizer; import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.CMAESOptimizer; import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.NelderMeadSimplex; import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.PowellOptimizer; import org.apache.commons.math3.optim.nonlinear.scalar.noderiv.SimplexOptimizer; import org.apache.commons.math3.optim.univariate.BrentOptimizer; import org.apache.commons.math3.optim.univariate.SearchInterval; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; import org.apache.commons.math3.util.FastMath; import org.apache.commons.math3.util.MathArrays; import org.apache.commons.math3.util.Pair; import org.apache.commons.math3.util.Precision; import org.apache.commons.math3.util.MathUtils; import org.apache.commons.math3.util.PairFunction; import org.apache.commons.math3.util.CombinatoricsUtils; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import javax.swing.JFrame; import javax.swing.JPanel; import javafx.application.Application; import javafx.embed.swing.SwingFXUtils; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Slider; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.paint.Color; import javafx.stage.Stage; public class HeatMap extends Application { private static final int WIDTH = 800; private static final int HEIGHT = 600; private static final double MIN_X = -10; private static final double MAX_X = 10; private static final double MIN_Y = -10; private static final double MAX_Y = 10; private static final int NUM_POINTS = 100; private static final int NUM_VALUES = 100; private static final Color[] COLORS = new Color[] { Color.BLUE, Color.GREEN, Color.YELLOW, Color.ORANGE, Color.RED }; private static final double[] COLOR_THRESHOLDS = new double[] { 0.2, 0.4, 0.6, 0.8 }; private static final double ALPHA = 0.7; private static final Random RANDOM = new Random(); private static final double[][] DATA = generateData(); private static final KrigingInterpolator INTERPOLATOR = new KrigingInterpolator(); private static final BufferedImage IMAGE = createImage(); private static final Slider SLIDER = new Slider(0, 1, 0.5); @Override public void start(Stage primaryStage) throws Exception { Canvas canvas = new Canvas(WIDTH, HEIGHT); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.drawImage(SwingFXUtils.toFXImage(IMAGE, null), 0, 0); canvas.setOnMousePressed(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (event.getButton().equals(MouseButton.PRIMARY)) { double x = event.getX() * (MAX_X - MIN_X) / WIDTH + MIN_X; double y = event.getY() * (MAX_Y - MIN_Y) / HEIGHT + MIN_Y; double value = INTERPOLATOR.interpolate(DATA, new double[] {x, y}); System.out.println(String.format("(%f, %f) -> %f", x, y, value)); } } }); SLIDER.valueProperty().addListener((obs, oldVal, newVal) -> { gc.drawImage(SwingFXUtils.toFXImage(createImage(newVal.doubleValue()), null), 0, 0); }); BorderPane root = new BorderPane(canvas, null, null, null, SLIDER); Scene scene = new Scene(root); primaryStage.setScene(scene); primaryStage.show(); } private static double[][] generateData() { double[][] data = new double[NUM_POINTS][3]; for (int i = 0; i < NUM_POINTS; i++) { double x = RANDOM.nextDouble() * (MAX_X - MIN_X) + MIN_X; double y = RANDOM.nextDouble() * (MAX_Y - MIN_Y) + MIN_Y; double value = RANDOM.nextDouble(); data[i][0] = x; data[i][1] = y; data[i][2] = value; } return data; } private static BufferedImage createImage() { return createImage(0.5); } private static BufferedImage createImage(double threshold) { BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = image.createGraphics(); double[][] values = new double[NUM_VALUES][NUM_VALUES]; for (int i = 0; i < NUM_VALUES; i++) { double x = MIN_X + (MAX_X - MIN_X) * i / (NUM_VALUES - 1); for (int j = 0; j < NUM_VALUES; j++) { double y = MIN_Y + (MAX_Y - MIN_Y) * j / (NUM_VALUES - 1); double value = INTERPOLATOR.interpolate(DATA, new double[] {x, y}); values[i][j] = value; } } double minValue = Double.POSITIVE_INFINITY; double maxValue = Double.NEGATIVE_INFINITY; for (int i = 0; i < NUM_VALUES; i++) { for (int j = 0; j < NUM_VALUES; j++) { if (values[i][j] < minValue) { minValue = values[i][j]; } if (values[i][j] > maxValue) { maxValue = values[i][j]; } } } for (int i = 0; i < NUM_VALUES; i++) { for (int j = 0; j < NUM_VALUES; j++) { double x = MIN_X + (MAX_X - MIN_X) * i / (NUM_VALUES - 1); double y = MIN_Y + (MAX_Y - MIN_Y) * j / (NUM_VALUES - 1); double value = values[i][j]; if (value < minValue) { value = minValue; } if (value > maxValue) { value = maxValue; } double ratio = (value - minValue) / (maxValue - minValue); if (ratio < threshold) { continue; } Color color = getColor(ratio); g2d.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (ALPHA * 255))); int x1 = (int) Math.round((i - 0.5) * WIDTH / NUM_VALUES); int y1 = (int) Math.round((j - 0.5) * HEIGHT / NUM_VALUES); int x2 = (int) Math.round((i + 0.5) * WIDTH / NUM_VALUES); int y2 = (int) Math.round((j + 0.5) * HEIGHT / NUM_VALUES); g2d.fill(new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1)); } } g2d.dispose(); return image; } private static Color getColor(double ratio) { int index = 0; for (int i = 0; i < COLOR_THRESHOLDS.length; i++) { if (ratio >= COLOR_THRESHOLDS[i]) { index = i + 1; } } if (index >= COLORS.length) { index = COLORS.length - 1; } return COLORS[index]; } } ``` 这个示例程序创建了一个名为HeatMap的JavaFX应用程序,使用Apache Commons Math库来执行克里金插值,并使用Java 2D API绘制热力图。它的主要思路是,首先生成一些随机数据点,然后使用克里金插值算法对它们进行插值并生成一个平滑的表面。然后,将这个表面转换为热力图,根据表面的高度值来选择颜色和透明度,以便更好地显示数据的分布。在这个示例程序中,你可以使用鼠标单击热力图上的任何点来获得该点的值。程序还包括一个滑块,可用于调整热力图的阈值,以控制要绘制的数据的数量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值