Color Tracking

 Wabカメラ画像の中から指定した色を抽出します。
又、その色の部分の移動の軌跡を描画します。

 カラー画像(RGB)をHue(色相)、Saturation(彩度)、Value(明度)に変換し、HSV各々の上限下限を設定し、その条件を満足する色の画像を抽出します。
移動の軌跡は、その色の中心点の移動をカメラ画像の中に線画します。

  プログラムは、https://github.com/Laex/Delphi-OpenCVからダウンロードしたファイルの"Samples"ホルダーの中に入っているものを修正したものです。
OpenCVのインストール方法は、このホームページのOpenCV_WabCamera.htmlと、https://github.com/Laex/Delphi-OpenCVを参照してください。
 Delphiのみで、作成をしても出来ない事はないのですが、労力を考え、OpenCVを利用しています。

 一般的には Hue(色相) 0~360°、Saturation(彩度) 0~100%(0~1)、Value(明度)0~100%(0~1)で表しますが、OpenCVでは、Hue(色相) 0~180、Saturation(彩度) 0~255、Value(明度)0~255 で表しています。(Hue色相はリングなので、360°は0°と同じです。OPenCVの場合は180と0は同じことになりますが検索する場合180と設定しても0は検索されません。)
各データーを1バイトで表現する為です。
色の抽出は、H、S、Vの各値の上限と下限を設定し、その中に入るものを抽出します。
抽出判定は、関数cvInRangeSを使用しますが、問題はHue(色相)の判定で、特に(色相0)の時です。
にもバラつきがあり、0を挟んで、±5~10位なので、その範囲を指定したいのですが、関数cvInRangSは、HSV処理に限った関数ではないのでHue(色相)のリング処理はされません。
360°表記で0±10°の場合OpenCVの場合、175~180と、0~5の範囲で検出し、2つの検出結果を合算して、検出結果とする必要があります。
 プログラムでの入力は、上限値を360+10の370とするか、10とし、下限値を350として入力すると、0±10°の範囲で検出が出来る様になっています。
下限値-10°でのマイナス入力処理はしません、必要であればプログラムを修正してください。

 色相は0~360°の値で入力しますが、上限が360°を超える場合リングなので0に戻りますので、下限350°で上限10°の場合、下限350°、上限370°として入力します。
彩度と、明度は、0~1の値です。
 マウスで、左の画像部をクリックすると、その部分の色のHSVの値が表示されますので、その値を参考にHSVの検出範囲を設定します。
 移動軌跡の表示色の変更は、プログラムの変更が必要です。


プログラム

unit uMainForm;

interface
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, ocv.highgui_c, ocv.core.types_c,
  Vcl.StdCtrls, Vcl.ExtCtrls, System.UIConsts, System.Math;
type
  TMainForm = class(TForm)
    pb1: TPaintBox;
    pb2: TPaintBox;
    rb1: TRadioButton;
    rb2: TRadioButton;
    CapSttBtn: TButton;
    CapStpBtn: TButton;
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    LabeledEdit3: TLabeledEdit;
    Memo1: TMemo;
    LabeledEdit4: TLabeledEdit;
    LabeledEdit5: TLabeledEdit;
    LabeledEdit6: TLabeledEdit;
    procedure FormDestroy(Sender: TObject);
    procedure pb1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure CapSttBtnClick(Sender: TObject);
    procedure CapStpBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    procedure OnIdle(Sender: TObject; var Done: Boolean);
    procedure DetectingRedObjects(img: pIplImage);
    procedure TrackingRedObjects(img: pIplImage);
    procedure trackObject(imgThresh: pIplImage);
    procedure RGBtoHSV(Red, Green, Blue: Integer; var Hue, Saturation, Value: Double);
    procedure Dispmess(mess: string);
  public
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses
  ocv.core_c, ocv.imgproc_c, ocv.imgproc.types_c, ocv.utils;

var
  capture       : pCvCapture;
  lastX, lastY  : Integer;
  imgTracking   : pIplImage;
  Hmax,Smax,Vmax   : Double;
  Hmin,Smin,Vmin   : Double;

// This function threshold the HSV image and create a binary image
function GetThresholdedImage(imgHSV: pIplImage; FZERO: boolean): pIplImage;
begin
  Result := cvCreateImage(cvGetSize(imgHSV), IPL_DEPTH_8U, 1);
  // Convet a video into a binary image based on the any color.
  // Any color area of the video is assined to '1' and other area is assigned to '0' in the binary image
  if not FZERO then
    cvInRangeS(imgHSV, cvScalar(HMIN, SMIN, VMIN), cvScalar(HMAX, SMAX, VMAX), Result)
  else
    cvInRangeS(imgHSV, cvScalar(0, SMIN, VMIN), cvScalar(HMAX - 180, SMAX, VMAX), Result)
end;

procedure TMainForm.DetectingRedObjects(img: pIplImage);
var
  imgHSV      : pIplImage;
  imgThresh   : pIplImage;
  frame       : pIplImage;
  imgThreshOr : pIplImage;
begin
  frame := cvCloneImage(img);

  // smooth the original image using Gaussian kernel
  cvSmooth(frame, frame, CV_GAUSSIAN, 3, 3);

  imgHSV := cvCreateImage(cvGetSize(frame), IPL_DEPTH_8U, 3);

  // Change the color format from BGR to HSV
  cvCvtColor(frame, imgHSV, CV_BGR2HSV);

  imgThresh := GetThresholdedImage(imgHSV, False);
  // 色相のリング対策
  if HMAX > 180 then begin
     imgThreshOr := GetThresholdedImage(imgHSV, True);
     cvOr(imgThreshOr, imgThresh, imgThresh, NIl);
     cvReleaseImage(imgThreshOr);
  end;
  // smooth the binary image using Gaussian kernel
  cvSmooth(imgThresh, imgThresh, CV_GAUSSIAN, 3, 3);

  ipDraw(pb1.Canvas.Handle, frame, pb1.ClientRect);

  ipDraw(pb2.Canvas.Handle, imgThresh, pb1.ClientRect);

  // Clean up used images
  cvReleaseImage(imgHSV);
  cvReleaseImage(imgThresh);
  cvReleaseImage(frame);
end;

procedure TMainForm.trackObject(imgThresh: pIplImage);
var
  moments   : pCvMoments;
  moment10, moment01, area: Double;
  posX, posY: Integer;
begin
  // Calculate the moments of 'imgThresh'
  moments := allocMem(sizeof(TCvMoments));
  cvMoments(imgThresh, moments, 1);
  moment10 := cvGetSpatialMoment(moments, 1, 0);
  moment01 := cvGetSpatialMoment(moments, 0, 1);
  area := cvGetCentralMoment(moments, 0, 0);

  // if the area<1000, I consider that the there are no object in the image and it's because of the noise, the area is not zero
  if (area > 1000) then
  begin
    // calculate the position of the ball
    posX := Trunc(moment10 / area);
    posY := Trunc(moment01 / area);

    if (lastX >= 0) and (lastY >= 0) and (posX >= 0) and (posY >= 0) then
      // Draw a yellow line from the previous point to the current point
      cvLine(imgTracking, cvPoint(posX, posY), cvPoint(lastX, lastY), cvScalar(0, 0, 255), 4);

    lastX := posX;
    lastY := posY;
  end;
  freemem(moments);
end;

procedure TMainForm.TrackingRedObjects(img: pIplImage);
var
  imgHSV      : pIplImage;
  imgThresh   : pIplImage;
  frame       : pIplImage;
  imgThreshOr : pIplImage;
begin
  frame := cvCloneImage(img);

  cvSmooth(frame, frame, CV_GAUSSIAN, 3, 3); // smooth the original image using Gaussian kernel

  imgHSV := cvCreateImage(cvGetSize(frame), IPL_DEPTH_8U, 3);
  cvCvtColor(frame, imgHSV, CV_BGR2HSV);     // Change the color format from BGR to HSV
  imgThresh := GetThresholdedImage(imgHSV, False);
  // 色相のリング対策
  if HMAX > 180 then begin
     imgThreshOr := GetThresholdedImage(imgHSV, true);
     cvOr(imgThreshOr, imgThresh, imgThresh, NIl);
     cvReleaseImage(imgThreshOr);
  end;

  cvSmooth(imgThresh, imgThresh, CV_GAUSSIAN, 3, 3); // smooth the binary image using Gaussian kernel

  // track the possition of the ball
  trackObject(imgThresh);

  // Add the tracking image and the frame
  cvAdd(frame, imgTracking, frame);

  ipDraw(pb1.Canvas.Handle, frame, pb1.ClientRect);

  ipDraw(pb2.Canvas.Handle, imgThresh, pb1.ClientRect);

  // Clean up used images
  cvReleaseImage(imgHSV);
  cvReleaseImage(imgThresh);
  cvReleaseImage(frame);
end;

procedure TMainForm.OnIdle(Sender: TObject; var Done: Boolean);
var
  frame: pIplImage;
begin
  if Assigned(capture) then begin
    frame := cvQueryFrame(capture);
    if Assigned(frame) then begin
      if rb2.Checked then
        DetectingRedObjects(frame)
      else
        if rb1.Checked then begin
          if not Assigned(imgTracking) then begin
            // create a blank image and assigned to 'imgTracking' which has the same size of original video
            imgTracking := cvCreateImage(cvGetSize(frame), IPL_DEPTH_8U, 3);
            cvZero(imgTracking); // covert the image, 'imgTracking' to black
          end;
          TrackingRedObjects(frame);
        end;
    end;
  end;
  Done := False;
end;

// RGB から HSV 変換ルーチン Hsu 色相 Saturation 彩度 Value 明度
procedure TMainForm.RGBtoHSV(Red, Green, Blue: Integer; var Hue, Saturation, Value: Double);
var
  CMax, CMin, CG: Integer;
begin
  Hue := 0;
  CMax := Max(Red, Max(Green, Blue)); // 最大値検索
  CMin := Min(Red, Min(Green, Blue)); // 最小値検索
  Value := CMax / 255;                // 最大値が明度の値
  // Value(明度) = 0 の時
  if Value = 0 then begin
    Saturation := 0;
    exit;
  end;
  // Value(明度) > 0 の時
  Saturation := (CMax - CMin) / CMax;                   // 彩度の計算
  if CMax > CMin then begin                             // 色相の計算 CMAXとCMINが等しい場合は H(色相) = 0
    CG := CMax - CMin;
    if Red = CMax then Hue := 60 * (Green - Blue) / CG
    else
      if Green = CMax then Hue := 60 * (Blue - Red) / CG + 120
      else
        if Blue = CMax then Hue := 60 * (Red - Green) / CG + 240;
    if Hue < 0 then Hue := Hue + 360;
  end;
end;

// 表示画像からのHSV取得
procedure TMainForm.pb1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  RGBColor: Tcolor;
  H,S,V   : Double;
  R,G,B   : integer;
begin
  RGBColor := pb1.Canvas.Pixels[x,y];
  B := RGBColor and $00FF0000;
  B := B shr 16;
  G := RGBColor and $0000FF00;
  G := G shr  8;
  R := RGBColor and $000000FF;
  RGBtoHSV(R, G, B, H, S, V);
  memo1.Clear;
  memo1.Lines.Add('Hue        ' + floatTostrF(H, ffFixed, 3, 0));
  memo1.Lines.Add('Saturation ' + floatTostrF(S, ffFixed, 1, 3));
  memo1.Lines.Add('Value      ' + floatTostrF(V, ffFixed, 1, 3));
end;

procedure TMainForm.CapStpBtnClick(Sender: TObject);
begin
  Application.OnIdle := nil;
  if Assigned(capture) then cvReleaseCapture(capture);
  if Assigned(imgTracking) then cvReleaseImage(imgTracking);
  CapStpBtn.Enabled := False;
  CapSttBtn.Enabled := True;
end;

procedure TMainForm.Dispmess(mess: string);
begin
  ShowMessagePos(mess, MainForm.Left + 20, MainForm.Top + 30);
end;

// 色相0~360°彩度0~1.0 明度0~1.0 が一般的ですが
// OpenCVでは、色相0~180° 彩度0~255 明度0~255 です。
// 角値を1byteで表現しています
procedure TMainForm.CapSttBtnClick(Sender: TObject);
var
  ch: integer;
begin
  val(LabeledEdit1.Text, HMAX, ch);
  if ch <> 0 then begin
    Dispmess('Hue(色相)上限に間違いがあります。');
    exit;
  end;
  val(LabeledEdit2.Text, SMAX, ch);
  if ch <> 0 then begin
    Dispmess('Saturation(彩度)上限に間違いがあります。');
    exit;
  end;
  val(LabeledEdit3.Text, VMAX, ch);
  if ch <> 0 then begin
    Dispmess('Value(明度)上限に間違いがあります。');
    exit;
  end;
  val(LabeledEdit4.Text, HMIN, ch);
  if ch <> 0 then begin
    Dispmess('Hue(色相)下限に間違いがあります。');
    exit;
  end;
  val(LabeledEdit5.Text, SMIN, ch);
  if ch <> 0 then begin
    Dispmess('Saturation(彩度)下限に間違いがあります。');
    exit;
  end;
  val(LabeledEdit6.Text, VMIN, ch);
  if ch <> 0 then begin
    Dispmess('Value(明度)下限に間違いがあります。');
    exit;
  end;
  if HMAX > 510 then begin
    Dispmess('Hue(色相)の上限の値が大きすぎます。');
    exit;
  end;
  if HMIN > 360 then begin
    Dispmess('Hue(色相)の下限の値が大きすぎます。');
    exit;
  end;
  if (HMAX < 0) or (HMIN < 0) then begin
    Dispmess('Hue(色相)の上限又は下限の値が0以下です。');
    exit;
  end;
  if SMAX < SMIN then begin
    Dispmess('Saturation(彩度)の上限と下限の値が逆です。');
    exit;
  end;
  if VMAX < VMIN then begin
    Dispmess('Value(明度)の上限と下限の値が逆です。');
    exit;
  end;
  if HMAX < HMIN then HMAX := 360 + HMAX;
  // opencv では色相(Hue)の値は0~180°
  HMAX := HMAX / 2;
  HMIN := HMIN / 2;
  // 彩度(Saturation)の値は0~255
  SMAX := SMAX * 255;
  SMIN := SMIN * 255;
  // 明度(Valu)の値は0~255
  VMAX := VMAX * 255;
  VMIN := VMIN * 255;
  lastX := -1;
  lastY := -1;
  capture := cvCreateCameraCapture(CV_CAP_ANY);
  if Assigned(capture) then begin
   // 画像のサイズ設定 カメラの値に合わせて下さい。
    cvSetCaptureProperty(Capture, CV_CAP_PROP_FRAME_WIDTH, 640);
    cvSetCaptureProperty(Capture, CV_CAP_PROP_FRAME_HEIGHT, 480);

    Application.OnIdle := OnIdle;
    CapStpBtn.Enabled := True;
    CapSttBtn.Enabled := False;
  end;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  CapStpBtn.Enabled := False;
  Memo1.Clear;
  Memo1.Font.Height := 15;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  if Assigned(capture) then cvReleaseCapture(capture);
  if Assigned(imgTracking) then cvReleaseImage(imgTracking);
end;

end.

画像処理一覧へ戻る

      最初に戻る