ラベル OpenCV の投稿を表示しています。 すべての投稿を表示
ラベル OpenCV の投稿を表示しています。 すべての投稿を表示

2013年2月13日水曜日

Image recognition and Noise reduction

ロボットの原価を低減させる為に、市販の部品を使っていますが、いろいろ課題が発生します。
ロボットの目には、2,000円台のWebcamを使っていますが、高価な物に比べて不安定です。従って、画像認識において、図1のような元のイメージからピンクのボールを認識する場合、大量のノイズが発生する事が多いです。
図2は、その状態を解り易くする為に二値化した物です。大量のノイズが存在します。
このような状況で、図3の様にボールだけを認識しなければなりません。
画像処理は、奥が深いです。だから、面白いです。更に、ロボットと組み合せると、”面白い”の二乗になります。ロボットは、動いていますから、限りなく短い時間で処理をしなければなりません。その情報を元に、駆動系の制御をします。
webcamが、どのような画像を取得しているか解りません。従って、プログラムはそれを前提に作らないとなりません。いやはや、とっても面白い世界です。

しかし、結局、ロボットは、ソフトウェアが勝負ですね。また、ソフトの工数を減らす為には、高価なデバイスが必要になります。結局、高い原価構成のジレンマに陥ります。
だから、企業レベルでロボットの原価を考えると、価格は高くならざるおえません。
それを、僕は、RDCSで解決しています。

図1 元画像 
(白い格子は、画像の保存時に付加)
図2 noiseの大量発生
noiseが解り易いように二値化した画像
HSV色空間では、大量のnoiseを拾ってしまう場合がある
 図3 認識結果
noise処理を行い、ボールだけを認識する 
この処理もRaspberry Piで実行しますが、画像の保存をしなければ処理は瞬時に終わります。

peace!

2013年2月9日土曜日

Performance of the recognizing a moving ball on Raspberry Pi

自律型ロボットは、自分で空間認識をする必要があります。その為には、リアルタイムで画像解析が必要になりますが、Raspberry Piでそれをする事は無理があります。
しかし、どこまで可能なのか?知る必要があり各種の画像処理のパフォーマンステストをしています。
結果的に言えば、私のプログラムのチューニングをすれば、かなりの事ができるという事でした。プログラムを見直すと、無駄な処理やループが沢山ありました。今は、とにかく、数を優先している為、ロジックに無駄が沢山あります (-_-)

(私が開発しているのは、人が操作しない、自律型ロボットです。人が操作する物は、ラジコンだと考えて、ハッキリ分けています。趣味の一つがラジコン飛行なので、、、)

テストの例
「画像入手(webcam 640x480 CV_U8C3) -> 動くボール認識 -> 認識画像出力」
上記100回の処理時間 → 10回計測 平均45秒 約2回/秒 の認識処理
同上 ファイル出力無し → 10回計測 平均30秒 約3回/秒 の認識処理

テスト方法は、下図のピンクのボールを手に持って、Webcamの前で不規則に動かします。Raspberry Pi上の認識プログラムが、usb接続されたwebcamから画像を入手して、動いているボールだけを認識します。
図1
ピンクのボールをwebcamの前で不規則に動かし、ボールだけを認識する
図2
左上から右下に、処理結果の順で並べている
赤が、手で不規則に動かしたボールの認識結果。
光の影響があるので、完全な'円'になっていない。白丸は同じボールであることを示す
認識プログラムは、複数の動く物体を認識できる
図2の8枚のボールの動きは、約3.6秒間(45秒/100x8)の動きのsnapshotになります。
1回の処理毎に、認識した結果をファイルに保存した画像の一部を並べました。保存をしない場合は、同じ時間で、12回のボール認識ができます。

1秒間に、3回の画像認識処理ができれば、僕のロボットの要求仕様を十分に満たす事が出来ます。実際には、風景内の沢山の認識をしているので、ロボットの運用上は1秒間に1回の画像認識処理で十分です。

Raspberry Piは、見た目以上に、すごいヤツです。

peace!

2013年2月7日木曜日

A memorandum : fswebcam

fswebcamは、デフォルトでbannerを出力するので、それを抑制する設定。
忘れない様に、、、
いままでは、画像処理の途中で無くなっていたので無視していたけど、
ある処理をする為に、赤いラインが邪魔になるので、調べました。
デバイスを指定しているのは、Raspberry Piに2台のWebcamを接続している為。
保存ファイル名は、2050-02-07-123950.jpgの様になります。


fswebcam -d /dev/video0 -r 640*480 --no-banner --line-colour '#FF000000' -save /tmp/`date +%Y-%m-%d-%H%M%S.jpg` -S 3


bannerが消えました
ファイル名の2050年は、冗談です。 (^_^)

peace!

Recognizing tomatos on my robot using Raspberry Pi

15万円台、トマト収穫ロボットの基礎技術の紹介です。

今回は、ロボットの目、形状認識の紹介です。
特徴は、2次元の画像データ1枚から、トマトを抽出し、かつ抽出したトマトの3次元の位相関係を作る点です。この位相関係が出来れば、その後の処理を効率よくすすめる事が出来ます。
    Step1: 2次元画像を使ったトマト形状認識と、その他 controlデータ抽出
    Step2: 3次元位相構造の作成

この認識で使っているアルゴリズムは、Computer Visionで開発された様々なOpenCVに実装されているアルゴリズムと、トマトを栽培しながら観察した植物体の位相構造などから作ったアルゴリズムを組合せました。(3D GISの開発経験も役に立ちました)
この内容は、トマト収穫機や収穫済みのトマトのパッキングなど、広範囲に使える基礎的な技術です。また、webcamなどの安価なカメラで行う事を前提に開発しています。

下に表示した画像からのトマト抽出結果は、ブログ標題の背景写真を使っています。トマトの表面が光っている、またガクがついているなど、画像を使った形状認識に於いては、条件が悪いデータです。

青い範囲(640x480)が、Actuatorのmotion planning の対象であり、その中は精度を高めて認識をしています。赤い丸で囲まれた物が、第一段階の画像処理をして認識したトマトです。青い四角の範囲では、ほぼ100%の認識です。この処理は、アルゴリズムの高速化に苦労しました。今は、i5クラスのマシンで1秒の数分の1以下のオーダです。

第一ステップの認識
青い四角は、特に認識精度を要求する範囲
赤い○は、トマトとして認識した形状
シアンの○は、後処理に必要なcontrolデータ
画像のふちは、切れたデータの処理が完成していないので
誤認識が発生しています。(改善予定)
この処理の後に、第二段階として、さらに独自のテンプレートを使った位相構造抽出処理をして、一定の誤差内の3次元データを生成します。3次元データが出来た段階で、トマト認識処理は終了です。第二段階は位相構造の演算が主なので短時間で終わります。
その3次元データは、ActuatorのMotion Planningの元データになります。
このように書くと、単純だけど、僕に取っては認識処理は結構複雑で悩みました。

可能な限り、2次元の画像データから3次元のデータを生成します。その方がコストが安く出来ます。全ては、15万円トマト収穫機の実現のためです。

この認識は、Raspberry Piの性能では負荷が高いので、他の処理への影響も考えてserviceで実施する様にしています。
ただし、プログラム処理ロジックのチューニングの余地がかなりあるので、他の機能の性能が定量的に把握できた段階で、再検討をします。1台のRaspberry Piを専用に割当てても良いし。。。。。

今は、Raspberry Piで画像を取得して、Recognizing Serviceに送って、あっという間に処理して返します。RDCSの腕の見せ所です。
RDCSによって、Raspberry Piは、実際の性能以上のロボットコントローラになります。
こういうのが、smartですよね。
The network is the computer. いい言葉です。

peace!

2013年2月5日火曜日

OpenCv cookbook for Raspberry Pi

何気に、OpenCVのソフトを整理していたら、cookbook 向きのサンプルプログラム集ができてきた。基本的な動きが分かるプログラムだけど、全部で40数個。
今度の教育の提案の時に使おうと思います。

下は、有名なLenna さんの画像を使って顔と目の認識。OpenCVは、こんな事が簡単に出来てしまいます。Raspberry Piはこの処理を”一呼吸”おいた感じでやってしまいました
LXDE の上で、大変な事になっています。
ついでに、gimp が起動しています。
僕は、グラフィック系の勉強は、あんまりパフォーマンスが高くないマシンの方が良いと思っているので、Raspberry Piがちょうどいいかもしれません。
恐るべし、Raspberry Pi。最近、そう思ってきた。

まぁ、提案の題は、こんな感じ。大人向きだから、これでいいでしょう。
子供達のための、ビジュアル技法を使った、問題解決法

peace!

2013年1月28日月曜日

Xtion Pro and OpenNI2 on Raspberry Pi

OpenNI2のversion up が去年の12月にありました。
暫く移行を躊躇していました。しかし、そろそろ考えないとならないので検証しました。
Raspberry PiへのOpenNI2のインストール方法は、---> Here click!

      Xtion Pro-->  Raspberry Pi  > Arch Linux  >  OpenNI2  >  LXDE  >  AP

本日テストをして、その結果から、OpenNI2に移行する事に決めました。
写真は、Color stream と Depth steam テストのsnap shotです。
上のDepth画像と、下のVideo画像は、対象は違います。
Depth Stream  (snap shot)
Video Stream (snap shot)
LXDE上で、それぞれOpenCVを使ってストリームを表示しましたが、スムーズに表示できました。十分使える範囲だと思います。
これで、懸案の、OpenNI2への移行もなんとかなりそうです。
ただし、コンパイル&リンクの時間は、以前より長いかもしれません。

このような検証をする為に、LXDEの環境も作ってありますが、やはり使っているとフラストレーションが溜ります。
今回も、実行モジュールができるまでは、MacからSSH接続をして、画面表示の状態を確認するときだけLXDEで画面を見ます。

PCLの入力システムとしてのRaspberry Pi + Xtionは、なかなかの組合せです。
ただし、NITEは、考えてはなりませぬ。

今回のOpenNI2も、Kinectを公式にはサポートできない。
だんだんと、Raspberry PiでKinectを使う事が難しくなってきた。

いよいよ、Kinect 2台は、農場の監視カメラか。。。。。

peace!

2013年1月17日木曜日

Recognizing and Extracting tomatos from a photo digital image.

トマト収穫ロボットを作るためには、画像からのトマト認識が重要です。
その画像認識技術の開発を進めています。
複数の方法を組合せて認識しますが、安定してきたので紹介します。
OpenCVも使っていますが、認識技術の中核は自作しました。

下の写真は、遠目で撮った写真から収穫が可能なトマトの候補を認識した結果です。何故、このような離れた位置からの認識が必要なのかは、長くなるので別な機会に説明します。
まだまだロジックの改良が必要ですが、目標は達成できそうです。
トマト収穫ロボットでは、2000x1500程度の画素数のwebcamを使うので、遠目の撮影を行う為のトマトとの位置関係、そして移動ロボットへの搭載方法を考えています。
元々、Low Cost Robotなので、1台専用にしても良いですが、、、

葉の陰や、重なっていても、それなりに認識しています。
この処理は、ロボットに搭載しているRaspberry Piの性能では、実用上の速度で出来ないので、RDCS上に配置されたi5 2.5GHz のマシンで動く、Space Recognizing Serviceで行います。その結果をロボットに返します。
遠目の撮影で、かつ 影になっているのトマト認識
黄緑色の十字マーカーは、認識した収穫候補のトマトを表します 
(処理対象の画像解像度 3680 × 2760)
画像認識の点で言えば、如何に、一枚の画像から多くの情報を、位相関係も含めて、かつ高速に抽出するのかが、私の一番重要視している点です。
50センチ程度から撮影した画像で行えば、ほぼ位相関係も含めて認識が可能となってきました。実際の収穫対象のトマトを選定する為には、まずは十分です。あとは、画像から推定するトマトの糖度ですが、なんとかなるでしょう。なんたって、サンプルが大量にありますから、実験しながら試行錯誤し、閾値の組合せを作れば良いです。接近したトマト画像の認識処理は、Raspberry Piで処理する目処も立ちました。

ちなみに、このトマトは、フルーツトマトで、糖度が約10以上あります。
私のトマト栽培は、化学合成農薬/肥料や、有害物の混ざり込む可能性のある農業用水を使いません。有用な微生物や安全な水を活用して作っています。
その理由は、自分が化学合成物に少し敏感な体質だからです。安心して食べれる物を作って、普通に食べれるのは、そのような体質の私にとっては大きな幸せです。

あ〜、時間が足りない、プログラムもっと、作りたい、改良したい。
工作の方は、ラジコン飛行機をしていたので、なんとかなりそうです。
そんな事もあり、主要な機能を遠隔から無線で操縦するする物はラジコンだと思っています。ロボットは、対象の作業を自律的に行う装置だと考えています。私は、人手を減らす為に使うので操縦しないですむようにしたいです。

peace!

2012年10月3日水曜日

Performance testing Xtion on Raspberry Pi
Using a capturing server

Raspberry Pi is a good machine. But it don't have a capacity for 3D processing.  For this test, I use a Raspberry Pi as a capturing service (server).  And MacMini is 3D Processing service and viewing.
My test environment diagram is below.

Raspberry Piは、3D処理をするには性能がたりません。従って、Xtionを使ったcaputuring setverとして使い、空間認識などの処理をMacminiで処理します。そのテストをしました。
テストは、RDCS環境を使いまいた。RDCSは、分散コンピューティングをベースにしたロボットプラットフォームです。その機能を使ってテストをしました。
Micminiでpoint cloud処理をしています。さすがに瞬間的な処理です。


 Capturing Service        3D recognizing service
   Xtion
    Raspberry Pi                Mac MIni
----------------------------------------------------  Coordinator Layer
---------------------------------------------------- (Service Control)
             |                                |
----------------------------------------------------      Cooperative Layer
---------------------------------------------------- (Communication Control)

A performance testing was executed on RDCS.

RDCS is a robot platform based on a distributed computing model.
A lots of robot function (hardware, software) is connected by  "Cooperative Control" and "Coordinator".
"Cooperative Control" is a network communication control.  The control  connects computer , actuator, sensor and other robot device.
"Coordinator" offers robot required software. "Coordinator" integrates a lot robot software on RDCS.

This test flow is :

(1) 3D recognized service requests a data set ( image and depth ) to Capturing service.
(2) Capturing service on Raspberry Pi execute to capture image and depth.
(3) 3D recognized service receive a couple of capturing data.
(4) 3D processing and viewing

RESULT (sec)
(1)      0-1
(2)     8-10 (include the time which is Xtion initialize and terminate)
                   A Capture time is 1-2 sec.
(3)      0-1
(4)      0-1


This result means that a single robot don't have all function in it.
Therefore, it's able to develop a lightweight and low cost robot.
There are a lot of kinds wrok in farm. And my farm is large. Thus I need  a lot of robot. To do that,  I have to develop a low cost and multi-function robot.
My robots are structured by base robot and add-in function in order to realize a multi-function of robot.
And these robots have to been set up by commercialized product and builded  by component-based.

Now I'm developing a core component of robot  using 2 or 3 Raspberry Pi.
A core component of robot is structured by  6 components which are robot platform(RDCS), space recognition, obstacle detection, moving, fail-safe and recovery.
An effectivity of RDCS which I have developed was  confirmed by this testing.


この結果は、1台のロボットに全ての機能を集約しなくても良い事を示しています。
従って、軽量なかつ低価格のロボットを作る事ができます。
ロボットプラットフォーム、障害物検知、通信、移動、フェイルセーフと回復機能を持ったシンプルなロボットを、Raspberry Piを2、3個使って作れば良いと思います。その上に、アクチュエータなどのロボットのパーツを載せればよのではないでしょうか?
私の開発している、RDCSのアーキテクチャーの有効性を確認できました。




                                                     3D point cloud

Peace!

2012年9月28日金曜日

Performance testing Xtion on Raspberry Pi
Capturing RGB

Yesterday, I had done a performance testing.
A test program attached this post.
It's a processing time  from capturing  a  RBG data via OpenNI -Xtion  to writing a formated data.

A test path is below.
Xtion -> OpenNI (RGB, Depth) -> OpenCV (conversion format, writing) -> SD-card

(1)Capturer : 640x480, RGB24
(2)Pieces of writing format type are bmp, dib, jpd, jp2, png, pbm, pgm, ppm, ras, and tiff.

RESULT
(1) It's take 15-20 sec from capturing to writing SD-card.
        processing             time (sec)
     Initialize OpenNI            : 4 - 5
     Capture depth & RBG  : 0 - 1
     Merge                           : 3 - 4
     Convert & save            : 4 - 5
     Terminate                     : 4 - 5
     ( Processing time on MacMini (i5 2.5GHz 8G) is 2sec. )
   
(2) Processing time depends data format a little.
     Jpeg format is faster than png, 2-3 seconds.
(3) There aren't a difference  of processing time between clock speed 700MHz and 800MHz.

Next performance testing is a following process.
Capture (Raspberry Pi) ---  depth & RGB  (UDP) --> Receive (other machine).
This test use a function of Cooperative control of RDCS.

Peace!

/*****************************************************************************************
        Sample snapshot & save  program   (No Warranty)
******************************************************************************************/
OpenNI  1.5.4
Sensor  5.1.2.1
OpenCV 2.4
Xtion Pro Live

(1) Raspberry Pi  800MHz and 700MHz
Arch Linux arm 3.2.27-8-ARCH+
gcc (GCC) 4.7.1 20120721
It need a compiler option "-mcpu=arm1176jzf-s".

(2) Macmini
Darwin Kernel Version 12.2.0
llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658)

(3) Windows
No test.

//---------------------------------------------------------------------------
// Includes
//---------------------------------------------------------------------------
#include <XnOS.h>
#include <opencv2/opencv.hpp>
#include <XnCppWrapper.h>
#include <fstream>
#include <iostream>
#include <string>
#include <cstring>

#include <stdio.h>
//#include <stdlib.h>
#include <time.h>
#include <math.h>

using namespace std;
using namespace xn;
using namespace cv;
//---------------------------------------------------------------------------
// Defines
//---------------------------------------------------------------------------
#define SAMPLE_XML_PATH "../../Config/SamplesConfig.xml"

//#define PR1(A)
//#define PR2(A, B)
#define PR1(A) std::cout << A << endl;
#define PR2(A, B) std::cout << A << ":" << B << endl;
#define ER1(A) std::cout << A << endl;
#define ER2(A, B) std::cout << A << ":" << B << endl;

//---------------------------------------------------------------------------
// Globals
//---------------------------------------------------------------------------
XnStatus nRetVal = XN_STATUS_OK;

float* g_pDepthHist;
XnRGB24Pixel* g_pTexMap = NULL;
unsigned int g_nTexMapX = 0;
unsigned int g_nTexMapY = 0;
XnDepthPixel g_nZRes;

Context g_context;
xn::ScriptNode g_scriptNode;
xn::DepthGenerator g_depth;
xn::ImageGenerator g_image;
xn::DepthMetaData g_depthMD;
xn::ImageMetaData g_imageMD;


cv::Mat colorArr[3];
cv::Mat colorImage;

const XnRGB24Pixel* pImageRow;
const XnRGB24Pixel* pPixel;

void printTime(const char *msg) ;

//---------------------------------------------------------------------------
// Code
//---------------------------------------------------------------------------

/************************************************************************
*** Callback glutIdle
*************************************************************************/
void printTime(const char *msg) {
time_t timer;
struct tm *date;
char str[256];

timer = time(NULL);
date = localtime(&timer);
PR2(msg, asctime(date));
}
/************************************************************************
*** main
*************************************************************************/

int main(int argc, char* argv[])
{
XnStatus rc;
EnumerationErrors errors;
IplImage bgrIpl;
cv::Mat bgrImage;
int i, iFrame, iClean;
int iCnt = 0;

char framenumber[10];
std::stringstream ss;
std::string str_frame_number;



// -------------------------------------------------------------------------
// check argument.
// -------------------------------------------------------------------------
if (argc != 4) {
PR1("argument error. output file, format (jpg, png), count.");
return 1;
}
iFrame = atoi(argv[3]);
if (iFrame < 1 || iFrame > 100) {
ER1("frame 1-100.");
return 1;
}
if (!(strcmp(argv[2], "bmp") == 0 ||
strcmp(argv[2], "dib") == 0 ||
strcmp(argv[2], "jpg") == 0 ||
strcmp(argv[2], "jp2") == 0 ||
strcmp(argv[2], "png") == 0 ||
strcmp(argv[2], "pbm") == 0 ||
strcmp(argv[2], "pgm") == 0 ||
strcmp(argv[2], "ppm") == 0 ||
strcmp(argv[2], "ras") == 0 ||
strcmp(argv[2], "tiff") == 0
)) {
ER1("format error.");
return 1;
}

// -------------------------------------------------------------------------
// set up OpenNI camera.
// -------------------------------------------------------------------------
// read a device configuration file and initialyze
rc = g_context.InitFromXmlFile(SAMPLE_XML_PATH, g_scriptNode, &errors);
if (rc == XN_STATUS_NO_NODE_PRESENT){
XnChar strError[1024];
errors.ToString(strError, 1024);
ER1(strError);
return (rc);
}else if (rc != XN_STATUS_OK){
ER2("Open failed ", xnGetStatusString(rc));
return (rc);
}

// Searches for an existing created node of a specified type and returns a reference to it.
rc = g_context.FindExistingNode(XN_NODE_TYPE_DEPTH, g_depth);
if (rc != XN_STATUS_OK){
ER1("No depth node exists! Check your XML.");
return 1;
}

// Searches for an existing created node of a specified type and returns a reference to it.
rc = g_context.FindExistingNode(XN_NODE_TYPE_IMAGE, g_image);
if (rc != XN_STATUS_OK){
ER1("No image node exists! Check your XML.");
return 1;
}

PR1("first genelation*******************");
g_depth.GetMetaData(g_depthMD);
g_image.GetMetaData(g_imageMD);
PR1("***********************************");

// Hybrid mode isn't supported in this sample
if (g_imageMD.FullXRes() != g_depthMD.FullXRes() || g_imageMD.FullYRes() != g_depthMD.FullYRes()){
ER1 ("The device depth and image resolution must be equal!");
return 1;
}

// RGB is the only image format supported.
if (g_imageMD.PixelFormat() != XN_PIXEL_FORMAT_RGB24){
ER1("The device image format must be RGB24");
return 1;
}

// Texture map init
g_nTexMapX = (((unsigned short)(g_depthMD.FullXRes()-1) / 512) + 1) * 512;
g_nTexMapY = (((unsigned short)(g_depthMD.FullYRes()-1) / 512) + 1) * 512;
g_pTexMap = (XnRGB24Pixel*)malloc(g_nTexMapX * g_nTexMapY * sizeof(XnRGB24Pixel));

g_nZRes = g_depthMD.ZRes();
g_pDepthHist = (float*)malloc(g_nZRes * sizeof(float));

rc = g_context.WaitAnyUpdateAll();
if (rc != XN_STATUS_OK){
ER2("Read failed ", xnGetStatusString(rc));
return 1;
}
// -------------------------------------------------------------------------
// main routine.
// -------------------------------------------------------------------------

for (i = 0; i < iFrame; i++) {
printTime("start generation");
g_depth.GetMetaData(g_depthMD);
g_image.GetMetaData(g_imageMD);
printTime("end generation");
}

printTime("start output");
g_depth.GetAlternativeViewPointCap().SetViewPoint(g_image);
pImageRow = g_imageMD.RGB24Data();

colorArr[0] = cv::Mat(g_imageMD.YRes(), g_imageMD.XRes(),CV_8U);
colorArr[1] = cv::Mat(g_imageMD.YRes(), g_imageMD.XRes(),CV_8U);
colorArr[2] = cv::Mat(g_imageMD.YRes(), g_imageMD.XRes(),CV_8U);

printTime("create new matrix");
PR2("g_imageMD.YRes", g_imageMD.YRes());
for (int y = 0; y < g_imageMD.YRes(); y++){
pPixel = pImageRow;
uchar* Bptr = colorArr[0].ptr<uchar>(y);
uchar* Gptr = colorArr[1].ptr<uchar>(y);
uchar* Rptr = colorArr[2].ptr<uchar>(y);
for(int x=0; x < g_imageMD.XRes() ;++x , ++pPixel){
Bptr[x] = pPixel->nBlue;
Gptr[x] = pPixel->nGreen;
Rptr[x] = pPixel->nRed;
}
pImageRow += g_imageMD.XRes();
}
printTime("start merge");
cv::merge(colorArr, 3, colorImage);

printTime("start save");
sprintf(framenumber,"%s.%s",argv[1], argv[2]);
ss << framenumber;
ss >> str_frame_number;
std::string str_aux = str_frame_number;
bgrIpl = colorImage;
cvSaveImage(str_aux.c_str(), &bgrIpl);
printTime("end output");
//g_context.Shutdown();

return 0;
}