android app.です。
こちらはjavaで開発しました。
opencvにまつわる情報でkotlinのものが少ないため。
アプリ紹介
紹介動画
苦労した点
今回は色々苦労があった。
①viewでタッチの機能を備えるため、画像をimage viewでなく、自作
②タッチで座標を拾う
③open cvを使う
①viewを自作
こちらのようにviewを自作した。詳細は中を見たほうが理解が進むであろう。
public class Myview extends View {
private Context context;
private Drawable drawable;
private Rect lastRect;
private Bitmap bmpview=null;
private Integer flgview;
private int[][] posi={{1,125,125},{2,325,125},{3,525,125},{4,725,125}};
public Myview(Context context){
super(context);
this.context=context;
}
public Myview(Context context, AttributeSet attrs) {
super(context,attrs);
flgview=0;
}
//ここでメインアクティビティからbmpを受け取り、viewに描画の準備
public void showCanvas(Bitmap bmp, Integer flgbase){
bmpview=bmp;
flgview=flgbase;
invalidate();
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
c.drawColor(Color.WHITE);
int w=this.getWidth();
int h=this.getHeight();
Rect rect=new Rect(5,5,w-10,h-10);
if(bmpview==null){
Paint p=new Paint();
p.setStyle(Paint.Style.STROKE);
p.setColor(Color.DKGRAY);
c.drawRect(rect,p);
for (int i=0; i<10; i++){
Paint p2=new Paint();
p2.setStyle(Paint.Style.FILL);
p2.setColor(Color.rgb(25*i,0,0));
c.drawCircle(25*i+125, 25*i+125, 100, p2);
}
}
else {
//上で受け取ったbmpを使ってbmpを画面に描画
c.drawBitmap(bmpview,0,0,new Paint());
Paint p00=new Paint();
p00.setStyle(Paint.Style.FILL);
p00.setColor(Color.rgb(255,0,0));
//メインアクティビティの操作で赤点を出す
switch (flgview) {
case 0:
break;
case 1:
//タッチで移動
c.drawCircle(posi[0][1], posi[0][2], 10, p00);
c.drawCircle(posi[1][1], posi[1][2], 10, p00);
c.drawCircle(posi[2][1], posi[2][2], 10, p00);
c.drawCircle(posi[3][1], posi[3][2], 10, p00);
break;
case 2:
break;
case 3:
break;
case 4:
break;
}
}
}
//ここで画面タッチ時の処理をする
@Override
public boolean onTouchEvent(MotionEvent event) {
int action=event.getAction();
int x=(int)event.getX();
int y=(int)event.getY();
//四隅ドラッグ
if(flgview==1) {
int index = -1;
int i;
int hantei = 30;
//タッチの処理の仕方がいくつかあるが、色々やった結果これに。他でやると赤点が重なった。。
switch (action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < 4; i++) {
if (posi[i][1] - hantei < x && posi[i][1] + hantei > x &&
posi[i][2] - hantei < y && posi[i][2] + hantei > y) {
index = i + 1;
}
}
break;
}
switch (index) {
case -1:
break;
case 1:
posi[0][1] = x;
posi[0][2] = y;
break;
case 2:
posi[1][1] = x;
posi[1][2] = y;
break;
case 3:
posi[2][1] = x;
posi[2][2] = y;
break;
case 4:
posi[3][1] = x;
posi[3][2] = y;
break;
default:
break;
}
}
invalidate();
return true;
}
//これはメインアクティビティで使う。
public int[][] getposition(){
return posi;
}
}
②タッチの座標を拾い処理&③opencvで処理
以下のようにタッチした画像を拾って4角の座標をmyviewの関数から拾う。
そして4角の座標から射影変換を実施
こちらはmain actibityに記載。
4角の座標を拾って、opencvの処理に入れるのに苦労した。
射影変換では角の順番を正確に入れる必要があるため。
private Bitmap shaei(Bitmap bmphiki){
Mat mat=new Mat();
Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ
int [][]posi_shaei=myview.getposition();
int [][]posi_shaei1=new int[4][3];
int w=bmphiki.getWidth();
int h=bmphiki.getHeight();
Bitmap bmpmodori=Bitmap.createBitmap(
bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);
int kyori;
Integer[] irekae={-1,-1,-1,-1};
int count=0;
kyori=w*w+h*h;
//左上
for(int i=0; i<4; i++){
if((posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2])<kyori){
irekae[0]=i;
kyori=(posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2]);
}
}
kyori=w*w+h*h;
//右上
for(int i=0; i<4; i++){
if(Arrays.asList(irekae).contains(i)){
count=count+1;
}
else{
if(((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
+(posi_shaei[i][2])*(posi_shaei[i][2]))<kyori){
irekae[1]=i;
kyori=((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
+(posi_shaei[i][2])*(posi_shaei[i][2]));
}
}
}
kyori=w*w+h*h;
//右下
for(int i=0; i<4; i++){
if(Arrays.asList(irekae).contains(i)){
count=count+1;
}
else{
if(((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
irekae[2]=i;
kyori=((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
}
}
}
kyori=w*w+h*h;
//左下
for(int i=0; i<4; i++){
if(Arrays.asList(irekae).contains(i)){
count=count+1;
}
else{
if(((posi_shaei[i][1])*(posi_shaei[i][1])
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
irekae[3]=i;
kyori=((posi_shaei[i][1])*(posi_shaei[i][1])
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
}
}
}
//射影変換処理
List<Point> srcPoints_t=new ArrayList();
for (int i=0;i<4;i++){
posi_shaei1[i][0]=i;
posi_shaei1[i][1]=posi_shaei[irekae[i]][1];
posi_shaei1[i][2]=posi_shaei[irekae[i]][2];
srcPoints_t.add(new Point(posi_shaei1[i][1],posi_shaei1[i][2]));
}
Mat srcPoints= Converters.vector_Point_to_Mat(srcPoints_t, CvType.CV_32F);
List<Point> dstpoints_t=new ArrayList();
dstpoints_t.add(new Point(0,0)); //0 左上
dstpoints_t.add(new Point(w,0)); //1 右上
dstpoints_t.add(new Point(w,h)); //2 右下
dstpoints_t.add(new Point(0,h)); //3 左下
Mat dstpoints=Converters.vector_Point_to_Mat(dstpoints_t, CvType.CV_32F);
Mat perspectiveMmat= Imgproc.getPerspectiveTransform(srcPoints,dstpoints);
Imgproc.warpPerspective(
mat,mat,perspectiveMmat,mat.size(),Imgproc.INTER_LINEAR);
Utils.matToBitmap(mat, bmpmodori);
return bmpmodori;
}
せっかくなのでmain activityの全容
参考になればと。処理はコードから察してください。。
画像はアプリ固有フォルダに格納される。
public class MainActivity extends AppCompatActivity {
private static final int RESULT_PICK_IMAGEFILE = 1000;
private float pointX;
private float pointY;
private Myview myview;
private Bitmap bmp;
private int itemid_button=-1;
private Button Button1;
private Button Button2;
private Button Button3;
private int seekvalue=50;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//自作viewとレイアウトの紐づけ
myview=this.findViewById(R.id.Myview01);
//opencv おまじない
OpenCVLoader.initDebug();
//ボタン設定
Button1=findViewById(R.id.button1);
Button2=findViewById(R.id.button2);
Button3=findViewById(R.id.button3);
//写真読み込み
Button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, RESULT_PICK_IMAGEFILE);
}
});
Button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(itemid_button==R.id.no1){
bmp=shaei(bmp);
myview.showCanvas(bmp, 0);
}
if(itemid_button==R.id.no2){
LayoutInflater inflater=(LayoutInflater)MainActivity.this.
getSystemService(LAYOUT_INFLATER_SERVICE) ;
final View layout=inflater.inflate(
R.layout.dialog_contents_item,
(ViewGroup)findViewById(R.id.dialogcustom)
);
//seekbar設定 (view)layoutからfindviewする!!
SeekBar seek = (SeekBar) layout.findViewById(R.id.scale_value);
seek.setProgress(seekvalue); //バーの値に応じた初期位置に
seek.setOnSeekBarChangeListener(new SeekBarChangeListener());
AlertDialog.Builder builder=new AlertDialog.Builder(
MainActivity.this);
builder.setTitle("settting threshold");
builder.setView(layout);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialog, int which){
Toast.makeText(MainActivity.this,String.valueOf(seekvalue),Toast.LENGTH_SHORT).show();
bmp=background(bmp, seekvalue);
myview.showCanvas(bmp, 0);
}
});
builder.setNegativeButton("cancel", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialog, int which){
}
});
Log.i("tag0","tag0");
builder.create().show();
}
if(itemid_button==R.id.no3){
bmp=openingshori(bmp);
myview.showCanvas(bmp, 0);
}
}
});
Button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if((itemid_button==R.id.no1)||
(itemid_button==R.id.no2)||
(itemid_button==R.id.no3)){
//画像の保存///////////////////////
Context context = getApplicationContext();
File path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File fname=new File( path,
new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss",
java.util.Locale.JAPAN).format(new Date())
+ ".jpg");
ByteArrayOutputStream baos=new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] bmptojpg=baos.toByteArray();
try{
FileOutputStream fos=new FileOutputStream(fname, true);
fos.write(bmptojpg);
fos.close();
Toast.makeText(MainActivity.this,"saved",Toast.LENGTH_SHORT).show();
}catch (Exception e){
Toast.makeText(MainActivity.this,"error...",Toast.LENGTH_SHORT).show();
}
}
}
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == RESULT_PICK_IMAGEFILE && resultCode == RESULT_OK) {
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
try {
bmp = getBitmapFromUri(uri);
myview.showCanvas(bmp, 0);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater=getMenuInflater();
inflater.inflate(R.menu.menu_options,menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (bmp != null) {
int itemId = item.getItemId();
itemid_button=itemId;
switch (itemId) {
case R.id.no1:
Toast.makeText(this, "please put red dot to 4 edge.", Toast.LENGTH_SHORT).show();
myview.showCanvas(bmp, 1);
Button2.setText("finish!!");
Button3.setText("save!!");
break;
case R.id.no2:
Toast.makeText(this, "clean background", Toast.LENGTH_SHORT).show();
myview.showCanvas(bmp, 2);
Button2.setText("clean");
Button3.setText("save!!");
break;
case R.id.no3:
Toast.makeText(this, "thin line del.", Toast.LENGTH_SHORT).show();
Button2.setText("line del.");
Button3.setText("save!!");
break;
}
}
else{
Toast.makeText(this, "please set image", Toast.LENGTH_SHORT).show();
}
return super.onOptionsItemSelected(item);
}
private Bitmap shaei(Bitmap bmphiki){
Mat mat=new Mat();
Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ
int [][]posi_shaei=myview.getposition();
int [][]posi_shaei1=new int[4][3];
int w=bmphiki.getWidth();
int h=bmphiki.getHeight();
Bitmap bmpmodori=Bitmap.createBitmap(
bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);
int kyori;
Integer[] irekae={-1,-1,-1,-1};
int count=0;
kyori=w*w+h*h;
//左上
for(int i=0; i<4; i++){
if((posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2])<kyori){
irekae[0]=i;
kyori=(posi_shaei[i][1]*posi_shaei[i][1]+posi_shaei[i][2]*posi_shaei[i][2]);
}
}
kyori=w*w+h*h;
//右上
for(int i=0; i<4; i++){
if(Arrays.asList(irekae).contains(i)){
count=count+1;
}
else{
if(((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
+(posi_shaei[i][2])*(posi_shaei[i][2]))<kyori){
irekae[1]=i;
kyori=((w-posi_shaei[i][1])*(w-posi_shaei[i][1])
+(posi_shaei[i][2])*(posi_shaei[i][2]));
}
}
}
kyori=w*w+h*h;
//右下
for(int i=0; i<4; i++){
if(Arrays.asList(irekae).contains(i)){
count=count+1;
}
else{
if(((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
irekae[2]=i;
kyori=((posi_shaei[i][1]-w)*(posi_shaei[i][1]-w)
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
}
}
}
kyori=w*w+h*h;
//左下
for(int i=0; i<4; i++){
if(Arrays.asList(irekae).contains(i)){
count=count+1;
}
else{
if(((posi_shaei[i][1])*(posi_shaei[i][1])
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h))<kyori){
irekae[3]=i;
kyori=((posi_shaei[i][1])*(posi_shaei[i][1])
+(posi_shaei[i][2]-h)*(posi_shaei[i][2]-h));
}
}
}
//射影変換処理
List<Point> srcPoints_t=new ArrayList();
for (int i=0;i<4;i++){
posi_shaei1[i][0]=i;
posi_shaei1[i][1]=posi_shaei[irekae[i]][1];
posi_shaei1[i][2]=posi_shaei[irekae[i]][2];
srcPoints_t.add(new Point(posi_shaei1[i][1],posi_shaei1[i][2]));
}
Mat srcPoints= Converters.vector_Point_to_Mat(srcPoints_t, CvType.CV_32F);
List<Point> dstpoints_t=new ArrayList();
dstpoints_t.add(new Point(0,0)); //0 左上
dstpoints_t.add(new Point(w,0)); //1 右上
dstpoints_t.add(new Point(w,h)); //2 右下
dstpoints_t.add(new Point(0,h)); //3 左下
Mat dstpoints=Converters.vector_Point_to_Mat(dstpoints_t, CvType.CV_32F);
Mat perspectiveMmat= Imgproc.getPerspectiveTransform(srcPoints,dstpoints);
Imgproc.warpPerspective(
mat,mat,perspectiveMmat,mat.size(),Imgproc.INTER_LINEAR);
Utils.matToBitmap(mat, bmpmodori);
return bmpmodori;
}
private Bitmap background(Bitmap bmphiki, int seekvalue){
Mat mat=new Mat();
Mat mask=new Mat();
Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ
//2値化
Imgproc.cvtColor(mat, mat, COLOR_RGB2GRAY); // まずグレースケールへ(明るさだけの形式)
Imgproc.threshold(mat, mask, seekvalue, 255.0, Imgproc.THRESH_BINARY);
//Core.bitwise_not(mask,mask);
Log.i("tag2","tag2");
//背景白との足し算
Core.add(mat,mask,mat);
Bitmap bmpmodori=Bitmap.createBitmap(
bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(mat, bmpmodori);
return bmpmodori;
}
private Bitmap openingshori(Bitmap bmphiki){
Mat mat=new Mat();
Utils.bitmapToMat(bmphiki, mat); // OpenCVの行列へ
Bitmap bmpmodori=Bitmap.createBitmap(
bmphiki.getWidth(), bmphiki.getHeight(), Bitmap.Config.ARGB_8888);
//opening
Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_DILATE,
new Mat(), new Point(-1, -1), 2);
//closing
//Imgproc.morphologyEx(mat, mat, Imgproc.MORPH_CLOSE,
// new Mat(3, 3, CvType.CV_8UC1), new Point(-1, -1), 2);
Utils.matToBitmap(mat, bmpmodori);
return bmpmodori;
}
class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
//つまみをドラッグしたとき
seekvalue=seekBar.getProgress();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
//つまみに触れたとき
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//つまみを離したとき
}
}
}
まとめ
他に色々苦労したのがあったが、それは他のページによくまとまったのがあったので、ここでは割愛。opencvの関数を使うにあたり以下の書物が参考になった。近々、アプリ開発のための参考書をまとめたい。
以下の書物でopencv3とopencvがあり、改定前後になっているが、前verでは射影変換がなかったりする。自分は先に3を買い、中古で安くなった初期本を買い、自炊したときにこの違いに気づいた。前verはラクマにて出品中ですので、興味あるかたはご購入ください。先着1名様。
改定前にないもの
・画面上に文字記載
・透視投影(射影変換)
・トリミング
・モザイク処理
・コーナー検出
・顔検出
・オブジェクト除去
・ダメージ補修
・テンプレートマッチング
・特異点検出
Javaで始めるOpenCV3プログラミング 注)ver違いあり
リンク

