소스 검색

Initial revision.

Andrew Klopper 6 년 전
커밋
4de086ce4f

+ 253 - 0
index.html

@@ -0,0 +1,253 @@
1
+<!DOCTYPE html>
2
+<html>
3
+    <head>
4
+        <meta charset="UTF-8">
5
+        <script src="js/vendor/interact/interact.min.js"></script>
6
+        <script src="js/vendor/opencv/opencv.js"></script>
7
+        <script src="js/vendor/load-image/load-image.all.min.js"></script>
8
+        <script src="js/vendor/plotly/plotly-latest.min.js"></script>
9
+        <script src="js/vendor/panzoom/panzoom.js"></script>
10
+        <script src="js/xword.js"></script>
11
+        <style>
12
+            canvas {
13
+                width: 400px;
14
+            }
15
+            #sourceImageContainer {
16
+                width: 400px;
17
+                overflow: hidden;
18
+            }
19
+            #sourceImage {
20
+                position: relative;
21
+                width: min-content;
22
+            }
23
+            #sourceImage canvas {
24
+                width: auto;
25
+            }
26
+            #sourceImage .overlay {
27
+                position: absolute;
28
+                left: 0;
29
+                top: 0;
30
+            }
31
+            .handle {
32
+                position: absolute;
33
+                box-sizing: border-box;
34
+                width: 301px;
35
+                height: 301px;
36
+                border-radius: 50%;
37
+                border: 6px solid red;
38
+                transform: translate(-50%, -50%);
39
+            }
40
+            #graph {
41
+                width: 1000px;
42
+                height: 800px;
43
+            }
44
+        </style>
45
+    </head>
46
+    <body>
47
+        <div>
48
+            <input type="file" id="fileInput">
49
+        </div>
50
+        <div id="sourceImageContainer">
51
+            <div id="sourceImage"></div>
52
+        </div>
53
+        <button id="recalculateButton">Recalculate</button>
54
+        <div>
55
+            <canvas id="output"></canvas>
56
+        </div>
57
+        <div id="graph"></div>
58
+        <script>
59
+            'use strict';
60
+
61
+            let sourceImage = document.getElementById("sourceImage");
62
+            let sourceImageParent = sourceImage.parentElement;
63
+            let fileInput = document.getElementById("fileInput");
64
+
65
+            let isSquare = true;
66
+
67
+            let gaussianBlurSize = 11;
68
+            let adaptiveThresholdBlockSize = 11;
69
+            let adaptiveThresholdMeanAdjustment = 2;
70
+            let numDilations = 1;
71
+
72
+            let contourErosionKernelSize = 5;
73
+            let contourErosionIterations = 5;
74
+
75
+            let lineDetectorElementSize = 51;
76
+
77
+            let samplingBlockSizeRatio = 0.25;
78
+            let samplingThresholdQuantile = 0.3;
79
+            let samplingThreshold = null;
80
+
81
+            let gridLineThickness = 4;
82
+            let gridSquareInternalSize = 64;
83
+            let gridMarginSize = 20;
84
+
85
+            let img = null;
86
+            let zoomCallback = null;
87
+
88
+            let zoomer = Panzoom(sourceImage, {
89
+                excludeClass: 'handle'
90
+            });
91
+            sourceImage.addEventListener('panzoomzoom', function(event) {
92
+                if (zoomCallback != null) zoomCallback(event);
93
+            });
94
+            
95
+            sourceImageParent.addEventListener('wheel', function(event) {
96
+                if (! event.shiftKey) return;
97
+                zoomer.zoomWithWheel(event, {disablePan: true});
98
+            });
99
+
100
+            fileInput.onchange = function(e) {
101
+                if (e.target.files.length > 0) {
102
+                    loadImage(
103
+                        e.target.files[0],
104
+                        function(canvas) {
105
+                            let imageWidth = canvas.width;
106
+                            let imageHeight = canvas.height;
107
+                            let imageScale = sourceImageParent.offsetWidth / imageWidth;
108
+                            sourceImageParent.style.height = Math.round(imageHeight * imageScale) + 'px';
109
+
110
+                            if (img != null) {
111
+                                img.delete();
112
+                                img = null;
113
+                            }
114
+
115
+                            while (sourceImage.firstChild) {
116
+                                sourceImage.removeChild(sourceImage.firstChild);
117
+                            }
118
+                            sourceImage.appendChild(canvas);
119
+
120
+                            zoomer.setOptions({
121
+                                minScale: imageScale,
122
+                                maxScale: 1
123
+                            });
124
+                            zoomer.zoomToPoint(imageScale, {clientX: sourceImageParent.offsetLeft, clientY: sourceImageParent.offsetTop});
125
+
126
+                            let overlay = document.createElement('canvas');
127
+                            overlay.className = 'overlay';
128
+                            overlay.width = imageWidth;
129
+                            overlay.height = imageHeight;
130
+                            sourceImage.appendChild(overlay);
131
+
132
+                            let context = overlay.getContext('2d');
133
+                            context.globalAlpha = 0.6;
134
+                            context.strokeStyle = 'red';
135
+
136
+                            let handles = [];
137
+
138
+                            function getNewCorners() {
139
+                                let corners = [];
140
+                                handles.forEach(function(handle) {
141
+                                    corners.push(new cv.Point(
142
+                                        parseFloat(handle.dataset.left),
143
+                                        parseFloat(handle.dataset.top)
144
+                                    ));
145
+                                });
146
+                                return corners;
147
+                            }
148
+
149
+                            function redrawOverlay() {
150
+                                let corners = getNewCorners();
151
+                                context.lineWidth = 1 / zoomer.getScale();
152
+                                context.clearRect(0, 0, imageWidth, imageHeight);
153
+                                context.beginPath();
154
+                                let corner = corners[corners.length - 1];
155
+                                context.moveTo(corner.x, corner.y);
156
+                                corners.forEach(function(corner) {
157
+                                    context.lineTo(corner.x, corner.y);
158
+                                });
159
+                                context.stroke();
160
+                            }
161
+
162
+                            function dragMoveListener(event) {
163
+                                let target = event.target;
164
+                                let newLeft = Math.min(
165
+                                    imageWidth - 1,
166
+                                    Math.max(0, parseFloat(target.dataset.left) + event.dx / zoomer.getScale())
167
+                                );
168
+                                let newTop = Math.min(
169
+                                    imageHeight - 1,
170
+                                    Math.max(0, parseFloat(target.dataset.top) + event.dy / zoomer.getScale())
171
+                                );
172
+                                target.dataset.left = newLeft;
173
+                                target.dataset.top = newTop;
174
+                                target.style.left = newLeft + 'px';
175
+                                target.style.top = newTop + 'px';
176
+                                redrawOverlay();
177
+                            }
178
+
179
+                            function recalculate() {
180
+                                let square = extractSquare(img, getNewCorners());
181
+                                try {
182
+                                    let numRows = getLineFrequency(square, lineDetectorElementSize, 1);
183
+                                    let numCols = getLineFrequency(square, lineDetectorElementSize, 0);
184
+                                    if (isSquare && (numRows !== numCols)) {
185
+                                        console.log("WARNING: crossword is not square");
186
+                                    }
187
+                                    cv.imshow('output', square);
188
+                                } finally {
189
+                                    square.delete();
190
+                                }
191
+                            }
192
+
193
+                            document.getElementById('recalculateButton').onclick = recalculate;
194
+                            zoomCallback = function(event) {
195
+                                redrawOverlay();
196
+                            };
197
+
198
+                            let src = cv.imread(canvas);
199
+                            try {
200
+                                //removeColours(src, src, 48);
201
+                                //cv.imshow('output', src);
202
+
203
+                                cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
204
+
205
+                                img = new cv.Mat();
206
+                                preprocessImage(src, img, gaussianBlurSize, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment, numDilations);
207
+                                //cv.imshow('output', img);
208
+                                let corners;
209
+                                let biggest = findBiggestContour(img);
210
+                                try {
211
+                                    let eroded = erodeContour(img.size(), biggest, contourErosionKernelSize, contourErosionIterations);
212
+                                    try {
213
+                                        corners = getContourCorners(img.size(), eroded);
214
+                                    } finally {
215
+                                        eroded.delete();
216
+                                    }
217
+                                } finally {
218
+                                    biggest.delete();
219
+                                }
220
+
221
+                                corners.forEach(function(corner) {
222
+                                    let handle = document.createElement('div');
223
+                                    handle.className = 'handle';
224
+                                    handle.dataset.left = corner.x;
225
+                                    handle.dataset.top = corner.y;
226
+                                    handle.style.left = handle.dataset.left + 'px';
227
+                                    handle.style.top = handle.dataset.top + 'px';
228
+                                    sourceImage.appendChild(handle);
229
+                                    interact(handle).draggable({
230
+                                        onmove: dragMoveListener
231
+                                    });
232
+                                    handles.push(handle);
233
+                                });
234
+
235
+                                redrawOverlay();
236
+                                recalculate();
237
+
238
+                            } finally {
239
+                                src.delete();
240
+                            }
241
+                        },
242
+                        {
243
+                            orientation: true
244
+                        }
245
+                    );
246
+                }
247
+            };
248
+        </script>
249
+    </body>
250
+</html>
251
+<!--
252
+vim:ts=4:sw=4:expandtab
253
+-->

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 4 - 0
js/vendor/interact/interact.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2 - 0
js/vendor/load-image/load-image.all.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 45 - 0
js/vendor/opencv/opencv.js


+ 164 - 0
js/vendor/opencv/utils.js

@@ -0,0 +1,164 @@
1
+function Utils(errorOutputId) { // eslint-disable-line no-unused-vars
2
+    let self = this;
3
+    this.errorOutput = document.getElementById(errorOutputId);
4
+
5
+    const OPENCV_URL = 'opencv.js';
6
+    this.loadOpenCv = function(onloadCallback) {
7
+        let script = document.createElement('script');
8
+        script.setAttribute('async', '');
9
+        script.setAttribute('type', 'text/javascript');
10
+        script.addEventListener('load', () => {
11
+            if (cv.getBuildInformation)
12
+            {
13
+                console.log(cv.getBuildInformation());
14
+                onloadCallback();
15
+            }
16
+            else
17
+            {
18
+                // WASM
19
+                cv['onRuntimeInitialized']=()=>{
20
+                    console.log(cv.getBuildInformation());
21
+                    onloadCallback();
22
+                }
23
+            }
24
+        });
25
+        script.addEventListener('error', () => {
26
+            self.printError('Failed to load ' + OPENCV_URL);
27
+        });
28
+        script.src = OPENCV_URL;
29
+        let node = document.getElementsByTagName('script')[0];
30
+        node.parentNode.insertBefore(script, node);
31
+    };
32
+
33
+    this.createFileFromUrl = function(path, url, callback) {
34
+        let request = new XMLHttpRequest();
35
+        request.open('GET', url, true);
36
+        request.responseType = 'arraybuffer';
37
+        request.onload = function(ev) {
38
+            if (request.readyState === 4) {
39
+                if (request.status === 200) {
40
+                    let data = new Uint8Array(request.response);
41
+                    cv.FS_createDataFile('/', path, data, true, false, false);
42
+                    callback();
43
+                } else {
44
+                    self.printError('Failed to load ' + url + ' status: ' + request.status);
45
+                }
46
+            }
47
+        };
48
+        request.send();
49
+    };
50
+
51
+    this.loadImageToCanvas = function(url, cavansId) {
52
+        let canvas = document.getElementById(cavansId);
53
+        let ctx = canvas.getContext('2d');
54
+        let img = new Image();
55
+        img.crossOrigin = 'anonymous';
56
+        img.onload = function() {
57
+            canvas.width = img.width;
58
+            canvas.height = img.height;
59
+            ctx.drawImage(img, 0, 0, img.width, img.height);
60
+        };
61
+        img.src = url;
62
+    };
63
+
64
+    this.executeCode = function(textAreaId) {
65
+        try {
66
+            this.clearError();
67
+            let code = document.getElementById(textAreaId).value;
68
+            eval(code);
69
+        } catch (err) {
70
+            this.printError(err);
71
+        }
72
+    };
73
+
74
+    this.clearError = function() {
75
+        this.errorOutput.innerHTML = '';
76
+    };
77
+
78
+    this.printError = function(err) {
79
+        if (typeof err === 'undefined') {
80
+            err = '';
81
+        } else if (typeof err === 'number') {
82
+            if (!isNaN(err)) {
83
+                if (typeof cv !== 'undefined') {
84
+                    err = 'Exception: ' + cv.exceptionFromPtr(err).msg;
85
+                }
86
+            }
87
+        } else if (typeof err === 'string') {
88
+            let ptr = Number(err.split(' ')[0]);
89
+            if (!isNaN(ptr)) {
90
+                if (typeof cv !== 'undefined') {
91
+                    err = 'Exception: ' + cv.exceptionFromPtr(ptr).msg;
92
+                }
93
+            }
94
+        } else if (err instanceof Error) {
95
+            err = err.stack.replace(/\n/g, '<br>');
96
+        }
97
+        this.errorOutput.innerHTML = err;
98
+    };
99
+
100
+    this.loadCode = function(scriptId, textAreaId) {
101
+        let scriptNode = document.getElementById(scriptId);
102
+        let textArea = document.getElementById(textAreaId);
103
+        if (scriptNode.type !== 'text/code-snippet') {
104
+            throw Error('Unknown code snippet type');
105
+        }
106
+        textArea.value = scriptNode.text.replace(/^\n/, '');
107
+    };
108
+
109
+    this.addFileInputHandler = function(fileInputId, canvasId) {
110
+        let inputElement = document.getElementById(fileInputId);
111
+        inputElement.addEventListener('change', (e) => {
112
+            let files = e.target.files;
113
+            if (files.length > 0) {
114
+                let imgUrl = URL.createObjectURL(files[0]);
115
+                self.loadImageToCanvas(imgUrl, canvasId);
116
+            }
117
+        }, false);
118
+    };
119
+
120
+    function onVideoCanPlay() {
121
+        if (self.onCameraStartedCallback) {
122
+            self.onCameraStartedCallback(self.stream, self.video);
123
+        }
124
+    };
125
+
126
+    this.startCamera = function(resolution, callback, videoId) {
127
+        const constraints = {
128
+            'qvga': {width: {exact: 320}, height: {exact: 240}},
129
+            'vga': {width: {exact: 640}, height: {exact: 480}}};
130
+        let video = document.getElementById(videoId);
131
+        if (!video) {
132
+            video = document.createElement('video');
133
+        }
134
+
135
+        let videoConstraint = constraints[resolution];
136
+        if (!videoConstraint) {
137
+            videoConstraint = true;
138
+        }
139
+
140
+        navigator.mediaDevices.getUserMedia({video: videoConstraint, audio: false})
141
+            .then(function(stream) {
142
+                video.srcObject = stream;
143
+                video.play();
144
+                self.video = video;
145
+                self.stream = stream;
146
+                self.onCameraStartedCallback = callback;
147
+                video.addEventListener('canplay', onVideoCanPlay, false);
148
+            })
149
+            .catch(function(err) {
150
+                self.printError('Camera Error: ' + err.name + ' ' + err.message);
151
+            });
152
+    };
153
+
154
+    this.stopCamera = function() {
155
+        if (this.video) {
156
+            this.video.pause();
157
+            this.video.srcObject = null;
158
+            this.video.removeEventListener('canplay', onVideoCanPlay);
159
+        }
160
+        if (this.stream) {
161
+            this.stream.getVideoTracks()[0].stop();
162
+        }
163
+    };
164
+};

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 472 - 0
js/vendor/panzoom/panzoom.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
js/vendor/plotly/plotly-latest.min.js


+ 255 - 0
js/xword.js

@@ -0,0 +1,255 @@
1
+// vim:ts=4:sw=4:expandtab
2
+
3
+'use strict';
4
+
5
+function plot(elementId, seriesList) {
6
+    let plots = [];
7
+    for (let i = 0; i < seriesList.length; i++) {
8
+        let y = seriesList[i];
9
+        let x = [];
10
+        for (let i = 0; i < y.length; i++) {
11
+            x.push(i);
12
+        }
13
+        plots.push({x: x, y: y});
14
+    }
15
+    Plotly.plot(document.getElementById(elementId), plots);
16
+}
17
+
18
+function removeColours(src, dst, threshold) {
19
+    let from = src.data32S;
20
+    let to = dst.data32S;
21
+    let pixels = src.rows * src.cols;
22
+    for (let i = 0; i < pixels; i++) {
23
+        let pixel = from[i];
24
+        let r = pixel & 0xFF;
25
+        let g = (pixel >> 8) & 0xFF;
26
+        let b = (pixel >> 16) & 0xFF;
27
+        to[i] = from[i] | (Math.max(r, g, b) - Math.min(r, g, b) > threshold ? 0xFFFFFF : 0);
28
+    }
29
+}
30
+
31
+function preprocessImage(src, dst, gaussianBlurSize, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment, numDilations) {
32
+    cv.GaussianBlur(src, dst, new cv.Size(gaussianBlurSize, gaussianBlurSize), 0);
33
+    cv.adaptiveThreshold(dst, dst, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment);
34
+    let kernel = cv.getStructuringElement(cv.MORPH_CROSS, new cv.Size(3, 3));
35
+    try {
36
+        cv.dilate(dst, dst, kernel, new cv.Point(-1, -1), numDilations);
37
+    } finally {
38
+        kernel.delete();
39
+    }
40
+}
41
+
42
+function morphOpenImage(src, dst, kernelSize, iterations) {
43
+    let kernel = cv.getStructuringElement(cv.MORPH_RECT, kernelSize);
44
+    try {
45
+        cv.morphologyEx(src, dst, cv.MORPH_OPEN, kernel, new cv.Point(-1, -1), iterations);
46
+    } finally {
47
+        kernel.delete();
48
+    }
49
+}
50
+
51
+function findBiggestContour(img) {
52
+    let contours = new cv.MatVector();
53
+    try {
54
+        let hierarchy = new cv.Mat();
55
+        try {
56
+            cv.findContours(img, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
57
+
58
+            let biggest = null;
59
+            let maxArea = 0;
60
+            for (let i = 0; i < contours.size(); i++) {
61
+                let contour = contours.get(i);
62
+                let area = cv.contourArea(contour);
63
+                if (area > maxArea) {
64
+                    maxArea = area;
65
+                    if (biggest !== null) {
66
+                        biggest.delete();
67
+                    }
68
+                    biggest = contour;
69
+                } else {
70
+                    contour.delete();
71
+                }
72
+            }
73
+            return biggest;
74
+        } finally {
75
+            hierarchy.delete();
76
+        }
77
+    } finally {
78
+        contours.delete();
79
+    }
80
+}
81
+
82
+function erodeContour(imageSize, contour, kernelSize, iterations) {
83
+    let contourImg = cv.Mat.zeros(imageSize.height, imageSize.width, cv.CV_8U);
84
+    try {
85
+        let contours = new cv.MatVector();
86
+        try {
87
+            contours.push_back(contour);
88
+            cv.drawContours(contourImg, contours, 0, new cv.Scalar(255), -1);
89
+        } finally {
90
+            contours.delete();
91
+        }
92
+        morphOpenImage(contourImg, contourImg, new cv.Size(kernelSize, kernelSize), iterations);
93
+        return findBiggestContour(contourImg);
94
+    } finally {
95
+        contourImg.delete();
96
+    }
97
+}
98
+
99
+function getContourCorners(imageSize, contour) {
100
+    let topLeft = new cv.Point(imageSize.width, imageSize.height);
101
+    let topRight = new cv.Point(-1, imageSize.height);
102
+    let bottomLeft = new cv.Point(imageSize.width, -1);
103
+    let bottomRight = new cv.Point(-1, -1);
104
+    for (let i = 0; i < contour.rows; i++) {
105
+        let vertex = new cv.Point(contour.data32S[i * 2], contour.data32S[i * 2 + 1]);
106
+        let sum = vertex.x + vertex.y;
107
+        let diff = vertex.x - vertex.y;
108
+        if (sum < topLeft.x + topLeft.y) {
109
+            topLeft = vertex;
110
+        }
111
+        if (sum > bottomRight.x + bottomRight.y) {
112
+            bottomRight = vertex;
113
+        }
114
+        if (diff < bottomLeft.x - bottomLeft.y) {
115
+            bottomLeft = vertex;
116
+        }
117
+        if (diff > topRight.x - topRight.y) {
118
+            topRight = vertex;
119
+        }
120
+    }
121
+    return [topLeft, topRight, bottomRight, bottomLeft];
122
+}
123
+
124
+function segmentLength(p1, p2) {
125
+    let dx = p1.x - p2.x;
126
+    let dy = p1.y - p2.y;
127
+    return Math.sqrt(dx ** 2 + dy ** 2);
128
+}
129
+
130
+function getLongestSide(corners) {
131
+    let previous = corners[corners.length - 1];
132
+    let max = 0;
133
+    for (let i = 0; i < corners.length; i++) {
134
+        let current = corners[i];
135
+        let length = segmentLength(previous, current);
136
+        if (length > max) {
137
+            max = length;
138
+        }
139
+        previous = current;
140
+    }
141
+    return max;
142
+}
143
+
144
+function extractSquare(img, corners) {
145
+    let longest = getLongestSide(corners);
146
+    let end = longest - 1;
147
+    let sourceRect = cv.matFromArray(4, 1, cv.CV_32FC2, [corners[0].x, corners[0].y, corners[1].x, corners[1].y, corners[2].x, corners[2].y, corners[3].x, corners[3].y]);
148
+    try {
149
+        let destRect = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, end, 0, end, end, 0, end]);
150
+        try {
151
+            let m = cv.getPerspectiveTransform(sourceRect, destRect);
152
+            try {
153
+                let destImg = new cv.Mat();
154
+                try {
155
+                    cv.warpPerspective(img, destImg, m, new cv.Size(longest, longest));
156
+                    return destImg;
157
+                } catch (err) {
158
+                    destImg.delete();
159
+                    throw err;
160
+                }
161
+            } finally {
162
+                m.delete();
163
+            }
164
+        } finally {
165
+            destRect.delete();
166
+        }
167
+    } finally {
168
+        sourceRect.delete();
169
+    }
170
+}
171
+
172
+function indexOfMax(arr) {
173
+    return arr.reduce((iMax, x, i, arr) => x > arr[iMax] ? i : iMax, 0);
174
+}
175
+
176
+function getFundamentalFrequency(mag) {
177
+    mag = mag.slice(0, Math.ceil(mag.length / 2));
178
+    mag[0] = 0;
179
+    return indexOfMax(mag);
180
+}
181
+
182
+function createMatVector(length) {
183
+    let vec = new cv.MatVector();
184
+    try {
185
+        let mat = new cv.Mat();
186
+        try {
187
+            for (let i = 0; i < length; i++) {
188
+                vec.push_back(mat);
189
+            }
190
+        } finally {
191
+            mat.delete();
192
+        }
193
+        return vec;
194
+    } catch (err) {
195
+        vec.delete();
196
+        throw err;
197
+    }
198
+}
199
+
200
+function getLineFFT(img, lineDetectorElementSize, axis) {
201
+    let lines = new cv.Mat();
202
+    try {
203
+        morphOpenImage(img, lines, axis === 1 ? new cv.Size(lineDetectorElementSize, 1) : new cv.Size(1, lineDetectorElementSize), 1);
204
+        let sums = new cv.Mat();
205
+        try {
206
+            cv.reduce(lines, sums, axis, cv.REDUCE_SUM, cv.CV_32FC1);
207
+            let fft = new cv.Mat();
208
+            try {
209
+                cv.dft(sums, fft, cv.DFT_COMPLEX_OUTPUT, 0);
210
+                return fft;
211
+            } catch (err) {
212
+                fft.delete();
213
+                throw err;
214
+            }
215
+        } finally {
216
+            sums.delete();
217
+        }
218
+    } finally {
219
+        lines.delete();
220
+    }
221
+}
222
+
223
+function getFFTMagnitude(fft) {
224
+    let planes = createMatVector(2);
225
+    try {
226
+        cv.split(fft, planes);
227
+        let real = planes.get(0);
228
+        try {
229
+            let imag = planes.get(1);
230
+            try {
231
+                let ret = [];
232
+                let length = Math.max(real.cols, real.rows);
233
+                for (let i = 0; i < length; i++) {
234
+                    ret.push(Math.sqrt(real.data32F[i] ** 2 + imag.data32F[i] ** 2))
235
+                }
236
+                return ret;
237
+            } finally {
238
+                imag.delete();
239
+            }
240
+        } finally {
241
+            real.delete();
242
+        }
243
+    } finally {
244
+        planes.delete();
245
+    }
246
+}
247
+
248
+function getLineFrequency(img, lineDetectorElementSize, axis) {
249
+    let fft = getLineFFT(img, lineDetectorElementSize, axis);
250
+    try {
251
+        return getFundamentalFrequency(getFFTMagnitude(fft));
252
+    } finally {
253
+        fft.delete();
254
+    }
255
+}