图像编辑器 Monica 之各种图形绘制、图像调色

一. 图像编辑器 Monica

Monica 是一款跨平台的桌面图像编辑软件(早期主要是个人为了验证一些算法而产生的)。

85cd853f8f167f5444265228267f920c.jpeg
screenshot.png

其技术栈如下:

  • Kotlin 编写 UI(Kotlin Compose Desktop 作为 UI 框架)

  • 基于 mvvm 模式,依赖注入使用 koin,编译使用 JDK 17。

  • 部分算法使用 Kotlin 实现。

  • 其余的算法使用 OpenCV C++ 来实现,Kotlin 通过 jni 来调用。

  • Monica 所使用的模型,主要使用 ONNXRuntime 进行部署和推理。

  • 其余少部分模型使用 OpenCV DNN 进行部署和推理。

  • 本地的算法库使用 C++ 17 编译。

Monica 目前还处于开发阶段,当前版本的可以参见 github 地址:https://github.com/fengzhizi715/Monica

在这个月里,我完成了 Monica 比较重要的两个功能:图形绘制、图像调色。

二. 图形绘制

Monica 支持在图像上的任意位置绘制线段、圆、三角形、矩形、任意多边形,在任意位置添加文字,以及对这些绘制的图形更改属性。

2.1 形状绘制

下面是展示形状绘制的入口。f37f1ef54e4d8b1977d55265f05c1e8c.jpeg

以及绘制形状的页面。02255c36149d6d98629d3778b375728f.jpeg

Monica 提供了图像上的任意位置绘制各种图形的功能,以及修改图形的属性比如图像的颜色、透明度、是否填充、边框类型。2de7c259113369917df671e878ad1745.jpeg

5d262e8fd1f2969782522a6dbb5a7cce.jpeg
保存图像.png

在图像中绘制形状,主要是调用 Compose 的 Canvas API。在实现绘制功能之前,需要先定义好能够绘制图形的类型。

sealed class Shape {
    data class Line(val from: Offset, val to: Offset, val shapeProperties: ShapeProperties): Shape()

    data class Circle(val center: Offset, val radius:Float, val shapeProperties: ShapeProperties): Shape()

    data class Triangle(val first: Offset, val second: Offset?=null, val third: Offset?=null, val shapeProperties: ShapeProperties): Shape()

    data class Rectangle(val tl: Offset, val bl: Offset, val br: Offset, val tr: Offset, val rectFirst: Offset,val shapeProperties: ShapeProperties): Shape()

    data class Polygon(val points: List<Offset>, val shapeProperties: ShapeProperties): Shape()

    data class Text(val point: Offset, val message:String, val shapeProperties: ShapeProperties): Shape()
}

对于不同图形的绘制,需要确定好相关点的坐标。这块的逻辑比较多,可以查看项目的源码。下面主要讲讲如何绘制图形。

class ShapeDrawingViewModel {

    fun drawShape(canvasDrawer:CanvasDrawer,
                  lines: Map<Offset, Line>,
                  circles: Map<Offset, Circle>,
                  triangles: Map<Offset, Triangle>,
                  rectangles: Map<Offset, Rectangle>,
                  polygons: Map<Offset, Polygon>,
                  texts: Map<Offset, Text>,
                  saveFlag: Boolean = false) {

        lines.forEach {

            val line = it.value

            if (line.from != Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(line.from, line.shapeProperties.color)
            }

            if (line.from != Offset.Unspecified && line.to != Offset.Unspecified) {
                canvasDrawer.line(line.from,line.to, Style(null, line.shapeProperties.color, line.shapeProperties.border, null, fill = line.shapeProperties.fill, scale = 1f, alpha = line.shapeProperties.alpha, bounded = true))
            }
        }

        circles.forEach {

            val circle = it.value

            if (circle.center != Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(circle.center, circle.shapeProperties.color)
            }

            canvasDrawer.circle(circle.center, circle.radius, Style(null, circle.shapeProperties.color, circle.shapeProperties.border, null, fill = circle.shapeProperties.fill, scale = 1f, alpha = circle.shapeProperties.alpha, bounded = true))
        }

        triangles.forEach {
            val triangle = it.value

            if (triangle.first != Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(triangle.first, triangle.shapeProperties.color)
            }

            if (triangle.second != Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(triangle.second!!, triangle.shapeProperties.color)
                canvasDrawer.line(triangle.first,triangle.second, Style(null, triangle.shapeProperties.color, triangle.shapeProperties.border, null, fill = triangle.shapeProperties.fill, scale = 1f, alpha = triangle.shapeProperties.alpha, bounded = true))
            }

            if (triangle.first != Offset.Unspecified && triangle.second != Offset.Unspecified && triangle.third != Offset.Unspecified) {
                val list = mutableListOf<Offset>().apply {
                    add(triangle.first)
                    add(triangle.second!!)
                    add(triangle.third!!)
                }

                canvasDrawer.polygon(list, Style(null, triangle.shapeProperties.color, triangle.shapeProperties.border, null, fill = triangle.shapeProperties.fill, scale = 1f, alpha = triangle.shapeProperties.alpha,  bounded = true))
            }
        }

        rectangles.forEach {
            val rect = it.value

            if (rect.rectFirst!=Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(rect.rectFirst, rect.shapeProperties.color)
            }

            if (rect.tl!=Offset.Unspecified && rect.bl!=Offset.Unspecified && rect.br!=Offset.Unspecified && rect.tr!=Offset.Unspecified) {
                val list = mutableListOf<Offset>().apply {
                    add(rect.tl)
                    add(rect.bl)
                    add(rect.br)
                    add(rect.tr)
                }

                canvasDrawer.polygon(list, Style(null, rect.shapeProperties.color, rect.shapeProperties.border, null, fill = rect.shapeProperties.fill, scale = 1f, alpha = rect.shapeProperties.alpha, bounded = true))
            }
        }

        polygons.forEach {
            val polygon = it.value

            if (polygon.points[0]!=null && polygon.points[0] != Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(polygon.points[0] , polygon.shapeProperties.color)
            }

            if (polygon.points.size>1 && polygon.points[1] != Offset.Unspecified && !saveFlag) {
                canvasDrawer.point(polygon.points[1] , polygon.shapeProperties.color)
                canvasDrawer.line(polygon.points[0], polygon.points[1], Style(null, polygon.shapeProperties.color, Border.Line, null, fill = polygon.shapeProperties.fill, scale = 1f, alpha = polygon.shapeProperties.alpha, bounded = true))
            }

            canvasDrawer.polygon(polygon.points, Style(null, polygon.shapeProperties.color, polygon.shapeProperties.border, null, fill = polygon.shapeProperties.fill, scale = 1f, alpha = polygon.shapeProperties.alpha, bounded = true))
        }

        texts.forEach {
            val text = it.value

            if (text.point!= Offset.Unspecified) {
                val list = mutableListOf<String>().apply {
                    add(text.message)
                }
                canvasDrawer.text(text.point, list, text.shapeProperties.color, text.shapeProperties.fontSize)
            }
        }
    }
    ......

}

2.2 添加文字

Monica 支持在图像的任意位置添加文字,修改文字的属性比如字体大小、颜色。4ec61274cf08df726c68289c86fa33d6.jpeg

05e6d1b9f1bb785486d10289d40585d6.jpeg
修改属性.png
0e7accfc51dae9ce4286bdae785a641a.jpeg
修改颜色.png
8dd5fbf6fcbd69db9c76e22d811731eb.jpeg
保存图像.png

在图像中添加文字,需要一个可以拖动的 TextField ,这样才能在图像的任意位置添加文字。因此,DraggableTextField 控件如下:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DraggableTextField(
    modifier: Modifier = Modifier,
    text: String,
    bitmapWidth: Int,
    bitmapHeight: Int,
    density: Density,
    onTextChanged: (String) -> Unit,
    onDragged: (Offset) -> Unit
) {
    var offset by remember { mutableStateOf(Offset.Zero) }
    val halfWidth = bitmapWidth/2
    val halfHeight = bitmapHeight/2
    val halfTextFieldWidth = 125/density.density
    val halfTextFieldHeight = 65/density.density

    Box(
        modifier = modifier
            .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
            .pointerInput(Unit) {
                detectDragGestures { change ->
                    offset += change
                    if (abs(offset.x) > halfWidth - halfTextFieldWidth || abs(offset.y) > halfHeight - halfTextFieldHeight) {
                        offset -= change
                        return@detectDragGestures
                    }
                }
            }
            .shadow(8.dp)
            .background(Color.White)
            .padding(16.dp)
            .fillMaxWidth()
            .wrapContentHeight(Alignment.Top)
            .clip(RoundedCornerShape(8.dp))
    ) {
        Column {
            TextField (
                value = text,
                onValueChange = onTextChanged,
                modifier = Modifier.width(220.dp)
            )

            confirmButton(true, modifier = Modifier.align(Alignment.End).padding(top = 5.dp)) {
                onDragged.invoke(offset)
            }
        }
    }
}

需要注意的是 DraggableTextField 中的 offset 通过 onDragged 回调给当前图像,但是 offset 要变成图像中的当前的坐标,还需要做一些坐标转换才行。

三. 图像调色

Monica 支持调节图像的对比度、色调、饱和度、亮度、色温等,从而帮助大家调整图像的色彩。

77044ae3b3ec65fa56646bdbefa567de.jpeg
图像调色的入口.png
9687dddae2b369e777c62b6c07d45098.jpeg
图像调色的界面.png
0ec394ccf3788f3949fb728ac3b94a18.jpeg
支持拖动调节各个参数.png
90f91d7d4d9b50b52c6816e38596beb5.jpeg
支持拖动调节各个参数.png
3cc73249ae4e62d22436c5e2c6746100.jpeg
保存图像.png

3.1 应用层的设计和调用

该模块功能的实现,最终也是调用了封装 OpenCV 的函数。对于应用层,需要编写好调用 jni 层的代码:

object ImageProcess {

    ......

    /**
     * 初始化图像调色模块
     */
    external fun initColorCorrection(src: ByteArray): Long

    /**
     * 图像调色
     */
    external fun colorCorrection(src: ByteArray, colorCorrectionSettings: ColorCorrectionSettings, cppObjectPtr:Long):IntArray

    /**
     * 删除 ColorCorrection
     */
    external fun deleteColorCorrection(cppObjectPtr:Long): Long

    ......
}

其中,initColorCorrection() 返回的是 Long 类型,其实是一个指针的地址,表示的是一个 C++ 对象。之所以这么做是为了在处理当前图片时,能够复用该  C++ 对象。

viewModel 的 colorCorrection 会调用 ImageProcess 的 colorCorrection() 函数,然后将结果展示到 UI 上。离着这个界面的时候,会清除所有的状态,以及回收所用到的 C++ 对象。

class ColorCorrectionViewModel {

    private val logger: Logger = logger<ColorCorrectionViewModel>()

    var contrast by mutableStateOf(255f )
    var hue by mutableStateOf(180f )
    var saturation by mutableStateOf(255f )
    var lightness by mutableStateOf(255f )
    var temperature by mutableStateOf(255f )
    var highlight by mutableStateOf(255f )
    var shadow by mutableStateOf(255f )
    var sharpen by mutableStateOf(0f )
    var corner by mutableStateOf(0f )

    private var cppObjectPtr:Long = 0

    private var init:AtomicBoolean = AtomicBoolean(false)

    fun colorCorrection(state: ApplicationState, image: BufferedImage, colorCorrectionSettings: ColorCorrectionSettings,
                        success: CVSuccess) {

        logger.info("colorCorrectionSettings = ${GsonUtils.toJson(colorCorrectionSettings)}")

        state.scope.launchWithLoading {
            if (!init.get()) {
                init.set(true)

                val byteArray = image.image2ByteArray()
                cppObjectPtr = ImageProcess.initColorCorrection(byteArray)
            }

            OpenCVManager.invokeCV(image,
                action  = { byteArray -> ImageProcess.colorCorrection(byteArray, colorCorrectionSettings, cppObjectPtr) },
                success = { success.invoke(it) },
                failure = { e ->
                    logger.error("colorCorrection is failed", e)
                })
        }
    }

    ......

    fun clearAllStatus() {
        init.set(false)

        contrast = 255f
        hue = 180f
        saturation = 255f
        lightness = 255f
        temperature = 255f
        highlight = 255f
        shadow = 255f
        sharpen = 0f
        corner = 0f

        colorCorrectionSettings = ColorCorrectionSettings()

        ImageProcess.deleteColorCorrection(cppObjectPtr)
        cppObjectPtr = 0
    }
}

3.2 jni 层的编写

对于 jni 层,需要先在头文件里定义好应用层对应的函数

JNIEXPORT jlong JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_initColorCorrection
(JNIEnv* env, jobject,jbyteArray array);

JNIEXPORT jintArray JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_colorCorrection
        (JNIEnv* env, jobject,jbyteArray array, jobject jobj,  jlong cppObjectPtr);

JNIEXPORT void JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_deleteColorCorrection
        (JNIEnv* env, jobject, jlong cppObjectPtr);

然后,编写对应的实现。

JNIEXPORT jlong JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_initColorCorrection
(JNIEnv* env, jobject, jbyteArray array) {
    Mat image = byteArrayToMat(env, array);

    // 创建 C++ 对象并存储指针
    ColorCorrection* cppObject = new ColorCorrection(image);
    return reinterpret_cast<jlong>(cppObject);
}

JNIEXPORT jintArray JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_colorCorrection
        (JNIEnv* env, jobject,jbyteArray array, jobject jobj,  jlong cppObjectPtr) {

    ColorCorrection* colorCorrection = reinterpret_cast<ColorCorrection*>(cppObjectPtr);
    ColorCorrectionSettings colorCorrectionSettings;

    Mat image = byteArrayToMat(env, array);

    // 获取 jclass 实例
    jclass jcls = env->FindClass("cn/netdiscovery/monica/ui/controlpanel/colorcorrection/model/ColorCorrectionSettings");
    jfieldID contrastId = env->GetFieldID(jcls, "contrast", "I");
    jfieldID hueId = env->GetFieldID(jcls, "hue", "I");
    jfieldID saturationId = env->GetFieldID(jcls, "saturation", "I");
    jfieldID lightnessId = env->GetFieldID(jcls, "lightness", "I");
    jfieldID temperatureId = env->GetFieldID(jcls, "temperature", "I");
    jfieldID highlightId = env->GetFieldID(jcls, "highlight", "I");
    jfieldID shadowId = env->GetFieldID(jcls, "shadow", "I");
    jfieldID sharpenId = env->GetFieldID(jcls, "sharpen", "I");
    jfieldID cornerId = env->GetFieldID(jcls, "corner", "I");
    jfieldID statusId = env->GetFieldID(jcls, "status", "I");

    colorCorrectionSettings.contrast = env->GetIntField(jobj, contrastId);
    colorCorrectionSettings.hue = env->GetIntField(jobj, hueId);
    colorCorrectionSettings.saturation = env->GetIntField(jobj, saturationId);
    colorCorrectionSettings.lightness = env->GetIntField(jobj, lightnessId);
    colorCorrectionSettings.temperature = env->GetIntField(jobj, temperatureId);
    colorCorrectionSettings.highlight = env->GetIntField(jobj, highlightId);
    colorCorrectionSettings.shadow = env->GetIntField(jobj, shadowId);
    colorCorrectionSettings.sharpen = env->GetIntField(jobj, sharpenId);
    colorCorrectionSettings.corner = env->GetIntField(jobj, cornerId);
    colorCorrectionSettings.status = env->GetIntField(jobj, statusId);

    Mat dst;

    try {
        colorCorrection->doColorCorrection(colorCorrectionSettings, dst);
    } catch(...) {
    }

    jthrowable mException = NULL;
    mException = env->ExceptionOccurred();

    if (mException != NULL) {
      env->ExceptionClear();
      jclass exceptionClazz = env->FindClass("java/lang/Exception");
      env->ThrowNew(exceptionClazz, "jni exception");
      env->DeleteLocalRef(exceptionClazz);

      return env->NewIntArray(0);
    }

    env->DeleteLocalRef(jcls);  // 手动释放局部引用

    return matToIntArray(env, dst);
}

JNIEXPORT void JNICALL Java_cn_netdiscovery_monica_opencv_ImageProcess_deleteColorCorrection
        (JNIEnv* env, jobject, jlong cppObjectPtr) {
    // 删除 C++对象,防止内存泄漏
    ColorCorrection* colorCorrection = reinterpret_cast<ColorCorrection*>(cppObjectPtr);
    delete colorCorrection;
}

jni 层还需要调用 C++ 对应的 ColorCorrection 类,这块因为篇幅原因暂时省略,感兴趣的话可以直接看项目的源码。

同意还有一个值得注意的是,从应用层传递的 ColorCorrectionSettings 对象,需要通过 jobject 转换到 jclass 然后再获取对应的各个属性。在 jni 层也需要定义好对应的 ColorCorrectionSettings 对象。

typedef struct {
    int contrast;
    int hue;
    int saturation;
    int lightness;
    int temperature;
    int highlight;
    int shadow;
    int sharpen;
    int corner;
    int status;
} ColorCorrectionSettings;

四. 总结

Monica 支持了图形绘制、图像调色之后,它才算是一款比较基础的图像编辑器。后面还有很长的路要走,Monica 现有的每一个功能都需要打磨一下。

到农历过年前,我对 Monica 的规划是争取完善 CV 算法快速调参的模块和将部分模型部署到云端。如果能完成这些的话,明年可以做更多有意思的功能。

最后,Monica github 地址:https://github.com/fengzhizi715/Monica

Java与Android技术栈】公众号

关注 Java/Kotlin 服务端、桌面端 、Android 、机器学习、端侧智能

更多精彩内容请关注:

内容概要:本文详细介绍了900W或1Kw,20V-90V 10A双管正激可调电源充电机的研发过程和技术细节。首先阐述了项目背景,强调了充电机在电动汽车和可再生能源领域的重要地位。接着深入探讨了硬件设计方面,包括PCB设计、磁性器件的选择及其对高功率因数的影响。随后介绍了软件实现,特别是程序代码中关键的保护功能如过流保护的具体实现方法。此外,文中还提到了充电机所具备的各种保护机制,如短路保护、欠压保护、电池反接保护、过流保护和过温度保护,确保设备的安全性和可靠性。通讯功能方面,支持RS232隔离通讯,采用自定义协议实现远程监控和控制。最后讨论了散热设计的重要性,以及为满足量产需求所做的准备工作,包括提供详细的PCB图、程序代码、BOM清单、磁性器件和散热片规格书等源文件。 适合人群:从事电力电子产品研发的技术人员,尤其是关注电动汽车充电解决方案的专业人士。 使用场景及目标:适用于需要高效、可靠充电解决方案的企业和个人开发者,旨在帮助他们快速理解和应用双管正激充电机的设计理念和技术要点,从而加速产品开发进程。 其他说明:本文不仅涵盖了理论知识,还包括具体的工程实践案例,对于想要深入了解充电机内部构造和工作原理的人来说是非常有价值的参考资料。
内容概要:本文档详细介绍了机器人/AGV/AMR与电梯系统之间的模式切换操作指南。涵盖人工切换模式、智能自动切换以及智慧软件调控三大方面。通过AGV模式切换读卡器实现人工与机器人模式间的转换,利用无源触点隔离技术和多协议适配接口确保电梯与机器人系统的无缝对接,并设有硬件级互锁电路保障安全。智能自动切换中,机器人通过485通讯、TCP/IP等与电梯控制系统交互,在确认安全条件下启动专用模式并及时恢复。故障应急响应策略包括三级容错机制、智能诊断系统和安全优先策略,确保电梯运行的安全性和可靠性。智慧软件调控则根据场景需求灵活调整模式,提升物流高峰时段的运输效率,同时兼顾日常的人性化需求。硬件协同部分介绍了AGV电梯控制主板、楼层触点扩展板、电梯状态检测器、楼层传感器和外呼控制器等组件的作用。 适合人群:从事机器人、AGV、AMR研发与维护的技术人员,以及负责电梯系统集成和管理的专业人士。 使用场景及目标:①实现机器人/AGV/AMR与电梯系统的无缝对接;②确保模式切换过程中的安全性与可靠性;③提高物流高峰时段的运输效率;④优化日常运营中的人性化需求。 其他说明:本文档不仅提供了具体的操作步骤和技术细节,还强调了系统设计的安全性和智能化特点,适用于各类主流电梯品牌,并通过多种通信协议实现了广泛的兼容性。
内容概要:本文详细介绍了如何利用西门子200SMART PLC与两台三菱E700变频器通过Modbus RTU协议实现通信的方法。文中涵盖了硬件准备、通信协议设置、PLC程序设计、变频器设置与响应、源程序及其注释、相关软件工具和使用说明。具体来说,文章首先介绍了所需的硬件设备如PLC、变频器、通信电缆和触摸屏,然后讲解了Modbus RTU通信协议的具体设置,包括波特率、数据位等参数的配置。接着,文章展示了PLC程序的设计思路,重点在于如何用Modbus指令完成数据交互,并通过详细的注释使程序易于理解。此外,还涉及了变频器作为从站的设置方法,确保其能正确响应PLC发出的命令。最后,提供了昆仑通泰触摸屏软件、200SMART V2.4编程软件、变频器应用说明书等辅助工具,方便用户更好地掌握整个系统的运作。 适用人群:从事工业自动化领域的工程师和技术人员,尤其是那些需要深入了解PLC与变频器间通信机制的人群。 使用场景及目标:适用于希望构建高效稳定的工业控制系统的企业和个人开发者。通过学习本文提供的实例,读者可以掌握PLC与变频器之间的Modbus通信技术,从而实现对生产设备的精准控制,提升工作效率的同时减少能源消耗。 其他说明:本文不仅提供了理论指导,还有实际的操作指南和完整源代码,有助于读者快速上手实践。同时,配套的软件和文档资料也为后续的学习和项目开发打下了坚实的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值