# 攝影機學習 ## OV2640使用 : 因為SCCB協議遇見問題,直接買OV2640做使用,這裡原廠已經處理好ov2640到rgblcd的驅動,因此只要寫應用就好。 我以為這裡很簡單,結果這裡遇到超級大坑。 ### 硬體架構: 先說硬體架構,左邊為lcd設備,為800*480,**格式為ARGB888**,右邊為OV2640,因為lcd設備是原先就已經設定好了,所以只去研究ov2640使用。 ![15451](https://hackmd.io/_uploads/Sy6o2YYh1x.jpg) 開發版使用I2C協議設定OV2640相關參數,CSI協議去捕捉訊號: ![image](https://hackmd.io/_uploads/Syj_pFt2Jx.png) 使用的cpu引腳如下: ![image](https://hackmd.io/_uploads/BknPCKt3ke.png) ### 通訊協議: 這裡介紹一下ov2640的通訊協議,跟ov7670很像,我猜ov系列都採用這種架構。 ov2640內部邏輯: ![image](https://hackmd.io/_uploads/ByXtz5Knyl.png) 看懂邏輯之後就能懂各種引腳怎麼使用,首先是xvclk,需要給ov2640一個clk訊號,我使用的ov2640已經具有內部晶振12mhz,所以無須提供,另外有一個PLL,可以放大兩倍clk,用於提高ov2640的fps。 而sio_c和sio_d當作i2c協議即可,因為driver部分也是用i2c2的driver,使用後可設定ov2640相關參數。 resetb 和 pwdn 就用在reset和低功率模式,reset可以在改寫i2c參數後重啟設備,更改其設定。 而**pclk herf vsync** 則是輸出圖片資料時的相關訊號,類似輸出的clk,ov2640輸出資料的引腳同樣是8位,也就是每次能傳遞8bit。 輸出訊號如下: ![image](https://hackmd.io/_uploads/BkA1HqFn1g.png) 每個vsync訊號影片代表新的一個frame,也就是一張圖,href訊號每個positive edge期間代表圖片的每行,pclk代表一組8bit的data準備好能讀取了,可以設定pclk是 positive 還是 negative讀取data。 以rgb565 (red 5bit green 6bit blue 5bit 1個pixel 16bit) 舉例,假設是 640*480 的圖片,vsync 來訊號後,第一個row有640 pixel,這段pixel輸出期間href為positive,表示為第一行,而一個pixel有16bit,代表需要送兩次8bit的資料,所以pclk需要傳遞兩次,而到下個row期間herf會下降再上升,代表第二個row,以此類推直到下張圖片vsync會上升在下降。 ## 軟體架構 軟體分為應用和驅動 ### OV2640 driver: driver 使用廠商提供的driver,這裡本來懶得研究的,結果遇到bug,害我整個driver幾乎都看過了。 主要程式分別是ov2640.c和mx6s_capture.c 細節請看github。 兩者皆是採用v4l2架構寫成,要寫成整體架構如下: #### mx6s_capture.c mx6s_capture.c是csi的driver,負責處理接收到的data,可以給各種攝影機Driver使用,其原理是通過dma讀取數據,這裡定義v4l2 中ioctl可以使用的對應操作。 ![image](https://hackmd.io/_uploads/Skj9A3MpJg.png) 這裡可以設置一個queue,用來儲存dma讀取到的資料, #### ov2640.c 接下來是ov2640.c 主要用來設置i2c相關參數(底層就是i2c的driver),最主要的function 為ov2640_set_params(): 會根據ioctl傳入的對應參數,去write i2c 對應的reg ![image](https://hackmd.io/_uploads/SJVtwTMpyl.png) 因此我們只要在對應 reg 中調整你要的值就能完成 i2c的設定了: 第一個值是要寫的寄存器,第二個值是要寫的值,根據datasheet進行調整。 ![image](https://hackmd.io/_uploads/H1RADaGakx.png) 我認為ov2640最重要的reg就是 0xDA : ![image](https://hackmd.io/_uploads/By4Ud6Ma1l.png) 這裡不需要自己配置,會根據ioctl傳入的format配置,圖片大小也是同理。 ![image](https://hackmd.io/_uploads/B1a5u6G6yx.png) 有yuyv uyvy rgb565等。 ![image](https://hackmd.io/_uploads/ryo6uafp1l.png) #### v4l2 ioctl 流程 這邊先介紹v4l2 driver如何 初始化ioctl,首先是應用中的ioctl: ![image](https://hackmd.io/_uploads/HJelBaf61l.png) 接著來到內核,V4L2處理IOCTL的程式為v4l2-ioctl.c,其中有個結構體如下: 宏定義第一個參數是ioctl的設定,當使用該ioctl,則會使用第二個值,也就是對應的function。 ![image](https://hackmd.io/_uploads/ry4YBTMTJl.png) ![image](https://hackmd.io/_uploads/HysmbTGT1x.png) 在function中,會根據傳進來結構體的type,執行不同case,ret的值就會使用宏定義去執行對應的函數: 有點忘記那個宏定義在哪了,但原理就是那樣。 ![image](https://hackmd.io/_uploads/r1WfGTfTke.png) 最後會去執行這個function: 設置csi的初始對應參數。 ![image](https://hackmd.io/_uploads/HyNaXpMTJx.png) 而同樣的ov2640中的i2c設置也是通過ioctl去啟動。 ### OV2640 應用: 應用實際上就是利用ioctl去控制設備相關參數,其餘流程皆差不多。 例如這裡設置rgb格式: ![image](https://hackmd.io/_uploads/Hy5ZsaGTyl.png) 其中這裡使用了v4l2的buffer,去儲存ov2640讀到的圖片,通過buffer,避免兩者互相等待,同時根據上面設置的圖片長寬,可以知道buffer多大,driver真的是方便的東西。 ![image](https://hackmd.io/_uploads/rJsb2pM61x.png) 這裡rgblcd是開發版設定好的設備樹,如何執行就沒去研究了,將讀取到的資料寫進 rgblcd mmap 對應內存,就會顯示資料了。 映射地址: ![image](https://hackmd.io/_uploads/ryEya6fayx.png) 寫入地址程式: ```c static void v4l2_read_data(void) { struct v4l2_buffer buf = {0}; unsigned int *base; // ARGB8888 占 4 字节 unsigned short *start; int min_w, min_h; int j, i; if (width > frm_width) min_w = frm_width; else min_w = width; if (height > frm_height) min_h = frm_height; else min_h = height; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; for ( ; ; ) { for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) { ioctl(v4l2_fd, VIDIOC_DQBUF, &buf); // 出队 for (j = 0, base = (unsigned int *)screen_base, start = buf_infos[buf.index].start; j < min_h; j++) { for (i = 0; i < min_w; i++) { unsigned short pixel = start[i]; // 获取 RGB565 像素 unsigned int r, g, b, argb; r = ((pixel & 0xF800) >> 8) | ((pixel & 0xF800) >> 13); // 5-bit → 8-bit g = ((pixel & 0x07E0) >> 3) | ((pixel & 0x07E0) >> 9); // 6-bit → 8-bit b = ((pixel & 0x001F) << 3) | ((pixel & 0x001F) >> 2); // 5-bit → 8-bit // **ARGB8888 格式:A(8) | R(8) | G(8) | B(8)** argb = (0xFF << 24) | (r << 16) | (g << 8) | b; // Alpha 设为 0xFF(不透明) base[i] = argb; // 赋值到 LCD 屏幕缓冲区 } base += width; // LCD 指向下一行 start += frm_width; // 视频帧指向下一行 } ioctl(v4l2_fd, VIDIOC_QBUF, &buf); // 重新入队 } } } ``` #### 遇到的bug 接下來講卡了我兩個多禮拜的bug,我應用是直接拿別人的code來使用,因為他的設備和顯示器大小和我一模一樣,只有攝像機是用ov5640,但沒想到跑下去圖片會變這樣: ![15448](https://hackmd.io/_uploads/SJ1uCpGakx.jpg) 原先應用如下: ```c static void v4l2_read_data(void) { struct v4l2_buffer buf = {0}; unsigned short *base; unsigned short *start; int min_w, min_h; int j; if (width > frm_width) min_w = frm_width; else min_w = width; if (height > frm_height) min_h = frm_height; else min_h = height; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; for ( ; ; ) { for(buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) { ioctl(v4l2_fd, VIDIOC_DQBUF, &buf); //出队 for (j = 0, base=screen_base, start=buf_infos[buf.index].start; j < min_h; j++) { memcpy(base, start, min_w * 2); //RGB565 一个像素占2个字节 base += width; //LCD显示指向下一行 start += frm_width;//指向下一行数据 } // 数据处理完之后、再入队、往复 ioctl(v4l2_fd, VIDIOC_QBUF, &buf); } } } ``` 我上網查類似問題都說是格式錯誤,也就是yuyv讀成uvyv之類的原因,害我一直往這方面去修改,跑去看driver是不是設定有誤,搞來搞去還是差不多結果: ![15447](https://hackmd.io/_uploads/ByaEJRf6Jx.jpg) 後來我就覺得driver錯的可能性極低,那code寫得那麼漂亮,而且我看日期都是2009年附近的,有錯早就改了,而且我把i2c的id可以讀出來,我很確定我i2c沒有問題,有成功設置成rgb565格式。 後來我開始往為什麼我大小設置成640*480,但大小只有四分之一,而且還會重複,我才靈機一動發現是螢幕的問題,因為讀到的數據是rgb565格式,但rgblcd是argb8888格式的,因此要再做處理。 所以數據只有一半,導致memcpy圖片只有四分之一,並且rgb565換行後,argb8888還沒,所以會產生兩張重複的圖。 到這裡我才明白韌體常說的通靈到底是什麼意思,真的是突然通到的,還有韌體中的記憶體真的好重要,必須符合相同格式。 最後修正後成果如下: ![15446](https://hackmd.io/_uploads/r1OSRpz61e.jpg) 調整焦距後成果如下: ![15445](https://hackmd.io/_uploads/SkHICaM6Jl.jpg) ## 攝影機學習使用 ov7670 並撰寫 driver(待完成) 硬體接線和控制方法構想如下: ![image](https://hackmd.io/_uploads/BydP88Vikg.png) 1. reset 和 3.3v 接開發版供給的3.3v 2. gnd 和 pwdn(不接也沒關係) 接開發版的地 3. xclk 利用開發版原廠就有的pwm 驅動供給 20MHz,建議為 12~48MHz,最好為4的倍數,有使用led測試10Hz pwm功能正常,但在高需要示波器,經費不足,放棄。 4. xclk 和 d7~d0 使用 gpio,進行時序和圖片資料的讀取(尚未撰寫)。 目前卡一個大問題在SCCB協議上,目前想法是使用I2C,但不知為何,就是無法成功讀取到設備ID,代碼有參考別人的,跟我的並無多大差別,推測錯誤出現在xclk的供給,這裡我是使用pwm訊號模擬20MHz,但有文章說需要上拉電阻,可有些文章又說不需要,這裡在等待蝦皮送電阻來,再進行測試。 帶fifo的OV7670(這裡搞完ov2640快吐了,之後再研究) 如果能成功完成,可學習v4l2 driver撰寫,會比使用i2c+gpio去硬讀來的方便。 # AI布置於嵌入式系統學習 ## 模型選擇 這裡選擇的AI為YOLOv8,這裡選擇AI為YOLOv8是因為我論文就做這個,使用上比較熟悉,不然最新已經出到YOLOv12,不得不感慨AI更迭真的是很快,在我剛畢業時才出到v9,才半年左右就已經更迭三代。 想挑戰不使用python,部屬在imx6ull上跑yolo,用python我以前碩論就用過了,沒什麼意思。 ## 為什麼用tflite? 因為開發版只有cpu,為提高模型運算速度,需要對模型進行量化,減少計算量,原本打算量化為tflite格式(tensorflow lite),利用tensorflow提供的動態庫丟到imx6ull的lib,並用別人寫好的code就能運行,使用tensorflow 提供的docker時,docker網路一直出問題(起碼有學到docker怎麼用),索性放棄,轉而使用onxx。 onxx相較tflite在推理速度上有差異,但tflite實在是太難用了,放棄,onxx概念是將yolo 原先的格式 .pt,轉化成.onxx,更適合部屬在各種架構,因為pt是pytorch,只能布置在pytorch上,onxx則是三大架構都可,但參數就不能像tflite一樣做縮減了。 這裡onxx用c++去跑,因為已經有別人現成寫好的,結果發現onxx不支援arm32架構的mcu。 不過不支援是正常的,因為我用tflite都跑超慢。 ## 前置作業: 將opencv 動態庫丟到mcu上,而tflite只有靜態庫,在編譯時會自動鏈接,不需要安裝動態庫。 ## 模型用法: 這裡只需將模型訓練後的參數,和圖片,通過tflite的api進行輸入,模型就會將預測結果輸出,這裡是一組vector形式的tensor,vector大小是[1,8400,5],這裡的tensor shape我是用python一個一個步驟print出來看的,不然c++做機器學習超難用,每個tensor都要手動處理,chatgpt 又靠不住。 1就是class的dimension,因為我只有辨識人臉,所以只有一組tensor,8400是預測結果,5則是 x y width hight confidence,有這五組tensor,再配合opencv 的 dnn.nms retrangle lib,就能畫出bounding box 了。 用opencv處理tensor並畫到image的 code : ```cpp pair<vector<vector<float>>, vector<float>> applyNMS(const vector<vector<float>>& boxes, const vector<float>& confidences, float iouThreshold) { vector<Rect> nmsBoxes; for (const auto& box : boxes) { float xCenter = box[0]; float yCenter = box[1]; float w = box[2]; float h = box[3]; // 將相對座標轉換為 int 類型的絕對像素座標 (用於 NMSBoxes) nmsBoxes.emplace_back(static_cast<int>(xCenter - w / 2), static_cast<int>(yCenter - h / 2), static_cast<int>(w), static_cast<int>(h)); } vector<int> indices; NMSBoxes(nmsBoxes, confidences, 0.5f, iouThreshold, indices); vector<vector<float>> filteredBoxes; vector<float> filteredConfidences; for (int i : indices) { int index = indices[i]; filteredBoxes.push_back(boxes[index]); // 保留原始的 float 座標 filteredConfidences.push_back(confidences[index]); } return make_pair(filteredBoxes, filteredConfidences); } void draw_predictions(Mat& img, float* outputData, int numDetections, int imgWidth, int imgHeight) { vector<vector<float>> boxes; vector<float> confidences; for (int i = 0; i < numDetections; i++) { float x = outputData[(numDetections*0) + i]; float y = outputData[(numDetections*1) + i]; float w = outputData[(numDetections*2) + i]; float h = outputData[(numDetections*3) + i]; float conf = outputData[(numDetections*4) + i]; if (conf > 0.5) { printf("x=%f,y=%f,w=%f,h=%f\r\n",x, y, w, h); boxes.push_back({x, y, w, h}); // 相對座標 confidences.push_back(conf); } } // 應用 NMS auto [nmsBoxes, nmsConfidences] = applyNMS(boxes, confidences, 0.4f); // 繪製框 for (size_t i = 0; i < nmsBoxes.size(); i++) { float xCenter = nmsBoxes[i][0]; float yCenter = nmsBoxes[i][1]; float w = nmsBoxes[i][2]; float h = nmsBoxes[i][3]; float conf = nmsConfidences[i]; int xMin = (int)((xCenter - w / 2.0) * imgWidth); int yMin = (int)((yCenter - h / 2.0) * imgHeight); int xMax = (int)((xCenter + w / 2.0) * imgWidth); int yMax = (int)((yCenter + h / 2.0) * imgHeight); printf("xm=%d,ym=%d,xmax=%d,ymax=%d\r\n",xMin,yMin,xMax,yMax); rectangle(img, Point(xMin, yMin), Point(xMax, yMax), Scalar(0, 255, 0), 4); string label = "Class " + to_string(static_cast<int>(outputData[i * 6 + 5])) + " " + to_string(conf); putText(img, label, Point(xMin, yMin - 10), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 255, 0), 1); } } ``` 最後這裡為了練習用threading,有加入thread優化程式,雖然這個任務因為沒有gpu,算是cpu密集型,IO只佔了一點點,優化並不明顯。 thread 的程式部分 (三個thread) ```cpp static void v4l2_read(unsigned short * save) { struct v4l2_buffer buf = {0}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; while (true) { for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) { ioctl(v4l2_fd, VIDIOC_DQBUF, &buf); // 出队 { unique_lock<mutex> lock(v42tfl); v4cv.wait(lock, [] { return !buffer; }); memcpy(save, buf_infos[buf.index].start, min_w * min_h * 2); buffer=true; } tfcv.notify_one(); // 通知推理线程 ioctl(v4l2_fd, VIDIOC_QBUF, &buf); // 入队 } } } ///////////////////////////////////////////////////////////////////////// opce process image and tflite ini model code static void tflite_predict(const string & model_path,unsigned short * save,uint8_t * bgr_buffer) { auto model = load_model(model_path); if (!model) { cerr << "无法加载模型: " << model_path << endl; exit(1); } cv::Mat image(min_h, min_w, CV_8UC3); uint8_t * bgr_save = (uint8_t *) malloc(sizeof(uint8_t)*3*min_w*min_h); ops::builtin::BuiltinOpResolver resolver; InterpreterBuilder builder(*model, resolver); unique_ptr<Interpreter> interpreter; builder(&interpreter); if (!interpreter || interpreter->AllocateTensors() != kTfLiteOk) { cerr << "解释器初始化失败" << endl; exit(1); } int input_idx = interpreter->inputs()[0]; TfLiteTensor* input_tensor = interpreter->tensor(input_idx); int input_width = input_tensor->dims->data[1]; int input_height = input_tensor->dims->data[2]; int input_channels = input_tensor->dims->data[3]; cv::Mat reimage(input_height, input_width, CV_8UC3); while (true) { { unique_lock<mutex> lock(v42tfl); tfcv.wait(lock, [] { return buffer; }); // RGB565 转 RGB888 uint8_t* base = bgr_save; unsigned short* start = save; for (int j = 0; j < min_h; j++) { for (int i = 0; i < min_w; i++) { unsigned short pixel = *(start + i); base[3 * i + 2] = ((pixel & 0xF800) >> 8) | ((pixel & 0xF800) >> 13); base[3 * i + 1] = ((pixel & 0x07E0) >> 3) | ((pixel & 0x07E0) >> 9); base[3 * i] = ((pixel & 0x001F) << 3) | ((pixel & 0x001F) >> 2); } base += (3 * frm_width); start += frm_width; } buffer = false; } v4cv.notify_one(); // 通知 V4L2 线程 memcpy(image.data, bgr_save, 3 * min_w * min_h); cvtColor(image, image, COLOR_BGR2RGB); cv::resize(image, reimage, cv::Size(input_width, input_height)); cvtColor(image, image, COLOR_RGB2BGR); reimage.convertTo(reimage, CV_32F, 1.0 / 255); if (image.channels() != input_channels) { cerr << "图片通道数与模型不匹配" << endl; exit(1); } memcpy(interpreter->typed_input_tensor<float>(0), reimage.data, reimage.total() * reimage.elemSize()); if (interpreter->Invoke() != kTfLiteOk) { cerr << "推理失败" << endl; exit(1); } int output_idx = interpreter->outputs()[0]; TfLiteTensor* output_tensor = interpreter->tensor(output_idx); float* output_data = interpreter->typed_output_tensor<float>(0); int num_detections = output_tensor->dims->data[2]; printf("num_detectiom = %d\r\n",num_detections); draw_predictions(image, output_data, num_detections, image.cols, image.rows); { unique_lock<mutex> lock1(tf2lcl); lcv.wait(lock1,[] {return !op;}); memcpy(bgr_buffer, image.data, 3 * min_w * min_h); op = true; } lcv.notify_one(); // 通知 LCD 线程 } free(bgr_save); } static int fb_dev_init(void) { struct fb_var_screeninfo fb_var = {0}; struct fb_fix_screeninfo fb_fix = {0}; unsigned long screen_size; /* 打开framebuffer设备 */ fb_fd = open(FB_DEV, O_RDWR); if (0 > fb_fd) { fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno)); return -1; } /* 获取framebuffer设备信息 */ ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var); ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix); screen_size = fb_fix.line_length * fb_var.yres; width = fb_var.xres; height = fb_var.yres; /* 内存映射 */ screen_base = (unsigned long *) mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); if (MAP_FAILED == (void *)screen_base) { perror("mmap error"); close(fb_fd); return -1; } /* LCD背景刷白 */ memset(screen_base, 0xFF, screen_size); return 0; } ```