搞了个图像描边的需求

image border

最近项目中搞了个图像描边的需求,常见的美图工具 App 都有类似的功能,典型的如美图秀秀,一开始觉得应该不太复杂,正常评估时间,实际做的时候,发现问题比想象中的复杂多了,结果项目不得不延期 😭,所以有必要搞篇文章来总结下教训。

先说整体的流程: 1、原图 -> 2、抠图 -> 3、边缘检测 -> 4、绘制边缘 -> 5、结果导出

这个流程还是很容易想到,但是除了最后一步相对来说容易点,2、3、4 都是一路坑 😞

image matting

首先是抠图,就是这样的

跟我们这边的算法同学对接,爬虫收集图像、标注、模型训练 一套组合下来,效果不理想,生产环境不可用,第一步就卡住了 😞

为了赶项目周期,最后使用了阿里云的方案,这里就不多说了,算法同学持续优化模型,待成熟之后替换阿里云。

Edge detection

这一步相对来说是最复杂的,这里遇到的问题也是最大,耗时最久

最初的方案大致是这样的:抠图结果 -> 采样缩放图像 -> 遍历图像 bitmap 取满足条件的点,条件简单的理解就是点周围 3x3 范围的点像素值取平均值。
为什么要检测边缘,是因为要做虚线描边,获取连续的边缘点之后然后在画布上连接点画出来。

代码大概是这样的…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
+ (NSArray *)imageFindContours:(UIImage *)image {
    NSMutableArray *points = [[NSMutableArray array] init];
    UIImage *newImage = [image mediumResolution:CGSizeMake(30, 30)];
    
    CFDataRef imageData = CGDataProviderCopyData(CGImageGetDataProvider(newImage.CGImage));
    const uint8_t *data = CFDataGetBytePtr(imageData);
    
    int w = newImage.size.width;
    int h = newImage.size.height;
    unsigned char *bitmap = malloc(w * h * 4);
    memcpy(bitmap, data, w * h * 4);
    
    CGFloat leftmost = 1;
    CGFloat rightmost = 0;
    for (int i = 1; i < h - 1; i += 2) {
        for (int j = 1; j < w - 1; j += 2) {
            unsigned int left       = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(-1, 0)];
            unsigned int right      = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(1, 0)];
            unsigned int up         = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(0, -1)];
            unsigned int down       = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(0, 1)];
            unsigned int leftUp     = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(-1, -1)];
            unsigned int rightUp    = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(1, -1)];
            unsigned int leftDown   = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(-1, 1)];
            unsigned int rightDown  = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(1, 1)];
            unsigned int center     = [WDImageBorder valueForBitmap:data stride:w position:CGPointMake(i, j) offsets:CGSizeMake(0, 0)];
            unsigned int avg        = (left + right + up + down + leftUp + rightUp + leftDown + rightDown + center) / 9;
            
            int offset = i * w + j;
            if ((avg >= (255. * 0.4) && avg <= (255. * 0.9)) && center > 65) {
                bitmap[offset * 4] = 255;
                bitmap[offset * 4 + 1] = 0;
                bitmap[offset * 4 + 2] = 0;
                bitmap[offset * 4 + 3] = 255;
                
                CGFloat scale = 1.15;
                CGFloat x = (CGFloat)((((float) j / w) - 0.5) * scale + 0.5);
                CGFloat y = (CGFloat)((((float) i / h) - 0.5) * scale + 0.5);
                CGPoint point = CGPointMake(x, y);
                [points addObject:[NSValue valueWithCGPoint:point]];
                
                if (x <= leftmost) leftmost = x;
                if (x >= rightmost) rightmost = x;
            }
        }
    }
    ...
}

这个有个致命的问题,就是找到点之后,但是没办法有序的连起来,也试过一些方案,但是图像的边缘情况太复杂了,总是有问题,这里就不多说了。

接下来就需求其他的方案,大名鼎鼎的 OpenCV 出场了,参考官方文档,编译产物,接入 app 调试下来就能获取正确的结果了,这里就不多说了,直接看下代码,对应的节点有注释说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
+ (NSArray *)findContours:(UIImage *)image {

    Mat src;
    Mat src_gray;

    src = [self cvMatFromUIImage:image];
    cvtColor(src, src_gray, COLOR_BGR2GRAY);
    
    //UIImage *grayImg = [self UIImageFromCVMat:src_gray];
    
    blur(src_gray, src_gray, cv::Size(3, 3));

    //UIImage *blurgrayImg = [self UIImageFromCVMat:src_gray];
    
    /// 利用阈值二值化
    threshold(src_gray,src_gray,128,255,cv::THRESH_BINARY);
    /// 用Canny算子检测边缘
    //Canny(src_gray, src_gray, 128, 255 , 3);
    
    //UIImage *canny_outputImg = [self UIImageFromCVMat:src_gray];
    
    vector<vector<cv::Point> > contours;
    vector<Vec4i> hierarchy;


    /// 寻找轮廓
    findContours(src_gray, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, cv::Point(0, 0));

    /// 绘出轮廓
    Mat drawing = Mat::zeros(src_gray.size(), CV_8UC3);
    for (int i = 0; i < contours.size(); i++) {
        Scalar color = Scalar(255, 255, 255);
        drawContours(drawing, contours, i, color, 1, 8, hierarchy, 0, cv::Point());
    }
    //UIImage *contoursImg = [self UIImageFromCVMat:drawing];

    NSMutableArray *array = [NSMutableArray arrayWithCapacity:contours.size()];

    for (int i = 0; i < contours.size(); i++) {

        if (hierarchy[i][3] >= 0 || hierarchy[i][2] >= 0) {
            continue;
        }

        vector<cv::Point> vect = contours[i];

        std::vector<cv::Point>::const_iterator it; // declare a read-only iterator
        it = vect.cbegin(); // assign it to the start of the vector

        while (it != vect.cend()) { // while it hasn't reach the end
            //std::cout << it->x <<' '<< it->y <<' '; // print the value of the element it points to
            
            [array addObject:@(CGPointMake(it->x / image.size.width, it->y / image.size.height))];
            ++it; // and iterate to the next element
        }
    }

    return @[array];
}

这个方案唯一的缺陷就是需要引入 OpenCV 静态库,增加包大小,也想过咱只用到了边缘检测,其他的牛逼功能暂时也用不到,裁剪下只保留需要的类是不是就可以,但是大致翻了下,牵扯的太多,最终放弃了,最后也并没有使用 OpenCV 的方案。

因为发现了更轻量级的方案,Suzuki 边缘检测算法,后面也了解该算法其实就是 OpenCV 内部的一种边缘检测方案。 恰好 Android 同学找到了一个开源库,java 版本的 Suzuki 边缘检测算法。
代码拉下来结合算法文档来回撸几遍,大致能理解了,android 直接 java 拖进去用上,ios 翻译成 OC,也不复杂,因为算法核心方法也不过几十行代码,就算不理解,硬翻也能翻译过来。

贴一下翻译成 OC 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
+ (NSArray *)findContours:(UIImage *)img threshold:(CGFloat)threshold {
    img = [self blurImage:img blur:0.2];

    /// 记录原图尺寸
    int ow = (int) img.size.width;

    int w = ow;
    int h = (int) img.size.height;

    /// 考虑字节对齐 w 要重新计算

    CFDataRef imageData = CGDataProviderCopyData(CGImageGetDataProvider(img.CGImage));
    const uint8_t *data = CFDataGetBytePtr(imageData);

    w = (int) CFDataGetLength(imageData) / (h*4);

    char *F = malloc((size_t) CFDataGetLength(imageData)/4);

    /// 二值化处理
    threshold *= 255.f;
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (data[(i * w + j) * 4] > threshold) {
                F[i * w + j] = 1;
            } else {
                F[i * w + j] = 0;
            }
        }
    }

    NSMutableArray<Contour *> *contours = [NSMutableArray array];

    for (int i = 1; i < h - 1; i++) {
        F[i * w] = 0;
        F[i * w + w - 1] = 0;
    }
    for (int i = 0; i < w; i++) {
        F[i] = 0;
        F[w * h - 1 - i] = 0;
    }

    int nbd = 1;
    int lnbd = 1;

    for (int i = 1; i < h - 1; i++) {
        lnbd = 1;

        for (int j = 1; j < w - 1; j++) {

            int i2 = 0, j2 = 0;
            if (F[i * w + j] == 0) {
                continue;
            }
            //(a) If fij = 1 and fi, j-1 = 0, then decide that the pixel
            //(i, j) is the border following starting point of an outer
            //border, increment NBD, and (i2, j2) <- (i, j - 1).
            if (F[i * w + j] == 1 && F[i * w + (j - 1)] == 0) {
                nbd++;
                i2 = i;
                j2 = j - 1;

                //(b) Else if fij >= 1 and fi,j+1 = 0, then decide that the
                //pixel (i, j) is the border following starting point of a
                //hole border, increment NBD, (i2, j2) <- (i, j + 1), and
                //LNBD + fij in case fij > 1.
            } else if (F[i * w + j] >= 1 && F[i * w + j + 1] == 0) {
                nbd++;
                i2 = i;
                j2 = j + 1;
                if (F[i * w + j] > 1) {
                    lnbd = F[i * w + j];
                }
            } else {
                //(c) Otherwise, go to (4).
                //(4) If fij != 1, then LNBD <- |fij| and resume the raster
                //scan from pixel (i,j+1). The algorithm terminates when the
                //scan reaches the lower right corner of the picture
                if (F[i * w + j] != 1) {
                    lnbd = ABS(F[i * w + j]);
                }
                continue;

            }
            //(2) Depending on the types of the newly found border
            //and the border with the sequential number LNBD
            //(i.e., the last border met on the current row),
            //decide the parent of the current border as shown in Table 1.
            // TABLE 1
            // Decision Rule for the Parent Border of the Newly Found Border B
            // ----------------------------------------------------------------
            // Type of border B'
            // \    with the sequential
            //     \     number LNBD
            // Type of B \                Outer border         Hole border
            // ---------------------------------------------------------------
            // Outer border               The parent border    The border B'
            //                            of the border B'
            //
            // Hole border                The border B'      The parent border
            //                                               of the border B'
            // ----------------------------------------------------------------

            Contour *B = [Contour new];
            B.points = [NSMutableArray array];
            [B.points addObject:[NSValue valueWithCGPoint:CGPointMake(j * 1.f / ow, i * 1.f / h)]];
            B.isHole = (j2 == (j + 1));
            B.idx = nbd;
            [contours addObject:B];

            Contour *B0 = [Contour new];
            for (int c = 0; c < contours.count; c++) {
                if (contours[c].idx == lnbd) {
                    B0 = contours[c];
                    break;
                }
            }
            if (B0.isHole) {
                if (B.isHole) {
                    B.parentIdx = B0.parentIdx;
                } else {
                    B.parentIdx = lnbd;
                }
            } else {
                if (B.isHole) {
                    B.parentIdx = lnbd;
                } else {
                    B.parentIdx = B0.parentIdx;
                }
            }

            //(3) From the starting point (i, j), follow the detected border:
            //this is done by the following substeps (3.1) through (3.5).

            //(3.1) Starting from (i2, j2), look around clockwise the pixels
            //in the neigh- borhood of (i, j) and tind a nonzero pixel.
            //Let (i1, j1) be the first found nonzero pixel. If no nonzero
            //pixel is found, assign -NBD to fij and go to (4).

            int i1j1[2] = {-1, -1};

            cwNon0(F, w, h, i, j, i2, j2, 0, i1j1);
            if (i1j1[0] == -1 && i1j1[1] == -1) {
                F[i * w + j] = -nbd;
                //go to (4)
                if (F[i * w + j] != 1) {
                    lnbd = ABS(F[i * w + j]);
                }
                continue;
            }
            int i1 = i1j1[0];
            int j1 = i1j1[1];

            // (3.2) (i2, j2) <- (i1, j1) ad (i3,j3) <- (i, j).
            i2 = i1;
            j2 = j1;
            int i3 = i;
            int j3 = j;


            while (true) {
                //(3.3) Starting from the next elementof the pixel (i2, j2)
                //in the counterclock- wise order, examine counterclockwise
                //the pixels in the neighborhood of the current pixel (i3, j3)
                //to find a nonzero pixel and let the first one be (i4, j4).

                int i4j4[2] = {-1, -1};

                ccwNon0(F, w, h, i3, j3, i2, j2, 1, i4j4);

                int i4 = i4j4[0];
                int j4 = i4j4[1];

                [contours[contours.count - 1].points addObject:[NSValue valueWithCGPoint:CGPointMake(j4 * 1.f / ow,
                                                                                                     i4 * 1.f / h)]];

                //(a) If the pixel (i3, j3 + 1) is a O-pixel examined in the
                //substep (3.3) then fi3, j3 <-  -NBD.
                if (F[i3 * w + j3 + 1] == 0) {
                    F[i3 * w + j3] = (char) -nbd;

                    //(b) If the pixel (i3, j3 + 1) is not a O-pixel examined
                    //in the substep (3.3) and fi3,j3 = 1, then fi3,j3 <- NBD.
                } else if (F[i3 * w + j3] == 1) {
                    F[i3 * w + j3] = (char) nbd;
                } else {
                    //(c) Otherwise, do not change fi3, j3.
                }

                //(3.5) If (i4, j4) = (i, j) and (i3, j3) = (i1, j1)
                //(coming back to the starting point), then go to (4);
                if (i4 == i && j4 == j && i3 == i1 && j3 == j1) {
                    if (F[i * w + j] != 1) {
                        lnbd = ABS(F[i * w + j]);
                    }
                    break;

                    //otherwise, (i2, j2) + (i3, j3),(i3, j3) + (i4, j4),
                    //and go back to (3.3).
                } else {
                    i2 = i3;
                    j2 = j3;
                    i3 = i4;
                    j3 = j4;
                }
            }
        }
    }

    free(F);

    ...
}

SDF

上面提到边缘检测找连续的边缘点只是为了解决虚线描边,其他的描边情况其实是用不到这些点的,但是这里也遇到问题了。

一开始的想法跟上面通过 3x3 范围取平均值,通过条件过滤来做的,kernel code 大概是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static NSString *KernelString = @"\
kernel vec4 borderDraw(sampler image, sampler mask, sampler source, vec4 rgba, float midpoint, float width) \
{\
    vec2 uv = destCoord();\
    vec4 color = sample(image, samplerTransform(image, uv));\
    vec4 sumColor = vec4(0.0);\
    float radiu = 2 * width;\
    for (float m = -radiu; m <= radiu; m += 2) {\
        for (float n = -radiu; n <= radiu; n += 2) {\
            vec4 rgba = sample(image, samplerTransform(image, vec2(uv.x + m, uv.y + n)));\
            sumColor += rgba;\
        }\
    }\
    float avg = sumColor.a / float(radiu * radiu / 2.0);\
    if (color.a < 1.0 && (avg > .05 && avg < 1.)) {\
        return rgba;\
    }\
    return color;\
}";

这套方案做 demo 的时候,感觉效果还行,一点点毛疵,以为是条件判断不严谨,以为后续调整下可以解决,还有个严重的问题,就是这个方案计算量太大,图片分辨率 1080 左右,表现就有点卡,尤其是拖动滑竿调整,描边粗细 、间距 ,实时渲染有明显的卡顿,但是我们又不能降低图片质量,所以这个方案最终也就是停留在 demo 阶段了。

因为计算量太大,所以想办法降低像素计算量,SDF 出场了,通过距离场,可以生成一张图,这张图可以告知像素边界信息,直接通过边界信息,省去了极大的计算量。

SDF :signed distance filed 有向距离场
sdf 有两种方式,一种是循环(横向 x 纵向) 一种是双线性(横向 + 纵向),很明显前一种计算量远远大于后一种,联调下来第二种方案实际效果也是相当不错了。

这里还要考虑一个问题,因为描边是有粗细跟间距的,所以可以通过调整距离参数生成图,很好的解决了描边粗细跟间距问题的,SDF 方案在虚线描边的 case 也是有用的,通过把 SDF 生成图拿去做边缘检测找连续点。

来看下 SDF 生成图的效果

抠图
 


横向 SDF 结果
 


接纵向 SDF 结果

贴一下 Metal 版本的 SDF 计算逻辑

横向 SDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
extern "C" { namespace coreimage {

    constant float threshold = 0.5;

    float source(sampler image,float2 uv)
    {
        return image.sample(image.transform(uv)).a - threshold;
    }

    float4 sdfhor(sampler image,float width,destination dest)
    {
        float D2 = width * 2.0 + 1.0;

        // 获取当前点坐标
        float2 uv = dest.coord();
        float s = sign(source(image,uv));

        float d = 0.;
        for(int i= 0; i < width; i++) {
            d ++;

            float sp = sign(source(image,float2(uv.x + d, uv.y)));

            if(s * sp < 0.) {
                break;
            }

            sp = sign(source(image,float2(uv.x - d, uv.y)));

            if(s * sp < 0.) {
                break;
            }
        }

        float sd = -s * d / D2 ;

        return float4(float3(sd),1.0);
    }
    
}}

纵向 SDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
extern "C" {
    namespace coreimage {
        
        float sd(sampler image,float2 uv,float width)
        {
            float D2 = float(width * 2 + 1);
            
            float x = image.sample(image.transform(uv)).x;
            return x * D2;
        }

        float4 sdf(sampler image,float width,destination dest)
        {
            // 获取当前点坐标
            float2 uv = dest.coord();
            
            float dx = sd(image,uv,width);
            float dMin = abs(dx);
            float dy = 0.0;

            for(int i= 0; i < width; i++){
                dy += 1.0;
                float2 offset = float2(0.0, dy);

                float dx1 = sd(image,uv+offset,width);
            
                //sign switch
                if(dx1 * dx < 0.){
                    dMin = dy;
                    break;
                }

                dMin = min(dMin, length (float2(dx1, dy)));

                float dx2 = sd(image,uv-offset,width);

                //sign switch
                if(dx2 * dx < 0.){
                    dMin = dy;
                    break;
                }

                dMin = min(dMin, length (float2(dx2, dy)));

                if(dy > dMin)break;
            }

            float D2 = float(width * 2 + 1);

            dMin *= sign(dx);
            float d = dMin/D2;
            d = 1.0 - d;
            d = smoothstep(0.5 ,1.0, d);
            
            return float4(float3(d),1.0);
        }
    }
}

border

有了距离场,描边的工作就一下子简单多了。 目前实现的五种描边效果就是这样式的

项目中使用 coreimage 自定义 kernel 做的,当然也可以 metal 搞定

sdfsourceKernelString 对应上面第三个效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static NSString *sdfsourceKernelString = @"\
kernel vec4 borderDraw(sampler image, sampler sdf,  sampler source, float d1, float d2) \
{\
    vec2 uv = destCoord();\
    vec4 color = sample(image, samplerTransform(image, uv));\
    vec4 sdfcolor = sample(sdf, samplerTransform(sdf, uv));\
    vec4 sourcecolor = sample(source, samplerTransform(source, uv));\
    if (sdfcolor.x >= d1 && sdfcolor.x <= d2) {\
        return sourcecolor;\
    }\
    return color;\
}";


static NSString *sdfKernelString = @"\
kernel vec4 borderDraw(sampler image, sampler sdf, sampler source, vec4 rgba, vec2 offset, float d1, float d2) \
{\
    vec2 uv = destCoord();\
    vec4 color = sample(image, samplerTransform(image, uv));\
    vec4 offsetColor = sample(image, samplerTransform(image, uv-offset));\
    vec4 sdfcolor = sample(sdf, samplerTransform(sdf, uv));\
    vec4 sourcecolor = sample(source, samplerTransform(source, uv));\
    if(offset.x != 0.0 || offset.y != 0.0) {\
        if(color.a < 0.5 && offsetColor.a > 0.5 ) {\
            return mix(rgba,color,color.a);\
        }\
    } else if (sdfcolor.x >= d1 && sdfcolor.x <= d2) {\
        return mix(rgba,sourcecolor,offsetColor.a);\
    }\
    return color;\
}";

最后一个虚线描边就是常规的连接边缘点安排画布绘制,然后跟抠图做一个合并导出,就不展开说了。

Othter

最后还有一些注意点,比如抠图图像是在边缘,则需要考虑下预留描边空间,判断是否有落在边缘,如果有则补充点空间。
还有一个就是最小包围盒,抠图很可能只占据原图的一部分预期,为了展示效果,需要把抠图的最小包围盒找到,找这个最小包围盒,不需要那么精确,找出一个差不多的最小矩形框就行,项目上用的就是粗暴的像素遍历,找四个角的位置就可以了,通过最小包围盒,也能判断抠图是否靠近边缘。

The Last

综上,关键的几个步骤基本都尝试了多种方式,分析比较得出最合适项目需求的技术方案, 最终从性能、体验等维度拿到相对不错的结果,单纯从描边功能上来说,对比修复工具也不输 O (∩_∩) O 哈哈~

reference

PContour
SDF
双线性 SDF

相关文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值