有没有想过一张照片中除了能看到的图像,还隐藏了多少信息?
一、相关信息了解
怀着好奇心开启对图片信息的探索之旅。
首先了解到的一个名词EXIF,引用维基百科上的解释
可交换图像文件格式(英语:Exchangeable image file format,官方简称Exif),是专门为数码相机的照片设定的文件格式,可以记录数码照片的属性信息和拍摄数据。
其中包含的信息,同样来自维基百科
项目 | 信息(举例) |
---|---|
制造厂商 | Canon |
相机型号 | Canon EOS-1Ds Mark III |
图像方向 | 正常(upper-left) |
图像分辨率X | 300 |
图像分辨率Y | 300 |
分辨率单位 | dpi |
软件 | Adobe Photoshop CS Macintosh |
最后异动时间 | 2005:10:06 12:53:19 |
YCbCrPositioning | 2 |
曝光时间 | 0.00800 (1/125) sec |
光圈值 | F22 |
拍摄模式 | 光圈优先 |
ISO感光值 | 100 |
Exif信息版本 | 30,32,32,31 |
图像拍摄时间 | 2005:09:25 15:00:18 |
图像存入时间 | 2005:09:25 15:00:18 |
曝光补偿(EV+-) | 0 |
测光模式 | 点测光(Spot) |
闪光灯 | 关闭 |
镜头实体焦长 | 12 mm |
Flashpix版本 | 30,31,30,30 |
图像色域空间 | sRGB |
图像尺寸X | 5616 pixel |
图像尺寸Y | 3744 pixel |
二、方案调研
那在Android上我们怎么获取到这些信息呢?
搜索官方API文档,可以发现其中有个
android.media.ExifInterface,虽然它后缀是Interface,但它不是接口,是一个类,不过文档中已经建议使用AndroidX ExifInterface Library,按照指示走就完事了,又杠不过他。
根据文档中的介绍,这是一个用于读写各种图像文件格式的Exif标签的类
This is a class for reading and writing Exif tags in various image file formats.
Supported for reading: JPEG, PNG, WebP, HEIF, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF.
Supported for writing: JPEG, PNG, WebP.
其中包含了很多TAG,简单举几个例子
TAG_DATETIME | 图像创建的时间和 |
TAG_MAKE | 制造商 |
TAG_MODEL | 设备名称或型号 |
TAG_SOFTWARE | 记录用于生成图像的相机或图像输入设备的软件或固件的名称和版本 |
TAG_GPS_LONGITUDE | 经度(度分秒格式) |
TAG_GPS_LATITUDE | 维度(度分秒格式) |
获取这些tag信息可以getAttribute方法,既然官方已经提供了方法,那我们就着手提取信息,开始编码。
三、代码实现
ExifInterface要读图片信息首先要有图片路径
val intent = Intent(Intent.ACTION_PICK)
intent.data = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
startActivityForResult(intent, REQUEST_CODE_ALBUM_PIC)
这里我们调用系统相册来获取图片路径,不过在onActivityResult中获得的路径是content://media/external/images/media/119460形式的,但是ExifInterface需要的是真实的文件路径,所以需要转换一下
private fun getFilePathFromContentUri(
selectedVideoUri: Uri?,
contentResolver: ContentResolver
): String? {
val filePath: String
val filePathColumn = arrayOf(MediaColumns.DATA)
val cursor: Cursor =
contentResolver.query(selectedVideoUri, filePathColumn, null, null, null)
cursor.moveToFirst()
val columnIndex: Int = cursor.getColumnIndex(filePathColumn[0])
filePath = cursor.getString(columnIndex)
cursor.close()
return filePath
}
转换后的路径如下/storage/emulated/0/DCIM/Camera/IMG_20200419_154706.jpg
路径有了以后就可以愉快的获取图片信息了
private fun getInfo(path: String) {
try {
val exifInterface = ExifInterface(path)
val dateTime = exifInterface.getAttribute(ExifInterface.TAG_DATETIME)
val length = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)
val width = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)
val model = exifInterface.getAttribute(ExifInterface.TAG_MODEL)
val make = exifInterface.getAttribute(ExifInterface.TAG_MAKE)
val software = exifInterface.getAttribute(ExifInterface.TAG_SOFTWARE)
val altitude_ref =
exifInterface.getAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF)
val altitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_ALTITUDE)
val latitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE)
val latitude_ref =
exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF)
val longitude_ref =
exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF)
val longitude =
exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE)
val timestamp =
exifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP)
val processing_method =
exifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD)
//转换经纬度格式
lat = convert2Decimal(latitude)
lon = convert2Decimal(longitude)
val stringBuilder = StringBuilder()
stringBuilder
.append("时间 = $dateTime\n")
.append("长 = $length\n")
.append("宽 = $width\n")
.append("型号 = $model\n")
.append("制造商 = $make\n")
.append("软件 = $software\n")
.append("海拔高度 = $altitude_ref\n")
.append("GPS参考高度 = $altitude\n")
.append("GPS时间戳 = $timestamp\n")
.append("GPS定位类型 = $processing_method\n")
.append("GPS纬度 = $lat$latitude_ref\n")
.append("GPS经度 = $lon$longitude_ref\n")
//将获取的到的信息设置到TextView上
imageInfo.text = stringBuilder.toString()
} catch (e: IOException) {
e.printStackTrace()
}
}
这样获取的经纬度是度分秒形式,例如41/1,8/1,446548/10000,为了方便使用将其转换小数形式
private fun convert2Decimal(string: String?): Double {
var dimensionality = 0.0
if (null == string) {
return dimensionality
}
Log.i(TAG, string)
//用,将数值分成3份
val split = string.split(",").toTypedArray()
for (i in split.indices) {
val s = split[i].split("/").toTypedArray()
//用114/1得到度分秒数值
val v = s[0].toDouble() / s[1].toDouble()
//将分秒分别除以60和3600得到度,并将度分秒相加
dimensionality += v / 60.0.pow(i.toDouble())
}
return dimensionality
}
我找了一张手机拍摄的照片原图进行测试,效果如下,其中拍摄时间、手机型号、地理位置都被查询出来了
为了更直观的查看位置信息,可以接入地图的sdk,这里我接入了高德的sdk,在gradle中加入相关依赖
dependencies {
...
// 3D地图so及jar
implementation 'com.amap.api:3dmap:latest.integration'
// 定位功能
implementation 'com.amap.api:location:latest.integration'
//搜索功能
implementation 'com.amap.api:search:latest.integration'
}
实现了两个功能
-
通过经纬度定位将地图定位到指定位置
-
通过经纬度获取地方名称并显示在地图上
布局很简单,只有一个AMap
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.amap.api.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Activity实现如下,关键代码已经添加注释
package com.ax.detectivetools.Map
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import com.amap.api.maps.AMap
import com.amap.api.maps.CameraUpdateFactory
import com.amap.api.maps.model.CameraPosition
import com.amap.api.maps.model.LatLng
import com.amap.api.maps.model.MarkerOptions
import com.amap.api.services.core.LatLonPoint
import com.amap.api.services.geocoder.GeocodeResult
import com.amap.api.services.geocoder.GeocodeSearch
import com.amap.api.services.geocoder.RegeocodeQuery
import com.amap.api.services.geocoder.RegeocodeResult
import com.ax.detectivetools.R
import kotlinx.android.synthetic.main.activity_map.*
class MapActivity : AppCompatActivity(), GeocodeSearch.OnGeocodeSearchListener {
private var lat = 0.0
private var lon = 0.0
private lateinit var aMap: AMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_map)
mapView.onCreate(savedInstanceState)
aMap = mapView.map
lat = intent.getDoubleExtra("lat", 0.0)
lon = intent.getDoubleExtra("lon", 0.0)
// 通过经纬度获取地址信息
val geocoderSearch = GeocodeSearch(this)
geocoderSearch.setOnGeocodeSearchListener(this)
val query = RegeocodeQuery(LatLonPoint(lat, lon), 200f, GeocodeSearch.AMAP)
geocoderSearch.getFromLocationAsyn(query)
}
private fun locate(address: String) {
val latLng = LatLng(lat, lon)
// 地图上显示位置信息
aMap.addMarker(MarkerOptions().position(latLng).title("地址:").snippet(address))
// 地图定位到对应位置
aMap.moveCamera(CameraUpdateFactory.newCameraPosition(CameraPosition(latLng, 10f, 0f, 0f)))
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle?, outPersistentState: PersistableBundle?) {
super.onSaveInstanceState(outState, outPersistentState)
mapView.onSaveInstanceState(outState)
}
companion object {
private const val TAG = "MapActivity"
}
/**
* 通过经纬度进行逆地理编码获得地址信息后回调
*/
override fun onRegeocodeSearched(p0: RegeocodeResult?, p1: Int) {
p0?.regeocodeAddress?.formatAddress?.let { locate(it) }
}
override fun onGeocodeSearched(p0: GeocodeResult?, p1: Int) {
}
}
效果如下
tips:
-
建议在不必要的情况下关闭照相机的保存位置信息功能
-
给别人发送图片时尽量避免发送原图
欢迎关注公众号:从技术到艺术