├── LICENSE
├── README.md
└── docs
├── css
└── style.css
├── index.html
└── js
├── item-extract.js
└── opencv.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 GaHing
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > 写于 2019 年,注意时效性
2 |
3 | ## 介绍
4 |
5 | 基于 opencv.js 实现矩形抽离的纯前端项目
6 |
7 | 
8 |
9 | ## 在线地址
10 |
11 | [rectangle-extract-opencvjs](https://francecil.github.io/rectangle-extract-opencvjs/)
12 |
13 | ## 实现思路
14 |
15 | 详见 [基于 opencv.js 实现图像内矩形物品的自动提取](https://juejin.im/post/5e268bd86fb9a0300d61a0c7)
16 |
--------------------------------------------------------------------------------
/docs/css/style.css:
--------------------------------------------------------------------------------
1 | /* display loading gif and hide webpage */
2 | .modal {
3 | display: none;
4 | position: fixed;
5 | z-index: 1000;
6 | top: 0;
7 | left: 0;
8 | height: 100%;
9 | width: 100%;
10 | background: rgba( 255, 255, 255, .8)
11 | url('http://i.stack.imgur.com/FhHRx.gif')
12 | 50% 50%
13 | no-repeat;
14 | }
15 |
16 | /* prevent scrollbar from display during load */
17 | body.loading {
18 | overflow: hidden;
19 | }
20 |
21 | /* display the modal when loading class is added to body */
22 | body.loading .modal {
23 | display: block;
24 | }
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 矩形提取
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
OpenCV - 矩形提取
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
36 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
53 |
54 |
55 |
56 |
57 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/docs/js/item-extract.js:
--------------------------------------------------------------------------------
1 | const g_nLowDifference = 35
2 | const g_nUpDifference = 35; //负差最大值、正差最大值
3 | const UNCAL_THETA = 0.5;
4 | class Line {
5 | constructor(rho, theta) {
6 | this.rho = rho
7 | this.theta = theta
8 | let a = Math.cos(theta);
9 | let b = Math.sin(theta);
10 | let x0 = a * rho;
11 | let y0 = b * rho;
12 | this.startPoint = { x: x0 - 400 * b, y: y0 + 400 * a };
13 | this.endPoint = { x: x0 + 400 * b, y: y0 - 400 * a };
14 | }
15 | }
16 | /**
17 | * @param {Object} srcMat
18 | */
19 | function itemExtract (srcMat, name) {
20 | let scale = getScale(Math.max(srcMat.rows, srcMat.cols))
21 | let preMat = preProcess(srcMat, scale)
22 | let grayMat = getSegmentImage(preMat)
23 | let lines = getLinesWithDetect(grayMat)
24 | let points = getFourVertex(lines, scale, { height: srcMat.rows, width: srcMat.cols })
25 | let result = getResultWithMap(srcMat, points)
26 | cv.imshow(name, result);
27 | preMat.delete()
28 | grayMat.delete()
29 | srcMat.delete()
30 | result.delete()
31 | }
32 | /**
33 | * 获取缩放比例
34 | * @param {*} len
35 | */
36 | function getScale (len) {
37 | let scale = 1
38 | while (len > 200) {
39 | scale /= 2
40 | len >>= 1
41 | }
42 | return scale
43 | }
44 | /**
45 | * 预处理
46 | * @param {*} src
47 | */
48 | function preProcess (src, scale) {
49 | let smallMat = resize(src, scale)
50 | let result = filter(smallMat)
51 | smallMat.delete()
52 | return result
53 | }
54 | /**
55 | * 调整至指定宽高
56 | * @param {*} src
57 | * @param {*} scale 缩放比例
58 | */
59 | function resize (src, scale = 1) {
60 | let smallMat = new cv.Mat();
61 | let dsize = new cv.Size(0, 0);
62 | cv.resize(src, smallMat, dsize, scale, scale, cv.INTER_AREA)
63 | return smallMat
64 | }
65 | /**
66 | * 滤波:保边去噪
67 | * @param {*} mat
68 | */
69 | function filter (src) {
70 | let dst = new cv.Mat();
71 | cv.cvtColor(src, src, cv.COLOR_RGBA2RGB, 0);
72 | // 双边滤波
73 | cv.bilateralFilter(src, dst, 9, 75, 75, cv.BORDER_DEFAULT);
74 | return dst
75 | }
76 | /**
77 | * 通过分割图像获取前景灰度图
78 | * @param {*} src
79 | */
80 | function getSegmentImage (src) {
81 | const mask = new cv.Mat(src.rows + 2, src.cols + 2, cv.CV_8U, [0, 0, 0, 0])
82 | const seed = new cv.Point(src.cols >> 1, src.rows >> 1)
83 | let flags = 4 + (255 << 8) + cv.FLOODFILL_FIXED_RANGE
84 | let ccomp = new cv.Rect()
85 | let newVal = new cv.Scalar(255, 255, 255)
86 | // 选取中点,采用floodFill漫水填充
87 | cv.threshold(mask, mask, 1, 128, cv.THRESH_BINARY);
88 | cv.floodFill(src, mask, seed, newVal, ccomp, new cv.Scalar(g_nLowDifference, g_nLowDifference, g_nLowDifference), new cv.Scalar(g_nUpDifference, g_nUpDifference, g_nUpDifference), flags);
89 | // 再次执行一次滤波去除噪点
90 | cv.medianBlur(mask, mask, 9);
91 | return mask
92 | }
93 |
94 |
95 | function getLinesFromData32F (data32F) {
96 | let lines = []
97 | let len = data32F.length / 2
98 | for (let i = 0; i < len; ++i) {
99 | let rho = data32F[i * 2];
100 | let theta = data32F[i * 2 + 1];
101 | lines.push(new Line(rho, theta))
102 | }
103 | return lines
104 | }
105 | /**
106 | * 直线检测
107 | * @param {*} mat
108 | */
109 | function getLinesWithDetect (src) {
110 | let dst = cv.Mat.zeros(src.rows, src.cols, cv.CV_8UC3);
111 | let lines = new cv.Mat();
112 | // Canny 算子进行边缘检测
113 | cv.Canny(src, src, 50, 200, 3);
114 | cv.HoughLines(src, lines, 1, Math.PI / 180,
115 | 30, 0, 0, 0, Math.PI);
116 | // draw lines
117 | for (let i = 0; i < lines.rows; ++i) {
118 | let rho = lines.data32F[i * 2];
119 | let theta = lines.data32F[i * 2 + 1];
120 | let a = Math.cos(theta);
121 | let b = Math.sin(theta);
122 | let x0 = a * rho;
123 | let y0 = b * rho;
124 | let startPoint = { x: x0 - 400 * b, y: y0 + 400 * a };
125 | let endPoint = { x: x0 + 400 * b, y: y0 - 400 * a };
126 | cv.line(dst, startPoint, endPoint, [255, 0, 0, 255]);
127 | }
128 | let lineArray = getLinesFromData32F(lines.data32F)
129 | // drawLineMat(src.rows, src.cols, lineArray)
130 | return lineArray
131 | }
132 | /**
133 | * 计算两直线间的交点
134 | * @param {*} l1
135 | * @param {*} l2
136 | */
137 | function getIntersection (l1, l2) {
138 | //角度差太小 不算,
139 | let minTheta = Math.min(l1.theta, l2.theta)
140 | let maxTheta = Math.max(l1.theta, l2.theta)
141 | if (Math.abs(l1.theta - l2.theta) < UNCAL_THETA || Math.abs(minTheta + Math.PI - maxTheta) < UNCAL_THETA) {
142 | return;
143 | }
144 | //计算两条直线的交点
145 | let intersection;
146 | //y = a * x + b;
147 | let a1 = Math.abs(l1.startPoint.x - l1.endPoint.x) < Number.EPSILON ? 0 : (l1.startPoint.y - l1.endPoint.y) / (l1.startPoint.x - l1.endPoint.x);
148 | let b1 = l1.startPoint.y - a1 * (l1.startPoint.x);
149 | let a2 = Math.abs((l2.startPoint.x - l2.endPoint.x)) < Number.EPSILON ? 0 : (l2.startPoint.y - l2.endPoint.y) / (l2.startPoint.x - l2.endPoint.x);
150 | let b2 = l2.startPoint.y - a2 * (l2.startPoint.x);
151 | if (Math.abs(a2 - a1) > Number.EPSILON) {
152 | let x = (b1 - b2) / (a2 - a1)
153 | let y = a1 * x + b1
154 | intersection = { x, y }
155 | }
156 | return intersection
157 | }
158 | /**
159 | * 计算所有交点
160 | * @param {*} lines
161 | */
162 | function getAllIntersections (lines) {
163 | let points = []
164 | for (let i = 0; i < lines.length; i++) {
165 | for (let j = i + 1; j < lines.length; j++) {
166 | let point = getIntersection(lines[i], lines[j])
167 | if (point) {
168 | points.push(point)
169 | }
170 | }
171 | }
172 | return points
173 | }
174 | /**
175 | * 聚类取均值
176 | * @param {*} points
177 | * @param {*} param1
178 | */
179 | function getClusterPoints (points, { width, height }) {
180 | const DISTANCE = Math.max(40, (width + height) / 20)
181 | const isNear = (p1, p2) => Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) < DISTANCE
182 | // 多边形中心点坐标
183 | const center = {
184 | x: points.reduce((sum, p) => sum + p.x, 0) / points.length,
185 | y: points.reduce((sum, p) => sum + p.y, 0) / points.length
186 | }
187 | points.sort((p1,p2)=>{
188 | // y=kx theta = atan(k)
189 | // TODO cache calc
190 | const theta1 = Math.atan((p1.y-center.y)/((p1.x-center.x) || 0.01))
191 | const theta2 = Math.atan((p2.y-center.y)/((p2.x-center.x) || 0.01))
192 | return theta1 - theta2
193 | })
194 |
195 |
196 | let clusters = [[points[0]]]
197 | for (let i = 1; i < points.length; i++) {
198 | if (isNear(points[i], points[i - 1])) {
199 | clusters[clusters.length - 1].push(points[i])
200 | } else {
201 | clusters.push([points[i]])
202 | }
203 | }
204 | // 除去量最少的,仅保留四个聚类
205 | clusters = clusters.sort((c1, c2) => c2.length - c1.length).slice(0, 4)
206 | const result = clusters.map(cluster => {
207 | const x = ~~(cluster.reduce((sum, cur) => sum + cur.x, 0) / cluster.length)
208 | const y = ~~(cluster.reduce((sum, cur) => sum + cur.y, 0) / cluster.length)
209 | return { x, y }
210 | })
211 | return result
212 | }
213 | /**
214 | * 顺时针排序,以中心点左上角为第一个点
215 | * @param {*} points
216 | */
217 | function getSortedVertex (points) {
218 | let center = {
219 | x: points.reduce((sum, p) => sum + p.x, 0) / 4,
220 | y: points.reduce((sum, p) => sum + p.y, 0) / 4
221 | }
222 | let sortedPoints = []
223 | sortedPoints.push(points.find(p => p.x < center.x && p.y < center.y))
224 | sortedPoints.push(points.find(p => p.x > center.x && p.y < center.y))
225 | sortedPoints.push(points.find(p => p.x > center.x && p.y > center.y))
226 | sortedPoints.push(points.find(p => p.x < center.x && p.y > center.y))
227 | return sortedPoints
228 | }
229 |
230 | /**
231 | * 根据聚类获得四个顶点的坐标
232 | */
233 | function getFourVertex (lines, scale, { width, height }) {
234 | // 缩放 + 过滤
235 | let allPoints = getAllIntersections(lines).map(point => ({
236 | x: ~~(point.x / scale), y: ~~(point.y / scale)
237 | })).filter(({ x, y }) => !(x < 0 || x > width || y < 0 || y > height))
238 | const points = getClusterPoints(allPoints, { width, height })
239 | const sortedPoints = getSortedVertex(points)
240 | return sortedPoints
241 | }
242 | /**
243 | * 抠图,映射
244 | * @param {*} src
245 | * @param {*} points
246 | */
247 | function getResultWithMap (src, points) {
248 | let array = []
249 | points.forEach(point => {
250 | array.push(point.x)
251 | array.push(point.y)
252 | })
253 | // console.log(points, array)
254 | let dst = new cv.Mat();
255 | let dsize = new cv.Size(0, 0);
256 | let dstWidth = src.cols
257 | let dstHeight = src.rows
258 | let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, array);
259 | let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, dstWidth, 0, dstWidth, dstHeight, 0, dstHeight]);
260 | let M = cv.getPerspectiveTransform(srcTri, dstTri);
261 | cv.warpPerspective(src, dst, M, dsize);
262 | let resizeDst = resize(dst, 0.5)
263 | M.delete(); srcTri.delete(); dstTri.delete(); dst.delete()
264 | return resizeDst
265 | }
266 | function drawLineMat (rows, cols, lines) {
267 | let dst = cv.Mat.zeros(rows, cols, cv.CV_8UC3);
268 | let color = new cv.Scalar(255, 0, 0);
269 | for (let line of lines) {
270 | cv.line(dst, line.startPoint, line.endPoint, color);
271 | }
272 | cv.imshow("canvasOutput", dst);
273 | }
--------------------------------------------------------------------------------