OpenCV DNN ile Daha Iyi Kenar Belirleme

OpenCV ile kenar belirleme için çoğunlukla Canny kenar belirleme yöntemi tercih ediliyor. Canny yönteminin yetersiz kaldığı durumlarda artık dnn ile daha gerçekçi kenar belirleme sonucu göreceğiz.

Canny algoritması John F. Canny tarafından 1986’da geliştirildi. 4 aşamalı bir algoritmadır.

  • Noise removal: Resimdeki istenmeyen gürültüyü Gaussian Filter ile azaltarak Canny algoritmasının kenar olarak algılaması engellenir.
  • Gradient Calculation: Her piksel için Sobel, Prewitt ya da Robert operatörleriyle gradyan (görüntü yoğunluk derecesi) hesaplanır.
    $$Edge\ Graident\ (G) = { \sqrt{G_{x}^{2} + G_{y}^{2}} }$$
    $$Angle\ (\theta) = { \tan^{-1}(\frac{G_{y}}{G_{x}}) }$$
  • Non-Maximal Suppression: Kenarlarda olmayan pikseller kaldırılır. En yüksek gradyan değeri olan pikseller kenar olarak kabul edilir. Gerçekte gradyan hesabı tek pikselde değilde komşu piksellerle birlikte yapılır.
  • Hysteresis Thresholding: Min ve Max threshold değerine bakılarak eldeki piksellerin kenar mı değil mi olduğuna dair son karar verilir. Gradyan değeri Max değerden yüksekse kenar kabul edilir, Min değerden düşükse kenar kabul edilmez. Max ile Min arasında kalıyorsa bağlantılı olduğu kenar var mı diye bakılır eğer varsa kenar olarak kabul edilir.
    Bu aşamalardan sonra Canny yöntemi bitmiş ve kenar belirlenmiş olur.

Canny Yönteminin Sorunları

Canny yöntemi yerel değişimlere odaklanarak kenar bulmaya çalışır, resmin içeriğine göre karar vermez. Bu sebeple her durumda güzel ve istenen sonuçlar alınmaz.
İçeriğe bakarak kenar bulma derin öğrenme ya da makine öğrenmesi tabanlı yöntemler ile mümkün hale gelmiştir.

OpenCV ile Derin Öğrenme Tabanlı Kenar Bulma

OpenCV 3.4.3 ve üst sürümlerde kendi içinde entegre DNN modülü barındırmaya başladı. Bu DNN tabanlı kenar bulma yöntemi Holistically Nested Edge Detection ya da HED olarak bilinmeye başladı.
arch
HED, ara katmanların yan çıktılarını kullanır. Önceki katmanların çıktılarına yan çıktı denmektedir ve 5 convolutional katmanın çıktısı asıl tahmini oluşturmak için birleştirilir. Her katmanda oluşturulan özellik haritaları farklı boyutta olduğundan, görüntüye farklı ölçeklerde etkili bir şekilde bakıyor.
HED yöntemi en yüksek doğrulukta sonuç üretmese de DNN tabanlı çözümler içinde en hızlısı sayılabilir.
comparsion

C++ ile OpenCV DNN Tabanlı Kenar Bulma Yöntemi

Koda geçmeden önce Caffe ile eğitilmiş model ve prototxt dosyalarını edinmek gerekiyor.
OpenCV dnn::Layer sınıfından kalıtım yoluyla MyCropLayer adlı sınıf oluşturup kendi implementasyonumuzu yapacağız.

MyCropLayer.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <opencv2/core.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/dnn/layer.details.hpp>
#include <opencv2/dnn/shape_utils.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/opencv.hpp>
class MyCropLayer : public cv::dnn::Layer
{
public:
explicit MyCropLayer(const cv::dnn::LayerParams &params);
static cv::Ptr<cv::dnn::Layer> create(cv::dnn::LayerParams &params){
return cv::Ptr<Layer>(new MyCropLayer(params));
}
virtual bool getMemoryShapes(const std::vector<std::vector<int>> &inputs, const int requiredOutputs,std::vector<std::vector<int>> &outputs, std::vector<std::vector<int>> &internals) const CV_OVERRIDE{
CV_UNUSED(requiredOutputs);
CV_UNUSED(internals);
std::vector<int> outShape(4);
outShape[0] = inputs[0][0]; // batch size
outShape[1] = inputs[0][1]; // number of channels
outShape[2] = inputs[1][2];
outShape[3] = inputs[1][3];
outputs.assign(1, outShape);
return false;
}
virtual void forward(std::vector<cv::Mat *> &input, std::vector<cv::Mat> &output, std::vector<cv::Mat> &internals) CV_OVERRIDE{
cv::Mat *inp = input[0];
cv::Mat out = output[0];
int ystart = (inp->size[2] - out.size[2]) / 2;
int xstart = (inp->size[3] - out.size[3]) / 2;
int yend = ystart + out.size[2];
int xend = xstart + out.size[3];
const int batchSize = inp->size[0];
const int numChannels = inp->size[1];
const int height = out.size[2];
const int width = out.size[3];
int sz[] = {static_cast<int>(batchSize), numChannels, height, width};
out.create(4, sz, CV_32F);
for (int i = 0; i < batchSize; i++) {
for (int j = 0; j < numChannels; j++) {
cv::Mat plane(inp->size[2], inp->size[3], CV_32F, inp->ptr<float>(i, j));
cv::Mat crop = plane(cv::Range(ystart, yend), cv::Range(xstart, xend));
cv::Mat targ(height, width, CV_32F, out.ptr<float>(i, j));
crop.copyTo(targ);
}
}
}
virtual void forward(cv::InputArrayOfArrays inputs_arr, cv::OutputArrayOfArrays outputs_arr, cv::OutputArrayOfArrays internals_arr) CV_OVERRIDE{
std::vector<cv::Mat> inputs, outputs;
inputs_arr.getMatVector(inputs);
outputs_arr.getMatVector(outputs);
cv::Mat &inp = inputs[0];
cv::Mat &out = outputs[0];
int ystart = (inp.size[2] - out.size[2]) / 2;
int xstart = (inp.size[3] - out.size[3]) / 2;
int yend = ystart + out.size[2];
int xend = xstart + out.size[3];
const int batchSize = inp.size[0];
const int numChannels = inp.size[1];
const int height = out.size[2];
const int width = out.size[3];
int sz[] = {static_cast<int>(batchSize), numChannels, height, width};
out.create(4, sz, CV_32F);
for (int i = 0; i < batchSize; i++) {
for (int j = 0; j < numChannels; j++) {
cv::Mat plane(inp.size[2], inp.size[3], CV_32F, inp.ptr<float>(i, j));
cv::Mat crop = plane(cv::Range(ystart, yend), cv::Range(xstart, xend));
cv::Mat targ(height, width, CV_32F, out.ptr<float>(i, j));
crop.copyTo(targ);
}
}
}
};

MyCropLayer sınıfımızı implement ettikten sonra main.cpp dosyasında ilgili hpp dosyasını çağırdıktan sonra

1
CV_DNN_REGISTER_LAYER_CLASS(Crop, MyCropLayer);

bildirimi ile reimplement ettiğimiz sınıfı OpenCV Layer sınıfına kaydediyoruz. Böylelikle OpenCV, Crop sınıfı yerine implement ettiğimiz MyCropLayer sınıfını kullanacaktır.
Yine main.cpp dosyasında bir fonksiyon oluşturalım:

hedEdgeDetectDNN function from main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void hedEdgeDetectDNN(cv::Mat &image, std::string prototxt, std::string caffemodel, int size = 128)
{
cv::dnn::Net net = cv::dnn::readNet(prototxt, caffemodel);

cv::Size reso(size, size); //image will be resized to sizexsize
cv::Mat theInput;

cv::resize(image, theInput, reso);
cv::Mat blob = cv::dnn::blobFromImage(theInput,1.0,reso,cv::Scalar(104.00698793, 116.66876762, 122.67891434),false,false);
net.setInput(blob);
cv::Mat out = net.forward(); // outputBlobs contains all output blobs for each layer specified in outBlobNames.

std::vector<cv::Mat> vectorOfImagesFromBlob;
cv::dnn::imagesFromBlob(out, vectorOfImagesFromBlob);
cv::Mat tmpMat = vectorOfImagesFromBlob[0] * 255;
cv::Mat tmpMatUchar;
cv::cvtColor(tmpMat, tmpMatUchar, cv::COLOR_GRAY2BGR);
cv::resize(tmpMatUchar, image, image.size());
}

Fonksiyon parametrelerinde &image değişkeni kaynak/hedef resmimizi gösterir. prototxt ve caffemodel değişkenleri ise prototxt ve caffemodel dosya yolunu gösterecektir. Son olarak size değişkeni ise resmin DNN ile işleme girmeden önceki ölçekleneceği boyutu ayarlamak için kullanılacaktır. Boyutun arttırılması kaliteyi arttırırken işlem süresini önemli ölçüde arttırmaktadır.
Fonksiyon içeriğinde ise readNet ile model dosyası okunur. Daha sonra giriş resmi belirlenen bir boyuta ölçeklenir. Ardından blobFromImage ile görüntüden blob oluşturulup sinir ağına giriş olarak verilir.
Tekrar blob’dan görüntü elde etmek için imagesFromBlob fonksiyonu çağırılır. Tek resim gönderildiği için vectorOfImagesFromBlob[0] konumundaki görüntüyü tekrar eski boyutuna getirip giriş resmine atayarak fonksiyonu bitirir.

Çeşitli Resimler ve Sonuçları

  • Orijinal (Üst) Canny (Sol alt) HED (Sağ alt)
    SoloTurk
    Koenigsegg

İleri okuma ve kaynaklar

Github Pages Üzerine Hexo Kurulumu Cisco Router Yönlendirme

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×