অন ডিভাইস ইমেজ সেগমেন্টেশন – DeepLabv3

সেগমেন্টেশন হচ্ছে একটা ইমেজের মধ্যে ডিটেকটেড অবজেক্ট গুলো ঠিক কোন কোন পিক্সেলে রয়েছে, তা বের করা। ইমেজ ক্লাসিফিকেশন vs অবজেক্ট ডিটেকশন vs ইমেজ সেগমেন্টেশন লেখাটায় ক্লাসিফিকেশন, ডিটেকশনের এবং সেগমেন্টেশনের মধ্যে পার্থক্য জানতে পারবেন।

আমরা DeepLabv3 ব্যবহার করে কিভাবে ইমেজ সেগমেন্টেশন করা যায় তা দেখব। এই ক্ষেত্রে আমরা প্রিবিল্ড CoreML মডেল ব্যবহার করব। আর আমরা কাজ করব iOS অ্যাপ নিয়ে। যদিও ম্যাকে একই কোড কাজ করবে। অ্যাপলের ওয়েব সাইট থেকে DeeplabV3 মডেলটি ডাউনলোড করে নিন।

ডাউনলোড করার পর চাইলে প্রিভিউ দেখে নিতে পারেন একটা ইমেজ ইনপুট দিলে কি আউটপুট পাওয়া যাবে, তা দেখতে পাবেন।

এবার একটা iOS প্রজেক্ট তৈরি করে নিব এক্সকোডে। এর আগে CoreML মডেল ও এর ব্যবহার এ দেখেছি কিভাবে একটা CoreML মডেল লোড করা যায় এবং ব্যবহার করা যায়।

আমরা প্রথমে ডাউনলোডকৃত DeepLabV3.mlmodel মডেল ফাইল প্রজেক্টে কপি করব। ড্র্যাগ & ড্রপ করে প্রজেক্টে যুক্ত করা যাবে। Assets.xcassets ফোল্ডারে একটা ইমেজ রাখব। আমি নিচের ইমেজটি ব্যবহার করেছিঃ

এই ভদ্রলোককে পেয়েছি পূর্বাচল। ভাবুক হয়ে বসে ছিল। কয়েকটা ছবি তুলেছিলাম ঐদিন। এখন মডেল হিসেবে কাজে লাগছে।

আমরা সিম্পল একটা UI তৈরি করে নিবঃ

        VStack {
            if let inputImage = inputImage {
                Image(uiImage: inputImage)
                    .resizable()
                    .scaledToFit()
                
            } else {
                Text("No image selected")
                    .padding()
            }
            
            
            Button(action: applyImageSegmentation) {
                Text("Segment Image")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            
            if let segmentedMask = segmentedMask {
                Image(uiImage: segmentedMask)
                    .resizable()
                    .scaledToFit()
            }
            
        }

এই দুইটা ভ্যারিয়েবল ব্যবহার করবঃ

    @State private var inputImage: UIImage? = UIImage(named: "cat")
    @State private var segmentedMask: UIImage? = nil

এসেট ফোল্ডার থেকে এখন হার্ড কোড করে ইমেজ ইনপুট নিচ্ছি। চাইলে গ্যালারি বা ক্যামেরা থেকেও নেওয়া যাবে। বুঝার সুবিধার্থে প্রজেক্ট সিম্পল রাখছি।


applyImageSegmentation ফাংশন এভাবে লিখবঃ

    func applyImageSegmentation() {
        do {
            let modelConfiguration = MLModelConfiguration()
            // load model
            let model = try VNCoreMLModel(for: DeepLabV3(configuration: modelConfiguration).model)
            
            let request = VNCoreMLRequest(model: model) { request, error in
                if let results = request.results as? [VNCoreMLFeatureValueObservation],
                   let multiArray = results.first?.featureValue.multiArrayValue {
                    
                    // convert array to image
                    self.segmentedMask = self.multiArrayToMaskImage(multiArray)
              
                } else {
                    print("No valid segmentation results")
                }
            }
            
            request.imageCropAndScaleOption = .scaleFill
            
            if let cgImage = inputImage?.cgImage {
                let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
                try handler.perform([request])
            }
        } catch {
            print("Error: \(error.localizedDescription)")

        }
    }

DeepLabV3 মডেল যদি আমরা ওপেন করে দেখি, দেখব আউটপুট আউটপুট হিসেবে MultiArray রিটার্ণ করে। এই MultiArray মূলত সেগমেন্টেড এরিয়া। যে এরিয়াতে ডিটেকটেড অবজেক্ট পাওয়া যাবে।

আমরা চাইলে এই মাল্টিঅ্যারেকে ইমেজ মাস্কে কনভার্ট করে নিতে পারিঃ

    func multiArrayToMaskImage(_ multiArray: MLMultiArray) -> UIImage? {
        let height = multiArray.shape[0].intValue
        let width = multiArray.shape[1].intValue
        
        var pixelBuffer = [UInt8](repeating: 0, count: width * height)
        
        for y in 0..<height {
            for x in 0..<width {
                let value = multiArray[[y as NSNumber, x as NSNumber]].int32Value
                // Convert to binary mask: 255 (white) for the object, 0 (black) for the background
                pixelBuffer[y * width + x] = value == 0 ? 0 : 255
            }
        }
        
        let context = CGContext(data: &pixelBuffer, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue)
        
        if let cgImage = context?.makeImage() {
            return UIImage(cgImage: cgImage)
        }
        return nil
    }

আউটপুট পাবো এমনঃ

সম্পূর্ন কোডঃ

import SwiftUI
import CoreML
import Vision

struct OnlySegmentation: View {
    
    @State private var inputImage: UIImage? = UIImage(named: "cat")
    @State private var segmentedMask: UIImage? = nil
    var body: some View {
        
        VStack {
            if let inputImage = inputImage {
                Image(uiImage: inputImage)
                    .resizable()
                    .scaledToFit()
                
            } else {
                Text("No image selected")
                    .padding()
            }
            
            
            Button(action: applyImageSegmentation) {
                Text("Segment Image")
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            
            if let segmentedMask = segmentedMask {
                Image(uiImage: segmentedMask)
                    .resizable()
                    .scaledToFit()
            }
            
        }
        .padding()
    }
    
    func applyImageSegmentation() {
        do {
            let modelConfiguration = MLModelConfiguration()
            // load model
            let model = try VNCoreMLModel(for: DeepLabV3(configuration: modelConfiguration).model)
            
            let request = VNCoreMLRequest(model: model) { request, error in
                if let results = request.results as? [VNCoreMLFeatureValueObservation],
                   let multiArray = results.first?.featureValue.multiArrayValue {
                    
                    // convert array to image
                    self.segmentedMask = self.multiArrayToMaskImage(multiArray)
              
                } else {
                    print("No valid segmentation results")

                }
            }
            
            request.imageCropAndScaleOption = .scaleFill
            
            if let cgImage = inputImage?.cgImage {
                let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
                try handler.perform([request])
            }
        } catch {
            print("Error: \(error.localizedDescription)")

        }
    }
    
    func multiArrayToMaskImage(_ multiArray: MLMultiArray) -> UIImage? {
        let height = multiArray.shape[0].intValue
        let width = multiArray.shape[1].intValue
        
        var pixelBuffer = [UInt8](repeating: 0, count: width * height)
        
        for y in 0..<height {
            for x in 0..<width {
                let value = multiArray[[y as NSNumber, x as NSNumber]].int32Value
                // Convert to binary mask: 255 (white) for the object, 0 (black) for the background
                pixelBuffer[y * width + x] = value == 0 ? 0 : 255
            }
        }
        
        let context = CGContext(data: &pixelBuffer, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue)
        
        if let cgImage = context?.makeImage() {
            return UIImage(cgImage: cgImage)
        }
        return nil
    }
}

সেগমেন্টটেড ইমেজ

উপরের উদাহরণে আমরা শুধু মাত্র সেগমেন্টেড মাস্ক অংশটা দেখিয়েছি। আমরা চাইলে সেগমেন্টেড ইমেজ নিতে পারি। এর জন্য আমাদের উপরের মাস্ক ইমেজ লাগবে, এবং ইনপুট ইমেজ লাগবে। আবার মাস্ক ইমেজ এবং ইনপুট ইমেজ, দুইটার সাইজ সমান হতে হবে। তাই দুইটাই রিসাইজ করে নিতে হবে। তার জন্য UIImage এর একটা এক্সটেনশন তৈরি কতে নিতে পারি এভাবেঃ

extension UIImage {
    func resizedImage(for targetSize: CGSize) -> UIImage? {
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        return renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: targetSize))
        }
    }
}

এরপর একটা ফাংশন লিখব, যেন শুধু ইনপুট ইমেজ থেকে মাস্ক ইমেজের অংশটুকু সিলেক্ট করেঃ

    private func applyMask(to inputImage: UIImage, mask: UIImage) -> UIImage? {
        guard let maskCGImage = mask.cgImage, let inputCGImage = inputImage.cgImage else { return nil }
        
        let inputCIImage = CIImage(cgImage: inputCGImage)
        let maskCIImage = CIImage(cgImage: maskCGImage)
        
        
        // Apply the mask to the original image
        let filteredImage = inputCIImage.applyingFilter("CIBlendWithMask", parameters: [
            "inputMaskImage": maskCIImage
        ])
        
        let context = CIContext()
        if let outputCGImage = context.createCGImage(filteredImage, from: filteredImage.extent) {
            return UIImage(cgImage: outputCGImage)
        }
        
        return nil
    }

আউটপুট পাবো এমনঃ

সম্পূর্ণ কোডঃ

import SwiftUI
import CoreML
import Vision
import UIKit

struct ContentView: View {
    @State private var inputImage: UIImage? = UIImage(named: "cat")?.resizedImage(for: CGSize(width: 513, height: 513))
    @State private var segmentedMask: UIImage? = nil
    @State private var segmentedImage: UIImage? = nil
    var body: some View {
        ScrollView{
            VStack {
                if let inputImage = inputImage {
                    Image(uiImage: inputImage)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 200)
                    
                } else {
                    Text("No image selected")
                        .padding()
                }
                
                
                Button(action: applyImageSegmentation) {
                    Text("Segment Image")
                        .padding()
                        .background(Color.green)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
                
                if let segmentedMask = segmentedMask {
                    Image(uiImage: segmentedMask)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 200)
                }
                
                if let segmentedImage = segmentedImage {
                    Image(uiImage: segmentedImage)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 200)
                        .border(Color.gray, width: 1)
                }
                
            }
            .padding()
        }
    }
    
    private func applyImageSegmentation() {
        do {
            let modelConfiguration = MLModelConfiguration()
            // load model
            let model = try VNCoreMLModel(for: DeepLabV3(configuration: modelConfiguration).model)
            
            let request = VNCoreMLRequest(model: model) { request, error in
                if let results = request.results as? [VNCoreMLFeatureValueObservation],
                   let multiArray = results.first?.featureValue.multiArrayValue {
                    // arry to image mask
                    let segementMask = self.multiArrayToMaskImage(multiArray)
                    // Ensure the segmented mask is resized to the original image size
                    if let maskImage = segementMask {
                        self.segmentedMask = maskImage.resizedImage(for: inputImage!.size)
                    }
                    
                } else {
                    print("No valid segmentation results")
                }
            }
            
            if let cgImage = inputImage?.cgImage {
                let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
                try handler.perform([request])
            }
        } catch {
            print("Error: \(error.localizedDescription)")
            
        }
    }
    
    private func multiArrayToMaskImage(_ multiArray: MLMultiArray) -> UIImage? {
        let height = multiArray.shape[0].intValue
        let width = multiArray.shape[1].intValue
        
        var pixelBuffer = [UInt8](repeating: 0, count: width * height)
        
        for y in 0..<height {
            for x in 0..<width {
                let value = multiArray[[y as NSNumber, x as NSNumber]].int32Value
                // Convert to binary mask: 255 (white) for the object, 0 (black) for the background
                pixelBuffer[y * width + x] = value == 0 ? 0 : 255
            }
        }
        
        let context = CGContext(data: &pixelBuffer, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue)
        
        if let cgImage = context?.makeImage() {
            // Resize the mask to match the input image size
            let maskImage = UIImage(cgImage: cgImage).resizedImage(for: inputImage!.size)
            // Get Segemented Image
            segmentedImage = applyMask(to: inputImage!, mask: maskImage!)
            
            return maskImage
        }
        
        
        return nil
    }
    
    private func applyMask(to inputImage: UIImage, mask: UIImage) -> UIImage? {
        guard let maskCGImage = mask.cgImage, let inputCGImage = inputImage.cgImage else { return nil }
        
        let inputCIImage = CIImage(cgImage: inputCGImage)
        let maskCIImage = CIImage(cgImage: maskCGImage)
        
        
        // Apply the mask to the original image
        let filteredImage = inputCIImage.applyingFilter("CIBlendWithMask", parameters: [
            "inputMaskImage": maskCIImage
        ])
        
        let context = CIContext()
        if let outputCGImage = context.createCGImage(filteredImage, from: filteredImage.extent) {
            return UIImage(cgImage: outputCGImage)
        }
        
        return nil
    }
}

extension UIImage {
    func resizedImage(for targetSize: CGSize) -> UIImage? {
        let renderer = UIGraphicsImageRenderer(size: targetSize)
        return renderer.image { _ in
            self.draw(in: CGRect(origin: .zero, size: targetSize))
        }
    }
}

সেগমেন্টেড ইমেজের ব্যাকগ্রাউন্ড পরিবর্তন

আমরা চাইলে এবার সেগমেন্টেড ইমেজে যে কোন ইমেজ অথবা কালার ব্যাকগ্রাউন্ড দিতে পারে। applyMask এ একাধিক ইমেজ নিয়ে এভাবে একটা কম্পোজিস্ট তৈরি করিঃ

    private func applyMask(to inputImage: UIImage, mask: UIImage) -> UIImage? {
        guard let maskCGImage = mask.cgImage, let inputCGImage = inputImage.cgImage else { return nil }
        
        
        // add a bg image to your Assets.xcassets
        let bgImage = UIImage(named: "bg")?.resizedImage(for: CGSize(width: 513, height: 513))
        
        let input = CIImage(cgImage: inputCGImage)
        let mask = CIImage(cgImage: maskCGImage)
        let background = CIImage(cgImage: (bgImage?.cgImage!)!)
        
        
        if let compositeImage = CIFilter(name: "CIBlendWithMask", parameters: [
                                        kCIInputImageKey: input,
                                        kCIInputBackgroundImageKey:background,
                                        kCIInputMaskImageKey:mask])?.outputImage
        {
            
            
            let ciContext = CIContext(options: nil)

            let filteredImageRef = ciContext.createCGImage(compositeImage, from: compositeImage.extent)
            
            return UIImage(cgImage: filteredImageRef!)
            
        }
        
        return nil
    }

Assets.xcassets এ bg নামে একটা ইমেজ রাখতে হবে। না হলে প্রোগ্রাম ক্র্যাস করবে। আউটোপুট পাবো এমনঃ

বিড়ালটাকে শুটকির দেশে পাঠিয়ে দিলাম। B)

গিটহাবে সোর্সকোড পাওয়া যাবে। ইনিশিয়ালি OnlySegmentation ফাইল লোড হবে। ছাইলে ImageSegmentationApp.swift ফাইল থেকে ContentView লোড করে ফাইনাল আউটপুট দেখা যাবে।

আমি এখানে অনেক কিছুই সিমপ্লিপাই করে লিখেছি। আরেকটু বেটার ইমপ্লিমেন্টেশনের জন্য Core ML Background Removal in SwiftUI লেখাটি পড়তে পারেন। এছাড়া CoreMLHelpers রিপোজিটোরির ফাইল গুলো কাজ দিবে।

Leave a Reply