react-native-facefusion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +259 -0
- package/android/build.gradle +129 -0
- package/android/src/main/AndroidManifest.xml +66 -0
- package/android/src/main/cpp/CMakeLists.txt +73 -0
- package/android/src/main/java/com/facefusion/BitmapDecode.kt +112 -0
- package/android/src/main/java/com/facefusion/ContentGate.kt +86 -0
- package/android/src/main/java/com/facefusion/DeviceProbe.kt +158 -0
- package/android/src/main/java/com/facefusion/FaceCrop.kt +57 -0
- package/android/src/main/java/com/facefusion/FacefusionModule.kt +396 -0
- package/android/src/main/java/com/facefusion/FacefusionPackage.kt +37 -0
- package/android/src/main/java/com/facefusion/FacefusionPreviewView.kt +30 -0
- package/android/src/main/java/com/facefusion/FacefusionPreviewViewManager.kt +25 -0
- package/android/src/main/java/com/facefusion/GallerySave.kt +82 -0
- package/android/src/main/java/com/facefusion/ModelDownload.kt +351 -0
- package/android/src/main/java/com/facefusion/ModelPaths.kt +120 -0
- package/android/src/main/java/com/facefusion/PhotoSwap.kt +125 -0
- package/android/src/main/java/com/facefusion/PipeGuard.kt +74 -0
- package/android/src/main/java/com/facefusion/PreviewSurfaceHolder.kt +104 -0
- package/android/src/main/java/com/facefusion/SourceFaces.kt +125 -0
- package/android/src/main/java/com/facefusion/SwapConfig.kt +31 -0
- package/android/src/main/java/com/facefusion/TargetFaces.kt +88 -0
- package/android/src/main/java/com/facefusion/VideoSwap.kt +501 -0
- package/android/src/main/java/com/facefusion/VideoSwapService.kt +85 -0
- package/android/src/main/java/com/facefusion/mobile/NativePipe.kt +192 -0
- package/lib/module/FacefusionPreview.js +6 -0
- package/lib/module/FacefusionPreview.js.map +1 -0
- package/lib/module/FacefusionPreview.native.js +15 -0
- package/lib/module/FacefusionPreview.native.js.map +1 -0
- package/lib/module/NativeFacefusion.js +53 -0
- package/lib/module/NativeFacefusion.js.map +1 -0
- package/lib/module/detectSourceFaces.js +6 -0
- package/lib/module/detectSourceFaces.js.map +1 -0
- package/lib/module/detectSourceFaces.native.js +15 -0
- package/lib/module/detectSourceFaces.native.js.map +1 -0
- package/lib/module/detectTargetFaces.js +6 -0
- package/lib/module/detectTargetFaces.js.map +1 -0
- package/lib/module/detectTargetFaces.native.js +13 -0
- package/lib/module/detectTargetFaces.native.js.map +1 -0
- package/lib/module/index.js +11 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/models.js +16 -0
- package/lib/module/models.js.map +1 -0
- package/lib/module/models.native.js +43 -0
- package/lib/module/models.native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/probeDevice.js +6 -0
- package/lib/module/probeDevice.js.map +1 -0
- package/lib/module/probeDevice.native.js +14 -0
- package/lib/module/probeDevice.native.js.map +1 -0
- package/lib/module/saveToGallery.js +6 -0
- package/lib/module/saveToGallery.js.map +1 -0
- package/lib/module/saveToGallery.native.js +15 -0
- package/lib/module/saveToGallery.native.js.map +1 -0
- package/lib/module/swapPhoto.js +6 -0
- package/lib/module/swapPhoto.js.map +1 -0
- package/lib/module/swapPhoto.native.js +16 -0
- package/lib/module/swapPhoto.native.js.map +1 -0
- package/lib/module/swapVideo.js +12 -0
- package/lib/module/swapVideo.js.map +1 -0
- package/lib/module/swapVideo.native.js +29 -0
- package/lib/module/swapVideo.native.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/FacefusionPreview.d.ts +4 -0
- package/lib/typescript/src/FacefusionPreview.d.ts.map +1 -0
- package/lib/typescript/src/FacefusionPreview.native.d.ts +142 -0
- package/lib/typescript/src/FacefusionPreview.native.d.ts.map +1 -0
- package/lib/typescript/src/NativeFacefusion.d.ts +291 -0
- package/lib/typescript/src/NativeFacefusion.d.ts.map +1 -0
- package/lib/typescript/src/detectSourceFaces.d.ts +3 -0
- package/lib/typescript/src/detectSourceFaces.d.ts.map +1 -0
- package/lib/typescript/src/detectSourceFaces.native.d.ts +11 -0
- package/lib/typescript/src/detectSourceFaces.native.d.ts.map +1 -0
- package/lib/typescript/src/detectTargetFaces.d.ts +3 -0
- package/lib/typescript/src/detectTargetFaces.d.ts.map +1 -0
- package/lib/typescript/src/detectTargetFaces.native.d.ts +9 -0
- package/lib/typescript/src/detectTargetFaces.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/models.d.ts +7 -0
- package/lib/typescript/src/models.d.ts.map +1 -0
- package/lib/typescript/src/models.native.d.ts +31 -0
- package/lib/typescript/src/models.native.d.ts.map +1 -0
- package/lib/typescript/src/probeDevice.d.ts +3 -0
- package/lib/typescript/src/probeDevice.d.ts.map +1 -0
- package/lib/typescript/src/probeDevice.native.d.ts +10 -0
- package/lib/typescript/src/probeDevice.native.d.ts.map +1 -0
- package/lib/typescript/src/saveToGallery.d.ts +2 -0
- package/lib/typescript/src/saveToGallery.d.ts.map +1 -0
- package/lib/typescript/src/saveToGallery.native.d.ts +9 -0
- package/lib/typescript/src/saveToGallery.native.d.ts.map +1 -0
- package/lib/typescript/src/swapPhoto.d.ts +3 -0
- package/lib/typescript/src/swapPhoto.d.ts.map +1 -0
- package/lib/typescript/src/swapPhoto.native.d.ts +12 -0
- package/lib/typescript/src/swapPhoto.native.d.ts.map +1 -0
- package/lib/typescript/src/swapVideo.d.ts +6 -0
- package/lib/typescript/src/swapVideo.d.ts.map +1 -0
- package/lib/typescript/src/swapVideo.native.d.ts +20 -0
- package/lib/typescript/src/swapVideo.native.d.ts.map +1 -0
- package/package.json +132 -0
- package/patches/ffjni-analyse-faces.patch +63 -0
- package/scripts/check-tarball.sh +68 -0
- package/scripts/fetch-upstream.sh +91 -0
- package/src/FacefusionPreview.native.tsx +18 -0
- package/src/FacefusionPreview.tsx +9 -0
- package/src/NativeFacefusion.ts +324 -0
- package/src/detectSourceFaces.native.tsx +17 -0
- package/src/detectSourceFaces.tsx +10 -0
- package/src/detectTargetFaces.native.tsx +15 -0
- package/src/detectTargetFaces.tsx +10 -0
- package/src/index.tsx +25 -0
- package/src/models.native.tsx +45 -0
- package/src/models.tsx +23 -0
- package/src/probeDevice.native.tsx +13 -0
- package/src/probeDevice.tsx +7 -0
- package/src/saveToGallery.native.tsx +16 -0
- package/src/saveToGallery.tsx +9 -0
- package/src/swapPhoto.native.tsx +20 -0
- package/src/swapPhoto.tsx +12 -0
- package/src/swapVideo.native.tsx +40 -0
- package/src/swapVideo.tsx +31 -0
- package/third_party/facefusion-mobile/NOTICE +49 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.facefusion
|
|
2
|
+
|
|
3
|
+
import com.facefusion.mobile.NativePipe
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Upstream's NSFW content gate (ADR-0003), run on the public API path where a JS caller
|
|
7
|
+
* cannot skip it -- no `skipCheck` option exists anywhere above [PhotoSwap]/[VideoSwap].
|
|
8
|
+
*
|
|
9
|
+
* **This is the one file a personal, gate-free build deletes.** `android/build.gradle`
|
|
10
|
+
* generates a no-op stand-in with this class's public surface when the file is absent, so
|
|
11
|
+
* deleting it is the only change needed -- [PhotoSwap]/[VideoSwap] call [ContentGate]
|
|
12
|
+
* exactly the same way either build compiles it. That build renames itself `-dev` with a
|
|
13
|
+
* distinct `applicationId` (see `example/android/app/build.gradle`), upstream's own switch,
|
|
14
|
+
* not a bypass flag.
|
|
15
|
+
*
|
|
16
|
+
* Gates on `nsfw_2` alone, not upstream's 2-of-3 vote across three models -- 461 MB was not
|
|
17
|
+
* worth it against a 266 MB app (`docs/roadmap.md` 2) -- and the quantised build every tier
|
|
18
|
+
* but v79 uses sits ~0.087 mean closer to flagging. Both limits belong in the README, not
|
|
19
|
+
* hidden, per ADR-0003.
|
|
20
|
+
*/
|
|
21
|
+
object ContentGate {
|
|
22
|
+
|
|
23
|
+
/** Refused. [isQuantised] lets a caller mention the known quantisation bias in its own
|
|
24
|
+
* message without this class importing any UI concern. */
|
|
25
|
+
class Refused(message: String, val isQuantised: Boolean) : Exception(message)
|
|
26
|
+
|
|
27
|
+
// content_analyser.py:detect_with_nsfw_2 -- logit[0] - logit[1] > 0.25 flags a frame.
|
|
28
|
+
private const val THRESHOLD = 0.25f
|
|
29
|
+
|
|
30
|
+
// Refuse a video only on the AGGREGATE rate across its sampled frames, not on the first
|
|
31
|
+
// flagged one -- a single false positive should not sink an otherwise-clean clip.
|
|
32
|
+
private const val VIDEO_FLAG_RATE = 0.10
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One frame -- a still photo. Call with the TARGET frame, the content actually being
|
|
36
|
+
* processed, before [NativePipe.processFrame] runs: refusing up front skips the swap
|
|
37
|
+
* entirely instead of doing the work and then discarding it.
|
|
38
|
+
*/
|
|
39
|
+
fun checkFrame(bgr: ByteArray, w: Int, h: Int) {
|
|
40
|
+
val quantised = NativePipe.contentGateIsQuantised()
|
|
41
|
+
val score = NativePipe.contentScore(bgr, w, h)
|
|
42
|
+
// A failure to measure is treated as an error, never as "allow" (docs/02-upstream.md).
|
|
43
|
+
if (score.isNaN()) {
|
|
44
|
+
throw Refused("Content check failed: ${NativePipe.lastError()}", quantised)
|
|
45
|
+
}
|
|
46
|
+
if (score > THRESHOLD) {
|
|
47
|
+
throw Refused("Content check flagged this image (score %.2f)".format(score), quantised)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Accumulates one sampled frame per second of a video target. [VideoSwap] already decodes
|
|
53
|
+
* every frame for the swap itself and buffers every encoded frame until the whole clip is
|
|
54
|
+
* done before muxing -- so [sample] rides that same loop for free, and [finish] applies the
|
|
55
|
+
* rate rule before the muxer (and thus [outputPath]) is ever touched, exactly like refusing
|
|
56
|
+
* a still up front.
|
|
57
|
+
*/
|
|
58
|
+
class VideoSampler {
|
|
59
|
+
private var sampled = 0
|
|
60
|
+
private var flagged = 0
|
|
61
|
+
private var quantised = false
|
|
62
|
+
|
|
63
|
+
fun sample(bgr: ByteArray, w: Int, h: Int) {
|
|
64
|
+
quantised = NativePipe.contentGateIsQuantised()
|
|
65
|
+
val score = NativePipe.contentScore(bgr, w, h)
|
|
66
|
+
if (score.isNaN()) {
|
|
67
|
+
throw Refused("Content check failed: ${NativePipe.lastError()}", quantised)
|
|
68
|
+
}
|
|
69
|
+
sampled++
|
|
70
|
+
if (score > THRESHOLD) flagged++
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Call once, after every frame has been offered to [sample]. A clip too short to have
|
|
74
|
+
* sampled anything is not refused -- there is nothing measured to refuse it on. */
|
|
75
|
+
fun finish() {
|
|
76
|
+
if (sampled == 0) return
|
|
77
|
+
val rate = flagged.toDouble() / sampled
|
|
78
|
+
if (rate > VIDEO_FLAG_RATE) {
|
|
79
|
+
throw Refused(
|
|
80
|
+
"Content check flagged ${(rate * 100).toInt()}% of sampled frames (limit 10%)",
|
|
81
|
+
quantised,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
package com.facefusion
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.facefusion.mobile.NativePipe
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What this phone's Hexagon NPU says about itself.
|
|
8
|
+
*
|
|
9
|
+
* [ok] is the only field that decides whether the rest mean anything. `false` means the
|
|
10
|
+
* probe failed — no QNN runtime, no Hexagon DSP, an emulator — and **not** that the chip
|
|
11
|
+
* is old or unsupported. [tier] and [tierChain] are still filled in either way, because
|
|
12
|
+
* the native layer's fallback (`v68`, the floor that runs on every HTP) is a usable
|
|
13
|
+
* answer even when nothing could be measured.
|
|
14
|
+
*/
|
|
15
|
+
data class DeviceProbe(
|
|
16
|
+
val ok: Boolean,
|
|
17
|
+
val tier: String,
|
|
18
|
+
val tierChain: List<String>,
|
|
19
|
+
val arch: Int,
|
|
20
|
+
val vtcmMb: Int,
|
|
21
|
+
val socModel: Int,
|
|
22
|
+
val signedPd: Boolean,
|
|
23
|
+
val dlbc: Boolean,
|
|
24
|
+
val error: String,
|
|
25
|
+
) {
|
|
26
|
+
companion object {
|
|
27
|
+
/** The tier every unmeasured device falls back to; mirrors `tierChain()` in ffqnn.cpp. */
|
|
28
|
+
private const val FALLBACK_TIER = "v68"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cached because the chain is a property of the *silicon* and cannot change while the
|
|
32
|
+
* process lives, and because measuring it brings the QNN backend up — far too expensive
|
|
33
|
+
* to repeat every time a screen wants to know which models to download.
|
|
34
|
+
*
|
|
35
|
+
* Only a real answer is cached. A probe that failed must not be remembered as "this chip
|
|
36
|
+
* can load nothing" for the rest of the process, and the fallback is not a measurement.
|
|
37
|
+
*
|
|
38
|
+
* The chain is cached; the tier [ModelPaths.tier] resolves against disk deliberately is
|
|
39
|
+
* not. What a device *can* load is fixed; what is *on disk* changes the moment a
|
|
40
|
+
* download finishes.
|
|
41
|
+
*/
|
|
42
|
+
@Volatile
|
|
43
|
+
private var chainCache: List<String>? = null
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every tier this chip can load, best first — `["v81", "v73", "v68"]`.
|
|
47
|
+
*
|
|
48
|
+
* **Not** "this tier and every older one": the native `tierChain()` skips arches the
|
|
49
|
+
* chip cannot run, so it must never be reconstructed from a single tier.
|
|
50
|
+
*
|
|
51
|
+
* Falls back to a one-entry `v68` chain when nothing could be measured, which is a
|
|
52
|
+
* usable answer rather than an error — see the class docs.
|
|
53
|
+
*/
|
|
54
|
+
fun tierChain(context: Context): List<String> {
|
|
55
|
+
chainCache?.let { return it }
|
|
56
|
+
if (NativePipe.loadError != null) return listOf(FALLBACK_TIER)
|
|
57
|
+
|
|
58
|
+
val libDir = context.applicationInfo.nativeLibraryDir
|
|
59
|
+
val chain = NativePipe.probeTierChain(libDir, libDir)
|
|
60
|
+
.split(',')
|
|
61
|
+
.map { it.trim() }
|
|
62
|
+
.filter { it.isNotEmpty() }
|
|
63
|
+
|
|
64
|
+
if (chain.isEmpty()) return listOf(FALLBACK_TIER)
|
|
65
|
+
chainCache = chain
|
|
66
|
+
return chain
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs the probe. **Blocking** — it `dlopen`s the QNN backend and creates a device
|
|
71
|
+
* handle, which is tens of milliseconds on real hardware and a failed `dlopen`
|
|
72
|
+
* anywhere else. Call it off the main thread.
|
|
73
|
+
*/
|
|
74
|
+
fun probe(context: Context): DeviceProbe {
|
|
75
|
+
NativePipe.loadError?.let { return failed("libffnative.so did not load: $it") }
|
|
76
|
+
|
|
77
|
+
val chain = tierChain(context)
|
|
78
|
+
|
|
79
|
+
// Both directories are the same one. The QAIRT runtime ships as ordinary .so files
|
|
80
|
+
// in jniLibs, so the installer extracts backend, stub and skel side by side into
|
|
81
|
+
// the app's nativeLibraryDir -- and that extraction is exactly what
|
|
82
|
+
// `useLegacyPackaging = true` guarantees, because the backend is opened by path.
|
|
83
|
+
val libDir = context.applicationInfo.nativeLibraryDir
|
|
84
|
+
val info = parse(NativePipe.probeDeviceInfo(libDir, libDir))
|
|
85
|
+
val ok = info["ok"] == "1"
|
|
86
|
+
|
|
87
|
+
return DeviceProbe(
|
|
88
|
+
ok = ok,
|
|
89
|
+
tier = chain.first(),
|
|
90
|
+
tierChain = chain,
|
|
91
|
+
arch = info["arch"]?.toIntOrNull() ?: 0,
|
|
92
|
+
vtcmMb = info["vtcm"]?.toIntOrNull() ?: 0,
|
|
93
|
+
socModel = info["soc"]?.toIntOrNull() ?: 0,
|
|
94
|
+
signedPd = info["signedPd"] == "1",
|
|
95
|
+
dlbc = info["dlbc"] == "1",
|
|
96
|
+
// lastError() is whatever failed most recently, so it is only meaningful when
|
|
97
|
+
// something did. Reading it on the success path would report a stale message.
|
|
98
|
+
error = if (ok) "" else explain(NativePipe.lastError(), libDir),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Turns the native layer's `deviceCreate failed -- the DSP is not reachable from this
|
|
104
|
+
* process` into something a person can act on.
|
|
105
|
+
*
|
|
106
|
+
* That message is accurate and useless: the overwhelmingly common cause is not a broken
|
|
107
|
+
* phone but a **missing Hexagon skel for this chip's architecture**. `libQnnHtp.so`
|
|
108
|
+
* loads `libQnnHtpV<arch>Skel.so` chosen from the *silicon*, not from the model tier --
|
|
109
|
+
* so a build that bundles v73/v79/v81 cannot reach the DSP on an 8 Gen 1 (v69) no
|
|
110
|
+
* matter which models are on disk. Reported 2026-09-12 by the first person outside
|
|
111
|
+
* Saad's own 8 Elite to run this.
|
|
112
|
+
*
|
|
113
|
+
* The arch cannot be named here: it is measured by the very call that just failed, so
|
|
114
|
+
* on this path it reads 0. What *can* be named is what this build actually ships, which
|
|
115
|
+
* is enough for anyone to see the mismatch.
|
|
116
|
+
*/
|
|
117
|
+
private fun explain(nativeError: String, libDir: String): String {
|
|
118
|
+
if (!nativeError.contains("deviceCreate")) return nativeError
|
|
119
|
+
val bundled = java.io.File(libDir)
|
|
120
|
+
.list { _, name -> SKEL.matches(name) }
|
|
121
|
+
?.map { SKEL.find(it)!!.groupValues[1] }
|
|
122
|
+
?.sortedBy { it.toIntOrNull() ?: 0 }
|
|
123
|
+
.orEmpty()
|
|
124
|
+
return when {
|
|
125
|
+
bundled.isEmpty() ->
|
|
126
|
+
"$nativeError. No Hexagon runtime is bundled in this build at all -- see the " +
|
|
127
|
+
"QAIRT SDK step in the README."
|
|
128
|
+
else ->
|
|
129
|
+
"$nativeError. This build bundles the Hexagon runtime for " +
|
|
130
|
+
bundled.joinToString(", ") { "v$it" } +
|
|
131
|
+
" only; this chip needs a different one, so no swap can run here."
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private val SKEL = Regex("""libQnnHtpV(\d+)Skel\.so""")
|
|
136
|
+
|
|
137
|
+
private fun failed(reason: String) = DeviceProbe(
|
|
138
|
+
ok = false,
|
|
139
|
+
tier = FALLBACK_TIER,
|
|
140
|
+
tierChain = listOf(FALLBACK_TIER),
|
|
141
|
+
arch = 0,
|
|
142
|
+
vtcmMb = 0,
|
|
143
|
+
socModel = 0,
|
|
144
|
+
signedPd = false,
|
|
145
|
+
dlbc = false,
|
|
146
|
+
error = reason,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
/** `ok=1;arch=79;vtcm=8;…` -> a map. Pairs without an `=` are dropped. */
|
|
150
|
+
private fun parse(info: String): Map<String, String> =
|
|
151
|
+
info.split(';')
|
|
152
|
+
.mapNotNull { field ->
|
|
153
|
+
val i = field.indexOf('=')
|
|
154
|
+
if (i <= 0) null else field.substring(0, i) to field.substring(i + 1)
|
|
155
|
+
}
|
|
156
|
+
.toMap()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.facefusion
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Crops a packed BGR frame to one face's box, and pastes a same-sized crop back — how
|
|
5
|
+
* [PhotoSwap]/[VideoSwap] swap only a chosen *target* face without a new native entry
|
|
6
|
+
* point: [com.facefusion.mobile.NativePipe.processFrame] already swaps every face it's
|
|
7
|
+
* handed, so handing it a tight crop around one face swaps only that one. The same trick
|
|
8
|
+
* [SourceFaces] already uses on the source side for `setSource` — this is the BGR-buffer
|
|
9
|
+
* version of it, since [PhotoSwap]/[VideoSwap] both work in packed BGR, not `Bitmap`s, by
|
|
10
|
+
* the time a target face box would apply.
|
|
11
|
+
*/
|
|
12
|
+
object FaceCrop {
|
|
13
|
+
|
|
14
|
+
/** Same margin [SourceFaces] uses on the source side, for the same reason: the
|
|
15
|
+
* detector/landmarker inside `processFrame` need a bit of surrounding context, not a
|
|
16
|
+
* box drawn exactly to the model's own edges. */
|
|
17
|
+
const val MARGIN = 0.4f
|
|
18
|
+
|
|
19
|
+
/** [box] (`[left, top, right, bottom]`, as returned by [TargetFaces.detect]) expanded by
|
|
20
|
+
* [margin] and clamped to `0..w`/`0..h`, as pixel ints. */
|
|
21
|
+
fun rect(box: FloatArray, w: Int, h: Int, margin: Float = MARGIN): IntArray {
|
|
22
|
+
require(box.size == 4) { "box must be [left, top, right, bottom]" }
|
|
23
|
+
val (boxLeft, boxTop, boxRight, boxBottom) = box
|
|
24
|
+
val bw = boxRight - boxLeft
|
|
25
|
+
val bh = boxBottom - boxTop
|
|
26
|
+
val left = (boxLeft - bw * margin).toInt().coerceIn(0, w - 1)
|
|
27
|
+
val top = (boxTop - bh * margin).toInt().coerceIn(0, h - 1)
|
|
28
|
+
val right = (boxRight + bw * margin).toInt().coerceIn(left + 1, w)
|
|
29
|
+
val bottom = (boxBottom + bh * margin).toInt().coerceIn(top + 1, h)
|
|
30
|
+
return intArrayOf(left, top, right, bottom)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Extracts `rect` from a packed BGR buffer (3 bytes/pixel, no row padding) of width [w]. */
|
|
34
|
+
fun crop(bgr: ByteArray, w: Int, rect: IntArray): ByteArray {
|
|
35
|
+
val left = rect[0]
|
|
36
|
+
val top = rect[1]
|
|
37
|
+
val cw = rect[2] - left
|
|
38
|
+
val ch = rect[3] - top
|
|
39
|
+
val out = ByteArray(cw * ch * 3)
|
|
40
|
+
for (row in 0 until ch) {
|
|
41
|
+
System.arraycopy(bgr, ((top + row) * w + left) * 3, out, row * cw * 3, cw * 3)
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Writes a buffer from [crop] back into [bgr] (width [w]) at [rect]. The inverse of
|
|
47
|
+
* [crop] — call with the same `rect` used to produce it. */
|
|
48
|
+
fun paste(bgr: ByteArray, w: Int, cropped: ByteArray, rect: IntArray) {
|
|
49
|
+
val left = rect[0]
|
|
50
|
+
val top = rect[1]
|
|
51
|
+
val cw = rect[2] - left
|
|
52
|
+
val ch = rect[3] - top
|
|
53
|
+
for (row in 0 until ch) {
|
|
54
|
+
System.arraycopy(cropped, row * cw * 3, bgr, ((top + row) * w + left) * 3, cw * 3)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|