🥬 🐶的uniapp学习之🦌 【提取图片主题色生成背景 】

1,288 阅读8分钟

canvas的使用

前言

首先,这篇文章的最终的效果不是很成功。记录一下我在这个失败过程中遇到的问题和尝试过的技术。

我想实现类似QQ音乐的如下效果 背景色是根据海报的主题色生成的,最后经过了高斯模糊。

刚开始考虑用js去实现,js肯定是可以实现但是尝试了几次效果不是很好。并且,对于计算像素点来说,还是在后端计算比较好。

这个过程我的理解:大概就是把图片分成 1px * 1px的像素点。计算出每个相同颜色像素点出现的次数。统计出最多的就是主题色。得到的是rgb颜色 rgb(255,255,255) 这种。

rgb 颜色

作为一个初级程序员👨🏻‍💻来说,对rgb颜色并不是很了解,特意去查了一下。

  • rgb 分别带表 red green blue 三基色。

  • 每个色阶 都是 0~255,代表亮度。三色都是0时,是最暗的黑色调,当全是255时,是最亮的白色调。

  • 按照计算,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216。通常也被简称为1600万色或千万色。也称为24位色(2的24次方)。

前端获取图片色调

对于<image>来说是无法去操作它的像素点的,通常情况下,要将其生成<canvas>才能去读取图片数据。

js读取本地图片生成canvas

我先尝试了在文件夹 📂 的html文件中读取文件夹中的图片。

【1】 在标签中画一个image 和 一个 canvas

                <img id="scream" src="shuijiao.jpg"/>
		<p>画布:</p>
		<canvas id="myCanvas">
		您的浏览器不支持 HTML5 canvas 标签。
		</canvas>

【2】 获得图片 和 画布的 打印出来可以发现打印的是dom元素

getContext("2d") 是建立一个2维渲染的上下文 具体语法请看 ✈️

let img=document.getElementById("scream");
console.log(img)
let ctx=document.getElementById("myCanvas").getContext("2d");

【3】 对于读取图片来说,加载是缓慢的。所以需要 onload 来等待加载完成。 上一步创建了上下文 ,drawImage是将canvas图像源画到上下文。

img.onload=function(img,ctx){
	ctx.drawImage(img,0,0,img.width,img.height);
	console.log(ctx)
	var imgData_obj = ctx.getImageData(0,0,250,150)    // 获取画布上的图像像素矩阵
	// var imgData = imgData_obj.data;
	console.log(imgData_obj)
	return ctx
}

我们说一下drawImage

它有 image sx sy sWidth sHeight dx dy dWidth dHeight 九个参数

image.png

语法: 参数个数可以使 3 5 9 ,注意对应参数都代表什么 详细的可以看一下✈️

void ctx.drawImage(image, dx, dy);
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

然后是getImageData

用来描述canvas隐含的像素数据

语法:参数分别是距离顶部的x轴距离、y轴距离,将要被提取区域的高、将要被提取区域的高。

tx.getImageData(sx, sy, sw, sh);

然后问题来了:画布是生成了 但是获取不到像素点的数据。一直报图片跨域问题。然后百度了很多这个错误还是解决的不了🙅🏻‍♀️。我觉得跟直接在文件夹中读取图片有些关系。索性也是试一试,还是去uniapp中写代码吧。

image.png

uniapp生成canvas

我开始的时候,像上面的写法,首先画一个图片 和 一个画布,然后通过getElementById获取元素。但是发现画布一直都没有画上,一直是白色的。

审查元素时发现,它会在canvas标签外还包了一层<uni-canvas>标签。因此一直是画不上去的。 image.png

后来百度错误,无意中发现uni中有一系列处理同样过程的方法。

uniapp方法的使用

首先在methods中定义方法,然后再onLoad生命周期中去调用这个方法。我们在下面再去分析代码

getImage() {
 // 获得图片信息
  uni.getImageInfo({
  // 注意图片的地址
    src: "/static/rang.png",
    success(res) {
      console.log(res.path)
      let imgHeight = 100;
      let imgWidth =100;
      var ctx = uni.createCanvasContext("logo") // 使用画布创建上下文 图片
      ctx.drawImage(res.path, 0, 0, 100, 100) // 设置图片坐标及大小,括号里面的分别是(图片路径,x坐标,y坐标,width,height)
      ctx.save(); //保存
      ctx.draw(true, () => {
        uni.canvasGetImageData({
          canvasId: 'logo',
          x: 0,
          y: 0,
          width: 100,
          height: 100,
          success: (res) => {
            let data = res.data;
            let arr = []
            var r = 1,
              g = 1,
              b = 1;
            // 取所有像素的平均值
            for (var row = 0; row < imgHeight; row++) {
              for (var col = 0; col < imgWidth; col++) {
                // console.log(data[((img.width * row) + col) * 4])
                if (row == 0) {
                  r += data[((imgWidth * row) + col)];
                  g += data[((imgWidth * row) + col) + 1];
                  b += data[((imgWidth * row) + col) + 2];
                  arr.push([r,g,b])
                } else {
                  r += data[((imgWidth * row) + col) * 4];
                  g += data[((imgWidth * row) + col) * 4 + 1];
                  b += data[((imgWidth * row) + col) * 4 + 2];
                  arr.push([r,g,b])
                }
              }
            }
            console.log(arr[8000])
            // 求取平均值
            r /= (imgWidth * imgHeight);
            g /= (imgWidth * imgHeight);
            b /= (imgWidth * imgHeight);
            // 将最终的值取整
            r = Math.round(r);
            g = Math.round(g);
            b = Math.round(b);
            let obj = {
              r,
              g,
              b
            }
            console.log(obj)
          },
          fail: (fail) => {
            console.log("ss")
          }
        })
      })
    }
  })
},

uni.getImageInfo

官网💻

【作用】:获取图片信息

【类型】:是一个对象

【参数】

  • src 图片的地址 (必填)
  • success 成功的回调函数
  • fail 失败的回调函数
  • complete 调用结束的回调函数

success回调返回参数

  • width:图片宽度 px
  • height:图片高度 px
  • path:返回本地的图片路径
  • type:返回图片的格式
  • orientation:返回图片的方向

【写法】

    uni.getImageInfo({
        src:"",
        success(res){
        
        }
    })

uni.createCanvasContext

我们在成功的回调函数中又使用了uni.createCanvasContext方法

【官网】💻

【作用】:创建canvas绘图上下文,但是需要指定canvasid。

  • 我们创建了名为 logo 的画布,然后通过 drawImage方法向canvas中填入我们的图片。。这个使用方法和原生的canvas是一样的。官网描述可以看一下,也可以看我们上一篇文章。

  • save()是保存当前的绘图上下文。 官网描述

  • draw()将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中。

参数1:reserve 布尔型 非必填。是否接着上一次绘制,true为接着上一次绘制。如果为false,清除掉之前的绘制。

参数2:绘制完成的回调

uni.canvasGetImageData

绘制完成后我们通过uni.canvasGetImageData来获得图片的数据

【官网】💻

image.png

image.png

这个 canvasId 要对应上

uni.canvasGetImageData({ 
    canvasId: 'log', 
    x: 0, 
    y: 0, 
    width: 100, 
    height: 100, 
    success(res) { 
    console.log(res.width) // 100 
    console.log(res.height) // 100 
    console.log(res.data instanceof Uint8ClampedArray) // true 
    console.log(res.data.length) // 100 * 100 * 4
    } 
   })

最终我们根据像素点 求出了平均值

我们的图片如下: logo.png 计算出的rgb值如下[🌈颜色值转换]:

image.png

但是这涉及到了计算量,在前端做计算不是很好。而且现在的写法对于颜色较多的图片实现上不是很好。 所以打算再用python去实现

python处理

接下来用到了Python的PIL库。将处理之后的rgb颜色返回到前端,前端做为背景色后再使用高斯模糊。

先熟悉一下两个库

【PIL】

[简介]: Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL功能非常强大,但API却非常简单易用。

[安装]

pip install pillow

[API]

这是一个大佬对每个API的讲解✈️,我就不赘述了

【colorsys】

[是什么]

是RGB (Red Green Blue) 色彩空间与三种其他色彩坐标系统 YIQ, HLS (Hue Lightness Saturation) 和 HSV (Hue Saturation Value) 表示的颜色值之间的双向转换。

所有这些色彩空间的坐标都使用浮点数值来表示。 在 YIQ 空间中,Y 坐标取值为[0,1],而 I 和 Q 坐标均可以为正数或负数。 在所有其他空间中,坐标取值均为 0 和 1 之间。 我们这里用到了把颜色从RGB值转为HSV值:colorsys.rgb_to_hsv(*r*, *g*, *b*)

这是这个库的文档✈️

【上代码】

因为我是在Django项目使用的,所以把这部份实现的代码从 view中拆分出来了,然后view中再去调用这个函数

[getImageBackground.py]

import colorsys
from PIL import Image
def get_dominant_color(image):
    # 颜色模式转换,以便输出rgb颜色值
    image = image.convert('RGBA')

    # 生成缩略图,减少计算量,减小cpu压力
    print(image.size)                   
    # image.thumbnail((200, 200))
    
    # 用于计算像素出现次数
    max_score = 0
    # 最后返回的rgb颜色
    dominant_color = 0
 
    for count, (r, g, b, a) in image.getcolors(image.size[0] * image.size[1]):
        print(r,g,b,a)
        # 跳过纯黑色
        if a == 0:
            continue   
        if r<45 and g<45 and b<45:
            continue
        saturation = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)[1]

        y = min(abs(r * 2104 + g * 4130 + b * 802 + 4096 + 131072) >> 13, 235)

        y = (y - 16.0) / (235 - 16)

        # 忽略高亮色
        if y > 0.9 or y<0.3:
            continue

        # Calculate the score, preferring highly saturated colors.
        # Add 0.1 to the saturation so we don't completely ignore grayscale
        # colors by multiplying the count by zero, but still give them a low
        # weight.
        score = (saturation + 0.1) * count

        if score > max_score:
            secondScore = max_score
            max_score = score
            dominant_color = (r, g, b)
    return dominant_color
    

  • image.thumbnail:是生成缩略图,但是我觉得生成缩略图后,对色彩提取不是很准确。image还有一个resize方法,它们俩的区别在于thumbnail是按比例缩放。
img = Image.open('./1.jpg') print("thumbnail前的尺寸", img.size)   // 500px * 336px
img.thumbnail((128, 128))                                         // 128px * 86px
img.resize((128,128))                                             // 128px * 128px
  • 然后定义了两个变量分别用于用于计算像素出现次数和rgb颜色值。遍历像素点 ,跳过纯黑色,但是我发现当图片颜色较深的时候效果还是不是很好。

  • 我去ps中试了一下rgb颜色 R G B三个值都小于45的时候都是接近于黑色的深色。把这些点都跳过会好些。

image.png

  • 对y的计算我也没有看懂,希望大佬看到能解释一下。y的最终值,是HSV的颜色值,也代表着亮度。我们过滤掉了高亮的 和 暗色的像素点。
  • 下面的判断取到最大值,并返回RGB颜色值

views.py

  • 使用PIL的open方法读取图片(📢 注意图片的路径 看下截图的文件层级)
  • import djangoProject.utils.getImageBackground as getBackground 导入写好的方法
  • 最后将rgb值以json的形式返回到前端
from django.http import HttpResponse,JsonResponse
import djangoProject.utils.getImageBackground as getBackground
import os
from PIL import Image
def my_view(request):
    img = Image.open(r"./static/background/leaf.png")
    rgb = getBackground.get_dominant_color(img)
    return JsonResponse({"rgb1":rgb})

前端接收返回值再处理

发起请求

这里没有对前端的请求方式进行封装,直接使用uni.request

async request() {

		let result = await uni.request({
			url: 'http://127.0.0.1:8000/my_view/',
		});
		let [error, res] = result; //ES6对数组的解构
		console.log(res)
		if (res.statusCode === 200) {
			let rgb = res.data.rgb;
			this.rgb = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`
			console.log(this.rgb)
		}
		if (res.statusCode === 404) {
			console.log('找不到接口资源');
		}
	}

我们第一次尝试的时候出现了跨域问题。在uniapp中跨域问题有些不好处理,我选择了在后端进行了跨域处理。

使用[django-cors-headers]

【安装】

pip install django-cors-headers

settings.py

【注册corsheaders】

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'first',
    'corsheaders',   // 添加到app中
    'static'
]

【添加中间件】

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',          # 放到common前
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware', 
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

请求成功

接口返回值如下

image.png

渲染背景

  • 视图中 用到了动态变量渲染css:style="{}" ,this.rgb是定义的变量
  • 高斯模糊filter: blur(20px);给图像设置高斯模糊。"radius"一值设定高斯函数的标准差,或者是屏幕上以多少像素融在一起, 所以值越大越模糊;如果没有设定值,则默认是0;这个参数可设置css长度值,但不接受百分比值。
  • onLoad() 是Uniapp的初始化生命周期
<template>
	<view class="content" :style="{'background-color':this.rgb,height:'100%'}">
	</view>
</template>

<script>
	export default {
		data() {
			return {
				title: 'Hello',
				rgb: ''
			}
		},
		onLoad() {
			this.request()
		},
		methods: {
			async request() {
				let result = await uni.request({
					url: 'http://127.0.0.1:8000/my_view/',
				});
				let [error, res] = result; //ES6对数组的解构
				console.log(res)
				if (res.statusCode === 200) {
					let rgb = res.data.rgb;
                                        // 模板字符串
					this.rgb = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`
					console.log(this.rgb)
				}
				if (res.statusCode === 404) {
					console.log('找不到接口资源');
				}
			}
		}
	}
</script>

<style>
	.content {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		filter: blur(20px);
	}

	.logo {
		height: 200rpx;
		width: 200rpx;
		margin-top: 200rpx;
		margin-left: auto;
		margin-right: auto;
		margin-bottom: 50rpx;
	}

	.text-area {
		display: flex;
		justify-content: center;
		background-color: #f7fcfe;
	}

	.title {
		font-size: 36rpx;
		color: #8f8f94;
	}

	page {

		uni-page-body {
			background-color: #f7fcfe;
			height: 100%;
			font-size: 14px;
			line-height: 1.8;
			display: flex;

		}
	}
</style>

看下效果

还算可以

image.png

image.png

image.png