กว่าจะทำ Real-Time Machine Learning กับ Flutter Camera ได้

Amorn Apichattanakul
KBTG Life
Published in
7 min readSep 8, 2022

ล่าสุดผมได้ถูกมอบหมายงานให้ทำ ML (Machine Learning) บน Flutter แบบเรียลไทม์ครับ ซึ่งกว่าจะทำได้ก็เจอปัญหาและอุปสรรคระดับนึงเลย หลังจากได้คลุกคลีมั่วซั่ว งมอยู่ได้ประมาณเดือนกว่าๆ ก็มานั่งคิดว่าเอาละ มาเขียน Blog สำหรับเคสนี้เก็บไว้ดีกว่า เผื่อในอนาคตผมย้อนกลับมาดูและเผื่อใครสนใจ จะได้ไม่ต้องเสียเวลามางมแบบผมอีก ซึ่งผมจะเล่าให้ฟังว่ากว่าจะได้ ต้องทำอะไรมาแล้วบ้าง

Flutter with Real-time ML

สำหรับคนที่รีบ สามารถเลื่อนไปด้านล่างสุดได้เลยครับ ผมมีโค้ดตัวอย่างให้เลยว่าทำยังไงถึงผ่าน แต่สำหรับคนที่อยากรู้ว่าผมทำอะไรมาแล้วบ้างจะได้ไม่ทำซ้ำ ไล่อ่านตามด้านล่างได้เลยครับ

งาน ML ที่ผมจะทำก็คือการตรวจสอบใบหน้าผ่านแอปพลิเคชันโดยใช้ Flutter ซึ่งระบบ ML จะตรวจสอบว่ารูปหน้าที่ส่งมาให้กับทางระบบหลังบ้านนั้นเป็นรูปคนจริงๆ ที่เกิดจากการถ่ายรูป Selfie ไม่ใช่รูปหน้าที่ถ่ายเป็นรูปมาอีกที ซึ่งปกติแล้วสมัยก่อนเวลาเราจะเปิดบัญชีธนาคารนั้น เราจะต้องใช้บัตรประชาชนและเดินไปที่สาขาธนาคาร แต่สมัยนี้เราสามารถทำหลายอย่างผ่านแอปพลิเคชันได้แล้ว รวมถึงการเปิดบัญชีธนาคารด้วย เราจึงต้องมีกระบวนการตรวจสอบใบหน้าว่าเป็นหน้าจริงๆ และมีรูปที่ตรงและใกล้เคียงกับบัตรประชาชน ซึ่งเราจะใช้ ML มาช่วยในส่วนนี้ และในกรณีนี้เราจะใช้ Flutter มาเป็นตัวอย่างกันครับ ซึ่งขั้นตอนคือเราจะส่งรูปส่งเข้าไปใน ML และทำการตรวจสอบว่าหน้าเป็นหน้าจริงหรือไม่ เมื่อตรวจสอบว่าเป็นหน้าจริงแล้ว เราก็จะส่งรูปไปให้ทางเซิร์ฟเวอร์เพื่อตรวจสอบใบหน้าว่าตรงหรือใกล้เคียงกับรูปบัตรประชาชนหรือไม่

เวอร์ชันแรก

ผมลองถ่ายรูป Selfie แบบตั้งวนลูบเอาไว้ทุกๆ 1 วินาที และเปลี่ยนรูปให้เป็น UInt8List ตัวไบนารี่ใน Flutter จากนั้นส่งค่าไปให้ Native เพื่อทำการสร้างรูปก่อนส่งเข้าไปใน ML ซึ่งสำหรับเวอร์ชันแรกนี้ ผมต้องการรีบทำแบบเร็วๆ เพื่อจะทดสอบ SDK ว่าใช้งานได้ดีไหม จากภาพรวมคือสามารถทำงานได้ แต่จะเป็นเฉพาะเครื่องแรงๆ เท่านั้นนะครับ เพราะว่ากระบวนการถ่ายรูปทุกๆ 1 วินาทีนั้นจะใช้เวลาประมาณ 300–400 ms ที่จะทำการเปลี่ยนรูปเป็นไบนารี่ ส่งรูปกลับไปให้ Native และทำการเช็ค Liveness ด้วยเหตุนี้เองจึงทำให้เรามีเวลาเหลือเพียงแค่ 600 ms ที่จะเริ่มถ่ายรูปอีกครั้ง โดย 300–400 ms นี้คือเครื่อง iPhone 11 Pro max ของผมเองที่ค่อนข้างแรงแล้ว ผมได้ลองกับเครื่อง iPhone X พบว่าใช้เวลานานถึงระดับ 1200 ms เลย ทำให้ไม่สามารถทำงานได้เสร็จภายใน 1 วินาที ดังนั้นไม่ถือว่าเป็นการตรวจสอบแบบเรียลไทม์สำหรับทุกเครื่องครับ และยิ่งไปกว่านั้น iOS จะมีเสียงแชะถ่ายรูปด้วย ทำให้เกิด User Experience ไม่ดี แต่ผมมองว่าเท่านี้ก็โอเคแล้วสำหรับการทดสอบเบื้องต้น อย่างน้อยเทสเตอร์ของเราก็สามารถทำงานต่อได้เพื่อทดสอบระบบ โดยจะมีข้อแม้ว่าให้ทำการเทสเฉพาะเครื่องแรงๆ ก่อนเท่านั้น หลังจากส่งเวอร์ชันแรกให้กับทางเทสเตอร์ ผมก็ได้เริ่มทำเวอร์ชันสองต่อเลยทันที

เวอร์ชันสอง

เนื่องจากการถ่ายรูปแบบรัวๆ นั้นไม่เร็วพอสำหรับการทำ Liveness แบบเรียลไทม์ ผมได้ศึกษาเพิ่มเติม แล้วพบว่า Native นั้นใช้ Live Feed จาก Camera เพื่อส่งเข้าไปที่ ML จึงพยายามหาดูว่า Flutter Camera มีอะไรคล้ายๆ แบบนี้หรือไม่ ปรากฏว่าเจอครับ

ใน Flutter Camera https://pub.dev/packages/camera จะมีฟังก์ชันที่เรียกว่า startImageStream เพื่อ Stream รูปออกมาได้

controller.startImageStream((cameraImage) async {
// Feed image into ML
});

ซึ่งสิ่งที่จะได้มาก็คือ CameraImage โดยปกติแล้วถ้าเราทำ ML เกี่ยวกับรูปนั้น คนส่วนใหญ่จะใช้ Firebase ซึ่งรับค่าเป็น CameraImage อยู่แล้ว ก็ง่ายเลยไม่ต้องทำอะไรมาก

แต่ในบางเคส งานของเราอาจจะไม่เหมาะสมกับ Firebase และเราต้องใช้ ML ของเราเอง แน่นอนครับ SDK ส่วนใหญ่ไม่ได้ออกแบบมาสำหรับ Flutter ทำให้ CameraImage ที่เป็นฟอร์แมตสำหรับ Flutter ไม่สามารถใช้งานได้ เราจึงต้องมีการ Process ข้อมูลบางอย่างให้เป็นในฟอร์แมตที่ SDK เหล่านั้นนำไปใช้ ซึ่งนั่นก็คือปัญหาใหญ่สำหรับ Flutter เลย เพราะ SDK ส่วนใหญ่จะรับรูปที่เป็นฟอร์แมท RGB จำพวก JPG ไม่ก็ PNG ซึ่ง CameraImage ของ Flutter ก็ทำเป็น JPG ได้เหมือนกัน แต่เฉพาะ Android เท่านั้น ส่วน iOS ต้องไปทำการเแปลงเอาเอง จากที่ไปอ่าน Documents มา ปรากฏว่า Flutter Camera สนับสนุนเพียงแค่ 2 ฟอร์แมทเท่านั้น คือ YUV420 และ BGRA8888 ผมตัดสินใจว่าไปกับ BGRA8888 ดีกว่า เพราะ YUV420 จากที่อ่าน Documents แล้วเหมือนเป็นวิดีโอมากกว่า

Camera Image Documentation

เนื่องจาก Android เป็นฟอร์แมท JPG อยู่แล้ว ก็สบายเราละ ไม่ต้องทำอะไร เหลือแค่ iOS ที่เราจะต้องมาทำเป็น JPG นี่สิ หลังจากค้นคว้าไปมาก็เจอกับ GitHub ตัวนี้

ผมก็ทำตามเค้าเลยครับ ได้รูป JPG กลับมาแบบที่ต้องการ แต่ Performance ไม่ค่อยดีตามที่คาดไว้ ถ้าลองใช้แค่รูปเดียวนั้นไม่มีปัญหาครับ แต่เนื่องจากเราจะต้อง Process ตลอดเวลา และส่งรูปมาต่อเนื่อง ทำให้ iOS มันกระตุกไม่ไหลลื่นอย่างที่ต้องการ ซึ่งหลังจากไปไล่ดู ก็พบว่า Camera Image นั้นส่งรูปมาทุกๆ 20–30 ms ถ้าส่งให้ทำงานตลอดเวลาคงไม่ไหว

ด้วยเหตุนี้ ผมจึงเพิ่มฟังก์ชัน Throttling ขึ้นมา คือต่อให้ Camera Image ส่งมาเยอะขนาดไหน ผมก็จะทำงานทุกๆ 500 ms แทน แต่หลังจากที่ทดสอบระหว่าง 30 ms กับ 500 ms แล้วดูจะไม่ค่อยแตกต่างสักเท่าไหร่ในกรณีของงานผม ก็ยังดูว่าเป็นแบบเรียลไทม์อยู่ เพราะตอนที่เราตรวจสอบนั้น หน้าของเราจะอยู่ตรงกลางจอและไม่ได้ขยับมากมาย ทำให้ผู้ใช้งานแยกไม่ออกว่าเรา Process ทุกๆ 500 ms แทนที่จะเป็น 30 ms ทั้งนี้ iOS ก็ยังกระตุกอยู่ดี ถึงจะดีขึ้นมาหน่อยก็ตาม

หลังจากปวดกบาลกับ iOS ขอพักด้วยการสลับมาแก้ Android ดีกว่า ดูมีอนาคตกว่ามากเพราะเป็น JPG อยู่แล้ว อย่างน้อยเอาให้เสร็จสักแพลตฟอร์ม จะได้จบเป็นเรื่องๆ ไป ซึ่งพอเราสั่ง Camera Image ให้เป็น JPG นั้น Performance ก็ดี ลื่นไหล ไม่ติดขัดปัญหาอะไรกับทางฝั่ง Flutter แต่ดันไปติดตอนที่ทำรูปในส่วนของ Kotlin เนี่ยแหละ ปัญหาก็คือออออออ

ทำไมรูปที่ออกมาจาก Camera Image มันหมุน 90 องศา!!!

ผมนี่อึ้งไปเลย มันมี Use Case ไหนในโลกมนุษย์ฟะ ที่เราอยากได้รูปที่หมุน 90 องศาทั้งๆ ที่เราถ่ายรูปหน้าตรง?!?! แต่คนก็ใช้ Flutter Camera กันทั่วโลก คงมีซัมติงที่ทำให้เขาทำแบบนี้ ผมจึงไปหาโค้ด Kotlin เพื่อหมุนอีก 270 องศาให้กลับมาหน้าตรง โดยใช้ Matrix ใน Kotlin แล้วก็สร้าง Bitmap จากไบนารี่อันนั้น ซึ่งผลลัพธ์ที่ได้ก็คือ Android รุ่นกลางๆ สามารถทำงานได้ระดับนึง แต่จะกระตุกนิดหน่อย เนื่องจากเราทำหลายอย่าง ทั้งสร้างรูป หมุนรูป แถม ML ก็ทำงานอีก ทำให้ใช้พลังงานเยอะอยู่ แต่สุดท้ายแล้วก็ดีกว่าถ่ายรูปแบบต่อเนื่องที่เคยทำไป นับว่าเพียงพอสำหรับ Android ในตอนนี้ ทำงานได้แล้ว แต่ยัง Lag อยู่ จึงขอหยุดแค่นี้ก่อน

ทีนี้กลับมาที่ iOS ครับ มาหาวิธีกันต่อว่าทำยังไงให้กระตุกน้อยลง คราวนี้ลองมานั่งดูเรื่อง Threading ใน Flutter ซึ่งผมก็ได้ทำตามบทความนี้ครับ

โดยปกติแล้วผมแทบไม่ต้องยุ่งกับ Threading ใน Flutter เลย เพราะ awaitก็ไม่ได้ Block UI Thread อยู่แล้ว ไม่เหมือนกับ iOS ที่ผมจะต้องใช้ Thread อยู่บ่อยๆ สำหรับงานที่ต้องใช้การคำนวณเยอะๆ

ผมลองใช้ Threading กับ Isolate ซึ่งใช้ Image Lib จาก Flutter สำหรับการแปลงค่า BGRA เป็น RGB

พอลองแล้ว โอ้โห คนละโลกเลยครับ เครื่ง iPhone X นี่ไหลลื่น Smooth Like Butter 😄 (เครดิตวง BTS)

หลังจากที่ได้ทำเวอร์ชันที่ 2 ไปแล้ว iOS และ Android Mid-Tier ทำงานได้ดี ก็เลยลองกับคนกลุ่มใหญ่ดูบ้าง ได้ลองแค่ 3–4 เครื่องแล้วก็ยังเรียบร้อยดูดีอยู่

แต่เมื่อทดลองกับกลุ่มใหญ่ 20 เครื่องก็ปะทะปัญหาใหญ่กับ Android คือ Flag JPG นั้นใช้งานไม่ได้ ซึ่งImageFormatGroup.jpegจะ Crash ในบางเครื่อง จากที่ไปค้นคว้ามา บางคนก็เป็นจริงๆ

ผมได้ Error จากเครื่องตามนี้เลยครับ GetYUVPlaneInfo: Invalid format passed: 0x21

เครื่อง Xiaomi Note 8 จะ Crash เมื่อเปิด Image Stream สำหรับ JPG แต่ถ้าปรับเป็น Default YUV420 กลับไม่มีปัญหาใดๆ ซึ่งผมมี Android 20 เครื่อง ดันเป็นแค่เครื่องเดียว ทำให้ผมกังวลว่าเป็นแค่เครื่องนี้เครื่องเดียวหรือเปล่านะ หรือถ้าออกไปตลาดแล้ว อาจจะมีเครื่องอื่นเป็นอีก

หลังจากที่ลองค้นหาข้อมูล ก็ตัดสินใจว่าผมน่าจะไปกับ YUV420 ดีกว่า เพราะเป็นฟอร์แมท Default ใน Camera Image ซึ่งไม่ควรจะมีปัญหาใดๆ ทั้งนี้ผมไม่อยากเสี่ยงกับ JPG สำหรับ Flutter ดังนั้นผมต้องหาวิธีแปลงค่าจาก YUV420 ให้มาเป็น RGB JPG บน Native ซึ่งผมก็ได้ไปเจอ Sample Code ด้านล่างนี้ในเน็ตมาครับ

List<int> strides = Int32List(image.planes.length * 2);int index = 0;List<Uint8List> data = image.planes.map((plane) {   strides[index] = (plane.bytesPerRow);   index++;   strides[index] = (plane.bytesPerPixel)!;   index++;   return plane.bytes;}).toList();await _channel.invokeMethod<Uint8List>("checkLiveness", {   'platforms': data,   'height': image.height,   'width': image.width,   'strides': strides});

ต้องขอโทษสำหรับคนที่ทำโค้ดนี้ด้วยครับ ผมอยากให้เครดิตคนคิดส่วนนี้ แต่ผมหาต้นตำรับไม่เจอ ถ้าใครเจอคนแชร์ส่วนนี้ รบกวนแจ้งมาได้ครับ ผมจะแก้บทความเพื่อเพิ่มเครดิตให้ครับ

สำหรับในฝั่ง Android ผมไปเจอโค้ดสำหรับการแปลงจาก YUV มาเป็น JPG ตามลิงก์ด้านล่าง

พอทำทั้งหมดเสร็จ ก็ทำงานได้เรียบร้อยเหมือนเดิมครับ แต่ก็ยังกระตุกอยู่บ้างเล็กน้อย เพราะงานหลักๆ ตอนนี้คือเราแก้เรื่อง Crash อย่างเดียว ยังไม่ได้ยุ่งเกี่ยวกับ Performance ใดๆ ก็ถึงเวลาที่ผมจะมา Improve ส่วนนี้แล้วครับ

เริ่มจาก Android ก่อนเลย ผมไปค้นคว้าเกี่ยวกับ Kotlin Coroutine แล้วนำมาใช้ พบว่าหลังจากที่ใช้ Threading ทำให้ Android ไหลลื่นขึ้นกว่าก่อนเยอะมาก ตอนนี้ Android ทุกรุ่นที่ผมมี แม้กระทั่งเครื่องที่เก่าๆ อย่าง Oppo a3s ปี 2018 ราคาประมาณ 4,000 บาท ก็ยังลื่นมากๆ เลย จบไปกับ Android ผมพอใจกับผลลัพธ์อย่างมาก ถึงคิวของ iOS ครับ

และแล้วก็มาถึง… เวอร์ชั่นสุดท้ายของผม

ถึงเวลาดูว่าเราจะทำยังไงกับ iOS รุ่นเก่าๆ เช่น iPhone 6s, 6s Plus และ 7 ได้บ้าง ที่ตอนนี้กระตุกจนเล่นแทบไม่ได้ ถึงตรงนี้ Isolate ที่ผมทำไปนั้นทำให้ Medium Tier เล่นได้ดีแล้ว iPhone X, XS สามารถใช้งานได้ปกติ แต่พอมาไล่ดูดีๆ ปรากฏว่าแม้ Isolate จะช่วยทำให้แอปไม่กระตุกก็จริง แต่สำหรับ iPhone 6s ใช้เวลาในการ Convert BGRA นานประมาณ 1.5 วินาทีเลย นี่ยังไม่รวมที่ต้องส่งไปเข้า Liveness อีก ซึ่งเครื่องรุ่นเก่าๆ ก็ใช้เวลาอีก 1.5 วินาทีเช่นกัน แปลว่าตั้งแต่ที่เราเริ่มส่งรูปไป ระบบจะใช้เวลาตรวจสอบทั้งหมดมากถึง 3 วินาที ทำให้ไม่สามารถทำงานได้อย่างที่ต้องการ ผมจึงตัดสินใจดึงส่วนที่ทำการ Convert นั้นมาไว้ที่ Native แทน เพื่อดูว่าเราจะลดเวลา 1.5 วินาทีลงได้มั้ย ให้เหลือสัก 0.5 วิก็ยังดี แต่ผลลัพธ์ออกมาน่าตกใจมาก ผมสามารถลดเวลา Process จาก 1.5 วินาที เหลือเพียงแค่ระดับ 0.01 วินาทีเท่านั้น!!! ซึ่งวิธีการ Convert ใน Swift ผมได้แชร์ไว้ที่ Repository ใน GitHub ที่ด้านล่างสุดของบทความแล้วครับ แต่ด้านล่างนี้เป็นตัวอย่างโค้ดที่ใช้ในการ Convert รูป

private func bytesToPixelBuffer(width: Int, height: Int, baseAddress: UnsafeMutableRawPointer, bytesPerRow: Int) -> CVBuffer? {   var dstPixelBuffer: CVBuffer?   CVPixelBufferCreateWithBytes(kCFAllocatorDefault, width, height,       kCVPixelFormatType_32BGRA, baseAddress, bytesPerRow, nil, nil, nil, &dstPixelBuffer)   return dstPixelBuffer ?? nil}private func createImage(from pixelBuffer: CVPixelBuffer) -> CGImage? {   var cgImage: CGImage?   VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil,    imageOut: &cgImage)   return cgImage}private func createUIImageFromRawData(data: Data, imageWidth: Int, imageHeight: Int, bytes: Int) -> UIImage? {   data.withUnsafeBytes { rawBufferPointer in      let rawPtr = rawBufferPointer.baseAddress!      let address = UnsafeMutableRawPointer(mutating:rawPtr)      guard let pxBuffer = bytesToPixelBuffer(width: imageWidth, height: imageHeight, baseAddress: address, bytesPerRow: bytes), let cgiImage = createImage(from: pxBuffer) else {      return nil}   return UIImage(cgImage: cgiImage)}

และก็มาถึงส่วนสุดท้ายที่ผมไม่สามารถแก้ไขได้ ก็คือตัว Flutter Image Stream นั่นเอง

ในส่วนของ Performance ตัวนี้ ถึงผมจะสร้างโปรเจคแบบง่ายๆ ไม่มีอะไรเลย ทำ Flutter Camera และ Stream Image ออกมา ไม่ต้องทำงานใดๆ ทั้งสิ้น พวกเครื่อง Low Tier ก็ยังกระตุกครับ ผมจึงลองไม่เปิดฟังก์ชัน Image Stream ในหน้า Preview พบว่ากล้องนั้นลื่นปกติเหมือนเครื่องอื่นๆ ไม่มีปัญหาใดๆ ก็แสดงว่าเราต้องห้ามเปิด Image Stream ในเครื่องรุ่นเก่าๆ แต่ถ้าไม่เปิดแล้วจะทำ Liveness ยังไง?

พอมานั่งคิดดีๆ เราเปิด Image Stream ก็จริง แต่เราต้องการแค่รูปเดียวทุกๆ 500 ms ที่เหลือเป็นแค่เศษที่เราต้องทิ้งไปฟรีๆ ถ้าเราเปิดกล้องแล้วปิดทันทีล่ะ หลังจากที่ผมลองเปิดไป 50 ms เท่านั้น ปรากฏว่าภายใน 50 ms เราก็ได้รูปมาประมาณ 1–2 รูป ซึ่งก็เพียงพอสำหรับ Liveness แล้ว จากนั้นเราก็ปิดทันที ปรากฏว่า iPhone 7 นั้นลื่นไหลสบายเลยครับ ส่วน iPhone 6s และ iPhone 6s Plus ดีขึ้นมากๆ แทบดูไม่ออกเลยว่ากระตุก ผมได้ทำการทดลองตามด้านล่างเพื่อดู Performance ของ Flutter Camera ซึ่งในตัวอย่างเป็น Flutter App ที่มีเพียงแค่ Camera เท่านั้นนะครับ ไม่ได้มีการทำงานใดๆ

Performance Inspection

iPhone 11 Pro Max — 53% CPU, 190MB Memory ได้ Image มาทุกๆ 20–40 ms

iPhone 6s — 118% CPU, 160MB Memory ได้ Image มาทุกๆ 20–30 ms

iPhone 6s Plus — 138% CPU, 187 MB Memory ได้ Image มาทุกๆ 20–50 ms

iPhone 6s Plus แบบปิด Camera Stream เปิดเพียงแค่ Camera Preview ใช้ 46% CPU

iPhone 6s Plus แบบเปิดปิด Image Stream ปรากฏว่า CPU จะอยู่แถวๆ 50%-70%

ในขณะที่ Android Oppo A3s Low-Tier (2018) Andriod 8.1.0, Ram 2GB ยังทำงานได้ดีกว่า iPhone 6s เยอะเลย แม้ผมจะเปิด Image Stream ตลอดเวลา ก็ยังใช้เพียง 14% CPU และประมาณ 320 MB Ram

งั้นมาลองหน่อยสิ ถ้าผมเปิดกล้อง iPhone 6s Plus ง่ายๆ ใน Native Camera จะเห็นใช้ 57% CPU, 39.1 MB เท่ากับ CPU เราใช้พอๆ กัน แต่ว่า Ram จะต่างกันเยอะ

มาที่ขั้นตอนสุดท้าย เรายังติดปัญหาอีกส่วน คือเรื่องรูป UIImage ที่แชร์ด้านบน ซึ่งเมื่อเราจะนำรูปมาใช้ ดันเจอปัญหา Thread 1: EXC_BAD_ACCESS

ผมนำ Keyword ที่มีปัญหาไปกูเกิลมา โดยใช้คำว่า CGDataProvider_BufferIsNotBigEnough เพื่อหาวิธีแก้ไขปัญหา ปรากฏว่าส่วนใหญ่จะไม่มีคำตอบที่ตรงกับปัญหาของผมเลย ซึ่งมีคนบอกว่าผมใช้รูปใหญ่ไป ไม่ก็เครื่องผมเก่าเกินไป แต่แม้กระทั่ง iPhone 11 Pro Max ของผมก็ยังมีปัญหา งั้นไม่น่าจะใช่แล้ว ผมลองหาวิธีแก้หลายๆ อย่าง ตั้งแต่ใส่รูปเล็กๆ ลงไปบ้าง Feed รูปให้ช้าลงบ้าง แต่ก็ไม่หาย อันนี้ทำเอาผมมึนงง ไม่รู้ว่าจะไปยังไงต่อเลยครับ งมอยู่ประมาณ 1–2 วัน ลองผิดลองถูกไปมา จนในที่สุดก็เข้าใจว่า SDK นั้นคืนรูปที่เรา Feed เข้าไปกลับมาให้เรา แสดงว่ารูปที่เรา Feed เข้าไปนั้นเอามาใช้ไม่ได้นะสิ ผมจึงลองค้นหาจากส่วนนั้น แล้วพบว่ารูปที่ใส่นั้นส่ง UIImage ที่เป็น Reference เข้าไป ไม่ใช่ Value Type ดังนั้นเราจะต้อง Copy ออกมาเป็น Value Type เพื่อนำมาใช้ ก็เลยได้ Keyword เกี่ยวกับ Deep Copy PixelBuffer มาทำการกูเกิลต่อ ลองผิดลองถูกจนผ่านนนนนนนน จบแล้วครับ แก้ได้ทุกปัญหาจริงๆ

ฟันธงแล้วนะ

ไม่มีข้อโต้แย้งใดๆ

TL;DR คุณแม่ขอร้องเลยว่าอย่าทำ Real-time ML ใน Flutter เล้ยยยย ไปทำบน Native เถอะ ค่อยส่งผลลัพธ์กลับมาที่ Flutter ผมว่าวิธีนี้ง่ายกว่ามากๆ แทนที่จะมางมวิธีต่างๆ ใน Flutter แบบผมเนี่ย

อ่าว แล้วผมไม่เชียร์ให้ทำใน Flutter เหรอ? ใช่ครับ ไม่เชียร์ครับ ถ้าจุดประสงค์ที่เราใช้ Flutter คือการทำให้เราทำงานง่าย ทำงานเร็วขึ้น ถ้าทำตรงนี้ไม่ได้แล้วเราจะใช้ทำไมล่ะครับ ไม่ต้องยึดติดครับ Flutter ไม่ใช่สุดยอด Language ที่ทำได้ทุกอย่างในโลก อันไหนทำได้เราก็ทำ อันไหนทำไม่ได้ก็ไม่ต้องฝืนครับ 😁

เพิ่มเติม: 15 Dec 2024

ตอนนี้ผมมีบทความใหม่ ที่ทำต่อจากเรื่องนี้ครับ ซึ่งจะออกในรูปแบบ Native มากกว่ามาใช้กับ Flutter ถ้าใครสนใจสามารถไปที่ Medium ด้านล่างได้เลยครับ

แต่สำหรับใครที่ว่ามันซับซ้อนไปต้องไปยุ่งกับ Native เยอะแยะจัง ก็ยังสามารถใช้วิธีแบบนี้ต่อได้ครับ

ด้านล่างคือตัวอย่างโค้ดที่ผมได้ Implement การ Convert รูปทั้งหมดของ Camera Image มาเป็น JPG เพื่อใช้ใน SDK ครับ ในกรณีของคนที่ยืนกรานว่ายังไงผมก็อยากทำใน Flutter เพราะด้วยเหตุผลต่างๆ ก็ว่าไป สามารถนำ Sample ด้านล่างไปใช้ศึกษาได้เลยครับ

ด้านล่างเป็นตัวอย่างวิดีโอที่แสดงให้ดูว่า Flutter Stream Performance กับแต่ละเครื่องเป็นยังไง

ส่วนด้านล่างคือตัวอย่างที่ใช้ใน Flutter นะครับ จะได้ออกมาเป็นแบบนี้

ข้อสรุป

Flutter Camera ไม่เหมาะกับเครื่องเก่าๆ ครับ เพราะ Flutter จะส่งรูปมาให้ตลอดเวลา ทำให้ทำงานหนัก แต่ถ้า Flutter Camera สามารถรับค่า Parameter ว่า Frame Rate ของรูปที่ต้องการให้ส่งกลับไปได้เป็นเท่าไหร่จะดีมากๆ เลยครับ เพราะในบาง Use Case เราต้องการรูปรายละเอียดสูงก็จริง แต่บาง Use Case ไม่ได้ต้องการความเร็วแบบเรียลไทม์เลย อย่างเคสนี้ของผม ผมอยากได้รูปทุกๆ 200 ms มากกว่าที่จะได้รูปแบบ 20–30 ms ซึ่งถ้าแก้ได้จะลดพลังในการทำงานได้ถึงระดับ 90% เลย!!

สิ่งที่ผมคิดก็มีคนคิดแล้วเหมือนกันนะ โดยเค้า Request มาจะ 2 ปีได้แล้ว

ถ้าดูจาก Feedback แล้ว ไม่น่าจะได้เร็วๆ นี้แน่เลย ผมว่าถ้าต้องการแบบเรียลไทม์จริงๆ ไปทำ Native เถอะครับ จริงๆ ผมก็อยากเปิด PR ไปแก้เหมือนกันนะ ถ้ามีโอกาส

สำหรับใครที่ดูบทความนี้แล้วสนใจจะใช้ Liveness ในงาน ไม่ว่าจะเป็น Flutter หรือ Native ก็ตาม ติดต่อมาที่ KBTG หรือผมก็ได้ครับ ผมจะส่งเรื่องไปที่ทีมที่เกี่ยวข้องให้ ซึ่ง KBTG Face Liveness ผ่านมาตราฐาน ISO 30107–3 ทดสอบจาก iBeta มาเรียบร้อย เป็นมาตรฐานโลกสำหรับการทำ SDK Liveness เลยนะครับ

อ้างอิงจากเว็บไชต์

สำหรับชาวเทคคนไหนที่สนใจเรื่องราวดีๆแบบนี้ หรืออยากเรียนรู้เกี่ยวกับ Product ใหม่ๆ ของ KBTG สามารถติดตามรายละเอียดกันได้ที่เว็บไซต์ www.kbtg.tech

--

--

Amorn Apichattanakul
KBTG Life

Google Developer Expert for Flutter & Dart | Senior Flutter/iOS Software Engineer @ KBTG