Skip to content

Commit 50cc599

Browse files
Added dexined quantized model for edge detection (#272)
* Added dexined.onnx file * Added sample, license, example outputs * Added a seperate wrapper class for supporting functions * Shifted to Tickmeter, and renamed files to demo.cpp and demo.py
1 parent c764e50 commit 50cc599

File tree

9 files changed

+335
-0
lines changed

9 files changed

+335
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
cmake_minimum_required(VERSION 3.22.2)
2+
project(opencv_zoo_edge_detection_dexined)
3+
4+
set(OPENCV_VERSION "5.0.0")
5+
set(OPENCV_INSTALLATION_PATH "" CACHE PATH "Where to look for OpenCV installation")
6+
7+
# Find OpenCV
8+
find_package(OpenCV ${OPENCV_VERSION} REQUIRED HINTS ${OPENCV_INSTALLATION_PATH})
9+
10+
add_executable(edge_detection edge_detection.cpp)
11+
target_link_libraries(edge_detection ${OpenCV_LIBS})

models/edge_detection_dexined/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Xavier Soria Poma
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.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# DexiNed
2+
3+
DexiNed is a Convolutional Neural Network (CNN) architecture for edge detection.
4+
5+
Notes:
6+
7+
- Model source: [ONNX](https://drive.google.com/file/d/1u_qXqXqaIP_SqdGaq4CbZyjzkZb02XTs/view).
8+
- Model source: [.pth](https://drive.google.com/file/d/1V56vGTsu7GYiQouCIKvTWl5UKCZ6yCNu/view).
9+
- This ONNX model has fixed input shape, but OpenCV DNN infers on the exact shape of input image. See https://github.com/opencv/opencv_zoo/issues/44 for more information.
10+
11+
## Requirements
12+
Install latest OpenCV >=5.0.0 and CMake >= 3.22.2 to get started with.
13+
14+
## Demo
15+
16+
### Python
17+
18+
Run the following command to try the demo:
19+
20+
```shell
21+
# detect on camera input
22+
python demo.py
23+
# detect on an image
24+
python demo.py --input /path/to/image
25+
26+
# get help regarding various parameters
27+
python demo.py --help
28+
```
29+
30+
### C++
31+
32+
```shell
33+
# A typical and default installation path of OpenCV is /usr/local
34+
cmake -B build -D OPENCV_INSTALLATION_PATH=/path/to/opencv/installation .
35+
cmake --build build
36+
37+
# detect on camera input
38+
./build/demo
39+
# detect on an image
40+
./build/demo --input=/path/to/image
41+
# get help messages
42+
./build/demo -h
43+
```
44+
45+
### Example outputs
46+
47+
![chicky](./example_outputs/chicky_output.jpg)
48+
49+
## License
50+
51+
All files in this directory are licensed under [MIT License](./LICENSE).
52+
53+
## Reference
54+
55+
- https://github.com/xavysp/DexiNed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#include <opencv2/dnn.hpp>
2+
#include <opencv2/imgproc.hpp>
3+
#include <opencv2/highgui.hpp>
4+
#include <iostream>
5+
#include <string>
6+
#include <cmath>
7+
#include <vector>
8+
9+
using namespace cv;
10+
using namespace cv::dnn;
11+
using namespace std;
12+
13+
class Dexined {
14+
public:
15+
Dexined(const string& modelPath) {
16+
loadModel(modelPath);
17+
}
18+
19+
// Function to set up the input image and process it
20+
void processFrame(const Mat& image, Mat& result) {
21+
Mat blob = blobFromImage(image, 1.0, Size(512, 512), Scalar(103.5, 116.2, 123.6), false, false, CV_32F);
22+
net.setInput(blob);
23+
applyDexined(image, result);
24+
}
25+
26+
private:
27+
Net net;
28+
29+
// Load Model
30+
void loadModel(const string modelPath) {
31+
net = readNetFromONNX(modelPath);
32+
net.setPreferableBackend(DNN_BACKEND_DEFAULT);
33+
net.setPreferableTarget(DNN_TARGET_CPU);
34+
}
35+
36+
// Function to apply sigmoid activation
37+
static void sigmoid(Mat& input) {
38+
exp(-input, input); // e^-input
39+
input = 1.0 / (1.0 + input); // 1 / (1 + e^-input)
40+
}
41+
42+
// Function to process the neural network output to generate edge maps
43+
static pair<Mat, Mat> postProcess(const vector<Mat>& output, int height, int width) {
44+
vector<Mat> preds;
45+
preds.reserve(output.size());
46+
for (const Mat &p : output) {
47+
Mat img;
48+
Mat processed;
49+
if (p.dims == 4 && p.size[0] == 1 && p.size[1] == 1) {
50+
processed = p.reshape(0, {p.size[2], p.size[3]});
51+
} else {
52+
processed = p.clone();
53+
}
54+
sigmoid(processed);
55+
normalize(processed, img, 0, 255, NORM_MINMAX, CV_8U);
56+
resize(img, img, Size(width, height));
57+
preds.push_back(img);
58+
}
59+
Mat fuse = preds.back();
60+
Mat ave = Mat::zeros(height, width, CV_32F);
61+
for (Mat &pred : preds) {
62+
Mat temp;
63+
pred.convertTo(temp, CV_32F);
64+
ave += temp;
65+
}
66+
ave /= static_cast<float>(preds.size());
67+
ave.convertTo(ave, CV_8U);
68+
return {fuse, ave};
69+
}
70+
71+
// Function to apply the Dexined model
72+
void applyDexined(const Mat& image, Mat& result) {
73+
int originalWidth = image.cols;
74+
int originalHeight = image.rows;
75+
vector<Mat> outputs;
76+
net.forward(outputs);
77+
pair<Mat, Mat> res = postProcess(outputs, originalHeight, originalWidth);
78+
result = res.first; // or res.second for average edge map
79+
}
80+
};
81+
82+
int main(int argc, char** argv) {
83+
const string about =
84+
"This sample demonstrates edge detection with dexined edge detection techniques.\n\n";
85+
const string keys =
86+
"{ help h | | Print help message. }"
87+
"{ input i | | Path to input image or video file. Skip this argument to capture frames from a camera.}"
88+
"{ model | edge_detection_dexined_2024sep.onnx | Path to the dexined.onnx model file }";
89+
90+
CommandLineParser parser(argc, argv, keys);
91+
if (parser.has("help"))
92+
{
93+
cout << about << endl;
94+
parser.printMessage();
95+
return -1;
96+
}
97+
98+
parser = CommandLineParser(argc, argv, keys);
99+
string model = parser.get<String>("model");
100+
parser.about(about);
101+
102+
VideoCapture cap;
103+
if (parser.has("input"))
104+
cap.open(samples::findFile(parser.get<String>("input")));
105+
else
106+
cap.open(0);
107+
108+
namedWindow("Input", WINDOW_AUTOSIZE);
109+
namedWindow("Output", WINDOW_AUTOSIZE);
110+
moveWindow("Output", 200, 0);
111+
112+
// Create an instance of Dexined
113+
Dexined dexined(model);
114+
Mat image;
115+
116+
for (;;){
117+
cap >> image;
118+
if (image.empty())
119+
{
120+
cout << "Press any key to exit" << endl;
121+
waitKey();
122+
break;
123+
}
124+
125+
Mat result;
126+
dexined.processFrame(image, result);
127+
128+
imshow("Input", image);
129+
imshow("Output", result);
130+
int key = waitKey(1);
131+
if (key == 27 || key == 'q')
132+
{
133+
break;
134+
}
135+
}
136+
destroyAllWindows();
137+
return 0;
138+
}

models/edge_detection_dexined/demo.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import cv2 as cv
2+
import argparse
3+
from dexined import Dexined
4+
5+
def get_args_parser(func_args):
6+
parser = argparse.ArgumentParser(add_help=False)
7+
parser.add_argument('--input', help='Path to input image or video file. Skip this argument to capture frames from a camera.', default=0, required=False)
8+
parser.add_argument('--model', help='Path to dexined.onnx', default='edge_detection_dexined_2024sep.onnx', required=False)
9+
10+
args, _ = parser.parse_known_args()
11+
parser = argparse.ArgumentParser(parents=[parser],
12+
description='', formatter_class=argparse.RawTextHelpFormatter)
13+
return parser.parse_args(func_args)
14+
15+
def main(func_args=None):
16+
args = get_args_parser(func_args)
17+
18+
dexined = Dexined(modelPath=args.model)
19+
20+
# Open video or capture from camera
21+
cap = cv.VideoCapture(cv.samples.findFile(args.input) if args.input else 0)
22+
if not cap.isOpened():
23+
print("Failed to open the input video")
24+
exit(-1)
25+
26+
cv.namedWindow('Input', cv.WINDOW_AUTOSIZE)
27+
cv.namedWindow('Output', cv.WINDOW_AUTOSIZE)
28+
cv.moveWindow('Output', 200, 50)
29+
30+
# Process frames
31+
tm = cv.TickMeter()
32+
while cv.waitKey(1) < 0:
33+
hasFrame, image = cap.read()
34+
if not hasFrame:
35+
print("Press any key to exit")
36+
cv.waitKey(0)
37+
break
38+
39+
tm.start()
40+
result = dexined.infer(image)
41+
tm.stop()
42+
label = 'Inference time: {:.2f} ms, FPS: {:.2f}'.format(tm.getTimeMilli(), tm.getFPS())
43+
44+
cv.imshow("Input", image)
45+
cv.putText(result, label, (0, 15), cv.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255))
46+
cv.imshow("Output", result)
47+
48+
cv.destroyAllWindows()
49+
50+
if __name__ == '__main__':
51+
main()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import cv2 as cv
2+
import numpy as np
3+
4+
class Dexined:
5+
def __init__(self, modelPath='edge_detection_dexined_2024sep.onnx', backendId=0, targetId=0):
6+
self._modelPath = modelPath
7+
self._backendId = backendId
8+
self._targetId = targetId
9+
10+
# Load the model
11+
self._model = cv.dnn.readNetFromONNX(self._modelPath)
12+
self.setBackendAndTarget(self._backendId, self._targetId)
13+
14+
@property
15+
def name(self):
16+
return self.__class__.__name__
17+
18+
def setBackendAndTarget(self, backendId, targetId):
19+
self._backendId = backendId
20+
self._targetId = targetId
21+
self._model.setPreferableBackend(self._backendId)
22+
self._model.setPreferableTarget(self._targetId)
23+
24+
@staticmethod
25+
def sigmoid(x):
26+
return 1.0 / (1.0 + np.exp(-x))
27+
28+
def postProcessing(self, output, shape):
29+
h, w = shape
30+
preds = []
31+
for p in output:
32+
img = self.sigmoid(p)
33+
img = np.squeeze(img)
34+
img = cv.normalize(img, None, 0, 255, cv.NORM_MINMAX, cv.CV_8U)
35+
img = cv.resize(img, (w, h))
36+
preds.append(img)
37+
fuse = preds[-1]
38+
ave = np.array(preds, dtype=np.float32)
39+
ave = np.uint8(np.mean(ave, axis=0))
40+
return fuse, ave
41+
42+
def infer(self, image):
43+
inp = cv.dnn.blobFromImage(image, 1.0, (512, 512), (103.5, 116.2, 123.6), swapRB=False, crop=False)
44+
self._model.setInput(inp)
45+
46+
# Forward pass through the model
47+
out = self._model.forward()
48+
result, _ = self.postProcessing(out, image.shape[:2])
49+
50+
return result
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:a50d01dc8481549c7dedb9eb3e0123b810a016520df75e4669a504609982cdd0
3+
size 47235563
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)