基于区块链的图像分享
本文是对该系统实现的一个总结和备忘
本系统的结构图大致如下图,前端采用vue及element-ui plus,后端使用Django,数据库使用Mysql,区块链环境使用ganache-cli,文件系统使用go-ipfs,后端和区块链使用web3.py,后端与ipfs交互使用ipfshttpclient
环境搭建
区块链相关环境搭建
ganache-cli
下载参考:
https://github.com/trufflesuite/ganache-cli
如图自动生成了10个各有100以太的账户
智能合约编写使用remix,是一个在线的编辑器https://remix.ethereum.org/
简单的使用流程如下
编译成功后,在此处可以复制生成的abi
之后部署合约,首先需要连接本地的区块链环境,如下所示,选择web3 provider
注意应该对应自己本地环境对应的地址
连接后,在下图一处部署合约,部署成功后可在二处复制合约地址,在三处调用测试合约的相关功能。
web3.py使用
pip install web3
参考链接:
web3.py文档
https://www.pianshen.com/article/18638804/
https://blog.csdn.net/qq_41907714/category_10590479.html
https://www.jianshu.com/p/520516c7d377?utm_campaign
https://blog.csdn.net/weixin_39430411/article/details/104270968
https://www.freesion.com/article/1744847080/
https://www.freesion.com/article/1744847080/
https://blog.csdn.net/BF02jgtRS00XKtCx/article/details/106030165
连接区块链环境大致如下:
#连接区块链
web3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
#得到合约部署的abi,我这里放入文件中,所以从文件中读取
with open('tool/test.abi') as file:
myabi = json.loads(file.read())
#其中address为合约部署的地址
contract = web3.eth.contract(address=address, abi=myabi)
#可以判断区块链连接是否成功
if web3.eth.getBlock(0) is None:
print("连接失败")
elif web3.isConnected():
print("连接成功")
#调用合约的两种方式
contract.functions.调用的函数().call()
contract.functions.调用的函数().transact({'from':web3.eth.accounts[0]})
ipfs
这里只使用命令行版,下载后通过ipfs daemon启动,如下所示
这里也是通过Python与其交互,所以安装
pip install ipfshttpclient
具体使用见:https://github.com/ipfs-shipyard/py-ipfs-http-client
报错解决
pip install web3报错
AttributeError: 'NoneType' object has no attribute 'bytes'
使用如下命令解决
easy_install -U pip
再次运行报错
error: Microsoft Visual C++ 14.0 is required. Get it with "Build Tools for Visual Studio"
在如下连接下载对应版本,其中的cp对应版本,然后pip install 对应模块
https://www.lfd.uci.edu/~gohlke/pythonlibs/
下载了三个模块:
cytoolz.dicttoolz
lru-dict
bitarray._bitarray
但是bitarray还是报错,使用下面连接中第三种方法解决
https://zhuanlan.zhihu.com/p/126669852
运行ganache-cli出错
报错信息如下
Ganache CLI v6.10.1 (ganache-core: 2.11.2)
Error: Callback was already called.
at C:\Users\AppData\Roaming\npm\node_modules\ganache-cli\build\ganache-core.node.cli.js:19:276
at s.<anonymous> (C:\Users\AppData\Roaming\npm\node_modules\ganache-cli\build\ganache-core.node.cli.js:19:2238)
at s.emit (events.js:315:20)
at s.destroy (C:\Users\AppData\Roaming\npm\node_modules\ganache-cli\build\ganache-core.node.cli.js:39:744230)
at finish (internal/streams/writable.js:670:14)
at processTicksAndRejections (internal/process/task_queues.js:80:21)
使用如下命令卸载后重装
npm uninstall -g ganache-cli
npm install -g ganache-cli
使用ipfs时报错
报错如下:
https://dist.ipfs.io/
The migrations of fs-repo failed:
failed to find latest fs-repo-migrations: http.DefaultClient.Do error: Get "https://ipfs.io/ipfs/QmYRLRDKobvg1AXTGeK5Xk6ntWTsjGiHbyNKhWfz7koGpa/fs-repo-migrations/versions": dial tcp 103.252.115.153:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
If you think this is a bug, please file an issue and include this whole log output.
https://github.com/ipfs/fs-repo-migrations
Error: failed to find latest fs-repo-migrations: http.DefaultClient.Do error: Get "https://ipfs.io/ipfs/QmYRLRDKobvg1AXTGeK5Xk6ntWTsjGiHbyNKhWfz7koGpa/fs-repo-migrations/versions": dial tcp 103.252.115.153:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
可能因为之前用过ipfs,之后又删除了,产生了冲突,可以将C:\Users\自己的路径.ipfs该处的.ipfs文件删除
功能实现
合约编写
solidity中文文档
使用智能合约主要进行以下功能的设计:
- 任何系统和用户都可以调用本文设计的智能合约,将自己原创作品的版权信息登记在区块链中。
- 对于图像的下载,能够对智能合约将下载者和下载图像的相关信息进行记录,用作之后侵权判定的证据之一。
- 当侵权发生时,可以通过智能合约查询侵权图片的相关信息和图片的下载记录,并对侵权行为进行判定。
- 用于实现图像分享平台的补充功能:基于智能合约实现用户之间的转账,账户信息的查询等操作。
图像的版权信息组成
变量名 | 类型 | 含义 |
---|---|---|
Name | string | 图片上传时的名字 |
PictureID | uint | 图片唯一的编号,由合约自动递增生成 |
PictureHash | string | 图片经过ORB算法计算得到的特征从而得到的Hash,在上传到IPFS上是,根据其内容生成,也是访问该图片存储在IPFS上的特征时所需要的唯一链接 |
OwnerID | string | 图片上传者唯一的编号,11位数字组成 |
UploadTime | uint | 时间戳,表示上传时间,为1970年后经过的秒数 |
HashID | bytes | 将以上变量数值使用keccak256算法得到的哈希值 |
实现代码
pragma solidity 0.8.0;
contract ImageSharing{
uint public PictureID;
//用于图像版权信息保存,通过图像编号映射到该图像
mapping(uint=>Picture)Stores;
//用于保存下载记录,通过图像编号映射到下一层映射A,A保存下载记录,通过下载序号映射到下载记录DownUser
mapping(uint=>mapping(uint=>DownUser)) User;
//通过用户编号映射对应的余额,因为solidity不支持浮点数的保存和运算,又因为系统场景中,金钱的交易一般为小数点后有两位的小数,所以在保存前和查询后需要对金额进行乘以100和除以100的操作
mapping(string=>uint) Money;
//保存下载记录
struct DownUser{
uint DownTime;
string UserID;
}
//图片版权信息
struct Picture {
uint PictureID;
string name;
string PictureHash;
string OwnerID;
uint UpLoadTime;
bytes32 HashID;
uint DownNumber;
}
event backPictureID(uint);
constructor(){
PictureID=0;
}
//向账户存钱
function AddMoney(string memory _UserID,uint _num)public returns(bool
){
Money[_UserID]=Money[_UserID]+_num;
return (true);
}
//从账户取钱
function SubMoney(string memory _UserID,uint _num)public returns(bool
){
Money[_UserID]=Money[_UserID]-_num;
return (true);
}
//将钱从一个账户向另一个账户转移
function tran(string memory _UserID1,string memory _UserID2,uint _num)public returns(bool
){
Money[_UserID1]=Money[_UserID1]-_num;
Money[_UserID2]=Money[_UserID2]+_num;
return (true);
}
//查询账户余额
function GetMoney(string memory _UserID)public view returns(uint
){
return (
Money[_UserID]
);
}
//上传图片,保存图片信息,生成图像唯一编号PictureID,并对版权信息哈希后保存为HashID
function Upload(
string memory _name,
string memory _PictureHash,
string memory _OwnerID,
uint _UpLoadTime
)public{
PictureID+=1;
bytes32 HashID=keccak256(abi.encodePacked(PictureID,_name,_PictureHash,_OwnerID,_UpLoadTime));
Picture memory picture=Picture(PictureID,_name,_PictureHash,_OwnerID,_UpLoadTime,HashID,0);
Stores[PictureID] = picture;
emit backPictureID(PictureID);
}
//得到图像相关信息,图像的编号PictureID,图像的名称name,图像特征的地址PictureHash,图像上传者编号OwnerID,图像上传时间,HashID
function GetPicture(uint _PictureID)public view returns(uint,string memory,string memory,string memory,uint,bytes32
){
Picture memory picture = Stores[_PictureID];
return (
picture.PictureID,
picture.name,
picture.PictureHash,
picture.OwnerID,
picture.UpLoadTime,
picture.HashID
);
}
//保存下载记录
function Download(uint _PictureID,uint _DownTime,string memory _UserID)public{
Stores[_PictureID].DownNumber++;
Picture memory picture = Stores[_PictureID];
DownUser memory downuser=DownUser(_DownTime,_UserID);
User[_PictureID][picture.DownNumber]=downuser;
}
//用于侵权判定,判断该用户是否下载过该图片
function judge(uint _PictureID,string memory _UserID)public view returns(bool,uint){
Picture memory picture = Stores[_PictureID];
for(uint i=1;i<=picture.DownNumber;i++){
if(keccak256(abi.encodePacked(User[_PictureID][i].UserID))==keccak256(abi.encodePacked(_UserID))){
return(true,User[_PictureID][i].DownTime);
}
}
return(false,0);
}
//得到图片下载量
function GetDownNUmber(uint _PictureID)public view returns (uint){
Picture memory picture = Stores[_PictureID];
return picture.DownNumber;
}
//用于得到图片的一个下载记录
function GetUser(uint _PictureID,uint _i)public view returns (string memory,uint){
return (
User[_PictureID][_i].UserID,
User[_PictureID][_i].DownTime
);
}
}
数字水印
arnold
参考:https://www.jianshu.com/p/39727dbaffd9
水印嵌入前通过Arnold变换,提高其安全性
采用广义Arnold变换,其变换公式如下所示,其中x,y为原像素位置,x’,y’为变换后像素位置,N为图像边长。a,b为参数。在实际变换时应进行多次变换,并将变换轮次n与参数a,b作为变换密钥。
变换:
逆变换:
实现代码:
import cv2
import numpy as np
def en_arnold(img):
# 广义arnold几个参数,相当于密钥
a = 6
b = 7
count = 8
size=img.shape[0]
img2 = np.ones((size, size))
for n in range(count):
for i in range(size):
for j in range(size):
m = (i + a * j) % size
n = (b * i + (a * b + 1) * j) % size
img2[m][n] = img[i][j]
return img2
def de_arnold(img):
# 广义arnold几个参数,相当于密钥
# img = cv2.imread(r'C:/Users/Desktop/huiwater2.png', cv2.IMREAD_UNCHANGED)
a = 6
b = 7
count = 8
size=img.shape[0]
img2 = np.ones((size, size))
for n in range(count):
for i in range(size):
for j in range(size):
m = ((a * b + 1) * i - a * j) % size
n = (-b * i + j) % size
img2[m][n] = img[i][j]
cv2.imwrite('media/downimg/watermark.png', img2)
return True
DCT域水印
水印方面主要有以下功能:
- 水印能够自动根据下载图片的用户的信息和图片版权信息自动生成。
- 水印在图像下载时自动进行嵌入,嵌入后不影响图像的正常使用。
- 在侵权判定时,能够提取图像中的水印,作为侵权判定的依据之一。
水印的生成与变换
水印根据下载者信息和下载图片在下载时自动生成,为正方形的PNG二值图片格式,之后经过Arnold变换,提高其安全性,具体流程如下:
- 根据下载图片和下载用户查询得到四种信息,分别为图片编号PictureID,上传者信息OwnerID,下载时间DownTime与下载者UserID。并将上述信息进行拼接。
- 通过Pillow中的ImageDraw.Draw创建长和宽都为144的正方形图片,并通过multiline_text将拼接好的信息写入图片。
- 然后对水印进行置乱操作。水印的变换采用广义的Arnold变换,需要设置三个参数,相当于图像加密的密钥。这里设置的参数为a 为6,b为7,置乱的轮数count为8。提取水印后需要进行逆变换,逆变换时参数设置要与变换时参数设置相同。
水印嵌入与提取
- 嵌入过程
水印选择在图片的亮度信号的DCT域中嵌入。具体流程如下:
- 读入:在YCrCb色彩空间上读入图像,将读入的亮度信号Y与色度信号U和V分离,之后仅对亮度信号Y进行操作。
- 填充与分块计算:为了方便进行DCT系数的计算,需要先对亮度信号Y进行扩充,将其行列数扩充为8的倍数。填充的值为0。之后将亮度信号Y分成8*8的块,分别对每一块计算其DCT系数。
- 确定嵌入的位置:嵌入在中低频系数中,选择嵌在每一个8*8DCT系数块通过z字形扫描后,排在第3,4,5的位置中。
- 嵌入:将水印信息按顺序嵌在确定的位置,因为水印为二值图像,像素只有255与0两个值,所以如果水印为255,则将该位置值改为10,如果水印值为0,则将该位置改为-10,最后直到水印嵌入完毕。嵌入位置如下图所示。
- 还原:之后将每一个8*8的DCT系数块分别进行逆DCT系数变换,此过程会出现使图像出现一定损失。之后将经过嵌入变换后的亮度信号Y进行裁剪,去除开始时填充的部分。最后将亮度信号Y与原始的色度信号U和V一起还原回原来的图像。
嵌入流程示意图如下图所示。
- 提取过程
水印提取与水印嵌入过程基本相同。首先在YCrCb色彩空间上读入图像,将亮度信号Y与色度信号U和V分离,然后对亮度信号Y进行扩充。扩充后划分为8*8的块,对每一块计算DCT系数。
不同之处在于:扫描与嵌入时确定的同样的位置,对该位置的值进行判断,如果值为正数,则判定该处嵌入的水印信息为255,如果该处的值为负数,则判定该处嵌入的水印信息为0。提取出水印的像素之后,将其还原,水印提取完毕。
提取流程示意图如下所示。
参考及代码
https://blog.csdn.net/qq_26140297/article/details/79945052?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-14.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-14.control
https://zhuanlan.zhihu.com/p/183262908
https://www.doc88.com/p-5038685851301.html
https://blog.csdn.net/weixin_42537048/article/details/103673540
https://blog.csdn.net/u011436429/article/details/80393445
https://blog.csdn.net/qq_34784753/article/details/60103888
https://blog.csdn.net/weixin_44586750/article/details/103168949
https://blog.csdn.net/sunmingyang1987/article/details/103306532
#水印的自动生成
from PIL import Image, ImageDraw
def new_mark(word):
marksize=144
small_mark = Image.new('1', (marksize, marksize), color=255)
draw = ImageDraw.Draw(small_mark)
draw.multiline_text((0, 0), word, fill=0)
small_mark.save('tool/watermark/watermark.png')
#水印的嵌入与提取
import cv2
import numpy as np
def embed(img,mark):
# 得到Y
#img = cv2.imread(r'C:/Users/Desktop/host.jpg', cv2.IMREAD_UNCHANGED)
size1, size2, size3 = img.shape
img_YUV = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
img_Y = img_YUV[:, :, 1]
# 扩充
if (size1 % 8 != 0):
supply1 = 8 - size1 % 8
else:
supply1 = 0
if (size2 % 8 != 0):
supply2 = 8 - size2 % 8
else:
supply2 = 0
img_Y = cv2.copyMakeBorder(img_Y, 0, supply1, 0, supply2, cv2.BORDER_CONSTANT, value=0)
# 分块计算dct
img_Y_dct = np.zeros_like(img_Y)
img_Y = np.float32(img_Y)
img_Y_dct = np.float32(img_Y_dct)
for i in range(0, img_Y.shape[0], 8):
for j in range(0, img_Y.shape[1], 8):
img_Y_dct[i:(i + 8), j:(j + 8)] = cv2.dct(img_Y[i:(i + 8), j:(j + 8)])
# 记录能嵌入水印的位置
loc = []
for i in range(0, img_Y.shape[0]):
for j in range(0, img_Y.shape[1]):
if (i % 8 + j % 8 == 3):
loc.append((i, j))
# 嵌入水印
#mark = cv2.imread(r'C:/Users/Desktop/watermark.png', 0)
mark_list = mark.reshape(-1)
n = 0
i = 0
while (n < len(mark_list)):
while (i < len(loc)):
if (mark_list[n] == 255):
img_Y_dct[loc[i][0]][loc[i][1]] = 10
n = n + 1
else:
img_Y_dct[loc[i][0]][loc[i][1]] = -10
n = n + 1
i = i + 1
break
# 逆DCT
for i in range(0, img_Y.shape[0], 8):
for j in range(0, img_Y.shape[1], 8):
img_Y[i:(i + 8), j:(j + 8)] = cv2.idct(img_Y_dct[i:(i + 8), j:(j + 8)])
#还原图片
c = img_Y[0:img_Y.shape[0] - supply1, 0:img_Y.shape[1] - supply2]
img_YUV[:, :, 1] = c
img = cv2.cvtColor(img_YUV, cv2.COLOR_YCrCb2RGB)
img = np.uint8(img)
cv2.imwrite('media/downimg/2.jpg', img, [cv2.IMWRITE_JPEG_QUALITY, 100])
def extract(img):
#读取
# img = cv2.imread(r'C:\Users\Downloads\2.jfif', cv2.IMREAD_UNCHANGED)
size1, size2, size3 = img.shape
img_YUV = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
img_Y = img_YUV[:, :, 1]
# 扩充
if (size1 % 8 != 0):
supply1 = 8 - size1 % 8
else:
supply1 = 0
if (size2 % 8 != 0):
supply2 = 8 - size2 % 8
else:
supply2 = 0
img_Y = cv2.copyMakeBorder(img_Y, 0, supply1, 0, supply2, cv2.BORDER_CONSTANT, value=0)
# 分块计算dct
img_Y_dct = np.zeros_like(img_Y)
img_Y = np.float32(img_Y)
img_Y_dct = np.float32(img_Y_dct)
for i in range(0, img_Y.shape[0], 8):
for j in range(0, img_Y.shape[1], 8):
img_Y_dct[i:(i + 8), j:(j + 8)] = cv2.dct(img_Y[i:(i + 8), j:(j + 8)])
#确定可能的嵌入位置
loc = []
water = []
for i in range(0, img_Y.shape[0]):
for j in range(0, img_Y.shape[1]):
if (i % 8 + j % 8 == 3):
loc.append((i, j))
#提取
i = 0
j = 0
while (j < 144 * 144):
if (img_Y_dct[loc[i][0]][loc[i][1]] > 0):
water.append(255)
else:
water.append(0)
j = j + 1
i = i + 1
water = np.array(water)
water_img = water.reshape((144, 144))
return water_img
# cv2.imwrite(r'C:/Users/Desktop/huiwater2.png', water_img)
效果如图
可见水印
为了防止截屏图片,添加可见水印
from PIL import Image, ImageDraw
# 添加可见水印
def new_logo(name):
# 简单的嵌入操作,主要是粘贴
im1 = Image.open('watermark/watermark.png') # 嵌的水印
r, g, b, a = im1.split()
im2 = Image.open(name) # 被嵌的大图片,name为嵌入图片路径
x, y = im2.size
im2.paste(im1, (int(x / 2 - 150), int(y / 2 - 35)), mask=a)
im2.save('shuiyin/'+name)
效果如图:
沙箱支付
系统有支付功能,这里使用手机支付宝扫码支付的方式进行充值,使用Python-alipay-sdk,使用说明:https://github.com/fzlee/alipay/blob/master/docs/apis.zh-hans.md#verification
沙箱环境的配置参考:
https://pea328.blog.csdn.net/article/details/102502851?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
https://www.cnblogs.com/qikeyishu/p/11564756.html
在电脑上使用沙箱环境支付操作时,出现“提示存在钓鱼风险”的页面,可能是在浏览器中同时登陆了真正的支付宝账号,所以在使用时从另一个浏览器进行调试。
#以下为程序中配置的信息
from alipay import AliPay
alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
自己配置的公钥
-----END PUBLIC KEY-----"""
app_private_key_string = """-----BEGIN RSA PRIVATE KEY-----
自己配置的私钥
-----END RSA PRIVATE KEY-----"""
alipay = AliPay(
appid = '2021000117650',
app_notify_url = ' ',
app_private_key_string = app_private_key_string,
alipay_public_key_string = alipay_public_key_string,
debug=True,
)
充值时前端生成uuid,请求charge函数处理,后端会返回一个支付链接,前端打开
uuid生成参考:https://www.cnblogs.com/lingfenglian/p/12605465.html
如下所示:
charge(){
this.chargeDialogVisible = true
var that = this;
that.axios
.post(that.baseURL + "charge/",{
sessionID: sessionStorage.sessionID,
amount:that.chargeNum,
uuid:guid()
})
.then((response) => {
const status = response.data.status;
if (status === 1) {
that.$message({
message: response.data.msg,
showClose: true,
type: "success",
});
// window.location.href = response.data.url
window.open(response.data.url)
} else {
that.$message({
message: response.data.msg,
showClose: true,
type: "warning",
});
}
})
.catch(function (err) {
that.$message({
message: err,
showClose: true,
type: "warning",
});
});
},
后端charge函数生成支付信息,通过return_url进行支付成功之后的操作
return_url会请求v_charge,v_charge进行支付后的判定和信息的记录
def Charge(request):
#得到信息
data=json.loads(request.body.decode('utf-8'))
sessionID=data['sessionID']
session = Session.objects.get(pk=sessionID)
UserID=session.get_decoded()['username']
uuid=data['uuid']
amount=data['amount']
# 发起支付请求
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=uuid,
total_amount=amount,
subject='充值',
return_url='http://192.168.43.10:8000/vcharge/?sessionID='+sessionID
)
pay_url = 'https://openapi.alipaydev.com/gateway.do?' + order_string
# obj_charge=charge(uuid=uuid,status=0,kind=1,ID=UserID,amount=a)
obj_charge=charge(uuid=uuid,status=0,kind=1,userID=UserID,amount=amount)
obj_charge.save()
return JsonResponse({'status': 1, 'msg': '成功','url':pay_url})
def v_charge(request):
orderID=request.GET.get('out_trade_no')
print(orderID)
sessionID=request.GET.get('sessionID')
session = Session.objects.get(pk=sessionID)
UserID=session.get_decoded()['username']
Time=request.GET.get('timestamp')
#验证订单
result = alipay.api_alipay_trade_query(orderID)
print(result)
if(result['code']!='10000'):
return JsonResponse({'status': 0, 'msg': '失败'})
obj_charge=charge.objects.get(uuid=orderID)
amount=obj_charge.amount
obj_charge.status=1
obj_charge.Time=Time
obj_charge.save()
obj_user=userInformation.objects.get(username=UserID)
obj_user.remain=obj_user.remain+amount
obj_user.save()
#连接区块链
web3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
with open('tool/test.abi') as file:
myabi = json.loads(file.read())
contract = web3.eth.contract(address=address, abi=myabi)
if web3.eth.getBlock(0) is None:
print("连接失败")
elif web3.isConnected():
print("连接成功")
#上传到区块链
contract.functions.AddMoney(UserID,int(amount*100)).transact({'from':web3.eth.accounts[0]})
return HttpResponse("支付成功请关闭该页面")
图像特征
一方面为了防止图像重复上传,一方面需要保证图像确权的唯一性,所以要在图像上传时采用提取图像特征。开始计划使用图像哈希算法:phash,ahash,dhash。发现效果并不理想
参考:
https://segmentfault.com/a/1190000038308093?utm_source=sf-related
http://itindex.net/detail/42723-%E7%9B%B8%E4%BC%BC-%E5%9B%BE%E7%89%87%E6%90%9C%E7%B4%A2-%E5%93%88%E5%B8%8C
于是选用图像特征算法orb,主要完成以下功能:
- 自动对图像进行过滤,阻止相似图像重复上传与重复版权登记。当用户上传图像。系统利用ORB算法自动提取图片特征。然后通过与保存的已上传图像的特征进行对比,防止相似图像的再次上传。即使图像经过裁剪,旋转,缩放等操作,依然能对相似图片进行有效的过滤。从而确保图像确权的有效性。
- 提取图像特征,作为版权信息存证。在图像进行确权时需要将图像的特征作为版权信息之一进行存证。保存的特征主要包含图像关键点和关键点对应的特征向量。这些信息将作为侵权判定的依据,对于疑似侵权图片,可以提取其特征与已经确权的图像的特征进行对比,找出相似图像,从而对侵权行为进行判定。
特征提取
得到待上传图像后,先转化为灰度图像,再通过ORB算法提取得到图像的关键点。确定一个关键点的大概过程为:
- 先从图片中选取一个像素点,像素值为p,并设定一个阈值t,如果两个像素相差的绝对值大于阈值,即它们的像素之差不在(p-t,p+t)这一范围,那么则判定这两个像素点不同。
- 将选取的点与周围的16个像素点进行对比,如果周围的点中有连续的n个点与选取的点不同,则认为改点是一个关键点。
然后确定关键点所对应的特征向量:
- 选取一个关键点,并以它为中心,在周围划定一个像素区域。
- 在划定的区域中随机选取256个像素对。对每一个像素对进行对比,如果第一个像素值大于第二个像素值,则记为1,否则记为0。最终为该关键点创建由256位0,1比特组成的特征向量
- 对下一个关键点重复上述操作,最终完成特征向量的提取。
特征对比
得到图像的特征信息后就可以与已保存的图像特征对比,进行相似度检测。大致过程如下:
- 首先需要图像版权信息中图像特征保存的地址PictureHash遍历IPFS系统上保存的图像特征,并将其还原构造为所需的KeyPoint类型。具体过程在3中图像特征保存过程叙述。
- 构造匹配器对象,这里使用knnMatch进行k近邻比较,最后每个匹配的关键点都会得到最相似的两个关键点。之后对这两个关键点的欧式距离进行比较,设置条件为第一个关键点的距离需要小于第二个关键点距离的0.8倍。这样就筛选出最佳匹配的关键点。
- 计算最佳匹配的关键点占图像关键点的比值,设定阈值为0.1,只有大于阈值才认为两幅图像相似。如果相似则返回匹配的对象DMatch,否则返回false。
- 当两幅图像判定相似后,则不允许上传,并且通过返回的DMatch对象对两幅图像疑似相似特征进行可视化展示,向用户说明情况。如果返回的是false,则读取下一幅图像特征,直到所有图片匹配完成且都没有相似情况,则过滤完成,图像可以上传。
特征保存
存储设计如下:
在对图像进行确权前需要通过IPFS系统将图像特征进行保存,并得到图像特征的哈希地址这一版权信息。为了方便图像特征的保存和读取设计实现了如下流程:
- 将已经提取的图像的关键点统一存储在列表的逻辑结构,对每一个KeyPoint类的关键点读取其属性值,转化为字典结构的点的类型。
- 将转化后的关键点及其特征向量分别保存在以keypoints和descriptor为键值名的两对键值对中。
- 通过IPFS客户端接口add_json将特征以json格式上传至IPFS系统,并得到IPFS基于内容哈希得到的链接地址PictureHash。读取特征时,通过IPFS客户端的接口get_json读取json格式的已上传图片的特征。之后将特征还原构造,过程为上述过程的逆过程。
程序及参考
https://www.cnblogs.com/alexme/p/11345701.html
https://www.cnblogs.com/ronny/p/4081362.html
https://blog.csdn.net/gukedream/article/details/87040212
https://www.cnblogs.com/alexme/p/11353137.html
https://www.cnblogs.com/alexme/p/11345951.html
https://blog.csdn.net/yang843061497/article/details/38553765
import cv2
import matplotlib.pyplot as plt
import numpy as np
#为了在pycharm中输出全
np.set_printoptions(threshold=np.inf)
#计算图像特征
#输入读入的图像
#输出特征点和对应的描述子
def orb(img):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#这里的参数都可以改
orb = cv2.ORB_create(500, 2.0)
keypoints, descriptor = orb.detectAndCompute(img_gray, None)
feature={"keypoints":keypoints,"descriptor":descriptor}
return feature
#这里转成了字典json的形式
#这里涉及到keypoint类,参考:
#https://docs.opencv.org/2.4.9/modules/features2d/doc/common_interfaces_of_feature_detectors.html#keypoint
#https://blog.csdn.net/u010821666/article/details/52883580
#https://docs.opencv.org/2.4.9/modules/features2d/doc/common_interfaces_of_feature_detectors.html#Point2f%20pt
def changeFormat(feature):
keypoints=feature['keypoints']
descriptor=feature['descriptor']
keypoint=[]
for i in keypoints:
keypoint.append({"pt":i.pt,"size":i.size,"angle":i.angle,"response":i.response,"octave":i.octave,"class_id":i.class_id})
feature={"keypoints":keypoint,"descriptor":descriptor.tolist()}
return feature
#读入保存的特征
def backFormat(charater):
keypoint = []
for i in charater['keypoints']:
keypoint.append(
cv2.KeyPoint(x=i['pt'][0], y=i['pt'][1], _size=i['size'], _angle=i['angle'], _response=i['response'],
_octave=i['octave'], _class_id=i['class_id']))
feature = {"keypoints": keypoint, "descriptor": np.array(charater['descriptor'], dtype='uint8')}
return feature
#两张图相似度的比较,不相似输出false,相似输出匹配,参考
#http://www.woshicver.com/Sixth/5_9_%E7%89%B9%E5%BE%81%E5%8C%B9%E9%85%8D/
#https://cloud.tencent.com/developer/article/1470407
#https://www.jb51.cc/python/185807.html
#https://blog.csdn.net/weixin_44072651/article/details/89262277
#https://www.cnblogs.com/Lin-Yi/p/9433942.html
#https://blog.csdn.net/weixin_37565420/article/details/79090644
def contrast(feature1,feature2):
# bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck = True)
# matches = bf.match(feature1['descriptor'], feature2['descriptor'])
#与上不同,基于KNN的
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
matches = bf.knnMatch(feature1['descriptor'], trainDescriptors=feature2['descriptor'], k=2)
# print(matches)
matches= [m for (m, n) in matches if m.distance < 0.8 * n.distance]
# print(matches)
print(len(matches) / len(feature1['keypoints']))
if(len(matches)/max(len(feature1['keypoints']),len(feature2))>0.1):
return matches
else:
return False
#一个可视化的展示
def showMore(matches,feature1,feature2,img1,img2):
img_gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img_gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
result = cv2.drawMatches(img_gray1, feature1['keypoints'], img_gray2, feature2['keypoints'], matches, None,
flags=2)
cv2.imwrite('media/similar/63.jpg',result)
# plt.imshow(result)
# plt.show()
图像上传
上传采用element-ui的组件el-upload,通过action上传,on-success接收后端的处理结果,on-success绑定upload函数
<el-upload
action="http://192.168.43.10:8000/upload/"
:data="pictureData"
list-type="picture-card"
:on-success="upload"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<i class="el-icon-plus"></i>
</el-upload>
//该函数通过后端返回结果进行判断,如果上传图片与已上传图片有相似,展示相似图片
upload(response, file, fileList){
// location.reload()
console.log(response,file, fileList);
const status = response.status;
if (status === 1) {
this.$message({
message: response.msg,
showClose: true,
type: "success",
});
} else if(status===2){
this.$message({
message: response.msg,
showClose: true,
type: "warning",
});
//为了防止缓存导致不刷新,请求时通过Math.random()加入一个随机数
this.similar= this.baseURL+'media/'+response.similar+'?'+Math.random()
this.DialogSimilarVisible=true
}else{
this.$message({
message: response.msg,
showClose: true,
type: "warning",
});
}
},
后端的处理过程
def upload(request):
#得到上传的图片
#cv2从图片文件流获取image对象:https://www.imooc.com/wenda/detail/549505
data=request.FILES.get('file')
if not data:
return JsonResponse({'status':0,'msg':'图片不存在'})
#得到图片
fileNPArray = np.fromstring(data.read(), np.uint8)
img = cv2.imdecode(fileNPArray, cv2.IMREAD_UNCHANGED)
#得到图片各属性
sessionID=request.POST.get('OwnerID')
session = Session.objects.get(pk=sessionID)
OwnerID=session.get_decoded()['username']
# PictureHash=phash.phash(img)
feature2=orb.orb(img)
name=data.name
UploadTime=int(time.time())
#过滤相似图片
obj_images = image.objects.all().values('PictureHash','name')
client = ipfshttpclient.connect('/dns/localhost/tcp/5001/http')
images=list(obj_images)
for f in images:
charater = client.get_json(f['PictureHash'])
feature1 = orb.backFormat(charater)
matche=orb.contrast(feature2,feature1)
print(matche)
if(matche!=False):
print(555)
# img1=cv2.imread('media/'+f['name'],cv2.IMREAD_UNCHANGED)
#读中文文件名
img1 = cv2.imdecode(np.fromfile('media/'+f['name']), cv2.IMREAD_UNCHANGED)
orb.showMore(matche,feature2,feature1,img,img1)
return JsonResponse({'status': 2, 'msg': '图片相似','similar':'similar/63.jpg'})
#特征上传至ipfs
# client = ipfshttpclient.connect('/dns/localhost/tcp/5001/http')
print('22')
feature2 = orb.changeFormat(feature2)
PictureHash= client.add_json(feature2)
#连接区块链
web3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
with open('tool/test.abi') as file:
myabi = json.loads(file.read())
contract = web3.eth.contract(address=address, abi=myabi)
if web3.eth.getBlock(0) is None:
print("连接失败")
elif web3.isConnected():
print("连接成功")
#上传到区块链
tx_hash=contract.functions.Upload(name, PictureHash, OwnerID, UploadTime).transact({'from':web3.eth.accounts[0]})
receipt = web3.eth.getTransactionReceipt(tx_hash)
print(receipt)
print(contract.events.backPictureID().processReceipt(receipt))
PictureID=contract.events.backPictureID().processReceipt(receipt)[0]['args']['']
#上传到数据库
UploadTime=time.asctime(time.localtime(UploadTime))
print(UploadTime)
obj_image=image(PictureID=PictureID,name=str(PictureID)+'_'+name,PictureHash=PictureHash,OwnerID=OwnerID,UpLoadTime=UploadTime)
obj_image.save()
obj_user=userInformation.objects.get(username=OwnerID)
obj_user.UpNumber= obj_user.UpNumber+1
obj_user.save()
#保存图片
fpath=os.path.join(settings.MEDIA_ROOT,str(PictureID)+'_'+name)
print(fpath)
with open(fpath,'wb') as f:
for i in data.chunks():
f.write(i)
#添加可见水印
createwatermark.new_logo(str(PictureID)+'_'+name)
return JsonResponse({'status': 1, 'msg': '上传成功'})
如图所示,即使图片被裁减,也能判定相似,阻止上传
图像下载
当图像下载时,为了防止侵权行为的发生,需要将下载记录保存在区块链中。同时为了更好保护图片所有者的利益,下载者需要支付所有者为图片设置的金额后才能下载使用图片。具体过程设计如下:
- 前端生成uuid,产生订单信息,后端调用智能合约查询账户余额与图片价格进行对比,余额不足则禁止下载,反之进入下一步。
- 得到用户编号UserID,下载图片编号PictureID,下载时间DownTime,通过智能合约保存在区块链中,保存为两层映射,通过下载编号映射到下载者,下载者为一个结构体,包含下载者编号和下载时间。再通过PictureID映射到该映射,具体设计见4.3节智能合约的设计部分。最后将相关信息同步至数据库保存。
- 根据下载用户信息和图片信息生成水印,并嵌入到图像中,再返回给用户
前端
download(PictureID){
var that = this;
that.axios
.post(that.baseURL + "download/",{
sessionID: sessionStorage.sessionID,
PictureID:PictureID,
uuid:guid()
})
.then((response) => {
const status = response.data.status;
if (status === 1) {
that.$message({
message: response.data.msg,
showClose: true,
type: "success",
});
//图片的下载
let link = document.createElement('a')
let url = that.baseURL+'media/'+response.data.url //codeIMG 要下载的路径
fetch(url).then(res => res.blob()).then(blob=>{
link.href = URL.createObjectURL(blob)
console.log(link.href)
link.download = 'img'
document.body.appendChild(link)
link.click()
})
} else {
that.$message({
message: response.data.msg,
showClose: true,
type: "warning",
});
}
})
.catch(function (err) {
that.$message({
message: err,
showClose: true,
type: "warning",
});
});
},
后端
def download(request):
#连接区块链
web3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
with open('tool/test.abi') as file:
myabi = json.loads(file.read())
contract = web3.eth.contract(address=address, abi=myabi)
if web3.eth.getBlock(0) is None:
print("连接失败")
elif web3.isConnected():
print("连接成功")
#得到信息
data=json.loads(request.body.decode('utf-8'))
PictureID=data['PictureID']
sessionID=data['sessionID']
session = Session.objects.get(pk=sessionID)
UserID=session.get_decoded()['username']
uuid=data['uuid']
DownTime = int(time.time())
obj_img = image.objects.get(PictureID=PictureID)
amount=obj_img.interest
#验证余额
remains=contract.functions.GetMoney(UserID).call()
if(remains/100<Decimal(amount)):
return JsonResponse({'status': 0, 'msg': '余额不足'})
#保存订单信息
print(PictureID)
print(UserID)
print(time.asctime(time.localtime(DownTime)))
obj_orders=orders(uuid=uuid,status=1,kind=0,PictureID=PictureID,DownID=UserID,amount=amount,Time=time.asctime(time.localtime(DownTime)))
obj_orders.save()
#更改下载者信息
obj_user=userInformation.objects.get(username=UserID)
amount=obj_orders.amount
obj_user.remain=obj_user.remain-amount
obj_user.DownNumber=obj_user.DownNumber+1
obj_user.save()
#更改图片信息
obj_image=image.objects.get(PictureID=PictureID)
obj_image.DownNumber=obj_image.DownNumber+1
obj_image.save()
#更改受益人信息
obj_user=userInformation.objects.get(username=obj_image.OwnerID)
obj_orders=orders(uuid=uuid+'de',status=1,kind=1,PictureID=PictureID,DownID=obj_image.OwnerID,amount=amount,Time=time.asctime(time.localtime(DownTime)))
obj_orders.save()
obj_user.remain=obj_user.remain+amount
obj_user.interest = obj_user.interest + amount
obj_user.save()
#上传到区块链
tx_hash=contract.functions.tran(UserID,obj_image.OwnerID,int(amount*100)).transact({'from':web3.eth.accounts[0]})
print(tx_hash)
#上传到区块链
contract.functions.Download(int(PictureID), DownTime,UserID).transact({'from':web3.eth.accounts[0]})
#生成水印
obj_img = image.objects.get(PictureID=PictureID)
word=str(PictureID)+'\n'+str(obj_img.OwnerID)+'\n'+str(time.asctime(time.localtime(DownTime)))+'\n'+str(UserID)
createwatermark.new_mark(word)
img=cv2.imread('tool/watermark/watermark.png',cv2.IMREAD_UNCHANGED)
#嵌入水印
mark=arnold.en_arnold(img)
fpath=os.path.join(settings.MEDIA_ROOT,str(obj_img.name))
# img = cv2.imread(fpath, cv2.IMREAD_UNCHANGED)
print(fpath)
img = cv2.imdecode(np.fromfile(fpath), cv2.IMREAD_UNCHANGED)
watermark.embed(img,mark)
return JsonResponse({'status': 1, 'msg': '下载成功','url':'downimg/2.jpg'})
侵权判定过程
图像侵权判定主要是通过分析疑似侵权图片和疑似侵权信息,查询区块链上相应记录来判断是否存在侵权行为。
-
通过疑似侵权行为判定侵权
当管理员得到疑似侵权者编号UserID和疑似侵权图像编号PictureID后,可以通过智能合约查询区块链上保存的下载记录。通过遍历,查找是否存在该UserID的图片下载记录。如果不存在则返回false,如果存在,则返回下载时间DownTime。
-
通过疑似侵权图片判定侵权
- 如果得到了疑似侵权图片,则可以上传该图片,通过水印算法提取水印,然后根据水印中下载者编号UserID和图像编号PictureID查询区块链上记录。
- 如果得到了疑似侵权图片但无法得到水印,则同时将图片转为灰度图像后,通过ORB算法提取该图像特征。然后遍历所有已上传图像特征保存的地址PictureHash,通过IPFS读取该特征后进行比较,过程与图像过滤类似。最终得到所有可能侵权图片的版权信息,最后根据这些信息通过智能合约查询区块链上保存的下载记录进行侵权判定。
版权判定页
上传侵权图片,自动通过特征寻找相似图片,并显示版权信息
进行侵权判定
相关代码实现:
#根据特征寻找相似图像
def uploadRight(request):
data=request.FILES.get('file')
if not data:
return JsonResponse({'status':0,'msg':'图片不存在'})
#得到图片
fileNPArray = np.fromstring(data.read(), np.uint8)
img = cv2.imdecode(fileNPArray, cv2.IMREAD_UNCHANGED)
#提取水印
try:
water_img=watermark.extract(img)
flag=arnold.de_arnold(water_img)
except:
pass
#相似图片
similar_img=[]
feature2=orb.orb(img)
obj_images = image.objects.all().values()
client = ipfshttpclient.connect('/dns/localhost/tcp/5001/http')
images=list(obj_images)
for f in images:
charater = client.get_json(f['PictureHash'])
feature1 = orb.backFormat(charater)
matche=orb.contrast(feature1,feature2)
if(matche!=False):
similar_img.append(f)
flag=True
if(flag==True):
return JsonResponse({'status': 1, 'msg': '提取对比成功', 'src': 'downimg/watermark.png','similarPicture':similar_img})
else:
return JsonResponse({'status': 0, 'msg': "出错了"})
#查询区块链上下载记录
def search(request):
data=json.loads(request.body.decode('utf-8'))
PictureID=data['pictureID']
UserID=data['userID']
print(PictureID)
print(UserID)
#连接区块链
web3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))
with open('tool/test.abi') as file:
myabi = json.loads(file.read())
contract = web3.eth.contract(address=address, abi=myabi)
if web3.eth.getBlock(0) is None:
print("连接失败")
elif web3.isConnected():
print("连接成功")
flag,result=contract.functions.judge(int(PictureID), UserID).call()
print(flag)
print(result)
if(flag==False):
return JsonResponse({'status': 0, 'msg': "没有侵权"})
else:
DownTime = time.asctime(time.localtime(int(result)))
return JsonResponse({'status': 1, 'msg': "存在侵权",'time':DownTime})
其它内容
z字扫描算法实现
def z_scan(dct_array):
size, size2 = dct_array.shape
dct_list = []
i = 0
j = 0
count = 0
dct_list.append(dct_array[i][j])
count = count + 1
while (count < size * size2):
# 先向上扫
while (i - 1 >= 0 and j + 1 < size):
j = j + 1
i = i - 1
dct_list.append(dct_array[i][j])
count = count + 1
# 扫到头右移
if (j + 1 < size):
j = j + 1
dct_list.append(dct_array[i][j])
count = count + 1
else:
i = i + 1
dct_list.append(dct_array[i][j])
count = count + 1
# 向下扫
while (j - 1 >= 0 and i + 1 < size):
i = i + 1
j = j - 1
dct_list.append(dct_array[i][j])
count = count + 1
# 扫到头下移
if (i + 1 < size):
i = i + 1
dct_list.append(dct_array[i][j])
count = count + 1
else:
j = j + 1
dct_list.append(dct_array[i][j])
count = count + 1
return dct_list
element-ui布局参考
https://www.freesion.com/article/110081469/
使用vue-cli编译时报错
Couldn't parse bundle asset
更新node版本,可能是vue版本冲突
django配置mysql报错
解决方案:https://www.cnblogs.com/jiaoyang77/p/9333424.html
django跨域解决
https://www.cnblogs.com/lowmanisbusy/p/9589432.html
生成11位随机编号的代码
rand = "{0:0>11}".format(random.randint(0, 99999999999))