Compose For Desktop 实践:使用 Compose-jb 做一个时间水印助手

news2024/12/23 11:12:40

前言

在我之前的文章 在安卓中实现读取Exif获取照片拍摄日期后以水印文字形式添加到照片上 中,我们已经实现了在安卓端读取 Exif 信息后添加文字水印到图片上。

也正如我在这篇文章中所说的,其实这个需求使用手机来实现是非常不合理的,一般来说,这种工作都应该交由桌面端来实现。

而我在上篇文章中所述之所以没有使用 Compose-jb 实现跨平台的原因是没有找到合适的跨平台图片编辑库。

虽然现在依旧没有合适的跨平台编辑库,但是我现在决定做一个纯粹的桌面端,而不是继续拘泥于跨平台。

如此一来,可选择的库就多了。

先来看看实现效果:

原谅我的 UI 一如既往的丑,希望各位看官别在意,我们主要是实现需求,能用就行能用就行。

s1.png

s2.png

得益于 Compose 的特性,这个程序同时支持 Mac、Windows、Linux 系统。

代码地址:TimelapseHelper

UI布局

UI布局总体来说分为左右两个部分:左边的图像预览区(ImageContent)、右边的参数控制区(ControlContent)。

为了确保我们的内容能够完整显示,我们需要首先在 Window 入口处设置窗口最小尺寸限制:

window.minimumSize = Dimension(MinWindowSize.width.value.roundToInt(), MinWindowSize.height.value.roundToInt())

其中 MinWindowSize 是我自定义的一个变量:val MinWindowSize = DpSize(1100.dp, 700.dp)

下面分开讲解两个部分的UI布局。

ImageContent

图像预览区同样分为两个部分:上面的图像预览、下面的文件列表。

因为桌面端需要支持批量处理,一次可以添加不限制数量的多张图片,所以还需要加上一个文件列表,用来展示当前添加了那些文件。

具体代码如下:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ImageContent(
    onclick: () -> Unit,
    onDel: (index: Int) -> Unit,
    fileList: List<File> = emptyList()
) {
    var showImageIndex by remember { mutableStateOf(0) }

    Card(
        onClick = onclick,
        modifier = Modifier.size(CardSize).padding(16.dp),
        shape = RoundedCornerShape(8.dp),
        elevation = 4.dp,
        backgroundColor = CardColor,
        enabled = fileList.isEmpty()
    ) {
        if (fileList.isEmpty()) {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "请点击选择文件(夹)或拖拽文件(夹)至此\n仅支持 ${legalSuffixList.contentToString()}",
                    textAlign = TextAlign.Center
                )
            }
        }
        else {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Image(
                    bitmap = fileList[showImageIndex.coerceAtMost(fileList.lastIndex)].inputStream().buffered().use(::loadImageBitmap),
                    contentDescription = null,
                    modifier = Modifier.height(CardSize.height / 2).fillMaxWidth(),
                    contentScale = ContentScale.Fit
                )

                LazyColumn(
                    modifier = Modifier.fillMaxWidth()
                ) {
                    item {
                        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
                            Button(onClick = onclick ) {
                                Text("添加")
                            }
                            Button(onClick = { onDel(-1) }) {
                                Text("清空")
                            }
                        }

                    }

                    itemsIndexed(fileList) {index: Int, item: File ->
                        Row(
                            modifier = Modifier.fillMaxWidth(),
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.SpaceBetween
                        ) {
                            Text(
                                item.absolutePath,
                                modifier = Modifier.clickable {
                                    showImageIndex = index
                                }.weight(0.9f),
                            )

                            Icon(
                                imageVector = Icons.Rounded.Delete,
                                contentDescription = null,
                                modifier = Modifier.clickable {
                                    onDel(index)
                                }.weight(0.1f)
                            )
                        }
                    }
                }
            }
        }
    }
}

布局很简单,使用 Card 作为父布局,然后判断传入的文件列表是否为空 fileList.isEmpty() ,如果为空则显示提示文本,不为空则显示图像和文件列表。

在这里我们定义了一个名为 showImageIndexmutableState 用于记录当前显示预览的是第几个图像文件。

在我们点击 LazyColumn 中的文件时,会对应的更改这个值。

上面的代码我们还需要注意一点,那就是关于如何加载本地文件并显示。

我们使用的是 File.inputStream().buffered().use(::loadImageBitmap) 从这段代码不难看出,我们读取文件的输入流(inputStream)后,通过 loadImageBitmap 转为了 Image 组件支持的参数类型 ImageBitmap

同时,我们还将 LazyColumn 的第一列写为了两个按钮 “添加” 和 “清空” ,用于方便的继续添加文件和清空所有文件。

并且,每一个文件名称后面,我们都会跟上一个删除图标,用于删除单个文件。

效果如下:

s3.png

另外,在没有选中任何文件时,这个界面支持直接将文件或文件夹拖拽到应用中,也支持点击后打开文件选择界面。这部分内容的具体实现我们将在后面的实现逻辑中解释。

ControlContent

参数控制界面的效果如下:

s4.png

可以看到,这个界面无非就是一堆控件的堆叠,没有任何难度,所以我就不贴代码了。

需要注意的地方有两点:

一是布局之间会有关联影响,比如第一个 “输出路径” 这个参数,如果勾选了 “输出至原路径” ,则将输入框和"选择"按钮禁用,并更改输入框内容为 “原路径”。

实现起来也很简单,这里直接上代码:


var isUsingSourcePath by remember { mutableStateOf(true) }

// ……

Row(
    verticalAlignment = Alignment.CenterVertically,
) {
    Text("输出路径:")
    OutlinedTextField(
        value = outputPath,
        onValueChange = { outputPath = it },
        modifier = Modifier.width(CardSize.width / 3),
        enabled = !isUsingSourcePath
    )
    Button(
        onClick = {
                  // ……
        },
        modifier = Modifier.padding(start = 8.dp),
        enabled = !isUsingSourcePath
    ) {
        Text("选择")
    }
    Checkbox(
        checked = isUsingSourcePath,
        onCheckedChange = {
            isUsingSourcePath = it
            outputPath = if (it) "原路径" else ""
        }
    )
    Text("输出至原路径", fontSize = 12.sp)
}

另外一个需要注意的点是我们需要对输入框的内容做过滤。

因为实际上我们输入框中的内容基本都是有固定格式的。

比如第二个输入框 “导出图像质量”,需要限定输入内容为 0-1 的浮点数。

第三个输入框 “文字颜色”,输入格式为首字母为 “#” 剩下的是八位十六进制数。

最后一个输入框 “时区”,格式为首字母固定 “GMT” ,接下来紧跟一个 “+” 或者 “-”,最后是固定的 “xx:xx” 格式,其中 xx 可以是任意数字。(其实这里的时区可以使用多种表示方式,但是这里我们人为限制只能使用这种标准表示方式)

因为输入内容过滤我还没玩明白,所以这里就暂时不说了,等我玩明白了会另开一篇文章讲解。(我绝对不会承认其实是我代码在另外一台电脑上忘记 push 到 github 了,而我一时半会拿不到这台电脑)

逻辑代码

读取 Exif

由于我们这次是给桌面端写的程序,所以之前使用的安卓官方的 Exif 库显然是用不了的,好在我们有一大堆 java 库可以使用。

这里我选择的是 metadata-extractor 这个库。

首先在 build.gradle.kts 文件中添加依赖:

dependencies {
    commonMainImplementation("com.drewnoakes:metadata-extractor:2.18.0")
}

接下来是示例化 Metadata 对象:

val metadata = ImageMetadataReader.readMetadata(file)

这里因为我们传入的文件本来就是 File 类型,所以我们直接使用 File 实例化。

除此之外我们还可以使用输入流实例化:

val metadata = ImageMetadataReader.readMetadata(inputStream)

示例化完成后就是读取特定的 Exif 标签内容,这里我们直接读取 DATETIME_ORIGINAL 标签,不知道各个标签是什么意思的可以看我之前的文章,里面有详细解释:

val directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory::class.java)
val date = directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)

这样我们就能拿到一个 Date 对象,接下只要解析这个 Date 即可。

但是,你们觉得这样就完了吗?

非也非也,一开始我也以为这样就完了。

直到我实际使用时却发现,这样获取到的时间总是和实际时间相差八个小时。

不多不少刚刚好八个小时,有经验的读者可能已经意识到了,八个小时,那不就是时区不对嘛,因为中国的官方时区就是 GMT+08:00 。

其实这个问题也很好理解,正如我之前文章中所述,在旧版本的 Exif 标准中,并没有指定时区这一内容,也就是说, Exif 中保存的时间不包含时区信息,所以我们需要自己重新解析时区。

但是这里又出现一个问题,我们不能将时区写死,因为我们不能假定我们的用户就一定是某个时区的人,亦或者说,我们怎么能保证我们拍照就一定是在 GMT+08:00 拍呢?格局大一点。(狗头

所以,我这里将时区的选择权交给了用户自己,也就是我们上面 UI 一节中所示的需要用户自己输入时区信息。

所以,最终完整的获取 Exif 的函数应该是:

fun getDateFromExif(
    file: File,
    timeZoneID: String
): Date? {
    return try {
        val timeZone = TimeZone.getTimeZone(timeZoneID)
        val metadata = ImageMetadataReader.readMetadata(file)
        val directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory::class.java)
        directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL, timeZone)
    } catch (tr: Throwable) {
        tr.printStackTrace()
        null
    }
}

给图片添加文字水印

对于给图片添加文字水印这个需求,我们使用 JDK 中自带的 Graphics2D 来实现。

使用 Graphics2D 需要先从文件中读取文件流,然后将文件流转为 BufferedImage ,最后使用 BufferedImage 创建 Graphics2D 对象,文字添加完毕后再将 BufferedImage 写入文件中即可。

简单实现代码如下:

// 读取原文件
val targetImg: BufferedImage = ImageIO.read(file)
// 创建 Graphics2D
val graphics: Graphics2D = targetImg.createGraphics()
// 往 Graphics2D 上绘制文字
graphics.drawString(text, x, y)
// 保存文件
saveImage(targetImg, outPath, outputQuality)
// 关闭
graphics.dispose()

其中保存 BufferedImage 的函数如下:

fun saveImage(image: BufferedImage?, saveFile: File?, quality: Float) {
    val outputStream = ImageIO.createImageOutputStream(saveFile)
    val jpgWriter: ImageWriter = ImageIO.getImageWritersByFormatName("jpg").next()
    val jpgWriteParam: ImageWriteParam = jpgWriter.defaultWriteParam
    jpgWriteParam.compressionMode = ImageWriteParam.MODE_EXPLICIT
    jpgWriteParam.compressionQuality = quality
    jpgWriter.output = outputStream
    val outputImage = IIOImage(image, null, null)
    jpgWriter.write(null, outputImage, jpgWriteParam)
    jpgWriter.dispose()
    outputStream.flush()
    outputStream.close()
}

saveImage 这个函数接收一个名为 quality 用于指定保存文件时的质量。

具体实现是通过设置参数 jpgWriteParam.compressionQuality = quality

保存的时候记得要定义这个参数,否则默认值设置的压缩率比较大,一开始我没有设置这个值,导致我十几Mb的图片添加文字后只剩下了几百Kb,画质也肉眼可见的变差,都给我整不会了,这样显然是不符合我的要求的啊。

下面再看看给图片添加水印的具体实现代码:graphics.drawString(text, x, y)

第一个参数很好理解,就是要添加的文字字符串,第二和第三个参数分别表示放置文字的位置坐标。

这里的坐标表示的是第一个字符的基线坐标。

那么问题来了,坐标怎么拿呢?

还记得我们的UI界面吗?我们的软件是可以定义水印位置的,可以选择图片的四个角。

也就是说,我们需要单独处理一下坐标的计算:

// 水印坐标位置
val width: Int = targetImg.width //图片宽
val height: Int = targetImg.height //图片高
val textWidth = graphics.fontMetrics.stringWidth(text)
val textHeight = graphics.fontMetrics.height
val point = textPos.getPoint(width, height, textWidth, textHeight)
val x = point.x
val y = point.y

// ……

private fun TextPos.getPoint(
    width: Int,
    height: Int,
    textWidth: Int,
    textHeight: Int,
    padding: Int = 10
): Point {
    return when (this) {
        TextPos.LEFT_TOP -> {
            Point(padding, textHeight)
        }
        TextPos.LEFT_BOTTOM -> {
            Point(
                padding,
                (height - padding).coerceAtLeast(0)
            )
        }
        TextPos.RIGHT_TOP -> {
            Point(
                (width - textWidth - padding).coerceAtLeast(0),
                textHeight
            )
        }
        TextPos.RIGHT_BOTTOM -> {
            Point(
                (width - textWidth - padding).coerceAtLeast(0),
                (height - padding).coerceAtLeast(0)
            )
        }
    }
}

上面的 x、y 即计算出来的坐标。

其中,TextPos 是我定义的一个枚举类:

enum class TextPos {
    LEFT_TOP,
    LEFT_BOTTOM,
    RIGHT_TOP,
    RIGHT_BOTTOM
}

在上面的获取坐标的函数 getPoint 中,我们通过文字的高度 textHeight = graphics.fontMetrics.height ;所有文字的宽度 textWidth = graphics.fontMetrics.stringWidth(text) ,按照用户选择的文字位置计算出文字应该位于的坐标点。

例如,如果选择水印在左上角,则 x 坐标为 0(实际还添加了 padding),y 坐标为 文字高度

如果为右下角,则 x 坐标为 图片宽度 - 文字总宽度,y 坐标为 图片高度

现在,添加文字的代码已经全部完成,但是我们还需要加亿点小细节,例如设置文字大小,设置文字颜色等:

graphics.color = textColor //水印颜色
graphics.font = Font(null, Font.PLAIN, fontSize) // 文字样式,第一个参数是字体,这里直接使用 Null(因为支持多种桌面端,指定字体的话可能反而会找不到)

选择文件

直接调用文件选择

这里我们使用的是 java swing 中的文件选择器: JFileChooser 来实现文件选择功能:

fun showFileSelector(
    suffixList: Array<String> = arrayOf("jpg", "jpeg"), // 过滤的文件扩展名
    isMultiSelection: Boolean = true,  // 是否允许多选
    selectionMode: Int = JFileChooser.FILES_AND_DIRECTORIES, // 可以选择目录和文件
    selectionFileFilter: FileNameExtensionFilter? = FileNameExtensionFilter("图片(.jpg .jpeg)", *suffixList), // 文件过滤
    onFileSelected: (Array<File>) -> Unit, // 选择回调
    ) {
    JFileChooser().apply {
        // 这里是设置选择器的 UI
        try {
            val lookAndFeel = UIManager.getSystemLookAndFeelClassName()
            UIManager.setLookAndFeel(lookAndFeel)
            SwingUtilities.updateComponentTreeUI(this)
        } catch (e: Throwable) {
            e.printStackTrace()
        }

        fileSelectionMode = selectionMode
        isMultiSelectionEnabled = isMultiSelection
        fileFilter = selectionFileFilter

        // 显示选择器
        val result = showOpenDialog(ComposeWindow())
        
        // 选择后返回
        if (result == JFileChooser.APPROVE_OPTION) {
            if (isMultiSelection) {
                // this.selectedFiles 表示选中的多个文件 array,只有 isMultiSelectionEnabled 为 true 这个变量才有值,否则为 NUll
                onFileSelected(this.selectedFiles)
            }
            else {
                // 如果不开启多选,则返回的是单个文件 this.selectedFile ,但是我们回调接收的是 Array,所以需要手动创建
                val resultArray = arrayOf(this.selectedFile)
                onFileSelected(resultArray)
            }
        }
    }
}

代码很简单,这里就不再过多解释了,需要注意的点已经在注释中说明。

拖拽选择

拖拽选择需要调用到 awt 的原生代码。

我们需要给主入口的 window 添加一个 dropTarget 用于接收拖拽事件:

window.contentPane.dropTarget = dropFileTarget { fileList ->
    println(fileList)
}

其中,dropFileTarget 函数如下:

fun dropFileTarget(
    onFileDrop: (List<String>) -> Unit
): DropTarget {
    return object : DropTarget() {
        override fun drop(event: DropTargetDropEvent) {

            event.acceptDrop(DnDConstants.ACTION_REFERENCE)
            val dataFlavors = event.transferable.transferDataFlavors
            dataFlavors.forEach {
                if (it == DataFlavor.javaFileListFlavor) {
                    val list = event.transferable.getTransferData(it) as List<*>

                    val pathList = mutableListOf<String>()
                    list.forEach { filePath ->
                        pathList.add(filePath.toString())
                    }
                    onFileDrop(pathList)
                }
            }
            event.dropComplete(true)
        }
    }
}

需要注意的是,因为我们这个拖拽事件是添加到主入口的 window 的,而不是单独的图像预览 Card 这意味着接收拖拽事件的是整个程序窗口而不是单独的这个图像预览界面。

过滤文件

完成上面两种的选择文件代码后,我们的处理逻辑还没有完哦,别忘了,我们说过,这个文件选择支持多选文件,甚至是文件夹。

这意味着我们需要对传入的选择文件(夹)做遍历以及过滤处理:

fun filterFileList(fileList: List<String>): List<File> {
    val newFile = mutableListOf<File>()
    fileList.map {path ->
        newFile.add(File(path))
    }

    return filterFileList(newFile.toTypedArray())
}

fun filterFileList(fileList: Array<File>): List<File> {
    val newFileList = mutableListOf<File>()

    for (file in fileList) {
        if (file.isDirectory) {
            newFileList.addAll(getAllFile(file))
        }
        else {
            if (file.extension.lowercase() in legalSuffixList) {
                newFileList.add(file)
            }
        }
    }

    return newFileList
}

private fun getAllFile(file: File): List<File> {
    val newFileList = mutableListOf<File>()
    val fileTree = file.walk()
    fileTree.maxDepth(Int.MAX_VALUE)
        .filter { it.isFile }
        .filter { it.extension.lowercase() in legalSuffixList }
        .forEach {
            newFileList.add(it)
        }

    return newFileList
}

然后在选择文件的回调处调用即可。

上面的代码做的工作就是遍历接收到的文件列表,如果是文件则判断扩展名是否符合需求,符合则添加至文件列表。

如果是文件夹则使用 FileTreeWalk 遍历这个文件夹,然后找出符合条件的文件添加至文件列表,这里我们的遍历深度是最大(Int.MAX_VALUE)也就是说会遍历该文件的所有子文件,以及子文件夹,包括所有深度的子文件夹的所有子文件。

总结

Compose-jb 让原本的移动端开发者也能很方便的进行桌面端开发,但是毕竟 Compose 只是一个 UI 工具包,对于实际的业务逻辑代码,还是需要调用原生 API 来实现。

好在 Kotlin 是 jvm 语言,并且 Compose-jb 的实现也是基于 java 的 Swing ,也就是说对于安卓开发者来说,即使很多逻辑需要调用的也只是 Swing API ,对于安卓开发来说,基本没有什么门槛,看一下文档基本就能上手写了。

参考资料

  1. 使用ComposeDesktop开发一款桌面端多功能APK工具
  2. From Swing to Jetpack Compose Desktop #2
  3. Java中图片添加水印(文字+图片水印)
  4. Image and in-app icons manipulations

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550153.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CPU和显卡才是最抗热的?

高温是电脑蓝屏和掉帧的罪魁祸首&#xff0c;虽然硬件有了保护不会因为高温烧坏&#xff0c;但当你的工作进行到一半时突然蓝屏&#xff0c;或是游戏中的关键时刻突然掉帧&#xff0c;你的内心肯定是崩溃的&#xff0c;那么电脑中的硬件温度应该控制在多少度呢&#xff1f; 首先…

二进制部署高可用Kubernetes集群 (成功) 看报错

SUMMARY 参考网上的教程和网课&#xff0c;进行二进制高可用Kubernetes部署。并对整个过程和报错进行简单记录。 架构图 设备规划 序号名字功能VMNET 1备注 1备注 2备注 3 备注 4备注 50orgin界面192.168.164.10haproxykeepalived192.168.164.2001reporsitory仓库192.168.1…

〖大学生·技术人必学的职业规划白宝书 - 优质简历篇②〗- 面试官所青睐的优秀简历是什么样的?

历时18个月&#xff0c;采访 850 得到的需求。 不管你是在校大学生、研究生、还是在职的小伙伴&#xff0c;该专栏有你想要的职业规划、简历、面试的答案。说明&#xff1a;该文属于 大学生技术人职业规划白宝书 专栏&#xff0c;购买任意白宝书体系化专栏可加入TFS-CLUB 私域社…

【OpenCv • c++】形态学技术操作 —— 顶帽操作与黑帽操作

&#x1f680; 个人简介&#xff1a;CSDN「博客新星」TOP 10 &#xff0c; C/C 领域新星创作者&#x1f49f; 作 者&#xff1a;锡兰_CC ❣️&#x1f4dd; 专 栏&#xff1a;【OpenCV • c】计算机视觉&#x1f308; 若有帮助&#xff0c;还请关注➕点赞➕收藏&#xff…

Ae 入门系列之十一:抠像

抠像&#xff0c;英文为 Keying&#xff0c;故也被称为“键控”。在早期电视节目制作中&#xff0c;意思是吸取画面中的某一颜色并使得此颜色相关区域成为透明&#xff0c;从而保留所需要的内容。 我们通常称要抠出&#xff08;要保留&#xff09;的主体为前景 Foreground&…

进程间通信-管道

文章目录 1. 进程间通信介绍1.1 进程间通信目的1.2 进程间通信分类 2. 管道2.1 什么是管道2.2 站在文件描述符角度-深度理解管道2.2.1 具体通信的过程 2.3 匿名管道2.4 代码实现 3. 进程控制4. 管道读写规则5. 管道特点6. 命名管道6.1 创建一个命名管道6.2 代码实现 1. 进程间通…

索引的底层数据结构,讲讲B+ 树,B树

B 树的变种。 B树&#xff1a;所有节点&#xff08;非叶子节点 叶子节点&#xff09;&#xff0c;都存储真正的行数据&#xff0c;所以一个数据页能存储的数量&#xff0c;相较于 B 树&#xff0c;就少很多。 B 树&#xff1a; ● 只有叶子节点存储具体的行数据&#xff0c;非…

【进阶】C 语言表驱动法编程原理与实践

数据压倒一切。如果选择了正确的数据结构并把一切组织的井井有条&#xff0c;正确的算法就不言自明。编程的核心是数据结构&#xff0c;而不是算法。——Rob Pike 目录 说明 概念提出 查表方式 直接查找 索引查找 分段查找 实战示例 字符统计 月天校验 名称构造 值名…

Python——sentenceSimilarity 的简单demo(测试句子相似度)

一、sentenceSimilarity 是什么&#xff1f; sentenceSimilarity 属于机器学习的领域 Python 中的 sentenceSimilarity 库是一个用于计算句子相似度的工具库&#xff0c;主要用于自然语言处理相关的应用中。该库支持多种模型计算句子相似度&#xff0c;包括 TF-IDF、LSI、LDA 等…

MySQL进阶篇(三)

七.MySQL管理 7.1 系统数据库 Mysql数据库安装完成后&#xff0c;自带了一下四个数据库&#xff0c;具体作用如下&#xff1a; 7.2 常用工具 7.2.1 mysql 该mysql不是指mysql服务&#xff0c;而是指mysql的客户端工具。 -e选项可以在Mysql客户端执行SQL语句&#xff0c;而…

【Linux】Linux环境基础开发工具使用(yum和vim)

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;Linux ⭐代码仓库&#xff1a;Linux 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们的支持是我…

NSFW 图片分类

NSFW指的是 不适宜工作场所&#xff08;“Not Safe (or Suitable) For Work;”&#xff09;。在本文中&#xff0c;将介绍如何创建一个检测NSFW图像的图像分类模型。 数据集 由于数据集的性质&#xff0c;我们无法从一些数据集的网站(如Kaggle等)获得所有图像。 但是我们找到…

少儿编程 中国电子学会图形化编程等级考试Scratch编程四级真题解析(选择题)2023年3月

2023年3月scratch编程等级考试四级真题 选择题(共25题,每题2分,共50分) 1、编写一段程序,从26个英文字母中,随机选出10个加入列表a。空白处应填入的代码是 A、 B、 C、 D、 答案:C

[CTF/网络安全] 攻防世界 simple_php 解题详析

[CTF/网络安全] 攻防世界 simple_php 解题详析 代码解读PHP弱语言特性姿势参数a限制绕过参数b限制绕过 总结 题目描述&#xff1a;小宁听说php是最好的语言,于是她简单学习之后写了几行php代码。 代码解读 $a$_GET[a]; 从HTTP GET请求参数中获取一个名为a的变量&#xff0c…

协同过滤算法的召回率、准确率、覆盖率、新颖度

python版计算协同过滤推荐算法的召回率、准确率、覆盖率、新颖度 推荐算法网站示例Demo 点我跳转图书管理推荐系统 点我跳转课程推荐系统 点我跳转电影推荐系统 1、召回率、准确率 2、覆盖率、新颖度 覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将…

ChatGPT开始颠覆学习方式,应试教育面临哪些挑战?

ChatGPT爆火几个月&#xff0c;整个教育系统都在被颠覆。全球范围内&#xff0c;不少大学教授、系主任和管理人员&#xff0c;都在对课堂进行大规模的调整&#xff0c;以应对ChatGPT对教学活动造成的巨大冲击。 国内传统应试教育选出的分霸、考霸&#xff0c;是更能吃苦&#…

c++中的方法

c中的方法 static方法 与数据成员类似&#xff0c;方法有时会应用于全部对象而不是单个对象。可以编写static方法和数据成员。在方法声明前加上static即可。对于方法的定义前则不需要重复使用static关键字。 class Foo { public:static int sumFunc(int a, int b); };int Fo…

康耐视Visionpro工具-CogPMAlignTool为什么是最牛工具?

1.算法:有六种选项,分别是:PatMax,PatQuick, PatMax 与 PatQuick, PatFlex,PatMax-高灵敏度,透视 Patmax。 PatQuick 特点:速度最快,对于三维或者低质量原件最佳,承受更多图像差异; PatMax 特点:精确度最高,在二维元件上表现佳,最适合于细微细节; PatFlex 特点…

4. 通讯录实现的需求分析和架构设计

本文实现的是通讯录产品的需求分析和架构设计&#xff0c;重点在于结构层次的设计&#xff0c;方便代码阅读和维护。 一、通讯录实现的需求分析 1、通讯录的功能清单 添加一个人员打印显示所有人员删除一个人员查找一个人员保存文件加载文件 2&#xff0c;数据存储信息 人员…

[CTF/网络安全] 攻防世界 disabled_button 解题详析

[CTF/网络安全] 攻防世界 disabled_button 解题详析 input标签姿势disable属性总结 题目描述&#xff1a;X老师今天上课讲了前端知识&#xff0c;然后给了大家一个不能按的按钮&#xff0c;小宁惊奇地发现这个按钮按不下去&#xff0c;到底怎么才能按下去呢&#xff1f; input标…