Android Oximeter using Image Processing and IIR filter

Golap Gunjan Barman
5 min readMay 14, 2021

Android Oximeter using Image Processing and IIR filter

Here in this blog, we’re going to create an Oximeter using Image Processing and IIR (Infinite Impulse Response) filter, camera API, and Surface View with Power manager.

IIR filter

IIR filters are one of two primary types of digital filters used in Digital Signal Processing (DSP) applications. They have been widely deployed in audio equalization, biomedical sensor signal processing, IoT/IIoT smart sensors, and high-speed telecommunication/RF applications.

Here users can easily check their Oxygen level by putting their finger over the camera and flashlight surface. Then by using the image processing and IIR filter this app will calculate the Oxygen level.

Main Layout

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="30dp"
android:background="@color/back"
tools:context=".MainActivity">
<ImageView
android:layout_width="match_parent"
android:layout_height="500dp"
android:src="@drawable/oxy"
android:contentDescription="@string/app_name"/>
<Space
android:layout_width="match_parent"
android:layout_height="60dp"/>
<Button
android:id="@+id/startVS"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Measuring"
android:padding="10dp"
android:backgroundTint="@color/white"
android:textColor="@color/back"
android:textStyle="bold"
android:textSize="18sp"/>
</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_main);
Button actionWrk = this.findViewById(R.id.startVS);
actionWrk.setOnClickListener(view -> {
Intent intent = new Intent(view.getContext(), OxygenProcess.class);
startActivity(intent);
finish();
});
}
}

OxygenProcess

activity_oxygen_process.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="40dp"
android:gravity="center"
android:background="@color/back"
tools:context=".OxygenProcess">
<SurfaceView
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/preview"/>
<ProgressBar
android:layout_width="60dp"
android:layout_height="60dp"
android:id="@+id/o2PB"
android:layout_marginTop="15dp"
android:indeterminate="false"
android:max="27"
android:progress="1"
android:progressDrawable="@drawable/cicular_progressbar"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please wait. It will take a while"
android:textColor="@color/white"
android:textSize="15sp"/>
</LinearLayout>

OxygenProcess.java

public class OxygenProcess extends AppCompatActivity {    private static final String TAG = "OxygenMeasure";
private static final AtomicBoolean processing = new AtomicBoolean(false);
private Camera camera = null;
private static SurfaceHolder previewHolder = null;
private SurfaceView preview = null;
private static PowerManager.WakeLock wakeLock = null;
private Toast mainToast;
private ProgressBar Prog02;
public int ProgP = 0;
public int inc = 0;
private static long startTime = 0;
private double SamplingFreq;
private static final double RedBlueRation = 0;
double Stdr = 0;
double Stdb= 0;
double sumred = 0;
double sumblue = 0;
public int o2;
public ArrayList<Double> RedAvgList = new ArrayList<>();
public ArrayList<Double> BlueAvgList = new ArrayList<>();
public int counter = 0;
@SuppressLint("InvalidWakeLockTag")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_oxygen_process);
preview = findViewById(R.id.preview);
previewHolder = preview.getHolder();
previewHolder.addCallback(surfaceCallback);
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
Prog02 = findViewById(R.id.o2PB);
Prog02.setProgress(0);
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, "NOTDIMSCREEN");
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
protected void onResume() {
super.onResume();
wakeLock.acquire();
camera = Camera.open();
camera.setDisplayOrientation(90);
startTime = System.currentTimeMillis();
}
@Override
protected void onPause() {
super.onPause();
wakeLock.release();
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
camera=null;
}
private final Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (data == null) throw new NullPointerException();
Camera.Size size = camera.getParameters().getPreviewSize();
if (size== null) throw new NullPointerException();
if (!processing.compareAndSet(false, true)) return;
int width = size.width;
int height = size.height;
double RedAvg;
double BlueAvg;
RedAvg = ImageProcessing.colordecoderRGB(data.clone(), height, width,1);
sumred+=RedAvg;
BlueAvg = ImageProcessing.colordecoderRGB(data.clone(), height, width,2);
sumblue+=BlueAvg;
RedAvgList.add(RedAvg);
BlueAvgList.add(BlueAvg);
++counter;
if (RedAvg < 200){
inc = 0;
ProgP = inc;
Prog02.setProgress(ProgP);
processing.set(false);
}
long endTime = System.currentTimeMillis();
double totleTimeInSec = (endTime - startTime)/1000d;
if (totleTimeInSec >=30){
startTime = System.currentTimeMillis();
SamplingFreq = (counter/totleTimeInSec);
Double[] Red = RedAvgList.toArray(new Double[RedAvgList.size()]);
Double[] Blue = BlueAvgList.toArray(new Double[BlueAvgList.size()]);
double HRFreq = Fft.FFT(Red, counter,SamplingFreq);
double bpm = (int) ceil(HRFreq * 60);
double meanr = sumred/counter;
double meanb = sumblue/counter;
for (int i = 0; i<counter-1; i++){
Double bufferb = Blue[i];
Stdb+=((bufferb - meanb) * (bufferb - meanb));
Double bufferr = Red[i];
Stdr+= ((bufferr - meanr) * (bufferr - meanr));
}
double varr = sqrt(Stdr/(counter -1));
double varb = sqrt(Stdb/(counter -1));
double R = (varr/meanr)/(varb/meanb);
double spo2 = 100 - 5 * R;
o2 = (int) (spo2);
if ((o2 < 80 || o2 > 99) || (bpm < 45 || bpm > 200)){
inc = 0;
ProgP = inc;
Prog02.setProgress(ProgP);
mainToast = Toast.makeText(getApplicationContext(),"Measurement Failure", Toast.LENGTH_SHORT);
mainToast.show();
startTime = System.currentTimeMillis();
counter = 0;
processing.set(false);
return;
}
} if (o2 != 0){
Intent i = new Intent(OxygenProcess.this, OxygenCalculate.class);
i.putExtra("o2r", o2);
startActivity(i);
finish();
}
if (RedAvg != 0 ){
ProgP = inc++/34;
Prog02.setProgress(ProgP);
}
processing.set(false);
}
};
private final SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
try {
camera.setPreviewDisplay(previewHolder);
camera.setPreviewCallback(previewCallback);
}catch (Throwable t){
Log.d(TAG,"surfaceCreated: ",t);
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
Camera.Size size = getSmallestPreviewSize(width, height, parameters);
if (size!=null){
parameters.setPreviewSize(size.width, size.height);
}
camera.setParameters(parameters);
camera.startPreview();
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
}
};
private Camera.Size getSmallestPreviewSize(int width, int height, Camera.Parameters parameters) {
Camera.Size result = null;
for (Camera.Size size : parameters.getSupportedPictureSizes()){
if (size.width <= width && size.height <= height){
if (result == null){
result = size;
}
else {
int resultArea = result.width * result.height;
int newArea = size.width * size.height;
if (newArea < resultArea) result = size;
}
}
}
return result;
}
@Override
public void onBackPressed() {
super.onBackPressed();
Intent i = new Intent(OxygenProcess.this, MainActivity.class);
startActivity(i);
finish();
}
}

Oxygen Calculation

activtiy_oxygen_calculate.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="40dp"
android:background="@color/back"
tools:context=".OxygenCalculate">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="YOUR OXYGEN LEVEL"
android:textAlignment="center"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/o2r"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="XX"
android:textColor="@android:color/white"
android:textSize="30sp"
android:textStyle="bold" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:layout_marginTop="120dp">
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:text="100 - 98"
android:textAlignment="textEnd"
android:textColor="@android:color/holo_blue_bright"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginEnd="10dp"
android:layout_weight="1"/>
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Normal"
android:textAlignment="textStart"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start">
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:text="97 - 95"
android:textAlignment="textEnd"
android:textColor="@android:color/holo_blue_bright"
android:textSize="20sp"
android:layout_marginEnd="10dp"
android:textStyle="bold"
android:layout_weight="1"/>
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Insufficient"
android:textAlignment="textStart"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:text="94 - 90"
android:textAlignment="textEnd"
android:textColor="@android:color/holo_blue_bright"
android:textSize="20sp"
android:layout_marginEnd="10dp"
android:textStyle="bold"
android:layout_weight="1"/>
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Decreased"
android:textAlignment="textStart"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:text="below 90"
android:textAlignment="textEnd"
android:textColor="@android:color/holo_blue_bright"
android:textSize="20sp"
android:layout_marginEnd="10dp"
android:textStyle="bold"
android:layout_weight="1"/>
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Critical"
android:textAlignment="textStart"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:text="below 80"
android:textAlignment="textEnd"
android:textColor="@android:color/holo_blue_bright"
android:textSize="20sp"
android:layout_marginEnd="10dp"
android:textStyle="bold"
android:layout_weight="1"/>
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Severe hypoxia"
android:layout_weight="1"
android:textAlignment="textStart"
android:textColor="@color/white"
android:textSize="20sp"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<TextView
android:layout_width="40dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="below 70"
android:layout_marginEnd="10dp"
android:textAlignment="textEnd"
android:textColor="@android:color/holo_blue_bright"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="Acute danger to life"
android:textAlignment="textStart"
android:layout_weight="1"
android:textColor="@color/white"
android:textSize="20sp"/>
</LinearLayout> <Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/sendo2"
android:text="Share your result"
android:textAllCaps="true"
android:layout_marginTop="60dp"
android:padding="12dp"
android:textSize="18sp"
android:textStyle="bold"
android:backgroundTint="@color/white"
android:textColor="@color/back" />
</LinearLayout>

OxygenCalculate.java

public class OxygenCalculate extends AppCompatActivity {    private String Date;
DateFormat df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
java.util.Date today = Calendar.getInstance().getTime();
int o2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_oxygen_calculate);
Date = df.format(today);
TextView Ro2 = this.findViewById(R.id.o2r);
Button So2 = this.findViewById(R.id.sendo2);
Bundle bundle = getIntent().getExtras();
if (bundle!=null){
o2 = bundle.getInt("o2r");
Ro2.setText(String.valueOf(o2));
}
So2.setOnClickListener(view -> {
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("message/rfc822");
i.putExtra(Intent.EXTRA_SUBJECT, "Oxygen Meter");
i.putExtra(Intent.EXTRA_TEXT, "Oxygen Level \n"+"at "+Date+" is "+o2);
try {
startActivity(
Intent.createChooser(i,"SEND..")
);
}catch (ActivityNotFoundException e){
Toast.makeText(this, "No sharing apps installed on this device", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
}

Output:

Contact Details:

Gmail id: barmangolap15@gmail.com

WhatsApp: +91 8473855948

Instagram id: @androidapps.development.blogs

You can follow me on YouTube:

Golap Barman

Also, visit my website for more content like this

www.gbandroidblogs.com

Follow me on Instagram

Android App Developer

Follow me on Facebook

GBAndroidBlogs

--

--

Golap Gunjan Barman

Hi everyone, myself Golap an Android app developer with UI/UX designer.