diff --git a/.gitignore b/.gitignore index ccf2efe..30dc95b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ proguard/ # Log Files *.log +*.iml diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..03ed9c3 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "master.sudoku" + minSdkVersion 21 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:23.3.0' + compile 'com.android.support:support-v4:23.3.0' + compile files('libs/opencv library - 2.4.5.jar') +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e571220 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/master/sudoku/activities/LoadPuzzleActivity.java b/app/src/main/java/master/sudoku/activities/LoadPuzzleActivity.java new file mode 100644 index 0000000..e86e036 --- /dev/null +++ b/app/src/main/java/master/sudoku/activities/LoadPuzzleActivity.java @@ -0,0 +1,107 @@ +package master.sudoku.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.Menu; +import android.view.MenuItem; + +import master.sudoku.R; +import master.sudoku.adapters.LoadPuzzlePagerAdapter; +import master.sudoku.fragments.LoadPuzzleFragment; +import master.sudoku.fragments.MainGameFragment; +import master.sudoku.model.Sudoku; + +public class LoadPuzzleActivity extends AppCompatActivity implements LoadPuzzleFragment.Callback { + + /** + * The {@link android.support.v4.view.PagerAdapter} that will provide + * fragments for each of the sections. We use a {@link FragmentPagerAdapter} + * derivative, which will keep every loaded fragment in memory. If this + * becomes too memory intensive, it may be best to switch to a + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + */ + LoadPuzzlePagerAdapter mSectionsPagerAdapter; + + /** + * The {@link ViewPager} that will host the section contents. + */ + ViewPager mViewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_load_puzzle); + + // Set up the action bar. + final ActionBar actionBar = getSupportActionBar(); +// actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + // Create the adapter that will return a fragment for each of the three + // primary sections of the activity. + mSectionsPagerAdapter = new LoadPuzzlePagerAdapter( + getSupportFragmentManager(), this); + + // Set up the ViewPager with the sections adapter. + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(mSectionsPagerAdapter); + + // When swiping between different sections, select the corresponding + // tab. We can also use ActionBar.Tab#select() to do this if we have + // a reference to the Tab. +// mViewPager +// .setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { +// @Override +// public void onPageSelected(int position) { +// actionBar.setSelectedNavigationItem(position); +// } +// }); + + // For each of the sections in the app, add a tab to the action bar. +// for (int i = 0; i < mSectionsPagerAdapter.getCount(); i++) { +// // Create a tab with text corresponding to the page title defined by +// // the adapter. Also specify this Activity object, which implements +// // the TabListener interface, as the callback (listener) for when +// // this tab is selected. +// actionBar.addTab(actionBar.newTab() +// .setText(mSectionsPagerAdapter.getPageTitle(i)) +// .setTabListener(this)); +// } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.load_puzzle, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + if (id == R.id.action_edit) { + editPuzzle(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void loadPuzzleDone(Sudoku model) { + MainGameFragment fragment = (MainGameFragment)mSectionsPagerAdapter.getItem(2); + fragment.setModel(model); + mViewPager.setCurrentItem(2); + } + + private void editPuzzle() { + MainGameFragment fragment = (MainGameFragment)mSectionsPagerAdapter.getItem(2); + fragment.editModel(); + mViewPager.setCurrentItem(2); + } +} diff --git a/app/src/main/java/master/sudoku/activities/MainActivity.java b/app/src/main/java/master/sudoku/activities/MainActivity.java new file mode 100644 index 0000000..3b45faf --- /dev/null +++ b/app/src/main/java/master/sudoku/activities/MainActivity.java @@ -0,0 +1,60 @@ +package master.sudoku.activities; + +import android.content.Intent; +import android.graphics.Point; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.Display; +import android.view.Menu; +import android.view.MenuItem; + +import master.sudoku.R; +import master.sudoku.config.DeviceConfig; +import master.sudoku.fragments.MainGameFragment; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Display display = this.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + DeviceConfig.mWidth = size.x; + DeviceConfig.mHeight = size.y; + DeviceConfig.mFontSize = 48; + + setContentView(R.layout.activity_main); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.container, new MainGameFragment()).commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + if (id == R.id.action_settings) { + return true; + } else if (id == R.id.load_puzzle) { + final Intent intent = new Intent(this.getBaseContext(), LoadPuzzleActivity.class); + this.startActivity(intent); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/app/src/main/java/master/sudoku/adapters/LoadPuzzlePagerAdapter.java b/app/src/main/java/master/sudoku/adapters/LoadPuzzlePagerAdapter.java new file mode 100644 index 0000000..94dcc80 --- /dev/null +++ b/app/src/main/java/master/sudoku/adapters/LoadPuzzlePagerAdapter.java @@ -0,0 +1,63 @@ +package master.sudoku.adapters; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import master.sudoku.activities.LoadPuzzleActivity; +import master.sudoku.fragments.CapturePuzzleFragment; +import master.sudoku.fragments.LoadPuzzleFragment; +import master.sudoku.fragments.MainGameFragment; +import master.sudoku.views.MainGameView; + +/** + * Created by zhangle on 03/01/2017. + */ +public class LoadPuzzlePagerAdapter extends FragmentPagerAdapter { + + private static final int sCount = 3; + private LoadPuzzleActivity mActivity; + private Fragment[] mFragments; + + public LoadPuzzlePagerAdapter(FragmentManager fm, LoadPuzzleActivity activity) { + super(fm); + if (mFragments == null) { + mFragments = new Fragment[sCount]; + } + mActivity = activity; + } + + @Override + public Fragment getItem(int position) { + if (mFragments[position] != null) { + return mFragments[position]; + } + Fragment f = null; + switch (position) { + case 0: + f = new CapturePuzzleFragment(); + break; + case 1: + f = new LoadPuzzleFragment(); + ((LoadPuzzleFragment)f).setCallback(mActivity); + break; + case 2: + f = new MainGameFragment(); + Bundle bundle = new Bundle(); + bundle.putInt("style", MainGameView.STYLE_LOAD); + f.setArguments(bundle); + break; + } + if (f != null) { + mFragments[position] = f; + } + return f; + } + + @Override + public int getCount() { + // Show 3 total pages. + return sCount; + } +} \ No newline at end of file diff --git a/app/src/main/java/master/sudoku/application/PuzzleMasterApp.java b/app/src/main/java/master/sudoku/application/PuzzleMasterApp.java new file mode 100644 index 0000000..3cfe79f --- /dev/null +++ b/app/src/main/java/master/sudoku/application/PuzzleMasterApp.java @@ -0,0 +1,117 @@ +package master.sudoku.application; + +import android.app.Application; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import master.sudoku.utils.AppUtil; + +/** + * Created by zhangle on 03/01/2017. + */ +public class PuzzleMasterApp extends Application { + private static PuzzleMasterApp sInstance; + + + private int mVersionCode; + private String mVersionName; + private String mDeviceId; + private Application mApp; + + private SharedPreferences mPreferences; + + public static PuzzleMasterApp getInstance() { + return sInstance; + } + + @Override + public void onCreate() { + super.onCreate(); + sInstance = this; + } + + public void setApplication(Application app) { + this.mApp = app; + } + + public Application getApplication() { + return mApp; + } + + public SharedPreferences getPreferences() { + if (mPreferences == null) { + mPreferences = getSharedPreferences(getPreferencesName(), MODE_PRIVATE); + } + return mPreferences; + } + + /** + * @return app shared preference name. + */ + protected String getPreferencesName() { + return mApp.getPackageName(); + } + + /** + * @return the application's version name set in the manifest. + */ + public String getVersionName() { + checkVersionInfo(); + return mVersionName; + } + + /** + * @return The unique device id. + */ + public String getDeviceId() { + if (mDeviceId == null) { + mDeviceId = generateDeviceId(); + } + return mDeviceId; + } + + /** + * @return true if the device is tablet, otherwise false. + */ + public boolean isTablet() { + final TelephonyManager tm = (TelephonyManager) mApp.getSystemService(Application.TELEPHONY_SERVICE); + return tm.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE; + } + + private void checkVersionInfo() { + if (mVersionName == null || mVersionCode == 0) { + try { + final PackageInfo packageInfo = mApp.getPackageManager() + .getPackageInfo(mApp.getApplicationInfo().packageName, 0); + if (packageInfo != null) { + mVersionName = packageInfo.versionName; + mVersionCode = packageInfo.versionCode; + } + } catch (Exception ignored) { + } + } + } + + private String generateDeviceId() { + final SharedPreferences sp = getPreferences(); + String result = sp.getString("device_id", null); + if (!TextUtils.isEmpty(result)) { + return result; + } + + if (TextUtils.isEmpty(result)) { + final TelephonyManager tm = (TelephonyManager) mApp.getSystemService(Application.TELEPHONY_SERVICE); + result = tm.getDeviceId(); + if (TextUtils.isEmpty(result)) { + result = tm.getSimSerialNumber(); + } + } + + result = AppUtil.getMd5(result); + sp.edit().putString("device_id", result).apply(); + + return result; + } +} diff --git a/app/src/main/java/master/sudoku/camera/CameraCallback.java b/app/src/main/java/master/sudoku/camera/CameraCallback.java new file mode 100644 index 0000000..b5fa51f --- /dev/null +++ b/app/src/main/java/master/sudoku/camera/CameraCallback.java @@ -0,0 +1,139 @@ +package master.sudoku.camera; + +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.view.Surface; +import android.view.SurfaceHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by zhangle on 11/01/2017. + */ +public class CameraCallback extends CameraDevice.StateCallback implements SurfaceHolder.Callback { + public static final String TAG = "CameraCallback"; + private final Context mContext; + + private android.hardware.camera2.CameraDevice mCamera; + private CaptureRequest.Builder mCptureRequestBuilder; + private CameraCaptureSession mCameraCaptureSession; + + private SurfaceHolder mHolder; + + private CameraCaptureSession.StateCallback mSessionCallback = new CameraCaptureSession.StateCallback() { + + @Override + public void onConfigured(CameraCaptureSession session) { + mCameraCaptureSession = session; + updatePreview(); + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + mCameraCaptureSession = null; + } + }; + + public CameraCallback(Context context) { + mContext = context; + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + try { + if (mCamera == null) { + mHolder = holder; + return; + } + List surfaceList = new ArrayList(); + surfaceList.add(holder.getSurface()); + mCptureRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + mCptureRequestBuilder.addTarget(holder.getSurface()); + mCamera.createCaptureSession(surfaceList, mSessionCallback, null); + } catch (Exception ex) { + if (mCamera != null) { + mCamera = null; + } + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + if (mCamera == null) { + return; + } +// mParameters = mCamera.getParameters(); +// mParameters.setPictureFormat(ImageFormat.JPEG); +// setPictureSize(mParameters); +// Point previewSizePt; +// if (mFoundCommonSize) { +// previewSizePt = new Point(mParameters.getPictureSize().width, +// mParameters.getPictureSize().height); +// } else { +// previewSizePt = getPreviewSize(mParameters, +// mParameters.getPictureSize()); +// } +// +// if (previewSizePt.x * previewSizePt.y < SharedConstants.MIN_PICTURE_SIZE) { +// Camera.Size size = getBestFitSize(mParameters.getSupportedPreviewSizes()); +// mParameters.setPreviewSize(size.width, size.height); +// } else { +// mParameters.setPreviewSize(previewSizePt.x, previewSizePt.y); +// } +// if (mContext.getPackageManager().hasSystemFeature( +// PackageManager.FEATURE_CAMERA_AUTOFOCUS)) { +// mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO); +// } +// mCamera.setParameters(mParameters); +// mCamera.startPreview(); + } + + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (mCamera != null) { + mCamera = null; + } + } + + public void setFlashLight(boolean isOn) { + if (mCamera == null) { + return; + } + } + + public void updatePreview() { + if(null == mCamera || null == mCameraCaptureSession) { + return; + } + mCptureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + try { + mCameraCaptureSession.setRepeatingRequest(mCptureRequestBuilder.build(), null, null); + } catch (CameraAccessException e) { + e.printStackTrace(); + } + } + @Override + public void onOpened(CameraDevice camera) { + this.mCamera = camera; + if (mHolder != null) { + surfaceCreated(mHolder); + } + } + + @Override + public void onDisconnected(CameraDevice camera) { + this.mCamera = null; + } + + @Override + public void onError(CameraDevice camera, int error) { + this.mCamera = null; + } +} diff --git a/app/src/main/java/master/sudoku/config/DeviceConfig.java b/app/src/main/java/master/sudoku/config/DeviceConfig.java new file mode 100644 index 0000000..3b0c088 --- /dev/null +++ b/app/src/main/java/master/sudoku/config/DeviceConfig.java @@ -0,0 +1,14 @@ +package master.sudoku.config; + +public class DeviceConfig { + public static int mHeight; + public static int mWidth; + public static float mFontSize; + /** + * 0: no error hint + * 1: hint once + * 2: hint kept all through the game + * 3: block game if error found + */ + public static int mErrorHintLevel = 1; +} diff --git a/app/src/main/java/master/sudoku/event/EventArgs.java b/app/src/main/java/master/sudoku/event/EventArgs.java new file mode 100644 index 0000000..54f9fef --- /dev/null +++ b/app/src/main/java/master/sudoku/event/EventArgs.java @@ -0,0 +1,35 @@ +/** + * + */ +package master.sudoku.event; + +/** + * @author dannyzha + * + */ +public class EventArgs { + + public static final int INPUT_PANEL_SELECT = 0; + + private int mEventType; + + private Object mEventData; + + /** + * Constructor + * @param eventType + * @param eventData + */ + public EventArgs(int eventType, Object eventData) { + this.mEventType = eventType; + this.mEventData = eventData; + } + + public int getEventType() { + return mEventType; + } + + public Object getEventData() { + return mEventData; + } +} diff --git a/app/src/main/java/master/sudoku/event/EventListener.java b/app/src/main/java/master/sudoku/event/EventListener.java new file mode 100644 index 0000000..3f7003e --- /dev/null +++ b/app/src/main/java/master/sudoku/event/EventListener.java @@ -0,0 +1,17 @@ +/** + * + */ +package master.sudoku.event; + +/** + * @author dannyzha + * + */ +public interface EventListener { + /** + * handle event generated from an event source + * @param args the event arguments + * @return whether the event is handled properly + */ + boolean handleEvent(EventArgs args); +} diff --git a/app/src/main/java/master/sudoku/event/EventSource.java b/app/src/main/java/master/sudoku/event/EventSource.java new file mode 100644 index 0000000..f8bdee9 --- /dev/null +++ b/app/src/main/java/master/sudoku/event/EventSource.java @@ -0,0 +1,38 @@ +/** + * + */ +package master.sudoku.event; + +import java.util.Vector; + +/** + * @author dannyzha + * + */ +public abstract class EventSource { + + protected Vector mListeners = new Vector(); + + public void addEventListener(EventListener listener) { + if(!mListeners.contains(listener)) { + mListeners.addElement(listener); + } + } + + public void removeEventListener(EventListener listener) { + if(mListeners.contains(listener)) { + mListeners.removeElement(listener); + } + } + + protected void triggerEvent(int eventType, Object eventData) { + triggerEvent(new EventArgs(eventType, eventData)); + } + + protected void triggerEvent(EventArgs args) { + for(int i=0; i 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + openCamera(); + } else { + + // permission denied, boo! Disable the + // functionality that depends on this permission. + } + return; + } + } + } + + private void openCamera() { + if (!checkPermission(Manifest.permission.CAMERA, REQUEST_CAMERA_OPEN)) { + return; + } + CameraManager mgr = (CameraManager) this.getContext().getSystemService(Context.CAMERA_SERVICE); + try { + String[] idList = mgr.getCameraIdList(); + mgr.openCamera(idList[0], mCameraCallback, null); + mSurfaceHolder.addCallback(mCameraCallback); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private boolean checkPermission(String permission, int requestCode) { + if (ContextCompat.checkSelfPermission(this.getActivity(), permission) + != PackageManager.PERMISSION_GRANTED) { + + // Should we show an explanation? + if (shouldShowRequestPermissionRationale(permission)) { + // Explain to the user why we need to read the contacts + } + + requestPermissions(new String[]{permission}, requestCode); + return false; + } + return true; + } + + +} diff --git a/app/src/main/java/master/sudoku/fragments/LoadPuzzleFragment.java b/app/src/main/java/master/sudoku/fragments/LoadPuzzleFragment.java new file mode 100644 index 0000000..433a138 --- /dev/null +++ b/app/src/main/java/master/sudoku/fragments/LoadPuzzleFragment.java @@ -0,0 +1,320 @@ +package master.sudoku.fragments; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; + +import java.io.File; +import java.io.InputStream; + +import master.sudoku.R; +import master.sudoku.model.Sudoku; +import master.sudoku.ocr.ImageCutter; +import master.sudoku.ocr.RecognizerNN; +import master.sudoku.utils.FileUtils; + +/** + * Created by zhangle on 03/01/2017. + */ +public class LoadPuzzleFragment extends Fragment { + + public static final int PICK_IMAGE = 1; + public static final int REQUEST_EXTERNAL_STORAGE_READ = 2; + public static final int REQUEST_EXTERNAL_STORAGE_WRITE = 3; + private ImageView mImgView = null; + private Button mLoadBtn = null; + private Bitmap mImageBitmap = null; + private Callback mCallback = null; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_load_puzzle, container, + false); + mImgView = (ImageView)rootView.findViewById(R.id.load_puzzle_image); + if (mImageBitmap != null) { + mImgView.setImageBitmap(mImageBitmap); + } + mImgView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + selectImage(); + } + }); + mLoadBtn = (Button)rootView.findViewById(R.id.load_puzzle_button); + mLoadBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + loadPuzzle(); + } + }); + return rootView; + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case REQUEST_EXTERNAL_STORAGE_READ: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + selectImage(); + } else { + + // permission denied, boo! Disable the + // functionality that depends on this permission. + } + return; + } + case REQUEST_EXTERNAL_STORAGE_WRITE: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadPuzzle(); + } else { + + // permission denied, boo! Disable the + // functionality that depends on this permission. + } + return; + } + // other 'case' lines to check for other + // permissions this app might request + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMAGE && resultCode == Activity.RESULT_OK) { + if (data == null) { + //Display an error + return; + } +// InputStream inputStream = context.getContentResolver().openInputStream(data.getData()); + //Now you can do whatever you want with your inpustream, save it as file, upload to a server, decode a bitmap... + + Uri uri = data.getData(); + if (uri != null) { + final String path = FileUtils.getFilePathFromUri(uri, this.getActivity()); + final File file = new File(path); + if (!file.exists()) { + return; + } + mImageBitmap = decodeBitmapFromFile(path); + mImgView.setImageBitmap(mImageBitmap); + } else { +// mImageBitmap = decodeBitmapFromByte(data); + } + } + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + private boolean checkPermission(String permission, int requestCode) { + if (ContextCompat.checkSelfPermission(this.getActivity(), permission) + != PackageManager.PERMISSION_GRANTED) { + + // Should we show an explanation? + if (shouldShowRequestPermissionRationale(permission)) { + // Explain to the user why we need to read the contacts + } + + requestPermissions(new String[]{permission}, requestCode); + return false; + } + return true; + } + + private void selectImage() { + if (!checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_STORAGE_READ)) { + return; + } + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, "Select Picture"), PICK_IMAGE); + } + + private void loadPuzzle() { + try { + if (!checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_EXTERNAL_STORAGE_WRITE)) { + return; + } + ImageCutter ic = new ImageCutter(mImageBitmap); + ic.saveImages(); + Sudoku model = parseModel(ic); + if (mCallback != null) { + mCallback.loadPuzzleDone(model); + } + } catch(Exception ex) { + ex.printStackTrace(); + } + } + + private Sudoku parseModel(ImageCutter ic) { + Sudoku model = new Sudoku(); + Resources res = getResources(); + InputStream is = res.openRawResource(R.raw.nn_weights_printed); + RecognizerNN rec = new RecognizerNN(is); + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + Bitmap img = ic.getImage(i, j); +// Recognizer rec = new Recognizer(img); + int num = rec.determine(img); + model.setInitValue(i, j, num); + } + } + return model; + } + + Bitmap decodeBitmapFromFile(String filename) { + final int maxWidth = getResources().getDisplayMetrics().widthPixels; + final int maxHeight = getResources().getDisplayMetrics().heightPixels; + + final BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); + decodeOptions.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, decodeOptions); + final int actualWidth = decodeOptions.outWidth; + final int actualHeight = decodeOptions.outHeight; + + // Then compute the dimensions we would ideally like to decode to. + final int desiredWidth = getResizedDimension(maxWidth, maxHeight, actualWidth, actualHeight); + final int desiredHeight = getResizedDimension(maxHeight, maxWidth, actualHeight, actualWidth); + + // Decode to the nearest power of two scaling factor. + decodeOptions.inJustDecodeBounds = false; + decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); + + Bitmap bitmap; + final Bitmap tempBitmap = BitmapFactory.decodeFile(filename, decodeOptions); + // If necessary, scale down to the maximal acceptable size. + if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || + tempBitmap.getHeight() > desiredHeight)) { + bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); + tempBitmap.recycle(); + + } else { + bitmap = tempBitmap; + } + +// mSampleSize = decodeOptions.inSampleSize; + if (!bitmap.isMutable()) { + Bitmap.Config config = Bitmap.Config.ARGB_8888; + bitmap = bitmap.copy(config , true); + } + return bitmap; + } + + Bitmap decodeBitmapFromByte(byte[] data) { + final int maxWidth = getResources().getDisplayMetrics().widthPixels; + final int maxHeight = getResources().getDisplayMetrics().heightPixels; + + final BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); + decodeOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + final int actualWidth = decodeOptions.outWidth; + final int actualHeight = decodeOptions.outHeight; + + // Then compute the dimensions we would ideally like to decode to. + final int desiredWidth = getResizedDimension(maxWidth, maxHeight, actualWidth, actualHeight); + final int desiredHeight = getResizedDimension(maxHeight, maxWidth, actualHeight, actualWidth); + + // Decode to the nearest power of two scaling factor. + decodeOptions.inJustDecodeBounds = false; + decodeOptions.inSampleSize = findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); + + final Bitmap bitmap; + final Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + // If necessary, scale down to the maximal acceptable size. + if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || + tempBitmap.getHeight() > desiredHeight)) { + bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true); + tempBitmap.recycle(); + + } else { + bitmap = tempBitmap; + } + +// mSampleSize = decodeOptions.inSampleSize; + return bitmap; + } + + /** + * Scales one side of a rectangle to fit aspect ratio. + * + * @param maxPrimary Maximum size of the primary dimension (i.e. mWidth for + * max mWidth), or zero to maintain aspect ratio with secondary + * dimension + * @param maxSecondary Maximum size of the secondary dimension, or zero to + * maintain aspect ratio with primary dimension + * @param actualPrimary Actual size of the primary dimension + * @param actualSecondary Actual size of the secondary dimension + */ + private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, + int actualSecondary) { + // If no dominant value at all, just return the actual. + if (maxPrimary == 0 && maxSecondary == 0) { + return actualPrimary; + } + + // If primary is unspecified, scale primary to match secondary scaling ratio. + if (maxPrimary == 0) { + double ratio = (double) maxSecondary / (double) actualSecondary; + return (int) (actualPrimary * ratio); + } + + if (maxSecondary == 0) { + return maxPrimary; + } + + double ratio = (double) actualSecondary / (double) actualPrimary; + int resized = maxPrimary; + if (resized * ratio > maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + /** + * Returns the largest power-of-two divisor for use in downscaling a bitmap + * that will not result in the scaling past the desired dimensions. + * + * @param actualWidth Actual mWidth of the bitmap + * @param actualHeight Actual mHeight of the bitmap + * @param desiredWidth Desired mWidth of the bitmap + * @param desiredHeight Desired mHeight of the bitmap + */ + private static int findBestSampleSize(int actualWidth, int actualHeight, + int desiredWidth, int desiredHeight) { + final double wr = (double) actualWidth / desiredWidth; + final double hr = (double) actualHeight / desiredHeight; + final double ratio = Math.min(wr, hr); + float n = 1.0f; + while ((n * 2) <= ratio) { + n *= 2; + } + + return (int) n; + } + + public interface Callback { + void loadPuzzleDone(Sudoku model); + } +} diff --git a/app/src/main/java/master/sudoku/fragments/MainGameFragment.java b/app/src/main/java/master/sudoku/fragments/MainGameFragment.java new file mode 100644 index 0000000..f97e25f --- /dev/null +++ b/app/src/main/java/master/sudoku/fragments/MainGameFragment.java @@ -0,0 +1,61 @@ +package master.sudoku.fragments; + +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import master.sudoku.R; +import master.sudoku.config.DeviceConfig; +import master.sudoku.model.Sudoku; +import master.sudoku.model.SudokuGame; +import master.sudoku.views.MainGameView; +import master.sudoku.views.SolveSudokuView; + +/** + * Created by zhangle on 03/01/2017. + */ +public class MainGameFragment extends Fragment { + + private int mStyle = MainGameView.STYLE_PLAY; + + private SolveSudokuView mGameView; + + public MainGameFragment() { + this.mStyle = MainGameView.STYLE_PLAY; + } + + public void setArguments(Bundle args) { + super.setArguments(args); + mStyle = args.getInt("style"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_sudoku_main, container, + false); + + mGameView = (SolveSudokuView) rootView.findViewById(R.id.solve_sudoku_view); + SudokuGame game = new SudokuGame(); + mGameView.setBound(new Rect(0,0, DeviceConfig.mWidth - 100, DeviceConfig.mHeight - 500)); + if (mStyle == MainGameView.STYLE_PLAY) { + mGameView.setModel(game.getModel1()); + } else if (mStyle == MainGameView.STYLE_LOAD) { + mGameView.setModel(new Sudoku()); + } + mGameView.setStyle(mStyle); + return rootView; + } + + public void setModel(Sudoku model) { + mGameView.setModel(model); + mGameView.invalidate(); + } + + public void editModel() { + mGameView.editModel(true); + } +} diff --git a/app/src/main/java/master/sudoku/logs/Logger.java b/app/src/main/java/master/sudoku/logs/Logger.java new file mode 100644 index 0000000..c14be69 --- /dev/null +++ b/app/src/main/java/master/sudoku/logs/Logger.java @@ -0,0 +1,26 @@ +/** + * + */ +package master.sudoku.logs; + +/** + * @author dannyzha + * + */ +public final class Logger { + + public static boolean ON = true; + + private static Logger sInstance; + + public static Logger getLogger() { + if(sInstance == null) { + sInstance = new Logger(); + } + return sInstance; + } + + public void debug(String log) { + System.out.println(log); + } +} diff --git a/app/src/main/java/master/sudoku/model/Index.java b/app/src/main/java/master/sudoku/model/Index.java new file mode 100644 index 0000000..50af653 --- /dev/null +++ b/app/src/main/java/master/sudoku/model/Index.java @@ -0,0 +1,64 @@ +/** + * + */ +package master.sudoku.model; + +/** + * @author dannyzha + * + */ +public class Index { + public static final int INVALID_INDEX = -1; + + private int i = INVALID_INDEX; + private int j = INVALID_INDEX; + + /** + * Constructor + * @param i + * @param j + */ + public Index(int i, int j) { + this.i = i; + this.j = j; + } + + public boolean equals(int i, int j) { + return this.i == i && this.j == j; + } + + public boolean equals(Object o) { + if(!(o instanceof Index)) { + return false; + } + Index other = (Index)o; + return other.equals(this.i, this.j); + } + + public int hashCode() { + int tmpI = i; + int tmpJ = j; + while(tmpJ > 0) { + tmpI *= 10; + tmpJ /= 10; + } + return tmpI + j; + } + + public int getI() { + return i; + } + + public void setI(int i) { + this.i = i; + } + + public int getJ() { + return j; + } + + public void setJ(int j) { + this.j = j; + } + +} diff --git a/app/src/main/java/master/sudoku/model/Sudoku.java b/app/src/main/java/master/sudoku/model/Sudoku.java new file mode 100644 index 0000000..9654a81 --- /dev/null +++ b/app/src/main/java/master/sudoku/model/Sudoku.java @@ -0,0 +1,186 @@ +/** + * + */ +package master.sudoku.model; + +import java.util.Vector; + +import master.sudoku.exception.SolutionException; + +/** + * @author dannyzha + * + */ +public class Sudoku { + public static final int SUDOKU_SIZE = 9; + + private static final int BLANK_VALUE = 0; + // matrix to store the values + private int[][] mInitMatrix; + private int[][] mResultMatrix; + + /** + * + */ + public Sudoku() { + mInitMatrix = new int[SUDOKU_SIZE][SUDOKU_SIZE]; + mResultMatrix = new int[SUDOKU_SIZE][SUDOKU_SIZE]; + initMatrix(mInitMatrix); + initMatrix(mResultMatrix); + } + + public boolean isBlank(int i, int j) { + return mResultMatrix[i][j] == BLANK_VALUE; + } + + public int getBlankSize() { + int size = 0; + for(int i=0; i idxList = getSquareIndexList(i, j); + for(int k=0; k getConflicts(int i, int j, int value) { + Vector result = new Vector(); + // check value in the row and column + for(int k=0; k idxList = getSquareIndexList(i, j); + for(int k=0; k getSquareIndexList(int i, int j) { + Vector result = new Vector(); + int iStart = i / 3 * 3; + int jStart = j / 3 * 3; + for(i=iStart; i 0) { + result.setInitValue(i, j, testValue[m]); + } + } + return result; + } + +} \ No newline at end of file diff --git a/app/src/main/java/master/sudoku/ocr/ImageCutter.java b/app/src/main/java/master/sudoku/ocr/ImageCutter.java new file mode 100644 index 0000000..b0b3117 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/ImageCutter.java @@ -0,0 +1,466 @@ +/** + * + */ +package master.sudoku.ocr; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Color; +import android.graphics.Rect; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; + +import master.sudoku.ocr.matrix.BoundaryMatrix; +import master.sudoku.ocr.matrix.ImageMatrix; +import master.sudoku.ocr.util.MatrixUtil; +import master.sudoku.ocr.util.ThresholdUtil; +import master.sudoku.utils.FileUtils; + +/** + * @author dannyzha + * + */ +public class ImageCutter +{ + /// + /// key: Index + /// value: Bitmap + /// + private Hashtable mImageTable; + + /// + /// Constructor + /// + /// + public ImageCutter(Bitmap orgImage) throws Exception + { + int bgColor = binarization(orgImage); + ThresholdUtil.BG_COLOR = bgColor; + ImageMatrix imgMatrix = new ImageMatrix(orgImage); + + BoundaryMatrix boundary = MatrixUtil.detectBoundary(imgMatrix); + if (!boundary.isValid()) + { + throw new Exception("Invalid image."); + } + buildImageTable(orgImage, boundary); + } + + /// + /// Another constructor + /// + /// + /// + public ImageCutter(Bitmap orgImage, BoundaryMatrix boundary) throws Exception + { + if (!boundary.isValid()) + { + throw new Exception("Invalid image."); + } + + buildImageTable(orgImage, boundary); + } + + public Bitmap get(Index index) + { + return mImageTable.get(index); + } + + public Bitmap getImage(int i, int j) + { + Enumeration enu = mImageTable.keys(); + while (enu.hasMoreElements()) + { + Index idx = enu.nextElement(); + if (idx.Equals(i, j)) + { + return (Bitmap)mImageTable.get(idx); + } + } + return null; + } + + /// + /// binaryzation an image, global threshold + /// + /// + /// the BG color of the image + private int binarization(Bitmap image) + { + int dimensionX = image.getWidth(); + int dimensionY = image.getHeight(); + int whitePixelCnt = 0, blackPixelCnt = 0; + + for (int x = 0; x < dimensionX; x++) + { + for (int y = 0; y < dimensionY; y++) + { + int value = ThresholdUtil.GetDarkValue(image.getPixel(x, y)); + if (value > ThresholdUtil.DARK_THRESHOLD) + { + image.setPixel(x, y, Color.argb(255, 255, 255, 255)); + whitePixelCnt++; + } + else + { + image.setPixel(x, y, Color.argb(255, 0, 0, 0)); + blackPixelCnt++; + } + } + } + // save the image to see the binarization result + saveImage(image, "binaryzation.jpg"); + + if (whitePixelCnt >= blackPixelCnt) + { + return Color.WHITE; + } + else + { + return Color.BLACK; + } + } + + /// + /// Adaptive Thresholding Using the Integral Image + /// http://people.scs.carleton.ca/~roth/iit-publications-iti/docs/gerh-50002.pdf + /// + /// + /// + private int binarization2(Bitmap image) + { + int dimensionX = image.getWidth(); + int dimensionY = image.getHeight(); + int whitePixelCnt = 0, blackPixelCnt = 0; + + // create integral table + int[] integralTable = new int[dimensionX * dimensionY]; + for (int x = 0; x < dimensionX; x++) + { + // reset this column sum + int sum = 0; + for (int y = 0; y < dimensionY; y++) + { + int index = y * dimensionX + x; + sum += ThresholdUtil.GetDarkValue(image.getPixel(x, y)); + if (x == 0) + integralTable[index] = sum; + else + integralTable[index] = integralTable[index - 1] + sum; + } + } + + int S = dimensionX >> 3; + int T = 8; + int S2 = S / 2; + + for (int x = 0; x < dimensionX; x++) + { + for (int y = 0; y < dimensionY; y++) + { + int x1=x-S2, x2=x+S2; + int y1=y-S2, y2=y+S2; + // check the border + if (x1 < 0) x1 = 0; + if (x2 >= dimensionX) x2 = dimensionX-1; + if (y1 < 0) y1 = 0; + if (y2 >= dimensionY) y2 = dimensionY-1; + + int count = (x2 - x1) * (y2 - y1); + int sum = integralTable[y2 * dimensionX + x2] - + integralTable[y1 * dimensionX + x2] - + integralTable[y2 * dimensionX + x1] + + integralTable[y1 * dimensionX + x1]; + + + int value = ThresholdUtil.GetDarkValue(image.getPixel(x, y)); + if (value * count > sum * (100 - T) / 100) + { + image.setPixel(x, y, Color.WHITE); + whitePixelCnt++; + } + else + { + image.setPixel(x, y, Color.BLACK); + blackPixelCnt++; + } + } + } + // save the image to see the binarization result + saveImage(image, "binaryzation.jpg"); + + if (whitePixelCnt >= blackPixelCnt) + { + return Color.WHITE; + } + else + { + return Color.BLACK; + } + } + + private Bitmap CropImage(Bitmap image, Rect area) + { + Bitmap cropped = Bitmap.createBitmap(image, area.left, area.top, area.width(), area.height()); + return cropped; + } + + private void buildImageTable(Bitmap orgImage, BoundaryMatrix boundary) + { + List xBoundary = boundary.getBoundaryXList(); + List yBoundary = boundary.getBoundaryYList(); + List xBoundaryWidth = boundary.getBoundaryWidthXList(); + List yBoundaryWidth = boundary.getBoundaryWidthYList(); + mImageTable = new Hashtable(); + for (int i = 0; i < 9; i++) + { + for (int j = 0; j < 9; j++) + { + int left = xBoundary.get(i)+xBoundaryWidth.get(i); + int top = yBoundary.get(j)+yBoundaryWidth.get(j); + int right = xBoundary.get(i + 1) - xBoundaryWidth.get(i + 1); + int bottom = yBoundary.get(j + 1) - yBoundaryWidth.get(j + 1); + // shrink the rect area + int xMargin = (int)((right - left) * 0.1); + int yMargin = (int)((bottom - top) * 0.1); + left += xMargin; + right -= xMargin; + top += yMargin; + bottom -= yMargin; + // remove margins + while (getForePixelSum(orgImage, left, top, bottom, true) == (bottom - top) && (left < right - 1)) { + left ++; + } + while (getForePixelSum(orgImage, right, top, bottom, true) == (bottom - top) && (right > left + 1)) { + right --; + } + while (getForePixelSum(orgImage, top, left, right, false) == (right - left) && (top < bottom - 1)) { + top ++; + } + while (getForePixelSum(orgImage, bottom, left, right, false) == (right - left) && (bottom > top + 1)) { + bottom --; + } + + while (getForePixelSum(orgImage, left, top, bottom, true) <= 1 && (left < right - 1)) { + left ++; + } + while (getForePixelSum(orgImage, right, top, bottom, true) <= 1 && (right > left + 1)) { + right --; + } + while (getForePixelSum(orgImage, top, left, right, false) <= 1 && (top < bottom - 1)) { + top ++; + } + while (getForePixelSum(orgImage, bottom, left, right, false) <= 1 && (bottom > top + 1)) { + bottom --; + } + Rect area = new Rect(left, top, right, bottom); + Bitmap crop = CropImage(orgImage, area); + //crop = removeMargin(crop); + mImageTable.put(new Index(i, j), crop); + } + } + } + + private int getForePixelSum(Bitmap image, int index, int lower, int upper, boolean isVertical) { + int sum = 0; + for (int i = lower; i < upper; i++) { + int color = isVertical ? image.getPixel(index, i) : image.getPixel(i, index); + if (color != ThresholdUtil.BG_COLOR) { + sum ++; + } + } + return sum; + } + +// private Bitmap removeMargin(Bitmap image) +// { +// image = denoise(image); +// int left = 0, right = image.getWidth(), top = 0, bottom = image.getHeight(); +// boolean changed = false; +// for (int x = 0; x < image.getWidth(); x++) +// { +// for (int y = 0; y < image.getHeight(); y++) +// { +// int value = ThresholdUtil.GetDarkValue(image.getPixel(x, y)); +// if (value <= ThresholdUtil.DARK_THRESHOLD) +// { +// left = x; +// break; +// } +// } +// if (left > 0) +// { +// changed = true; +// break; +// } +// } +// for (int x = image.getWidth() - 1; x >= 0; x--) +// { +// for (int y = 0; y < image.getHeight(); y++) +// { +// int value = ThresholdUtil.GetDarkValue(image.getPixel(x, y)); +// if (value <= ThresholdUtil.DARK_THRESHOLD) +// { +// right = x; +// break; +// } +// } +// if (right < image.getWidth()) +// { +// changed = true; +// break; +// } +// } +// for (int y = 0; y < image.getHeight(); y++) +// { +// for (int x = 0; x < image.getWidth(); x++) +// { +// int value = ThresholdUtil.GetDarkValue(image.getPixel(x, y)); +// if (value <= ThresholdUtil.DARK_THRESHOLD) +// { +// top = y; +// break; +// } +// } +// if (top > 0) +// { +// changed = true; +// break; +// } +// } +// for (int y = image.getHeight() - 1; y >= 0; y--) +// { +// for (int x = 0; x < image.getWidth(); x++) +// { +// int value = ThresholdUtil.GetDarkValue(image.getPixel(x, y)); +// if (value <= ThresholdUtil.DARK_THRESHOLD) +// { +// bottom = y; +// break; +// } +// } +// if (bottom < image.getHeight()) +// { +// changed = true; +// break; +// } +// } +// if(changed) +// { +// Logger.getLogger().debug("changed: " + "left:" + left + ", right:" + right + ", top:" + top + ", bottom:" + bottom); +// return CropImage(image, new Rect(left, top, right, bottom)); +// } +// return image; +// } + + private Bitmap denoise(Bitmap image) + { + Bitmap result = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Config.ALPHA_8); + for (int x = 0; x < image.getWidth(); x++) + { + for (int y = 0; y < image.getHeight(); y++) + { + if (ThresholdUtil.IsDark(image.getPixel(x, y))) + { + int neighborPixel = 0; + int x1 = x - 1, x2 = x + 1; + int y1 = y - 1, y2 = y + 1; + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 > image.getWidth() - 1) x2 = image.getWidth() - 1; + if (y2 > image.getHeight() - 1) y2 = image.getHeight() - 1; + + if (ThresholdUtil.IsDark(image.getPixel(x, y1))) + { + neighborPixel++; + } + if (ThresholdUtil.IsDark(image.getPixel(x, y2))) + { + neighborPixel++; + } + if (ThresholdUtil.IsDark(image.getPixel(x1, y))) + { + neighborPixel++; + } + if (ThresholdUtil.IsDark(image.getPixel(x2, y))) + { + neighborPixel++; + } + + + //if (ThresholdUtil.IsDark(image.GetPixel(x1, y1))) + //{ + // neighborPixel++; + //} + + //if (ThresholdUtil.IsDark(image.GetPixel(x2, y1))) + //{ + // neighborPixel++; + //} + + //if (ThresholdUtil.IsDark(image.GetPixel(x2, y2))) + //{ + // neighborPixel++; + //} + + //if (ThresholdUtil.IsDark(image.GetPixel(x1, y2))) + //{ + // neighborPixel++; + //} + + + if (neighborPixel > 2) + { + result.setPixel(x, y, Color.BLACK); + continue; + } + } + result.setPixel(x, y, Color.WHITE); + } + } + return result; + } + + public void saveImages() + { + Enumeration indexEnum = mImageTable.keys(); + while (indexEnum.hasMoreElements()) + { + Index idx = indexEnum.nextElement(); + Bitmap image = mImageTable.get(idx); + saveImage(image, idx.getI() + "_" + idx.getJ() + ".jpg"); + } + } + + public void saveImage(Bitmap img, String fileName) { + String path = FileUtils.getCachePath(); + OutputStream fOut = null; + File file = new File(path, fileName); + try { + if (!file.exists()) { + file.createNewFile(); + } + fOut = new FileOutputStream(file); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + img.compress(Bitmap.CompressFormat.JPEG, 85, bytes); + bytes.flush(); + fOut.write(bytes.toByteArray()); + bytes.close(); + fOut.flush(); + fOut.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/master/sudoku/ocr/Index.java b/app/src/main/java/master/sudoku/ocr/Index.java new file mode 100644 index 0000000..c4ec876 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/Index.java @@ -0,0 +1,54 @@ +/** + * + */ +package master.sudoku.ocr; + +/** + * @author dannyzha + * + */ +public class Index { + public static int INVALID_INDEX = -1; + + private int i = INVALID_INDEX; + private int j = INVALID_INDEX; + + /** + * Constructor + * @param i + * @param j + */ + public Index(int i, int j) { + this.i = i; + this.j = j; + } + + public boolean Equals(int i, int j) { + return this.i == i && this.j == j; + } + + @Override + public boolean equals(Object o) { + if(!(o instanceof Index)) { + return false; + } + Index other = (Index)o; + return other.Equals(this.i, this.j); + } + + public int getI() { + return i; + } + + public void setI(int i) { + this.i = i; + } + + public int getJ() { + return j; + } + + public void setJ(int j) { + this.j = j; + } +} diff --git a/app/src/main/java/master/sudoku/ocr/Recognizer.java b/app/src/main/java/master/sudoku/ocr/Recognizer.java new file mode 100644 index 0000000..534d6e1 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/Recognizer.java @@ -0,0 +1,140 @@ +/** + * + */ +package master.sudoku.ocr; + +import android.graphics.Bitmap; + +import java.util.List; + +import master.sudoku.ocr.matrix.ImageMatrix; + + +/** + * @author dannyzha + * + */ +public class Recognizer +{ + private Bitmap mImage; + +// private static final String BASE_FOLDER = FileUtils.getCachePath() + "\\Sudoku\\training\\"; + /// + /// key: Integer + /// value: List of ImageMatrix with the standard images + /// +// private static Hashtable> s_StandardTable; + + /// + /// static initializer + /// +// static +// { +// buildStandardTable(); +// printStandardTable(); +// } + + /// + /// Constructor + /// + /// + public Recognizer(Bitmap image) + { + this.mImage = image; + } + + public int determine() + { + ImageMatrix target = new ImageMatrix(mImage); + int result = 0; + if (target.isBlank()) + { + return 0; + } + int featureValue = target.getFeature().getFeatureValue(); + if (featureValue > 0) + { + return featureValue; + } +// double maxSimilarity = Double.MIN_VALUE; +// while(enu.hasMoreElements()) +// { +// int i = enu.nextElement(); +// List standards = s_StandardTable.get(i); +// for (int j = 0; j < standards.size(); j++) +// { +// //System.Console.WriteLine("Determine " + i + "," + j); +// double similarity = target.getFeature().getSimilarity(standards.get(j)); +// //System.Console.WriteLine("Similarity is:" + similarity); +// if (similarity > maxSimilarity) +// { +// maxSimilarity = similarity; +// result = i; +// } +// } +// } +// System.Console.WriteLine("Max similarity is:" + maxSimilarity); +// System.Console.WriteLine("Result is:" + result); + return result; + } + +// private static void buildStandardTable() +// { +// s_StandardTable = new Hashtable>(); +// for (int i = 0; i <= 9; i++) +// { +// String folder = BASE_FOLDER + i; +// File dic = new File(folder); +// File[] files = dic.listFiles(); +// List imgMatrixList = new ArrayList(); +// for (int j = 0; j < files.length; j++) +// { +// try +// { +// Bitmap img = BitmapFactory.decodeFile(files[i].getAbsolutePath()); +// ImageMatrix matrix = new ImageMatrix(img); +// imgMatrixList.add(matrix); +// } +// catch (Exception ex) +// { +// ex.printStackTrace(); +// } +// } +// s_StandardTable.put(i, imgMatrixList); +// } +// } + +// private static void printStandardTable() +// { +// Enumeration enu = s_StandardTable.keys(); +// while (enu.hasMoreElements()) +// { +// int i = (int)enu.nextElement(); +// List standards = (List)s_StandardTable.get(i); +// for (int j = 0; j < standards.size(); j++) +// { +// MatrixFeature2 feature = standards.get(j).getFeature(); +// System.out.println("Feature of " + i + ":"); +// System.out.print("Density X:"); +// printList(feature.mDensityX); +// System.out.print("Density Y:"); +// printList(feature.mDensityY); +// System.out.print("Segment X:"); +// printList(feature.mSegmentX); +// System.out.println("Segment Y:"); +// printList(feature.mSegmentY); +// System.out.println("============================"); +// } +// } +// } + + private static void printList(List list) + { + for(int i=0; i ThresholdUtil.DARK_THRESHOLD) { + result[i][j] = 1; + } else { + result[i][j] = 0; + } + } + } + return result; + } +} diff --git a/app/src/main/java/master/sudoku/ocr/matrix/BoundaryMatrix.java b/app/src/main/java/master/sudoku/ocr/matrix/BoundaryMatrix.java new file mode 100644 index 0000000..bb0e481 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/matrix/BoundaryMatrix.java @@ -0,0 +1,226 @@ +/** + * + */ +package master.sudoku.ocr.matrix; + +import java.util.ArrayList; +import java.util.List; + +import master.sudoku.logs.Logger; +import master.sudoku.ocr.util.ThresholdUtil; + +/** + * @author dannyzha + * + */ +public class BoundaryMatrix extends MatrixBase { + + private int mLeft; + private int mRight; + private int mTop; + private int mBottom; + + private List mBoundaryXList; + private List mBoundaryYList; + + private List mBoundaryWidthXList; + private List mBoundaryWidthYList; + + /// + /// average interval between boundaries + /// + private int mAveIntervalX = 0; + private int mAveIntervalY = 0; + + /// + /// Constructor + /// + /// + /// + public BoundaryMatrix(int dimensionX, int dimensionY) + { + super(dimensionX, dimensionY); + mBoundaryXList = new ArrayList(); + mBoundaryYList = new ArrayList(); + mBoundaryWidthXList = new ArrayList(); + mBoundaryWidthYList = new ArrayList(); + } + + + public int getLeft() { + return mLeft; + } + + public void setLeft(int left) { + mLeft = left; + } + + public int getRight() { + return mRight; + } + + public void setRight(int right) { + mRight = right; + } + + public int getTop() { + return mTop; + } + + public void setTop(int top) { + mTop = top; + } + + public int getBottom() { + return mBottom; + } + + public void setBottom(int bottom) { + mBottom = bottom; + } + + /// + /// + /// + public List getBoundaryXList() + { + return new ArrayList(mBoundaryXList); + } + + public List getBoundaryYList() + { + return new ArrayList(mBoundaryYList); + } + + public List getBoundaryWidthXList() + { + return new ArrayList(mBoundaryWidthXList); + } + + public List getBoundaryWidthYList() + { + return new ArrayList(mBoundaryWidthYList); + } + + /* (non-Javadoc) + * @see com.skyway.pandora.digitsrecognizer.matrix.MatrixBase#getValue(int, int) + */ + @Override + public int getValue(int x, int y) + { + if(x < 0 || x > mDimensionX || y < 0 || y > mDimensionY) + { + return -1; + } + for (int i = 0; i < mBoundaryXList.size(); i++) + { + if (mBoundaryXList.get(i) == x) + { + return ThresholdUtil.DARK_VALUE; + } + } + for (int i = 0; i < mBoundaryYList.size(); i++) + { + if (mBoundaryYList.get(i) == y) + { + return ThresholdUtil.DARK_VALUE; + } + } + return ThresholdUtil.SHALLOW_VALUE; + } +// +// public void addBoundaryX(int x) +// { +// if (mBoundaryXList.size() > 0) +// { +// int interval = Math.abs(x - mBoundaryXList.get(mBoundaryXList.size() - 1)); +// if (interval <= mBoundaryWidthXList.get(mBoundaryXList.size() - 1) +// || interval < mAveIntervalX / 2) +// { +// mBoundaryWidthXList.set(mBoundaryXList.size() - 1, interval + 1); +// return; +// } +// mAveIntervalX = ((mAveIntervalX * mBoundaryXList.size()) + interval) / (mBoundaryXList.size() + 1); +// } +// mBoundaryXList.add(x); +// mBoundaryWidthXList.add(1); +// } +// +// public void addBoundaryY(int y) +// { +// if (mBoundaryYList.size() > 0) +// { +// int interval = Math.abs(y - mBoundaryYList.get(mBoundaryYList.size() - 1)); +// if (interval <= mBoundaryWidthYList.get(mBoundaryYList.size() - 1) +// || interval < mAveIntervalY / 2) +// { +// mBoundaryWidthYList.set(mBoundaryYList.size() - 1, interval + 1); +// return; +// } +// mAveIntervalY = ((mAveIntervalY * mBoundaryYList.size()) + interval) / (mBoundaryYList.size() + 1); +// } +// mBoundaryYList.add(y); +// mBoundaryWidthYList.add(1); +// } + + public void clear() + { + mBoundaryXList.clear(); + mBoundaryYList.clear(); + } + + public void generateBoundarys() { + this.clear(); + Logger.getLogger().debug("left:" + mLeft + ", right:" + mRight + ", top:" + mTop + ", bottom:" + mBottom); + int mAveIntervalX = (mRight - mLeft) / 9; + int mAveIntervalY = (mBottom - mTop) / 9; + Logger.getLogger().debug("interval X:" + mAveIntervalX + ", interval Y:" + mAveIntervalY); + for (int i = 0; i < 10; i++) { + int x = mLeft + i * mAveIntervalX; + int y = mTop + i * mAveIntervalY; + Logger.getLogger().debug("boundary X" + i + ":" + x + ", boundary Y" + i + ":" + y); + mBoundaryXList.add(x); + mBoundaryYList.add(y); + mBoundaryWidthXList.add(1); // consider boundary line width 1px + mBoundaryWidthYList.add(1); + } + } + + public boolean isValid() + { + return mBoundaryXList.size() == 10 && mBoundaryYList.size() == 10; + } + + @Override + public void setValue(int x, int y, int value) { + // TODO Auto-generated method stub + + } + + @Override + public boolean isSimilar(MatrixBase another) { + // TODO Auto-generated method stub + return false; + } + + public void justifyBoundaries(List xBoundaryList, List yBoundaryList) { + if (xBoundaryList.size() == 8) { + for (int i = 0; i < 8; i++) { + int idealX = mBoundaryXList.get(i + 1); + int lineX = xBoundaryList.get(i); + if (Math.abs(lineX - idealX) <= 5) { + mBoundaryXList.set(i + 1, lineX); + } + } + } + if (yBoundaryList.size() == 8) { + for (int i = 0; i < 8; i++) { + int idealY = mBoundaryYList.get(i + 1); + int lineY = yBoundaryList.get(i); + if (Math.abs(lineY - idealY) <= 5) { + mBoundaryYList.set(i + 1, lineY); + } + } + } + } +} diff --git a/app/src/main/java/master/sudoku/ocr/matrix/ImageMatrix.java b/app/src/main/java/master/sudoku/ocr/matrix/ImageMatrix.java new file mode 100644 index 0000000..2a13a4c --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/matrix/ImageMatrix.java @@ -0,0 +1,133 @@ +/** + * + */ +//using System.Windows.Controls; +//using System.Windows.Media; +//using System.Windows.Media.Imaging; +package master.sudoku.ocr.matrix; + +import master.sudoku.ocr.util.ThresholdUtil; +import android.graphics.Bitmap; +import android.graphics.Color; + +/** + * @author dannyzha + * + */ +public class ImageMatrix extends MatrixBase +{ + private Bitmap mImage; + private MatrixFeature2 mFeature; + + public ImageMatrix(Bitmap image) + { + super(image.getWidth(), image.getHeight()); + mImage = image; + + mFeature = new MatrixFeature2(this); + //Image img = new Image(); + //img.source = bi; + + //img.Measure(new Size(100, 100)); + //img.Arrange(new Rect(0, 0, 100, 100)); + + //ScaleTransform scaleTrans = new ScaleTransform(); + //double scale = (double)500 / (double)Math.Max(bi.PixelHeight, bi.PixelWidth); + //scaleTrans.CenterX = 0; + //scaleTrans.CenterY = 0; + //scaleTrans.ScaleX = scale; + //scaleTrans.ScaleY = scale; + + //WriteableBitmap writeableBitmap = new WriteableBitmap(500, 500); + //writeableBitmap.Render(img, scaleTrans); + + //int[] pixelData = writeableBitmap.Pixels; + } + + public MatrixFeature2 getFeature() + { + return mFeature; + } + + public boolean isBlank() + { + return mFeature.isBlank(); + } + + public int getColor(int x, int y) + { + if (x < 0 || x > mDimensionX || y < 0 || y > mDimensionY) + { + return Color.WHITE; + } + return mImage.getPixel(x, y); + } + + /* (non-Javadoc) + * @see com.skyway.pandora.digitsrecognizer.matrix.MatrixBase#getValue(int, int) + */ + @Override + public int getValue(int x, int y) + { + if (x < 0 || x > mDimensionX || y < 0 || y > mDimensionY) + { + return -1; + } + int c = mImage.getPixel(x, y); + return ThresholdUtil.GetDarkValue(c); + } + + /* (non-Javadoc) + * @see com.skyway.pandora.digitsrecognizer.matrix.MatrixBase#setValue(int, int, int) + */ + @Override + public void setValue(int x, int y, int value) + { + // intended do nothing + } + + @Override + public boolean isSimilar(MatrixBase another) + { + return false; + //if (!(another is ImageMatrix)) + //{ + // return false; + //} + //double similarity = this.mFeature.getSimilarity(((ImageMatrix)another).Feature); + //System.Console.WriteLine("Similarity is:" + similarity); + //if (similarity > 90) + //{ + // return true; + //} + //return false; + } + + //public double getSimilarity(MatrixBase another) + //{ + // if (!(another is ImageMatrix)) + // { + // return 0; + // } + // return this.mFeature.getSimilarity(((ImageMatrix)another).Feature); + //} + + //private void buildColorTable() + //{ + // for (int x = 0; x < mDimensionX; x++) + // { + // for (int y = 0; y < mDimensionY; y++) + // { + // Color c = mImage.GetPixel(x, y); + // if (mColorTable.ContainsKey(c)) + // { + // mColorTable[c] = (int)mColorTable[c] + 1; + // } + // else + // { + // mColorTable.Add(c, 1); + // } + // } + // } + //} +} \ No newline at end of file diff --git a/app/src/main/java/master/sudoku/ocr/matrix/MatrixBase.java b/app/src/main/java/master/sudoku/ocr/matrix/MatrixBase.java new file mode 100644 index 0000000..6904702 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/matrix/MatrixBase.java @@ -0,0 +1,40 @@ +/** + * + */ +package master.sudoku.ocr.matrix; + +/** + * @author dannyzha + * + */ +abstract public class MatrixBase +{ + protected int mDimensionX; + protected int mDimensionY; + + public int getDimensionX() + { + return mDimensionX; + } + + public int getDimensionY() + { + return mDimensionY; + + } + + /** + * Constructor + * @param dimensionX + * @param dimensionY + */ + public MatrixBase(int dimensionX, int dimensionY) + { + mDimensionX = dimensionX; + mDimensionY = dimensionY; + } + + public abstract int getValue(int x, int y); + public abstract void setValue(int x, int y, int value); + public abstract boolean isSimilar(MatrixBase another); +} diff --git a/app/src/main/java/master/sudoku/ocr/matrix/MatrixFeature.java b/app/src/main/java/master/sudoku/ocr/matrix/MatrixFeature.java new file mode 100644 index 0000000..0471a1f --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/matrix/MatrixFeature.java @@ -0,0 +1,438 @@ +package master.sudoku.ocr.matrix; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import master.sudoku.ocr.util.MatrixUtil; +import master.sudoku.ocr.util.ThresholdUtil; + +class CountArray +{ + public int count1 = 0; + public int count2 = 0; + public int count3 = 0; + public int count4 = 0; + public int countN = 0; +} + +public class MatrixFeature +{ + /// + /// density of dark pixels, in persentage + /// + public List mDensityX = new ArrayList(); // indexed by X + public List mDensityY = new ArrayList(); // indexed by Y + + /// + /// the number of segments with dark pixels + /// + public List mSegmentX = new ArrayList(); // indexed by X + public List mSegmentY = new ArrayList(); // indexed by Y + + public List mPossibleValue = new ArrayList(); + + /// + /// Constructor + /// + /// + public MatrixFeature(MatrixBase matrix) + { + generateFeature(matrix); + checkMode(); + if (mSegmentX.size() > 0) + MaxSegmentX = MatrixUtil.getMax(mSegmentX); + if (mSegmentY.size() > 0) + MaxSegmentY = MatrixUtil.getMax(mSegmentY); + } + + public int SegmentModeX; + public int SegmentModeY; + public int MaxSegmentX; + public int MaxSegmentY; + + public List getDensityX() + { + return new ArrayList(mDensityX); + } + + public List getDensityY() + { + return new ArrayList(mDensityY); + } + + public List getSegmentX() + { + return new ArrayList(mSegmentX); + } + + public List getSegmentY() + { + return new ArrayList(mSegmentY); + } + + public int getDensitySumX() + { + return getListSum(mDensityX); + } + + public int getDensitySumY() + { + return getListSum(mDensityY); + } + + public int getSegmentSumX() + { + return getListSum(mSegmentX); + } + + public int getSegmentSumY() + { + return getListSum(mSegmentY); + } + + private int getListSum(List list) + { + int sum = 0; + for (int i = 0; i < list.size(); i++) + { + sum += list.get(i); + } + return sum; + } + + public boolean IsBlank() + { + return mDensityX.size() == 0 && mDensityY.size() == 0 && mSegmentX.size() == 0 && mSegmentY.size() == 0; + } + + public int GetResampleDensityX(int x, int dimensionX) + { + return resample(mDensityX, x, dimensionX); + } + + public int GetResampleDensityY(int y, int dimensionY) + { + return resample(mDensityY, y, dimensionY); + } + + public int GetResampleSegmentX(int x, int dimensionX) + { + return resample(mSegmentX, x, dimensionX); + } + + public int GetResampleSegmentY(int y, int dimensionY) + { + return resample(mSegmentY, y, dimensionY); + } + + private int resample(List source, int newIndex, int newCount) + { + if (source.size() == 0 || newCount == 0) return 0; + if (newCount == source.size()) return source.get(newIndex); + + int idx = source.size() * newIndex / newCount; + int cur = source.get(idx); + int prev = idx > 1 ? source.get(idx - 1) : cur; + int next = idx < source.size() - 1 ? source.get(idx + 1) : cur; + + return (int)(cur + prev + next)/3; + } + + /// + /// get similarity of two MatrixFeature + /// + /// + /// in between 0 and 100 + public double getSimilarity(MatrixFeature another) + { + double result = 100; + for (int x = 0; x < mDensityX.size(); x++) + { + int anotherDensity = another.GetResampleDensityX(x, mDensityX.size()); + double diff = (anotherDensity - mDensityX.get(x)) / (double)mDensityX.get(x); + result -= diff > 0 ? diff : (-diff); + } + + for (int y = 0; y < mDensityY.size(); y++) + { + int anotherDensity = another.GetResampleDensityY(y, mDensityY.size()); + double diff = (anotherDensity - mDensityY.get(y)) / (double)mDensityY.get(y); + result -= diff > 0 ? diff : (-diff); + } + + + for (int x = 0; x < mSegmentX.size(); x++) + { + int anotherSegment = another.GetResampleSegmentX(x, mSegmentX.size()); + double diff = (anotherSegment - mSegmentX.get(x)) / (double)mSegmentX.get(x); + result -= diff > 0 ? diff : (-diff); + } + + for (int y = 0; y < mSegmentY.size(); y++) + { + int anotherSegment = another.GetResampleSegmentY(y, mSegmentY.size()); + double diff = (anotherSegment - mSegmentY.get(y)) / (double)mSegmentY.get(y); + result -= diff > 0 ? diff : (-diff); + } + + result -= Math.abs(this.SegmentModeX - another.SegmentModeX); + result -= Math.abs(this.SegmentModeY - another.SegmentModeY); + result -= Math.abs(this.MaxSegmentX - another.MaxSegmentX); + result -= Math.abs(this.MaxSegmentY - another.MaxSegmentY); + + return result; + } + + private void generateFeature(MatrixBase matrix) + { + // generate the features on X-direction + for (int x = 0; x < matrix.getDimensionX(); x++) + { + int preValue = ThresholdUtil.SHALLOW_VALUE; + int darkCount = 0; + int segmentCount = 0; + for (int y = 0; y < matrix.getDimensionY(); y++) + { + int value = matrix.getValue(x, y); + if (value <= ThresholdUtil.DARK_THRESHOLD) + { + darkCount++; + if (preValue == ThresholdUtil.SHALLOW_VALUE) + { + segmentCount++; + } + preValue = ThresholdUtil.DARK_VALUE; + } + else + { + preValue = ThresholdUtil.SHALLOW_VALUE; + } + } + int density = darkCount * 100 / matrix.getDimensionY(); + if (density > 0) + { + mDensityX.add(density); + } + if (segmentCount > 0) + { + mSegmentX.add(segmentCount); + } + } + + // generate the features on Y-direction + for (int y = 0; y < matrix.getDimensionY(); y++) + { + int preValue = ThresholdUtil.SHALLOW_VALUE; + int darkCount = 0; + int segmentCount = 0; + for (int x = 0; x < matrix.getDimensionX(); x++) + { + int value = matrix.getValue(x, y); + if (value <= ThresholdUtil.DARK_THRESHOLD) + { + darkCount++; + if (preValue == ThresholdUtil.SHALLOW_VALUE) + { + segmentCount++; + } + preValue = ThresholdUtil.DARK_VALUE; + } + else + { + preValue = ThresholdUtil.SHALLOW_VALUE; + } + } + int density = darkCount * 100 / matrix.getDimensionX(); + if (density > 0) + { + mDensityY.add(density); + } + if (segmentCount > 0) + { + mSegmentY.add(segmentCount); + } + } + } + + /// + /// directly get value based on the feature + /// + /// + public int getFeatureValue() + { + //if (mDensityX.size() == 0 || mDensityY.size() == 0) + //{ + // return 0; + //} + //for (int i = 1; i <= 9; i++) + //{ + // mPossibleValue.Add(i); + //} + //IEnumerable modePossibleList = checkMode(); + //IEnumerable maxPossibleList = checkMax(); + + //IEnumerable result = modePossibleList.Intersect(maxPossibleList); + + //mPossibleValue.Clear(); + //IEnumerator enu = result.GetEnumerator(); + //while (enu.MoveNext()) + //{ + // mPossibleValue.Add(enu.Current); + //} + //if (mPossibleValue.size() == 1) + //{ + // return mPossibleValue[0]; + //} + return -1; + } + + private void checkMode() + { + int count1 = 0, count2 = 0, count3 = 0, count4 = 0, countN = 0; + // check mode on X direction + CountArray countArr = new CountArray(); + getCount(mSegmentX, countArr); + int max = getMax(countArr); + HashSet possibleValueX = new HashSet(); + if (max == count3) + { + this.SegmentModeX = 3; + //possibleValueX.Add(2); + //possibleValueX.Add(5); + //possibleValueX.Add(6); + //possibleValueX.Add(8); + //possibleValueX.Add(9); + } + else if (max == count2) + { + this.SegmentModeX = 2; + //possibleValueX.Add(3); + //possibleValueX.Add(4); + //possibleValueX.Add(7); + } + else if (max == count1) + { + this.SegmentModeX = 1; + //possibleValueX.Add(1); + } + + // check mode on Y direction + getCount(mSegmentY, countArr); + max = getMax(countArr); + HashSet possibleValueY = new HashSet(); + if (max == count2) + { + this.SegmentModeY = 2; + //possibleValueY.Add(2); + //possibleValueY.Add(3); + //possibleValueY.Add(4); + //possibleValueY.Add(6); + //possibleValueY.Add(8); + //possibleValueY.Add(9); + } + else if (max == count1) + { + this.SegmentModeY = 1; + //possibleValueY.Add(1); + //possibleValueY.Add(2); + //possibleValueY.Add(3); + //possibleValueY.Add(4); + //possibleValueY.Add(5); + //possibleValueY.Add(7); + } + + //return possibleValueX.Intersect(possibleValueY); + } + + private Iterable checkMax() + { + int xMax = MatrixUtil.getMax(mSegmentX); + HashSet possibleValueX = new HashSet(); + if (xMax == 1) + { + possibleValueX.add(1); + } + else if (xMax == 2) + { + possibleValueX.add(1); + possibleValueX.add(7); + } + else if (xMax == 3) + { + possibleValueX.add(2); + possibleValueX.add(3); + possibleValueX.add(5); + possibleValueX.add(6); + possibleValueX.add(9); + } + else if (xMax == 4) + { + possibleValueX.add(2); + possibleValueX.add(3); + possibleValueX.add(5); + possibleValueX.add(6); + possibleValueX.add(8); + } + int yMax = MatrixUtil.getMax(mSegmentY); + HashSet possibleValueY = new HashSet(); + if (yMax == 1) + { + possibleValueY.add(1); + possibleValueY.add(7); + } + else if (yMax == 2) + { + possibleValueY.add(2); + possibleValueY.add(3); + possibleValueY.add(4); + possibleValueY.add(5); + possibleValueY.add(6); + possibleValueY.add(7); + possibleValueY.add(8); + possibleValueY.add(9); + } + else if (yMax == 3) + { + possibleValueY.add(6); + possibleValueY.add(9); + } + return MatrixUtil.intersect(possibleValueX, possibleValueY); + } + + private void getCount(List input, CountArray countArr) + { + for (int i = 0; i < input.size(); i++) + { + switch (input.get(i)) + { + case 1: + countArr.count1 ++; + break; + case 2: + countArr.count2++; + break; + case 3: + countArr.count3++; + break; + case 4: + countArr.count4++; + break; + default: + countArr.countN++; + break; + } + } + } + + private int getMax(CountArray countArr) + { + return Math.max( + Math.max( + Math.max( + Math.max(countArr.count1, countArr.count2), + countArr.count3), + countArr.count4), + countArr.countN); + } +} diff --git a/app/src/main/java/master/sudoku/ocr/matrix/MatrixFeature2.java b/app/src/main/java/master/sudoku/ocr/matrix/MatrixFeature2.java new file mode 100644 index 0000000..9aca0e2 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/matrix/MatrixFeature2.java @@ -0,0 +1,287 @@ +package master.sudoku.ocr.matrix; + +import java.util.ArrayList; +import java.util.List; + +import master.sudoku.ocr.util.MatrixUtil; +import master.sudoku.ocr.util.ThresholdUtil; + + +public class MatrixFeature2 +{ + private int mAxisX = 0; + private int mHoleCnt = 0; + private int mLeftPitCnt = 0; + private int mRightPitCnt = 0; + private int mFirstPitPos = 0; // 1 for left, 2 for right, to distinguish '2' and '5' + private boolean mHasAxisX = false; + + /// + /// intersections on the x-axis + /// + private List mIntersections = new ArrayList(); + + /// + /// density of dark pixels, in persentage + /// + public List mDensityX = new ArrayList(); // indexed by X + public List mDensityY = new ArrayList(); // indexed by Y + + /// + /// the number of segments with dark pixels + /// + public List mSegmentX = new ArrayList(); // indexed by X + public List mSegmentY = new ArrayList(); // indexed by Y + + /// + /// Constructor + /// + /// + public MatrixFeature2(MatrixBase matrix) + { + if (matrix.getDimensionX() <= 3 || matrix.getDimensionY() <= 3) + return; + generateFeature(matrix); + } + + public boolean isBlank() + { + return mSegmentX.size() == 0 || mSegmentY.size() == 0 + || mDensityX.size() == 0 || mDensityY.size() == 0 + || mIntersections.size() == 0; + } + + public int getFeatureValue() + { + int maxSegmentX = MatrixUtil.getMax(mSegmentX); + int maxSegmentY = MatrixUtil.getMax(mSegmentY); + int maxDensityX = MatrixUtil.getMax(mDensityX); + + if (mHasAxisX) + { + if (maxSegmentY == 1) + return 1; + else + return 4; + } + switch (mHoleCnt) + { + case 0: + if (mLeftPitCnt == 0) + { + if (mRightPitCnt == 1) + { + return 7; + } + if (mRightPitCnt == 2) + return 3; + } + if (mLeftPitCnt == 1) + { + if (mRightPitCnt == 1) + { + if (mFirstPitPos == 1) + return 5; + else + return 2; + } + } + break; + case 1: + if (mLeftPitCnt == 1) + { + if (mRightPitCnt == 2) + { + if (maxDensityX >= 99) + return 4; + else + return 9; + } + else + return 4; + } + else if (mLeftPitCnt == 2) + return 6; + break; + case 2: + return 8; + + } + return 0; + } + + private void generateFeature(MatrixBase matrix) + { + mAxisX = matrix.getDimensionX() / 2; + generateIntersections(matrix); + generateDensityAndSegment(matrix); + checkPitAndHole(matrix); + } + + private void generateDensityAndSegment(MatrixBase matrix) + { + // generate density and segment-count on X-direction + for (int x = 0; x < matrix.getDimensionX(); x++) + { + int preValue = ThresholdUtil.SHALLOW_VALUE; + int darkCount = 0; + int segmentCount = 0; + for (int y = 0; y < matrix.getDimensionY(); y++) + { + int value = matrix.getValue(x, y); + if (value <= ThresholdUtil.DARK_THRESHOLD) + { + darkCount++; + if (preValue == ThresholdUtil.SHALLOW_VALUE) + { + segmentCount++; + } + preValue = ThresholdUtil.DARK_VALUE; + } + else + { + preValue = ThresholdUtil.SHALLOW_VALUE; + } + } + int density = darkCount * 100 / matrix.getDimensionY(); + if (density > 0) + { + mDensityX.add(density); + } + if (segmentCount > 0) + { + mSegmentX.add(segmentCount); + } + } + + // generate density and segment-count on Y-direction + for (int y = 0; y < matrix.getDimensionY(); y++) + { + int preValue = ThresholdUtil.SHALLOW_VALUE; + int darkCount = 0; + int segmentCount = 0; + for (int x = 0; x < matrix.getDimensionX(); x++) + { + int value = matrix.getValue(x, y); + if (value <= ThresholdUtil.DARK_THRESHOLD) + { + darkCount++; + if (preValue == ThresholdUtil.SHALLOW_VALUE) + { + segmentCount++; + } + preValue = ThresholdUtil.DARK_VALUE; + } + else + { + preValue = ThresholdUtil.SHALLOW_VALUE; + } + } + int density = darkCount * 100 / matrix.getDimensionX(); + if (density > 0) + { + mDensityY.add(density); + } + if (segmentCount > 0) + { + mSegmentY.add(segmentCount); + } + } + } + + private void generateIntersections(MatrixBase matrix) + { + // get intersections on x-axis + int preValue = ThresholdUtil.SHALLOW_VALUE; + for (int y = 0; y < matrix.getDimensionY(); y++) + { + int value = matrix.getValue(mAxisX, y); + if (value <= ThresholdUtil.DARK_THRESHOLD && preValue == ThresholdUtil.SHALLOW_VALUE) + { + mIntersections.add(y); + preValue = ThresholdUtil.DARK_VALUE; + } + else if (value > ThresholdUtil.DARK_THRESHOLD && preValue == ThresholdUtil.DARK_VALUE) + { + mIntersections.add(y); + preValue = ThresholdUtil.SHALLOW_VALUE; + } + } + if (preValue == ThresholdUtil.DARK_VALUE && mIntersections.size() <= 2) + { + mHasAxisX = true; + } + } + + private void checkPitAndHole(MatrixBase matrix) + { + for (int i = 1; i + 1 < mIntersections.size(); i += 2) + { + boolean leftPit = false, rightPit = false; + if (hasLeftPit(mIntersections.get(i), mIntersections.get(i+1), matrix)) + { + leftPit = true; + mLeftPitCnt++; + } + if (hasRightPit(mIntersections.get(i), mIntersections.get(i+1), matrix)) + { + rightPit = true; + mRightPitCnt++; + } + if (leftPit && rightPit) + { + mHoleCnt++; + } + if (mFirstPitPos==0) + { + if (leftPit) + mFirstPitPos = 1; + else if (rightPit) + mFirstPitPos = 2; + } + } + } + + private boolean hasLeftPit(int y1, int y2, MatrixBase matrix) + { + for (int y = y1; y < y2; y++) + { + int x = mAxisX; + for (; x >= 0; x--) + { + int value = matrix.getValue(x, y); + if (value <= ThresholdUtil.DARK_THRESHOLD) + { + break; + } + } + if (x <= 0) + { + return false; + } + } + return true; + } + + private boolean hasRightPit(int y1, int y2, MatrixBase matrix) + { + for (int y = y1; y < y2; y++) + { + int x = mAxisX; + for (; x < matrix.getDimensionX(); x++) + { + int value = matrix.getValue(x, y); + if (value <= ThresholdUtil.DARK_THRESHOLD) + { + break; + } + } + if (x >= matrix.getDimensionX()) + { + return false; + } + } + return true; + } + +} diff --git a/app/src/main/java/master/sudoku/ocr/util/MatrixUtil.java b/app/src/main/java/master/sudoku/ocr/util/MatrixUtil.java new file mode 100644 index 0000000..d179f76 --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/util/MatrixUtil.java @@ -0,0 +1,159 @@ +/** + * + */ +package master.sudoku.ocr.util; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import master.sudoku.ocr.matrix.BoundaryMatrix; +import master.sudoku.ocr.matrix.ImageMatrix; + +/** + * @author dannyzha + * + */ +public class MatrixUtil +{ + public static BoundaryMatrix detectBoundary(ImageMatrix imgMatrix) + { + BoundaryMatrix result = new BoundaryMatrix(imgMatrix.getDimensionX(), imgMatrix.getDimensionY()); + int left = findBoundary(imgMatrix, true, true); + int top = findBoundary(imgMatrix, false, true); + int right = findBoundary(imgMatrix, true, false); + int bottom = findBoundary(imgMatrix, false, false); + + result.setLeft(left); + result.setRight(right); + result.setTop(top); + result.setBottom(bottom); + + result.generateBoundarys(); + + List xList = getInnerBoundarys(imgMatrix, left, right, top, bottom, true); + List yList = getInnerBoundarys(imgMatrix, left, right, top, bottom, false); + result.justifyBoundaries(xList, yList); + return result; + } + + private static int findBoundary(ImageMatrix imgMatrix, boolean isHorizontal, boolean isAscend) { + int limit = isHorizontal ? imgMatrix.getDimensionX() : imgMatrix.getDimensionY(); + int sampleLimit = isHorizontal ? imgMatrix.getDimensionY() : imgMatrix.getDimensionX(); + int sampleSize = (int)(sampleLimit * 0.6); + for (int i = 0; i < limit; i++) + { + int foreColorCount = 0; + int idx = isAscend ? i : (limit - i -1); + for (int sampleIdx = 0; sampleIdx < sampleLimit; sampleIdx++) + { + int c = isHorizontal ? imgMatrix.getColor(idx, sampleIdx) : imgMatrix.getColor(sampleIdx, idx); + if (!ThresholdUtil.almostSameColor(ThresholdUtil.BG_COLOR, c)) + { + foreColorCount++; + } + if (foreColorCount > sampleSize) { + break; + } + } + if (foreColorCount > sampleSize) { + return idx; + } + } + return 0; + } + + private static List getInnerBoundarys(ImageMatrix imgMatrix, int left, int right, int top, int bottom, boolean isHorizontal) { + int boundaryTolerance = 5; + int idx1Lower = (isHorizontal ? left : top) + boundaryTolerance; + int idx1Upper = (isHorizontal ? right : bottom) - boundaryTolerance; + int idx2Lower = isHorizontal ? top : left; + int idx2Upper = isHorizontal ? bottom : right; + int foreCountLimit = (int)((idx2Upper - idx2Lower) * 0.8); + List result = new ArrayList(); + int lastIdx = idx1Lower; + for (int idx1 = idx1Lower; idx1 < idx1Upper; idx1++) { + int foreColorCount = 0; + for (int idx2 = idx2Lower; idx2 < idx2Upper; idx2++) { + int c = isHorizontal ? imgMatrix.getColor(idx1, idx2) : imgMatrix.getColor(idx2, idx1); + if (!ThresholdUtil.almostSameColor(ThresholdUtil.BG_COLOR, c)) + { + foreColorCount++; + } + } + if (foreColorCount > foreCountLimit && (idx1 - lastIdx) > boundaryTolerance) { + result.add(idx1); + lastIdx = idx1; + } + } + return result; + } + +// public static float calculateVariance(List samples) +// { +// float sumH = 0, sumS = 0, sumV = 0; +// float aveH = 0, aveS = 0, aveV = 0; +// for (int i = 0; i < samples.size(); i++) +// { +// sumH += samples.get(i) +// sumS += samples[i].GetSaturation(); +// sumV += samples[i].GetBrightness(); +// } +// aveH = sumH / samples.Count; +// aveS = sumS / samples.Count; +// aveV = sumV / samples.Count; +// sumH = sumS = sumV = 0; +// for (int i = 0; i < samples.Count; i++) +// { +// sumH += (samples[i].GetHue() - aveH) * (samples[i].GetHue() - aveH); +// sumS += (samples[i].GetSaturation() - aveS) * (samples[i].GetSaturation() - aveS); +// sumV += (samples[i].GetBrightness() - aveV) * (samples[i].GetBrightness() - aveV); +// } +// float resultH = sumH / samples.Count; +// float resultS = sumS / samples.Count; +// float resultV = sumV / samples.Count; +// +// return resultH + resultS + resultV; +// } + + public static int calculateVariance(List samples) + { + int sum = 0; + int ave = 0; + for (int i = 0; i < samples.size(); i++) + { + sum += samples.get(i); + } + ave = sum / samples.size(); + sum = 0; + for (int i = 0; i < samples.size(); i++) + { + sum += (samples.get(i) - ave) * (samples.get(i) - ave); + } + int result = sum / samples.size(); + return result / ave; + } + + public static int getMax(List values) { + int result = Integer.MIN_VALUE; + for(int i=0; i intersect(HashSet values1, HashSet values2) { + List result = new LinkedList(); + for(Iterator it = values1.iterator(); it.hasNext();) { + Integer value = it.next(); + if(values2.contains(value)) { + result.add(value); + } + } + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/master/sudoku/ocr/util/NeuralNetwork.java b/app/src/main/java/master/sudoku/ocr/util/NeuralNetwork.java new file mode 100644 index 0000000..a69e34f --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/util/NeuralNetwork.java @@ -0,0 +1,476 @@ +package master.sudoku.ocr.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Scanner; + +// MAS NEURONAS -> MENOR LEARNING RATE + +public class NeuralNetwork { + + private int hiddenSize, inputSize, outputSize, iters; + private double[][] weightsItoH; + private double[][] weightsHtoO; + private double[] ah; + private double[] ai; + private double[] ao; + private double LEARNING_RATE; +// private static final double E = 0.001; + + /** + * Creates a MLPNN with specified input layer size, hidden layer size and an + * output layers size of 1. This neural network is trained via + * backpropagation. + * + * @param learningRate + * Value of the learning rate to be used in backpropagation. + * @param inputSize + * Size of the input layer. + * @param hiddenSize + * Size of the hidden layer. + * @param outputSize + * Size of the hidden layer. + */ + public NeuralNetwork(double learningRate, int inputSize, int hiddenSize, + int outputSize) { + defaultInit(learningRate, inputSize, hiddenSize, outputSize); + } + + /** + * Creates a MLPNN using the LR, layer sizes and weights specified in a + * file. + * + * @param filename + * Name of the file where the LR, layer. sizes and weights are + * saved. + * @param defLr + * Default learning rate to use if an error occurs. + * @param defInSize + * Default input layer size to use if an error occurs. + * @param defHiSize + * Default hidden layer size to use if an error occurs. + * @param defOutSize + * Default output layer size to use if an error occurs. + * @throws FileNotFoundException + * Thrown if the file is not found. + */ + public NeuralNetwork(String filename, double defLr, int defInSize, + int defHiSize, int defOutSize) throws FileNotFoundException { + File file = new File(filename); + Scanner in = new Scanner(file); + try { + this.LEARNING_RATE = in.nextDouble(); + this.inputSize = in.nextInt(); + this.hiddenSize = in.nextInt(); + this.outputSize = in.nextInt(); + } catch (Exception e) { + in.close(); + defaultInit(defLr, defInSize, defHiSize, defOutSize); + return; + } + init(); + loadWeights(in, this.LEARNING_RATE, this.inputSize, this.hiddenSize, + this.outputSize); + in.close(); + } + + /** + * Initialize attributes. + */ + private void init() { + this.weightsItoH = new double[this.inputSize][this.hiddenSize]; + this.weightsHtoO = new double[this.hiddenSize][this.outputSize]; + this.ai = new double[this.inputSize]; + this.ah = new double[this.hiddenSize]; + this.ao = new double[this.outputSize]; + ah[this.hiddenSize - 1] = 1.0; // Bias units + ai[this.inputSize - 1] = 1.0; + iters = 0; + } + + /** + * Default attributes initialization. + * + * @param learningRate + * Value of the learning rate to be used in backpropagation. + * @param inputSize + * Size of the input layer. + * @param hiddenSize + * Size of the hidden layer. + * @param outputSize + * Size of the output layer. + */ + private void defaultInit(double learningRate, int inputSize, + int hiddenSize, int outputSize) { + this.LEARNING_RATE = learningRate; + this.inputSize = inputSize + 1; + this.hiddenSize = hiddenSize + 1; + this.outputSize = outputSize; + init(); + randomizeWeights(); + } + + /** + * Use this method to load the weights from a file. Expected file format: + * Learning rate Input size Hidden size. All weights from + * input layer to hidden layer. All weights from hidden layer to output + * layer. If the file format is wrong or its data is wrong, the weights are + * randomized. + * + * @param filename + * The name of the file to be laoded. + * @throws FileNotFoundException + * Thrown when the file is not found. + */ + public void loadWeights(String filename) throws Exception { + Scanner in = new Scanner(new File(filename)); + double lr = in.nextDouble(); + int inSize = in.nextInt(); + int hiSize = in.nextInt(); + int outSize = in.nextInt(); + loadWeights(in, lr, inSize, hiSize, outSize); + in.close(); + } + + public void loadWeights(InputStream is) throws Exception { +// BufferedReader br = new BufferedReader(new InputStreamReader(is)); +// String line = br.readLine(); +// String[] header = line.split(" "); +// double lr = Double.parseDouble(header[0]); +// int inSize = Integer.parseInt(header[1]); +// int hiSize = Integer.parseInt(header[2]); +// int outSize = Integer.parseInt(header[3]); +// +// loadWeights(br, lr, inSize, hiSize, outSize); +// br.close(); + Scanner in = new Scanner(is); + double lr = in.nextDouble(); + int inSize = in.nextInt(); + int hiSize = in.nextInt(); + int outSize = in.nextInt(); + loadWeights(in, lr, inSize, hiSize, outSize); + in.close(); + } + + /** + * Loads weights. + * + * @param in + * Scanner of file where weights are contained. + * @param lr + * Learning rate. + * @param inSize + * Input layer size. + * @param hiSize + * Hidden layer size. + * @param outSize + * Output layer size. + */ + private void loadWeights(Scanner in, double lr, int inSize, int hiSize, + int outSize) { + if (lr != LEARNING_RATE || inputSize != inSize || hiSize != hiddenSize + || outSize != outputSize) { + randomizeWeights(); + return; + } + for (int i = 0; i < inputSize; i++) + for (int j = 0; j < hiddenSize; j++) + weightsItoH[i][j] = in.nextDouble(); + for (int j = 0; j < hiddenSize; j++) + for (int k = 0; k < outputSize; k++) + weightsHtoO[j][k] = in.nextDouble(); + } + + private void loadWeights(BufferedReader br, double lr, int inSize, int hiSize, + int outSize) throws Exception { + if (lr != LEARNING_RATE || inputSize != inSize || hiSize != hiddenSize + || outSize != outputSize) { + randomizeWeights(); + return; + } + for (int i = 0; i < inputSize; i++) { + for (int j = 0; j < hiddenSize; j++) { + String line = br.readLine(); + weightsItoH[i][j] = Double.parseDouble(line); + } + } + for (int j = 0; j < hiddenSize; j++) { + for (int k = 0; k < outputSize; k++) { + String line = br.readLine(); + weightsHtoO[j][k] = Double.parseDouble(line); + } + } + } + + /** + * Use this method to load the weights from a file. Output file format: + * Learning rate Input size Hidden size All weights from + * input layer to hidden layer. All weights from hidden layer to output + * layer. + * + * @param filename + * Name of the file where the weights are going to be saved. + * @throws IOException + * Thrown if an I/O error occurs. + */ + public void saveWeights(String filename) throws IOException { + FileWriter f = new FileWriter(new File(filename)); + f.write(LEARNING_RATE + " " + inputSize + " " + hiddenSize + " " + + outputSize + "\n"); + for (int i = 0; i < inputSize; i++) + for (int j = 0; j < hiddenSize; j++) + f.write(String.format("%f\n", weightsItoH[i][j])); + for (int j = 0; j < hiddenSize; j++) + for (int k = 0; k < outputSize; k++) + f.write(String.format("%f\n", weightsHtoO[j][k])); + f.close(); + } + + /** + * Use this method to set all weights to random. + */ + public void randomizeWeights() { + for (int i = 0; i < inputSize; i++) + for (int j = 0; j < hiddenSize; j++) + weightsItoH[i][j] = rand(-1.0, 1.0); + for (int j = 0; j < hiddenSize; j++) + for (int k = 0; k < outputSize; k++) + weightsHtoO[j][k] = rand(-1.0, 1.0); + } + + /** + * Sigmoid function that is used (tanh). + * + * @param x + * Input value. + * @return logistic(x). + */ + private double sigmoid(double x) { + return 1. / (1 + Math.exp(-x)); + // return Math.tanh(x); + } + + /** + * Derivative of sigmoid function. + * + * @param y + * An activation value. + * @return y * (1 - y) + */ + private double dSigmoid(double y) { + return y * (1 - y); + // return 1 - y*y; + } + + /** + * Return a random number between a and b. + * + * @param a + * Lower bound. + * @param b + * Upper bound. + * @return Random number in range [a,b). + */ + private double rand(double a, double b) { + return a + (b - a) * Math.random(); + } + + /** + * Perform forward propagation through the NN. + * + * @param inputs + * Activation values for input layer. + * @return Activation value of output layer. + */ + private void forwardPropagation(int[] inputs) { + // Compute activations for input layer neurons + for (int i = 0; i < inputSize - 1; i++) + ai[i] = inputs[i]; + + // Compute activations for hidden layer neurons + for (int j = 0; j < hiddenSize - 1; j++) { + ah[j] = 0.0; + for (int i = 0; i < inputSize; i++) + ah[j] += weightsItoH[i][j] * ai[i]; + ah[j] = sigmoid(ah[j]); + } + + // Compute activations for output layer neurons + for (int k = 0; k < outputSize; k++) { + ao[k] = 0.0; + for (int j = 0; j < hiddenSize; j++) + ao[k] += ah[j] * weightsHtoO[j][k]; + ao[k] = sigmoid(ao[k]); + } + } + + /** + * Perform backpropagation algorithm to update the weights and train the NN. + * + */ + private void backPropagation(double[] errors) { + // Compute delta for output layer neuron + double[] deltak = new double[outputSize]; + for (int k = 0; k < outputSize; k++) + deltak[k] = dSigmoid(ao[k]) * errors[k]; + + // Compute delta for hidden layer neurons + double[] deltaj = new double[hiddenSize]; + for (int j = 0; j < hiddenSize; j++) + for (int k = 0; k < outputSize; k++) + deltaj[j] += dSigmoid(ah[j]) * deltak[k] * weightsHtoO[j][k]; + + // Update weights from input to hidden layer + for (int i = 0; i < inputSize; i++) + for (int j = 0; j < hiddenSize; j++) + weightsItoH[i][j] += LEARNING_RATE * deltaj[j] * ai[i]; + + // Update weights from hidden to output layer + for (int j = 0; j < hiddenSize; j++) + for (int k = 0; k < outputSize; k++) + weightsHtoO[j][k] += LEARNING_RATE * deltak[k] * ah[j]; + } + + /** + * Train the neural network for iterLimit iterations or until for each + * pattern input abs(expected - output) <= 0.01 + * + * @param inputs + * List of all input patterns to be used. + * @param outputs + * Expected output for each input pattern. + * @param iterLimit + * Limit of iterations. + */ + public void train(int[][] inputs, int[][] outputs, int iterLimit) { + for (int c = 0; c < iterLimit; c++, iters++) + for (int i = 0; i < inputs.length; i++) { + forwardPropagation(inputs[i]); + double[] errors = new double[outputSize]; + for (int k = 0; k < outputSize; k++) + errors[k] = outputs[i][k] - ao[k]; + backPropagation(errors); + } + } + + /** + * Run the neural network with pattern as input. + * + * @param pattern + * Input pattern. + * @return Neuron fired. + */ + public int eval(int[] pattern) { + forwardPropagation(pattern); + return interpret(); + } + + /** + * Maximum activation value. + * + * @return Neuron index. + */ + private int interpret() { + if (outputSize == 1) + return (ao[0] < 0.5) ? 0 : 1; + int index = 0; + double max = ao[0]; + for (int k = 1; k < outputSize; k++) + if (ao[k] > max) { + max = ao[k]; + index = k; + } + return index; + } + + /** + * Find the neuron with the maximum activation value. + * + * @return Neuron index. + */ + private int maxIndex(int[] pattern) { + int index = 0; + double max = pattern[0]; + for (int k = 1; k < outputSize; k++) + if (pattern[k] > max) { + max = pattern[k]; + index = k; + } + return index; + } + + /** + * Test the NN with specified inputs and expected outputs. + * + * @param inputs + * List of all input patterns to be used. + * @param outputs + * Expected output for each input pattern. + * @param print + * If true, print the expected output and the output and at the + * end, print the success rate and the mean square error. + * @return Array where index 0 = success rate, and index 1 = mean square + * error. + */ + public double[] test(int[][] inputs, int[][] outputs, boolean print) { + double[] r = { 0.0, 0.0 }; + System.out.println("Iterations: " + iters); + for (int i = 0; i < inputs.length; i++) { + int x = eval(inputs[i]); + int expected = maxIndex(outputs[i]); + if (print) + System.out.println("Expected: " + expected + " " + + Arrays.toString(outputs[i]) + " Result: " + x + " " + + Arrays.toString(ao)); + for (int k = 0; k < outputSize; k++) + r[1] += (outputs[i][k] - ao[k]) * (outputs[i][k] - ao[k]); + if (expected == x) + r[0] += 1.0 / inputs.length; + r[1] += (expected - x) * (expected - x) / (double) inputs.length; + } + r[1] *= 0.5; + if (print) { + System.out.println("Success rate: " + r[0] * 100 + "%"); + System.out.println("Squared Error: " + String.format("%.8f", r[1])); + // ERROR = 0.5 * sum(norm(expected - output)**2) + } + return r; + } + + /** + * Get the iterations that have been made to train the NN. + * + * @return Number of iterations performed + */ + public int iters() { + return iters; + } + + /** + * Unit test. Train the NN to perform XOR operations. + * + * @param args + * None expected + */ + public static void main(String[] args) { + int[][] inputs = { { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, 1 } }; + int[][] outputs = { { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 1, 0 }, + { 0, 0, 0, 1 } }; + NeuralNetwork nn = new NeuralNetwork(0.3, 2, 5, 4); + nn.train(inputs, outputs, 10000); + nn.test(inputs, outputs, true); + } + + public String toString() { + String s = "Weights I->H = " + Arrays.deepToString(weightsItoH); + s += "\nWeights H->O = " + Arrays.toString(weightsHtoO); + s += "\nLearning Rate = " + LEARNING_RATE; + return s; + } + +} diff --git a/app/src/main/java/master/sudoku/ocr/util/ThresholdUtil.java b/app/src/main/java/master/sudoku/ocr/util/ThresholdUtil.java new file mode 100644 index 0000000..f9ae44e --- /dev/null +++ b/app/src/main/java/master/sudoku/ocr/util/ThresholdUtil.java @@ -0,0 +1,33 @@ +package master.sudoku.ocr.util; + +import android.graphics.Color; + +public class ThresholdUtil +{ + public static int DARK_VALUE = 1; + public static int SHALLOW_VALUE = 255; + + public static int DARK_THRESHOLD = 128; + + public static int BG_COLOR = Color.WHITE; + + public static boolean almostSameColor(int c1, int c2) + { + int tolerance = 10; + return (Math.abs(Color.alpha(c1) - Color.alpha(c2)) < tolerance + && Math.abs(Color.red(c1) - Color.red(c2)) < tolerance + && Math.abs(Color.green(c1) - Color.green(c2)) < tolerance + && Math.abs(Color.blue(c1) - Color.blue(c2)) < tolerance); + } + + public static int GetDarkValue(int color) + { + //return (Color.red(color) + Color.green(color) + Color.blue(color)) / 3; + return (int)(Color.red(color) * 0.299 + Color.green((color)) * 0.587 + Color.blue(color) * 0.114); + } + + public static boolean IsDark(int color) + { + return !almostSameColor(color, BG_COLOR); + } +} diff --git a/app/src/main/java/master/sudoku/shapes/NumberCell.java b/app/src/main/java/master/sudoku/shapes/NumberCell.java new file mode 100644 index 0000000..7c3f643 --- /dev/null +++ b/app/src/main/java/master/sudoku/shapes/NumberCell.java @@ -0,0 +1,132 @@ +/** + * + */ +package master.sudoku.shapes; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; + +import master.sudoku.config.DeviceConfig; + +/** + * @author dannyzha + * + */ +public class NumberCell extends ShapeBase { + + private int mNumber = 0; + + private String mText = ""; + + private boolean mReadonly = false; + + private boolean mErrorFlag = false; + + private boolean mHighlighted = false; + + private static int NORMAL_COLOR = Color.BLACK; + private static int NORMAL_BG_COLOR = Color.WHITE; + private static int HIGHLIGHT_COLOR = Color.YELLOW; + private static int HIGHLIGHT_BG_COLOR = Color.CYAN; + private static int READONLY_COLOR = Color.WHITE; + private static int READONLY_BG_COLOR = Color.GRAY; + private static int ERROR_COLOR = Color.RED; + + private static Typeface NORMAL_FONT = Typeface.create("Helvetica", Typeface.BOLD); + + public int getNumber() { + return mNumber; + } + + public void setNumber(int mNumber) { + this.mNumber = mNumber; + } + + public boolean isReadonly() { + return mReadonly; + } + + public String getText() { + return mText; + } + + public void setText(String text) { + mText = text; + } + + public void setReadonly(boolean readonly) { + this.mReadonly = readonly; + } + + public void setErrorFlag(boolean errorFlag) { + this.mErrorFlag = errorFlag; + } + + public boolean isHightlighted() { + return mHighlighted; + } + + public void setHightlighted(boolean hightlighted) { + this.mHighlighted = hightlighted; + } + + /* (non-Javadoc) + * @see com.skyway.pandora.sudoku.shapes.ShapeBase#paint(javax.microedition.lcdui.Graphics) + */// canvas.clipRect(mBound); + + public void paint(Canvas canvas) { + if(mNumber >= 0 && mBound != null) { + this.prepareColor(); + mPaint.setColor(mBackgroundColor); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawRect(mBound, mPaint); + if(mText != null && mText.length() > 0) { + paintText(mText, canvas); + } else if(mNumber > 0) { + mText = String.valueOf(mNumber); + paintText(String.valueOf(mNumber), canvas); + } + } + } + + private void paintText(String text, Canvas canvas) { + mPaint.setColor(mColor); + mPaint.setTypeface(NORMAL_FONT); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setTextSize(DeviceConfig.mFontSize); + +// int strWidth = mPaint.measureText(mText); + Rect textBounds = new Rect(); + mPaint.getTextBounds(text, 0, text.length(), textBounds); + int strWidth = textBounds.width(); + int strHeight = textBounds.height(); + + int x = mBound.left + (mBound.width() - strWidth) / 2; + int y = mBound.top + (mBound.height() + strHeight) / 2; + + canvas.drawText(text, x, y, mPaint); + } + + private void prepareColor() { + mColor = NORMAL_COLOR; + mBackgroundColor = NORMAL_BG_COLOR; + + // error color has higher priority + if(mErrorFlag) { + this.mColor = ERROR_COLOR; + } + else if(mReadonly) { + this.mColor = READONLY_COLOR; + this.mBackgroundColor = READONLY_BG_COLOR; + } + + if(mHighlighted) { + this.mColor = HIGHLIGHT_COLOR; + this.mBackgroundColor = HIGHLIGHT_BG_COLOR; + } + } + +} diff --git a/app/src/main/java/master/sudoku/shapes/NumberGrid.java b/app/src/main/java/master/sudoku/shapes/NumberGrid.java new file mode 100644 index 0000000..f9f86df --- /dev/null +++ b/app/src/main/java/master/sudoku/shapes/NumberGrid.java @@ -0,0 +1,244 @@ +/** + * + */ +package master.sudoku.shapes; + +import android.graphics.Canvas; +import android.graphics.Point; +import android.graphics.Rect; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import master.sudoku.logs.Logger; +import master.sudoku.model.Index; + +/** + * @author dannyzha + * + */ +public class NumberGrid extends ShapeBase { + + protected int mCellWidth; + protected int mCellHeight; + protected int mDrawTop; + protected int mDrawBottom; + protected int mDrawLeft; + protected int mDrawRight; + + protected int mDimensionX; + protected int mDimensionY; + + private int mSelectedI; + private int mSelectedJ; + private NumberCell mSelectedCell; + + private boolean mHasError = false; + + protected Hashtable mCellTable = new Hashtable(); + protected Vector mLineList = new Vector(); + + /** + * Constructor + */ + public NumberGrid(int dimensionX, int dimensionY) { + this.mDimensionX = dimensionX; + this.mDimensionY = dimensionY; + + for(int i=0; i e = mCellTable.keys(); e.hasMoreElements(); ) { + ShapeBase shape = (ShapeBase)mCellTable.get(e.nextElement()); + shape.paint(canvas); + } + for(int i=0; i e = mCellTable.keys(); e.hasMoreElements(); ) { + NumberCell cell = mCellTable.get(e.nextElement()); + cell.setErrorFlag(false); + } + } + + public boolean hasError() { + return mHasError; + } + + public void selectByPixel(int pixelX, int pixelY) { + for(Enumeration e = mCellTable.keys(); e.hasMoreElements(); ) { + Index idx = (Index)e.nextElement(); + NumberCell cell = mCellTable.get(idx); + if(cell.getBound().contains(pixelX, pixelY)) { + if(mSelectedCell != null) { + mSelectedCell.setHightlighted(false); + } + if(Logger.ON) { + Logger.getLogger().debug("ready to set selected cell"); + } + cell.setHightlighted(true); + mSelectedCell = cell; + mSelectedI = idx.getI(); + mSelectedJ = idx.getJ(); + break; + } + } + } + + public void selectByIndex(int i, int j) { + if(mSelectedCell != null) { + mSelectedCell.setHightlighted(false); + } + Index idx = new Index(i, j); + NumberCell cell = mCellTable.get(idx); + cell.setHightlighted(true); + mSelectedCell = cell; + mSelectedI = i; + mSelectedJ = j; + } + + public void clearSelection() { + if(mSelectedCell != null) { + mSelectedCell.setHightlighted(false); + } + mSelectedCell = null; + mSelectedI = 0; + mSelectedJ = 0; + } + + public int getSelectedI() { + return mSelectedI; + } + + public int getSelectedJ() { + return mSelectedJ; + } + + public void setSelectedCellNumber(int number) { + if(mSelectedCell != null) { + mSelectedCell.setNumber(number); + } + } + + public int getSelectedCellNumber() { + if(mSelectedCell != null) { + return mSelectedCell.getNumber(); + } + return 0; + } + + private void centralizeGrid(Rect rect) { + // centralize the grid + int totalWidth = mCellWidth * mDimensionX; + int totalHeight = mCellHeight * mDimensionY; + + mDrawTop = rect.top; + mDrawLeft = rect.left; + if(rect.width() > totalWidth) { + mDrawTop = mDrawTop + (rect.height() - totalHeight)/2; + } + if(rect.height() > totalHeight) { + mDrawLeft = mDrawLeft + (rect.width() - totalWidth)/2; + } + mDrawRight = mDrawLeft + totalWidth; + mDrawBottom = mDrawTop + totalHeight; + } + + protected void initLines(Rect rect) { + mLineList.removeAllElements(); + int x = mDrawLeft; + int y = mDrawTop; + for(int i=0; i<=mDimensionX; i++) { + // create vertical lines + Point start = new Point(x, mDrawTop); + Point end = new Point(x, mDrawBottom); + ThicknessLine vLine = new ThicknessLine(start, end); + + if(i % mDimensionX == 0) { + vLine.setThickness(3); + } + this.mLineList.addElement(vLine); + x += mCellWidth; + } + + for(int j=0; j<=mDimensionY; j++) { + // create horizontal lines + Point start = new Point(mDrawLeft, y); + Point end = new Point(mDrawRight, y); + ThicknessLine hLine = new ThicknessLine(start, end); + + if(j % mDimensionY == 0) { + hLine.setThickness(3); + } + this.mLineList.addElement(hLine); + x += mCellWidth; + y += mCellHeight; + } + } + + protected void initGrid() { + for(int i=0; i idxList = getSquareIndexList(i, j); + for(int k=0; k getSquareIndexList(int i, int j) { + Vector result = new Vector(); + int iStart = i / 3 * 3; + int jStart = j / 3 * 3; + for(i=iStart; i 1) { +// int xIncrement = 0; +// int yIncrement = 0; +// if(mSlopeType == SLOPE_TYPE_HORIZONTAL) { +// yIncrement = 1; +// } +// else if(mSlopeType == SLOPE_TYPE_VERTICAL) { +// xIncrement = 1; +// } +// else if(mSlopeType == SLOPE_TYPE_DIAGONAL) { +// xIncrement = 1; +// yIncrement = 1; +// } +// int startX = mStart.getX(); +// int startY = mStart.getY(); +// int endX = mEnd.getX(); +// int endY = mEnd.getY(); +// for(int i=0; i mValueList; + + /** + * Constructor + * @param i + * @param j + */ + public CellValue(int i, int j) { + mIdx = new Index(i, j); + mValueList = new Vector(); + } + + /** + * Constructor + * @param idx + */ + public CellValue(Index idx) { + this.mIdx = idx; + mValueList = new Vector(); + } + + /** + * get a copy of the value set + * @return + */ + public Vector getValueList() { + Vector result = new Vector(); + for(int i=0; i valueList = this.getValueList(); + for(int i=0; i mGuessHistory; + private int mCurrentGuess; + + /** + * Constructor + */ + public Guess(CellValue cellValue, Sudoku bakModel) { + this.mCellValue = cellValue; + this.mBakModel = bakModel.copyResultModel(); + this.mGuessHistory = new Vector(); + } + + public boolean canGuess() { + return mGuessHistory.size() < mCellValue.getValueCount(); + } + + /** + * if able to guess, guess one value and fill in the guess model, otherwise do nothing + * @return true if able to guess, false if guess number is used out. + */ + public boolean guessOne() throws SolutionException { + if(mGuessHistory.size() >= mCellValue.getValueCount()) { + return false; + } + mGuessModel = mBakModel.copyResultModel(); + Vector valueList = mCellValue.getValueList(); + for(int m=0; m mCellValueList; + private SolutionStatus mStatus; + private boolean mDebug = true; + + private Stack mGuessStack; + private Vector mGuessHistory; + + // numbers for statistics + private int mCalculateRound = 0; + private int mRollbackRound = 0; + private int mGuessRound = 0; + + /** + * Constructor + * @param model + */ + public Solution(Sudoku model) { + this.mModel = model; + this.mCellValueList = new Vector(); + this.mStatus = SolutionStatus.Solving; + this.mGuessStack = new Stack(); + this.mGuessHistory = new Vector(); + } + + public void solve() throws SolutionException { + while(mModel.getBlankSize() > 0) { + mCalculateRound ++; + int oldBlankSize = mModel.getBlankSize(); + try { + this.buildCellValue(); + this.checkSingleValue(); + } catch(SolutionException ex) { + if(mDebug) { + System.err.println(ex.getMessage()); + System.err.println("ready to roll-back"); + } + rollBackAndGuessOtherValue(); + continue; + } + + if(oldBlankSize == mModel.getBlankSize()) { + addNewGuess(); + } + } + } + + public void buildCellValue() throws SolutionException { + mCellValueList.removeAllElements(); + for(int i=0; i valueList = new Vector(); + + for(int value=1; value<=9; value++) { + if(mModel.acceptValue(i, j, value)) { + valueList.addElement(Integer.valueOf(value)); + } else { + if(mDebug) { + System.out.println("for Cell["+i+","+j+"]:" + value + " not valid."); + } + } + } + + if(valueList.size() == 0) { + this.mStatus = SolutionStatus.Failed; + throw new SolutionException("Impossible to fill value in Cell["+i+","+j+"]"); + } + + if(mDebug) { + System.out.println("getting value size for Cell["+i+","+j+"]:" + valueList.size()); + } + CellValue result = new CellValue(i, j); + for(int k=0; k klass) { + if (AppUtil.hasGingerbread()) { + final StrictMode.ThreadPolicy.Builder threadPolicyBuilder = + new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog(); + + final StrictMode.VmPolicy.Builder vmPolicyBuilder = + new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog(); + if (AppUtil.hasHoneycomb()) { + threadPolicyBuilder.penaltyFlashScreen(); + if (klass != null) { + vmPolicyBuilder.setClassInstanceLimit(klass, 1); + } + } + + StrictMode.setThreadPolicy(threadPolicyBuilder.build()); + StrictMode.setVmPolicy(vmPolicyBuilder.build()); + } + } + + public static final boolean hasEclair() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR; + } + + public static final boolean hasCupcake() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE; + } + + public static boolean hasFroyo() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; + } + + public static boolean hasGingerbread() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; + } + + public static boolean hasHoneycomb() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + } + + public static boolean hasHoneycombMR1() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1; + } + + public static boolean hasJellyBean() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } + + public static boolean hasJellyBeanMR1() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + public static boolean hasJellyBeanMR2() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; + } + + public static boolean hasKitKat() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + /** + * Convert a String object to UTF-8 bytes array. + * + * @param s The String to be converted. + * @return UTF-8 encoded bytes array. + */ + public static byte[] toUtf8(String s) { + return encode(UTF_8, s); + } + + /** + * Build a String from UTF-8 bytes array. + * + * @param b The bytes array to be decoded. + * @return The byte relative String object. + */ + public static String fromUtf8(byte[] b) { + return decode(UTF_8, b); + } + + /** + * Convert a String object to ASCII bytes array. + * + * @param s The String to be converted. + * @return ASCII encoded bytes array. + */ + public static byte[] toAscii(String s) { + return encode(ASCII, s); + } + + /** + * Build a String from ASCII bytes array. + * + * @param b The bytes array to be decoded. + * @return The byte relative String object. + */ + public static String fromAscii(byte[] b) { + return decode(ASCII, b); + } + + private static byte[] encode(Charset charset, String s) { + if (s == null) { + return null; + } + final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s)); + final byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + return bytes; + } + + private static String decode(Charset charset, byte[] b) { + if (b == null) { + return null; + } + final CharBuffer buffer = charset.decode(ByteBuffer.wrap(b)); + return new String(buffer.array(), 0, buffer.length()); + } + + public static String getSmallHash(final String value) { + final MessageDigest sha; + try { + sha = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + sha.update(AppUtil.toUtf8(value)); + final int hash = getSmallHashFromSha1(sha.digest()); + return Integer.toString(hash); + } + + private static int getSmallHashFromSha1(byte[] sha1) { + final int offset = sha1[19] & 0xf; + return ((sha1[offset] & 0x7f) << 24) + | ((sha1[offset + 1] & 0xff) << 16) + | ((sha1[offset + 2] & 0xff) << 8) | ((sha1[offset + 3] & 0xff)); + } + + public static String getMd5(String value) { + final MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + return value; + } + md5.update(AppUtil.toUtf8(value)); + final StringBuilder sb = new StringBuilder(); + for (byte b : md5.digest()) { + if (b < 0) { + b += 256; + } + if (b < 16) { + b = 0; + } + sb.append(Integer.toHexString(b)); + } + + return sb.toString(); + } + + public static String encrypt(String source, String key) { + try { + DESKeySpec desKey = new DESKeySpec(key.getBytes()); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secureKey = keyFactory.generateSecret(desKey); + @SuppressLint("GetInstance") + Cipher cipher = Cipher.getInstance("DES"); + SecureRandom random = new SecureRandom(); + cipher.init(Cipher.ENCRYPT_MODE, secureKey, random); + final byte[] bytes = Base64.encode(cipher.doFinal(source.getBytes()), 0); + return new String(bytes); + } catch (Exception e) { + return ""; + } + } +} diff --git a/app/src/main/java/master/sudoku/utils/FileUtils.java b/app/src/main/java/master/sudoku/utils/FileUtils.java new file mode 100644 index 0000000..a35e164 --- /dev/null +++ b/app/src/main/java/master/sudoku/utils/FileUtils.java @@ -0,0 +1,232 @@ +package master.sudoku.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import master.sudoku.application.PuzzleMasterApp; + +public class FileUtils { + + private static final String APP_DIR = "PuzzleMaster"; + + private static final String CACHE_DIR = "Cache"; + + private static final String CRASH_DIR = "crash"; + + public static File getAppDir() { + File file = null; + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + file = new File(Environment.getExternalStorageDirectory(), APP_DIR); + } else { + file = new File(PuzzleMasterApp.getInstance().getCacheDir(), APP_DIR); + } + if (file != null && !file.exists()) { + file.mkdirs(); + } + return file; + } + + public static File getCacheDir() { + File file = new File(getAppDir(), CACHE_DIR); + if (!file.exists()) { + file.mkdirs(); + } + return file; + } + + @SuppressLint("NewApi") + public static String getFilePathFromUri(Uri uri, Context ctx) { + String result = uri.toString(); + if (uri.getScheme().compareTo("file") == 0) { // file:///开头的uri + result = result.replace("file://", ""); // 替换file:// + } else { + Cursor cursor = null; + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(ctx, uri)) { + String wholeID = DocumentsContract.getDocumentId(uri); + String id = wholeID.split(":")[1]; + String[] column = {MediaStore.Images.Media.DATA}; + String sel = MediaStore.Images.Media._ID + "=?"; + cursor = ctx.getContentResolver().query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + column, sel, new String[]{id}, null); + int columnIndex = cursor.getColumnIndex(column[0]); + if (cursor.moveToFirst()) { + result = cursor.getString(columnIndex); + } + } else { + String[] proj = {MediaStore.Images.Media.DATA}; + cursor = ctx.getContentResolver().query(uri, proj, null, + null, null); + int column_index = cursor + .getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + result = cursor.getString(column_index); + } + } catch(Exception ex) { + ex.printStackTrace(); + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + return result; + } + + public static String getCachePath() { + return getCacheDir().getAbsolutePath(); + } + + public static File getCrashDir() { + File file = new File(getAppDir(), CRASH_DIR); + if (!file.exists()) { + file.mkdirs(); + } + return file; + } + + public static void clearDir(File dir) { + if (dir == null || !dir.exists()) { + return; + } + try { + File[] files = dir.listFiles(); + for (int i = 0; i < files.length; i++) { + files[i].delete(); + } + } catch (Exception ignore) { + } + } + + public static void clearImgcache() { + File dir = getCacheDir(); + if (dir == null || !dir.exists()) { + return; + } + try { + File[] files = dir.listFiles(); + for (int i = 0; i < files.length; i++) { + files[i].delete(); + } + } catch (Exception ignore) { + } + } + + public static String getFileTextData(File file) { + if (file != null && file.exists()) { + InputStreamReader isReader = null; + BufferedReader reader = null; + try { + isReader = new InputStreamReader(new FileInputStream(file), + "utf-8"); + reader = new BufferedReader(isReader); + StringBuffer sb = new StringBuffer(""); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + sb.append("\n"); + } + String result = sb.toString(); + return result; + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + if (isReader != null) { + try { + isReader.close(); + } catch (IOException e) { + } + } + } + + } + return null; + } + + public static void saveFileTextData(File file, String data) { + if (file != null && data != null) { + if (file.exists()) { + file.delete(); + } + OutputStream out = null; + try { + out = new FileOutputStream(file); + out.write(data.getBytes("UTF-8")); + out.flush(); + } catch (Exception e) { + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + } + } + } + } + } + + /** + * Get the directory size except the crop images + */ + public static long getUnCropImgSize(File directory) { + long size = 0; + File[] fileList = directory.listFiles(); + for (File file : fileList) { + if (file.isDirectory()) { + size += getUnCropImgSize(file); + } else { + if (!file.getName().endsWith("crop.jpg")) { + size += file.length(); + } + } + } + return size; + } + + public static String Str2MD5(String sourceStr) { + String result = ""; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(sourceStr.getBytes()); + byte b[] = md.digest(); + int i; + StringBuffer buf = new StringBuffer(""); + for (int offset = 0; offset < b.length; offset++) { + i = b[offset]; + if (i < 0) { + i += 256; + } + if (i < 16) { + buf.append("0"); + } + buf.append(Integer.toHexString(i)); + } + result = buf.toString(); + } catch (NoSuchAlgorithmException e) { + } + return result; + } +} diff --git a/app/src/main/java/master/sudoku/utils/UiUtil.java b/app/src/main/java/master/sudoku/utils/UiUtil.java new file mode 100644 index 0000000..b29723b --- /dev/null +++ b/app/src/main/java/master/sudoku/utils/UiUtil.java @@ -0,0 +1,12 @@ +/** + * + */ +package master.sudoku.utils; + +/** + * @author dannyzha + * + */ +public final class UiUtil { + +} diff --git a/app/src/main/java/master/sudoku/views/MainGameView.java b/app/src/main/java/master/sudoku/views/MainGameView.java new file mode 100644 index 0000000..d96d2f9 --- /dev/null +++ b/app/src/main/java/master/sudoku/views/MainGameView.java @@ -0,0 +1,147 @@ +/** + * + */ +package master.sudoku.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; + +import master.sudoku.event.EventArgs; +import master.sudoku.event.EventListener; +import master.sudoku.logs.Logger; +import master.sudoku.model.Sudoku; +import master.sudoku.widgets.InputPanel; +import master.sudoku.widgets.MatrixGrid; + +/** + * @author dannyzha + * + */ +public class MainGameView extends ViewBase implements EventListener { + + + public final static int STYLE_PLAY = 0; + public final static int STYLE_LOAD = 1; + + protected int mStyle = STYLE_PLAY; + protected Sudoku mModel; + protected MatrixGrid mGrid; + protected InputPanel mInputPanel; + + /** + * Need this constructor to fix the "Error inflating class" error + * @param context + * @param attrs + */ + public MainGameView(Context context, AttributeSet attrs) { + super(context, attrs); + + mGrid = new MatrixGrid(this); + mInputPanel = new InputPanel(this); + mInputPanel.addEventListener(mGrid); + } + + public MainGameView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mGrid = new MatrixGrid(this); + mInputPanel = new InputPanel(this); + mInputPanel.addEventListener(mGrid); + } + + public void setModel(Sudoku model) { + this.mModel = model; + mGrid.setModel(this.mModel); + } + + public void editModel(boolean editing) { + mGrid.setIsEditing(editing); + } + + public void setBound(Rect bound) { + super.setBound(bound); + + int matrixW = bound.width(); + int matrixH = bound.height(); + if(matrixW > matrixH) { + matrixW = matrixH; + } else { + matrixH = matrixW; + } + Rect matrixRect = new Rect(bound.left, bound.top, bound.left + matrixW, bound.top + matrixH); + mGrid.setBound(matrixRect); + + if(Logger.ON) { + Logger.getLogger().debug("bound.getHeight is:" + bound.height()); + Logger.getLogger().debug("matrixH is:" + matrixH); + Logger.getLogger().debug("matrixW is:" + matrixW); + } + Rect inputRect = new Rect(matrixRect.left, matrixRect.bottom + 10, + matrixRect.left + matrixW, matrixRect.bottom + 10 + bound.height() - matrixH - 10); + mInputPanel.setBound(inputRect); + } + + public void setStyle(int style) { + mStyle = style; + mInputPanel.setInputOnly(style == STYLE_LOAD); + } + + public void paint(Canvas canvas) { + if(Logger.ON) { + Logger.getLogger().debug("paiting MainGameView"); + } + mGrid.paint(canvas); + mInputPanel.paint(canvas); + } + + public void onTap(int x, int y) { + boolean handled = false; + handled |= mGrid.onTap(x, y); + if(!handled) { + mInputPanel.onTap(x, y); + } + } + + public void onPointerPressed(int x, int y) { + boolean handled = false; + handled |= mGrid.onPointerPressed(x, y); + if(!handled) { + mInputPanel.onPointerPressed(x, y); + } + } + + public void onPointerReleased(int x, int y) { + boolean handled = false; + handled |= mGrid.onPointerReleased(x, y); + if(!handled) { + mInputPanel.onPointerReleased(x, y); + } + } + + public void onKeyEvent(int key) { + boolean handled = false; + handled |= mGrid.onKeyEvent(key); + if(!handled) { + mInputPanel.onKeyEvent(key); + } + } + +// public void invalidate() { +// +// } +// +// public void invalidate(Rect rect) { +//// mCanvas.repaint(rect.left, rect.top, rect.width(), rect.height()); +// } + +// public void repaint() { +//// mCanvas.repaint(); +// } + + public boolean handleEvent(EventArgs args) { + return false; + } + +} diff --git a/app/src/main/java/master/sudoku/views/SolveSudokuView.java b/app/src/main/java/master/sudoku/views/SolveSudokuView.java new file mode 100644 index 0000000..2ee302b --- /dev/null +++ b/app/src/main/java/master/sudoku/views/SolveSudokuView.java @@ -0,0 +1,80 @@ +/** + * + */ +package master.sudoku.views; + +import android.content.Context; +import android.util.AttributeSet; + +import master.sudoku.event.EventArgs; +import master.sudoku.exception.SolutionException; +import master.sudoku.model.Sudoku; +import master.sudoku.solve.Solution; + +/** + * @author dannyzha + * + */ +public class SolveSudokuView extends MainGameView { + + /** + * Need this constructor to fix the "Error inflating class" error + * @param context + * @param attrs + */ + public SolveSudokuView(Context context, AttributeSet attrs) { + super(context, attrs); + + mInputPanel.addEventListener(this); + mGrid.setIsEditing(true); + } + public SolveSudokuView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mInputPanel.addEventListener(this); + mGrid.setIsEditing(true); + } + + public boolean handleEvent(EventArgs args) { + switch(args.getEventType()) { + case EventArgs.INPUT_PANEL_SELECT: + Integer value = (Integer)args.getEventData(); + try { + if(value != null && value.intValue() == 0) { + Solution s = new Solution(mModel); + printModel(mModel, System.out); + s.solve(); + printModel(mModel, System.out); + mGrid.setIsEditing(false); + } + + } catch (SolutionException e) { + e.printStackTrace(); + } + this.invalidate(); + return true; + } + return false; + } + + private static void printModel(Sudoku model, java.io.PrintStream ps) { + for(int i=0; i<9; i++) { + if(i % 3 == 0) { + for(int j=0; j<9; j++) { + if(j % 3 == 0) { + ps.print('-'); + } + ps.print("---"); + } + ps.println(); + } + for(int j=0; j<9; j++) { + if(j % 3 == 0) { + ps.print('|'); + } + ps.print(" " + model.getValue(i, j) + " "); + } + ps.println('|'); + } + } +} diff --git a/app/src/main/java/master/sudoku/views/ViewBase.java b/app/src/main/java/master/sudoku/views/ViewBase.java new file mode 100644 index 0000000..81fab62 --- /dev/null +++ b/app/src/main/java/master/sudoku/views/ViewBase.java @@ -0,0 +1,90 @@ +/** + * + */ +package master.sudoku.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + + +/** + * @author dannyzha + * + */ +public abstract class ViewBase extends View { + + protected Canvas mCanvas; + + protected Rect mBound; + + private int mStartX; + + private int mStartY; + + /** + * Need this constructor to fix the "Error inflating class" error + * @param context + * @param attrs + */ + public ViewBase(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ViewBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setBound(Rect bound) { + this.mBound = bound; + } + + public Rect getBound() { + return mBound; + } + + public void setCanvas(Canvas canvas) { + this.mCanvas = canvas; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch(event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mStartX = (int)event.getX(); + mStartY = (int)event.getY(); + this.onPointerPressed(mStartX, mStartY); + break; + case MotionEvent.ACTION_UP: + int endX = (int)event.getX(); + int endY = (int)event.getY(); + this.onPointerReleased(endX, endY); + if(Math.abs(endX - mStartX) <= 3 && Math.abs(endY - mStartY) <= 3) { + this.onTap(mStartX, mStartY); + } + break; + } + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + paint(canvas); + } + /** + * Paint the view + * @param canvas + */ + abstract public void paint(Canvas canvas); + // abstract public void repaint(); +// abstract public void invalidate(); +// abstract public void invalidate(Rect rect); + abstract public void onTap(int x, int y); + abstract public void onPointerPressed(int x, int y); + abstract public void onPointerReleased(int x, int y); + abstract public void onKeyEvent(int key); + +} diff --git a/app/src/main/java/master/sudoku/widgets/InputPanel.java b/app/src/main/java/master/sudoku/widgets/InputPanel.java new file mode 100644 index 0000000..bdb9141 --- /dev/null +++ b/app/src/main/java/master/sudoku/widgets/InputPanel.java @@ -0,0 +1,94 @@ +/** + * + */ +package master.sudoku.widgets; + +import android.graphics.Canvas; +import android.graphics.Rect; + +import master.sudoku.event.EventArgs; +import master.sudoku.logs.Logger; +import master.sudoku.shapes.NumberGrid; +import master.sudoku.views.ViewBase; + +/** + * @author dannyzha + * + */ +public class InputPanel extends WidgetBase { + + private NumberGrid mGrid; + private boolean inputOnly = false; + + /** + * Constructor + * @param parent + */ + public InputPanel(ViewBase parent) { + super(parent); + mGrid = new NumberGrid(5, 2); + + for(int i=0; i<5; i++) { + for(int j=0; j<2; j++) { + mGrid.setNumber(i, j, j*5+i+1); + mGrid.setReadOnly(i, j, true); + } + } + mGrid.setNumber(4, 1, 0); + mGrid.setText(4, 1, "S"); + } + + public void setBound(Rect rect) { + super.setBound(rect); + mGrid.setBound(rect); + } + + public void setInputOnly(boolean inputOnly) { + if (inputOnly) { + mGrid.setText(4, 1, "OK"); + } else { + mGrid.setText(4, 1, "S"); + } + } + + /* (non-Javadoc) + * @see com.skyway.pandora.sudoku.widgets.WidgetBase#paint(javax.microedition.lcdui.Graphics) + */ + public void paint(Canvas canvas) { + mGrid.paint(canvas); + } + + public boolean onTap(int x, int y) { + return false; + } + + public boolean onPointerPressed(int x, int y) { + if(Logger.ON) { + Logger.getLogger().debug("InputPanel.onPointerPressed, position is:[" + x + "," + y + "]."); + } + if(mGrid.getBound().contains(x, y)) { + if(Logger.ON) { + Logger.getLogger().debug("InputPanel.onPointerPressed, mGrid.getBound().containsPoint(x, y)"); + } + mGrid.selectByPixel(x, y); + mParentView.invalidate(this.getBound()); + this.triggerEvent(EventArgs.INPUT_PANEL_SELECT, Integer.valueOf(mGrid.getSelectedCellNumber())); + return true; + } + return false; + } + + public boolean onPointerReleased(int x, int y) { + if(Logger.ON) { + Logger.getLogger().debug("InputPanel.onPointerReleased, position is:[" + x + "," + y + "]."); + } + mGrid.clearSelection(); + mParentView.invalidate(this.getBound()); + return true; + } + + public boolean onKeyEvent(int key) { + // TODO Auto-generated method stub + return false; + } +} diff --git a/app/src/main/java/master/sudoku/widgets/MatrixGrid.java b/app/src/main/java/master/sudoku/widgets/MatrixGrid.java new file mode 100644 index 0000000..f3c8eb7 --- /dev/null +++ b/app/src/main/java/master/sudoku/widgets/MatrixGrid.java @@ -0,0 +1,200 @@ +/** + * + */ +package master.sudoku.widgets; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.view.KeyEvent; + +import java.util.Vector; + +import master.sudoku.config.DeviceConfig; +import master.sudoku.event.EventArgs; +import master.sudoku.event.EventListener; +import master.sudoku.exception.SolutionException; +import master.sudoku.logs.Logger; +import master.sudoku.model.Index; +import master.sudoku.model.Sudoku; +import master.sudoku.shapes.SudokuGrid; +import master.sudoku.views.ViewBase; + +/** + * @author dannyzha + * + */ +public class MatrixGrid extends WidgetBase implements EventListener { + + private Sudoku mModel; + private SudokuGrid mGrid; + private boolean mIsEditing = false; + + /** + * Constructor + */ + public MatrixGrid(ViewBase parent) { + super(parent); + mGrid = new SudokuGrid(Sudoku.SUDOKU_SIZE, Sudoku.SUDOKU_SIZE); + } + + public void setModel(Sudoku model) { + this.mModel = model; + SudokuGrid oldGrid = mGrid; + mGrid = new SudokuGrid(Sudoku.SUDOKU_SIZE, Sudoku.SUDOKU_SIZE); + mGrid.setBound(oldGrid.getBound()); + for(int i=0; i 0) { + mGrid.setNumber(i, j, value); + mGrid.setReadOnly(i, j, true); + } + } + } + } + + public void setIsEditing(boolean isEditing) { + mIsEditing = isEditing; + } + + public void setBound(Rect rect) { + super.setBound(rect); + mGrid.setBound(rect); + } + + public void paint(Canvas canvas) { +// for(int i=0; i 0) { + mGrid.setNumber(i, j, value); + } + } + } + mGrid.paint(canvas); + } + + public boolean onTap(int x, int y) { + if(Logger.ON) { + Logger.getLogger().debug("MatrixGrid.onTap, position is:[" + x + "," + y + "]."); + } + if(mGrid.getBound().contains(x, y)) { + if(Logger.ON) { + Logger.getLogger().debug("MatrixGrid.onTap, mGrid.getBound().containsPoint(x, y)"); + } + mGrid.selectByPixel(x, y); + mParentView.invalidate(this.getBound()); + return true; + } + return false; + } + + public boolean onPointerPressed(int x, int y) { + // TODO Auto-generated method stub + return false; + } + + public boolean onPointerReleased(int x, int y) { + // TODO Auto-generated method stub + return false; + } + + public boolean onKeyEvent(int key) { + if(Logger.ON) { + Logger.getLogger().debug("MatrixGrid.onKeyEvent, key is:" + key); + } + int i = mGrid.getSelectedI(); + int j = mGrid.getSelectedJ(); + + switch(key) { + case KeyEvent.KEYCODE_DPAD_LEFT: + i -= 1; + if(i<0) { + i += Sudoku.SUDOKU_SIZE; + } + mGrid.selectByIndex(i, j); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + i += 1; + if(i >= Sudoku.SUDOKU_SIZE) { + i -= Sudoku.SUDOKU_SIZE; + } + mGrid.selectByIndex(i, j); + break; + case KeyEvent.KEYCODE_DPAD_UP: + j -= 1; + if(j<0) { + j += Sudoku.SUDOKU_SIZE; + } + mGrid.selectByIndex(i, j); + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + j += 1; + if(j >= Sudoku.SUDOKU_SIZE) { + j -= Sudoku.SUDOKU_SIZE; + } + mGrid.selectByIndex(i, j); + mParentView.invalidate(this.getBound()); + return true; + } + return false; + } + + public boolean handleEvent(EventArgs args) { + switch(args.getEventType()) { + case EventArgs.INPUT_PANEL_SELECT: + int i = mGrid.getSelectedI(); + int j = mGrid.getSelectedJ(); + Integer value = (Integer)args.getEventData(); + try { + if(value.intValue() <= 0) { + return false; + } + if(DeviceConfig.mErrorHintLevel==3 && mGrid.hasError()) { + //TODO: pop up error message + return true; + } + if(DeviceConfig.mErrorHintLevel==1) { + mGrid.clearErrorFlag(); + } + mGrid.clearErrorFlagForCell(i, j); + + if(mModel.acceptValue(i, j, value.intValue())) { + mModel.setResultValue(i, j, value.intValue()); + } + else { + Vector conflicts = mModel.getConflicts(i, j, value.intValue()); + + if(DeviceConfig.mErrorHintLevel > 0) { + mGrid.setErrorFlag(i, j, true); + for(int k=0; k mCornerRectList; + private Point mCenterPt; + private Paint mPaint; + private int mBoxWidth = sDefaultBoxWidth; + private boolean mIsInAnimation = false; + private int mColor = sScanningColor; + + private Runnable mThread = new Runnable() + { + public void run() + { + if (mProgressRect == null) { + initProgress(); + } + if (mPaint != null) { + //set color to Focus-Done-Color while doing the animation + mPaint.setColor(sScanningColor); + } + if(mProgressRect.height() >= mBoundRect.height()) { + if(mProgressGoingDown) { + mProgressRect.top = mProgressRect.bottom; + } + else { + mProgressRect.bottom = mProgressRect.top; + } + mProgressGoingDown = !mProgressGoingDown; + } + if(mProgressGoingDown) { + mProgressRect.bottom += sAnimationStep; + } + else { + mProgressRect.top -= sAnimationStep; + } + invalidate((int)(mProgressRect.left-10), (int)(mProgressRect.top-10), + (int)(mProgressRect.right+10), (int)(mProgressRect.bottom+10)); + postDelayed(this, sInterval);//延迟mInterval后执行当前线程 + } + }; + + /** + * Constructor + * @param context + */ + public ScannerBox(Context context) { + super(context); + } + + /** + * Constructor + * @param context + * @param attrs + */ + public ScannerBox(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * set center point of the box + * @param x + * @param y + */ + public void setCenterPoint(int x, int y) { + if(mCenterPt == null) { + mCenterPt = new Point(); + } + mCenterPt.set(x, y); + if(mBoundRect == null) { + return; + } + float oldLeft = mBoundRect.left; + float oldTop = mBoundRect.top; + float oldRight = mBoundRect.right; + float oldBottom = mBoundRect.bottom; + relocateBound(); + invalidate((int)(oldLeft-10), (int)(oldTop-10), + (int)(oldRight+10), (int)(oldBottom+10)); + } + + public void setScanResult(boolean success) { + mColor = success ? sScanDoneColor : sScanErrorColor; + if(mPaint != null) { + mPaint.setColor(mColor); + } + if(mBoundRect != null) { + invalidate((int)(mBoundRect.left-10), (int)(mBoundRect.top-10), + (int)(mBoundRect.right+10), (int)(mBoundRect.bottom+10)); + } + } + + public void startAnimation() { + if(!mIsInAnimation) { + mIsInAnimation = true; + post(mThread); + } + } + + private void setBoxWidth(int width) { + mBoxWidth = width; + relocateBound(); + invalidate((int)(mBoundRect.left-20), (int)(mBoundRect.top-20), + (int)(mBoundRect.right+20), (int)(mBoundRect.bottom+20)); + } + + @Override + protected void onDraw(Canvas canvas) { + if(mCenterPt == null) { + mCenterPt = new Point(this.getWidth()/2, this.getHeight()/2); + } + if(mBoundRect == null) { + float ratio = (float)this.getWidth() / sDefaultBoxWidth; + scaleDrawSizes(ratio); + relocateBound(); + } + + drawLines(canvas); + drawProgressRect(canvas); + } + + /** + * Arc rectangle index starts on left-top corner of the out bound rectangle and goes clockwise from 0 to 3 + * ------------- + * | 0 | | 1 | + * |---|---|---| + * | | | | + * |---|---|---| + * | 3 | | 2 | + * |---|---|---| + */ + private void drawLines(Canvas c) { + RectF r = mCornerRectList.get(0); + c.drawLine(r.left + sCornerRadius, r.top, r.right, r.top, mPaint); + c.drawLine(r.left, r.top + sCornerRadius, r.left, r.bottom, mPaint); + + r = mCornerRectList.get(1); + c.drawLine(r.left, r.top, r.right - sCornerRadius, r.top, mPaint); + c.drawLine(r.right, r.top + sCornerRadius, r.right, r.bottom, mPaint); + + r = mCornerRectList.get(2); + c.drawLine(r.left, r.bottom, r.right - sCornerRadius, r.bottom, mPaint); + c.drawLine(r.right, r.top, r.right, r.bottom - sCornerRadius, mPaint); + + r = mCornerRectList.get(3); + c.drawLine(r.left + sCornerRadius, r.bottom, r.right, r.bottom, mPaint); + c.drawLine(r.left, r.top, r.left, r.bottom - sCornerRadius, mPaint); + } + + private void initPaint() { + mPaint = new Paint(); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setAntiAlias(true); + mPaint.setColor(mColor); + mPaint.setStrokeWidth(sStrokeSize); + + mProgressPaint = new Paint(); + mProgressPaint.setStyle(Paint.Style.FILL); + } + + /** + * Arc rectangle index starts on left-top corner of the out bound rectangle and goes clockwise from 0 to 3 + * ------------- + * | 0 | | 1 | + * |---|---|---| + * | | | | + * |---|---|---| + * | 3 | | 2 | + * |---|---|---| + */ + private void initCornerRects() { + mCornerRectList = new ArrayList(); + for(int i=0; i<4; i++) { + mCornerRectList.add(new RectF()); + } + } + + private void scaleDrawSizes(float ratio) { + if(!sIsScaled) { + sLineLength = (int)(sLineLength * ratio); + mBoxWidth = (int)(mBoxWidth * ratio); + sStrokeSize = (int)(sStrokeSize * ratio); + sCornerRadius = (int)(sCornerRadius * ratio); + sDefaultBoxWidth = (int)(sDefaultBoxWidth * ratio); + sAnimationStep = (int)(sAnimationStep * ratio); + sIsScaled = true; + } + + initPaint(); + initCornerRects(); + } + + private void relocateBound() { + if(mBoundRect == null) { + mBoundRect = new RectF(); + } + mBoundRect.set(mCenterPt.x - mBoxWidth/2, mCenterPt.y - mBoxWidth/2, + mCenterPt.x + mBoxWidth/2, mCenterPt.y + mBoxWidth/2); + for(int i=0; i mFixedShapes = new Vector(); + + protected Rect mBound; + + /** + * Constructor + * @param parent + */ + public WidgetBase(ViewBase parent) { + this.mParentView = parent; + } + + public void setBound(Rect rect) { + this.mBound = rect; + } + + public Rect getBound() { + return mBound; + } + + abstract public void paint(Canvas canvas); + abstract public boolean onTap(int x, int y); + abstract public boolean onPointerPressed(int x, int y); + abstract public boolean onPointerReleased(int x, int y); + abstract public boolean onKeyEvent(int key); + +} diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..288b665 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..6ae570b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d4fb7cd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/no_image.png b/app/src/main/res/drawable-xhdpi/no_image.png new file mode 100644 index 0000000..9ef0941 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/no_image.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..85a6081 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/layout/activity_load_puzzle.xml b/app/src/main/res/layout/activity_load_puzzle.xml new file mode 100644 index 0000000..c11a243 --- /dev/null +++ b/app/src/main/res/layout/activity_load_puzzle.xml @@ -0,0 +1,7 @@ + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..efc78fc --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/fragment_capture_puzzle.xml b/app/src/main/res/layout/fragment_capture_puzzle.xml new file mode 100644 index 0000000..8425197 --- /dev/null +++ b/app/src/main/res/layout/fragment_capture_puzzle.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_load_puzzle.xml b/app/src/main/res/layout/fragment_load_puzzle.xml new file mode 100644 index 0000000..1463d42 --- /dev/null +++ b/app/src/main/res/layout/fragment_load_puzzle.xml @@ -0,0 +1,34 @@ + + + + + + +