چگونه با استفاده از OpenCV یک Node.js CLI به همراه مدل‌های عصبی برای طبقه‌بندی تصویر بنویسیم؟
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 13 دقیقه

چگونه با استفاده از OpenCV یک Node.js CLI به همراه مدل‌های عصبی برای طبقه‌بندی تصویر بنویسیم؟

خلاصه اولیه: در این مقاله ما ۳ چیز را یاد خواهیم گرفت (این‌ها مواردی هستند که من در هنگام ساخت پروژه خود برای گیت‌هاب باید تحمل می‌کردم):

  1. چگونه یک فایل بزرگ را با استفاده از git-Ifs (Git Large File System = سیستم فایل بزرگ گیت) به یک پروژه گیت‌هاب وارد کنیم؟
  2. چگونه یک Node CLI (command-line Interface = رابط خط دستوری) بسازیم؟
  3. چگونه با یک شبکه عصبی عمیق، (Deep Neural Network) تصاویر را طبقه‌بندی کنیم؟

من برای هر ناحیه یک بخش را می‌سازم؛ پس تمام آن‌ها، یا فقط بخش‌هایی که به آن‌ها علاقه‌مندید را بخوانید.

پس‌زمینه

قبل از این که شروع کنیم، کمی اطلاعات درباره این موارد به شما خواهد داد. در جایی که من کار می‌کنم، ما از دوربین‌ها برای انجام تجزیه و تحلیل استفاده می‌کنیم. (مانند تشخیص نشت نفت یا گاز) وقتی که یک هشدار نمایش داده می‌شود، تصاویر سریعی از جریان MPEG گرفته می‌شوند. سپس یک برنامه‌نویس دیگر در گروه من مقداری کد Python نوشت تا این تصویر را طبقه‌بندی کند. من در تعجب بودم که آیا همین کار را می‌توان با استفاده از Node انجام داد یا نه. من قبلا با شبکه‌های عصبی (Neural Network) کار نکرده‌ام، پس این برای من یک چالش خواهد بود. من شروع به استفاده از tensorflow.js‌ کردم، اما در تبدیل مدل‌های موجود به مدل‌های وب دوست که پکیج ftjs-node نیاز داشت، به مشکلاتی بر خوردم، تا این که با پکیح opencv4nodejs آشنا شدم. از آن موقع به بعد همه چیز بهتر کار کرد.

پس از این که پکیج خود را سر هم کردم، در قرار دادن پروژه بر روی گیت‌هاب به مشکل بر خوردم. فایل‌های مدل بیش از حد بزرگ بودند. سپس درباره git-lfs (Git Large File System = سیستم فایل بزرگ گیت) شنیدم. پس از چند روز کار کردن با آن،‌ توانستم آن را به کار بگیرم. بعد هم با مشکلات npm روبه‌رو شدم. من npm publish را امتحان کردم، اما پس از پکیج کردن، آپلود کردن به registry با شکست مواجه شد و خطای «JavaScript head out of memory» بروز داده شد. باز هم فایل مورد نظر بیش از حد بزرگ بود.

من هنوز هم آن را بر روی npm registry قرار نداده‌ام و هنوز هم باید راه‌های جدیدی را بررسی کنم.

گیت‌هاب و فایل‌های بزرگ

اول از همه، گیت‌هاب برخی محدودیت‌ها را دارد. با توجه به اسناد خودشان: «ما یک محدودیت سختگیرانه برای فایل‌های بزرگ‌تر از ۱۰۰ مگابایت داریم.» پس وقتی که مدل‌ها بزرگ‌تر از آن هستند، کار نخواهد کرد.

به gif-lfs وارد شوید. این الماس کوچک شما را قادر می‌سازد تا فایل‌های عظیم را بر روی گیت‌هاب قرار دهید. همچنین git-lfs با Gitkraken هم به خوبی کار می‌کند.

متوجه LFS در انتهای این فایل‌ها شدید؟ عالی است.

حال همه چیز به همین راحتی پیش نرفت. قبل از این که فایل‌های عظیم را در مخزن خود قرار دهید، باید git-lfs را راه‌اندازی کنید و به آن بگویید که چه نوع فایلی را برای پروژه خود می‌خواهید ارسال کنید. راهنمای مربوطه را بر روی وبسایت گیت‌هاب، و در اینجا بخوانید.

ساخت یک CLI با استفاده از Node

مطمئنم که همگی CLI (Command-Line Interface = رابط خط دستوری) را شنیده‌اید. CLI کاربر را قادر می‌سازد تا به روش یک برنامه، با کامپیوتر در تعامل باشد. با ساخت یک CLI با استفاده از Node، پکیج مورد نظر می‌تواند به گونه‌ای اجرا شود که انگار یک برنامه کمپایل شده موجود بر روی کامپیوتر است.

برای مثال، برای اجرای پکیج Node‌ من که classify نام دارد، معمولا این کار را باید انجام داد (در پوشه classify):

node index.js [arguments]

می‌توانید پکیج را به صورت global در اکوسیستم Node نصب کنید، اکوسیستم Node پکیج را به مسیر مورد نظر اضافه می‌کند. برای نصب (از پوشه classify) این دستور را اجرا کنید:

npm install -g . classify

این دستور، پکیج را با استفاده از نام «classify» در پوشه فعلی نصب می‌کند.

حال می‌توانید بیانیه‌ای مانند این مورد را از خط دستوری اجرا کنید:

classify --image <path to image file> --filter ./filter.txt --confidence 50

خروجی CLI

تمام CLIها باید یک خروجی داشته باشند تا کاربر بتواند نحوه استفاده از آن را درک کند. در این مورد، classify چنین ظاهری خواهد داشت:

classify

یک تصویر را با استفاده از یادگیری ماشین منتقل شده در مسیر تصویر،‌ طبقه‌بندی می‌کند.

Options

--image imagePath     [required] مسیر تصویر                                                   
--confidence value    [optional; default 50] حداقل سطح مورد استفاده برای طبقه‌بندی
--filter filterFile   [optional] یک فایل فیلتر، برای فیلتر کردن طبقه‌بندی‌ها.     
--quick               [optional; default slow] طبقه‌بندی سریع که ممکن است کمی در دقت ضعیف‌تر باشد.                                                                 
--version             نسخه برنامه.                                                         
--help                پرینت گرفتن این راهنما

البته، پکیج‌هایی وجود دارند که به شما در انجام این کار کمک کنند. من از command-line-usage و command-line-args استفاده کردم تا این کار را برای من انجام دهند.

اما قبل از این که به آن وارد شویم، چگونه می‌توان یک فایل JavaScript را با استفاده از Node‌، بدون مشخص کردن آن بر روی خط دستوری، اجرا کرد؟

این مسئله مربوط به اولین خط تمام اسکریپت‌ها بر روی یک سیستم لینوکس است. در اینجاست که تفسیر کننده با استفاده از نشانه‌گذاری she-bang مشخص شده است:

#!/usr/bin/env node

این به محیط می‌گوید که از Node به عنوان یک تفسیر کننده برای اسکریپت استفاده کند و باید در هنگام ساخت یک CLI، در بالای فایل JavaScript باشد.

command-line-usage

این مورد که استفاده از آن هم ساده است، چیزی که کاربر خواهد دید را تعریف می‌کند.

کد آن را در اینجا مشاهده می‌کنید:

const commandLineUsage = require('command-line-usage')

const sections = [

  {

    header: 'classify',

    content: یک تصویر را با استفاده از یادگیری ماشین منتقل شده در مسیر تصویر،‌ طبقه‌بندی می‌کند.

.'

  },

  {

    header: 'Options',

    optionList: [

      {

        name: 'image',

        typeLabel: '{underline imagePath}',

        description: '[required] مسیر تصویر.'

      },

      {

        name: 'confidence',

        typeLabel: '{underline value}',

        description: '[optional; default 50] حداقل سطح مورد استفاده برای طبقه‌بندی (ex: 50 for 50%).'

      },

      {

        name: 'filter',

        typeLabel: '{underline filterFile}',

        description: '[optional] یک فایل فیلتر، برای فیلتر کردن طبقه‌بندی‌ها.'

      },

      {

        name: 'quick',

        description: '[optional; default slow] طبقه‌بندی سریع که ممکن است کمی در دقت ضعیف‌تر باشد.'

      },

      {

        name: 'version',

        description: نسخه برنامه.'

      },

      {

        name: 'help',

        description: پرینت گرفتن این راهنما.'

      }

    ]

  }

]

const usage = commandLineUsage(sections)

سپس برای خروجی گیری نتایج، باید از چنین کدی استفاده کنید:

console.log(usage)
command-line-args

استفاده از این مورد هم ساده است. فقط مطمئن شوید که همه چیز را پردازش کرده، و اعتبارسنجی می‌کنید:

const fs = require('fs')

const path = require('path')

const commandLineArgs = require('command-line-args')

/**

 * اگر آبجکت منتقل شده خالی است،‌ مقدار صحیح را برگردان

 * @param {Object} obj 

 */

const isEmptyObject = (obj) => {

  return JSON.stringify(obj) === JSON.stringify({})

}

const optionDefinitions = [

  { name: 'image', alias: 'i', type: String },

  { name: 'confidence', alias: 'c', type: Number },

  { name: 'filter', alias: 'f', type: String },

  { name: 'quick', alias: 'q' },

  { name: 'version', alias: 'v' },

  { name: 'help', alias: 'h' }

]

let options

try {

  options = commandLineArgs(optionDefinitions)

}

catch(e) {

  console.error()

  console.error('classify:', e.name, e.optionName)

  console.log(usage)

  process.exit(1)

}

// بررسی برای کمک

if (isEmptyObject(options) || 'help' in options) {

  console.log(usage)

  process.exit(1)

}

// بررسی برای نسخه

if ('version' in options) {

  let pkg = require('./package.json')

  console.log(pkg.version)

  process.exit(1)

}

let imagePath

// بررسی برای مسیر

if ('image' in options) {

  imagePath = options.image

}

if (!imagePath) {

  console.error('"--image imagePath" is required.')

  process.exit(1)

}

if (!fs.existsSync(imagePath)) {

  console.log(`exiting: could not find image: ${imagePath}`)

  process.exit(2)

}

let confidence = 50 // default

if ('confidence' in options) {

  confidence = options.confidence

}

// اعتبارسنجی confidence

if (confidence < 0) {

  console.error(`Negative numbers are not valid for 'confidence'.`)

  process.exit(1)

}

if (confidence > 100) {

  console.error(`A value greater than 100 is not valid for 'confidence'.`)

  process.exit(1)

}

confidence = confidence / 100.0

let filterItems = []

if ('filter' in options) {

  const filterFile = options.filter

  // تایید وجود فایل

  if (!fs.existsSync(filterFile)) {

    console.log(`exiting: could not find filter file: ${filterFile}`)

    process.exit(2)

  }

  filterItems = fs.readFileSync(filterFile).toString().split('\n')

}

let quick = false

if ('quick' in options) {

  quick = true

}

// دریافت فایل داده‌ها بر حسب مدل و گزینه‌های سریع

let dataFile

if (model === 'coco') {

  if (quick) {

    dataFile = 'coco300'

  }

  else {

    dataFile = 'coco512'

  }

}

else if (model === 'inception') {

  dataFile = 'inception224'

}

if (!dataFile) {

  console.error(`'${model}' is not valid model.`)

  process.exit(1)

}

در اینجا متوجه خواهید شد که برای دستور --version‌ تمام کاری که باید انجام دهیم این است که فایل package.json را بخوانیم و نسخه را خروجی دهیم. به این صورت، فقط باید آن را در یک محل نگهداری کنیم.

باقی پردازش بررسی این است که ببینیم آیا یک گزینه استفاده شده است یا نه. اگر نشده است، آن را اعتبارسنجی کنیم و...

پس از این که تمام داده‌های مورد نیاز برای پردازش طبقه‌بندی را جمع‌آوری کردیم، آماده‌ایم تا طبقه‌بندی را شروع کنیم.

طبقه‌بندی تصویر با OpenCV

حال که ما تمام پارامترهای دریافت شده از تعامل کاربر با CLI را جمع‌آوری کردیم، بخش جالب ماجرا می‌تواند شروع شود. پردازش سطح بالا آنچنان که فکر می‌کنید هم سخت نیست:

// OpenCV

const cv = require('opencv4nodejs')

// راه‌اندازی مدل

let net

if (dataFile === 'coco300' || dataFile === 'coco512') {

  net = cv.readNetFromCaffe(prototxt, modelFile)

}

// خواندن تصویر

const img = cv.imread(imagePath)

// زمان شروع طبقه‌بندی

let start = new Date()

// دریافت پیش‌بینی‌ها

const predictions = predict(img).filter((item) => {

  // فیلتر کردن چیزی که نمی‌خواهیم

  if (item.confidence < confidence) {

    return false

  }

  // کاربر می‌خواهد آیتم‌ها را فیلتر کند

  if (filterItems.length > 0) {

    if (filterItems.indexOf(classes[item.classIndex]) < 0) {

      return false

    }

  }

  return true

})

// پایان طبقه‌بندی

let end = new Date()

finalize(start, end)

// نوشتن تصویر بروزرسانی شده با نام جدید

updateImage(imagePath, img, predictions)

باید متوجه شده باشید که اینجا، جایی است که برخی داده‌های جمع‌آوری شده از کاربر را برای فیلتر کردن بر حسب confidence استفاده می‌کنیم.

اما، تابع predict جایی است که اکثر کار ما برای بازگردانی پیش‌بینی‌ها انجام می‌شود. و مسئله فقط تابع predict نیست! تعداد زیادی توابع پشتیبانی دیگر هم برای استخراج داده‌ها مورد نیاز هستند.

بیایید به نحوه انجام این قضیه نگاهی داشته باشیم:

/**

 * طبقه‌بندی‌ها را بر حسب تصویر منتقل شده پیش‌بینی می‌کند

 * @param {Object} img The image to use for predictions

 */

const predict = (img) => {

  // سفید رنگ‌بهتری برای پدینگ است

  const white = new cv.Vec(255, 255, 255)

  // تغییر اندازه فایل مدل

  const theImage = img.resizeToMax(modelData.size, modelData.size).padToSquare(white)

  const inputBlob = cv.blobFromImage(theImage)

  net.setInput(inputBlob)

  // forward pass input through entire network, will return

  // classification result as (coco: 1x1xNxM Mat) (inception: 1xN Mat)

  let outputBlob = net.forward()

  if (dataFile === 'coco300' || dataFile === 'coco512') {

    // extract NxM Mat from 1x1xNxM Mat

    outputBlob = outputBlob.flattenFloat(outputBlob.sizes[2], outputBlob.sizes[3])

    // منتقل کردن تصویر اصلی

    return extractResultsCoco(outputBlob, img)

  }

}

اول از همه، باید بدانید که این مدل‌ها trained (آموزش دیده) هستند. یکی از آن‌ها در ابعاد 300 * 300 و دیگری 500 * 500 است. مدل 300 * 300 سریع‌تر است و داده‌های کمتری در آن وجود دارد. مدل 512 * 512 کندتر است و به طور کلی پیش‌بینی‌های دقیق‌تری دارد، زیرا داده‌های بیشتر در آن وجود دارد.

در تابع بالا متوجه خواهید شد، باید تصویر ورودی را به گونه‌ای تغییر اندازه دهیم که با اندازه تصویر trained در مدل برابری داشته باشد. اگر تصویر مورد نظر مربع نباشد، باید آن را pad کنیم. معمولا رنگ سفید استفاده می‌شود؛ زیرا مشکلات کمتری نسبت به یک رنگ مانند سیاه دارد.

سپس تصویر مورد نظر به blob تبدیل می‌شود و به net.setInput منتقل می‌شود. اگر به یاد داشته باشید، ما قبلا این کد را داشتیم:

// initialize model from prototxt and modelFile
let net
if (dataFile === 'coco300' || dataFile === 'coco512') {
  net = cv.readNetFromCaffe(prototxt, modelFile)
}

پس حال باید متوجه شده باشید که آخرین کاری که در تابع قبلی انجام می‌شود، استخراج نتیجه است:

/**

 * Extracts results from a network OutputBob

 * @param {Object} outputBlob The outputBlob returned from net.forward()

 * @param {Object} img The image used for classification

 */

const extractResultsCoco = (outputBlob, img) => {

  return Array(outputBlob.rows).fill(0)

    .map((res, i) => {

      // get class index

      const classIndex = outputBlob.at(i, 1);

      const confidence = outputBlob.at(i, 2);

      // output blobs are in a percentage

      const bottomLeft = new cv.Point(

        outputBlob.at(i, 3) * img.cols,

        outputBlob.at(i, 6) * img.rows

      );

      const topRight = new cv.Point(

        outputBlob.at(i, 5) * img.cols,

        outputBlob.at(i, 4) * img.rows

      );

      // create a rect

      const rect = new cv.Rect(

        bottomLeft.x,

        topRight.y,

        topRight.x - bottomLeft.x,

        bottomLeft.y - topRight.y

      );

      return ({

        classIndex,

        confidence,

        rect

      })

    })

}

اینجا جایی است که شما می‌توانید به index (که به کلاس‌های طبقه‌بندی مربوط است)، سطح confidence طبقه‌بندی و ناحیه rest آبجکت شناخته شده دسترسی داشته باشید. این‌ها predictionهای (پیش‌بینی‌های) ما هستند که سپس فیلتر می‌شوند.

ممکن است این تکه کد را از بالا به یاد داشته باشید:

// نوشتن تصویر بروزرسانی شده با نام جدید
updateImage(imagePath, img, predictions)

هدف این است که یک تصویر جدید را به همراه پیش‌بینی‌های فیلتر شده در یک فایل بنویسیم، تا کاربر بتواند آبجکت مورد نظر و confidence آن را ببیند. این‌ها توابعی هستند که این اتفاق را ممکن می‌کنند:

/**

 * ساخت یک رنگ تصادفی

 */

const getRandomColor = () => new cv.Vec(Math.random() * 255, Math.random() * 255, Math.random() * 255);

/**

 * Returns a function that, for each prediction, draws a rect area with rndom color

 * @param {Arry} آرایه پیش‌بینی‌ها

 */

const makeDrawClassDetections = (predictions) => (drawImg, getColor, thickness = 2) => {

  predictions

    .forEach((p) => {

      let color = getColor()

      let confidence = p.confidence

      let rect = p.rect

      let className = classes[p.classIndex]

      drawRect(className, confidence, drawImg, rect, color, { thickness })

    })

  return drawImg

}

/*

  Take the original image and add rectanges on predictions.

  نوشتن آن در یک فایل جدید.

 */

const updateImage = (imagePath, img, predictions) => {

  // get the filename and replace last occurrence of '.' with '_classified.'

  const filename = imagePath.replace(/^.*[\\\/]/, '').replace(/.([^.]*)$/,`_classified_${dataFile}_${confidence * 100.0}.` + '$1')

  // get function to draw rect around predicted object

  const drawClassDetections = makeDrawClassDetections(predictions);

  // draw a rect around predicted object

  drawClassDetections(img, getRandomColor);

  // write updated image to current directory

  cv.imwrite('./' + filename, img)

}

// draw a rect and label in specified area

/**

 * 

 * @param {String} className Predicted class name (identified object)

 * @param {Number} confidence The confidence level (ie: .80 = 80%)

 * @param {Object} image The image

 * @param {Object} rect The rect area

 * @param {Object} color The color to use

 * @param {Object} [opts={ thickness: 2 }] Options (currently only supports thikness)

 */

const drawRect = (className, confidence, image, rect, color, opts = { thickness: 2 }) => {

  let level = Math.round(confidence * 100.0)

  image.drawRectangle(

    rect,

    color,

    opts.thickness,

    cv.LINE_8

  )

  // draw the label (className and confidence level)

  let label = className + ': ' + level

  image.putText(label, new cv.Point2(rect.x, rect.y + 20), cv.FONT_ITALIC, .65, color, 2)

}

من به این توابع به گونه‌ای وارد نخواهم شد که انگار JavaScript ساده و رایج هستند.

هشدارها

همیشه باید از نوعی فیلتر کردن استفاده کنید. سعی کنید همیشه از سطح confidence استفاده کنید. من معمولا از ۵۰ استفاده می‌کنم، اما احتمالا ۳۰ حداقل مقداری است که انتخاب خواهم کرد. می‌پرسید چرا؟ زیرا این اتفاقی است که می‌افتد:

اگر تصویر شلوغ باشد، تعداد زیادی طبقه‌بندی دریافت خواهید کرد، که اکثر آن‌ها هم جعلی هستند و سطح confidence کمتر از ۱۰ را دارند. سعی کنید با سطح confidence بازی کنید و ببینید که چه چیزی برای شما بهتر کار می‌کند. دقت کنید که این تصویر، مشابه تصویر اول این مقاله است. (چرا شما را به عقب برگرداندم؟)

مثال‌ها

قطار طبقه‌بندی نشده:

قطار طبقه‌بندی شده:

افراد طبقه‌بندی نشده:

افراد طبقه‌بندی شده:

اگر این شخص دندان‌ها خود را نشان می‌داد، احتمالا ۱۰۰ درصد یک فرد به حساب می‌آمد. اما شوخی‌ها به کنار، این کار جالب بود. من همچنان در حالت یادگیری هستم، و مقدار زیادی هم برای یادگیری وجود دارد. امیدوارم مقاله‌ای که نوشتم به شما در راه خود کمک کند. پروژه کامل را می‌توانید بر روی گیت‌هاب مشاهده کنید.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
5 از 1 رای

/@er79ka

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

عرفان کاکایی

مقالات برگزیده

مقالات برگزیده را از این قسمت میتوانید ببینید

مشاهده همه مقالات