2024/04/17

OpenCV

簡介

OpenCV 的全稱是 Open Source Computer Vision Library, 是一個跨平台的電腦視覺庫,由英特爾公司發起並參與開發。

在 openSUSE Tumbleweed 安裝:

sudo zypper in opencv-devel

安裝完成以後,接下來進行是否安裝成功的測試。下面是一個顯示圖片的 display.cpp

#include <stdio.h>
#include <opencv2/opencv.hpp>

int main(int argc, char* argv[]) {

  if ( argc != 2 ) {
    printf("usage: display <File_Path>\n");
    return -1;
  }

  cv::Mat image = cv::imread( argv[1], 1 );

  if ( !image.data ) {
    printf("No image data\n");
    return -1;
  }

  cv::namedWindow("Display Image", cv::WINDOW_AUTOSIZE);
  cv::imshow("Display Image", image);
  cv::waitKey(0);
  cv::destroyWindow("Display Image");

  return 0;
}

如果直接編譯,使用:

g++ display.cpp -lopencv_core -lopencv_imgproc -lopencv_imgcodecs -lopencv_highgui -o display

或者是使用 pkg-config,假設是 OpenCV 4,那麼使用下列的指令:

g++ display.cpp `pkg-config opencv4 --libs` -o display

或者使用 CMake。這個例子所需要的 CMake 設定檔 CMakeLists.txt 內容如下:

cmake_minimum_required(VERSION 2.8)
project( DisplayImage )
find_package( OpenCV REQUIRED )
add_executable( display display.cpp )
target_link_libraries( display ${OpenCV_LIBS} )

接下來使用下列的指令編譯:

cmake .
make

要注意的是,OpenCL 預設使用的 color model 為 BGR,而一般流行使用的 color model 為 RGB。

還有的 color model 為 LAB:

  • L – Lightness ( Intensity ).
  • a – color component ranging from Green to Magenta.
  • b – color component ranging from Blue to Yellow.
cv::cvtColor(bright, brightLAB, cv::COLOR_BGR2LAB);

The YCrCb color space is derived from the RGB color space and has the following three compoenents.

  • Y – Luminance or Luma component obtained from RGB after gamma correction.
  • Cr = R – Y ( how far is the red component from Luma ).
  • Cb = B – Y ( how far is the blue component from Luma ).
cv::cvtColor(bright, brightYCB, cv::COLOR_BGR2YCrCb);

The HSV color space has the following three components

  • H – Hue ( Dominant Wavelength ).
  • S – Saturation ( Purity / shades of the color ).
  • V – Value ( Intensity ).
cv::cvtColor(bright, brightHSV, cv::COLOR_BGR2HSV);

要注意的是,要觀察圖片的軌跡或者是偵測線條(edge detection)時,很常用的手法是將圖轉為灰階,然後再使用各種演算法取得結果。


再來是建立一個空白的 image 並且顯示:

#include <opencv2/opencv.hpp>
#include <string>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{
  Mat image(600, 800, CV_8UC3, Scalar(200, 150, 130));

  string windowName = "Window with Blank Image";

  namedWindow(windowName);
  imshow(windowName, image);
  waitKey(0);
  destroyWindow(windowName);

  return 0;
}

再來是讀取 Vedio 檔案並且播放的範例:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a vedio file." << endl;
        return 0;
    }

    VideoCapture vid_capture(argv[1]);

    if (!vid_capture.isOpened())
    {
        cout << "Error opening video stream or file" << endl;
    }

    else
    {
        int fps = vid_capture.get(CAP_PROP_FPS);
        cout << "Frames per second :" << fps;

        int frame_count = vid_capture.get(CAP_PROP_FRAME_COUNT);
        cout << "  Frame count :" << frame_count;
    }

    while (vid_capture.isOpened())
    {
        Mat frame;

        bool isSuccess = vid_capture.read(frame);

        if (isSuccess == true)
        {
            imshow("Frame", frame);
        }

        if (isSuccess == false)
        {
            cout << "Video camera is disconnected" << endl;
            break;
        }

        int key = waitKey(20);
        if (key == 'q')
        {
            cout << "q key is pressed by the user. Stopping the video" << endl;
            break;
        }
    }

    vid_capture.release();
    destroyAllWindows();
    return 0;
}

下面是 image resize 的例子:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    imshow("Original Image", image);

    int down_width = 300;
    int down_height = 200;
    Mat resized_down;
    resize(image, resized_down, Size(down_width, down_height), INTER_LINEAR);

    int up_width = 600;
    int up_height = 400;
    Mat resized_up;
    resize(image, resized_up, Size(up_width, up_height), INTER_LINEAR);

    imshow("Resized Down by defining height and width", resized_down);
    waitKey();
    imshow("Resized Up image by defining height and width", resized_up);
    waitKey();

    destroyAllWindows();
    return 0;
}

下面是 crop image 的例子:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat img = imread(argv[1]);
    cout << "Width : " << img.size().width << endl;
    cout << "Height: " << img.size().height << endl;
    cout << "Channels: :" << img.channels() << endl;

    int firstx = img.size().width / 4;
    int firsty = img.size().height / 4;
    int secondx = img.size().width / 4 * 3;
    int secondy = img.size().height / 4 * 3;

    Mat cropped_image = img(Range(firsty, secondy), Range(firstx, secondx));

    imshow(" Original Image", img);
    imshow("Cropped Image", cropped_image);

    //Save the cropped Image
    imwrite("CroppedImage.jpg", cropped_image);

    waitKey(0);
    destroyAllWindows();
    return 0;
}

下面是 image rotation 的例子:

#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    imshow("image", image);
    waitKey(0);
    double angle = 45;

    Point2f center((image.cols - 1) / 2.0, (image.rows - 1) / 2.0);
    Mat rotation_matix = getRotationMatrix2D(center, angle, 1.0);

    Mat rotated_image;
    // rotate the image using warpAffine
    warpAffine(image, rotated_image, rotation_matix, image.size());
    imshow("Rotated image", rotated_image);
    waitKey(0);
    imwrite("rotated_im.jpg", rotated_image);

    return 0;
}

當我們對圖片加上某個常數,就是增加亮度或者是減少亮度。例如下面的式子是增加亮度:

new_image (i, j) = image(i, j) + c

所以我們可以這樣寫:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);

    if (image.empty())
    {
        cout << "Could not open or find the image" << endl;
        return 0;
    }

    Mat imageBrighnessHigh50;
    image.convertTo(imageBrighnessHigh50, -1, 1, 50); //increase the brightness by 50

    Mat imageBrighnessHigh100;
    image.convertTo(imageBrighnessHigh100, -1, 1, 100);

    Mat imageBrighnessLow50;
    image.convertTo(imageBrighnessLow50, -1, 1, -50); //decrease the brightness by 50

    Mat imageBrighnessLow100;
    image.convertTo(imageBrighnessLow100, -1, 1, -100);

    String windowNameOriginalImage = "Original Image";
    String windowNameBrightnessHigh50 = "Brightness Increased by 50";
    String windowNameWithBrightnessHigh100 = "Brightness Increased by 100";
    String windowNameBrightnessLow50 = "Brightness Decreased by 50";
    String windowNameBrightnessLow100 = "Brightness Decreased by 100";

    namedWindow(windowNameOriginalImage, WINDOW_NORMAL);
    namedWindow(windowNameBrightnessHigh50, WINDOW_NORMAL);
    namedWindow(windowNameWithBrightnessHigh100, WINDOW_NORMAL);
    namedWindow(windowNameBrightnessLow50, WINDOW_NORMAL);
    namedWindow(windowNameBrightnessLow100, WINDOW_NORMAL);

    imshow(windowNameOriginalImage, image);
    imshow(windowNameBrightnessHigh50, imageBrighnessHigh50);
    imshow(windowNameWithBrightnessHigh100, imageBrighnessHigh100);
    imshow(windowNameBrightnessLow50, imageBrighnessLow50);
    imshow(windowNameBrightnessLow100, imageBrighnessLow100);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

對於 Contrast 來說,就是乘以某個數(> 1 就是增加對比,1 > c > 0 就是減少對比)。

new_image (i, j) = image(i, j) * c

所以我們可以這樣寫:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    if (image.empty())
    {
        cout << "Could not open or find the image" << endl;
        cin.get();
        return 0;
    }

    Mat imageContrastHigh2;
    image.convertTo(imageContrastHigh2, -1, 2, 0); //increase the contrast by 2

    Mat imageContrastHigh4;
    image.convertTo(imageContrastHigh4, -1, 4, 0);

    Mat imageContrastLow0_5;
    image.convertTo(imageContrastLow0_5, -1, 0.5, 0);

    Mat imageContrastLow0_25;
    image.convertTo(imageContrastLow0_25, -1, 0.25, 0);

    String windowNameOriginalImage = "Original Image";
    String windowNameContrastHigh2 = "Contrast Increased by 2";
    String windowNameContrastHigh4 = "Contrast Increased by 4";
    String windowNameContrastLow0_5 = "Contrast Decreased by 0.5";
    String windowNameContrastLow0_25 = "Contrast Decreased by 0.25";

    namedWindow(windowNameOriginalImage, WINDOW_NORMAL);
    namedWindow(windowNameContrastHigh2, WINDOW_NORMAL);
    namedWindow(windowNameContrastHigh4, WINDOW_NORMAL);
    namedWindow(windowNameContrastLow0_5, WINDOW_NORMAL);
    namedWindow(windowNameContrastLow0_25, WINDOW_NORMAL);

    imshow(windowNameOriginalImage, image);
    imshow(windowNameContrastHigh2, imageContrastHigh2);
    imshow(windowNameContrastHigh4, imageContrastHigh4);
    imshow(windowNameContrastLow0_5, imageContrastLow0_5);
    imshow(windowNameContrastLow0_25, imageContrastLow0_25);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

OpenCV 也具有繪圖(例如畫線、畫圓,以及加上文字等)的能力。下面是一個例子。

// Import dependencies
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat img = imread(argv[1]);
    imshow("Original Image", img);
    waitKey();
    if (img.empty())
    {
        cout << "Could not read image" << endl;
    }

    Mat imageChanged = img.clone();
    Point pointA(350, 370);
    Point pointB(500, 370);
    line(imageChanged, pointA, pointB, Scalar(255, 255, 0), 3, 8, 0);

    Point circle_center(420, 130);
    int radius = 150;
    circle(imageChanged, circle_center, radius, Scalar(0, 0, 255), 3, 8, 0);

    putText(imageChanged, "The best!", Point(50, 500), FONT_HERSHEY_COMPLEX,
            1.5, Scalar(250, 225, 100));

    imshow("Changed Image", imageChanged);
    waitKey();

    // save image
    imwrite("ChangedImage.jpg", imageChanged);
    destroyAllWindows();

    return 0;
}

下面是輸出的結果:


OpenCV 提供了函數 cv::threshold(),可以用來執行圖像二值化的工作。 圖像的二值化就是將圖像上的像素點的灰度值設置為 0 或 255,這樣將使整個圖像呈現出明顯的黑白效果。 在圖像處理中,二值圖像佔有非常重要的地位,圖像的二值化使圖像的數據大為減少,從而凸顯出目標的輪廓。

#include "opencv2/opencv.hpp"

using namespace cv;
using namespace std;

int main( int argc, char** argv )
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat src = imread(argv[1], IMREAD_GRAYSCALE);
    Mat dst;


    // Basic threhold example
    threshold(src,dst,0, 255, THRESH_BINARY);
    imwrite("opencv-threshold-example.jpg", dst);

    // Thresholding with maxval set to 128
    threshold(src, dst, 0, 128, THRESH_BINARY);
    imwrite("opencv-thresh-binary-maxval.jpg", dst);

    // Thresholding with threshold value set 127
    threshold(src,dst,127,255, THRESH_BINARY);
    imwrite("opencv-thresh-binary.jpg", dst);

    // Thresholding using THRESH_BINARY_INV
    threshold(src,dst,127,255, THRESH_BINARY_INV);
    imwrite("opencv-thresh-binary-inv.jpg", dst);

    // Thresholding using THRESH_TRUNC
    threshold(src,dst,127,255, THRESH_TRUNC);
    imwrite("opencv-thresh-trunc.jpg", dst);

    // Thresholding using THRESH_TOZERO
    threshold(src,dst,127,255, THRESH_TOZERO);
    imwrite("opencv-thresh-tozero.jpg", dst);

    // Thresholding using THRESH_TOZERO_INV
    threshold(src,dst,127,255, THRESH_TOZERO_INV);
    imwrite("opencv-thresh-to-zero-inv.jpg", dst);
}

Histogram of an image is the graphical representation of the distribution of intensities of pixels. It provides an estimate of where pixel values are concentrated and whether there are unusual deviations.

#include "opencv2/opencv.hpp"

using namespace cv;
using namespace std;

int main( int argc, char** argv )
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);

    if (image.empty())
    {
        cout << "Could not open or find the image" << endl;
        return 0;
    }

    cvtColor(image, image, COLOR_BGR2GRAY);

    Mat hist_equalized_image;
    equalizeHist(image, hist_equalized_image);

    String windowNameOfOriginalImage = "Original Image";
    String windowNameOfHistogramEqualized = "Histogram Equalized Image";

    namedWindow(windowNameOfOriginalImage, WINDOW_NORMAL);
    namedWindow(windowNameOfHistogramEqualized, WINDOW_NORMAL);

    imshow(windowNameOfOriginalImage, image);
    imshow(windowNameOfHistogramEqualized, hist_equalized_image);

    waitKey(0);
    destroyAllWindows();
}

下面是針對彩色的範例:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    if (image.empty())
    {
        cout << "Could not read image" << endl;
    }

    Mat hist_equalized_image;
    cvtColor(image, hist_equalized_image, COLOR_BGR2YCrCb);

    vector<Mat> vec_channels;
    split(hist_equalized_image, vec_channels);

    equalizeHist(vec_channels[0], vec_channels[0]);

    merge(vec_channels, hist_equalized_image);

    cvtColor(hist_equalized_image, hist_equalized_image, COLOR_YCrCb2BGR);

    String windowNameOfOriginalImage = "Original Image";
    String windowNameOfHistogramEqualized = "Histogram Equalized Color Image";

    namedWindow(windowNameOfOriginalImage, WINDOW_NORMAL);
    namedWindow(windowNameOfHistogramEqualized, WINDOW_NORMAL);

    imshow(windowNameOfOriginalImage, image);
    imshow(windowNameOfHistogramEqualized, hist_equalized_image);

    waitKey(0); // Wait for any keystroke in any one of the windows

    destroyAllWindows(); //Destroy all opened windows

    return 0;
}

equalizeHist 在一些情況下會製造雜訊,也可以使用 CLAHE 來作為改進的演算法:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    if (image.empty())
    {
        cout << "Could not read image" << endl;
    }

    Mat hist_equalized_image;
    cvtColor(image, hist_equalized_image, COLOR_BGR2YCrCb);

    vector<Mat> vec_channels;
    Ptr<CLAHE> clahe = createCLAHE();
    split(hist_equalized_image, vec_channels);

    clahe->apply(vec_channels[0], vec_channels[0]);

    merge(vec_channels, hist_equalized_image);

    cvtColor(hist_equalized_image, hist_equalized_image, COLOR_YCrCb2BGR);

    String windowNameOfOriginalImage = "Original Image";
    String windowNameOfHistogramEqualized = "Histogram Equalized Color Image";

    namedWindow(windowNameOfOriginalImage, WINDOW_NORMAL);
    namedWindow(windowNameOfHistogramEqualized, WINDOW_NORMAL);

    imshow(windowNameOfOriginalImage, image);
    imshow(windowNameOfHistogramEqualized, hist_equalized_image);

    waitKey(0); // Wait for any keystroke in any one of the windows

    destroyAllWindows(); //Destroy all opened windows

    return 0;
}

Edge detection is an image-processing technique, which is used to identify the boundaries (edges) of objects, or regions within an image.

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat img = imread(argv[1]);

    if (img.empty())
    {
        cout << "Could not open or find the image" << endl;
        return 0;
    }
    imshow("original Image", img);
    waitKey(0);

    Mat img_gray;
    cvtColor(img, img_gray, COLOR_BGR2GRAY);
    Mat img_blur;
    GaussianBlur(img_gray, img_blur, Size(3,3), 0);

    Mat sobelx, sobely, sobelxy;
    Sobel(img_blur, sobelx, CV_64F, 1, 0, 5);
    Sobel(img_blur, sobely, CV_64F, 0, 1, 5);
    Sobel(img_blur, sobelxy, CV_64F, 1, 1, 5);
    imshow("Sobel X", sobelx);
    waitKey(0);
    imshow("Sobel Y", sobely);
    waitKey(0);
    imshow("Sobel XY using Sobel() function", sobelxy);
    waitKey(0);

    Mat edges;
    Canny(img_blur, edges, 100, 200, 3, false);
    imshow("Canny edge detection", edges);
    waitKey(0);

    destroyAllWindows();
    return 0;
}

The image filtering is a neighborhood operation in which the value of any given pixel in the output image is determined by applying a certain algorithm to the pixel values ​​in the vicinity of the corresponding input pixel.

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    if (image.empty())
    {
        cout << "Could not read image" << endl;
    }

    Mat image_blurred_with_3x3_kernel;
    blur(image, image_blurred_with_3x3_kernel, Size(3, 3));

    Mat image_blurred_with_5x5_kernel;
    blur(image, image_blurred_with_5x5_kernel, Size(5, 5));

    String window_name = "Display";
    String window_name_blurred_with_3x3_kernel = "Display Blurred with 3 x 3 Kernel";
    String window_name_blurred_with_5x5_kernel = "Display Blurred with 5 x 5 Kernel";

    namedWindow(window_name);
    namedWindow(window_name_blurred_with_3x3_kernel);
    namedWindow(window_name_blurred_with_5x5_kernel);

    imshow(window_name, image);
    imshow(window_name_blurred_with_3x3_kernel, image_blurred_with_3x3_kernel);
    imshow(window_name_blurred_with_5x5_kernel, image_blurred_with_5x5_kernel);

    waitKey(0);
    destroyAllWindows();

    return 0;
}

下面是另外的例子:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    if (image.empty())
    {
        cout << "Could not read image" << endl;
    }

    Mat kernel1 = (Mat_<double>(3, 3) << 0, 0, 0, 0, 1, 0, 0, 0, 0);
    Mat identity;
    filter2D(image, identity, -1, kernel1, Point(-1, -1), 0, 4);
    imshow("Original", image);
    imshow("Identity", identity);
    waitKey();
    imwrite("identity.jpg", identity);
    destroyAllWindows();

    Mat kernel2 = Mat::ones(5, 5, CV_64F);
    kernel2 = kernel2 / 25;
    Mat img;
    filter2D(image, img, -1, kernel2, Point(-1, -1), 0, 4);
    imshow("Original", image);
    imshow("Kernel blur", img);
    imwrite("blur_kernel.jpg", img);
    waitKey();
    destroyAllWindows();
}

下面是 mouse callback function 的例子:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

void CallBackFunc(int event, int x, int y, int flags, void *userdata)
{
    if (event == EVENT_LBUTTONDOWN)
    {
        cout << "Left button of the mouse is clicked - position (" << x << ", " << y << ")" << endl;
    }
    else if (event == EVENT_RBUTTONDOWN)
    {
        cout << "Right button of the mouse is clicked - position (" << x << ", " << y << ")" << endl;
    }
    else if (event == EVENT_MBUTTONDOWN)
    {
        cout << "Middle button of the mouse is clicked - position (" << x << ", " << y << ")" << endl;
    }
    else if (event == EVENT_MOUSEMOVE)
    {
        cout << "Mouse move over the window - position (" << x << ", " << y << ")" << endl;
    }
}

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        cout << "Please give a file." << endl;
        return 0;
    }

    Mat image = imread(argv[1]);
    if (image.empty())
    {
        cout << "Could not read image" << endl;
    }

    namedWindow("My Window", 1);
    setMouseCallback("My Window", CallBackFunc, NULL);
    imshow("My Window", image);
    waitKey(0);
    destroyAllWindows();
    return 0;
}

使用 SimpleBlobDetector 執行 Blob detection:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char *argv[])
{
    Mat im = imread("blob.jpg", IMREAD_GRAYSCALE);

    // Setup SimpleBlobDetector parameters.
    SimpleBlobDetector::Params params;

    // Change thresholds
    params.minThreshold = 10;
    params.maxThreshold = 200;

    // Filter by Area.
    //params.filterByArea = true;
    //params.minArea = 1500;

    // Filter by Circularity
    //params.filterByCircularity = true;
    //params.minCircularity = 0.1;

    // Filter by Convexity
    params.filterByConvexity = true;
    params.minConvexity = 0.5;

    // Filter by Inertia
    //params.filterByInertia = true;
    //params.minInertiaRatio = 0.01;

    Ptr<SimpleBlobDetector> detector = SimpleBlobDetector::create(params);

    std::vector<KeyPoint> keypoints;
    detector->detect(im, keypoints);

    Mat im_with_keypoints;
    drawKeypoints(im, keypoints, im_with_keypoints, Scalar(0, 0, 255), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);

    namedWindow("keypoints", 1);
    imshow("keypoints", im_with_keypoints);
    waitKey(0);
}

下面是針對單一圖片的 Face Detection 例子。如果要改寫成使用 camera,那就是你拿到 frame 以後,使用同樣的方式去偵測。

#include "opencv2/objdetect.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>

using namespace std;
using namespace cv;

CascadeClassifier face_cascade;

int main( int argc, const char** argv )
{
    if ( argc != 2 ) {
        printf("usage: display <File_Path>\n");
        return -1;
    }

    /*
     * From https://github.com/opencv/opencv/tree/master/data/haarcascades
     */
    string face_cascade_name("haarcascade_frontalface_default.xml");

    if( !face_cascade.load( face_cascade_name ) )
    {
        cout << "--(!)Error loading face cascade\n";
        return -1;
    };

    cv::Mat frame = cv::imread( argv[1], 1 );
    if ( !frame.data ) {
        printf("No image data\n");
        return -1;
    }
    cv::namedWindow("Display", cv::WINDOW_AUTOSIZE);

    Mat frame_gray;
    cvtColor( frame, frame_gray, COLOR_BGR2GRAY );
    equalizeHist( frame_gray, frame_gray );

    std::vector<Rect> faces;
    face_cascade.detectMultiScale( frame_gray, faces );
    cout << "Find: " << faces.size() << endl;
    for (auto&& feature : faces) {
        cv::rectangle(frame, feature, cv::Scalar(0, 0, 255), 2);
    }

    imshow( "Display", frame );
    waitKey(0);
    return 0;
}

參考連結

2024/04/16

KDE Plasma

Linux(或者也可以說是 UNIX-like)系統在桌面環境上擁有非常多的選擇,而 KDE Plasma 是其中一個。

KDE Plasma 基於 Qt 框架所開發, 有一些 Linux 發行版的預設桌面環境是 KDE Plasma,為 Linux 最流行的幾個桌面環境之一。 因為 KDE 所採用的 Qt 框架一開始並非自由軟體, 導致了 GNOME 專案的誔生(在 GNOME 專案誔生之後,Qt 框架核心改為雙重授權, 因此自由軟體可以使用 GPL 授權的 Qt 框架,而在之後又加入可以選擇 LGPL 授權,但是因為非核心模組之間的授權不一, 如果使用 Qt 框架開發商業軟體需要仔細研究每個使用模組的授權以避免踩雷)。 而 GNOME 2 到 GNOME 3 引進了激烈的變動,又產生了幾個桌面環境的專案, 包含不贊同 GNOME 3 的改變而延續 GNOME 2 開發的 MATE, 以及基於 GNOME 3 原始碼實現 GNOME 2 功能的 Cinnamon

另外還有 Xfce,設計目的是「設計為可作為實際應用,快速載入及執行程式,並減少耗用系統資源」, 原本使用 XForms,之後改用 GTK 開發,是一個較為輕量但仍然提供豐富功能的桌面環境。

雖然 Xfce 已經比 KDE 與 GNOME 更為輕量,但是如果需要資源需求更少的視窗管理員, 可以考慮 IceWM

就我個人而言,我認為 GNOME 3 過度激烈的變動產生了很多問題,因此導致了很多重覆開發的桌面環境, 雖然使用者有很多選擇,但是也造成了開發資源上的浪費。

X11 and Wayland

目前 KDE Plasma 支援 X11 以及 Wayland 協定,openSUSE Tumbleweed 目前的預設為 X11(未來預設可能換成 Wayland)。 如果只是要暫時切換,logout 以後找到選擇 session 設定的地方設定即可。

首先確認 Display Manager 為 SDDM,如果是, 那麼如果要將登入的時候預設選項改為 Wayland,將 /etc/sddm.conf.d 目錄下的 kde_settings.conf 從 default 改為下列的值:

[Autologin]
Relogin=false
Session=plasmawayland
User=

有效的選項可以查看 /usr/share/xsessions 以及 /usr/share/wayland-sessions 二個目錄下的設定。

如果要查看目前的設定,在命令列使用下列的指令查看:

echo $XDG_SESSION_TYPE

PulseAudio and PipeWire

二者我都有用過,都工作的很好。

PipeWire 是目前 openSUSE Tumbleweed 的預設值 (在 20220708 之後,不過如果是從 openSUSE Leap 升上來的,如果使用者沒有手動變更,便用的仍然是 PulseAudio)。 就我來說,建議使用 PipeWire,因為提供了 PulseAudio 相容的軟體介面,但是延遲較低且通常使用的資源比較少。

下面是安裝的指令:

sudo zypper in pipewire-pulseaudio pipewire-alsa

要注意與 PulseAudio 無法共存,所以要選擇 deinstallation PulseAudio 的套件。

輸入法

目前 Linux 主流的輸入法框架為 IBus (Intelligent Input Bus), 以及 Fcitx (Free Chinese Input Tool for X)。

因為 Linux 是個自由的環境,所以關於輸入法的問題可能在各個層面產生。 就目前而言,輸入法框架我會比較推薦使用 Fcitx 5。 我之前使用的輸入法框架為 IBus,因為我是行列輸入法的使用者, 而 IBus 有 ibus-array, 相對符合我的使用習慣。不過因為 Fcitx 5 提供了足夠好的 sample code, 所以在我自己寫出來 fcitx5-array 之後, 目前我已經切換到 Fcitx 5,只有少數時候會使用 IBus。

Plasma desktop behaves strangely

Plasma problems are usually caused by unstable Plasma widgets (colloquially called plasmoids) or Plasma themes. First, find which was the last widget or theme you had installed and disable or uninstall it.

If you cannot find the problem, but you do not want all the settings to be lost, navigate to ~/.config/ and run the following command:

for j in plasma*; do mv -- "$j" "${j%}.bak"; done

Edit KDE Application Launcher Menus

使用 kmenuedit 這個程式編輯。

KDE File Associations

You can start this module by opening System Settings and selecting File Associations in the Common Appearance and Behavior category. Alternatively, you can start it by typing kcmshell6 filetypes from the terminal or KRunner.

Bookmarks Editor

KDE 一些程式(例如 Konsole)的 Edit Bookmarks 使用 keditbookmarks 實作, 如果沒有安裝,在選單上就不會出現相關的選項。

Double click

修改 ~/.config/kdeglobals,找到 [KDE],然後加入下面的設定:

SingleClick=false
就可以設定要 double click 才會啟動程式或者是開啟檔案。

Disable KWallet

KDE 錢包(KWallet)是一款密碼管理加密服務,可以幫助你記住所有的密碼,並加密確保密碼的安全。 你只需要將要記住的密碼加入到 KWallet 中,KWallet 會將儲存密碼加密,確保安全性。當軟體需要輸入密碼時會自動呼叫 KWallet, 輸入錢包密碼後,就會自動完成輸入密碼的工作。

如果你不需要 KWallet 並且想要永久停止 KWallet,編輯~/.config/kwalletrc

[Wallet]
Enabled=false

PPPOE (DSL setting)

使用 KDE Network Manager 管理連線,需要在 YaST Network setting 將設定網路的管理者設為 Network Manager

接下來在 KDE Network Manager 新增加連線。
按下 Create button,下一步是設定服務商所提供的 Username 與 Password。

儲存以後就可以試著連線看設定是否正確。

我的應用程式列表

程式用途 程式名稱 備註
檔案管理員 Dolphin

使用 KDE 內建的檔案管理員,支援分割畫面功能。

網頁瀏覽器 Mozilla Firefox

Firefox 支援佈景主題 (Themes),我偏好使用 Dark 佈景主題。

下面是我目前有在使用的套件:

  • uBlock Origin: 高效率的廣告攔截工具
  • Decentraleyes: 用於保護使用者免遭集中的內容交付網路(CDN)的跟蹤
  • Privacy Badger: 用於阻止那些不遵守 DNT 協定的廣告商跟蹤行為
  • Flagfox: 以旗幟圖示顯示出目前網頁伺服器實際位置的擴充套件
  • Livemarks: Get auto-updated RSS feed bookmark folders
BitTorrent 客戶端 KTorrent

與 KGet 使用相同的 BitTorrent 函式庫,所以也可以考慮使用 KGet。

辦公室軟體 LibreOffice

LibreOffice 套件包含文書處理器、電子試算表、演示文稿程式、向量圖形編輯器和圖表工具、 資料庫管理程式及建立和編輯數學公式的應用程式。

文字編輯器 Kate

Settings->Configure Kate->Appearance->Borders->Show minimap 不要勾選選項
Settings->Configure Kate->Open/Save->Remove trailing spaces:選擇 Never
同時我習慣取消 LSP Client plugin。

數位影像繪圖 GIMP

跨平台的影像編輯軟體。

向量圖型編輯 Inkscape

跨平台的向量圖型編輯軟體。

文件檢視器 Okular

用來閱讀 PDF 文件。CHM 文件支援部份 openSUESE 沒有加入編譯選項,需要安裝 KchmViewer。
PDF 檔案也可以使用內建 pdf.js 的 Firefox 閱讀。

圖像瀏覽器 Gwenview

目前預設的圖像瀏覽器

音樂播放器 Elisa

KDE 的另外一個選擇是 Amarok,都是不錯的音樂播放器。

多媒體播放器 VLC media player

跨平台的多媒體播放器

虛擬機器 Vistual Box 安裝後需要加入使用者到 vboxusers 群組(以帳號 danilo 為例):
sudo usermod -a -G vboxusers danilo
檢查是否加入成功:
groups danilo
加入群組成功後,還需要重開機一次才會真的生效。

如果要移除,首先將使用者從群組中移除:
sudo gpasswd -d danilo vboxusers
再來移除軟體:
sudo zypper remove --clean-deps virtualbox

2024/04/09

w3m browser

w3m 是由日本東北大學教授伊藤彰則等人開發的文字網頁瀏覽器。 w3m 支援表格、框架、SSL連線、顏色,但是不支援 CSS 樣式與 JavaScript。

使用下列的指令在 openSUSE Tumbleweed 下安裝:

sudo zypper in w3m

(如果需要 inline-image,還要安裝 w3m-inline-image)


上下與 Page Up/Page Down 可以用來瀏覽網頁,TAB 鍵可以在連結之間跳耀。
左鍵與右鍵可以在瀏覽網頁時往前一頁或者是往後一頁。使用 Enter 選擇連結可以前往選擇的網址。
在網頁需要輸入文字的地方,需要按 Enter,然後才輸入文字。
按 U 鍵可以輸入 URL 並且前往新的網址。
按 V 鍵可以輸入檔案名稱並且瀏覽檔案。
按 R 鍵可以重讀網頁。
使用 / 可以在網頁搜尋。
按 T 鍵可以新開一個 browser tab,使用 { 與 } 可以在 tabs 之間切換(或者是 Shift + [ 與 Shift + ])。
也可以對一個連結按 Ctrl-t,這樣就會使用這個連結開啟新的 browser tab。
如果要關閉 browser tab,使用 Ctrl-q 關閉。
當我們要退出此程序時,只需按 q 鍵,然後接著 y 確認退出。(也可以使用 Q 鍵,不過這樣就不會有確認訊息)

另外,可以藉由設定 HTTP_HOME 或者 WWW_HOME 環境變數, 這樣執行 w3m 但是在命令列參數不指定網址時就會前往環境變數設定的網址。

2024/04/08

Midnight Commander

Midnight Commander 是一套文字使用者介面下的跨平台檔案管理器, 主介面由兩個顯示檔案系統的面板組成,支援使用滑鼠,由 Miguel de Icaza 於 1994 年創立,作為 Norton Commander 的克隆版本。 從 4.7.0 版開始,Midnight Commander 已經支援 Unicode。

使用下列的指令在 openSUSE Tumbleweed 下安裝:

sudo zypper in mc

安裝後可以使用下列的命令查看版本資訊:

mc -V

Midnight Commander 提供了一些內建的工具(並且有其檔案連結可以使用),

  • mcedit - Text and binary file editor, with regex replace, syntax highlighting, macros and shell piping
  • mcview - Text and hex viewer with goto marks and regex search
  • mcdiff - Compares and edits two files in-place

如果要在二個左右兩個面板間切換,使用 TAB。
Ctrl-u 可以用來交換左右兩個面板。
如果你需要讓目前的使用的面板以及另外一個面板同一個目錄,使用 Alt-i。
如果只需要一個面板的時候,可以使用 Alt-t 來嘗試切換成為只有一個面板。
如果要啟用 User menu,使用 F2 按鍵。
如果要查看某個檔案的內容,使用 F3 按鍵(然後在 viewer 內按 F4 可以切換為 hex mode)。
如果要編輯檔案,使用 F4 按鍵(如果沒有特別設定,使用內建的文字編輯器)。
如果要啟用 menu bar,使用 F9 按鍵。
在程式的最下方是 Function Key 列表 (Functin key F1 ~ F10,也可以使用 Alt + 數字鍵代替,其中 F10 使用的數字鍵為 0)。
Shell 指令可以在底部的小命令列中使用。 只需像平常一樣輸入命令即可。使用 Ctrl-o 可以將在底部的小命令列切換為全螢幕的 subshell。

如果要在命令列直接使用內建的文字編輯器編輯檔案,使用 -e 選項:

mc -e filename

(如果安裝時有建立 mcedit 的連結,也可以使用 mcedit 編輯)

2024/03/24

VIM


以下是基本操作方法:
VIM 共分為三種模式,分別是指令模式編輯模式指令列命令模式
輸入:在指令模式按 a、i、o,就會進入編輯模式,這時候可以編輯文件。 如果要回到指令模式時, 則必須要按下 Esc 這個按鍵退出編輯模式。

到第一行:gg
到最後一行:G
到某一行:xxG (xx 要代入行數數字)
到行首:0
到行末:$

複製:yy
貼上:p
復原:u
把剛剛回復的動作再取消:ctrl-r

刪一個字:x
刪整行:dd

使用指令列命令模式儲存檔案或者是離開。
儲存: :w
離開: :q
儲存兼離開 ::wq 或 shift+zz
強制離開(不儲存): :q!
設定儲存的檔名: :w filename

Add space or words to mufti-line by using VIM

1) 按 CTRL + V 進入 visual mode
2) 按 j 選擇要進行操作的行數(要多選一行,就多按一個 j)
3) 接下來按 I,開始插入要插入的文字
4) 如果是要插入空白,就插入空白,如果是文字,就插入要插入的文字
5) 按 ESC 離開

如果要刪除空白字元或者是 TAB,可以使用反縮排 < 或者是退格 X 的功能;也就是第三步要做的事情。

Replace

如果要在 VIM 中全域取代字串,下面是一個例子
:%s/foo/bar/g

所以移除行尾的空白可以這樣做:

:%s/\s\+$//g
如果只是在該列取代字串,下面是一個例子:
:s/foo/bar/g

If you open a file that uses both tabs and spaces, assuming you've got

:set expandtab ts=4 sw=4 ai

You can replace all the tabs with spaces in the entire file with

:%retab

expandtab 會將 tab 填為 4 個 spaces;而 ai 為 autoindent,會複製目前的 indent到新的一行。 如果只是想更改 TAB 的顯示長度,可以使用:

:set tabstop=4 shiftwidth=4

開啟/關閉vim的vi相容模式

一般都會使用以下指令來關閉 vim 的 vi 相容模式(並且放在設定第一行):

:set nocompatible

文字編碼

將 Vim 內部對於文字編碼方式改為 UTF-8:

set encoding=utf8

VIM 會自動偵測文字檔案的編碼,當然有可能會偵測錯誤,可以使用下列的指令讓 VIM 使用某個編碼重讀檔案:

:e ++enc=encoding

其中 encoding 需要代換為 big5, utf8 等編碼方式。

開啟/關閉vim的自動縮排功能

vim的自動縮排有「autoindent」、「smartindent」和「cindent」三種。「autoindent」是最基本的縮牌方式, 會在建立出新的一行的同時,自動對齊上一行的縮排。「smartindent」則是會在「autoindent」的基礎上, 針對「{」(左大括號)後所產生的新行,再多往內縮排一次,且若新行的開頭是「}」(右大括號),則會取消往內縮多排一次的動作。 而「cindent」則是會在「smartindent」的基礎上,加入簡單的C/C++、Java等程式語法結構的判斷, 如果「{」是發生在註解之內,就不會進行縮排,並且也不是根據新行的上一行來決定新行的縮排, 而是會根據新行所在的程式區塊(block)來決定,另外也會根據其他符號進行縮排。

:set cindent

使用以下指令可以關閉vim的「cindent」自動縮排功能:

:set nocindent

設定「backspace」鍵的功能

vim可以透過「backspace」環境變數,設定其在編輯模式下,鍵盤上「backspace」鍵的功能。可以使用以下指令來開啟「backspace」鍵的所有功能:

:set backspace=indent,eol,start

或者是:

:set backspace=2

「indent」表示允許使用「backspace」鍵來刪除縮排;「eol」表示允許使用「backspace」來刪除換行字元, 使其可以退回至上一行;「start」表示允許使用「ctrl+w」和「ctrl+u」快速鍵來刪除獨立詞語和同類字元(縮排、非縮排字元)。

語法高亮

下面是設定將語法高亮開啟的方式:

:syntax on

個人設定檔案

VIM 可以在家目錄下的 .vimrc 設置自己的 VIM 設定,所以參考上面的設定,可以這樣寫:

set nocompatible
set encoding=utf8
set backspace=indent,eol,start
set tabstop=4 shiftwidth=4
syntax on

Shell command

有二個方式,一個是執行一個 command(! 然後加上要執行的 command):

:!command

一個是執行目前預設的 shell:

:shell

Hex editor

如果要使用 VIM 編輯二進位檔案,可以配合外部工具程式 xxd 將資料轉換為 16 進位的排版格式。
在 Vim 中輸入以下的指令然後按下 Enter 鍵。

:%! xxd

編輯後如果要轉回原來的格式,一樣使用外部工具程式 xxd

:%! xxd -r

如果編輯後要存檔,使用 VIM 的儲存檔案指令 :w

2024/03/10

SQLite

SQLite 是一個很小的 C 語言函式庫。 這個函式庫本身就完全包含資料庫引擎的功能,而且可以嵌入至其他程式中。

SQLite 支援 In-Memory 與 Disk-based 等形式的使用方式, 下面是一個使用 C API 來開啟一個 In-Memory Database 的例子(使用 ":memory:" 檔名):

rc = sqlite3_open(":memory:", &db);

列舉資料庫中的表格

From within a C/C++ program (or a script using Tcl/Ruby/Perl/Python bindings) you can get access to table and index names by doing a SELECT on a special table named "SQLITE_MASTER". Every SQLite database has an SQLITE_MASTER table that defines the schema for the database. The SQLITE_MASTER table looks like this:

CREATE TABLE sqlite_master (
  type TEXT,
  name TEXT,
  tbl_name TEXT,
  rootpage INTEGER,
  sql TEXT
);
所以我們可以使用下列的方式來列舉資料庫中的表格:
SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;

Auto-increment column

下面二種寫法都會建立 autoincrementing column; the AUTOINCREMENT keyword only prevents reusing deleted values.
CREATE TABLE t1 (col1 INTEGER PRIMARY KEY);
CREATE TABLE t1 (col1 INTEGER PRIMARY KEY AUTOINCREMENT);

UPSERT

對於 SQLite 而言,有幾種方式提供 UPSERT 的語法。SQLite 提供了 REPLACE INTO 的語法,同時提供了與 PostgreSQL 類似的 INSERT INTO ... ON CONFLICT clause 的方式來處理。

C API

使用 C 語言開發 Sqlite 的程式非常容易,下面是一個簡單的例子:
#include <sqlite3.h>
#include <stdio.h>

static char *createsql = "CREATE TABLE Contact("
                         "ID INTEGER PRIMARY KEY,"
                         "Name VARCHAR(10),"
                         "PhoneNumber VARCHAR(10));";

static char *insertsql =
    "INSERT INTO Contact VALUES(1, 'Richard', '09990123456');";

static char *querysql = "SELECT * FROM Contact;";

void main(void) {
    int rows, cols;
    sqlite3 *db;
    char *errMsg = NULL;
    char **result;
    int i, j;

    if (sqlite3_open_v2("example.db3", &db,
                        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL)) {
        return;
    }

    sqlite3_exec(db, createsql, 0, 0, &errMsg);
    sqlite3_exec(db, insertsql, 0, 0, &errMsg);

    printf("%d\n", sqlite3_last_insert_rowid(db));

    sqlite3_get_table(db, querysql, &result, &rows, &cols, &errMsg);
    for (i = 0; i <= rows; i++) {
        for (j = 0; j < cols; j++) {
            printf("%s\t", result[i * cols + j]);
        }
        printf("\n");
    }

    sqlite3_free_table(result);
    sqlite3_close(db);
}

編譯時只要連結 sqlite 函式庫:

gcc  -lsqlite3  -o sqlite_ex sqlite_ex.c

2024/02/21

ODBC

ODBC(Open Database Connectivity)提供了一組標準的 API 來訪問資料庫管理系統(DBMS)。 這些 API 利用 SQL 來完成其大部分任務。 ODBC 是針對 C 的 API,不過很多的程式語言都有相關的 bindings。如果想使用 C 來寫 ODBC 程式, 可以從 ODBC from C Tutorial Part 1 開始學習如何撰寫。

The ODBC architecture has four components:

  • Application Performs processing and calls ODBC functions to submit SQL statements and retrieve results.

  • Driver Manager Loads and unloads drivers on behalf of an application. Processes ODBC function calls or passes them to a driver.

  • Driver Processes ODBC function calls, submits SQL requests to a specific data source, and returns results to the application. If necessary, the driver modifies an application's request so that the request conforms to syntax supported by the associated DBMS.

  • Data Source Consists of the data the user wants to access and its associated operating system, DBMS, and network platform (if any) used to access the DBMS.

In ODBC there are four main handle types and you will need to know at least three to do anything useful:

  • SQLHENV - environment handle

    This is the first handle you will need as everything else is effectively in the environment. Once you have an environment handle you can define the version of ODBC you require, enable connection pooling and allocate connection handles with SQLSetEnvAttr and SQLAllocHandle.

  • SQLHDBC - connection handle

    You need one connection handle for each data source you are going to connect to. Like environment handles, connection handles have attributes which you can retrieve and set with SQLSetConnectAttr and SQLGetConnectAttr.

  • SQLHSTMT - statement handle

    Once you have a connection handle and have connected to a data source you allocate statement handles to execute SQL or retrieve meta data. As with the other handles you can set and get statement attributes with SQLSetStmtAttr and SQLGetStmtAttr.

  • SQLHDESC - descriptor handle

    Descriptor handles are rarely used by applications even though they are very useful for more complex operations. Descriptor handles will be covered in later tutorials.

如果要查詢目前 unixODBC 的版本,使用下列的指令:

odbcinst --version

Listing Installed Drivers and Data Sources

如果是使用 unixODBC,可以使用下列命令列出已安裝的資料來源:

odbcinst -q -s

如果自己寫一個程式列出來已安裝的資料來源:

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main() {
    SQLHENV env = NULL;
    char driver[256];
    char attr[256];
    SQLSMALLINT driver_ret;
    SQLSMALLINT attr_ret;
    SQLUSMALLINT direction;
    SQLRETURN ret;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (ret != SQL_SUCCESS) {
        printf("SQLAllocHandle failed.\n");
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);

    direction = SQL_FETCH_FIRST;
    while (SQL_SUCCEEDED(ret = SQLDrivers(env, direction, driver,
                                          sizeof(driver), &driver_ret, attr,
                                          sizeof(attr), &attr_ret))) {
        direction = SQL_FETCH_NEXT;
        printf("%s - %s\n", driver, attr);
        if (ret == SQL_SUCCESS_WITH_INFO)
            printf("\tdata truncation\n");
    }

    if (env != NULL) {
        SQLFreeHandle(SQL_HANDLE_ENV, env);
    }
    return 0;
}

在 Windows 平台,需要連結 odbc32 library,使用 UnixODBC 平台則需要連結 libodbc (-lodbc) 才行。

接下來使用 PostgreSQL ODBC driver 測試,測試是否能夠正確連接到資料庫。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

void extract_error(char *fn, SQLHANDLE handle, SQLSMALLINT type) {
    SQLINTEGER i = 0;
    SQLINTEGER native;
    SQLCHAR state[7];
    SQLCHAR text[256];
    SQLSMALLINT len;
    SQLRETURN ret;

    fprintf(stderr,
            "\n"
            "The driver reported the following diagnostics whilst running "
            "%s\n\n",
            fn);

    do {
        ret = SQLGetDiagRec(type, handle, ++i, state, &native, text,
                            sizeof(text), &len);
        if (SQL_SUCCEEDED(ret))
            printf("%s:%ld:%ld:%s\n", state, i, native, text);
    } while (ret == SQL_SUCCESS);
}

int main() {
    SQLHENV env = NULL;
    SQLHDBC dbc = NULL;
    SQLRETURN ret;
    SQLCHAR outstr[1024];
    SQLSMALLINT outstrlen;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return(1);
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return(1);
    }

    ret = SQLDriverConnect(dbc, NULL, "DSN=PostgreSQL;", SQL_NTS, outstr,
                           sizeof(outstr), &outstrlen, SQL_DRIVER_COMPLETE);
    if (SQL_SUCCEEDED(ret)) {
        printf("Connected\n");
        printf("Returned connection string was:\n\t%s\n", outstr);
        if (ret == SQL_SUCCESS_WITH_INFO) {
            printf("Driver reported the following diagnostics\n");
            extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
        }
        SQLDisconnect(dbc); /* disconnect from driver */
    } else {
        fprintf(stderr, "Failed to connect\n");
        extract_error("SQLDriverConnect", dbc, SQL_HANDLE_DBC);
    }

    if (dbc)
        SQLFreeHandle(SQL_HANDLE_DBC, dbc);

    if (env)
        SQLFreeHandle(SQL_HANDLE_ENV, env);

    return 0;
}

下面列出目前資料庫中表格的資料。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

int main() {
    SQLHENV env = NULL;
    SQLHDBC dbc = NULL;
    SQLHSTMT stmt = NULL;
    SQLRETURN ret;
    SQLSMALLINT columns; /* number of columns in result-set */
    SQLCHAR buf[5][64];
    int row = 0;
    SQLLEN indicator[5];
    int i;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(dbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0, NULL,
                           SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    SQLTables(stmt, NULL, 0, NULL, 0, NULL, 0, "TABLE", SQL_NTS);
    SQLNumResultCols(stmt, &columns);
    for (i = 0; i < columns; i++) {
        SQLBindCol(stmt, i + 1, SQL_C_CHAR, buf[i], sizeof(buf[i]),
                   &indicator[i]);
    }

    /* Fetch the data */
    while (SQL_SUCCEEDED(SQLFetch(stmt))) {
        /* display the results that will now be in the bound area's */
        for (i = 0; i < columns; i++) {
            if (indicator[i] == SQL_NULL_DATA) {
                printf("  Column %u : NULL\n", i);
            } else {
                printf("  Column %u : %s\n", i, buf[i]);
            }
        }
    }

End:
    if (stmt)
        SQLFreeHandle(SQL_HANDLE_DBC, stmt);

    if (dbc)
        SQLFreeHandle(SQL_HANDLE_DBC, dbc);

    if (env)
        SQLFreeHandle(SQL_HANDLE_ENV, env);

    return 0;
}

接下來的程式使用 SQLExecDirect 與 SQLFetch 取得 PostgreSQL 的版本資訊。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;

    SQLRETURN ret;
    SQLCHAR Version[255];
    SQLLEN siVersion;

    char *sqlStatement = "Select version() as version";

    ret = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(hdbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0,
                           NULL, SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLAllocHandle(SQL_HANDLE_STMT) failed.\n");
        goto End;
    }

    ret = SQLExecDirect(hstmt, sqlStatement, strlen(sqlStatement));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecDirect() failed.\n");
        goto End;
    }

    while (1) {
        ret = SQLFetch(hstmt);
        if (ret == SQL_ERROR || ret == SQL_SUCCESS_WITH_INFO) {
            fprintf(stderr, "SQLFetch(hstmt) failed.\n");
            goto End;
        }

        if (ret == SQL_NO_DATA) {
            break;
        }

        if (ret == SQL_SUCCESS) {
            ret = SQLGetData(hstmt, 1, SQL_C_CHAR, Version, 255, &siVersion);
            printf("Versoin:\n%s\n", Version);
        }
    }

End:

    if (hstmt != SQL_NULL_HSTMT) {
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        hstmt = SQL_NULL_HSTMT;
    }

    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        hdbc = SQL_NULL_HDBC;
    }

    if (henv != SQL_NULL_HENV) {
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
        hstmt = SQL_NULL_HSTMT;
    }

    return 0;
}

下面是 SQLPrepare/SQLExecute 的範例(包含使用 SQLBindParameter)。

#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    SQLHENV henv = SQL_NULL_HENV;
    SQLHDBC hdbc = SQL_NULL_HDBC;
    SQLHSTMT hstmt = SQL_NULL_HSTMT;

    SQLRETURN ret;
    int RetParam = 1;
    SQLLEN cbRetParam = SQL_NTS;
    SQLCHAR Name[40];
    SQLLEN lenName = 0;
    SQLSMALLINT NumParams;

    char *sqlstr = NULL;

    ret = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0);
    ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
    if (!SQL_SUCCEEDED(ret)) {
        printf("SQLAllocHandle failed.\n");
        return (1);
    }

    ret = SQLDriverConnect(hdbc, NULL, "DSN=PostgreSQL;", SQL_NTS, NULL, 0,
                           NULL, SQL_DRIVER_COMPLETE);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "Failed to connect\n");
        goto End;
    }

    ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLAllocHandle(SQL_HANDLE_STMT) failed.\n");
        goto End;
    }

    sqlstr = "drop table if exists person";
    ret = SQLPrepare(hstmt, (SQLCHAR *)sqlstr, strlen(sqlstr));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare() failed.\n");
        goto End;
    }

    ret = SQLExecute(hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecute(drop) failed.\n");
        goto End;
    }

    sqlstr = "create table if not exists person (id integer, name varchar(40) "
             "not null)";
    ret = SQLPrepare(hstmt, (SQLCHAR *)sqlstr, strlen(sqlstr));
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare(create) failed.\n");
        goto End;
    }

    ret = SQLExecute(hstmt);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLExecute() failed.\n");
        goto End;
    }

    ret = SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_SLONG, SQL_INTEGER,
                           0, 0, &RetParam, 0, &cbRetParam);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLBindParameter(1) failed.\n");
        goto End;
    }

    ret = SQLBindParameter(hstmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 40,
                           0, Name, 40, &lenName);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLBindParameter(2) failed.\n");
        goto End;
    }

    ret = SQLPrepare(hstmt,
                     (SQLCHAR *)"insert into person (id, name) values (?, ?)",
                     SQL_NTS);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLPrepare(insert) failed.\n");
        goto End;
    }

    SQLNumParams(hstmt, &NumParams);
    if (!SQL_SUCCEEDED(ret)) {
        fprintf(stderr, "SQLNumParams() failed.\n");
        goto End;
    }

    printf("Num params : %i\n", NumParams);
    strcpy(Name, "Orange");
    lenName = strlen(Name);

    ret = SQLExecute(hstmt);
    if (ret == SQL_SUCCESS || ret == SQL_SUCCESS_WITH_INFO) {
        printf("Status : Success\n");
    } else {
        fprintf(stderr, "SQLExecute(insert) failed.\n");
    }

End:

    if (hstmt != SQL_NULL_HSTMT) {
        SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
        hstmt = SQL_NULL_HSTMT;
    }

    if (hdbc != SQL_NULL_HDBC) {
        SQLDisconnect(hdbc);
        SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
        hdbc = SQL_NULL_HDBC;
    }

    if (henv != SQL_NULL_HENV) {
        SQLFreeHandle(SQL_HANDLE_ENV, henv);
        hstmt = SQL_NULL_HSTMT;
    }

    return 0;
}

C++ Regular Expression

Posix Regular Expression

Posix Regular Expression 是 POSIX 所建立的其中一個標準(並且有 Basic 與 Extended 的分別), 大多數有支援 POSIX 標準的 libc library 都有實作(包含 glibc), 不過實作的細節可能在各個實作上會略有不同。因此,雖然 glibc 有內建,除非要相容於 GNU 工具程式的正規表示式, 否則不一定要使用 Posix Regular Expression。 對於 C 而言,最常被考慮的跨平台 Regular Expression library 為 PCRE 或者是 PCRE2。

下面是 1-9 位數不重複印出來的練習問題(在 Linux 測試,使用 glibc):

#include <stdio.h>
#include <math.h>
#include <regex.h>

int main() {
    regex_t re;
    int number = 0;
    long max = 0;

    printf("Please give a number: ");
    scanf("%d", &number);

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    regcomp(&re, "([0-9]).*\\1", REG_EXTENDED);
    for (long index = 1; index <= max; index++) {
        int value;
        char str[10];
        sprintf(str, "%ld", index);
        value = regexec(&re, str, 0, NULL, 0);

        if (value == REG_NOMATCH) {
            printf("%ld\n", index);
        }
    }

    regfree(&re);
    return 0;
}

PCRE 或者是 PCRE2 都有提供 Posix Regular Expression 相容的 API, 只是 Regular Expression 語法就沒有 Basic 與 Extended 的分別, 而是使用 PCRE 本身的語法。以 PCRE2 來說,只要改為 include pcre2posix.h, 連結時要加上 -lpcre2-posix-lpcre2-8 即可。

#include <stdio.h>
#include <math.h>
#include <pcre2posix.h>

int main() {
    regex_t re;
    int number = 0;
    long max = 0;

    printf("Please give a number: ");
    scanf("%d", &number);

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    regcomp(&re, "([0-9]).*\\1", 0);
    for (long index = 1; index <= max; index++) {
        int value;
        char str[10];
        sprintf(str, "%ld", index);
        value = regexec(&re, str, 0, NULL, 0);

        if (value == REG_NOMATCH) {
            printf("%ld\n", index);
        }
    }

    regfree(&re);
    return 0;
}

C++ Regular Expression

自 C++11 開始,C++ 標準函式庫提供了 Regular Expression library。

下面是 1-9 位數不重複印出來的練習問題:

#include <cmath>
#include <iostream>
#include <regex>
#include <string>

int main() {
    int number = 0;
    long max = 0;

    std::cout << "Please give a number: ";
    std::cin >> number;

    if (number < 1 || number > 9) {
        printf("Out of range.\n");
        return (1);
    }

    max = round(pow(10, number)) - 1;
    std::regex re("([0-9]).*\\1",  std::regex_constants::ECMAScript);
    for (long index = 1; index <= max; index++) {
        std::string s = std::to_string(index);

        std::smatch m;
        std::regex_search(s, m, re);

        if (m.empty()) {
            std::cout << index << std::endl;
        }
    }

    return 0;
}

2024/02/03

GNU Make

GNU Make 是一個工具程式, 經由讀取 Makefile 或者是 makefile 的檔案(也可以使用 -f 指定),自動化建構軟體。 Makefile 是由很多相依性項目(dependencies)和規則(rules)所組成。

GNU Makefile 可以在各個程式語言使用,而不僅限於 C 或者是 C++。
不過因為通常是使用 C 或者是 C++,所以接下來使用 C 作為例子,考慮一個簡單的程式 hello.c:

void say(const char *name);

int main() {
    say("Orange");

    return 0;
}

以及 say procedure 的實作:

#include <stdio.h>

void say(const char *name) { printf("Hello, %s.\n", name); }

那麼在命令列編譯的指令如下:

gcc hello.c say.c -o hello

Makefile 的規則如下:

target [target ...]: [component ...]
	Tab ↹[command 1]
		.
		.
		.
	Tab ↹[command n]

Makefile 支援 Suffix rules,例子如下:

.SUFFIXES: .txt .html

# From .html to .txt
.html.txt:
    lynx -dump $<   >   $@

Makefile 支援 Pattern rules,例子如下:

# From %.html to %.txt
%.txt : %.html
    lynx -dump $< > $@

其中 $@ 或者是 $<, $^, $? 都是 Makefile 的巨集。$@ 代表工作目標的完整檔案名稱,$< 代表觸發建制目標的檔案名稱。 $^ 代表所有的依賴檔案,並以空格隔開這些檔名。$? 代表比目標還要新的依賴檔案列表。而 $* 代表工作目標的主檔名(也就是不包含副檔名)。

撰寫一個 Makefile 如下:

CC = /usr/bin/gcc
CFLAGS= -O2 -Wall
PROGRAM = hello
SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,%.o,$(SRCS))

RM = rm

all: $(PROGRAM)

$(PROGRAM): $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) -o $@

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	$(RM) $(PROGRAM) *.o

.PHONY: all clean

2024/01/22

Bye bye, FreeBASIC

我曾經想過為什麼會有人想要寫一個 bye bye 的文章,以前的我沒有搞懂,現在我明白了,因為太過不爽,所以需要寫一篇文章記錄才行。

最近一個月我又(因為之前有試過一次)嘗試看看 FreeBASIC,然後就發現 ODBC 還是一樣只有支援 Windows 平台。我之前就是因為這樣,試到這裡的時候直接放棄,這表示 FreeBASIC 並沒有真正的跨平台,也可以說雖然可以編譯不同平台的原始碼,但是函式庫上的支援很糟糕(小提示:還有一些小地方可以得到這個結論,你可以觀察 crt 下面的 header files,例如 stat 和 select 的部份)。

這一次我比較深入的觀察 FreeBASIC ODBC headers,然後才發現其實只要修改關鍵的型別,加上一些 Windows 上使用的自定型別(例如 WORD),ODBC 程式就可以連結 unixODBC 然後順利編譯並且正確執行。

我發現 FreeBASIC 使用 fbfrog 執行轉換的工作。所以接下來我使用 fbfrog 試著轉換了幾個 FreeBASIC 目前沒有的函式庫(包含 libao, libarcihve, libmicrohttpd, taglib c bindings, and lmdb),雖然有些地方需要手動,不過因為這些函式庫我之前有用過,所以知道怎麼修正問題。

然後我想應該要將結果分享給社群,這樣也可以讓我知道我是不是有地方沒有搞定。我一開始分享在 Community Forum 的訊息是 FreeBASIC ODBC headers (unixODBC version),而文章是需要審核的,所以等待一小段時間以後就看到文章出現了。

接下來我將我轉換的函式庫 header files 資訊貼上去(因為是不同的函式庫,所以集中在一篇文章並不合理,我一個函式庫寫一篇文章),然後等待審核。結果下午我發現我無法登入,我被判定為 spammer,而之前審核通過的文章也直接被刪除。因為文章是有人審核的 ,所以也不要說是程式誤判。那麼如果與 FreeBASIC 相關的文章都無法發表,那是要交流什麼?這個程式語言社群並不歡迎他的使用者。

我很慶幸我在一開始使用就遇到這件事,讓我不會花更多的時間在這個程式語言上,我把我放在 Github 上的成果全部刪除,並且移除了電腦上安裝的 FreeBASIC。我想,就這樣吧,最後說一聲 bye bye。